gnss-js 2.6.0 → 2.8.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,
@@ -13,6 +13,30 @@ import {
13
13
  OMEGA_E
14
14
  } from "./chunk-FIEWO4J4.js";
15
15
 
16
+ // src/orbit/matrix.ts
17
+ function invertMatrix(A) {
18
+ const n = A.length;
19
+ const M = A.map((row, i) => [
20
+ ...row,
21
+ ...Array.from({ length: n }, (_, j) => i === j ? 1 : 0)
22
+ ]);
23
+ for (let col = 0; col < n; col++) {
24
+ let piv = col;
25
+ for (let r = col + 1; r < n; r++)
26
+ if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
27
+ if (Math.abs(M[piv][col]) < 1e-12) return null;
28
+ [M[col], M[piv]] = [M[piv], M[col]];
29
+ const d = M[col][col];
30
+ for (let j = 0; j < 2 * n; j++) M[col][j] /= d;
31
+ for (let r = 0; r < n; r++) {
32
+ if (r === col) continue;
33
+ const f = M[r][col];
34
+ for (let j = 0; j < 2 * n; j++) M[r][j] -= f * M[col][j];
35
+ }
36
+ }
37
+ return M.map((row) => row.slice(n));
38
+ }
39
+
16
40
  // src/orbit/almanac.ts
17
41
  var GM_GPS = 3986005e8;
18
42
  var GM_GAL = 3986004418e5;
@@ -382,15 +406,20 @@ function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
382
406
  }
