gnss-js 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,7 +47,12 @@ function vincenty(lat1, lon1, lat2, lon2) {
47
47
  (cosU2 * sinLambda2) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda2) ** 2
48
48
  );
49
49
  if (sinSigma === 0) {
50
- return { distance: 0, initialBearing: 0, finalBearing: 0 };
50
+ return {
51
+ distance: 0,
52
+ initialBearing: 0,
53
+ finalBearing: 0,
54
+ converged: true
55
+ };
51
56
  }
52
57
  cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda2;
53
58
  sigma = Math.atan2(sinSigma, cosSigma);
@@ -77,7 +82,7 @@ function vincenty(lat1, lon1, lat2, lon2) {
77
82
  cosLat1 * Math.sin(-dLon),
78
83
  -sinLat1 * cosLat2 + cosLat1 * sinLat2 * Math.cos(-dLon)
79
84
  );
80
- return { distance: distance2, initialBearing: ib, finalBearing: fb };
85
+ return { distance: distance2, initialBearing: ib, finalBearing: fb, converged: false };
81
86
  }
82
87
  const uSq = cos2Alpha * (WGS84_SEMI_MAJOR_AXIS ** 2 - WGS84_SEMI_MINOR_AXIS ** 2) / WGS84_SEMI_MINOR_AXIS ** 2;
83
88
  const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
@@ -94,10 +99,53 @@ function vincenty(lat1, lon1, lat2, lon2) {
94
99
  cosU1 * sinLambda,
95
100
  -sinU1 * cosU2 + cosU1 * sinU2 * cosLambda
96
101
  );
97
- return { distance, initialBearing, finalBearing };
102
+ return { distance, initialBearing, finalBearing, converged: true };
103
+ }
104
+ function vincentyDirect(lat1, lon1, bearing, distance) {
105
+ const a = WGS84_SEMI_MAJOR_AXIS, b = WGS84_SEMI_MINOR_AXIS, f = WGS84_FLATTENING;
106
+ const sinA1 = Math.sin(bearing), cosA1 = Math.cos(bearing);
107
+ const tanU1 = (1 - f) * Math.tan(lat1);
108
+ const cosU1 = 1 / Math.sqrt(1 + tanU1 ** 2), sinU1 = tanU1 * cosU1;
109
+ const sigma1 = Math.atan2(tanU1, cosA1);
110
+ const sinAlpha = cosU1 * sinA1;
111
+ const cos2Alpha = 1 - sinAlpha ** 2;
112
+ const uSq = cos2Alpha * (a ** 2 - b ** 2) / b ** 2;
113
+ const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
114
+ const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
115
+ let sigma = distance / (b * A);
116
+ let sigmaPrev;
117
+ let cos2SigmaM, sinSigma, cosSigma;
118
+ let iter = 0;
119
+ do {
120
+ cos2SigmaM = Math.cos(2 * sigma1 + sigma);
121
+ sinSigma = Math.sin(sigma);
122
+ cosSigma = Math.cos(sigma);
123
+ const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)));
124
+ sigmaPrev = sigma;
125
+ sigma = distance / (b * A) + deltaSigma;
126
+ } while (Math.abs(sigma - sigmaPrev) > 1e-12 && ++iter < 200);
127
+ const tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosA1;
128
+ const lat2 = Math.atan2(
129
+ sinU1 * cosSigma + cosU1 * sinSigma * cosA1,
130
+ (1 - f) * Math.sqrt(sinAlpha ** 2 + tmp ** 2)
131
+ );
132
+ const lambda = Math.atan2(
133
+ sinSigma * sinA1,
134
+ cosU1 * cosSigma - sinU1 * sinSigma * cosA1
135
+ );
136
+ const C = f / 16 * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha));
137
+ const L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)));
138
+ let lon2 = lon1 + L;
139
+ lon2 = Math.atan2(Math.sin(lon2), Math.cos(lon2));
140
+ return [lat2, lon2];
141
+ }
142
+ function meridianArc(lat) {
143
+ const n = WGS84_FLATTENING / (2 - WGS84_FLATTENING);
144
+ const n2 = n * n, n3 = n2 * n, n4 = n3 * n;
145
+ const mu = lat + (-3 * n / 2 + 9 * n3 / 16) * Math.sin(2 * lat) + (15 * n2 / 16 - 15 * n4 / 32) * Math.sin(4 * lat) + -35 * n3 / 48 * Math.sin(6 * lat) + 315 * n4 / 512 * Math.sin(8 * lat);
146
+ return WGS84_SEMI_MAJOR_AXIS / (1 + n) * (1 + n2 / 4 + n4 / 64) * mu;
98
147
  }
99
148
  function rhumbLine(lat1, lon1, lat2, lon2) {
100
- const dLat = lat2 - lat1;
101
149
  let dLon = lon2 - lon1;
102
150
  const MAX_LAT = Math.PI / 2 - 1e-10;
103
151
  const isometricLat = (lat) => {
@@ -111,33 +159,28 @@ function rhumbLine(lat1, lon1, lat2, lon2) {
111
159
  const psi1 = isometricLat(lat1);
112
160
  const psi2 = isometricLat(lat2);
113
161
  const dPsi = psi2 - psi1;
114
- const q = Math.abs(dPsi) > 1e-12 ? dLat / dPsi : Math.cos(lat1);
115
162
  if (Math.abs(dLon) > Math.PI) {
116
163
  dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
117
164
  }
118
165
  const bearing = Math.atan2(dLon, dPsi);
119
- const e22 = WGS84_ECCENTRICITY_SQUARED;
120
- const e4 = e22 * e22;
121
- const e6 = e4 * e22;
122
- const meridionalRadius = WGS84_SEMI_MAJOR_AXIS * (1 - e22) * (1 + 3 / 4 * e22 + 45 / 64 * e4 + 175 / 256 * e6);
123
- const distance = Math.sqrt(dLat ** 2 + q ** 2 * dLon ** 2) * meridionalRadius;
166
+ const dM = meridianArc(lat2) - meridianArc(lat1);
167
+ let q;
168
+ if (Math.abs(dPsi) > 1e-6) {
169
+ q = dM / dPsi;
170
+ } else {
171
+ const latM = (lat1 + lat2) / 2;
172
+ q = WGS84_SEMI_MAJOR_AXIS * Math.cos(latM) / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * Math.sin(latM) ** 2);
173
+ }
174
+ const distance = Math.sqrt(dM ** 2 + (q * dLon) ** 2);
124
175
  return { distance, bearing };
125
176
  }
126
177
  function euclidean3D(x1, y1, z1, x2, y2, z2) {
127
178
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2);
128
179
  }
129
180
  function greatCircleMidpoint(lat1, lon1, lat2, lon2) {
130
- const dLon = lon2 - lon1;
131
- const Bx = Math.cos(lat2) * Math.cos(dLon);
132
- const By = Math.cos(lat2) * Math.sin(dLon);
133
- const sumX = Math.cos(lat1) + Bx;
134
- const denom = Math.sqrt(sumX ** 2 + By ** 2);
135
- if (denom < 1e-15) {
136
- return [(lat1 + lat2) / 2, lon1];
137
- }
138
- const latMid = Math.atan2(Math.sin(lat1) + Math.sin(lat2), denom);
139
- const lonMid = lon1 + Math.atan2(By, sumX);
140
- return [latMid, lonMid];
181
+ const { distance, initialBearing } = vincenty(lat1, lon1, lat2, lon2);
182
+ if (distance === 0) return [lat1, lon1];
183
+ return vincentyDirect(lat1, lon1, initialBearing, distance / 2);
141
184
  }
