gnss-js 0.1.2 → 1.0.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/LICENSE +23 -0
- package/README.md +171 -5
- package/dist/analysis.cjs +1381 -0
- package/dist/analysis.d.cts +211 -0
- package/dist/analysis.d.ts +211 -0
- package/dist/analysis.js +14 -0
- package/dist/antex.cjs +239 -0
- package/dist/antex.d.cts +77 -0
- package/dist/antex.d.ts +77 -0
- package/dist/antex.js +8 -0
- package/dist/chunk-354IRDOG.js +239 -0
- package/dist/chunk-37QNKGTC.js +65 -0
- package/dist/chunk-5CMSYIYM.js +214 -0
- package/dist/chunk-5S5MONFK.js +0 -0
- package/dist/chunk-6FAL6P4G.js +12 -0
- package/dist/chunk-7NOFXKET.js +433 -0
- package/dist/chunk-CB6EOOLA.js +753 -0
- package/dist/chunk-HKN3PUGN.js +800 -0
- package/dist/chunk-IS4UUDBV.js +577 -0
- package/dist/chunk-K7WZQFBV.js +236 -0
- package/dist/chunk-LEEU5OIO.js +75 -0
- package/dist/chunk-PRSZIWKM.js +1482 -0
- package/dist/chunk-SDRRAJT5.js +433 -0
- package/dist/chunk-WR7LCB52.js +143 -0
- package/dist/chunk-Y3R57B5P.js +473 -0
- package/dist/chunk-YDMYO3YK.js +220 -0
- package/dist/constants.cjs +394 -0
- package/dist/constants.d.cts +14 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +103 -0
- package/dist/coordinates.cjs +334 -0
- package/dist/coordinates.d.cts +81 -0
- package/dist/coordinates.d.ts +81 -0
- package/dist/coordinates.js +39 -0
- package/dist/ecef-CF0uAysr.d.cts +40 -0
- package/dist/ecef-CF0uAysr.d.ts +40 -0
- package/dist/ephemeris-BUWzfmBy.d.cts +114 -0
- package/dist/ephemeris-BUWzfmBy.d.ts +114 -0
- package/dist/gnss-BT6ulR17.d.cts +56 -0
- package/dist/gnss-C-tgoYNa.d.ts +56 -0
- package/dist/index.cjs +5774 -78
- package/dist/index.d.cts +17 -96
- package/dist/index.d.ts +17 -96
- package/dist/index.js +352 -415
- package/dist/nav-BAI1a9vK.d.cts +108 -0
- package/dist/nav-BAI1a9vK.d.ts +108 -0
- package/dist/nmea.cjs +247 -0
- package/dist/nmea.d.cts +37 -0
- package/dist/nmea.d.ts +37 -0
- package/dist/nmea.js +12 -0
- package/dist/ntrip.cjs +169 -0
- package/dist/ntrip.d.cts +98 -0
- package/dist/ntrip.d.ts +98 -0
- package/dist/ntrip.js +10 -0
- package/dist/orbit.cjs +504 -0
- package/dist/orbit.d.cts +117 -0
- package/dist/orbit.d.ts +117 -0
- package/dist/orbit.js +29 -0
- package/dist/parser-JPjjFgeP.d.cts +73 -0
- package/dist/parser-JPjjFgeP.d.ts +73 -0
- package/dist/rinex.cjs +1375 -0
- package/dist/rinex.d.cts +116 -0
- package/dist/rinex.d.ts +116 -0
- package/dist/rinex.js +37 -0
- package/dist/rtcm3.cjs +1519 -0
- package/dist/rtcm3.d.cts +143 -0
- package/dist/rtcm3.d.ts +143 -0
- package/dist/rtcm3.js +31 -0
- package/dist/signals.cjs +858 -0
- package/dist/signals.d.cts +93 -0
- package/dist/signals.d.ts +93 -0
- package/dist/signals.js +70 -0
- package/dist/time-DnI1VpE8.d.cts +33 -0
- package/dist/time-DnI1VpE8.d.ts +33 -0
- package/dist/time.cjs +585 -0
- package/dist/time.d.cts +307 -0
- package/dist/time.d.ts +307 -0
- package/dist/time.js +154 -0
- package/package.json +112 -14
- package/.prettierrc +0 -5
- package/jest.config.ts +0 -9
- package/test/functions.test.ts +0 -63
- package/test/gnss-scales.test.ts +0 -95
- package/test/julian.test.ts +0 -145
- package/test/rinex.test.ts +0 -11
- package/test/test.ts +0 -10
- package/test/utc.test.ts +0 -25
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseRinexStream
|
|
3
|
+
} from "./chunk-CB6EOOLA.js";
|
|
4
|
+
import {
|
|
5
|
+
ARC_GAP_FACTOR,
|
|
6
|
+
BAND_LABELS,
|
|
7
|
+
C_LIGHT,
|
|
8
|
+
DUAL_FREQ_PAIRS,
|
|
9
|
+
FREQ,
|
|
10
|
+
SYSTEM_NAMES,
|
|
11
|
+
buildGloChannelMap,
|
|
12
|
+
buildObsIndices,
|
|
13
|
+
getFreq
|
|
14
|
+
} from "./chunk-354IRDOG.js";
|
|
15
|
+
|
|
16
|
+
// src/analysis/multipath.ts
|
|
17
|
+
var MIN_ARC_LENGTH = 10;
|
|
18
|
+
var MultipathAccumulator = class {
|
|
19
|
+
state = /* @__PURE__ */ new Map();
|
|
20
|
+
results = [];
|
|
21
|
+
interval;
|
|
22
|
+
obsIndices;
|
|
23
|
+
gloChannels;
|
|
24
|
+
constructor(header) {
|
|
25
|
+
this.interval = header.interval ?? 30;
|
|
26
|
+
this.obsIndices = buildObsIndices(header);
|
|
27
|
+
this.gloChannels = buildGloChannelMap(header.glonassSlots);
|
|
28
|
+
}
|
|
29
|
+
/** Observation callback — wire this into parseRinexStream. */
|
|
30
|
+
onObservation = (time, prn, _codes, values) => {
|
|
31
|
+
const sys = prn[0];
|
|
32
|
+
const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
|
|
33
|
+
if (!bandMap) return;
|
|
34
|
+
const bandData = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const [band, { C, L }] of bandMap) {
|
|
36
|
+
if (C === null) continue;
|
|
37
|
+
const cVal = values[C];
|
|
38
|
+
const lVal = values[L];
|
|
39
|
+
const freq = getFreq(this.gloChannels, prn, band);
|
|
40
|
+
if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
|
|
41
|
+
bandData.set(band, { C: cVal, L: lVal, f: freq });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (bandData.size < 2) return;
|
|
45
|
+
const pairs = DUAL_FREQ_PAIRS[sys] ?? [];
|
|
46
|
+
for (const [bi, bj] of pairs) {
|
|
47
|
+
const di = bandData.get(bi);
|
|
48
|
+
const dj = bandData.get(bj);
|
|
49
|
+
if (!di || !dj) continue;
|
|
50
|
+
const \u03BBi = C_LIGHT / di.f;
|
|
51
|
+
const \u03BBj = C_LIGHT / dj.f;
|
|
52
|
+
const \u03B1 = di.f * di.f / (dj.f * dj.f);
|
|
53
|
+
const coeff = 2 / (\u03B1 - 1);
|
|
54
|
+
const mp = di.C - (1 + coeff) * di.L * \u03BBi + coeff * dj.L * \u03BBj;
|
|
55
|
+
this.pushMp(prn, bi, bj, time, mp);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
pushMp(prn, band, refBand, time, mp) {
|
|
59
|
+
if (!isFinite(mp)) return;
|
|
60
|
+
const pairKey = `${band}-${refBand}`;
|
|
61
|
+
let satStates = this.state.get(prn);
|
|
62
|
+
if (!satStates) {
|
|
63
|
+
satStates = /* @__PURE__ */ new Map();
|
|
64
|
+
this.state.set(prn, satStates);
|
|
65
|
+
}
|
|
66
|
+
let bandState = satStates.get(pairKey);
|
|
67
|
+
if (!bandState) {
|
|
68
|
+
bandState = { arc: { times: [], rawMp: [] }, lastTime: 0 };
|
|
69
|
+
satStates.set(pairKey, bandState);
|
|
70
|
+
}
|
|
71
|
+
const gap = bandState.lastTime > 0 ? (time - bandState.lastTime) / 1e3 : 0;
|
|
72
|
+
if (bandState.lastTime > 0 && gap > this.interval * ARC_GAP_FACTOR) {
|
|
73
|
+
this.closeArc(prn, band, refBand, bandState);
|
|
74
|
+
}
|
|
75
|
+
bandState.arc.times.push(time);
|
|
76
|
+
bandState.arc.rawMp.push(mp);
|
|
77
|
+
bandState.lastTime = time;
|
|
78
|
+
}
|
|
79
|
+
/** External cycle-slip notification — close affected arcs for this PRN. */
|
|
80
|
+
notifySlip(_time, prn, bands) {
|
|
81
|
+
const satStates = this.state.get(prn);
|
|
82
|
+
if (!satStates) return;
|
|
83
|
+
for (const [pairKey, bandState] of satStates) {
|
|
84
|
+
const [band, refBand] = pairKey.split("-");
|
|
85
|
+
if (bands.has(band) || bands.has(refBand)) {
|
|
86
|
+
this.closeArc(prn, band, refBand, bandState);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
closeArc(prn, band, refBand, state) {
|
|
91
|
+
const arc = state.arc;
|
|
92
|
+
if (arc.times.length >= MIN_ARC_LENGTH) {
|
|
93
|
+
const mean = arc.rawMp.reduce((a, b) => a + b, 0) / arc.rawMp.length;
|
|
94
|
+
const sys = prn[0];
|
|
95
|
+
const bLabel = BAND_LABELS[sys]?.[band] ?? band;
|
|
96
|
+
const rLabel = BAND_LABELS[sys]?.[refBand] ?? refBand;
|
|
97
|
+
const label = `${prn} MP ${bLabel}-${rLabel}`;
|
|
98
|
+
const points = arc.times.map((t, i) => ({
|
|
99
|
+
time: t,
|
|
100
|
+
mp: arc.rawMp[i] - mean
|
|
101
|
+
}));
|
|
102
|
+
const rms = Math.sqrt(points.reduce((s, p) => s + p.mp * p.mp, 0) / points.length);
|
|
103
|
+
this.results.push({ prn, system: sys, band, refBand, label, points, rms });
|
|
104
|
+
}
|
|
105
|
+
state.arc = { times: [], rawMp: [] };
|
|
106
|
+
}
|
|
107
|
+
/** Finalize: close remaining arcs and compute statistics. */
|
|
108
|
+
finalize() {
|
|
109
|
+
for (const [prn, satStates] of this.state) {
|
|
110
|
+
for (const [pairKey, bandState] of satStates) {
|
|
111
|
+
const [band, refBand] = pairKey.split("-");
|
|
112
|
+
this.closeArc(prn, band, refBand, bandState);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const signalGroups = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const series of this.results) {
|
|
117
|
+
const key = `${series.system}-${series.band}-${series.refBand}`;
|
|
118
|
+
if (!signalGroups.has(key)) signalGroups.set(key, []);
|
|
119
|
+
signalGroups.get(key).push(series);
|
|
120
|
+
}
|
|
121
|
+
const signalStats = [];
|
|
122
|
+
for (const [, seriesList] of signalGroups) {
|
|
123
|
+
const { system, band, refBand } = seriesList[0];
|
|
124
|
+
let sumSq = 0;
|
|
125
|
+
let count = 0;
|
|
126
|
+
for (const s of seriesList) {
|
|
127
|
+
for (const p of s.points) {
|
|
128
|
+
sumSq += p.mp * p.mp;
|
|
129
|
+
count++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const rms = count > 0 ? Math.sqrt(sumSq / count) : 0;
|
|
133
|
+
const bLabel = BAND_LABELS[system]?.[band] ?? band;
|
|
134
|
+
const rLabel = BAND_LABELS[system]?.[refBand] ?? refBand;
|
|
135
|
+
const sysName = SYSTEM_NAMES[system] ?? system;
|
|
136
|
+
signalStats.push({
|
|
137
|
+
label: `MP ${bLabel}-${rLabel} (${sysName})`,
|
|
138
|
+
system,
|
|
139
|
+
band,
|
|
140
|
+
refBand,
|
|
141
|
+
rms,
|
|
142
|
+
count,
|
|
143
|
+
satellites: seriesList.length
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const sysOrder = "GRECIJS";
|
|
147
|
+
signalStats.sort((a, b) => {
|
|
148
|
+
const da = sysOrder.indexOf(a.system);
|
|
149
|
+
const db = sysOrder.indexOf(b.system);
|
|
150
|
+
if (da !== db) return (da === -1 ? 99 : da) - (db === -1 ? 99 : db);
|
|
151
|
+
return a.band.localeCompare(b.band);
|
|
152
|
+
});
|
|
153
|
+
return { series: this.results, signalStats };
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/analysis/cycle-slip.ts
|
|
158
|
+
var MW_MIN_THRESHOLD = 0.5;
|
|
159
|
+
var MW_SIGMA_FACTOR = 4;
|
|
160
|
+
var MW_MIN_EPOCHS = 3;
|
|
161
|
+
var GF_THRESHOLD_M = 0.15;
|
|
162
|
+
var SF_THRESHOLD_M = 3;
|
|
163
|
+
function mwSmoothInit(value) {
|
|
164
|
+
return { mean: value, m2: 0, n: 1 };
|
|
165
|
+
}
|
|
166
|
+
function mwSmoothUpdate(s, value) {
|
|
167
|
+
const n = s.n + 1;
|
|
168
|
+
const delta = value - s.mean;
|
|
169
|
+
const mean = s.mean + delta / n;
|
|
170
|
+
const delta2 = value - mean;
|
|
171
|
+
return { mean, m2: s.m2 + delta * delta2, n };
|
|
172
|
+
}
|
|
173
|
+
function mwSmoothStddev(s) {
|
|
174
|
+
return s.n > 1 ? Math.sqrt(s.m2 / (s.n - 1)) : Infinity;
|
|
175
|
+
}
|
|
176
|
+
var CycleSlipAccumulator = class {
|
|
177
|
+
obsIndices;
|
|
178
|
+
prev = /* @__PURE__ */ new Map();
|
|
179
|
+
events = [];
|
|
180
|
+
interval;
|
|
181
|
+
/** GLONASS PRN → channel k for FDMA frequency computation. */
|
|
182
|
+
gloChannels;
|
|
183
|
+
onSlip;
|
|
184
|
+
// Per-signal epoch/slip counting
|
|
185
|
+
signalEpochs = /* @__PURE__ */ new Map();
|
|
186
|
+
signalSlips = /* @__PURE__ */ new Map();
|
|
187
|
+
signalSats = /* @__PURE__ */ new Map();
|
|
188
|
+
constructor(header, onSlip) {
|
|
189
|
+
this.interval = header.interval ?? 30;
|
|
190
|
+
this.obsIndices = buildObsIndices(header);
|
|
191
|
+
this.gloChannels = buildGloChannelMap(header.glonassSlots);
|
|
192
|
+
this.onSlip = onSlip;
|
|
193
|
+
}
|
|
194
|
+
recordSlip(sigKey, time, prn, signal, magnitude, bands) {
|
|
195
|
+
this.events.push({ time, prn, signal, magnitude });
|
|
196
|
+
this.signalSlips.set(sigKey, (this.signalSlips.get(sigKey) ?? 0) + 1);
|
|
197
|
+
if (!this.signalSats.has(sigKey)) this.signalSats.set(sigKey, /* @__PURE__ */ new Set());
|
|
198
|
+
this.signalSats.get(sigKey).add(prn);
|
|
199
|
+
this.onSlip?.(time, prn, bands);
|
|
200
|
+
}
|
|
201
|
+
countEpoch(sigKey) {
|
|
202
|
+
this.signalEpochs.set(sigKey, (this.signalEpochs.get(sigKey) ?? 0) + 1);
|
|
203
|
+
}
|
|
204
|
+
/** Resolve frequency for a given PRN + band. Handles GLONASS FDMA. */
|
|
205
|
+
getFreqForPrn(prn, band) {
|
|
206
|
+
return getFreq(this.gloChannels, prn, band);
|
|
207
|
+
}
|
|
208
|
+
onObservation = (time, prn, _codes, values) => {
|
|
209
|
+
const sys = prn[0];
|
|
210
|
+
const idxMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
|
|
211
|
+
if (!idxMap) return;
|
|
212
|
+
if (sys !== "R" && !FREQ[sys]) return;
|
|
213
|
+
const phaseM = /* @__PURE__ */ new Map();
|
|
214
|
+
const codeM = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const [band, { L, C }] of idxMap) {
|
|
216
|
+
const freq = this.getFreqForPrn(prn, band);
|
|
217
|
+
if (!freq) continue;
|
|
218
|
+
const lVal = values[L];
|
|
219
|
+
if (lVal != null && lVal !== 0) phaseM.set(band, lVal * (C_LIGHT / freq));
|
|
220
|
+
if (C !== null) {
|
|
221
|
+
const cVal = values[C];
|
|
222
|
+
if (cVal != null && cVal !== 0) codeM.set(band, cVal);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (phaseM.size === 0) return;
|
|
226
|
+
const prev = this.prev.get(prn);
|
|
227
|
+
const gap = prev ? (time - prev.time) / 1e3 : 0;
|
|
228
|
+
const isArcBreak = !prev || gap > this.interval * ARC_GAP_FACTOR;
|
|
229
|
+
const mwSmooth = /* @__PURE__ */ new Map();
|
|
230
|
+
const gfMap = /* @__PURE__ */ new Map();
|
|
231
|
+
const pairs = DUAL_FREQ_PAIRS[sys] ?? [];
|
|
232
|
+
for (const [b1, b2] of pairs) {
|
|
233
|
+
const l1 = phaseM.get(b1);
|
|
234
|
+
const l2 = phaseM.get(b2);
|
|
235
|
+
const f1 = this.getFreqForPrn(prn, b1);
|
|
236
|
+
const f2 = this.getFreqForPrn(prn, b2);
|
|
237
|
+
if (!f1 || !f2) continue;
|
|
238
|
+
const pairKey = `${b1}-${b2}`;
|
|
239
|
+
if (l1 !== void 0 && l2 !== void 0) {
|
|
240
|
+
gfMap.set(pairKey, l1 - l2);
|
|
241
|
+
}
|
|
242
|
+
const c1 = codeM.get(b1);
|
|
243
|
+
const c2 = codeM.get(b2);
|
|
244
|
+
if (l1 !== void 0 && l2 !== void 0 && c1 !== void 0 && c2 !== void 0) {
|
|
245
|
+
const lambda_wl = C_LIGHT / (f1 - f2);
|
|
246
|
+
const phi_wl = (f1 * l1 - f2 * l2) / C_LIGHT;
|
|
247
|
+
const P_nl = (f1 * c1 + f2 * c2) / ((f1 + f2) * lambda_wl);
|
|
248
|
+
const mw = phi_wl - P_nl;
|
|
249
|
+
const prevSmooth = isArcBreak ? void 0 : prev?.mwSmooth.get(pairKey);
|
|
250
|
+
if (prevSmooth && prevSmooth.n >= MW_MIN_EPOCHS) {
|
|
251
|
+
const lbl1 = BAND_LABELS[sys]?.[b1] ?? b1;
|
|
252
|
+
const lbl2 = BAND_LABELS[sys]?.[b2] ?? b2;
|
|
253
|
+
const signal = `MW ${lbl1}-${lbl2}`;
|
|
254
|
+
const sigKey = `${sys}:${signal}`;
|
|
255
|
+
this.countEpoch(sigKey);
|
|
256
|
+
const stddev = mwSmoothStddev(prevSmooth);
|
|
257
|
+
const threshold = Math.max(MW_MIN_THRESHOLD, MW_SIGMA_FACTOR * stddev);
|
|
258
|
+
const deviation = Math.abs(mw - prevSmooth.mean);
|
|
259
|
+
if (deviation > threshold) {
|
|
260
|
+
this.recordSlip(sigKey, time, prn, signal, deviation, /* @__PURE__ */ new Set([b1, b2]));
|
|
261
|
+
mwSmooth.set(pairKey, mwSmoothInit(mw));
|
|
262
|
+
} else {
|
|
263
|
+
mwSmooth.set(pairKey, mwSmoothUpdate(prevSmooth, mw));
|
|
264
|
+
}
|
|
265
|
+
} else if (prevSmooth) {
|
|
266
|
+
mwSmooth.set(pairKey, mwSmoothUpdate(prevSmooth, mw));
|
|
267
|
+
} else {
|
|
268
|
+
mwSmooth.set(pairKey, mwSmoothInit(mw));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!isArcBreak && prev) {
|
|
273
|
+
const dualChecked = /* @__PURE__ */ new Set();
|
|
274
|
+
for (const [b1, b2] of pairs) {
|
|
275
|
+
const pairKey = `${b1}-${b2}`;
|
|
276
|
+
const lbl1 = BAND_LABELS[sys]?.[b1] ?? b1;
|
|
277
|
+
const lbl2 = BAND_LABELS[sys]?.[b2] ?? b2;
|
|
278
|
+
if (mwSmooth.has(pairKey)) {
|
|
279
|
+
dualChecked.add(b1);
|
|
280
|
+
dualChecked.add(b2);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const curGf = gfMap.get(pairKey);
|
|
284
|
+
const prevGf = prev.gf.get(pairKey);
|
|
285
|
+
if (curGf !== void 0 && prevGf !== void 0) {
|
|
286
|
+
const signal = `GF ${lbl1}-${lbl2}`;
|
|
287
|
+
const sigKey = `${sys}:${signal}`;
|
|
288
|
+
this.countEpoch(sigKey);
|
|
289
|
+
const jump = Math.abs(curGf - prevGf);
|
|
290
|
+
if (jump > GF_THRESHOLD_M) {
|
|
291
|
+
this.recordSlip(sigKey, time, prn, signal, jump, /* @__PURE__ */ new Set([b1, b2]));
|
|
292
|
+
}
|
|
293
|
+
dualChecked.add(b1);
|
|
294
|
+
dualChecked.add(b2);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const [band, lm] of phaseM) {
|
|
298
|
+
if (dualChecked.has(band)) continue;
|
|
299
|
+
const prevLm = prev.phase.get(band);
|
|
300
|
+
if (prevLm === void 0) continue;
|
|
301
|
+
const cm = codeM.get(band);
|
|
302
|
+
const prevCm = prev.code.get(band);
|
|
303
|
+
if (cm === void 0 || prevCm === void 0) continue;
|
|
304
|
+
const dPhase = lm - prevLm;
|
|
305
|
+
const dCode = cm - prevCm;
|
|
306
|
+
const jumpM = Math.abs(dPhase - dCode);
|
|
307
|
+
const lbl = BAND_LABELS[sys]?.[band] ?? band;
|
|
308
|
+
const sigKey = `${sys}:${lbl}`;
|
|
309
|
+
this.countEpoch(sigKey);
|
|
310
|
+
if (jumpM > SF_THRESHOLD_M) {
|
|
311
|
+
this.recordSlip(sigKey, time, prn, lbl, jumpM, /* @__PURE__ */ new Set([band]));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
this.prev.set(prn, { time, mwSmooth, gf: gfMap, phase: phaseM, code: codeM });
|
|
316
|
+
};
|
|
317
|
+
finalize() {
|
|
318
|
+
const satSlipCounts = {};
|
|
319
|
+
for (const ev of this.events) {
|
|
320
|
+
satSlipCounts[ev.prn] = (satSlipCounts[ev.prn] ?? 0) + 1;
|
|
321
|
+
}
|
|
322
|
+
const signalStats = [];
|
|
323
|
+
const allKeys = /* @__PURE__ */ new Set([...this.signalEpochs.keys(), ...this.signalSlips.keys()]);
|
|
324
|
+
for (const sigKey of allKeys) {
|
|
325
|
+
const [sys, ...rest] = sigKey.split(":");
|
|
326
|
+
const signal = rest.join(":");
|
|
327
|
+
const totalEpochs = this.signalEpochs.get(sigKey) ?? 0;
|
|
328
|
+
const totalSlips = this.signalSlips.get(sigKey) ?? 0;
|
|
329
|
+
const sysName = SYSTEM_NAMES[sys] ?? sys;
|
|
330
|
+
signalStats.push({
|
|
331
|
+
label: `${signal} (${sysName})`,
|
|
332
|
+
system: sys,
|
|
333
|
+
totalSlips,
|
|
334
|
+
satellites: this.signalSats.get(sigKey)?.size ?? 0,
|
|
335
|
+
totalEpochs,
|
|
336
|
+
slipRate: totalEpochs > 0 ? totalSlips / totalEpochs * 1e3 : 0
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
const sysOrder = "GRECIJS";
|
|
340
|
+
signalStats.sort((a, b) => {
|
|
341
|
+
const da = sysOrder.indexOf(a.system);
|
|
342
|
+
const db = sysOrder.indexOf(b.system);
|
|
343
|
+
if (da !== db) return (da === -1 ? 99 : da) - (db === -1 ? 99 : db);
|
|
344
|
+
return a.label.localeCompare(b.label);
|
|
345
|
+
});
|
|
346
|
+
return { events: this.events, signalStats, satSlipCounts };
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// src/analysis/completeness.ts
|
|
351
|
+
var CompletenessAccumulator = class {
|
|
352
|
+
// PRN → code → { seen, expected, present }
|
|
353
|
+
// "seen" gates counting: expected only increments after a satellite has transmitted
|
|
354
|
+
// the signal at least once (avoids inflating expected for signals a sat doesn't have,
|
|
355
|
+
// e.g. L5 on GPS Block IIR).
|
|
356
|
+
data = /* @__PURE__ */ new Map();
|
|
357
|
+
header;
|
|
358
|
+
constructor(header) {
|
|
359
|
+
this.header = header;
|
|
360
|
+
}
|
|
361
|
+
onObservation = (_time, prn, codes, values) => {
|
|
362
|
+
let satMap = this.data.get(prn);
|
|
363
|
+
if (!satMap) {
|
|
364
|
+
satMap = /* @__PURE__ */ new Map();
|
|
365
|
+
this.data.set(prn, satMap);
|
|
366
|
+
}
|
|
367
|
+
for (let i = 0; i < codes.length; i++) {
|
|
368
|
+
const code = codes[i];
|
|
369
|
+
const hasValue = values[i] != null && values[i] !== 0;
|
|
370
|
+
let cell = satMap.get(code);
|
|
371
|
+
if (!cell) {
|
|
372
|
+
cell = { seen: false, expected: 0, present: 0 };
|
|
373
|
+
satMap.set(code, cell);
|
|
374
|
+
}
|
|
375
|
+
if (hasValue) cell.seen = true;
|
|
376
|
+
if (cell.seen) cell.expected++;
|
|
377
|
+
if (hasValue) cell.present++;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
finalize() {
|
|
381
|
+
const cells = [];
|
|
382
|
+
const systemSet = /* @__PURE__ */ new Set();
|
|
383
|
+
for (const [prn, satMap] of this.data) {
|
|
384
|
+
const sys = prn[0];
|
|
385
|
+
systemSet.add(sys);
|
|
386
|
+
for (const [code, { seen, expected, present }] of satMap) {
|
|
387
|
+
if (!seen) continue;
|
|
388
|
+
const band = code[1] ?? "?";
|
|
389
|
+
cells.push({
|
|
390
|
+
prn,
|
|
391
|
+
system: sys,
|
|
392
|
+
code,
|
|
393
|
+
band,
|
|
394
|
+
expected,
|
|
395
|
+
present,
|
|
396
|
+
percent: expected > 0 ? present / expected * 100 : 0
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const sigAgg = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const c of cells) {
|
|
402
|
+
const key = `${c.system}:${c.code}`;
|
|
403
|
+
let agg = sigAgg.get(key);
|
|
404
|
+
if (!agg) {
|
|
405
|
+
agg = { expected: 0, present: 0, sats: /* @__PURE__ */ new Set() };
|
|
406
|
+
sigAgg.set(key, agg);
|
|
407
|
+
}
|
|
408
|
+
agg.expected += c.expected;
|
|
409
|
+
agg.present += c.present;
|
|
410
|
+
agg.sats.add(c.prn);
|
|
411
|
+
}
|
|
412
|
+
const signalStats = [];
|
|
413
|
+
for (const [key, agg] of sigAgg) {
|
|
414
|
+
const [sys, code] = key.split(":");
|
|
415
|
+
const band = code[1] ?? "?";
|
|
416
|
+
const bandLabel = BAND_LABELS[sys]?.[band] ?? `Band ${band}`;
|
|
417
|
+
const sysName = SYSTEM_NAMES[sys] ?? sys;
|
|
418
|
+
signalStats.push({
|
|
419
|
+
code,
|
|
420
|
+
system: sys,
|
|
421
|
+
label: `${code} (${sysName} ${bandLabel})`,
|
|
422
|
+
expected: agg.expected,
|
|
423
|
+
present: agg.present,
|
|
424
|
+
percent: agg.expected > 0 ? agg.present / agg.expected * 100 : 0,
|
|
425
|
+
satellites: agg.sats.size
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const sysOrder = "GRECIJS";
|
|
429
|
+
signalStats.sort((a, b) => {
|
|
430
|
+
const da = sysOrder.indexOf(a.system);
|
|
431
|
+
const db = sysOrder.indexOf(b.system);
|
|
432
|
+
if (da !== db) return (da === -1 ? 99 : da) - (db === -1 ? 99 : db);
|
|
433
|
+
return a.code.localeCompare(b.code);
|
|
434
|
+
});
|
|
435
|
+
const systems = [...systemSet].sort((a, b) => {
|
|
436
|
+
return (sysOrder.indexOf(a) === -1 ? 99 : sysOrder.indexOf(a)) - (sysOrder.indexOf(b) === -1 ? 99 : sysOrder.indexOf(b));
|
|
437
|
+
});
|
|
438
|
+
return { cells, signalStats, systems };
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/analysis/quality-analysis.ts
|
|
443
|
+
async function analyzeQuality(file, header, onProgress, signal) {
|
|
444
|
+
const mpAccum = new MultipathAccumulator(header);
|
|
445
|
+
const csAccum = new CycleSlipAccumulator(header, (time, prn, bands) => {
|
|
446
|
+
mpAccum.notifySlip(time, prn, bands);
|
|
447
|
+
});
|
|
448
|
+
const compAccum = new CompletenessAccumulator(header);
|
|
449
|
+
await parseRinexStream(
|
|
450
|
+
file,
|
|
451
|
+
onProgress,
|
|
452
|
+
signal,
|
|
453
|
+
(time, prn, codes, values) => {
|
|
454
|
+
csAccum.onObservation(time, prn, codes, values);
|
|
455
|
+
mpAccum.onObservation(time, prn, codes, values);
|
|
456
|
+
compAccum.onObservation(time, prn, codes, values);
|
|
457
|
+
},
|
|
458
|
+
true
|
|
459
|
+
/* workerMode: skip EpochSummary construction */
|
|
460
|
+
);
|
|
461
|
+
return {
|
|
462
|
+
cycleSlips: csAccum.finalize(),
|
|
463
|
+
completeness: compAccum.finalize(),
|
|
464
|
+
multipath: mpAccum.finalize()
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export {
|
|
469
|
+
MultipathAccumulator,
|
|
470
|
+
CycleSlipAccumulator,
|
|
471
|
+
CompletenessAccumulator,
|
|
472
|
+
analyzeQuality
|
|
473
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// src/nmea/index.ts
|
|
2
|
+
function nmeaCoordToDecimal(raw, direction) {
|
|
3
|
+
if (!raw || !direction) return NaN;
|
|
4
|
+
const isLon = direction === "E" || direction === "W";
|
|
5
|
+
const degLen = isLon ? 3 : 2;
|
|
6
|
+
const degrees = parseInt(raw.substring(0, degLen), 10);
|
|
7
|
+
const minutes = parseFloat(raw.substring(degLen));
|
|
8
|
+
const decimal = degrees + minutes / 60;
|
|
9
|
+
return direction === "S" || direction === "W" ? -decimal : decimal;
|
|
10
|
+
}
|
|
11
|
+
function verifyChecksum(sentence) {
|
|
12
|
+
const starIdx = sentence.indexOf("*");
|
|
13
|
+
if (starIdx === -1) return false;
|
|
14
|
+
const body = sentence.substring(sentence.indexOf("$") + 1, starIdx);
|
|
15
|
+
let xor = 0;
|
|
16
|
+
for (let i = 0; i < body.length; i++) xor ^= body.charCodeAt(i);
|
|
17
|
+
const expected = parseInt(sentence.substring(starIdx + 1, starIdx + 3), 16);
|
|
18
|
+
return xor === expected;
|
|
19
|
+
}
|
|
20
|
+
function buildDate(timeStr, dateStr) {
|
|
21
|
+
if (!timeStr || timeStr.length < 6) return null;
|
|
22
|
+
const h = parseInt(timeStr.substring(0, 2), 10);
|
|
23
|
+
const m = parseInt(timeStr.substring(2, 4), 10);
|
|
24
|
+
const s = parseFloat(timeStr.substring(4));
|
|
25
|
+
if (isNaN(h) || isNaN(m) || isNaN(s)) return null;
|
|
26
|
+
let year = 2e3, month = 0, day = 1;
|
|
27
|
+
if (dateStr && dateStr.length >= 6) {
|
|
28
|
+
day = parseInt(dateStr.substring(0, 2), 10);
|
|
29
|
+
month = parseInt(dateStr.substring(2, 4), 10) - 1;
|
|
30
|
+
const yy = parseInt(dateStr.substring(4, 6), 10);
|
|
31
|
+
year = yy < 80 ? 2e3 + yy : 1900 + yy;
|
|
32
|
+
}
|
|
33
|
+
const ms = Math.round(s % 1 * 1e3);
|
|
34
|
+
return new Date(Date.UTC(year, month, day, h, m, Math.floor(s), ms));
|
|
35
|
+
}
|
|
36
|
+
function parseGGA(fields) {
|
|
37
|
+
if (fields.length < 10) return null;
|
|
38
|
+
const quality = parseInt(fields[6], 10);
|
|
39
|
+
if (isNaN(quality) || quality === 0) return null;
|
|
40
|
+
const lat = nmeaCoordToDecimal(fields[2], fields[3]);
|
|
41
|
+
const lon = nmeaCoordToDecimal(fields[4], fields[5]);
|
|
42
|
+
if (isNaN(lat) || isNaN(lon)) return null;
|
|
43
|
+
return {
|
|
44
|
+
time: buildDate(fields[1]),
|
|
45
|
+
timeKey: fields[1],
|
|
46
|
+
lat,
|
|
47
|
+
lon,
|
|
48
|
+
alt: fields[9] ? parseFloat(fields[9]) : null,
|
|
49
|
+
satellites: fields[7] ? parseInt(fields[7], 10) : null,
|
|
50
|
+
fixQuality: quality
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function parseRMC(fields) {
|
|
54
|
+
if (fields.length < 10) return null;
|
|
55
|
+
if (fields[2] !== "A") return null;
|
|
56
|
+
const lat = nmeaCoordToDecimal(fields[3], fields[4]);
|
|
57
|
+
const lon = nmeaCoordToDecimal(fields[5], fields[6]);
|
|
58
|
+
if (isNaN(lat) || isNaN(lon)) return null;
|
|
59
|
+
return {
|
|
60
|
+
time: buildDate(fields[1], fields[9]),
|
|
61
|
+
timeKey: fields[1],
|
|
62
|
+
lat,
|
|
63
|
+
lon,
|
|
64
|
+
speed: fields[7] ? parseFloat(fields[7]) : null,
|
|
65
|
+
course: fields[8] ? parseFloat(fields[8]) : null,
|
|
66
|
+
hasDate: true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function computeStats(fixes) {
|
|
70
|
+
const valid = fixes.filter((f) => f.fixQuality > 0);
|
|
71
|
+
const withTime = valid.filter((f) => f.time !== null);
|
|
72
|
+
const withSat = valid.filter((f) => f.satellites !== null).map((f) => f.satellites);
|
|
73
|
+
let startTime = null;
|
|
74
|
+
let endTime = null;
|
|
75
|
+
let duration = null;
|
|
76
|
+
if (withTime.length >= 2) {
|
|
77
|
+
startTime = withTime[0].time;
|
|
78
|
+
endTime = withTime[withTime.length - 1].time;
|
|
79
|
+
duration = (endTime.getTime() - startTime.getTime()) / 1e3;
|
|
80
|
+
} else if (withTime.length === 1) {
|
|
81
|
+
startTime = endTime = withTime[0].time;
|
|
82
|
+
}
|
|
83
|
+
let totalDistance = null;
|
|
84
|
+
if (valid.length >= 2) {
|
|
85
|
+
let dist = 0;
|
|
86
|
+
for (let i = 1; i < valid.length; i++) {
|
|
87
|
+
const prev = valid[i - 1], curr = valid[i];
|
|
88
|
+
dist += haversine(prev.lat, prev.lon, curr.lat, curr.lon);
|
|
89
|
+
}
|
|
90
|
+
totalDistance = dist;
|
|
91
|
+
}
|
|
92
|
+
const speeds = valid.filter((f) => f.speed !== null).map((f) => f.speed * 1.852);
|
|
93
|
+
const maxSpeed = speeds.length > 0 ? Math.max(...speeds) : null;
|
|
94
|
+
let cep = null;
|
|
95
|
+
let drms2 = null;
|
|
96
|
+
let hRms = null;
|
|
97
|
+
let vRms = null;
|
|
98
|
+
if (valid.length >= 2) {
|
|
99
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
100
|
+
const R = 6378137;
|
|
101
|
+
const e2 = 0.00669437999014;
|
|
102
|
+
const ecef = valid.map((f) => {
|
|
103
|
+
const lat = toRad(f.lat), lon = toRad(f.lon), h = f.alt ?? 0;
|
|
104
|
+
const N = R / Math.sqrt(1 - e2 * Math.sin(lat) ** 2);
|
|
105
|
+
return [
|
|
106
|
+
(N + h) * Math.cos(lat) * Math.cos(lon),
|
|
107
|
+
(N + h) * Math.cos(lat) * Math.sin(lon),
|
|
108
|
+
((1 - e2) * N + h) * Math.sin(lat)
|
|
109
|
+
];
|
|
110
|
+
});
|
|
111
|
+
const n = ecef.length;
|
|
112
|
+
const mx = ecef.reduce((s, p) => s + p[0], 0) / n;
|
|
113
|
+
const my = ecef.reduce((s, p) => s + p[1], 0) / n;
|
|
114
|
+
const mz = ecef.reduce((s, p) => s + p[2], 0) / n;
|
|
115
|
+
const refLon = Math.atan2(my, mx);
|
|
116
|
+
const refP = Math.sqrt(mx * mx + my * my);
|
|
117
|
+
const refLat = Math.atan2(mz, refP * (1 - e2));
|
|
118
|
+
const sinLat = Math.sin(refLat), cosLat = Math.cos(refLat);
|
|
119
|
+
const sinLon = Math.sin(refLon), cosLon = Math.cos(refLon);
|
|
120
|
+
const enu = ecef.map((p) => {
|
|
121
|
+
const dx = p[0] - mx, dy = p[1] - my, dz = p[2] - mz;
|
|
122
|
+
return {
|
|
123
|
+
e: -sinLon * dx + cosLon * dy,
|
|
124
|
+
n: -cosLon * sinLat * dx - sinLon * sinLat * dy + cosLat * dz,
|
|
125
|
+
u: cosLon * cosLat * dx + sinLon * cosLat * dy + sinLat * dz
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
const hDist = enu.map((p) => Math.sqrt(p.e * p.e + p.n * p.n));
|
|
129
|
+
hDist.sort((a, b) => a - b);
|
|
130
|
+
const medIdx = Math.floor(hDist.length * 0.5);
|
|
131
|
+
cep = hDist[medIdx];
|
|
132
|
+
const hSqSum = enu.reduce((s, p) => s + p.e * p.e + p.n * p.n, 0);
|
|
133
|
+
hRms = Math.sqrt(hSqSum / n);
|
|
134
|
+
drms2 = 2 * hRms;
|
|
135
|
+
const vSqSum = enu.reduce((s, p) => s + p.u * p.u, 0);
|
|
136
|
+
vRms = Math.sqrt(vSqSum / n);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
totalFixes: fixes.length,
|
|
140
|
+
validFixes: valid.length,
|
|
141
|
+
duration,
|
|
142
|
+
startTime,
|
|
143
|
+
endTime,
|
|
144
|
+
totalDistance,
|
|
145
|
+
maxSpeed,
|
|
146
|
+
cep,
|
|
147
|
+
drms2,
|
|
148
|
+
hRms,
|
|
149
|
+
vRms,
|
|
150
|
+
avgSatellites: withSat.length > 0 ? withSat.reduce((a, b) => a + b, 0) / withSat.length : null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function haversine(lat1, lon1, lat2, lon2) {
|
|
154
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
155
|
+
const dLat = toRad(lat2 - lat1);
|
|
156
|
+
const dLon = toRad(lon2 - lon1);
|
|
157
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
|
158
|
+
return 6371e3 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
159
|
+
}
|
|
160
|
+
function parseNmeaFile(content) {
|
|
161
|
+
const lines = content.split(/\r?\n/);
|
|
162
|
+
const fixMap = /* @__PURE__ */ new Map();
|
|
163
|
+
const fixOrder = [];
|
|
164
|
+
let lastDate;
|
|
165
|
+
for (const raw of lines) {
|
|
166
|
+
const line = raw.trim();
|
|
167
|
+
if (!line.startsWith("$")) continue;
|
|
168
|
+
if (line.includes("*") && !verifyChecksum(line)) continue;
|
|
169
|
+
const fields = line.split("*")[0].split(",");
|
|
170
|
+
const sentenceId = fields[0].substring(3);
|
|
171
|
+
let partial = null;
|
|
172
|
+
if (sentenceId === "GGA") partial = parseGGA(fields);
|
|
173
|
+
else if (sentenceId === "RMC") partial = parseRMC(fields);
|
|
174
|
+
if (!partial) continue;
|
|
175
|
+
const key = partial.timeKey || `line-${fixOrder.length}`;
|
|
176
|
+
if (partial.hasDate && partial.time) {
|
|
177
|
+
const d = partial.time;
|
|
178
|
+
lastDate = `${String(d.getUTCDate()).padStart(2, "0")}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCFullYear() % 100).padStart(2, "0")}`;
|
|
179
|
+
}
|
|
180
|
+
const existing = fixMap.get(key);
|
|
181
|
+
if (existing) {
|
|
182
|
+
if (partial.alt !== void 0 && partial.alt !== null)
|
|
183
|
+
existing.alt = partial.alt;
|
|
184
|
+
if (partial.satellites !== void 0 && partial.satellites !== null)
|
|
185
|
+
existing.satellites = partial.satellites;
|
|
186
|
+
if (partial.fixQuality !== void 0)
|
|
187
|
+
existing.fixQuality = partial.fixQuality;
|
|
188
|
+
if (partial.speed !== void 0 && partial.speed !== null)
|
|
189
|
+
existing.speed = partial.speed;
|
|
190
|
+
if (partial.course !== void 0 && partial.course !== null)
|
|
191
|
+
existing.course = partial.course;
|
|
192
|
+
if (partial.hasDate && partial.time) existing.time = partial.time;
|
|
193
|
+
} else {
|
|
194
|
+
let time = partial.time;
|
|
195
|
+
if (!partial.hasDate && time && lastDate) {
|
|
196
|
+
time = buildDate(partial.timeKey, lastDate);
|
|
197
|
+
}
|
|
198
|
+
fixMap.set(key, {
|
|
199
|
+
time,
|
|
200
|
+
lat: partial.lat ?? 0,
|
|
201
|
+
lon: partial.lon ?? 0,
|
|
202
|
+
alt: partial.alt ?? null,
|
|
203
|
+
satellites: partial.satellites ?? null,
|
|
204
|
+
fixQuality: partial.fixQuality ?? 0,
|
|
205
|
+
speed: partial.speed ?? null,
|
|
206
|
+
course: partial.course ?? null
|
|
207
|
+
});
|
|
208
|
+
fixOrder.push(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const fixes = fixOrder.map((k) => fixMap.get(k));
|
|
212
|
+
return { fixes, stats: computeStats(fixes) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
nmeaCoordToDecimal,
|
|
217
|
+
verifyChecksum,
|
|
218
|
+
computeStats,
|
|
219
|
+
parseNmeaFile
|
|
220
|
+
};
|