383
407
  function computeDop(satAzEls) {
384
408
  const n = satAzEls.length;
385
- if (n < 4) return null;
409
+ const clocks = [...new Set(satAzEls.map((s) => s.prn?.[0] ?? ""))];
410
+ const m = 3 + clocks.length;
411
+ if (n < Math.max(4, m)) return null;
386
412
  const H = [];
387
- for (const { az, el } of satAzEls) {
413
+ for (const { az, el, prn } of satAzEls) {
388
414
  const cosEl = Math.cos(el);
389
- H.push([cosEl * Math.sin(az), cosEl * Math.cos(az), Math.sin(el), 1]);
415
+ const row = [cosEl * Math.sin(az), cosEl * Math.cos(az), Math.sin(el)];
416
+ const sys = prn?.[0] ?? "";
417
+ for (const c of clocks) row.push(c === sys ? 1 : 0);
418
+ H.push(row);
390
419
  }
391
- const HtH = Array.from({ length: 4 }, () => new Array(4).fill(0));
392
- for (let i = 0; i < 4; i++) {
393
- for (let j = 0; j < 4; j++) {
420
+ const HtH = Array.from({ length: m }, () => new Array(m).fill(0));
421
+ for (let i = 0; i < m; i++) {
422
+ for (let j = 0; j < m; j++) {
394
423
  let sum = 0;
395
424
  for (let k = 0; k < n; k++) {
396
425
  sum += H[k][i] * H[k][j];
@@ -398,46 +427,18 @@ function computeDop(satAzEls) {
398
427
  HtH[i][j] = sum;
399
428
  }
400
429
  }
401
- const Q = invert4x4(HtH);
430
+ const Q = invertMatrix(HtH);
402
431
  if (!Q) return null;
403
- const gdop = Math.sqrt(Q[0][0] + Q[1][1] + Q[2][2] + Q[3][3]);
404
- const pdop = Math.sqrt(Q[0][0] + Q[1][1] + Q[2][2]);
405
- const hdop = Math.sqrt(Q[0][0] + Q[1][1]);
406
- const vdop = Math.sqrt(Q[2][2]);
407
- return { gdop, pdop, hdop, vdop };
408
- }
409
- function invert4x4(m) {
410
- const a = m.flat();
411
- const inv = new Array(16).fill(0);
412
- inv[0] = a[5] * a[10] * a[15] - a[5] * a[11] * a[14] - a[9] * a[6] * a[15] + a[9] * a[7] * a[14] + a[13] * a[6] * a[11] - a[13] * a[7] * a[10];
413
- inv[4] = -a[4] * a[10] * a[15] + a[4] * a[11] * a[14] + a[8] * a[6] * a[15] - a[8] * a[7] * a[14] - a[12] * a[6] * a[11] + a[12] * a[7] * a[10];
414
- inv[8] = a[4] * a[9] * a[15] - a[4] * a[11] * a[13] - a[8] * a[5] * a[15] + a[8] * a[7] * a[13] + a[12] * a[5] * a[11] - a[12] * a[7] * a[9];
415
- inv[12] = -a[4] * a[9] * a[14] + a[4] * a[10] * a[13] + a[8] * a[5] * a[14] - a[8] * a[6] * a[13] - a[12] * a[5] * a[10] + a[12] * a[6] * a[9];
416
- inv[1] = -a[1] * a[10] * a[15] + a[1] * a[11] * a[14] + a[9] * a[2] * a[15] - a[9] * a[3] * a[14] - a[13] * a[2] * a[11] + a[13] * a[3] * a[10];
417
- inv[5] = a[0] * a[10] * a[15] - a[0] * a[11] * a[14] - a[8] * a[2] * a[15] + a[8] * a[3] * a[14] + a[12] * a[2] * a[11] - a[12] * a[3] * a[10];
418
- inv[9] = -a[0] * a[9] * a[15] + a[0] * a[11] * a[13] + a[8] * a[1] * a[15] - a[8] * a[3] * a[13] - a[12] * a[1] * a[11] + a[12] * a[3] * a[9];
419
- inv[13] = a[0] * a[9] * a[14] - a[0] * a[10] * a[13] - a[8] * a[1] * a[14] + a[8] * a[2] * a[13] + a[12] * a[1] * a[10] - a[12] * a[2] * a[9];
420
- inv[2] = a[1] * a[6] * a[15] - a[1] * a[7] * a[14] - a[5] * a[2] * a[15] + a[5] * a[3] * a[14] + a[13] * a[2] * a[7] - a[13] * a[3] * a[6];
421
- inv[6] = -a[0] * a[6] * a[15] + a[0] * a[7] * a[14] + a[4] * a[2] * a[15] - a[4] * a[3] * a[14] - a[12] * a[2] * a[7] + a[12] * a[3] * a[6];
422
- inv[10] = a[0] * a[5] * a[15] - a[0] * a[7] * a[13] - a[4] * a[1] * a[15] + a[4] * a[3] * a[13] + a[12] * a[1] * a[7] - a[12] * a[3] * a[5];
423
- inv[14] = -a[0] * a[5] * a[14] + a[0] * a[6] * a[13] + a[4] * a[1] * a[14] - a[4] * a[2] * a[13] - a[12] * a[1] * a[6] + a[12] * a[2] * a[5];
424
- inv[3] = -a[1] * a[6] * a[11] + a[1] * a[7] * a[10] + a[5] * a[2] * a[11] - a[5] * a[3] * a[10] - a[9] * a[2] * a[7] + a[9] * a[3] * a[6];
425
- inv[7] = a[0] * a[6] * a[11] - a[0] * a[7] * a[10] - a[4] * a[2] * a[11] + a[4] * a[3] * a[10] + a[8] * a[2] * a[7] - a[8] * a[3] * a[6];
426
- inv[11] = -a[0] * a[5] * a[11] + a[0] * a[7] * a[9] + a[4] * a[1] * a[11] - a[4] * a[3] * a[9] - a[8] * a[1] * a[7] + a[8] * a[3] * a[5];
427
- inv[15] = a[0] * a[5] * a[10] - a[0] * a[6] * a[9] - a[4] * a[1] * a[10] + a[4] * a[2] * a[9] + a[8] * a[1] * a[6] - a[8] * a[2] * a[5];
428
- let det = a[0] * inv[0] + a[1] * inv[4] + a[2] * inv[8] + a[3] * inv[12];
429
- if (Math.abs(det) < 1e-20) return null;
430
- det = 1 / det;
431
- const result = Array.from(
432
- { length: 4 },
433
- () => new Array(4).fill(0)
434
- );
435
- for (let i = 0; i < 4; i++) {
436
- for (let j = 0; j < 4; j++) {
437
- result[i][j] = inv[i * 4 + j] * det;
438
- }
439
- }
440
- return result;
432
+ let clockVar = 0;
433
+ for (let i = 3; i < m; i++) clockVar += Q[i][i];
434
+ const posVar = Q[0][0] + Q[1][1] + Q[2][2];
435
+ return {
436
+ gdop: Math.sqrt(posVar + clockVar),
437
+ pdop: Math.sqrt(posVar),
438
+ hdop: Math.sqrt(Q[0][0] + Q[1][1]),
439
+ vdop: Math.sqrt(Q[2][2]),
440
+ tdop: Math.sqrt(clockVar)
441
+ };
441
442
  }
442
443
  function selectEphemeris(ephemerides, prn, timeMs) {
443
444
  return selectBest(
@@ -697,6 +698,7 @@ function computeVisibilityFromPositions(all, elevationMaskDeg = 10) {
697
698
  const gdop = new Array(times.length).fill(null);
698
699
  const hdop = new Array(times.length).fill(null);
699
700
  const vdop = new Array(times.length).fill(null);
701
+ const tdop = new Array(times.length).fill(null);
700
702
  const visiblePerEpoch = times.map(() => []);
701
703
  for (const prn of all.prns) {
702
704
  const series = all.positions[prn];
@@ -710,7 +712,7 @@ function computeVisibilityFromPositions(all, elevationMaskDeg = 10) {
710
712
  visibleCount[i]++;
711
713
  const sys = prn[0];
712
714
  (visibleBySystem[sys] ??= new Array(times.length).fill(0))[i]++;
713
- visiblePerEpoch[i].push({ az: p.az, el: p.el });
715
+ visiblePerEpoch[i].push({ az: p.az, el: p.el, prn });
714
716
  }
715
717
  }
716
718
  }
@@ -720,6 +722,7 @@ function computeVisibilityFromPositions(all, elevationMaskDeg = 10) {
720
722
  gdop[i] = dop ? dop.gdop : null;
721
723
  hdop[i] = dop ? dop.hdop : null;
722
724
  vdop[i] = dop ? dop.vdop : null;
725
+ tdop[i] = dop ? dop.tdop : null;
723
726
  }
724
727
  const passes = [];
725
728
  for (const prn of all.prns) {
@@ -766,11 +769,13 @@ function computeVisibilityFromPositions(all, elevationMaskDeg = 10) {
766
769
  gdop,
767
770
  hdop,
768
771
  vdop,
772
+ tdop,
769
773
  passes
770
774
  };
771
775
  }
772
776
 
773
777
  export {
778
+ invertMatrix,
774
779
  almanacEpochMs,
775
780
  almanacSatPosition,
776
781
  glonassAlmanacEpochMs,
@@ -5,8 +5,9 @@ import {
5
5
  computeDop,
6
6
  computeSatPosition,
7
7
  ecefToAzEl,
8
+ invertMatrix,
8
9
  selectEphemeris
9
- } from "./chunk-JYWYQ7DW.js";
10
+ } from "./chunk-GLMQRZDL.js";
10
11
  import {
11
12
  ecefToGeodetic,
12
13
  getAer,
@@ -3730,28 +3731,6 @@ function sbasAirTropoVar(elRad) {
3730
3731
  const sigTropo = 0.12 * m;
3731
3732
  return sigMp * sigMp + NOISE_DIVG_AADA * NOISE_DIVG_AADA + sigTropo * sigTropo;
3732
3733
  }
3733
- function invertMatrix(A) {
3734
- const n = A.length;
3735
- const M = A.map((row, i) => [
3736
- ...row,
3737
- ...Array.from({ length: n }, (_, j) => i === j ? 1 : 0)
3738
- ]);
3739
- for (let col = 0; col < n; col++) {
3740
- let piv = col;
3741
- for (let r = col + 1; r < n; r++)
3742
- if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
3743
- if (Math.abs(M[piv][col]) < 1e-12) return null;
3744
- [M[col], M[piv]] = [M[piv], M[col]];
3745
- const d = M[col][col];
3746
- for (let j = 0; j < 2 * n; j++) M[col][j] /= d;
3747
- for (let r = 0; r < n; r++) {
3748
- if (r === col) continue;
3749
- const f = M[r][col];
3750
- for (let j = 0; j < 2 * n; j++) M[r][j] -= f * M[col][j];
3751
- }
3752
- }
3753
- return M.map((row) => row.slice(n));
3754
- }
3755
3734
  function sbasProtectionLevels(sats, opts = {}) {
3756
3735
  const kH = opts.kH ?? 6;
3757
3736
  const kV = opts.kV ?? 5.33;
@@ -3978,7 +3957,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3978
3957
  const azel = ecefToAzEl(x, y, z, sat.x, sat.y, sat.z);
3979
3958
  if (azel.el >= elevationMaskDeg * Math.PI / 180) {
3980
3959
  used.push(prn);
3981
- azels.push(azel);
3960
+ azels.push({ ...azel, prn });
3982
3961
  if (sbas) {
3983
3962
  const dx = sat.x - x, dy = sat.y - y, dz = sat.z - z;
3984
3963
  const dr = Math.hypot(dx, dy, dz) || 1;
@@ -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 };