142
185
  function horizonDistance(heightMeters) {
143
186
  if (heightMeters <= 0) return 0;
@@ -150,18 +193,36 @@ function horizonDistance(heightMeters) {
150
193
  // src/coordinates/projections.ts
151
194
  var e2 = WGS84_ECCENTRICITY_SQUARED;
152
195
  var k0 = 0.9996;
196
+ var UTM_BANDS = "CDEFGHJKLMNPQRSTUVWX";
197
+ function utmZone(latDeg, lonDeg) {
198
+ let zone = Math.floor((lonDeg + 180) / 6) + 1;
199
+ if (zone > 60) zone = 1;
200
+ if (latDeg >= 56 && latDeg < 64 && lonDeg >= 3 && lonDeg < 12) return 32;
201
+ if (latDeg >= 72 && latDeg <= 84) {
202
+ if (lonDeg >= 0 && lonDeg < 9) return 31;
203
+ if (lonDeg >= 9 && lonDeg < 21) return 33;
204
+ if (lonDeg >= 21 && lonDeg < 33) return 35;
205
+ if (lonDeg >= 33 && lonDeg < 42) return 37;
206
+ }
207
+ return zone;
208
+ }
153
209
  function geodeticToUtm(lat, lon) {
210
+ const rawLatDeg = lat * 180 / Math.PI;
211
+ const inRange = rawLatDeg >= -80 && rawLatDeg <= 84;
154
212
  const MAX_UTM_LAT = 84 * (Math.PI / 180);
155
213
  lat = Math.max(-MAX_UTM_LAT, Math.min(MAX_UTM_LAT, lat));
156
214
  const latDeg = lat * 180 / Math.PI;
215
+ lon = Math.atan2(Math.sin(lon), Math.cos(lon));
157
216
  const lonDeg = lon * 180 / Math.PI;
158
- const zone = Math.floor((lonDeg + 180) / 6) + 1;
217
+ const zone = utmZone(latDeg, lonDeg);
218
+ const band = UTM_BANDS[Math.max(0, Math.min(19, Math.floor((latDeg + 80) / 8)))];
159
219
  const lon0 = ((zone - 1) * 6 - 180 + 3) * (Math.PI / 180);
160
220
  const ep2 = e2 / (1 - e2);
161
221
  const N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - e2 * Math.sin(lat) ** 2);
162
222
  const T = Math.tan(lat) ** 2;
163
223
  const C = ep2 * Math.cos(lat) ** 2;
164
- const A = Math.cos(lat) * (lon - lon0);
224
+ const dLon0 = Math.atan2(Math.sin(lon - lon0), Math.cos(lon - lon0));
225
+ const A = Math.cos(lat) * dLon0;
165
226
  const M = WGS84_SEMI_MAJOR_AXIS * ((1 - e2 / 4 - 3 * e2 ** 2 / 64 - 5 * e2 ** 3 / 256) * lat - (3 * e2 / 8 + 3 * e2 ** 2 / 32 + 45 * e2 ** 3 / 1024) * Math.sin(2 * lat) + (15 * e2 ** 2 / 256 + 45 * e2 ** 3 / 1024) * Math.sin(4 * lat) - 35 * e2 ** 3 / 3072 * Math.sin(6 * lat));
166
227
  const easting = k0 * N * (A + (1 - T + C) * A ** 3 / 6 + (5 - 18 * T + T ** 2 + 72 * C - 58 * ep2) * A ** 5 / 120) + 5e5;
167
228
  let northing = k0 * (M + N * Math.tan(lat) * (A ** 2 / 2 + (5 - T + 9 * C + 4 * C ** 2) * A ** 4 / 24 + (61 - 58 * T + T ** 2 + 600 * C - 330 * ep2) * A ** 6 / 720));
@@ -169,7 +230,7 @@ function geodeticToUtm(lat, lon) {
169
230
  if (hemisphere === "S") {
170
231
  northing += 1e7;
171
232
  }
172
- return { easting, northing, zone, hemisphere };
233
+ return { easting, northing, zone, hemisphere, band, inRange };
173
234
  }
174
235
  function geodeticToMaidenhead(lat, lon) {
175
236
  const latDeg = Math.max(0, Math.min(179.999999, lat * 180 / Math.PI + 90));
@@ -229,6 +290,8 @@ export {
229
290
  deg2rad,
230
291
  deg2dms,
231
292
  vincenty,
293
+ vincentyDirect,
294
+ meridianArc,
232
295
  rhumbLine,
233
296
  euclidean3D,
234
297
  greatCircleMidpoint,
@@ -33,9 +33,11 @@ __export(coordinates_exports, {
33
33
  getEnuDifference: () => getEnuDifference,
34
34
  greatCircleMidpoint: () => greatCircleMidpoint,
35
35
  horizonDistance: () => horizonDistance,
36
+ meridianArc: () => meridianArc,
36
37
  rad2deg: () => rad2deg,
37
38
  rhumbLine: () => rhumbLine,
38
- vincenty: () => vincenty
39
+ vincenty: () => vincenty,
40
+ vincentyDirect: () => vincentyDirect
39
41
  });
40
42
  module.exports = __toCommonJS(coordinates_exports);
41
43
 
@@ -140,7 +142,12 @@ function vincenty(lat1, lon1, lat2, lon2) {
140
142
  (cosU2 * sinLambda2) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda2) ** 2
141
143
  );
142
144
  if (sinSigma === 0) {
143
- return { distance: 0, initialBearing: 0, finalBearing: 0 };
145
+ return {
146
+ distance: 0,
147
+ initialBearing: 0,
148
+ finalBearing: 0,
149
+ converged: true
150
+ };
144
151
  }
145
152
  cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda2;
146
153
  sigma = Math.atan2(sinSigma, cosSigma);
@@ -170,7 +177,7 @@ function vincenty(lat1, lon1, lat2, lon2) {
170
177
  cosLat1 * Math.sin(-dLon),
171
178
  -sinLat1 * cosLat2 + cosLat1 * sinLat2 * Math.cos(-dLon)
172
179
  );
173
- return { distance: distance2, initialBearing: ib, finalBearing: fb };
180
+ return { distance: distance2, initialBearing: ib, finalBearing: fb, converged: false };
174
181
  }
175
182
  const uSq = cos2Alpha * (WGS84_SEMI_MAJOR_AXIS ** 2 - WGS84_SEMI_MINOR_AXIS ** 2) / WGS84_SEMI_MINOR_AXIS ** 2;
176
183
  const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
@@ -187,10 +194,53 @@ function vincenty(lat1, lon1, lat2, lon2) {
187
194
  cosU1 * sinLambda,
188
195
  -sinU1 * cosU2 + cosU1 * sinU2 * cosLambda
189
196
  );
190
- return { distance, initialBearing, finalBearing };
197
+ return { distance, initialBearing, finalBearing, converged: true };
198
+ }
199
+ function vincentyDirect(lat1, lon1, bearing, distance) {
200
+ const a = WGS84_SEMI_MAJOR_AXIS, b = WGS84_SEMI_MINOR_AXIS, f = WGS84_FLATTENING;
201
+ const sinA1 = Math.sin(bearing), cosA1 = Math.cos(bearing);
202
+ const tanU1 = (1 - f) * Math.tan(lat1);
203
+ const cosU1 = 1 / Math.sqrt(1 + tanU1 ** 2), sinU1 = tanU1 * cosU1;
204
+ const sigma1 = Math.atan2(tanU1, cosA1);
205
+ const sinAlpha = cosU1 * sinA1;
206
+ const cos2Alpha = 1 - sinAlpha ** 2;
207
+ const uSq = cos2Alpha * (a ** 2 - b ** 2) / b ** 2;
208
+ const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
209
+ const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
210
+ let sigma = distance / (b * A);
211
+ let sigmaPrev;
212
+ let cos2SigmaM, sinSigma, cosSigma;
213
+ let iter = 0;
214
+ do {
215
+ cos2SigmaM = Math.cos(2 * sigma1 + sigma);
216
+ sinSigma = Math.sin(sigma);
217
+ cosSigma = Math.cos(sigma);
218
+ const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)));
219
+ sigmaPrev = sigma;
220
+ sigma = distance / (b * A) + deltaSigma;
221
+ } while (Math.abs(sigma - sigmaPrev) > 1e-12 && ++iter < 200);
222
+ const tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosA1;
223
+ const lat2 = Math.atan2(
224
+ sinU1 * cosSigma + cosU1 * sinSigma * cosA1,
225
+ (1 - f) * Math.sqrt(sinAlpha ** 2 + tmp ** 2)
226
+ );
227
+ const lambda = Math.atan2(
228
+ sinSigma * sinA1,
229
+ cosU1 * cosSigma - sinU1 * sinSigma * cosA1
230
+ );
231
+ const C = f / 16 * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha));
232
+ const L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)));
233
+ let lon2 = lon1 + L;
234
+ lon2 = Math.atan2(Math.sin(lon2), Math.cos(lon2));
235
+ return [lat2, lon2];
236
+ }
237
+ function meridianArc(lat) {
238
+ const n = WGS84_FLATTENING / (2 - WGS84_FLATTENING);
239
+ const n2 = n * n, n3 = n2 * n, n4 = n3 * n;
240
+ const mu = lat + (-3 * n / 2 + 9 * n3 / 16) * Math.sin(2 * lat) + (15 * n2 / 16 - 15 * n4 / 32) * Math.sin(4 * lat) + -35 * n3 / 48 * Math.sin(6 * lat) + 315 * n4 / 512 * Math.sin(8 * lat);
241
+ return WGS84_SEMI_MAJOR_AXIS / (1 + n) * (1 + n2 / 4 + n4 / 64) * mu;
191
242
  }
