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 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 (works with ReadableStream)
98
- const result = await parseRinexStream(stream, {
99
- onObservation: (time, prn, codes, values) => {
100
- // process each epoch
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[], positions.az/el/lat/lon: Float64Array
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
- MultipathAccumulator,
136
- CycleSlipAccumulator,
137
- CompletenessAccumulator,
138
- } from 'gnss-js/analysis';
139
-
140
- const mp = new MultipathAccumulator(header);
141
- const cs = new CycleSlipAccumulator(header);
142
- await parseRinexStream(stream, {
143
- onObservation: (time, prn, codes, values) => {
144
- mp.onObservation(time, prn, codes, values);
145
- cs.onObservation(time, prn, codes, values);
146
- },
147
- });
148
- const multipathResult = mp.finalize();
149
- const cycleSlipResult = cs.finalize();
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, _codes, values) => {
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, { C: cVal, L: lVal, f: freq });
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 >= MIN_ARC_LENGTH2) {
807
- const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
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 = 0;
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 { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
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
  });
@@ -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. "MP1 L1-L2". */
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. "MP1 L1-L2 (GPS)". */
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, _codes: string[], values: (number | null)[]) => void;
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 };
@@ -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. "MP1 L1-L2". */
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. "MP1 L1-L2 (GPS)". */
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, _codes: string[], values: (number | null)[]) => void;
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
- } from "./chunk-UQ7D53R2.js";
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
- let best = null;
223
- let bestDt = Infinity;
224
- for (const eph of ephemerides) {
225
- if (eph.prn !== prn) continue;
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 + info.week * 7 * 864e5 + tocSec * 1e3);
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: info.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: now2,
1406
+ lastSeen: now,
1403
1407
  signals
1404
1408
  });
1405
1409
  }