-
Notifications
You must be signed in to change notification settings - Fork 203
/
mgrs.js
305 lines (250 loc) · 13.4 KB
/
mgrs.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* MGRS / UTM Conversion Functions (c) Chris Veness 2014-2022 */
/* MIT Licence */
/* www.movable-type.co.uk/scripts/latlong-utm-mgrs.html */
/* www.movable-type.co.uk/scripts/geodesy-library.html#mgrs */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
import Utm, { LatLon as LatLonEllipsoidal, Dms } from './utm.js';
/**
* Military Grid Reference System (MGRS/NATO) grid references provides geocoordinate references
* covering the entire globe, based on UTM projections.
*
* MGRS references comprise a grid zone designator, a 100km square identification, and an easting
* and northing (in metres); e.g. ‘31U DQ 48251 11932’.
*
* Depending on requirements, some parts of the reference may be omitted (implied), and
* eastings/northings may be given to varying resolution.
*
* qv www.fgdc.gov/standards/projects/FGDC-standards-projects/usng/fgdc_std_011_2001_usng.pdf
*
* @module mgrs
*/
/*
* Latitude bands C..X 8° each, covering 80°S to 84°N
*/
const latBands = 'CDEFGHJKLMNPQRSTUVWXX'; // X is repeated for 80-84°N
/*
* 100km grid square column (‘e’) letters repeat every third zone
*/
const e100kLetters = [ 'ABCDEFGH', 'JKLMNPQR', 'STUVWXYZ' ];
/*
* 100km grid square row (‘n’) letters repeat every other zone
*/
const n100kLetters = [ 'ABCDEFGHJKLMNPQRSTUV', 'FGHJKLMNPQRSTUVABCDE' ];
/* Mgrs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* Military Grid Reference System (MGRS/NATO) grid references, with methods to parse references, and
* to convert to UTM coordinates.
*/
class Mgrs {
/**
* Creates an Mgrs grid reference object.
*
* @param {number} zone - 6° longitudinal zone (1..60 covering 180°W..180°E).
* @param {string} band - 8° latitudinal band (C..X covering 80°S..84°N).
* @param {string} e100k - First letter (E) of 100km grid square.
* @param {string} n100k - Second letter (N) of 100km grid square.
* @param {number} easting - Easting in metres within 100km grid square.
* @param {number} northing - Northing in metres within 100km grid square.
* @param {LatLon.datums} [datum=WGS84] - Datum UTM coordinate is based on.
* @throws {RangeError} Invalid MGRS grid reference.
*
* @example
* import Mgrs from '/js/geodesy/mgrs.js';
* const mgrsRef = new Mgrs(31, 'U', 'D', 'Q', 48251, 11932); // 31U DQ 48251 11932
*/
constructor(zone, band, e100k, n100k, easting, northing, datum=LatLonEllipsoidal.datums.WGS84) {
if (!(1<=zone && zone<=60)) throw new RangeError(`invalid MGRS zone ‘${zone}’`);
if (zone != parseInt(zone)) throw new RangeError(`invalid MGRS zone ‘${zone}’`);
const errors = []; // check & report all other possible errors rather than reporting one-by-one
if (band.length!=1 || latBands.indexOf(band) == -1) errors.push(`invalid MGRS band ‘${band}’`);
if (e100k.length!=1 || e100kLetters[(zone-1)%3].indexOf(e100k) == -1) errors.push(`invalid MGRS 100km grid square column ‘${e100k}’ for zone ${zone}`);
if (n100k.length!=1 || n100kLetters[0].indexOf(n100k) == -1) errors.push(`invalid MGRS 100km grid square row ‘${n100k}’`);
if (isNaN(Number(easting))) errors.push(`invalid MGRS easting ‘${easting}’`);
if (isNaN(Number(northing))) errors.push(`invalid MGRS northing ‘${northing}’`);
if (Number(easting) < 0 || Number(easting) > 99999) errors.push(`invalid MGRS easting ‘${easting}’`);
if (Number(northing) < 0 || Number(northing) > 99999) errors.push(`invalid MGRS northing ‘${northing}’`);
if (!datum || datum.ellipsoid==undefined) errors.push(`unrecognised datum ‘${datum}’`);
if (errors.length > 0) throw new RangeError(errors.join(', '));
this.zone = Number(zone);
this.band = band;
this.e100k = e100k;
this.n100k = n100k;
this.easting = Math.floor(easting);
this.northing = Math.floor(northing);
this.datum = datum;
}
/**
* Converts MGRS grid reference to UTM coordinate.
*
* Grid references refer to squares rather than points (with the size of the square indicated
* by the precision of the reference); this conversion will return the UTM coordinate of the SW
* corner of the grid reference square.
*
* @returns {Utm} UTM coordinate of SW corner of this MGRS grid reference.
*
* @example
* const mgrsRef = Mgrs.parse('31U DQ 48251 11932');
* const utmCoord = mgrsRef.toUtm(); // 31 N 448251 5411932
*/
toUtm() {
const hemisphere = this.band>='N' ? 'N' : 'S';
// get easting specified by e100k (note +1 because eastings start at 166e3 due to 500km false origin)
const col = e100kLetters[(this.zone-1)%3].indexOf(this.e100k) + 1;
const e100kNum = col * 100e3; // e100k in metres
// get northing specified by n100k
const row = n100kLetters[(this.zone-1)%2].indexOf(this.n100k);
const n100kNum = row * 100e3; // n100k in metres
// get latitude of (bottom of) band (10 bands above the equator, 8°latitude each)
const latBand = (latBands.indexOf(this.band)-10)*8;
// get southern-most northing of bottom of band, using floor() to extend to include entirety
// of bottom-most 100km square - note in northern hemisphere, centre of zone will be furthest
// south; in southern hemisphere extremity of zone will be furthest south, so use 3°E / 0°E
const lon = this.band >= 'N' ? 3 : 0;
const nBand = Math.floor(new LatLonEllipsoidal(latBand, lon).toUtm().northing/100e3)*100e3;
// 100km grid square row letters repeat every 2,000km north; add enough 2,000km blocks to
// get into required band
let n2M = 0; // northing of 2,000km block
while (n2M + n100kNum + this.northing < nBand) n2M += 2000e3;
return new Utm_Mgrs(this.zone, hemisphere, e100kNum+this.easting, n2M+n100kNum+this.northing, this.datum);
}
/**
* Parses string representation of MGRS grid reference.
*
* An MGRS grid reference comprises (space-separated)
* - grid zone designator (GZD)
* - 100km grid square letter-pair
* - easting
* - northing.
*
* @param {string} mgrsGridRef - String representation of MGRS grid reference.
* @returns {Mgrs} Mgrs grid reference object.
* @throws {Error} Invalid MGRS grid reference.
*
* @example
* const mgrsRef = Mgrs.parse('31U DQ 48251 11932');
* const mgrsRef = Mgrs.parse('31UDQ4825111932'); // military style no separators
* // mgrsRef: { zone:31, band:'U', e100k:'D', n100k:'Q', easting:48251, northing:11932 }
*/
static parse(mgrsGridRef) {
if (!mgrsGridRef) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
// check for military-style grid reference with no separators
if (!mgrsGridRef.trim().match(/\s/)) { // convert mgrsGridRef to standard space-separated format
const ref = mgrsGridRef.match(/(\d\d?[A-Z])([A-Z]{2})([0-9]{2,10})/i);
if (!ref) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
const [ , gzd, en100k, en ] = ref; // split grid ref into gzd, en100k, en
const [ easting, northing ] = [ en.slice(0, en.length/2), en.slice(-en.length/2) ];
mgrsGridRef = `${gzd} ${en100k} ${easting} ${northing}`;
}
// match separate elements (separated by whitespace)
const ref = mgrsGridRef.match(/\S+/g); // returns [ gzd, e100k, easting, northing ]
if (ref==null || ref.length!=4) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
const [ gzd, en100k, e, n ] = ref; // split grid ref into gzd, en100k, e, n
const [ , zone, band ] = gzd.match(/(\d\d?)([A-Z])/i); // split gzd into zone, band
const [ e100k, n100k ] = en100k.split(''); // split 100km letter-pair into e, n
// standardise to 10-digit refs - ie metres) (but only if < 10-digit refs, to allow decimals)
const easting = e.length>=5 ? e : e.padEnd(5, '0');
const northing = n.length>=5 ? n : n.padEnd(5, '0');
return new Mgrs(zone, band, e100k, n100k, easting, northing);
}
/**
* Returns a string representation of an MGRS grid reference.
*
* To distinguish from civilian UTM coordinate representations, no space is included within the
* zone/band grid zone designator.
*
* Components are separated by spaces: for a military-style unseparated string, use
* Mgrs.toString().replace(/ /g, '');
*
* Note that MGRS grid references get truncated, not rounded (unlike UTM coordinates); grid
* references indicate a bounding square, rather than a point, with the size of the square
* indicated by the precision - a precision of 10 indicates a 1-metre square, a precision of 4
* indicates a 1,000-metre square (hence 31U DQ 48 11 indicates a 1km square with SW corner at
* 31 N 448000 5411000, which would include the 1m square 31U DQ 48251 11932).
*
* @param {number} [digits=10] - Precision of returned grid reference (eg 4 = km, 10 = m).
* @returns {string} This grid reference in standard format.
* @throws {RangeError} Invalid precision.
*
* @example
* const mgrsStr = new Mgrs(31, 'U', 'D', 'Q', 48251, 11932).toString(); // 31U DQ 48251 11932
*/
toString(digits=10) {
if (![ 2, 4, 6, 8, 10 ].includes(Number(digits))) throw new RangeError(`invalid precision ‘${digits}’`);
const { zone, band, e100k, n100k, easting, northing } = this;
// truncate to required precision
const eRounded = Math.floor(easting/Math.pow(10, 5-digits/2));
const nRounded = Math.floor(northing/Math.pow(10, 5-digits/2));
// ensure leading zeros
const zPadded = zone.toString().padStart(2, '0');
const ePadded = eRounded.toString().padStart(digits/2, '0');
const nPadded = nRounded.toString().padStart(digits/2, '0');
return `${zPadded}${band} ${e100k}${n100k} ${ePadded} ${nPadded}`;
}
}
/* Utm_Mgrs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* Extends Utm with method to convert UTM coordinate to MGRS reference.
*
* @extends Utm
*/
class Utm_Mgrs extends Utm {
/**
* Converts UTM coordinate to MGRS reference.
*
* @returns {Mgrs}
* @throws {TypeError} Invalid UTM coordinate.
*
* @example
* const utmCoord = new Utm(31, 'N', 448251, 5411932);
* const mgrsRef = utmCoord.toMgrs(); // 31U DQ 48251 11932
*/
toMgrs() {
// MGRS zone is same as UTM zone
const zone = this.zone;
// convert UTM to lat/long to get latitude to determine band
const latlong = this.toLatLon();
// grid zones are 8° tall, 0°N is 10th band
const band = latBands.charAt(Math.floor(latlong.lat.toFixed(12)/8+10)); // latitude band
// columns in zone 1 are A-H, zone 2 J-R, zone 3 S-Z, then repeating every 3rd zone
const col = Math.floor(this.easting / 100e3);
// (note -1 because eastings start at 166e3 due to 500km false origin)
const e100k = e100kLetters[(zone-1)%3].charAt(col-1);
// rows in even zones are A-V, in odd zones are F-E
const row = Math.floor(this.northing / 100e3) % 20;
const n100k = n100kLetters[(zone-1)%2].charAt(row);
// truncate easting/northing to within 100km grid square & round to 1-metre precision
const easting = Math.floor(this.easting % 100e3);
const northing = Math.floor(this.northing % 100e3);
return new Mgrs(zone, band, e100k, n100k, easting, northing);
}
}
/**
* Extends LatLonEllipsoidal adding toMgrs() method to the Utm object returned by LatLon.toUtm().
*
* @extends LatLonEllipsoidal
*/
class Latlon_Utm_Mgrs extends LatLonEllipsoidal {
/**
* Converts latitude/longitude to UTM coordinate.
*
* Shadow of LatLon.toUtm, returning Utm augmented with toMgrs() method.
*
* @param {number} [zoneOverride] - Use specified zone rather than zone within which point lies;
* note overriding the UTM zone has the potential to result in negative eastings, and
* perverse results within Norway/Svalbard exceptions (this is unlikely to be relevant
* for MGRS, but is needed as Mgrs passes through the Utm class).
* @returns {Utm} UTM coordinate.
* @throws {Error} If point not valid, if point outside latitude range.
*
* @example
* const latlong = new LatLon(48.8582, 2.2945);
* const utmCoord = latlong.toUtm(); // 31 N 448252 5411933
*/
toUtm(zoneOverride=undefined) {
const utm = super.toUtm(zoneOverride);
return new Utm_Mgrs(utm.zone, utm.hemisphere, utm.easting, utm.northing, utm.datum, utm.convergence, utm.scale);
}
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
export { Mgrs as default, Utm_Mgrs as Utm, Latlon_Utm_Mgrs as LatLon, Dms };