192
243
  function rhumbLine(lat1, lon1, lat2, lon2) {
193
- const dLat = lat2 - lat1;
194
244
  let dLon = lon2 - lon1;
195
245
  const MAX_LAT = Math.PI / 2 - 1e-10;
196
246
  const isometricLat = (lat) => {
@@ -204,33 +254,28 @@ function rhumbLine(lat1, lon1, lat2, lon2) {
204
254
  const psi1 = isometricLat(lat1);
205
255
  const psi2 = isometricLat(lat2);
206
256
  const dPsi = psi2 - psi1;
207
- const q = Math.abs(dPsi) > 1e-12 ? dLat / dPsi : Math.cos(lat1);
208
257
  if (Math.abs(dLon) > Math.PI) {
209
258
  dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
210
259
  }
211
260
  const bearing = Math.atan2(dLon, dPsi);
212
- const e22 = WGS84_ECCENTRICITY_SQUARED;
213
- const e4 = e22 * e22;
214
- const e6 = e4 * e22;
215
- const meridionalRadius = WGS84_SEMI_MAJOR_AXIS * (1 - e22) * (1 + 3 / 4 * e22 + 45 / 64 * e4 + 175 / 256 * e6);
216
- const distance = Math.sqrt(dLat ** 2 + q ** 2 * dLon ** 2) * meridionalRadius;
261
+ const dM = meridianArc(lat2) - meridianArc(lat1);
262
+ let q;
263
+ if (Math.abs(dPsi) > 1e-6) {
264
+ q = dM / dPsi;
265
+ } else {
266
+ const latM = (lat1 + lat2) / 2;
267
+ q = WGS84_SEMI_MAJOR_AXIS * Math.cos(latM) / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * Math.sin(latM) ** 2);
268
+ }
269
+ const distance = Math.sqrt(dM ** 2 + (q * dLon) ** 2);
217
270
  return { distance, bearing };
218
271
  }
219
272
  function euclidean3D(x1, y1, z1, x2, y2, z2) {
220
273
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2);
221
274
  }
222
275
  function greatCircleMidpoint(lat1, lon1, lat2, lon2) {
223
- const dLon = lon2 - lon1;
224
- const Bx = Math.cos(lat2) * Math.cos(dLon);
225
- const By = Math.cos(lat2) * Math.sin(dLon);
226
- const sumX = Math.cos(lat1) + Bx;
227
- const denom = Math.sqrt(sumX ** 2 + By ** 2);
228
- if (denom < 1e-15) {
229
- return [(lat1 + lat2) / 2, lon1];
230
- }
231
- const latMid = Math.atan2(Math.sin(lat1) + Math.sin(lat2), denom);
232
- const lonMid = lon1 + Math.atan2(By, sumX);
233
- return [latMid, lonMid];
276
+ const { distance, initialBearing } = vincenty(lat1, lon1, lat2, lon2);
277
+ if (distance === 0) return [lat1, lon1];
278
+ return vincentyDirect(lat1, lon1, initialBearing, distance / 2);
234
279
  }
