If you cannot or do not want to use the leaflet plugin, the following javascript code will draw blue MGRS grid lines on a HTML canvas context which it is assumed already contains a map rendered using slippy map tiles, for example OpenStreetMap. These tiles come in different zoom sets from zoom 0 displaying the whole world in one tile, through to zoom 19 displaying the whole world in 2**19 tiles. SlippyMapTiles
The code will display one of the following grid sizes depending on the current zoom level : 100m, 1km, 10km, 100km or the MGRS zone. Grid lines are annotated at the top, bottom, left and right of the viewable area using 12pt san-serif font. The colour and the font size can easily be changed, see definitions at the top of the code block. The grid extends to double the viewing area in all directions to allow for user panning, this can also be easily changed.
The code in my answer makes the following assumptions.
A javascript function must be called to draw the MGRS grid and requires the four following parameters passed to it:
- Map, an existing HTML canvas object.
- Context, a canvas context that has already been created for Map.
- CentreX, half the width of the canvas in pixels.
- CentreY, half the height of the canvas in pixels.
In addition the following assumptions are also required, most of which (in my case) were required to render the map tiles in the first place and hence simplify the code presented below to draw the grid lines.
A. The lat,long centre of the currently viewing area for which the grid lines are required has been set in Map.CentreLat and Map.CentreLong.
B. If the canvas has already been scaled for display, this value must have been set in Map.ScaleFactor, if it has not scaled then Map.ScaleFactor must have been set to unity.
C. The full MGRS location string, e.g 30U WD 72583 66413 must have been pre-calculated and stored in Map.CentreMGRS using the function MGRSString, included in the code. This function is not invoked unless the map is panned, except at certain zoom levels where it can be invoked a maximum of two further times (per screen rendering). The function MGRSString is included in the utilities section below.
D. Map.Power has already been set for efficiency to Math.pow (2,zoom); where zoom is the current map zoom level (see drawing maps using slippy tiles)
E. Map.LongitudePerTile has already been set for efficiency to 360 / MapObj.Power;
F. Map.YOrigin has already been set to MercatorY (MapObj.CentreLat, Map.Power). MercatorY is defined in the code.
The primary function PlanMGRSGrid is attached below. This function takes the parameters listed 1-4 above and makes use of the data described in A-F above to decide what grid lines to render.
The decision is based on the value of the number of pixels per kilometre, which is directly related to the zoom level but is more useful when rendering.
As the number of pixels per kilometer increases, first the 100Km grid squares are rendered, then 10Km, then 1km and finally 100m. The code could relatively easily be extended to draw 10m but a need for this would be rare. See also special case below for zoom levels 0-5
The most tricky aspect of rendering MGRS tiles is coping with zone boundaries (the second most tricky aspect is that the orientation of the grid lines changes across each zone).
In order to cope with this, PlanMGRSGrid determines what zones are involved in the rendering and invokes a function DrawMGRSGridLines for each zone involved.
The zone in which the centre of the map is displayed first and then either the zone to the west of this (if one is needed) and/or the zone to the east of this.
For the zoom levels (0-5), this answer only draws the MGRS zones. These are presented as lines of longitude every six degrees and lines of latitude every 8 degrees and annotates them with the MGRS grid zone numbers e.g 30U. To do this a separate function DrawMGRSGridZones is invoked.
Code for PlanMGRSGrid is shown below. You may not like my code formatting (I started coding with Algol 60) but you can pretty print it to suit your coding style.
function PlanMGRSGrid (Map,Context,CentreX,CentreY)
{
if (Map.CentreMGRS.length < 17) return ; // off grid too far north or south
Context.strokeStyle = Context.fillStyle = "#0000ff" ;
Context.font = "12 px sans-serif" ;
var lng = MGRSZoneOrigin (Map.CentreLong);
var eastDiffLong = lng + 3 - Map.CentreLong ;
var westDiffLong = lng - 3 - Map.CentreLong ;
var easternLimit = eastDiffLong * Map.ScaleFactor * 256 / Map.LongitudePerTile ; // longitude change to end of zone (+ve)
var westernLimit = westDiffLong * Map.ScaleFactor * 256 / Map.LongitudePerTile ; // longitude change to end of zone (-ve)
var easternLimit2 = easternLimit + 6 * Map.ScaleFactor * 256 / Map.LongitudePerTile ; // longitude change to end of next zone
var westernLimit2 = westernLimit - 6 * Map.ScaleFactor * 256 / Map.LongitudePerTile ; // longitude change to end of next zone
switch (Map.CentreMGRS.length)
{
case 17 : var easting = Map.CentreMGRS.substr (6,5); var northing = Map.CentreMGRS.substr (12,5); var eastingLetter = Map.CentreMGRS.substr (3,1); var northingLetter = Map.CentreMGRS.substr (4,1); break ;
case 18 : easting = Map.CentreMGRS.substr (7,5); northing = Map.CentreMGRS.substr (13,5); eastingLetter = Map.CentreMGRS.substr (4,1); northingLetter = Map.CentreMGRS.substr (5,1); break ;
};
var ppkm = UnscaledPixelsPerKM (Map);
var offMapFactor = 2 ; // 2 times the distance from centre to an edge
var gridTop = - offMapFactor * CentreY / Map.ScaleFactor ;
var gridBottom = offMapFactor * CentreY / Map.ScaleFactor ;
var gridLeft = (( - CentreX * offMapFactor > westernLimit)? - CentreX * offMapFactor : westernLimit) / Map.ScaleFactor ;
var gridRight = (( CentreX * offMapFactor < easternLimit)? CentreX * offMapFactor : easternLimit) / Map.ScaleFactor ;
var eastingsLabelTop = (- CentreY + 12) / Map.ScaleFactor ;
var eastingsLabelBottom = ( CentreY - 5) / Map.ScaleFactor ;
var northingsLabelLeft = (- CentreX > westernLimit)? (- CentreX + 5 ) / Map.ScaleFactor : undefined ;
var northingsLabelRight = ( CentreX < easternLimit)? ( CentreX - 20) / Map.ScaleFactor : undefined ;
if (ppkm < 0.5) // draw lat long zones only
{
DrawMGRSGridZones (Map, Context, Map.CentreLat, Map.CentreLong, 256 / Map.LongitudePerTile, gridTop, (- CentreX * offMapFactor) / Map.ScaleFactor, gridBottom, (CentreX * offMapFactor) / Map.ScaleFactor, eastingsLabelTop, eastingsLabelBottom, (- CentreX + 5) / Map.ScaleFactor, (CentreX - 20) / Map.ScaleFactor); // note gridLeft, gridRight, northingsLabelLeft and northingsLabelRight cannot be used in this case
return ;
};
DrawMGRSGridLines (Map, Context, ppkm, eastingLetter, easting, northingLetter, northing, gridTop, gridLeft, gridBottom, gridRight, Map.MGRSConvergence, Map.MGRSConvRate, eastingsLabelTop, eastingsLabelBottom, northingsLabelLeft, northingsLabelRight, 0);
if (westernLimit > - CentreX) // display Additional Grid to West, but only if it will be visible with no panning
{
// set westing limits for additional grid
gridRight = westernLimit / Map.ScaleFactor ;
gridLeft = ((-CentreX * offMapFactor > westernLimit2)? - CentreX * offMapFactor : westernLimit2) / Map.ScaleFactor ;
// set new northing label locations
northingsLabelLeft = (- CentreX > westernLimit2)? (- CentreX + 5 ) / Map.ScaleFactor : undefined ;
northingsLabelRight = undefined ;
// choose new origin at the left edge of the screen and compute MGRS grid at it
var newLongPos = - CentreX ;
var newLong = Map.CentreLong + newLongPos * Map.LongitudePerTile / 256 ;
if (newLong < Map.CentreLong + westDiffLong - 6)
{
// special case - new origin is further west than the western edge of next zone - so choose another origin half way to left edge of screen
newLongPos = - CentreX / 2 ;
newLong = Map.CentreLong + newLongPos * Map.LongitudePerTile / 256 ;
};
var newMGRS = MGRSString (Map.CentreLat, newLong);
// set new easting and new northing to this new origin
switch (newMGRS.length)
{
case 17 : easting = newMGRS.substr (6,5); northing = newMGRS.substr (12,5); eastingLetter = newMGRS.substr (3,1); northingLetter = newMGRS.substr (4,1); break ;
case 18 : easting = newMGRS.substr (7,5); northing = newMGRS.substr (13,5); eastingLetter = newMGRS.substr (4,1); northingLetter = newMGRS.substr (5,1); break ;
};
// call DrawMGRSGridLines with a pixel offset based on the distance from the old origin to the new origin
//console.log ("Next Zone Westwards ", ppkm, eastingLetter, easting, northingLetter, northing, gridTop, gridLeft, gridBottom, gridRight, MGRSConvergence (Map.CentreLat, newLong), Map.MGRSConvRate, eastingsLabelTop, eastingsLabelBottom, northingsLabelLeft, northingsLabelRight, newLongPos);
DrawMGRSGridLines (Map, Context, ppkm, eastingLetter, easting, northingLetter, northing, gridTop, gridLeft, gridBottom, gridRight, MGRSConvergence (Map.CentreLat, newLong), Map.MGRSConvRate, eastingsLabelTop, eastingsLabelBottom, northingsLabelLeft, northingsLabelRight, newLongPos);
};
if (easternLimit < CentreX) // display Additional Grid to East, but only if it will be visible with no panning
{
// set easting limits for additional grid
gridLeft = easternLimit / Map.ScaleFactor ;
gridRight = ((CentreX * offMapFactor < easternLimit2)? CentreX * offMapFactor : easternLimit2) / Map.ScaleFactor ;
// set new northing label locations
northingsLabelLeft = undefined ;
northingsLabelRight = ( CentreX < easternLimit2)? ( CentreX - 20) / Map.ScaleFactor : undefined ;
// choose new origin at the right edge of the visible screen and compute MGRS grid at it
newLongPos = CentreX ;
newLong = Map.CentreLong + newLongPos * Map.LongitudePerTile / 256 ;
if (newLong > Map.CentreLong + eastDiffLong + 6)
{
// special case - new origin is further east than the eastern edge of next zone - so choose another origin half way to right edge of screen
newLongPos = CentreX / 2 ;
newLong = Map.CentreLong + newLongPos * Map.LongitudePerTile / 256 ;
};
newMGRS = MGRSString (Map.CentreLat, newLong);
// set new easting and new northing to this new origin
switch (newMGRS.length)
{
case 17 : easting = newMGRS.substr (6,5); northing = newMGRS.substr (12,5); eastingLetter = newMGRS.substr (3,1); northingLetter = newMGRS.substr (4,1); break ;
case 18 : easting = newMGRS.substr (7,5); northing = newMGRS.substr (13,5); eastingLetter = newMGRS.substr (4,1); northingLetter = newMGRS.substr (5,1); break ;
};
// draw additional grid with a pixel offset based on the distance from the old origin to the new origin
//console.log ("Next Zone Eastwards ", ppkm, eastingLetter, easting, northingLetter, northing, gridTop, gridLeft, gridBottom, gridRight, MGRSConvergence (Map.CentreLat, newLong), Map.MGRSConvRate, eastingsLabelTop, eastingsLabelBottom, northingsLabelLeft, northingsLabelRight, newLongPos);
DrawMGRSGridLines (Map, Context, ppkm, eastingLetter, easting, northingLetter, northing, gridTop, gridLeft, gridBottom, gridRight, MGRSConvergence (Map.CentreLat, newLong), Map.MGRSConvRate, eastingsLabelTop, eastingsLabelBottom, northingsLabelLeft, northingsLabelRight, newLongPos);
};
};// end of PlanMGRSGrid function
The code for the special case DrawMGRSGridZones is shown below
function DrawMGRSGridZones (Map,Context,Lat,Long,PPDegree,GridTop,GridLeft,GridBottom,GridRight,EastingsLabelTop,EastingsLabelBottom,NorthingsLabelLeft,NorthingsLabelRight)
{
console.log (GridTop,GridLeft,GridBottom,GridRight);
var eastLabelOffset = 3 / Map.ScaleFactor ;
var northLabelOffset = 5 / Map.ScaleFactor ;
var east , eastOrig , eastAlign , eastZone ;
var north, northOrig, northAlign, northZone ;
var gridX, gridY ;
//----------------
function yPos (lat)
{
return 256 * (MercatorY (lat, Map.Power) - Map.YOrigin);
};// end of local function
//----------------
function xPos (lng)
{
return 256 * (lng - Map.CentreLong) / Map.LongitudePerTile ;
};// end of local function
//----------------
// first plot lines of Longitude every six degrees from approximately near the centre across to the right of the area and then those from just left of the same place across to the left of the area
eastOrig = east = 6 * Math.floor (Long/6) ; // round to the nearest 6 degrees
eastAlign = Long - east ;
gridX = xPos (east);
while (gridX < GridRight) {plot6DegreesLongitude (east); east += 6 ; gridX = xPos (east)}; // allow east > 180
east = eastOrig - 6 ; // allow east < -180
gridX = xPos (east);
while (gridX > GridLeft ) {plot6DegreesLongitude (east); east -= 6 ; gridX = xPos (east)}; // allow east < -180
// then plot lines of Latitude every eight degrees from approximately near the centre up to the top of the area and then those from just below the same place down to the bottom of the area
//console.log ("Orig Lat " + Lat);
northOrig = north = 8 * Math.floor (Lat/8) ; // round to the nearest 8 degrees
//console.log ("Rounded Lat " + north);
northAlign = Lat - north ;
gridY = yPos (north);
while (gridY > GridTop ) {plot8DegreesLatitude (north); north += 8 ; gridY = yPos (north)};
north = northOrig - 8 ;
gridY = yPos (north);
while (gridY < GridBottom) {plot8DegreesLatitude (north); north -= 8 ; gridY = yPos (north)};
//----------------
function plot6DegreesLongitude (lng)
{
if (lng > 180) lng = lng - 360 ; // earth wrap around E to W
if (lng <= -180) lng = lng + 360 ; // earth wrap around W to E
eastZone = 1 + Math.floor ((lng+180)/6); // positive number
if (eastZone > 60) eastZone -= 60 ;
Context.beginPath ();
Context.lineWidth = 0.5 ;
Context.moveTo (gridX , GridTop);
Context.lineTo (gridX , GridBottom);
Context.stroke ();
Context.fillText (eastZone, gridX + eastLabelOffset, EastingsLabelTop ); // label is to the east (right) of the line
Context.fillText (eastZone, gridX + eastLabelOffset, EastingsLabelBottom);
};// end of local function
//------------------------
function plot8DegreesLatitude (lat)
{
//console.log ("Plot Latitude " + lat);
northZone = 'CDEFGHJKLMNPQRSTUVWXX'.charAt (Math.floor (lat/8 + 10)); // X is a double height band, hence XX
Context.beginPath ();
Context.lineWidth = 0.5 ;
Context.moveTo (GridLeft , gridY);
Context.lineTo (GridRight, gridY);
Context.stroke ();
Context.fillText (northZone, NorthingsLabelLeft , gridY - northLabelOffset); // label is to the north of (above) the line
Context.fillText (northZone, NorthingsLabelRight, gridY - northLabelOffset);
};// end of local function
//------------------------
};// end of DrawMGRSGridZones function
The following utility functions are required
function MGRSZoneOrigin (Long) // -180 <= Long < 180
{
return 6 * Math.floor ((Long + 180)/6) - 177 ;
};//end of MGRSZoneOrigin
//--------------------------------------------------------------
function MGRSConvergence (Lat, Long) // difference in radians between True North and MGRS Grid North (zero on Equator, up to +/- 3 degrees at 80 degrees North/South)
{
var posInZone = Long - MGRSZoneOrigin (Long);
var tanPIZ = Math.tan (posInZone * Math.PI/180);
var sinLat = Math.sin (Lat * Math.PI/180);
return Math.atan (tanPIZ * sinLat);
};//end of MGRSConvergence
//--------------------------------------------------------------------------------------
function MGRSConvergenceRateOfChange (Lat) // rads per KM
{
var zoneConvDiff = 0.104815 * Math.sin (Lat * Math.PI/180); // 0.104815 = 2 * tan (3 degrees)
var zoneKM = 111.1657 * 6 * Math.cos (Lat * Math.PI/180);
return zoneConvDiff / zoneKM ;
};//end of MGRSConvergenceRateOfChange
//--------------------------------------------------------------------------------------
function MercatorY (Lat,Power) // returns the mercator Y value for a Latitude for a particular map defined by the number of total number of tiles 2,4,8,16,32,64,128... for map levels 1,2,3,4,5,6,7....
{
var pi = Math.PI;
var latRad = Lat * pi/180 ;
var tanLat = Math.tan (latRad);
var secLat = 1/Math.cos (latRad);
return Power * (1 - Math.log (tanLat + secLat) / pi)/2 ;
};// end of MercatorY function
//--------------------------------------------------------------------------------------
function PreviousMGRSEastingsLetter (Letter)
{
var letters = "ABCDEFGHJKLMNPQRSTUVWXYZ" ;
var pos = letters.indexOf (Letter);
if (pos == 0) pos = letters.length ; // should never be requested
return letters [pos-1];
};//end of PreviousMGRSEastingsLetter
//--------------------------------------------------------------------------------------
function NextMGRSEastingsLetter (Letter)
{
var letters = "ABCDEFGHJKLMNPQRSTUVWXYZ" ;
var pos = letters.indexOf (Letter);
if (pos == letters.length-1) pos = 0 ; // should never be requested
return letters [pos+1];
};//end of NextMGRSEastingsLetter
//--------------------------------------------------------------------------------------
function PreviousMGRSNorthingsLetter (Letter)
{
var letters = "ABCDEFGHJKLMNPQRSTUV";
var pos = letters.indexOf (Letter);
if (pos == 0) pos = letters.length ;
return letters [pos-1];
};//end of PreviousMGRSNorthingsLetter
//--------------------------------------------------------------------------------------
function NextMGRSNorthingsLetter (Letter)
{
var letters = "ABCDEFGHJKLMNPQRSTUV";
var pos = letters.indexOf (Letter);
if (pos == letters.length-1) pos = -1 ;
return letters [pos+1];
};//end of NextMGRSNorthingsLetter
//--------------------------------------------------------------------------------------
function MGRSString (Lat, Long)
{
if (Lat < -80) return 'Too far South' ; if (Lat > 84) return 'Too far North' ;
var c = 1 + Math.floor ((Long+180)/6);
var e = c*6 - 183 ;
var k = Lat*Math.PI/180;
var l = Long*Math.PI/180;
var m = e*Math.PI/180;
var n = Math.cos (k);
var o = 0.006739496819936062*Math.pow (n,2);
var p = 40680631590769/(6356752.314*Math.sqrt(1 + o));
var q = Math.tan (k);
var r = q*q;
var t = l - m;
var u = 1.0 - r + o;
var v = 5.0 - r + 9*o + 4.0*(o*o);
var w = 5.0 - 18.0*r + (r*r) + 14.0*o - 58.0*r*o;
var x = 61.0 - 58.0*r + (r*r) + 270.0*o - 330.0*r*o;
var y = 61.0 - 479.0*r + 179.0*(r*r) - (r*r*r);
var z = 1385.0 - 3111.0*r + 543.0*(r*r) - (r*r*r);
var aa = p*n*t + (p/6.0*Math.pow (n,3)*u*Math.pow (t,3)) + (p/120.0*Math.pow (n,5)*w*Math.pow (t,5)) + (p/5040.0*Math.pow (n,7)*y*Math.pow (t,7));
var ab = 6367449.14570093*(k - (0.00251882794504*Math.sin (2*k)) + (0.00000264354112*Math.sin (4*k)) - (0.00000000345262*Math.sin (6*k)) + (0.000000000004892*Math.sin (8*k))) + (q/2.0*p*Math.pow (n,2)*Math.pow (t,2)) + (q/24.0*p*Math.pow (n,4)*v*Math.pow (t,4)) + (q/720.0*p*Math.pow (n,6)*x*Math.pow (t,6)) + (q/40320.0*p*Math.pow (n,8)*z*Math.pow (t,8));
aa = aa*0.9996 + 500000.0;
ab = ab*0.9996; if (ab < 0.0) ab += 10000000.0;
var ad = 'CDEFGHJKLMNPQRSTUVWXX'.charAt (Math.floor (Lat/8 + 10));
var ae = Math.floor (aa/100000);
var af = ['ABCDEFGH','JKLMNPQR','STUVWXYZ'][(c-1)%3].charAt (ae-1);
var ag = Math.floor (ab/100000)%20;
var ah = ['ABCDEFGHJKLMNPQRSTUV','FGHJKLMNPQRSTUVABCDE'][(c-1)%2].charAt (ag);
function pad (val) {if (val < 10) {val = '0000' + val} else if (val < 100) {val = '000' + val} else if (val < 1000) {val = '00' + val} else if (val < 10000) {val = '0' + val};return val};
aa = Math.floor (aa%100000); aa = pad (aa);
ab = Math.floor (ab%100000); ab = pad (ab);
var mgrs = c + ad + ' ' + af + ah + ' ' + aa + ' ' + ab;
return mgrs ;
};//end of MGRSString function
The code for DrawMGRSGridLines is presented as a separate answer as it will not fit.