gnss-js 2.5.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.
- package/dist/{chunk-B2XWO4RP.js → chunk-BWLLHLLS.js} +158 -0
- package/dist/{chunk-MIIM4LDY.js → chunk-CVCTRIWU.js} +87 -24
- package/dist/coordinates.cjs +91 -26
- package/dist/coordinates.d.cts +26 -3
- package/dist/coordinates.d.ts +26 -3
- package/dist/coordinates.js +7 -3
- package/dist/index.cjs +250 -27
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -3
- package/dist/positioning.cjs +180 -0
- package/dist/positioning.d.cts +56 -1
- package/dist/positioning.d.ts +56 -1
- package/dist/positioning.js +4 -2
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./chunk-V2D5KUKH.js";
|
|
23
23
|
import {
|
|
24
24
|
C_LIGHT,
|
|
25
|
+
FREQ,
|
|
25
26
|
OMEGA_E
|
|
26
27
|
} from "./chunk-FIEWO4J4.js";
|
|
27
28
|
|
|
@@ -1463,6 +1464,162 @@ function postProcessRtk(base, rover, ephemerides, baseEcef, opts = {}) {
|
|
|
1463
1464
|
return { track, solved, unmatched, fixRate: solved ? fixed / solved : 0 };
|
|
1464
1465
|
}
|
|
1465
1466
|
|
|
1467
|
+
// src/positioning/zero-baseline.ts
|
|
1468
|
+
function carrierFreqHz2(sys, band, gloChannel) {
|
|
1469
|
+
if (sys === "R") {
|
|
1470
|
+
if (band === "1") return 1602e6 + (gloChannel ?? 0) * 562500;
|
|
1471
|
+
if (band === "2") return 1246e6 + (gloChannel ?? 0) * 437500;
|
|
1472
|
+
return FREQ.R?.[band] ?? 0;
|
|
1473
|
+
}
|
|
1474
|
+
return FREQ[sys]?.[band] ?? 0;
|
|
1475
|
+
}
|
|
1476
|
+
function median(a) {
|
|
1477
|
+
if (a.length === 0) return 0;
|
|
1478
|
+
const s = [...a].sort((x, y) => x - y);
|
|
1479
|
+
const m = s.length >> 1;
|
|
1480
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
1481
|
+
}
|
|
1482
|
+
var ZeroBaselineEngine = class {
|
|
1483
|
+
ref;
|
|
1484
|
+
refSys;
|
|
1485
|
+
slip;
|
|
1486
|
+
/** Per (receiver|prn|code) continuous phase-SD arc. */
|
|
1487
|
+
arcs = /* @__PURE__ */ new Map();
|
|
1488
|
+
/** Per receiver: integrated phase-smoothed clock (m). */
|
|
1489
|
+
phaseClock = /* @__PURE__ */ new Map();
|
|
1490
|
+
/** Per (receiver|system): accumulated ISB (m) + count. */
|
|
1491
|
+
isb = /* @__PURE__ */ new Map();
|
|
1492
|
+
constructor(opts) {
|
|
1493
|
+
this.ref = opts.reference;
|
|
1494
|
+
this.refSys = opts.referenceSystem ?? "G";
|
|
1495
|
+
this.slip = opts.slipThreshM ?? 0.05;
|
|
1496
|
+
}
|
|
1497
|
+
reset() {
|
|
1498
|
+
this.arcs.clear();
|
|
1499
|
+
this.phaseClock.clear();
|
|
1500
|
+
this.isb.clear();
|
|
1501
|
+
}
|
|
1502
|
+
/** ISB/IFB estimates accumulated so far, per receiver × system. */
|
|
1503
|
+
biases() {
|
|
1504
|
+
const out = [];
|
|
1505
|
+
for (const [key, { sum, n }] of this.isb) {
|
|
1506
|
+
const [receiver, system] = key.split("|");
|
|
1507
|
+
out.push({ receiver, system, biasM: sum / n, n });
|
|
1508
|
+
}
|
|
1509
|
+
return out;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Ingest one time-aligned instant: observations per receiver id. Returns one
|
|
1513
|
+
* {@link ReceiverClockSample} per non-reference receiver present this epoch.
|
|
1514
|
+
*/
|
|
1515
|
+
process(epoch) {
|
|
1516
|
+
const refObs = epoch.obs[this.ref];
|
|
1517
|
+
if (!refObs) return [];
|
|
1518
|
+
const refByKey = /* @__PURE__ */ new Map();
|
|
1519
|
+
for (const o of refObs)
|
|
1520
|
+
if (o.pr != null && Number.isFinite(o.pr))
|
|
1521
|
+
refByKey.set(`${o.prn}|${o.code}`, o);
|
|
1522
|
+
const out = [];
|
|
1523
|
+
for (const rx of Object.keys(epoch.obs)) {
|
|
1524
|
+
if (rx === this.ref) continue;
|
|
1525
|
+
out.push(
|
|
1526
|
+
this.processReceiver(rx, epoch.obs[rx], refByKey, epoch.timeMs)
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
return out;
|
|
1530
|
+
}
|
|
1531
|
+
processReceiver(rx, obs, refByKey, t) {
|
|
1532
|
+
const codeBySys = /* @__PURE__ */ new Map();
|
|
1533
|
+
const phase = [];
|
|
1534
|
+
for (const o of obs) {
|
|
1535
|
+
if (o.pr == null || !Number.isFinite(o.pr)) continue;
|
|
1536
|
+
const r = refByKey.get(`${o.prn}|${o.code}`);
|
|
1537
|
+
if (!r || r.pr == null) continue;
|
|
1538
|
+
const sys = o.prn[0];
|
|
1539
|
+
(codeBySys.get(sys) ?? codeBySys.set(sys, []).get(sys)).push(
|
|
1540
|
+
o.pr - r.pr
|
|
1541
|
+
);
|
|
1542
|
+
if (o.cp != null && r.cp != null) {
|
|
1543
|
+
const f = carrierFreqHz2(
|
|
1544
|
+
sys,
|
|
1545
|
+
o.code[0],
|
|
1546
|
+
o.gloChannel ?? r.gloChannel ?? null
|
|
1547
|
+
);
|
|
1548
|
+
if (f > 0)
|
|
1549
|
+
phase.push({
|
|
1550
|
+
key: `${rx}|${o.prn}|${o.code}`,
|
|
1551
|
+
sdM: (o.cp - r.cp) * (C_LIGHT / f),
|
|
1552
|
+
lock: o.lockTimeS
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (codeBySys.size === 0)
|
|
1557
|
+
return {
|
|
1558
|
+
t,
|
|
1559
|
+
receiver: rx,
|
|
1560
|
+
clockOffsetM: null,
|
|
1561
|
+
clockOffsetPhaseM: this.phaseClock.get(rx) ?? null,
|
|
1562
|
+
used: 0,
|
|
1563
|
+
rejected: 0,
|
|
1564
|
+
residualRmsM: 0
|
|
1565
|
+
};
|
|
1566
|
+
const sysMedian = /* @__PURE__ */ new Map();
|
|
1567
|
+
for (const [sys, v] of codeBySys) sysMedian.set(sys, median(v));
|
|
1568
|
+
const allSd = [];
|
|
1569
|
+
for (const v of codeBySys.values()) allSd.push(...v);
|
|
1570
|
+
const clk = sysMedian.get(this.refSys) ?? median(allSd);
|
|
1571
|
+
for (const [sys, med] of sysMedian) {
|
|
1572
|
+
if (sys === this.refSys) continue;
|
|
1573
|
+
const cur = this.isb.get(`${rx}|${sys}`) ?? { sum: 0, n: 0 };
|
|
1574
|
+
cur.sum += med - clk;
|
|
1575
|
+
cur.n += 1;
|
|
1576
|
+
this.isb.set(`${rx}|${sys}`, cur);
|
|
1577
|
+
}
|
|
1578
|
+
let ss = 0, used = 0, rejected = 0;
|
|
1579
|
+
for (const [sys, v] of codeBySys) {
|
|
1580
|
+
const isb = sys === this.refSys ? 0 : sysMedian.get(sys) - clk;
|
|
1581
|
+
for (const sd of v) {
|
|
1582
|
+
const res = sd - clk - isb;
|
|
1583
|
+
if (Math.abs(res) > 5) {
|
|
1584
|
+
rejected++;
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
ss += res * res;
|
|
1588
|
+
used++;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const residualRmsM = used ? Math.sqrt(ss / used) : 0;
|
|
1592
|
+
const deltas = [];
|
|
1593
|
+
for (const p of phase) {
|
|
1594
|
+
const arc = this.arcs.get(p.key);
|
|
1595
|
+
this.arcs.set(p.key, { lastPhaseSdM: p.sdM, lastLock: p.lock });
|
|
1596
|
+
if (!arc) continue;
|
|
1597
|
+
const lockReset = p.lock != null && arc.lastLock != null && p.lock < arc.lastLock;
|
|
1598
|
+
if (lockReset) continue;
|
|
1599
|
+
deltas.push(p.sdM - arc.lastPhaseSdM);
|
|
1600
|
+
}
|
|
1601
|
+
let phaseClockM = this.phaseClock.get(rx) ?? null;
|
|
1602
|
+
if (phaseClockM == null) {
|
|
1603
|
+
phaseClockM = clk;
|
|
1604
|
+
} else if (deltas.length) {
|
|
1605
|
+
const md = median(deltas);
|
|
1606
|
+
const good = deltas.filter((d) => Math.abs(d - md) < this.slip);
|
|
1607
|
+
const step = good.length ? good.reduce((a, b) => a + b, 0) / good.length : md;
|
|
1608
|
+
phaseClockM += step;
|
|
1609
|
+
}
|
|
1610
|
+
this.phaseClock.set(rx, phaseClockM);
|
|
1611
|
+
return {
|
|
1612
|
+
t,
|
|
1613
|
+
receiver: rx,
|
|
1614
|
+
clockOffsetM: clk,
|
|
1615
|
+
clockOffsetPhaseM: phaseClockM,
|
|
1616
|
+
used,
|
|
1617
|
+
rejected,
|
|
1618
|
+
residualRmsM
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1466
1623
|
// src/positioning/sbas.ts
|
|
1467
1624
|
var P2_11 = 2 ** -11;
|
|
1468
1625
|
var P2_31 = 2 ** -31;
|
|
@@ -3874,6 +4031,7 @@ export {
|
|
|
3874
4031
|
solveDgnss,
|
|
3875
4032
|
RtkFloatEngine,
|
|
3876
4033
|
postProcessRtk,
|
|
4034
|
+
ZeroBaselineEngine,
|
|
3877
4035
|
sbasLongTermDeg,
|
|
3878
4036
|
sbasIonoDeg,
|
|
3879
4037
|
SbasProcessor,
|
|
@@ -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 {
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
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
|
|
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,
|
package/dist/coordinates.cjs
CHANGED
|
@@ -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 {
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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 =
|
|
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
|
|
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
|
});
|
package/dist/coordinates.d.cts
CHANGED
|
@@ -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
|
-
*
|
|
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 };
|