235
280
  function horizonDistance(heightMeters) {
236
281
  if (heightMeters <= 0) return 0;
@@ -243,18 +288,36 @@ function horizonDistance(heightMeters) {
243
288
  // src/coordinates/projections.ts
244
289
  var e2 = WGS84_ECCENTRICITY_SQUARED;
245
290
  var k0 = 0.9996;
291
+ var UTM_BANDS = "CDEFGHJKLMNPQRSTUVWX";
292
+ function utmZone(latDeg, lonDeg) {
293
+ let zone = Math.floor((lonDeg + 180) / 6) + 1;
294
+ if (zone > 60) zone = 1;
295
+ if (latDeg >= 56 && latDeg < 64 && lonDeg >= 3 && lonDeg < 12) return 32;
296
+ if (latDeg >= 72 && latDeg <= 84) {
297
+ if (lonDeg >= 0 && lonDeg < 9) return 31;
298
+ if (lonDeg >= 9 && lonDeg < 21) return 33;
299
+ if (lonDeg >= 21 && lonDeg < 33) return 35;
300
+ if (lonDeg >= 33 && lonDeg < 42) return 37;
301
+ }
302
+ return zone;
303
+ }
246
304
  function geodeticToUtm(lat, lon) {
305
+ const rawLatDeg = lat * 180 / Math.PI;
306
+ const inRange = rawLatDeg >= -80 && rawLatDeg <= 84;
247
307
  const MAX_UTM_LAT = 84 * (Math.PI / 180);
248
308
  lat = Math.max(-MAX_UTM_LAT, Math.min(MAX_UTM_LAT, lat));
249
309
  const latDeg = lat * 180 / Math.PI;
310
+ lon = Math.atan2(Math.sin(lon), Math.cos(lon));
250
311
  const lonDeg = lon * 180 / Math.PI;
251
- const zone = Math.floor((lonDeg + 180) / 6) + 1;
312
+ const zone = utmZone(latDeg, lonDeg);
313
+ const band = UTM_BANDS[Math.max(0, Math.min(19, Math.floor((latDeg + 80) / 8)))];
252
314
  const lon0 = ((zone - 1) * 6 - 180 + 3) * (Math.PI / 180);
253
315
  const ep2 = e2 / (1 - e2);
254
316
  const N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - e2 * Math.sin(lat) ** 2);
255
317
  const T = Math.tan(lat) ** 2;
256
318
  const C = ep2 * Math.cos(lat) ** 2;
257
- const A = Math.cos(lat) * (lon - lon0);
319
+ const dLon0 = Math.atan2(Math.sin(lon - lon0), Math.cos(lon - lon0));
320
+ const A = Math.cos(lat) * dLon0;
258
321
  const M = WGS84_SEMI_MAJOR_AXIS * ((1 - e2 / 4 - 3 * e2 ** 2 / 64 - 5 * e2 ** 3 / 256) * lat - (3 * e2 / 8 + 3 * e2 ** 2 / 32 + 45 * e2 ** 3 / 1024) * Math.sin(2 * lat) + (15 * e2 ** 2 / 256 + 45 * e2 ** 3 / 1024) * Math.sin(4 * lat) - 35 * e2 ** 3 / 3072 * Math.sin(6 * lat));
259
322
  const easting = k0 * N * (A + (1 - T + C) * A ** 3 / 6 + (5 - 18 * T + T ** 2 + 72 * C - 58 * ep2) * A ** 5 / 120) + 5e5;
260
323
  let northing = k0 * (M + N * Math.tan(lat) * (A ** 2 / 2 + (5 - T + 9 * C + 4 * C ** 2) * A ** 4 / 24 + (61 - 58 * T + T ** 2 + 600 * C - 330 * ep2) * A ** 6 / 720));
@@ -262,7 +325,7 @@ function geodeticToUtm(lat, lon) {
262
325
  if (hemisphere === "S") {
263
326
  northing += 1e7;
264
327
  }
265
- return { easting, northing, zone, hemisphere };
328
+ return { easting, northing, zone, hemisphere, band, inRange };
266
329
  }
267
330
  function geodeticToMaidenhead(lat, lon) {
268
331
  const latDeg = Math.max(0, Math.min(179.999999, lat * 180 / Math.PI + 90));
@@ -331,7 +394,9 @@ function geodeticToGeohash(lat, lon, precision = 8) {
331
394
  getEnuDifference,
332
395
  greatCircleMidpoint,
333
396
  horizonDistance,
397
+ meridianArc,
334
398
  rad2deg,
335
399
  rhumbLine,
336
- vincenty
400
+ vincenty,
401
+ vincentyDirect
337
402
  });
@@ -14,13 +14,29 @@ declare function deg2dms(deg: number): [number, number, number];
14
14
  * Vincenty's inverse formula — orthodromic (great-circle) distance,
15
15
  * initial bearing, and final bearing on the WGS84 ellipsoid.
16
16
  * All angles in radians.
17
- * @returns { distance (m), initialBearing (rad), finalBearing (rad) }
17
+ * @returns { distance (m), initialBearing (rad), finalBearing (rad),
18
+ * converged } — `converged` is false for near-antipodal pairs, where the
19
+ * iteration fails and a spherical approximation is returned instead
20
+ * (distance good to ~0.5 %, bearings unreliable).
18
21
  */
19
22
  declare function vincenty(lat1: number, lon1: number, lat2: number, lon2: number): {
20
23
  distance: number;
21
24
  initialBearing: number;
22
25
  finalBearing: number;
26
+ converged: boolean;
23
27
  };
28
+ /**
29
+ * Vincenty's direct formula — the point reached from (lat1, lon1) after
30
+ * `distance` metres along the geodesic leaving at `bearing` (radians,
31
+ * clockwise from north) on the WGS84 ellipsoid.
32
+ * @returns [lat, lon] in radians, lon wrapped to (-π, π]
33
+ */
34
+ declare function vincentyDirect(lat1: number, lon1: number, bearing: number, distance: number): [number, number];
35
+ /**
36
+ * Meridian arc length from the equator to geodetic latitude `lat` (radians)
37
+ * on WGS84, via the rectifying latitude (Helmert series in n, sub-mm).
38
+ */
39
+ declare function meridianArc(lat: number): number;
24
40
  /**
25
41
  * Loxodromic (rhumb line) distance and constant bearing on the WGS84 ellipsoid.
26
42
  * Uses the ellipsoidal rhumb-line formula with isometric latitudes.
@@ -41,7 +57,8 @@ declare function rhumbLine(lat1: number, lon1: number, lat2: number, lon2: numbe
41
57
  */
42
58
  declare function euclidean3D(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number;
43
59
  /**
44
- * Geodetic midpoint along the great-circle arc.
60
+ * Midpoint of the geodesic (shortest path on the WGS84 ellipsoid) between
61
+ * two points: half the Vincenty distance along the initial bearing.
45
62
  * @returns [lat, lon] in radians
46
63
  */
47
64
  declare function greatCircleMidpoint(lat1: number, lon1: number, lat2: number, lon2: number): [number, number];
@@ -57,12 +74,18 @@ declare function horizonDistance(heightMeters: number): number;
57
74
  * Convert geodetic coordinates to UTM.
58
75
  * @param lat Latitude in radians
59
76
  * @param lon Longitude in radians
77
+ * @returns easting/northing (m), zone (1–60, with the Norway/Svalbard
78
+ * exceptions), hemisphere, MGRS latitude `band` letter (C–X), and
79
+ * `inRange` — false outside UTM's 80°S–84°N, where the values are
80
+ * computed at the clamped latitude and are not a valid UTM position.
60
81
  */
61
82
  declare function geodeticToUtm(lat: number, lon: number): {
62
83
  easting: number;
63
84
  northing: number;
64
85
  zone: number;
65
86
  hemisphere: 'N' | 'S';
87
+ band: string;
88
+ inRange: boolean;
66
89
  };
67
90
  /**
68
91
  * Maidenhead grid locator (6 characters) from geodetic coordinates.
@@ -78,4 +101,4 @@ declare function geodeticToMaidenhead(lat: number, lon: number): string;
78
101
  */
79
102
  declare function geodeticToGeohash(lat: number, lon: number, precision?: number): string;
80
103
 
81
- export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, rad2deg, rhumbLine, vincenty };
104
+ export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, meridianArc, rad2deg, rhumbLine, vincenty, vincentyDirect };
@@ -14,13 +14,29 @@ declare function deg2dms(deg: number): [number, number, number];
14
14
  * Vincenty's inverse formula — orthodromic (great-circle) distance,
15
15
  * initial bearing, and final bearing on the WGS84 ellipsoid.
16
16
  * All angles in radians.
17
- * @returns { distance (m), initialBearing (rad), finalBearing (rad) }
17
+ * @returns { distance (m), initialBearing (rad), finalBearing (rad),
18
+ * converged } — `converged` is false for near-antipodal pairs, where the
19
+ * iteration fails and a spherical approximation is returned instead
20
+ * (distance good to ~0.5 %, bearings unreliable).
18
21
  */
19
22
  declare function vincenty(lat1: number, lon1: number, lat2: number, lon2: number): {
20
23
  distance: number;
21
24
  initialBearing: number;
22
25
  finalBearing: number;
26
+ converged: boolean;
23
27
  };
28
+ /**
29
+ * Vincenty's direct formula — the point reached from (lat1, lon1) after
30
+ * `distance` metres along the geodesic leaving at `bearing` (radians,
31
+ * clockwise from north) on the WGS84 ellipsoid.
32
+ * @returns [lat, lon] in radians, lon wrapped to (-π, π]
33
+ */
34
+ declare function vincentyDirect(lat1: number, lon1: number, bearing: number, distance: number): [number, number];
35
+ /**
36
+ * Meridian arc length from the equator to geodetic latitude `lat` (radians)
37
+ * on WGS84, via the rectifying latitude (Helmert series in n, sub-mm).
38
+ */
39
+ declare function meridianArc(lat: number): number;
24
40
  /**
25
41
  * Loxodromic (rhumb line) distance and constant bearing on the WGS84 ellipsoid.
26
42
  * Uses the ellipsoidal rhumb-line formula with isometric latitudes.
@@ -41,7 +57,8 @@ declare function rhumbLine(lat1: number, lon1: number, lat2: number, lon2: numbe
41
57
  */
42
58
  declare function euclidean3D(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number;
43
59
  /**
44
- * Geodetic midpoint along the great-circle arc.
60
+ * Midpoint of the geodesic (shortest path on the WGS84 ellipsoid) between
61
+ * two points: half the Vincenty distance along the initial bearing.
45
62
  * @returns [lat, lon] in radians
46
63
  */
47
64
  declare function greatCircleMidpoint(lat1: number, lon1: number, lat2: number, lon2: number): [number, number];
@@ -57,12 +74,18 @@ declare function horizonDistance(heightMeters: number): number;
57
74
  * Convert geodetic coordinates to UTM.
58
75
  * @param lat Latitude in radians
59
76
  * @param lon Longitude in radians
77
+ * @returns easting/northing (m), zone (1–60, with the Norway/Svalbard
78
+ * exceptions), hemisphere, MGRS latitude `band` letter (C–X), and
79
+ * `inRange` — false outside UTM's 80°S–84°N, where the values are
80
+ * computed at the clamped latitude and are not a valid UTM position.
60
81
  */
61
82
  declare function geodeticToUtm(lat: number, lon: number): {
62
83
  easting: number;
63
84
  northing: number;
64
85
  zone: number;
65
86
  hemisphere: 'N' | 'S';
87
+ band: string;
88
+ inRange: boolean;
66
89
  };
67
90
  /**
68
91
  * Maidenhead grid locator (6 characters) from geodetic coordinates.
@@ -78,4 +101,4 @@ declare function geodeticToMaidenhead(lat: number, lon: number): string;
78
101
  */
79
102
  declare function geodeticToGeohash(lat: number, lon: number, precision?: number): string;
80
103
 
81
- export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, rad2deg, rhumbLine, vincenty };
104
+ export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, meridianArc, rad2deg, rhumbLine, vincenty, vincentyDirect };
@@ -7,10 +7,12 @@ import {
7
7
  geodeticToUtm,
8
8
  greatCircleMidpoint,
9
9
  horizonDistance,
10
+ meridianArc,
10
11
  rad2deg,
11
12
  rhumbLine,
12
- vincenty
13
- } from "./chunk-MIIM4LDY.js";
13
+ vincenty,
14
+ vincentyDirect
15
+ } from "./chunk-CVCTRIWU.js";
14
16
  import {
15
17
  clampUnit,
16
18
  ecefToGeodetic,
@@ -33,7 +35,9 @@ export {
33
35
  getEnuDifference,
34
36
  greatCircleMidpoint,
35
37
  horizonDistance,
38
+ meridianArc,
36
39
  rad2deg,
37
40
  rhumbLine,
38
- vincenty
41
+ vincenty,
42
+ vincentyDirect
39
43
  };
package/dist/index.cjs CHANGED
@@ -261,6 +261,7 @@ __export(src_exports, {
261
261
  lambdaSearch: () => lambdaSearch,
262
262
  looksLikeRtcm2: () => looksLikeRtcm2,
263
263
  maskRadForAzimuth: () => maskRadForAzimuth,
264
+ meridianArc: () => meridianArc,
264
265
  moonEcef: () => moonEcef,
265
266
  navTimesFromEph: () => navTimesFromEph,
266
267
  niellMapping: () => niellMapping,
@@ -355,6 +356,7 @@ __export(src_exports, {
355
356
  updateStreamStats: () => updateStreamStats,
356
357
  verifyChecksum: () => verifyChecksum,
357
358
  vincenty: () => vincenty,
359
+ vincentyDirect: () => vincentyDirect,
358
360
  wlWavelength: () => wlWavelength,
359
361
  writeCrx: () => writeCrx,
360
362
  writeModernObsBlob: () => writeModernObsBlob,
@@ -1125,7 +1127,12 @@ function vincenty(lat1, lon1, lat2, lon2) {
1125
1127
  (cosU2 * sinLambda2) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda2) ** 2
1126
1128
  );
1127
1129
  if (sinSigma === 0) {
1128
- return { distance: 0, initialBearing: 0, finalBearing: 0 };
1130
+ return {
1131
+ distance: 0,
1132
+ initialBearing: 0,
1133
+ finalBearing: 0,
1134
+ converged: true
1135
+ };
1129
1136
  }
1130
1137
  cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda2;
1131
1138
  sigma = Math.atan2(sinSigma, cosSigma);
@@ -1155,7 +1162,7 @@ function vincenty(lat1, lon1, lat2, lon2) {
1155
1162
  cosLat1 * Math.sin(-dLon),
1156
1163
  -sinLat1 * cosLat2 + cosLat1 * sinLat2 * Math.cos(-dLon)
1157
1164
  );
1158
- return { distance: distance2, initialBearing: ib, finalBearing: fb };
1165
+ return { distance: distance2, initialBearing: ib, finalBearing: fb, converged: false };
1159
1166
  }
1160
1167
  const uSq = cos2Alpha * (WGS84_SEMI_MAJOR_AXIS ** 2 - WGS84_SEMI_MINOR_AXIS ** 2) / WGS84_SEMI_MINOR_AXIS ** 2;
1161
1168
  const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
@@ -1172,10 +1179,53 @@ function vincenty(lat1, lon1, lat2, lon2) {
1172
1179
  cosU1 * sinLambda,
1173
1180
  -sinU1 * cosU2 + cosU1 * sinU2 * cosLambda
1174
1181
  );
1175
- return { distance, initialBearing, finalBearing };
1182
+ return { distance, initialBearing, finalBearing, converged: true };
1183
+ }
1184
+ function vincentyDirect(lat1, lon1, bearing, distance) {
1185
+ const a = WGS84_SEMI_MAJOR_AXIS, b = WGS84_SEMI_MINOR_AXIS, f = WGS84_FLATTENING;
1186
+ const sinA1 = Math.sin(bearing), cosA1 = Math.cos(bearing);
1187
+ const tanU1 = (1 - f) * Math.tan(lat1);
1188
+ const cosU1 = 1 / Math.sqrt(1 + tanU1 ** 2), sinU1 = tanU1 * cosU1;
1189
+ const sigma1 = Math.atan2(tanU1, cosA1);
1190
+ const sinAlpha = cosU1 * sinA1;
1191
+ const cos2Alpha = 1 - sinAlpha ** 2;
1192
+ const uSq = cos2Alpha * (a ** 2 - b ** 2) / b ** 2;
1193
+ const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
1194
+ const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
1195
+ let sigma = distance / (b * A);
1196
+ let sigmaPrev;
1197
+ let cos2SigmaM, sinSigma, cosSigma;
1198
+ let iter = 0;
1199
+ do {
1200
+ cos2SigmaM = Math.cos(2 * sigma1 + sigma);
1201
+ sinSigma = Math.sin(sigma);
1202
+ cosSigma = Math.cos(sigma);
1203
+ const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)));
1204
+ sigmaPrev = sigma;
1205
+ sigma = distance / (b * A) + deltaSigma;
1206
+ } while (Math.abs(sigma - sigmaPrev) > 1e-12 && ++iter < 200);
1207
+ const tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosA1;
1208
+ const lat2 = Math.atan2(
1209
+ sinU1 * cosSigma + cosU1 * sinSigma * cosA1,
1210
+ (1 - f) * Math.sqrt(sinAlpha ** 2 + tmp ** 2)
1211
+ );
1212
+ const lambda = Math.atan2(
1213
+ sinSigma * sinA1,
1214
+ cosU1 * cosSigma - sinU1 * sinSigma * cosA1
1215
+ );
1216
+ const C2 = f / 16 * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha));
1217
+ const L = lambda - (1 - C2) * f * sinAlpha * (sigma + C2 * sinSigma * (cos2SigmaM + C2 * cosSigma * (-1 + 2 * cos2SigmaM ** 2)));
1218
+ let lon2 = lon1 + L;
1219
+ lon2 = Math.atan2(Math.sin(lon2), Math.cos(lon2));
1220
+ return [lat2, lon2];
1221
+ }
1222
+ function meridianArc(lat) {
1223
+ const n = WGS84_FLATTENING / (2 - WGS84_FLATTENING);
1224
+ const n2 = n * n, n3 = n2 * n, n4 = n3 * n;
1225
+ const mu = lat + (-3 * n / 2 + 9 * n3 / 16) * Math.sin(2 * lat) + (15 * n2 / 16 - 15 * n4 / 32) * Math.sin(4 * lat) + -35 * n3 / 48 * Math.sin(6 * lat) + 315 * n4 / 512 * Math.sin(8 * lat);
1226
+ return WGS84_SEMI_MAJOR_AXIS / (1 + n) * (1 + n2 / 4 + n4 / 64) * mu;
1176
1227
  }
