gnss-js 1.4.1 → 1.5.1
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/README.md +46 -23
- package/dist/analysis.cjs +168 -20
- package/dist/analysis.d.cts +54 -4
- package/dist/analysis.d.ts +54 -4
- package/dist/analysis.js +7 -3
- package/dist/{chunk-PQYBTE42.js → chunk-CD5YSYCG.js} +11 -17
- package/dist/{chunk-65CQINSB.js → chunk-FGRC3PB3.js} +6 -2
- package/dist/{chunk-UQ7D53R2.js → chunk-GDGHTC3E.js} +162 -16
- package/dist/{chunk-PX7TPPTQ.js → chunk-SOJNWF5T.js} +1 -1
- package/dist/index.cjs +181 -35
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -5
- package/dist/orbit.cjs +11 -17
- package/dist/orbit.d.cts +1 -1
- package/dist/orbit.d.ts +1 -1
- package/dist/orbit.js +1 -1
- package/dist/positioning.cjs +2 -0
- package/dist/positioning.js +2 -2
- package/dist/rtcm3.cjs +6 -2
- package/dist/rtcm3.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -94,12 +94,16 @@ const itrf = transformFrame(
|
|
|
94
94
|
```ts
|
|
95
95
|
import { parseRinexStream, parseNavFile } from 'gnss-js/rinex';
|
|
96
96
|
|
|
97
|
-
// Streaming observation parser (
|
|
98
|
-
const result = await parseRinexStream(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
// Streaming observation parser (positional arguments)
|
|
98
|
+
const result = await parseRinexStream(
|
|
99
|
+
file, // File (plain, Hatanaka .crx, or .gz)
|
|
100
|
+
(percent) => {}, // optional progress callback
|
|
101
|
+
undefined, // optional AbortSignal
|
|
102
|
+
(time, prn, codes, values) => {
|
|
103
|
+
// per-satellite observation callback
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
// result.header: RinexHeader
|
|
103
107
|
|
|
104
108
|
// Navigation file parser
|
|
105
109
|
const nav = parseNavFile(navFileText);
|
|
@@ -125,28 +129,36 @@ for (const frame of decoder) {
|
|
|
125
129
|
import { computeAllPositions, computeDop } from 'gnss-js/orbit';
|
|
126
130
|
|
|
127
131
|
const positions = computeAllPositions(ephemerides, times, receiverPosition);
|
|
128
|
-
// positions.prns: string[], positions.times: number[]
|
|
132
|
+
// positions.prns: string[], positions.times: number[]
|
|
133
|
+
// positions.positions[prn][epochIdx]: { lat, lon, az, el } | null
|
|
129
134
|
```
|
|
130
135
|
|
|
131
136
|
### Signal analysis
|
|
132
137
|
|
|
133
138
|
```ts
|
|
134
|
-
import {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
import { analyzeQuality } from 'gnss-js/analysis';
|
|
140
|
+
|
|
141
|
+
// One re-parse pass: multipath (per code band, Anubis-style), cycle
|
|
142
|
+
// slips, completeness, and slant-TEC ionosphere series.
|
|
143
|
+
const q = await analyzeQuality(file, header);
|
|
144
|
+
// q.multipath, q.cycleSlips, q.completeness, q.iono
|
|
145
|
+
|
|
146
|
+
// The accumulators (MultipathAccumulator, CycleSlipAccumulator,
|
|
147
|
+
// CompletenessAccumulator, IonoAccumulator) are also exported for
|
|
148
|
+
// wiring into a custom parseRinexStream pass.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Ionosphere and DCBs
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { parseSinexBiasDcb, applyIonoDcb } from 'gnss-js/analysis';
|
|
155
|
+
|
|
156
|
+
// q.iono: slant TEC per satellite from the geometry-free phase
|
|
157
|
+
// levelled to the code — DCB-biased until calibrated:
|
|
158
|
+
const satDcb = parseSinexBiasDcb(sinexBiasText, obsEpochMs); // ESA .BIA / CAS .BSX
|
|
159
|
+
const { result, receiverDcbTecu } = applyIonoDcb(q.iono, satDcb);
|
|
160
|
+
// result.series[n].points: calibrated STEC (TECU); receiver bias
|
|
161
|
+
// estimated from the night-time floor per system/signal pair.
|
|
150
162
|
```
|
|
151
163
|
|
|
152
164
|
### NTRIP client
|
|
@@ -173,6 +185,17 @@ const { reader, abort } = await connectToMountpoint(
|
|
|
173
185
|
);
|
|
174
186
|
```
|
|
175
187
|
|
|
188
|
+
### NMEA and ANTEX
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { parseNmeaFile, computeStats } from 'gnss-js/nmea';
|
|
192
|
+
import { parseAntex } from 'gnss-js/antex';
|
|
193
|
+
|
|
194
|
+
const track = parseNmeaFile(nmeaText); // fixes, satellites, per-sentence records
|
|
195
|
+
const stats = computeStats(track.fixes); // position/HDOP/speed statistics
|
|
196
|
+
const antex = parseAntex(antexText); // antenna PCO/PCV calibrations
|
|
197
|
+
```
|
|
198
|
+
|
|
176
199
|
### Constants
|
|
177
200
|
|
|
178
201
|
```ts
|
package/dist/analysis.cjs
CHANGED
|
@@ -24,10 +24,24 @@ __export(analysis_exports, {
|
|
|
24
24
|
CycleSlipAccumulator: () => CycleSlipAccumulator,
|
|
25
25
|
IonoAccumulator: () => IonoAccumulator,
|
|
26
26
|
MultipathAccumulator: () => MultipathAccumulator,
|
|
27
|
-
analyzeQuality: () => analyzeQuality
|
|
27
|
+
analyzeQuality: () => analyzeQuality,
|
|
28
|
+
applyIonoDcb: () => applyIonoDcb,
|
|
29
|
+
parseSinexBiasDcb: () => parseSinexBiasDcb
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(analysis_exports);
|
|
30
32
|
|
|
33
|
+
// src/analysis/stats-util.ts
|
|
34
|
+
var MIN_ARC_LENGTH = 10;
|
|
35
|
+
function median(values) {
|
|
36
|
+
const s = [...values].sort((a, b) => a - b);
|
|
37
|
+
const m = s.length >> 1;
|
|
38
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
39
|
+
}
|
|
40
|
+
function percentile(values, p) {
|
|
41
|
+
const s = [...values].sort((a, b) => a - b);
|
|
42
|
+
return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
// src/constants/gnss.ts
|
|
32
46
|
var SYSTEM_NAMES = {
|
|
33
47
|
G: "GPS",
|
|
@@ -215,15 +229,9 @@ function buildObsIndices(header) {
|
|
|
215
229
|
var ARC_GAP_FACTOR = 5;
|
|
216
230
|
|
|
217
231
|
// src/analysis/multipath.ts
|
|
218
|
-
var MIN_ARC_LENGTH = 10;
|
|
219
232
|
var MP_JUMP_M = 1.25;
|
|
220
233
|
var MP_JUMP_WINDOW = 5;
|
|
221
234
|
var MP_EDIT_SIGMA = 3;
|
|
222
|
-
function median(values) {
|
|
223
|
-
const s = [...values].sort((a, b) => a - b);
|
|
224
|
-
const m = s.length >> 1;
|
|
225
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
226
|
-
}
|
|
227
235
|
var MultipathAccumulator = class {
|
|
228
236
|
state = /* @__PURE__ */ new Map();
|
|
229
237
|
results = [];
|
|
@@ -704,12 +712,6 @@ var CompletenessAccumulator = class {
|
|
|
704
712
|
// src/analysis/ionosphere.ts
|
|
705
713
|
var TEC_FACTOR = 403e15;
|
|
706
714
|
var GF_JUMP_M = 0.15;
|
|
707
|
-
var MIN_ARC_LENGTH2 = 10;
|
|
708
|
-
function median2(values) {
|
|
709
|
-
const s = [...values].sort((a, b) => a - b);
|
|
710
|
-
const m = s.length >> 1;
|
|
711
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
712
|
-
}
|
|
713
715
|
var IonoAccumulator = class {
|
|
714
716
|
state = /* @__PURE__ */ new Map();
|
|
715
717
|
closed = /* @__PURE__ */ new Map();
|
|
@@ -717,13 +719,15 @@ var IonoAccumulator = class {
|
|
|
717
719
|
obsIndices;
|
|
718
720
|
gloChannels;
|
|
719
721
|
pairLabel = /* @__PURE__ */ new Map();
|
|
722
|
+
pairCodes = /* @__PURE__ */ new Map();
|
|
723
|
+
pairMeta = /* @__PURE__ */ new Map();
|
|
720
724
|
constructor(header) {
|
|
721
725
|
this.interval = header.interval ?? 30;
|
|
722
726
|
this.obsIndices = buildObsIndices(header);
|
|
723
727
|
this.gloChannels = buildGloChannelMap(header.glonassSlots);
|
|
724
728
|
}
|
|
725
729
|
/** Observation callback — wire this into parseRinexStream. */
|
|
726
|
-
onObservation = (time, prn,
|
|
730
|
+
onObservation = (time, prn, codes, values) => {
|
|
727
731
|
const sys = prn[0];
|
|
728
732
|
const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
|
|
729
733
|
if (!bandMap) return;
|
|
@@ -734,7 +738,12 @@ var IonoAccumulator = class {
|
|
|
734
738
|
const lVal = values[L];
|
|
735
739
|
const freq = getFreq(this.gloChannels, prn, band);
|
|
736
740
|
if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
|
|
737
|
-
bandData.set(band, {
|
|
741
|
+
bandData.set(band, {
|
|
742
|
+
C: cVal,
|
|
743
|
+
L: lVal,
|
|
744
|
+
f: freq,
|
|
745
|
+
code: codes[C] ?? `C${band}`
|
|
746
|
+
});
|
|
738
747
|
}
|
|
739
748
|
}
|
|
740
749
|
if (bandData.size < 2) return;
|
|
@@ -753,7 +762,9 @@ var IonoAccumulator = class {
|
|
|
753
762
|
const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
|
|
754
763
|
const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
|
|
755
764
|
this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
|
|
765
|
+
this.pairCodes.set(`${sys}:${pairKey}`, [di.code, dj.code]);
|
|
756
766
|
}
|
|
767
|
+
this.pairMeta.set(`${prn}:${pairKey}`, { fi: di.f, gamma });
|
|
757
768
|
this.push(prn, pairKey, time, l4, p4, di.f, gamma);
|
|
758
769
|
break;
|
|
759
770
|
}
|
|
@@ -803,8 +814,8 @@ var IonoAccumulator = class {
|
|
|
803
814
|
}
|
|
804
815
|
closeArc(prn, pairKey, ps) {
|
|
805
816
|
const arc = ps.arc;
|
|
806
|
-
if (arc.times.length >=
|
|
807
|
-
const level =
|
|
817
|
+
if (arc.times.length >= MIN_ARC_LENGTH) {
|
|
818
|
+
const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
|
|
808
819
|
const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
|
|
809
820
|
let satArcs = this.closed.get(prn);
|
|
810
821
|
if (!satArcs) {
|
|
@@ -835,7 +846,7 @@ var IonoAccumulator = class {
|
|
|
835
846
|
const series = [];
|
|
836
847
|
let sum = 0;
|
|
837
848
|
let count = 0;
|
|
838
|
-
let maxStec =
|
|
849
|
+
let maxStec = -Infinity;
|
|
839
850
|
for (const [prn, satArcs] of this.closed) {
|
|
840
851
|
let bestKey = null;
|
|
841
852
|
let bestLen = 0;
|
|
@@ -854,18 +865,153 @@ var IonoAccumulator = class {
|
|
|
854
865
|
count++;
|
|
855
866
|
if (p.stec > maxStec) maxStec = p.stec;
|
|
856
867
|
}
|
|
868
|
+
const meta = this.pairMeta.get(`${prn}:${bestKey}`);
|
|
869
|
+
const tecuPerNs = meta ? C_LIGHT * 1e-9 / (meta.gamma - 1) * (meta.fi * meta.fi / TEC_FACTOR) : 0;
|
|
857
870
|
series.push({
|
|
858
871
|
prn,
|
|
859
872
|
system: sys,
|
|
860
873
|
label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
|
|
874
|
+
codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
|
|
875
|
+
tecuPerNs,
|
|
861
876
|
points
|
|
862
877
|
});
|
|
863
878
|
}
|
|
864
879
|
series.sort((a, b) => a.prn.localeCompare(b.prn));
|
|
865
|
-
return {
|
|
880
|
+
return {
|
|
881
|
+
series,
|
|
882
|
+
maxStec: count > 0 ? maxStec : 0,
|
|
883
|
+
meanStec: count > 0 ? sum / count : 0
|
|
884
|
+
};
|
|
866
885
|
}
|
|
867
886
|
};
|
|
868
887
|
|
|
888
|
+
// src/analysis/dcb.ts
|
|
889
|
+
var PRN_RE = /^[A-Z]\d{2}$/;
|
|
890
|
+
var SVN_RE = /^[A-Z]\d{3}$/;
|
|
891
|
+
var OBS_RE = /^[CL]\d[A-Z]$/;
|
|
892
|
+
function sinexEpochMs(s, openValue) {
|
|
893
|
+
const [y, d, sec] = s.split(":").map(Number);
|
|
894
|
+
if (!y) return openValue;
|
|
895
|
+
return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
|
|
896
|
+
}
|
|
897
|
+
function parseSinexBiasDcb(text, epochMs) {
|
|
898
|
+
const rows = /* @__PURE__ */ new Map();
|
|
899
|
+
for (const line of text.split("\n")) {
|
|
900
|
+
if (!line.startsWith(" DSB") && !line.startsWith("DSB")) continue;
|
|
901
|
+
const t = line.trim().split(/\s+/);
|
|
902
|
+
if (t.length < 9 || !SVN_RE.test(t[1]) || !PRN_RE.test(t[2]) || !OBS_RE.test(t[3]) || !OBS_RE.test(t[4])) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const unitIdx = t.indexOf("ns");
|
|
906
|
+
if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
|
|
907
|
+
const value = parseFloat(t[unitIdx + 1]);
|
|
908
|
+
if (!isFinite(value)) continue;
|
|
909
|
+
const start = sinexEpochMs(t[5] ?? "", -Infinity);
|
|
910
|
+
const end = sinexEpochMs(t[6] ?? "", Infinity);
|
|
911
|
+
const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
|
|
912
|
+
const prn = t[2];
|
|
913
|
+
const pair = `${t[3]}-${t[4]}`;
|
|
914
|
+
let sat = rows.get(prn);
|
|
915
|
+
if (!sat) {
|
|
916
|
+
sat = /* @__PURE__ */ new Map();
|
|
917
|
+
rows.set(prn, sat);
|
|
918
|
+
}
|
|
919
|
+
const prev = sat.get(pair);
|
|
920
|
+
if (!prev || covers && !prev.covers || covers === prev.covers && end > prev.end) {
|
|
921
|
+
sat.set(pair, { value, end, covers });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const out = /* @__PURE__ */ new Map();
|
|
925
|
+
for (const [prn, sat] of rows) {
|
|
926
|
+
const m = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const [pair, row] of sat) m.set(pair, row.value);
|
|
928
|
+
out.set(prn, m);
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
function applyIonoDcb(iono, satDcb) {
|
|
933
|
+
const missing = [];
|
|
934
|
+
let corrected = 0;
|
|
935
|
+
const dcbFor = (s) => {
|
|
936
|
+
const sat = satDcb.get(s.prn);
|
|
937
|
+
if (!sat) return null;
|
|
938
|
+
const fwd = sat.get(`${s.codes[0]}-${s.codes[1]}`);
|
|
939
|
+
if (fwd !== void 0) return fwd;
|
|
940
|
+
const rev = sat.get(`${s.codes[1]}-${s.codes[0]}`);
|
|
941
|
+
if (rev !== void 0) return -rev;
|
|
942
|
+
return null;
|
|
943
|
+
};
|
|
944
|
+
const series = iono.series.map((s) => {
|
|
945
|
+
const dcbNs = dcbFor(s);
|
|
946
|
+
if (dcbNs === null) {
|
|
947
|
+
missing.push(s.prn);
|
|
948
|
+
return s;
|
|
949
|
+
}
|
|
950
|
+
corrected++;
|
|
951
|
+
const shift = dcbNs * s.tecuPerNs;
|
|
952
|
+
return {
|
|
953
|
+
...s,
|
|
954
|
+
points: s.points.map((p) => ({ time: p.time, stec: p.stec + shift }))
|
|
955
|
+
};
|
|
956
|
+
});
|
|
957
|
+
const missingSet = new Set(missing);
|
|
958
|
+
const receiverDcbTecu = {};
|
|
959
|
+
const groups = /* @__PURE__ */ new Map();
|
|
960
|
+
for (const s of series) {
|
|
961
|
+
const key = `${s.system} ${s.label}`;
|
|
962
|
+
let g = groups.get(key);
|
|
963
|
+
if (!g) {
|
|
964
|
+
g = [];
|
|
965
|
+
groups.set(key, g);
|
|
966
|
+
}
|
|
967
|
+
g.push(s);
|
|
968
|
+
}
|
|
969
|
+
const out = [];
|
|
970
|
+
for (const [key, group] of groups) {
|
|
971
|
+
const samples = [];
|
|
972
|
+
for (const s of group) {
|
|
973
|
+
if (missingSet.has(s.prn)) continue;
|
|
974
|
+
for (const p of s.points) samples.push(p.stec);
|
|
975
|
+
}
|
|
976
|
+
if (samples.length >= 100) {
|
|
977
|
+
const rx = percentile(samples, 0.01);
|
|
978
|
+
receiverDcbTecu[key] = rx;
|
|
979
|
+
for (const s of group) {
|
|
980
|
+
out.push({
|
|
981
|
+
...s,
|
|
982
|
+
points: s.points.map((p) => ({
|
|
983
|
+
time: p.time,
|
|
984
|
+
stec: p.stec - rx
|
|
985
|
+
}))
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
out.push(...group);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
out.sort((a, b) => a.prn.localeCompare(b.prn));
|
|
993
|
+
let sum = 0;
|
|
994
|
+
let count = 0;
|
|
995
|
+
let maxStec = -Infinity;
|
|
996
|
+
for (const s of out) {
|
|
997
|
+
for (const p of s.points) {
|
|
998
|
+
sum += p.stec;
|
|
999
|
+
count++;
|
|
1000
|
+
if (p.stec > maxStec) maxStec = p.stec;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
result: {
|
|
1005
|
+
series: out,
|
|
1006
|
+
maxStec: count > 0 ? maxStec : 0,
|
|
1007
|
+
meanStec: count > 0 ? sum / count : 0
|
|
1008
|
+
},
|
|
1009
|
+
satellitesCorrected: corrected,
|
|
1010
|
+
satellitesMissing: [...new Set(missing)].sort(),
|
|
1011
|
+
receiverDcbTecu
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
869
1015
|
// src/rinex/crx.ts
|
|
870
1016
|
function crxRepair(old, diff) {
|
|
871
1017
|
const chars = old.split("");
|
|
@@ -1687,5 +1833,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
|
|
|
1687
1833
|
CycleSlipAccumulator,
|
|
1688
1834
|
IonoAccumulator,
|
|
1689
1835
|
MultipathAccumulator,
|
|
1690
|
-
analyzeQuality
|
|
1836
|
+
analyzeQuality,
|
|
1837
|
+
applyIonoDcb,
|
|
1838
|
+
parseSinexBiasDcb
|
|
1691
1839
|
});
|
package/dist/analysis.d.cts
CHANGED
|
@@ -26,7 +26,7 @@ interface MultipathSeries {
|
|
|
26
26
|
band: string;
|
|
27
27
|
/** Reference band digit used in dual-frequency combination, e.g. "2". */
|
|
28
28
|
refBand: string;
|
|
29
|
-
/** Human-readable label, e.g. "
|
|
29
|
+
/** Human-readable label, e.g. "G01 MP L1-L2". */
|
|
30
30
|
label: string;
|
|
31
31
|
/** Time series of multipath values. */
|
|
32
32
|
points: MultipathPoint[];
|
|
@@ -34,7 +34,7 @@ interface MultipathSeries {
|
|
|
34
34
|
rms: number;
|
|
35
35
|
}
|
|
36
36
|
interface MultipathSignalStat {
|
|
37
|
-
/** Human-readable label, e.g. "
|
|
37
|
+
/** Human-readable label, e.g. "MP L1-L2 (GPS)". */
|
|
38
38
|
label: string;
|
|
39
39
|
/** System letter, e.g. "G". */
|
|
40
40
|
system: string;
|
|
@@ -231,6 +231,12 @@ interface IonoSeries {
|
|
|
231
231
|
system: string;
|
|
232
232
|
/** Band pair used, e.g. "L1-L2". */
|
|
233
233
|
label: string;
|
|
234
|
+
/** Observation codes of the pair, e.g. ["C1C", "C2W"] — the key for
|
|
235
|
+
* matching differential code bias products. */
|
|
236
|
+
codes: [string, string];
|
|
237
|
+
/** TECU shift caused by 1 ns of geometry-free code bias (B_i − B_j);
|
|
238
|
+
* multiply a product DCB in ns by this to correct the series. */
|
|
239
|
+
tecuPerNs: number;
|
|
234
240
|
/** Time series of slant TEC values (TECU, DCB-biased). */
|
|
235
241
|
points: IonoPoint[];
|
|
236
242
|
}
|
|
@@ -249,9 +255,11 @@ declare class IonoAccumulator {
|
|
|
249
255
|
private obsIndices;
|
|
250
256
|
private gloChannels;
|
|
251
257
|
private pairLabel;
|
|
258
|
+
private pairCodes;
|
|
259
|
+
private pairMeta;
|
|
252
260
|
constructor(header: RinexHeader);
|
|
253
261
|
/** Observation callback — wire this into parseRinexStream. */
|
|
254
|
-
onObservation: (time: number, prn: string,
|
|
262
|
+
onObservation: (time: number, prn: string, codes: string[], values: (number | null)[]) => void;
|
|
255
263
|
private push;
|
|
256
264
|
/** External cycle-slip notification — close affected arcs. */
|
|
257
265
|
notifySlip(_time: number, prn: string, bands: Set<string>): void;
|
|
@@ -260,6 +268,48 @@ declare class IonoAccumulator {
|
|
|
260
268
|
finalize(): IonoResult;
|
|
261
269
|
}
|
|
262
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Differential code bias (DCB) products and their application to the
|
|
273
|
+
* slant-TEC series from the ionosphere module.
|
|
274
|
+
*
|
|
275
|
+
* Satellite DCBs come from SINEX_BIAS files published by the IGS
|
|
276
|
+
* analysis centres (e.g. CAS `CAS0MGXRAP_*_DCB.BSX`). The levelled
|
|
277
|
+
* geometry-free combination carries B_i − B_j (satellite) plus the
|
|
278
|
+
* receiver bias; subtracting the product value per satellite and then
|
|
279
|
+
* estimating the receiver bias from the night-time floor (the levelled
|
|
280
|
+
* combination is low-noise and TEC is near zero at night — Hans van
|
|
281
|
+
* der Marel's "no negative values" criterion) yields calibrated STEC.
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/** PRN → "C1C-C2W" → bias (ns), satellite entries only. */
|
|
285
|
+
type SatDcbMap = Map<string, Map<string, number>>;
|
|
286
|
+
/**
|
|
287
|
+
* Parse satellite DSB entries from a SINEX_BIAS file (ESA .BIA,
|
|
288
|
+
* CAS/GFZ .BSX). Station entries (which carry a site name between PRN
|
|
289
|
+
* and the codes) are skipped. Long-history files publish several
|
|
290
|
+
* validity windows per satellite/pair — with values differing by many
|
|
291
|
+
* ns across SVN swaps — so pass the observation epoch to select the
|
|
292
|
+
* covering window; without it, the latest window wins. Values are
|
|
293
|
+
* returned in ns as published.
|
|
294
|
+
*/
|
|
295
|
+
declare function parseSinexBiasDcb(text: string, epochMs?: number): SatDcbMap;
|
|
296
|
+
interface IonoDcbResult {
|
|
297
|
+
result: IonoResult;
|
|
298
|
+
/** Satellites whose product DCB was found and applied. */
|
|
299
|
+
satellitesCorrected: number;
|
|
300
|
+
/** PRNs with no matching product entry (left sat-DCB-biased). */
|
|
301
|
+
satellitesMissing: string[];
|
|
302
|
+
/** Estimated receiver bias per "system label" group (TECU). */
|
|
303
|
+
receiverDcbTecu: Record<string, number>;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Apply satellite DCBs from a product file to an ionosphere result and
|
|
307
|
+
* estimate the receiver bias per system/pair group from the night-time
|
|
308
|
+
* floor (1st percentile → 0). Series without a product entry get the
|
|
309
|
+
* satellite part left in; they are reported in `satellitesMissing`.
|
|
310
|
+
*/
|
|
311
|
+
declare function applyIonoDcb(iono: IonoResult, satDcb: SatDcbMap): IonoDcbResult;
|
|
312
|
+
|
|
263
313
|
/**
|
|
264
314
|
* Combined quality analysis — runs cycle slip detection, data completeness,
|
|
265
315
|
* multipath and ionosphere analysis in a single file re-parse pass.
|
|
@@ -273,4 +323,4 @@ interface QualityResult {
|
|
|
273
323
|
}
|
|
274
324
|
declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
|
|
275
325
|
|
|
276
|
-
export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoPoint, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, analyzeQuality };
|
|
326
|
+
export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb };
|
package/dist/analysis.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface MultipathSeries {
|
|
|
26
26
|
band: string;
|
|
27
27
|
/** Reference band digit used in dual-frequency combination, e.g. "2". */
|
|
28
28
|
refBand: string;
|
|
29
|
-
/** Human-readable label, e.g. "
|
|
29
|
+
/** Human-readable label, e.g. "G01 MP L1-L2". */
|
|
30
30
|
label: string;
|
|
31
31
|
/** Time series of multipath values. */
|
|
32
32
|
points: MultipathPoint[];
|
|
@@ -34,7 +34,7 @@ interface MultipathSeries {
|
|
|
34
34
|
rms: number;
|
|
35
35
|
}
|
|
36
36
|
interface MultipathSignalStat {
|
|
37
|
-
/** Human-readable label, e.g. "
|
|
37
|
+
/** Human-readable label, e.g. "MP L1-L2 (GPS)". */
|
|
38
38
|
label: string;
|
|
39
39
|
/** System letter, e.g. "G". */
|
|
40
40
|
system: string;
|
|
@@ -231,6 +231,12 @@ interface IonoSeries {
|
|
|
231
231
|
system: string;
|
|
232
232
|
/** Band pair used, e.g. "L1-L2". */
|
|
233
233
|
label: string;
|
|
234
|
+
/** Observation codes of the pair, e.g. ["C1C", "C2W"] — the key for
|
|
235
|
+
* matching differential code bias products. */
|
|
236
|
+
codes: [string, string];
|
|
237
|
+
/** TECU shift caused by 1 ns of geometry-free code bias (B_i − B_j);
|
|
238
|
+
* multiply a product DCB in ns by this to correct the series. */
|
|
239
|
+
tecuPerNs: number;
|
|
234
240
|
/** Time series of slant TEC values (TECU, DCB-biased). */
|
|
235
241
|
points: IonoPoint[];
|
|
236
242
|
}
|
|
@@ -249,9 +255,11 @@ declare class IonoAccumulator {
|
|
|
249
255
|
private obsIndices;
|
|
250
256
|
private gloChannels;
|
|
251
257
|
private pairLabel;
|
|
258
|
+
private pairCodes;
|
|
259
|
+
private pairMeta;
|
|
252
260
|
constructor(header: RinexHeader);
|
|
253
261
|
/** Observation callback — wire this into parseRinexStream. */
|
|
254
|
-
onObservation: (time: number, prn: string,
|
|
262
|
+
onObservation: (time: number, prn: string, codes: string[], values: (number | null)[]) => void;
|
|
255
263
|
private push;
|
|
256
264
|
/** External cycle-slip notification — close affected arcs. */
|
|
257
265
|
notifySlip(_time: number, prn: string, bands: Set<string>): void;
|
|
@@ -260,6 +268,48 @@ declare class IonoAccumulator {
|
|
|
260
268
|
finalize(): IonoResult;
|
|
261
269
|
}
|
|
262
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Differential code bias (DCB) products and their application to the
|
|
273
|
+
* slant-TEC series from the ionosphere module.
|
|
274
|
+
*
|
|
275
|
+
* Satellite DCBs come from SINEX_BIAS files published by the IGS
|
|
276
|
+
* analysis centres (e.g. CAS `CAS0MGXRAP_*_DCB.BSX`). The levelled
|
|
277
|
+
* geometry-free combination carries B_i − B_j (satellite) plus the
|
|
278
|
+
* receiver bias; subtracting the product value per satellite and then
|
|
279
|
+
* estimating the receiver bias from the night-time floor (the levelled
|
|
280
|
+
* combination is low-noise and TEC is near zero at night — Hans van
|
|
281
|
+
* der Marel's "no negative values" criterion) yields calibrated STEC.
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/** PRN → "C1C-C2W" → bias (ns), satellite entries only. */
|
|
285
|
+
type SatDcbMap = Map<string, Map<string, number>>;
|
|
286
|
+
/**
|
|
287
|
+
* Parse satellite DSB entries from a SINEX_BIAS file (ESA .BIA,
|
|
288
|
+
* CAS/GFZ .BSX). Station entries (which carry a site name between PRN
|
|
289
|
+
* and the codes) are skipped. Long-history files publish several
|
|
290
|
+
* validity windows per satellite/pair — with values differing by many
|
|
291
|
+
* ns across SVN swaps — so pass the observation epoch to select the
|
|
292
|
+
* covering window; without it, the latest window wins. Values are
|
|
293
|
+
* returned in ns as published.
|
|
294
|
+
*/
|
|
295
|
+
declare function parseSinexBiasDcb(text: string, epochMs?: number): SatDcbMap;
|
|
296
|
+
interface IonoDcbResult {
|
|
297
|
+
result: IonoResult;
|
|
298
|
+
/** Satellites whose product DCB was found and applied. */
|
|
299
|
+
satellitesCorrected: number;
|
|
300
|
+
/** PRNs with no matching product entry (left sat-DCB-biased). */
|
|
301
|
+
satellitesMissing: string[];
|
|
302
|
+
/** Estimated receiver bias per "system label" group (TECU). */
|
|
303
|
+
receiverDcbTecu: Record<string, number>;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Apply satellite DCBs from a product file to an ionosphere result and
|
|
307
|
+
* estimate the receiver bias per system/pair group from the night-time
|
|
308
|
+
* floor (1st percentile → 0). Series without a product entry get the
|
|
309
|
+
* satellite part left in; they are reported in `satellitesMissing`.
|
|
310
|
+
*/
|
|
311
|
+
declare function applyIonoDcb(iono: IonoResult, satDcb: SatDcbMap): IonoDcbResult;
|
|
312
|
+
|
|
263
313
|
/**
|
|
264
314
|
* Combined quality analysis — runs cycle slip detection, data completeness,
|
|
265
315
|
* multipath and ionosphere analysis in a single file re-parse pass.
|
|
@@ -273,4 +323,4 @@ interface QualityResult {
|
|
|
273
323
|
}
|
|
274
324
|
declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
|
|
275
325
|
|
|
276
|
-
export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoPoint, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, analyzeQuality };
|
|
326
|
+
export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb };
|
package/dist/analysis.js
CHANGED
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
CycleSlipAccumulator,
|
|
4
4
|
IonoAccumulator,
|
|
5
5
|
MultipathAccumulator,
|
|
6
|
-
analyzeQuality
|
|
7
|
-
|
|
6
|
+
analyzeQuality,
|
|
7
|
+
applyIonoDcb,
|
|
8
|
+
parseSinexBiasDcb
|
|
9
|
+
} from "./chunk-GDGHTC3E.js";
|
|
8
10
|
import "./chunk-3U5AX7PY.js";
|
|
9
11
|
import "./chunk-FIEWO4J4.js";
|
|
10
12
|
export {
|
|
@@ -12,5 +14,7 @@ export {
|
|
|
12
14
|
CycleSlipAccumulator,
|
|
13
15
|
IonoAccumulator,
|
|
14
16
|
MultipathAccumulator,
|
|
15
|
-
analyzeQuality
|
|
17
|
+
analyzeQuality,
|
|
18
|
+
applyIonoDcb,
|
|
19
|
+
parseSinexBiasDcb
|
|
16
20
|
};
|
|
@@ -219,20 +219,10 @@ function invert4x4(m) {
|
|
|
219
219
|
return result;
|
|
220
220
|
}
|
|
221
221
|
function selectEphemeris(ephemerides, prn, timeMs) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const ephTime = eph.tocDate.getTime();
|
|
227
|
-
const dt = Math.abs(timeMs - ephTime);
|
|
228
|
-
const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
|
|
229
|
-
if (dt > maxAge) continue;
|
|
230
|
-
if (dt < bestDt) {
|
|
231
|
-
bestDt = dt;
|
|
232
|
-
best = eph;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return best;
|
|
222
|
+
return selectBest(
|
|
223
|
+
ephemerides.filter((e) => e.prn === prn),
|
|
224
|
+
timeMs
|
|
225
|
+
);
|
|
236
226
|
}
|
|
237
227
|
function computeSatPosition(eph, timeMs) {
|
|
238
228
|
if (eph.system === "R" || eph.system === "S") {
|
|
@@ -325,8 +315,9 @@ function selectBest(ephs, timeMs) {
|
|
|
325
315
|
return best;
|
|
326
316
|
}
|
|
327
317
|
var GPS_EPOCH_MS = START_GPS_TIME.getTime();
|
|
318
|
+
var MS_PER_WEEK = 7 * 864e5;
|
|
328
319
|
var BDS_EPOCH_MS = START_BDS_TIME.getTime();
|
|
329
|
-
var GAL_EPOCH_MS = GPS_EPOCH_MS;
|
|
320
|
+
var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK;
|
|
330
321
|
function ephInfoToEphemeris(info) {
|
|
331
322
|
const sys = info.prn.charAt(0);
|
|
332
323
|
if (sys === "R") {
|
|
@@ -370,6 +361,7 @@ function ephInfoToEphemeris(info) {
|
|
|
370
361
|
return null;
|
|
371
362
|
}
|
|
372
363
|
let epochMs;
|
|
364
|
+
let week = info.week;
|
|
373
365
|
const tocSec = info.toc ?? info.toe;
|
|
374
366
|
if (sys === "C") {
|
|
375
367
|
epochMs = BDS_EPOCH_MS;
|
|
@@ -377,8 +369,10 @@ function ephInfoToEphemeris(info) {
|
|
|
377
369
|
epochMs = GAL_EPOCH_MS;
|
|
378
370
|
} else {
|
|
379
371
|
epochMs = GPS_EPOCH_MS;
|
|
372
|
+
const weeksNow = (info.lastReceived - GPS_EPOCH_MS) / MS_PER_WEEK;
|
|
373
|
+
week += 1024 * Math.round((weeksNow - week) / 1024);
|
|
380
374
|
}
|
|
381
|
-
const tocDate = new Date(epochMs +
|
|
375
|
+
const tocDate = new Date(epochMs + week * MS_PER_WEEK + tocSec * 1e3);
|
|
382
376
|
return {
|
|
383
377
|
system: sys,
|
|
384
378
|
prn: info.prn,
|
|
@@ -404,7 +398,7 @@ function ephInfoToEphemeris(info) {
|
|
|
404
398
|
omega: info.argPerigee,
|
|
405
399
|
omegaDot: info.omegaDot ?? 0,
|
|
406
400
|
idot: info.idot ?? 0,
|
|
407
|
-
week
|
|
401
|
+
week,
|
|
408
402
|
svHealth: info.health,
|
|
409
403
|
tgd: 0
|
|
410
404
|
};
|
|
@@ -1345,6 +1345,11 @@ function rtcm3Constellation(msgType) {
|
|
|
1345
1345
|
if (msgType >= 1111 && msgType <= 1117) return "QZSS";
|
|
1346
1346
|
if (msgType >= 1121 && msgType <= 1127) return "BeiDou";
|
|
1347
1347
|
if (msgType >= 1131 && msgType <= 1137) return "NavIC";
|
|
1348
|
+
if (msgType >= 1015 && msgType <= 1017) return "GPS";
|
|
1349
|
+
if (msgType === 1030) return "GPS";
|
|
1350
|
+
if (msgType === 1031) return "GLONASS";
|
|
1351
|
+
if (msgType >= 1057 && msgType <= 1062) return "GPS";
|
|
1352
|
+
if (msgType >= 1063 && msgType <= 1068) return "GLONASS";
|
|
1348
1353
|
if (msgType === 1230) return "GLONASS";
|
|
1349
1354
|
return null;
|
|
1350
1355
|
}
|
|
@@ -1384,7 +1389,6 @@ function updateStreamStats(stats, frames, rawBytes) {
|
|
|
1384
1389
|
entry.totalBytes += frame.length + 6;
|
|
1385
1390
|
const msmEpoch = decodeMsmFull(frame);
|
|
1386
1391
|
if (msmEpoch) {
|
|
1387
|
-
const now2 = Date.now();
|
|
1388
1392
|
for (const obs of msmEpoch.observations) {
|
|
1389
1393
|
const signals = [];
|
|
1390
1394
|
let bestCn0 = 0;
|
|
@@ -1399,7 +1403,7 @@ function updateStreamStats(stats, frames, rawBytes) {
|
|
|
1399
1403
|
prn: obs.prn,
|
|
1400
1404
|
system: obs.system,
|
|
1401
1405
|
cn0: bestCn0,
|
|
1402
|
-
lastSeen:
|
|
1406
|
+
lastSeen: now,
|
|
1403
1407
|
signals
|
|
1404
1408
|
});
|
|
1405
1409
|
}
|