gnss-js 1.21.0 → 1.23.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-CUWF56ST.js +966 -0
- package/dist/{chunk-DLXTTHRK.js → chunk-MCAVU3XL.js} +118 -9
- package/dist/{chunk-JNFV5BYB.js → chunk-MHGNKSSH.js} +2 -3
- package/dist/{chunk-K3V3NHWL.js → chunk-PZNVFCKJ.js} +25 -766
- package/dist/chunk-VTX3VCL4.js +769 -0
- package/dist/index.cjs +1906 -1099
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -5
- package/dist/orbit.cjs +2 -3
- package/dist/orbit.js +1 -1
- package/dist/positioning.cjs +734 -9
- package/dist/positioning.d.cts +238 -1
- package/dist/positioning.d.ts +238 -1
- package/dist/positioning.js +10 -4
- package/dist/sbf.js +2 -1
- package/dist/ubx.cjs +809 -10
- package/dist/ubx.d.cts +111 -1
- package/dist/ubx.d.ts +111 -1
- package/dist/ubx.js +6 -1
- package/package.json +1 -1
- package/dist/chunk-B5LU4746.js +0 -262
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeDop,
|
|
3
|
+
computeSatPosition,
|
|
4
|
+
ecefToAzEl,
|
|
5
|
+
selectEphemeris
|
|
6
|
+
} from "./chunk-MHGNKSSH.js";
|
|
7
|
+
import {
|
|
8
|
+
ecefToGeodetic
|
|
9
|
+
} from "./chunk-37QNKGTC.js";
|
|
10
|
+
import {
|
|
11
|
+
C_LIGHT,
|
|
12
|
+
OMEGA_E
|
|
13
|
+
} from "./chunk-FIEWO4J4.js";
|
|
14
|
+
|
|
15
|
+
// src/positioning/klobuchar.ts
|
|
16
|
+
function klobucharDelay(coeffs, latRad, lonRad, azRad, elRad, gpsTowSec) {
|
|
17
|
+
const { alpha, beta } = coeffs;
|
|
18
|
+
if (alpha.length < 4 || beta.length < 4) return 0;
|
|
19
|
+
const E = elRad / Math.PI;
|
|
20
|
+
const phiU = latRad / Math.PI;
|
|
21
|
+
const lambdaU = lonRad / Math.PI;
|
|
22
|
+
const psi = 0.0137 / (E + 0.11) - 0.022;
|
|
23
|
+
let phiI = phiU + psi * Math.cos(azRad);
|
|
24
|
+
if (phiI > 0.416) phiI = 0.416;
|
|
25
|
+
else if (phiI < -0.416) phiI = -0.416;
|
|
26
|
+
const lambdaI = lambdaU + psi * Math.sin(azRad) / Math.cos(phiI * Math.PI);
|
|
27
|
+
const phiM = phiI + 0.064 * Math.cos((lambdaI - 1.617) * Math.PI);
|
|
28
|
+
let t = 43200 * lambdaI + gpsTowSec;
|
|
29
|
+
t %= 86400;
|
|
30
|
+
if (t < 0) t += 86400;
|
|
31
|
+
let amp = 0;
|
|
32
|
+
let per = 0;
|
|
33
|
+
for (let n = 3; n >= 0; n--) {
|
|
34
|
+
amp = amp * phiM + alpha[n];
|
|
35
|
+
per = per * phiM + beta[n];
|
|
36
|
+
}
|
|
37
|
+
if (amp < 0) amp = 0;
|
|
38
|
+
if (per < 72e3) per = 72e3;
|
|
39
|
+
const F = 1 + 16 * Math.pow(0.53 - E, 3);
|
|
40
|
+
const x = 2 * Math.PI * (t - 50400) / per;
|
|
41
|
+
if (Math.abs(x) >= 1.57) return F * 5e-9;
|
|
42
|
+
const x2 = x * x;
|
|
43
|
+
return F * (5e-9 + amp * (1 - x2 / 2 + x2 * x2 / 24));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/positioning/rtk.ts
|
|
47
|
+
var L1_CODES = {
|
|
48
|
+
G: ["1C"],
|
|
49
|
+
E: ["1C"],
|
|
50
|
+
R: ["1C"],
|
|
51
|
+
J: ["1C"],
|
|
52
|
+
C: ["2I", "1P"]
|
|
53
|
+
// B1I first (classic B1), then B1C
|
|
54
|
+
};
|
|
55
|
+
function toRtkEpoch(meas) {
|
|
56
|
+
const out = /* @__PURE__ */ new Map();
|
|
57
|
+
const rank = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const m of meas) {
|
|
59
|
+
const prefs = L1_CODES[m.prn[0]];
|
|
60
|
+
if (!prefs || m.pr === null || !Number.isFinite(m.pr)) continue;
|
|
61
|
+
const r = prefs.indexOf(m.code);
|
|
62
|
+
if (r < 0) continue;
|
|
63
|
+
const prev = rank.get(m.prn);
|
|
64
|
+
if (prev !== void 0 && prev <= r) continue;
|
|
65
|
+
rank.set(m.prn, r);
|
|
66
|
+
out.set(m.prn, {
|
|
67
|
+
code: m.code,
|
|
68
|
+
pr: m.pr,
|
|
69
|
+
cp: m.cp ?? null,
|
|
70
|
+
lockTimeMs: m.lockTimeS !== void 0 ? Math.round(m.lockTimeS * 1e3) : void 0,
|
|
71
|
+
gloChannel: m.gloChannel ?? null
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function carrierFreqHz(sys, code, gloChannel) {
|
|
77
|
+
const band = code[0];
|
|
78
|
+
switch (sys) {
|
|
79
|
+
case "G":
|
|
80
|
+
case "J":
|
|
81
|
+
return band === "1" ? 157542e4 : band === "2" ? 12276e5 : 117645e4;
|
|
82
|
+
case "E":
|
|
83
|
+
return band === "1" ? 157542e4 : band === "5" ? 117645e4 : band === "7" ? 120714e4 : band === "8" ? 1191795e3 : 127875e4;
|
|
84
|
+
case "C":
|
|
85
|
+
return band === "1" ? 157542e4 : band === "2" ? 1561098e3 : band === "5" ? 117645e4 : band === "7" ? 120714e4 : 126852e4;
|
|
86
|
+
case "R": {
|
|
87
|
+
if (gloChannel === null) return 0;
|
|
88
|
+
if (band === "1") return 1602e6 + gloChannel * 562500;
|
|
89
|
+
if (band === "2") return 1246e6 + gloChannel * 437500;
|
|
90
|
+
return 1202025e3;
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function sagnac(pos, travelTimeS) {
|
|
97
|
+
const theta = OMEGA_E * travelTimeS;
|
|
98
|
+
const c = Math.cos(theta);
|
|
99
|
+
const s = Math.sin(theta);
|
|
100
|
+
return [pos.x * c + pos.y * s, -pos.x * s + pos.y * c, pos.z];
|
|
101
|
+
}
|
|
102
|
+
function tropoDelay(elevationRad) {
|
|
103
|
+
const sinEl = Math.sin(elevationRad);
|
|
104
|
+
return 2.47 / (sinEl + 0.0121);
|
|
105
|
+
}
|
|
106
|
+
function resolveEphemeris(src, prn, timeMs) {
|
|
107
|
+
if (Array.isArray(src))
|
|
108
|
+
return selectEphemeris(src, prn, timeMs);
|
|
109
|
+
return src.get(prn) ?? null;
|
|
110
|
+
}
|
|
111
|
+
function buildGeometry(rover, base, basePos, ephemerides, timeMs, maskRad, troposphere) {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const [prn, mR] of rover) {
|
|
114
|
+
const sys = prn[0];
|
|
115
|
+
if (!"GERCJ".includes(sys)) continue;
|
|
116
|
+
const mB = base.get(prn);
|
|
117
|
+
if (!mB || mB.code !== mR.code) continue;
|
|
118
|
+
if (mR.pr === null || mB.pr === null || !Number.isFinite(mR.pr) || !Number.isFinite(mB.pr))
|
|
119
|
+
continue;
|
|
120
|
+
const eph = resolveEphemeris(ephemerides, prn, timeMs);
|
|
121
|
+
if (!eph) continue;
|
|
122
|
+
const satAt = (pr) => {
|
|
123
|
+
const tTx = timeMs - pr / C_LIGHT * 1e3;
|
|
124
|
+
const dts = satClockCorrection(eph, tTx);
|
|
125
|
+
const sat = computeSatPosition(eph, tTx - dts * 1e3);
|
|
126
|
+
return Number.isFinite(sat.x) ? sat : null;
|
|
127
|
+
};
|
|
128
|
+
const satB = satAt(mB.pr);
|
|
129
|
+
const satR = satAt(mR.pr);
|
|
130
|
+
if (!satB || !satR) continue;
|
|
131
|
+
const travelB = Math.hypot(
|
|
132
|
+
satB.x - basePos[0],
|
|
133
|
+
satB.y - basePos[1],
|
|
134
|
+
satB.z - basePos[2]
|
|
135
|
+
) / C_LIGHT;
|
|
136
|
+
const [bx, by, bz] = sagnac(satB, travelB);
|
|
137
|
+
const rhoB = Math.hypot(bx - basePos[0], by - basePos[1], bz - basePos[2]);
|
|
138
|
+
const elB = ecefToAzEl(basePos[0], basePos[1], basePos[2], bx, by, bz).el;
|
|
139
|
+
if (elB < maskRad) continue;
|
|
140
|
+
const gloK = sys === "R" ? mR.gloChannel ?? mB.gloChannel ?? eph.freqNum ?? null : null;
|
|
141
|
+
const freq = carrierFreqHz(sys, mR.code, gloK);
|
|
142
|
+
out.push({
|
|
143
|
+
prn,
|
|
144
|
+
group: sys + mR.code,
|
|
145
|
+
lambda: freq > 0 ? C_LIGHT / freq : 0,
|
|
146
|
+
satR,
|
|
147
|
+
rhoB,
|
|
148
|
+
elB,
|
|
149
|
+
tropoB: troposphere ? tropoDelay(elB) : 0,
|
|
150
|
+
prR: mR.pr,
|
|
151
|
+
prB: mB.pr,
|
|
152
|
+
cpR: mR.cp ?? null,
|
|
153
|
+
cpB: mB.cp ?? null,
|
|
154
|
+
lockR: mR.lockTimeMs,
|
|
155
|
+
lockB: mB.lockTimeMs
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function roverTerms(g, x, y, z, troposphere) {
|
|
161
|
+
const travel = Math.hypot(g.satR.x - x, g.satR.y - y, g.satR.z - z) / C_LIGHT;
|
|
162
|
+
const [sx, sy, sz] = sagnac(g.satR, travel);
|
|
163
|
+
const rho = Math.hypot(sx - x, sy - y, sz - z);
|
|
164
|
+
const u = [
|
|
165
|
+
(x - sx) / rho,
|
|
166
|
+
(y - sy) / rho,
|
|
167
|
+
(z - sz) / rho
|
|
168
|
+
];
|
|
169
|
+
let dTropo = 0;
|
|
170
|
+
if (troposphere) {
|
|
171
|
+
const elR = ecefToAzEl(x, y, z, sx, sy, sz).el;
|
|
172
|
+
dTropo = tropoDelay(Math.max(elR, 0.05)) - g.tropoB;
|
|
173
|
+
}
|
|
174
|
+
return { rho, u, dTropo };
|
|
175
|
+
}
|
|
176
|
+
function sdVariance(sigmaM, elRad) {
|
|
177
|
+
const sinEl = Math.max(Math.sin(elRad), 0.05);
|
|
178
|
+
return 2 * sigmaM * sigmaM * (1 + 1 / (sinEl * sinEl));
|
|
179
|
+
}
|
|
180
|
+
function zeros(n, m) {
|
|
181
|
+
return Array.from({ length: n }, () => new Array(m).fill(0));
|
|
182
|
+
}
|
|
183
|
+
function matInv(A) {
|
|
184
|
+
const n = A.length;
|
|
185
|
+
const M = A.map((row, i) => {
|
|
186
|
+
const r = [...row, ...new Array(n).fill(0)];
|
|
187
|
+
r[n + i] = 1;
|
|
188
|
+
return r;
|
|
189
|
+
});
|
|
190
|
+
for (let col = 0; col < n; col++) {
|
|
191
|
+
let pivot = col;
|
|
192
|
+
for (let r = col + 1; r < n; r++) {
|
|
193
|
+
if (Math.abs(M[r][col]) > Math.abs(M[pivot][col])) pivot = r;
|
|
194
|
+
}
|
|
195
|
+
if (Math.abs(M[pivot][col]) < 1e-13) return null;
|
|
196
|
+
[M[col], M[pivot]] = [M[pivot], M[col]];
|
|
197
|
+
const d = M[col][col];
|
|
198
|
+
for (let c = 0; c < 2 * n; c++) M[col][c] /= d;
|
|
199
|
+
for (let r = 0; r < n; r++) {
|
|
200
|
+
if (r === col) continue;
|
|
201
|
+
const f = M[r][col];
|
|
202
|
+
if (f === 0) continue;
|
|
203
|
+
for (let c = 0; c < 2 * n; c++) M[r][c] -= f * M[col][c];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return M.map((row) => row.slice(n));
|
|
207
|
+
}
|
|
208
|
+
function matMul(A, B) {
|
|
209
|
+
const n = A.length;
|
|
210
|
+
const k = B.length;
|
|
211
|
+
const m = B[0]?.length ?? 0;
|
|
212
|
+
const C = zeros(n, m);
|
|
213
|
+
for (let i = 0; i < n; i++) {
|
|
214
|
+
for (let l = 0; l < k; l++) {
|
|
215
|
+
const a = A[i][l];
|
|
216
|
+
if (a === 0) continue;
|
|
217
|
+
for (let j = 0; j < m; j++) C[i][j] += a * B[l][j];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return C;
|
|
221
|
+
}
|
|
222
|
+
function transpose(A) {
|
|
223
|
+
const n = A.length;
|
|
224
|
+
const m = A[0]?.length ?? 0;
|
|
225
|
+
const T = zeros(m, n);
|
|
226
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) T[j][i] = A[i][j];
|
|
227
|
+
return T;
|
|
228
|
+
}
|
|
229
|
+
function groupEntries(geom) {
|
|
230
|
+
const groups = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const g of geom) {
|
|
232
|
+
const list = groups.get(g.group);
|
|
233
|
+
if (list) list.push(g);
|
|
234
|
+
else groups.set(g.group, [g]);
|
|
235
|
+
}
|
|
236
|
+
return groups;
|
|
237
|
+
}
|
|
238
|
+
function ddCovariance(rows, sigmaCode, sigmaPhase) {
|
|
239
|
+
const n = rows.length;
|
|
240
|
+
const R = zeros(n, n);
|
|
241
|
+
const sd = (r, g) => sdVariance(r.kind === "code" ? sigmaCode : sigmaPhase, g.elB);
|
|
242
|
+
for (let i = 0; i < n; i++) {
|
|
243
|
+
const ri = rows[i];
|
|
244
|
+
R[i][i] = sd(ri, ri.g) + sd(ri, ri.ref);
|
|
245
|
+
for (let j = i + 1; j < n; j++) {
|
|
246
|
+
const rj = rows[j];
|
|
247
|
+
if (ri.ref.prn === rj.ref.prn && ri.kind === rj.kind && ri.g.group === rj.g.group) {
|
|
248
|
+
R[i][j] = R[j][i] = sd(ri, ri.ref);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return R;
|
|
253
|
+
}
|
|
254
|
+
function solveDgnss(rover, base, basePos, ephemerides, timeMs, opts = {}) {
|
|
255
|
+
const {
|
|
256
|
+
elevationMaskDeg = 10,
|
|
257
|
+
troposphere = true,
|
|
258
|
+
maxIterations = 15,
|
|
259
|
+
convergenceM = 1e-4,
|
|
260
|
+
codeSigmaM = 0.3,
|
|
261
|
+
rejectThresholdM = 20,
|
|
262
|
+
initialPosition
|
|
263
|
+
} = opts;
|
|
264
|
+
let geom = buildGeometry(
|
|
265
|
+
rover,
|
|
266
|
+
base,
|
|
267
|
+
basePos,
|
|
268
|
+
ephemerides,
|
|
269
|
+
timeMs,
|
|
270
|
+
elevationMaskDeg * Math.PI / 180,
|
|
271
|
+
troposphere
|
|
272
|
+
);
|
|
273
|
+
const rejected = [];
|
|
274
|
+
for (; ; ) {
|
|
275
|
+
const groups = groupEntries(geom);
|
|
276
|
+
const rows = [];
|
|
277
|
+
const refSatellites = {};
|
|
278
|
+
for (const [group, list] of groups) {
|
|
279
|
+
if (list.length < 2) continue;
|
|
280
|
+
const ref = list.reduce((a, b) => b.elB > a.elB ? b : a);
|
|
281
|
+
refSatellites[group] = ref.prn;
|
|
282
|
+
for (const g of list) {
|
|
283
|
+
if (g === ref) continue;
|
|
284
|
+
rows.push({
|
|
285
|
+
g,
|
|
286
|
+
ref,
|
|
287
|
+
kind: "code",
|
|
288
|
+
z: g.prR - g.prB - (ref.prR - ref.prB)
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (rows.length < 3) return null;
|
|
293
|
+
const W = matInv(ddCovariance(rows, codeSigmaM, codeSigmaM));
|
|
294
|
+
if (!W) return null;
|
|
295
|
+
let [x, y, z] = initialPosition ?? basePos;
|
|
296
|
+
let iterations = 0;
|
|
297
|
+
let converged = false;
|
|
298
|
+
const residuals = {};
|
|
299
|
+
let cov = null;
|
|
300
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
301
|
+
iterations = iter + 1;
|
|
302
|
+
const H = [];
|
|
303
|
+
const v = [];
|
|
304
|
+
const terms = /* @__PURE__ */ new Map();
|
|
305
|
+
const termOf = (g) => {
|
|
306
|
+
let t = terms.get(g.prn);
|
|
307
|
+
if (!t) {
|
|
308
|
+
t = roverTerms(g, x, y, z, troposphere);
|
|
309
|
+
terms.set(g.prn, t);
|
|
310
|
+
}
|
|
311
|
+
return t;
|
|
312
|
+
};
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
const ts = termOf(row.g);
|
|
315
|
+
const tr = termOf(row.ref);
|
|
316
|
+
const pred = ts.rho - row.g.rhoB + ts.dTropo - (tr.rho - row.ref.rhoB + tr.dTropo);
|
|
317
|
+
v.push(row.z - pred);
|
|
318
|
+
H.push([ts.u[0] - tr.u[0], ts.u[1] - tr.u[1], ts.u[2] - tr.u[2]]);
|
|
319
|
+
}
|
|
320
|
+
const Ht = transpose(H);
|
|
321
|
+
const HtW = matMul(Ht, W);
|
|
322
|
+
const N = matMul(HtW, H);
|
|
323
|
+
const b = matMul(
|
|
324
|
+
HtW,
|
|
325
|
+
v.map((s) => [s])
|
|
326
|
+
);
|
|
327
|
+
const Ninv = matInv(N);
|
|
328
|
+
if (!Ninv) return null;
|
|
329
|
+
const dx = matMul(
|
|
330
|
+
Ninv,
|
|
331
|
+
b.map((r) => [r[0]])
|
|
332
|
+
);
|
|
333
|
+
x += dx[0][0];
|
|
334
|
+
y += dx[1][0];
|
|
335
|
+
z += dx[2][0];
|
|
336
|
+
cov = Ninv;
|
|
337
|
+
rows.forEach((row, i) => {
|
|
338
|
+
residuals[row.g.prn] = v[i] - (H[i][0] * dx[0][0] + H[i][1] * dx[1][0] + H[i][2] * dx[2][0]);
|
|
339
|
+
});
|
|
340
|
+
if (Math.hypot(dx[0][0], dx[1][0], dx[2][0]) < convergenceM) {
|
|
341
|
+
converged = true;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let worst = null;
|
|
346
|
+
let worstAbs = rejectThresholdM;
|
|
347
|
+
for (const [prn, r] of Object.entries(residuals)) {
|
|
348
|
+
if (Math.abs(r) > worstAbs) {
|
|
349
|
+
worstAbs = Math.abs(r);
|
|
350
|
+
worst = prn;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (worst && geom.length > 4) {
|
|
354
|
+
rejected.push(worst);
|
|
355
|
+
geom = geom.filter((g) => g.prn !== worst);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const usedSatellites = [
|
|
359
|
+
...new Set(rows.flatMap((r) => [r.g.prn, r.ref.prn]))
|
|
360
|
+
].sort();
|
|
361
|
+
return {
|
|
362
|
+
position: [x, y, z],
|
|
363
|
+
baseline: [x - basePos[0], y - basePos[1], z - basePos[2]],
|
|
364
|
+
sigmas: cov ? [
|
|
365
|
+
Math.sqrt(Math.max(cov[0][0], 0)),
|
|
366
|
+
Math.sqrt(Math.max(cov[1][1], 0)),
|
|
367
|
+
Math.sqrt(Math.max(cov[2][2], 0))
|
|
368
|
+
] : [0, 0, 0],
|
|
369
|
+
usedSatellites,
|
|
370
|
+
refSatellites,
|
|
371
|
+
residuals,
|
|
372
|
+
rejectedSatellites: rejected,
|
|
373
|
+
nSats: usedSatellites.length,
|
|
374
|
+
iterations,
|
|
375
|
+
converged
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
var RtkFloatEngine = class {
|
|
380
|
+
basePos;
|
|
381
|
+
ephemerides;
|
|
382
|
+
o;
|
|
383
|
+
/** State vector [x, y, z, N₁…] and covariance; null before init. */
|
|
384
|
+
x = null;
|
|
385
|
+
P = [];
|
|
386
|
+
amb = [];
|
|
387
|
+
refs = /* @__PURE__ */ new Map();
|
|
388
|
+
track = /* @__PURE__ */ new Map();
|
|
389
|
+
lastMs = null;
|
|
390
|
+
constructor(basePos, ephemerides, opts = {}) {
|
|
391
|
+
this.basePos = basePos;
|
|
392
|
+
this.ephemerides = ephemerides;
|
|
393
|
+
this.o = {
|
|
394
|
+
mode: opts.mode ?? "kinematic",
|
|
395
|
+
elevationMaskDeg: opts.elevationMaskDeg ?? 10,
|
|
396
|
+
troposphere: opts.troposphere ?? true,
|
|
397
|
+
codeSigmaM: opts.codeSigmaM ?? 0.3,
|
|
398
|
+
phaseSigmaM: opts.phaseSigmaM ?? 3e-3,
|
|
399
|
+
processNoisePosM: opts.processNoisePosM ?? 0,
|
|
400
|
+
kinematicSigmaM: opts.kinematicSigmaM ?? 30,
|
|
401
|
+
ambProcessNoiseCycles: opts.ambProcessNoiseCycles ?? 1e-4,
|
|
402
|
+
ambInitSigmaCycles: opts.ambInitSigmaCycles ?? 30,
|
|
403
|
+
maxGapMs: opts.maxGapMs ?? 1e4,
|
|
404
|
+
codeGateM: opts.codeGateM ?? 30,
|
|
405
|
+
slipGateM: opts.slipGateM ?? 5,
|
|
406
|
+
updateIterations: opts.updateIterations ?? 2
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/** Clear all filter state (position, ambiguities, lock history). */
|
|
410
|
+
reset() {
|
|
411
|
+
this.x = null;
|
|
412
|
+
this.P = [];
|
|
413
|
+
this.amb = [];
|
|
414
|
+
this.refs.clear();
|
|
415
|
+
this.track.clear();
|
|
416
|
+
this.lastMs = null;
|
|
417
|
+
}
|
|
418
|
+
ambIndex(prn) {
|
|
419
|
+
return this.amb.findIndex((a) => a.prn === prn);
|
|
420
|
+
}
|
|
421
|
+
/** Remove one ambiguity state (row/col) from x and P. */
|
|
422
|
+
dropAmb(idx) {
|
|
423
|
+
const s = 3 + idx;
|
|
424
|
+
this.amb.splice(idx, 1);
|
|
425
|
+
this.x.splice(s, 1);
|
|
426
|
+
this.P.splice(s, 1);
|
|
427
|
+
for (const row of this.P) row.splice(s, 1);
|
|
428
|
+
}
|
|
429
|
+
/** Append an ambiguity state with the given value and variance. */
|
|
430
|
+
addAmb(entry, value, variance) {
|
|
431
|
+
this.amb.push(entry);
|
|
432
|
+
this.x.push(value);
|
|
433
|
+
const n = this.x.length;
|
|
434
|
+
for (const row of this.P) row.push(0);
|
|
435
|
+
const newRow = new Array(n).fill(0);
|
|
436
|
+
newRow[n - 1] = variance;
|
|
437
|
+
this.P.push(newRow);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Re-map a group's DD ambiguities from the old reference r to the
|
|
441
|
+
* new reference r' (which must hold a state): N_i' = N_i − N_r' and
|
|
442
|
+
* the old reference's slot becomes N_r = −N_r'. Exact linear
|
|
443
|
+
* transform of state and covariance (P' = T P Tᵀ).
|
|
444
|
+
*/
|
|
445
|
+
retarget(group, oldRef, newRefIdx) {
|
|
446
|
+
const n = this.x.length;
|
|
447
|
+
const j = 3 + newRefIdx;
|
|
448
|
+
const T = zeros(n, n);
|
|
449
|
+
for (let i = 0; i < n; i++) T[i][i] = 1;
|
|
450
|
+
for (let k = 0; k < this.amb.length; k++) {
|
|
451
|
+
if (this.amb[k].group !== group) continue;
|
|
452
|
+
const s = 3 + k;
|
|
453
|
+
if (s === j) T[s][s] = -1;
|
|
454
|
+
else T[s][j] -= 1;
|
|
455
|
+
}
|
|
456
|
+
this.x = matMul(
|
|
457
|
+
T,
|
|
458
|
+
this.x.map((v) => [v])
|
|
459
|
+
).map((r) => r[0]);
|
|
460
|
+
this.P = matMul(matMul(T, this.P), transpose(T));
|
|
461
|
+
const entry = this.amb[newRefIdx];
|
|
462
|
+
entry.prn = oldRef;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Process one synchronized epoch (same nominal `timeMs` for both
|
|
466
|
+
* receivers). Returns null until a first position can be
|
|
467
|
+
* initialised (via an internal DGNSS solve) or when the epoch has
|
|
468
|
+
* fewer than 3 usable DD rows.
|
|
469
|
+
*/
|
|
470
|
+
process(rover, base, timeMs) {
|
|
471
|
+
const o = this.o;
|
|
472
|
+
const geom = buildGeometry(
|
|
473
|
+
rover,
|
|
474
|
+
base,
|
|
475
|
+
this.basePos,
|
|
476
|
+
this.ephemerides,
|
|
477
|
+
timeMs,
|
|
478
|
+
o.elevationMaskDeg * Math.PI / 180,
|
|
479
|
+
o.troposphere
|
|
480
|
+
);
|
|
481
|
+
const byPrn = new Map(geom.map((g) => [g.prn, g]));
|
|
482
|
+
const dtS = this.lastMs !== null ? (timeMs - this.lastMs) / 1e3 : 0;
|
|
483
|
+
if (this.x) {
|
|
484
|
+
for (let i = this.amb.length - 1; i >= 0; i--) {
|
|
485
|
+
const prn = this.amb[i].prn;
|
|
486
|
+
const tr = this.track.get(prn);
|
|
487
|
+
const seen = byPrn.get(prn);
|
|
488
|
+
const hasPhase = seen && seen.cpR !== null && seen.cpB !== null;
|
|
489
|
+
const staleMs = tr ? timeMs - tr.lastMs : Infinity;
|
|
490
|
+
if (!hasPhase && staleMs > o.maxGapMs) this.dropAmb(i);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const slipped = /* @__PURE__ */ new Set();
|
|
494
|
+
for (const g of geom) {
|
|
495
|
+
if (g.cpR === null || g.cpB === null) continue;
|
|
496
|
+
const tr = this.track.get(g.prn);
|
|
497
|
+
if (!tr) continue;
|
|
498
|
+
if (timeMs - tr.lastMs > o.maxGapMs) slipped.add(g.prn);
|
|
499
|
+
else if (g.lockR !== void 0 && tr.lockR !== void 0 && g.lockR < tr.lockR || g.lockB !== void 0 && tr.lockB !== void 0 && g.lockB < tr.lockB)
|
|
500
|
+
slipped.add(g.prn);
|
|
501
|
+
}
|
|
502
|
+
if (!this.x) {
|
|
503
|
+
const dg = solveDgnss(
|
|
504
|
+
rover,
|
|
505
|
+
base,
|
|
506
|
+
this.basePos,
|
|
507
|
+
this.ephemerides,
|
|
508
|
+
timeMs,
|
|
509
|
+
{
|
|
510
|
+
elevationMaskDeg: o.elevationMaskDeg,
|
|
511
|
+
troposphere: o.troposphere,
|
|
512
|
+
codeSigmaM: o.codeSigmaM
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
if (!dg) return null;
|
|
516
|
+
this.x = [...dg.position];
|
|
517
|
+
this.P = zeros(3, 3);
|
|
518
|
+
for (let i = 0; i < 3; i++) {
|
|
519
|
+
const s = Math.max(dg.sigmas[i], 1);
|
|
520
|
+
this.P[i][i] = 25 * s * s;
|
|
521
|
+
}
|
|
522
|
+
this.amb = [];
|
|
523
|
+
} else if (o.mode === "kinematic") {
|
|
524
|
+
for (let i = 0; i < 3; i++) {
|
|
525
|
+
for (let j = 0; j < this.x.length; j++) {
|
|
526
|
+
this.P[i][j] = 0;
|
|
527
|
+
this.P[j][i] = 0;
|
|
528
|
+
}
|
|
529
|
+
this.P[i][i] = o.kinematicSigmaM * o.kinematicSigmaM;
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
const q = o.processNoisePosM * o.processNoisePosM * dtS;
|
|
533
|
+
for (let i = 0; i < 3; i++) this.P[i][i] += q;
|
|
534
|
+
}
|
|
535
|
+
const qAmb = o.ambProcessNoiseCycles * o.ambProcessNoiseCycles * dtS;
|
|
536
|
+
for (let k = 0; k < this.amb.length; k++) this.P[3 + k][3 + k] += qAmb;
|
|
537
|
+
const groups = groupEntries(geom);
|
|
538
|
+
const newRefs = /* @__PURE__ */ new Map();
|
|
539
|
+
for (const [group, list] of groups) {
|
|
540
|
+
if (list.length < 2) continue;
|
|
541
|
+
const phase = list.filter(
|
|
542
|
+
(g) => g.cpR !== null && g.cpB !== null && !slipped.has(g.prn)
|
|
543
|
+
);
|
|
544
|
+
const pool = phase.length >= 2 ? phase : list;
|
|
545
|
+
const prevRef = this.refs.get(group);
|
|
546
|
+
const best = pool.reduce((a, b) => b.elB > a.elB ? b : a);
|
|
547
|
+
let ref = best;
|
|
548
|
+
if (best.prn !== prevRef) {
|
|
549
|
+
const anyState = this.amb.some((a) => a.group === group);
|
|
550
|
+
if (anyState) {
|
|
551
|
+
if (this.ambIndex(best.prn) >= 0) {
|
|
552
|
+
this.retarget(group, prevRef ?? best.prn, this.ambIndex(best.prn));
|
|
553
|
+
} else if (prevRef && pool.some((g) => g.prn === prevRef)) {
|
|
554
|
+
ref = pool.find((g) => g.prn === prevRef);
|
|
555
|
+
} else {
|
|
556
|
+
for (let i = this.amb.length - 1; i >= 0; i--)
|
|
557
|
+
if (this.amb[i].group === group) this.dropAmb(i);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
newRefs.set(group, ref);
|
|
562
|
+
this.refs.set(group, ref.prn);
|
|
563
|
+
}
|
|
564
|
+
for (const group of [...this.refs.keys()])
|
|
565
|
+
if (!newRefs.has(group)) this.refs.delete(group);
|
|
566
|
+
for (const prn of slipped) {
|
|
567
|
+
const i = this.ambIndex(prn);
|
|
568
|
+
if (i >= 0) this.dropAmb(i);
|
|
569
|
+
}
|
|
570
|
+
const initAmb = (g, ref) => {
|
|
571
|
+
if (g.lambda <= 0 || ref.lambda <= 0) return;
|
|
572
|
+
const zPhi = g.lambda * (g.cpR - g.cpB) - ref.lambda * (ref.cpR - ref.cpB);
|
|
573
|
+
const zP = g.prR - g.prB - (ref.prR - ref.prB);
|
|
574
|
+
const n0 = (zPhi - zP) / g.lambda;
|
|
575
|
+
this.addAmb(
|
|
576
|
+
{ prn: g.prn, group: g.group, lambda: g.lambda },
|
|
577
|
+
n0,
|
|
578
|
+
o.ambInitSigmaCycles * o.ambInitSigmaCycles
|
|
579
|
+
);
|
|
580
|
+
};
|
|
581
|
+
for (const [group, ref] of newRefs) {
|
|
582
|
+
for (const g of groups.get(group)) {
|
|
583
|
+
if (g === ref || g.cpR === null || g.cpB === null) continue;
|
|
584
|
+
if (ref.cpR === null || ref.cpB === null) continue;
|
|
585
|
+
const i = this.ambIndex(g.prn);
|
|
586
|
+
if (i >= 0) {
|
|
587
|
+
this.amb[i].lambda = g.lambda;
|
|
588
|
+
this.amb[i].group = g.group;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
initAmb(g, ref);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (let i = this.amb.length - 1; i >= 0; i--) {
|
|
595
|
+
const a = this.amb[i];
|
|
596
|
+
if (newRefs.get(a.group)?.prn === a.prn) this.dropAmb(i);
|
|
597
|
+
}
|
|
598
|
+
let rows = [];
|
|
599
|
+
for (const [group, ref] of newRefs) {
|
|
600
|
+
for (const g of groups.get(group)) {
|
|
601
|
+
if (g === ref) continue;
|
|
602
|
+
rows.push({
|
|
603
|
+
g,
|
|
604
|
+
ref,
|
|
605
|
+
kind: "code",
|
|
606
|
+
z: g.prR - g.prB - (ref.prR - ref.prB),
|
|
607
|
+
ambIdx: -1
|
|
608
|
+
});
|
|
609
|
+
const i = this.ambIndex(g.prn);
|
|
610
|
+
if (i >= 0 && g.cpR !== null && g.cpB !== null && ref.cpR !== null && ref.cpB !== null && g.lambda > 0 && ref.lambda > 0) {
|
|
611
|
+
rows.push({
|
|
612
|
+
g,
|
|
613
|
+
ref,
|
|
614
|
+
kind: "phase",
|
|
615
|
+
z: g.lambda * (g.cpR - g.cpB) - ref.lambda * (ref.cpR - ref.cpB),
|
|
616
|
+
ambIdx: i
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (rows.length < 3) {
|
|
622
|
+
this.lastMs = timeMs;
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
const predict = (row, xs) => {
|
|
626
|
+
const ts = roverTerms(row.g, xs[0], xs[1], xs[2], o.troposphere);
|
|
627
|
+
const tr = roverTerms(row.ref, xs[0], xs[1], xs[2], o.troposphere);
|
|
628
|
+
let pred = ts.rho - row.g.rhoB + ts.dTropo - (tr.rho - row.ref.rhoB + tr.dTropo);
|
|
629
|
+
const h = new Array(this.x.length).fill(0);
|
|
630
|
+
h[0] = ts.u[0] - tr.u[0];
|
|
631
|
+
h[1] = ts.u[1] - tr.u[1];
|
|
632
|
+
h[2] = ts.u[2] - tr.u[2];
|
|
633
|
+
if (row.ambIdx >= 0) {
|
|
634
|
+
pred += this.amb[row.ambIdx].lambda * this.x[3 + row.ambIdx];
|
|
635
|
+
h[3 + row.ambIdx] = this.amb[row.ambIdx].lambda;
|
|
636
|
+
}
|
|
637
|
+
return { pred, h };
|
|
638
|
+
};
|
|
639
|
+
const dropPrns = /* @__PURE__ */ new Set();
|
|
640
|
+
const codeInn = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const row of rows) {
|
|
642
|
+
if (row.kind !== "code") continue;
|
|
643
|
+
const v = row.z - predict(row, this.x).pred;
|
|
644
|
+
codeInn.set(row.g.prn, v);
|
|
645
|
+
if (Math.abs(v) > o.codeGateM) dropPrns.add(row.g.prn);
|
|
646
|
+
}
|
|
647
|
+
for (const row of rows) {
|
|
648
|
+
if (row.kind !== "phase" || dropPrns.has(row.g.prn)) continue;
|
|
649
|
+
const v = row.z - predict(row, this.x).pred;
|
|
650
|
+
const vc = codeInn.get(row.g.prn) ?? 0;
|
|
651
|
+
if (Math.abs(v - vc) > o.slipGateM) {
|
|
652
|
+
const zP = row.g.prR - row.g.prB - (row.ref.prR - row.ref.prB);
|
|
653
|
+
const lam = this.amb[row.ambIdx].lambda;
|
|
654
|
+
this.x[3 + row.ambIdx] = (row.z - zP) / lam;
|
|
655
|
+
const s = 3 + row.ambIdx;
|
|
656
|
+
for (let j = 0; j < this.x.length; j++) {
|
|
657
|
+
this.P[s][j] = 0;
|
|
658
|
+
this.P[j][s] = 0;
|
|
659
|
+
}
|
|
660
|
+
this.P[s][s] = o.ambInitSigmaCycles * o.ambInitSigmaCycles;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (dropPrns.size) rows = rows.filter((r) => !dropPrns.has(r.g.prn));
|
|
664
|
+
if (rows.length < 3) {
|
|
665
|
+
this.lastMs = timeMs;
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const n = this.x.length;
|
|
669
|
+
const R = ddCovariance(rows, o.codeSigmaM, o.phaseSigmaM);
|
|
670
|
+
const xPrior = [...this.x];
|
|
671
|
+
const PPrior = this.P;
|
|
672
|
+
let xi = [...this.x];
|
|
673
|
+
let K = [];
|
|
674
|
+
let H = [];
|
|
675
|
+
for (let it = 0; it < Math.max(o.updateIterations, 1); it++) {
|
|
676
|
+
H = [];
|
|
677
|
+
const v = [];
|
|
678
|
+
for (const row of rows) {
|
|
679
|
+
const { pred, h } = predict(row, xi);
|
|
680
|
+
let corr = 0;
|
|
681
|
+
for (let j = 0; j < n; j++) corr += h[j] * (xPrior[j] - xi[j]);
|
|
682
|
+
v.push(row.z - pred - corr);
|
|
683
|
+
H.push(h);
|
|
684
|
+
}
|
|
685
|
+
const Ht = transpose(H);
|
|
686
|
+
const S = matMul(matMul(H, PPrior), Ht);
|
|
687
|
+
for (let i = 0; i < rows.length; i++)
|
|
688
|
+
for (let j = 0; j < rows.length; j++) S[i][j] += R[i][j];
|
|
689
|
+
const Sinv = matInv(S);
|
|
690
|
+
if (!Sinv) {
|
|
691
|
+
this.lastMs = timeMs;
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
K = matMul(matMul(PPrior, Ht), Sinv);
|
|
695
|
+
const dx = matMul(
|
|
696
|
+
K,
|
|
697
|
+
v.map((s) => [s])
|
|
698
|
+
);
|
|
699
|
+
xi = xPrior.map((s, i) => s + dx[i][0]);
|
|
700
|
+
}
|
|
701
|
+
this.x = xi;
|
|
702
|
+
const KH = matMul(K, H);
|
|
703
|
+
const IKH = zeros(n, n);
|
|
704
|
+
for (let i = 0; i < n; i++)
|
|
705
|
+
for (let j = 0; j < n; j++) IKH[i][j] = (i === j ? 1 : 0) - KH[i][j];
|
|
706
|
+
const Pnew = matMul(IKH, PPrior);
|
|
707
|
+
for (let i = 0; i < n; i++)
|
|
708
|
+
for (let j = i; j < n; j++) {
|
|
709
|
+
const s = (Pnew[i][j] + Pnew[j][i]) / 2;
|
|
710
|
+
Pnew[i][j] = s;
|
|
711
|
+
Pnew[j][i] = s;
|
|
712
|
+
}
|
|
713
|
+
this.P = Pnew;
|
|
714
|
+
for (const g of geom) {
|
|
715
|
+
if (g.cpR === null || g.cpB === null) continue;
|
|
716
|
+
this.track.set(g.prn, { lockR: g.lockR, lockB: g.lockB, lastMs: timeMs });
|
|
717
|
+
}
|
|
718
|
+
this.lastMs = timeMs;
|
|
719
|
+
const ambiguities = {};
|
|
720
|
+
for (let k = 0; k < this.amb.length; k++)
|
|
721
|
+
ambiguities[this.amb[k].prn] = this.x[3 + k];
|
|
722
|
+
const refSatellites = {};
|
|
723
|
+
for (const [group, ref] of newRefs) refSatellites[group] = ref.prn;
|
|
724
|
+
const nSats = new Set(rows.flatMap((r) => [r.g.prn, r.ref.prn])).size;
|
|
725
|
+
return {
|
|
726
|
+
timeMs,
|
|
727
|
+
position: [this.x[0], this.x[1], this.x[2]],
|
|
728
|
+
floatBaseline: [
|
|
729
|
+
this.x[0] - this.basePos[0],
|
|
730
|
+
this.x[1] - this.basePos[1],
|
|
731
|
+
this.x[2] - this.basePos[2]
|
|
732
|
+
],
|
|
733
|
+
nSats,
|
|
734
|
+
ratio: void 0,
|
|
735
|
+
sigmas: [
|
|
736
|
+
Math.sqrt(Math.max(this.P[0][0], 0)),
|
|
737
|
+
Math.sqrt(Math.max(this.P[1][1], 0)),
|
|
738
|
+
Math.sqrt(Math.max(this.P[2][2], 0))
|
|
739
|
+
],
|
|
740
|
+
ambiguities,
|
|
741
|
+
refSatellites
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/positioning/index.ts
|
|
747
|
+
var GM_GPS = 3986005e8;
|
|
748
|
+
var F_REL = -4442807633e-19;
|
|
749
|
+
function satClockCorrection(eph, tMs) {
|
|
750
|
+
if (eph.system === "R" || eph.system === "S") {
|
|
751
|
+
const dt2 = (tMs - eph.tocDate.getTime()) / 1e3;
|
|
752
|
+
return eph.tauN + eph.gammaN * dt2;
|
|
753
|
+
}
|
|
754
|
+
const k = eph;
|
|
755
|
+
const dt = (tMs - k.tocDate.getTime()) / 1e3;
|
|
756
|
+
const poly = k.af0 + k.af1 * dt + k.af2 * dt * dt;
|
|
757
|
+
const a = k.sqrtA * k.sqrtA;
|
|
758
|
+
const n = Math.sqrt(GM_GPS / (a * a * a)) + k.deltaN;
|
|
759
|
+
const tk = dt;
|
|
760
|
+
const Mk = k.m0 + n * tk;
|
|
761
|
+
let Ek = Mk;
|
|
762
|
+
for (let i = 0; i < 8; i++) {
|
|
763
|
+
Ek = Mk + k.e * Math.sin(Ek);
|
|
764
|
+
}
|
|
765
|
+
const rel = F_REL * k.e * k.sqrtA * Math.sin(Ek);
|
|
766
|
+
return poly + rel;
|
|
767
|
+
}
|
|
768
|
+
function ionoFree(p1, p2, f1, f2) {
|
|
769
|
+
const g = f1 * f1 / (f1 * f1 - f2 * f2);
|
|
770
|
+
return g * p1 - (g - 1) * p2;
|
|
771
|
+
}
|
|
772
|
+
var GPS_EPOCH_MS_SPP = Date.UTC(1980, 0, 6);
|
|
773
|
+
var PRIMARY_FREQ_HZ = {
|
|
774
|
+
G: 157542e4,
|
|
775
|
+
E: 157542e4,
|
|
776
|
+
J: 157542e4,
|
|
777
|
+
S: 157542e4,
|
|
778
|
+
C: 1561098e3,
|
|
779
|
+
R: 1602e6
|
|
780
|
+
};
|
|
781
|
+
var F_L1 = 157542e4;
|
|
782
|
+
function tropoDelay2(elevationRad) {
|
|
783
|
+
const sinEl = Math.sin(elevationRad);
|
|
784
|
+
return 2.47 / (sinEl + 0.0121);
|
|
785
|
+
}
|
|
786
|
+
function sagnac2(pos, travelTimeS) {
|
|
787
|
+
const theta = OMEGA_E * travelTimeS;
|
|
788
|
+
const c = Math.cos(theta);
|
|
789
|
+
const s = Math.sin(theta);
|
|
790
|
+
return [pos.x * c + pos.y * s, -pos.x * s + pos.y * c, pos.z];
|
|
791
|
+
}
|
|
792
|
+
function solveLinear(A, b) {
|
|
793
|
+
const n = b.length;
|
|
794
|
+
const M = A.map((row, i) => [...row, b[i]]);
|
|
795
|
+
for (let col = 0; col < n; col++) {
|
|
796
|
+
let pivot = col;
|
|
797
|
+
for (let r = col + 1; r < n; r++) {
|
|
798
|
+
if (Math.abs(M[r][col]) > Math.abs(M[pivot][col])) pivot = r;
|
|
799
|
+
}
|
|
800
|
+
if (Math.abs(M[pivot][col]) < 1e-12) return null;
|
|
801
|
+
[M[col], M[pivot]] = [M[pivot], M[col]];
|
|
802
|
+
for (let r = 0; r < n; r++) {
|
|
803
|
+
if (r === col) continue;
|
|
804
|
+
const f = M[r][col] / M[col][col];
|
|
805
|
+
for (let c = col; c <= n; c++) M[r][c] -= f * M[col][c];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return M.map((row, i) => row[n] / M[i][i]);
|
|
809
|
+
}
|
|
810
|
+
function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
|
|
811
|
+
const {
|
|
812
|
+
elevationMaskDeg = 10,
|
|
813
|
+
troposphere = true,
|
|
814
|
+
maxIterations = 15,
|
|
815
|
+
convergenceM = 1e-4,
|
|
816
|
+
tgd = true,
|
|
817
|
+
iono
|
|
818
|
+
} = opts;
|
|
819
|
+
const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
|
|
820
|
+
const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
|
|
821
|
+
const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
|
|
822
|
+
if (all.length < minSats(all)) return null;
|
|
823
|
+
const gaussNewton = (prns) => {
|
|
824
|
+
const systems = [...new Set(prns.map((p) => p[0]))].sort();
|
|
825
|
+
const dim = 3 + systems.length;
|
|
826
|
+
const sysIndex = new Map(systems.map((s, i) => [s, 3 + i]));
|
|
827
|
+
let x2 = 0;
|
|
828
|
+
let y2 = 0;
|
|
829
|
+
let z2 = 0;
|
|
830
|
+
const clock2 = new Map(systems.map((s) => [s, 0]));
|
|
831
|
+
const residuals2 = {};
|
|
832
|
+
let iterations2 = 0;
|
|
833
|
+
let converged2 = false;
|
|
834
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
835
|
+
iterations2 = iter + 1;
|
|
836
|
+
const HtH = Array.from(
|
|
837
|
+
{ length: dim },
|
|
838
|
+
() => new Array(dim).fill(0)
|
|
839
|
+
);
|
|
840
|
+
const Htv = new Array(dim).fill(0);
|
|
841
|
+
let rows = 0;
|
|
842
|
+
for (const k of Object.keys(residuals2)) delete residuals2[k];
|
|
843
|
+
const positionSaneIter = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
|
|
844
|
+
const [rxLat, rxLon] = positionSaneIter ? ecefToGeodetic(x2, y2, z2) : [0, 0];
|
|
845
|
+
for (const prn of prns) {
|
|
846
|
+
const psr = pseudoranges.get(prn);
|
|
847
|
+
const eph = ephemerides.get(prn);
|
|
848
|
+
const sys = prn[0];
|
|
849
|
+
const clk = clock2.get(sys);
|
|
850
|
+
const tTx = timeMs - psr / C_LIGHT * 1e3;
|
|
851
|
+
const dtsClock = satClockCorrection(eph, tTx);
|
|
852
|
+
const sat = computeSatPosition(eph, tTx - dtsClock * 1e3);
|
|
853
|
+
if (!Number.isFinite(sat.x)) continue;
|
|
854
|
+
const isKepler = eph.system !== "R" && eph.system !== "S";
|
|
855
|
+
const dts = dtsClock - (tgd && isKepler ? eph.tgd : 0);
|
|
856
|
+
const travel = Math.hypot(sat.x - x2, sat.y - y2, sat.z - z2) / C_LIGHT;
|
|
857
|
+
const [sx, sy, sz] = sagnac2(sat, travel);
|
|
858
|
+
const rho = Math.hypot(sx - x2, sy - y2, sz - z2);
|
|
859
|
+
const ux = (x2 - sx) / rho;
|
|
860
|
+
const uy = (y2 - sy) / rho;
|
|
861
|
+
const uz = (z2 - sz) / rho;
|
|
862
|
+
let elev = Math.PI / 2;
|
|
863
|
+
let azim = 0;
|
|
864
|
+
let weight = 1;
|
|
865
|
+
const positionSane = positionSaneIter;
|
|
866
|
+
if (positionSane) {
|
|
867
|
+
const azel = ecefToAzEl(x2, y2, z2, sx, sy, sz);
|
|
868
|
+
elev = azel.el;
|
|
869
|
+
azim = azel.az;
|
|
870
|
+
if (iter >= 2 && elev < elevationMaskDeg * Math.PI / 180) continue;
|
|
871
|
+
const sinEl = Math.max(Math.sin(elev), 0.1);
|
|
872
|
+
weight = sinEl * sinEl;
|
|
873
|
+
}
|
|
874
|
+
const tropo = troposphere && positionSane ? tropoDelay2(elev) : 0;
|
|
875
|
+
let ionoM = 0;
|
|
876
|
+
if (iono && positionSane) {
|
|
877
|
+
const f = PRIMARY_FREQ_HZ[sys] ?? F_L1;
|
|
878
|
+
ionoM = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow) * (F_L1 / f * (F_L1 / f));
|
|
879
|
+
}
|
|
880
|
+
const predicted = rho + clk - C_LIGHT * dts + tropo + ionoM;
|
|
881
|
+
const v = psr - predicted;
|
|
882
|
+
const h = new Array(dim).fill(0);
|
|
883
|
+
h[0] = ux;
|
|
884
|
+
h[1] = uy;
|
|
885
|
+
h[2] = uz;
|
|
886
|
+
h[sysIndex.get(sys)] = 1;
|
|
887
|
+
for (let i = 0; i < dim; i++) {
|
|
888
|
+
for (let j = 0; j < dim; j++) HtH[i][j] += weight * h[i] * h[j];
|
|
889
|
+
Htv[i] += weight * h[i] * v;
|
|
890
|
+
}
|
|
891
|
+
residuals2[prn] = v;
|
|
892
|
+
rows++;
|
|
893
|
+
}
|
|
894
|
+
if (rows < dim) return null;
|
|
895
|
+
const dx = solveLinear(HtH, Htv);
|
|
896
|
+
if (!dx) return null;
|
|
897
|
+
x2 += dx[0];
|
|
898
|
+
y2 += dx[1];
|
|
899
|
+
z2 += dx[2];
|
|
900
|
+
for (const s of systems)
|
|
901
|
+
clock2.set(s, clock2.get(s) + dx[sysIndex.get(s)]);
|
|
902
|
+
if (Math.hypot(dx[0], dx[1], dx[2]) < convergenceM) {
|
|
903
|
+
converged2 = true;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return { x: x2, y: y2, z: z2, clock: clock2, residuals: residuals2, iterations: iterations2, converged: converged2 };
|
|
908
|
+
};
|
|
909
|
+
const REJECT_THRESHOLD_M = 50;
|
|
910
|
+
let candidates = all;
|
|
911
|
+
const rejected = [];
|
|
912
|
+
let inner = null;
|
|
913
|
+
for (let round = 0; round <= all.length; round++) {
|
|
914
|
+
inner = gaussNewton(candidates);
|
|
915
|
+
if (!inner) return null;
|
|
916
|
+
const vals = Object.entries(inner.residuals);
|
|
917
|
+
let worst = null;
|
|
918
|
+
let worstAbs = REJECT_THRESHOLD_M;
|
|
919
|
+
for (const [prn, v] of vals) {
|
|
920
|
+
if (Math.abs(v) > worstAbs) {
|
|
921
|
+
worstAbs = Math.abs(v);
|
|
922
|
+
worst = prn;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (!worst) break;
|
|
926
|
+
const remaining = candidates.filter((p) => p !== worst);
|
|
927
|
+
if (remaining.length < minSats(remaining)) break;
|
|
928
|
+
rejected.push(worst);
|
|
929
|
+
candidates = remaining;
|
|
930
|
+
}
|
|
931
|
+
if (!inner) return null;
|
|
932
|
+
const { x, y, z, clock, residuals, iterations, converged } = inner;
|
|
933
|
+
const used = [];
|
|
934
|
+
const azels = [];
|
|
935
|
+
for (const prn of candidates) {
|
|
936
|
+
if (!(prn in residuals)) continue;
|
|
937
|
+
const eph = ephemerides.get(prn);
|
|
938
|
+
const sat = computeSatPosition(eph, timeMs);
|
|
939
|
+
if (!Number.isFinite(sat.x)) continue;
|
|
940
|
+
const azel = ecefToAzEl(x, y, z, sat.x, sat.y, sat.z);
|
|
941
|
+
if (azel.el >= elevationMaskDeg * Math.PI / 180) {
|
|
942
|
+
used.push(prn);
|
|
943
|
+
azels.push(azel);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
position: [x, y, z],
|
|
948
|
+
clockBias: Object.fromEntries(clock),
|
|
949
|
+
usedSatellites: used,
|
|
950
|
+
rejectedSatellites: rejected,
|
|
951
|
+
residuals,
|
|
952
|
+
dop: computeDop(azels),
|
|
953
|
+
iterations,
|
|
954
|
+
converged
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export {
|
|
959
|
+
klobucharDelay,
|
|
960
|
+
toRtkEpoch,
|
|
961
|
+
solveDgnss,
|
|
962
|
+
RtkFloatEngine,
|
|
963
|
+
satClockCorrection,
|
|
964
|
+
ionoFree,
|
|
965
|
+
solveSpp
|
|
966
|
+
};
|