1177
1228
  function rhumbLine(lat1, lon1, lat2, lon2) {
1178
- const dLat = lat2 - lat1;
1179
1229
  let dLon = lon2 - lon1;
1180
1230
  const MAX_LAT = Math.PI / 2 - 1e-10;
1181
1231
  const isometricLat = (lat) => {
@@ -1189,33 +1239,28 @@ function rhumbLine(lat1, lon1, lat2, lon2) {
1189
1239
  const psi1 = isometricLat(lat1);
1190
1240
  const psi2 = isometricLat(lat2);
1191
1241
  const dPsi = psi2 - psi1;
1192
- const q = Math.abs(dPsi) > 1e-12 ? dLat / dPsi : Math.cos(lat1);
1193
1242
  if (Math.abs(dLon) > Math.PI) {
1194
1243
  dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
1195
1244
  }
1196
1245
  const bearing = Math.atan2(dLon, dPsi);
1197
- const e22 = WGS84_ECCENTRICITY_SQUARED;
1198
- const e4 = e22 * e22;
1199
- const e6 = e4 * e22;
1200
- const meridionalRadius = WGS84_SEMI_MAJOR_AXIS * (1 - e22) * (1 + 3 / 4 * e22 + 45 / 64 * e4 + 175 / 256 * e6);
1201
- const distance = Math.sqrt(dLat ** 2 + q ** 2 * dLon ** 2) * meridionalRadius;
1246
+ const dM = meridianArc(lat2) - meridianArc(lat1);
1247
+ let q;
1248
+ if (Math.abs(dPsi) > 1e-6) {
1249
+ q = dM / dPsi;
1250
+ } else {
1251
+ const latM = (lat1 + lat2) / 2;
1252
+ q = WGS84_SEMI_MAJOR_AXIS * Math.cos(latM) / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * Math.sin(latM) ** 2);
1253
+ }
1254
+ const distance = Math.sqrt(dM ** 2 + (q * dLon) ** 2);
1202
1255
  return { distance, bearing };
1203
1256
  }
1204
1257
  function euclidean3D(x12, y1, z1, x22, y2, z2) {
1205
1258
  return Math.sqrt((x22 - x12) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2);
1206
1259
  }
1207
1260
  function greatCircleMidpoint(lat1, lon1, lat2, lon2) {
1208
- const dLon = lon2 - lon1;
1209
- const Bx = Math.cos(lat2) * Math.cos(dLon);
1210
- const By = Math.cos(lat2) * Math.sin(dLon);
1211
- const sumX = Math.cos(lat1) + Bx;
1212
- const denom = Math.sqrt(sumX ** 2 + By ** 2);
1213
- if (denom < 1e-15) {
1214
- return [(lat1 + lat2) / 2, lon1];
1215
- }
1216
- const latMid = Math.atan2(Math.sin(lat1) + Math.sin(lat2), denom);
1217
- const lonMid = lon1 + Math.atan2(By, sumX);
1218
- return [latMid, lonMid];
1261
+ const { distance, initialBearing } = vincenty(lat1, lon1, lat2, lon2);
1262
+ if (distance === 0) return [lat1, lon1];
1263
+ return vincentyDirect(lat1, lon1, initialBearing, distance / 2);
1219
1264
  }
1220
1265
  function horizonDistance(heightMeters) {
1221
1266
  if (heightMeters <= 0) return 0;
@@ -1228,18 +1273,36 @@ function horizonDistance(heightMeters) {
1228
1273
  // src/coordinates/projections.ts
1229
1274
  var e2 = WGS84_ECCENTRICITY_SQUARED;
1230
1275
  var k0 = 0.9996;
1276
+ var UTM_BANDS = "CDEFGHJKLMNPQRSTUVWX";
1277
+ function utmZone(latDeg, lonDeg) {
1278
+ let zone = Math.floor((lonDeg + 180) / 6) + 1;
1279
+ if (zone > 60) zone = 1;
1280
+ if (latDeg >= 56 && latDeg < 64 && lonDeg >= 3 && lonDeg < 12) return 32;
1281
+ if (latDeg >= 72 && latDeg <= 84) {
1282
+ if (lonDeg >= 0 && lonDeg < 9) return 31;
1283
+ if (lonDeg >= 9 && lonDeg < 21) return 33;
1284
+ if (lonDeg >= 21 && lonDeg < 33) return 35;
1285
+ if (lonDeg >= 33 && lonDeg < 42) return 37;
1286
+ }
1287
+ return zone;
1288
+ }
1231
1289
  function geodeticToUtm(lat, lon) {
1290
+ const rawLatDeg = lat * 180 / Math.PI;
1291
+ const inRange = rawLatDeg >= -80 && rawLatDeg <= 84;
1232
1292
  const MAX_UTM_LAT = 84 * (Math.PI / 180);
1233
1293
  lat = Math.max(-MAX_UTM_LAT, Math.min(MAX_UTM_LAT, lat));
1234
1294
  const latDeg = lat * 180 / Math.PI;
1295
+ lon = Math.atan2(Math.sin(lon), Math.cos(lon));
1235
1296
  const lonDeg = lon * 180 / Math.PI;
1236
- const zone = Math.floor((lonDeg + 180) / 6) + 1;
1297
+ const zone = utmZone(latDeg, lonDeg);
1298
+ const band = UTM_BANDS[Math.max(0, Math.min(19, Math.floor((latDeg + 80) / 8)))];
1237
1299
  const lon0 = ((zone - 1) * 6 - 180 + 3) * (Math.PI / 180);
1238
1300
  const ep2 = e2 / (1 - e2);
1239
1301
  const N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - e2 * Math.sin(lat) ** 2);
1240
1302
  const T = Math.tan(lat) ** 2;
1241
1303
  const C2 = ep2 * Math.cos(lat) ** 2;
1242
- const A = Math.cos(lat) * (lon - lon0);
1304
+ const dLon0 = Math.atan2(Math.sin(lon - lon0), Math.cos(lon - lon0));
1305
+ const A = Math.cos(lat) * dLon0;
1243
1306
  const M = WGS84_SEMI_MAJOR_AXIS * ((1 - e2 / 4 - 3 * e2 ** 2 / 64 - 5 * e2 ** 3 / 256) * lat - (3 * e2 / 8 + 3 * e2 ** 2 / 32 + 45 * e2 ** 3 / 1024) * Math.sin(2 * lat) + (15 * e2 ** 2 / 256 + 45 * e2 ** 3 / 1024) * Math.sin(4 * lat) - 35 * e2 ** 3 / 3072 * Math.sin(6 * lat));
1244
1307
  const easting = k0 * N * (A + (1 - T + C2) * A ** 3 / 6 + (5 - 18 * T + T ** 2 + 72 * C2 - 58 * ep2) * A ** 5 / 120) + 5e5;
1245
1308
  let northing = k0 * (M + N * Math.tan(lat) * (A ** 2 / 2 + (5 - T + 9 * C2 + 4 * C2 ** 2) * A ** 4 / 24 + (61 - 58 * T + T ** 2 + 600 * C2 - 330 * ep2) * A ** 6 / 720));
@@ -1247,7 +1310,7 @@ function geodeticToUtm(lat, lon) {
1247
1310
  if (hemisphere === "S") {
1248
1311
  northing += 1e7;
1249
1312
  }
1250
- return { easting, northing, zone, hemisphere };
1313
+ return { easting, northing, zone, hemisphere, band, inRange };
1251
1314
  }
1252
1315
  function geodeticToMaidenhead(lat, lon) {
1253
1316
  const latDeg = Math.max(0, Math.min(179.999999, lat * 180 / Math.PI + 90));
@@ -19692,6 +19755,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
19692
19755
  lambdaSearch,
19693
19756
  looksLikeRtcm2,
19694
19757
  maskRadForAzimuth,
19758
+ meridianArc,
19695
19759
  moonEcef,
19696
19760
  navTimesFromEph,
19697
19761
  niellMapping,
@@ -19786,6 +19850,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
19786
19850
  updateStreamStats,
19787
19851
  verifyChecksum,
19788
19852
  vincenty,
19853
+ vincentyDirect,
19789
19854
  wlWavelength,
19790
19855
  writeCrx,
19791
19856
  writeModernObsBlob,
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@ export { D as DAYS_MJD2000_MJD, a as DAYS_MJD_JULIAN, H as HourCode, M as MILLIS
3
3
  export { WGS84_ECCENTRICITY_SQUARED, WGS84_FLATTENING, WGS84_SEMI_MAJOR_AXIS, WGS84_SEMI_MINOR_AXIS } from './constants.cjs';
4
4
  export { A as ARC_GAP_FACTOR, B as BAND_LABELS, a as BDS_SATELLITES, b as BdsSatellite, C as C_LIGHT, D as DEFAULT_ELEV_MASK_DEG, c as DUAL_FREQ_PAIRS, F as FREQ, G as GLO_CHANNEL_FALLBACK, d as GLO_F1_BASE, e as GLO_F1_STEP, f as GLO_F2_BASE, g as GLO_F2_STEP, h as GLO_F3, O as OMEGA_E, i as OnSlipDetected, S as SYSTEM_NAMES, j as SYS_SHORT, k as buildGloChannelMap, l as buildObsIndices, m as formatUTCTime, n as getFreq, o as gloFreq } from './gnss-BtXrG3zH.cjs';
5
5
  export { Scale, getBdsTime, getDateFromBdsTime, getDateFromDayOfWeek, getDateFromDayOfYear, getDateFromGalTime, getDateFromGloN, getDateFromGpsData, getDateFromGpsTime, getDateFromHourCode, getDateFromJulianDate, getDateFromMJD, getDateFromMJD2000, getDateFromRINEX, getDateFromTai, getDateFromTimeOfDay, getDateFromTt, getDateFromUnixTime, getDateFromUtc, getDayOfWeek, getDayOfYear, getGalTime, getGloN4, getGloNA, getGpsLeap, getGpsTime, getHourCode, getHoursFromTimeDifference, getJulianDate, getLeap, getMJD, getMJD2000, getMinutesFromTimeDifference, getNtpTime, getRINEX, getSecondsFromTimeDifference, getTaiDate, getTimeDifference, getTimeDifferenceFromDays, getTimeDifferenceFromHours, getTimeDifferenceFromMinutes, getTimeDifferenceFromObject, getTimeDifferenceFromSeconds, getTimeOfDay, getTimeOfWeek, getTotalDaysFromTimeDifference, getTtDate, getUnixTime, getUtcDate, getWeekNumber, getWeekOfYear } from './time.cjs';
6
- export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, rad2deg, rhumbLine, vincenty } from './coordinates.cjs';
6
+ export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, meridianArc, rad2deg, rhumbLine, vincenty, vincentyDirect } from './coordinates.cjs';
7
7
  export { c as clampUnit, e as ecefToGeodetic, g as geodeticToEcef, a as getAer, b as getEnuDifference } from './ecef-CF0uAysr.cjs';
8
8
  export { BiasRecord, BiasSinex, CompactEpoch, CrxField, DiffState, EMPTY_WARNINGS, Nav4Input, RinexWarning, RinexWarnings, WarningAccumulator, WarningSeverity, WriteCrxOptions, biasMetres, createGzipLineSink, crxDecompress, crxRepair, findSatBias, fmtD, fmtF, hdrLine, padL, padR, parseBiasSinex, parseCrxDataLine, stationHeaderLines, writeCrx, writeModernObsBlob, writeRinex2ObsBlob, writeRinex4ObsBlob, writeRinexNav, writeRinexNav4, writeRinexObsBlob } from './rinex.cjs';
9
9
  export { E as EpochSummary, P as ParseOptions, R as RinexHeader, a as RinexResult, b as RinexStats, S as SYSTEM_ORDER, c as SatObsCallback, p as parseRinexStream, s as systemCmp, d as systemName } from './parser-JPjjFgeP.cjs';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { D as DAYS_MJD2000_MJD, a as DAYS_MJD_JULIAN, H as HourCode, M as MILLIS
3
3
  export { WGS84_ECCENTRICITY_SQUARED, WGS84_FLATTENING, WGS84_SEMI_MAJOR_AXIS, WGS84_SEMI_MINOR_AXIS } from './constants.js';
4
4
  export { A as ARC_GAP_FACTOR, B as BAND_LABELS, a as BDS_SATELLITES, b as BdsSatellite, C as C_LIGHT, D as DEFAULT_ELEV_MASK_DEG, c as DUAL_FREQ_PAIRS, F as FREQ, G as GLO_CHANNEL_FALLBACK, d as GLO_F1_BASE, e as GLO_F1_STEP, f as GLO_F2_BASE, g as GLO_F2_STEP, h as GLO_F3, O as OMEGA_E, i as OnSlipDetected, S as SYSTEM_NAMES, j as SYS_SHORT, k as buildGloChannelMap, l as buildObsIndices, m as formatUTCTime, n as getFreq, o as gloFreq } from './gnss-DhnEr0Dy.js';
5
5
  export { Scale, getBdsTime, getDateFromBdsTime, getDateFromDayOfWeek, getDateFromDayOfYear, getDateFromGalTime, getDateFromGloN, getDateFromGpsData, getDateFromGpsTime, getDateFromHourCode, getDateFromJulianDate, getDateFromMJD, getDateFromMJD2000, getDateFromRINEX, getDateFromTai, getDateFromTimeOfDay, getDateFromTt, getDateFromUnixTime, getDateFromUtc, getDayOfWeek, getDayOfYear, getGalTime, getGloN4, getGloNA, getGpsLeap, getGpsTime, getHourCode, getHoursFromTimeDifference, getJulianDate, getLeap, getMJD, getMJD2000, getMinutesFromTimeDifference, getNtpTime, getRINEX, getSecondsFromTimeDifference, getTaiDate, getTimeDifference, getTimeDifferenceFromDays, getTimeDifferenceFromHours, getTimeDifferenceFromMinutes, getTimeDifferenceFromObject, getTimeDifferenceFromSeconds, getTimeOfDay, getTimeOfWeek, getTotalDaysFromTimeDifference, getTtDate, getUnixTime, getUtcDate, getWeekNumber, getWeekOfYear } from './time.js';
6
- export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, rad2deg, rhumbLine, vincenty } from './coordinates.js';
6
+ export { deg2dms, deg2rad, euclidean3D, geodeticToGeohash, geodeticToMaidenhead, geodeticToUtm, greatCircleMidpoint, horizonDistance, meridianArc, rad2deg, rhumbLine, vincenty, vincentyDirect } from './coordinates.js';
7
7
  export { c as clampUnit, e as ecefToGeodetic, g as geodeticToEcef, a as getAer, b as getEnuDifference } from './ecef-CF0uAysr.js';
8
8
  export { BiasRecord, BiasSinex, CompactEpoch, CrxField, DiffState, EMPTY_WARNINGS, Nav4Input, RinexWarning, RinexWarnings, WarningAccumulator, WarningSeverity, WriteCrxOptions, biasMetres, createGzipLineSink, crxDecompress, crxRepair, findSatBias, fmtD, fmtF, hdrLine, padL, padR, parseBiasSinex, parseCrxDataLine, stationHeaderLines, writeCrx, writeModernObsBlob, writeRinex2ObsBlob, writeRinex4ObsBlob, writeRinexNav, writeRinexNav4, writeRinexObsBlob } from './rinex.js';
9
9
  export { E as EpochSummary, P as ParseOptions, R as RinexHeader, a as RinexResult, b as RinexStats, S as SYSTEM_ORDER, c as SatObsCallback, p as parseRinexStream, s as systemCmp, d as systemName } from './parser-JPjjFgeP.js';
package/dist/index.js CHANGED
@@ -280,10 +280,12 @@ import {
280
280
  geodeticToUtm,
281
281
  greatCircleMidpoint,
282
282
  horizonDistance,
283
+ meridianArc,
283
284
  rad2deg,
284
285
  rhumbLine,
285
- vincenty
286
- } from "./chunk-MIIM4LDY.js";
286
+ vincenty,
287
+ vincentyDirect
288
+ } from "./chunk-CVCTRIWU.js";
287
289
  import {
288
290
  clampUnit,
289
291
  ecefToGeodetic,
@@ -638,6 +640,7 @@ export {
638
640
  lambdaSearch,
639
641
  looksLikeRtcm2,
640
642
  maskRadForAzimuth,
643
+ meridianArc,
641
644
  moonEcef,
642
645
  navTimesFromEph,
643
646
  niellMapping,
@@ -732,6 +735,7 @@ export {
732
735
  updateStreamStats,
733
736
  verifyChecksum,
734
737
  vincenty,
738
+ vincentyDirect,
735
739
  wlWavelength,
736
740
  writeCrx,
737
741
  writeModernObsBlob,
@@ -44,7 +44,7 @@ import {
44
44
  import "./chunk-WQX354KD.js";
45
45
  import "./chunk-JYWYQ7DW.js";
46
46
  import "./chunk-HVXYFUCB.js";
47
- import "./chunk-MIIM4LDY.js";
47
+ import "./chunk-CVCTRIWU.js";
48
48
  import "./chunk-37QNKGTC.js";
49
49
  import "./chunk-LEEU5OIO.js";
50
50
  import "./chunk-6FAL6P4G.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Comprehensive GNSS library for JavaScript — time scales, coordinates, RINEX parsing, RTCM3 decoding, orbit computation, and signal analysis",
5
5
  "type": "module",
6
6
  "sideEffects": false,