gnss-js 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/analysis.cjs CHANGED
@@ -24,7 +24,9 @@ __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
 
@@ -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
  }
@@ -854,10 +865,14 @@ 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
  }
@@ -866,6 +881,133 @@ var IonoAccumulator = class {
866
881
  }
867
882
  };
868
883
 
884
+ // src/analysis/dcb.ts
885
+ var PRN_RE = /^[A-Z]\d{2}$/;
886
+ var SVN_RE = /^[A-Z]\d{3}$/;
887
+ var OBS_RE = /^[CL]\d[A-Z]$/;
888
+ function sinexEpochMs(s) {
889
+ const [y, d, sec] = s.split(":").map(Number);
890
+ if (!y) return Infinity;
891
+ return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
892
+ }
893
+ function parseSinexBiasDcb(text, epochMs) {
894
+ const rows = /* @__PURE__ */ new Map();
895
+ for (const line of text.split("\n")) {
896
+ if (!line.startsWith(" DSB") && !line.startsWith("DSB")) continue;
897
+ const t = line.trim().split(/\s+/);
898
+ if (t.length < 9 || !SVN_RE.test(t[1]) || !PRN_RE.test(t[2]) || !OBS_RE.test(t[3]) || !OBS_RE.test(t[4])) {
899
+ continue;
900
+ }
901
+ const unitIdx = t.indexOf("ns");
902
+ if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
903
+ const value = parseFloat(t[unitIdx + 1]);
904
+ if (!isFinite(value)) continue;
905
+ const start = sinexEpochMs(t[5] ?? "");
906
+ const end = sinexEpochMs(t[6] ?? "");
907
+ const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
908
+ const prn = t[2];
909
+ const pair = `${t[3]}-${t[4]}`;
910
+ let sat = rows.get(prn);
911
+ if (!sat) {
912
+ sat = /* @__PURE__ */ new Map();
913
+ rows.set(prn, sat);
914
+ }
915
+ const prev = sat.get(pair);
916
+ if (!prev || covers && !prev.covers || covers === prev.covers && end > prev.end) {
917
+ sat.set(pair, { value, end, covers });
918
+ }
919
+ }
920
+ const out = /* @__PURE__ */ new Map();
921
+ for (const [prn, sat] of rows) {
922
+ const m = /* @__PURE__ */ new Map();
923
+ for (const [pair, row] of sat) m.set(pair, row.value);
924
+ out.set(prn, m);
925
+ }
926
+ return out;
927
+ }
928
+ function percentile(values, p) {
929
+ const s = [...values].sort((a, b) => a - b);
930
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
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 = 0;
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: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
1005
+ satellitesCorrected: corrected,
1006
+ satellitesMissing: [...new Set(missing)].sort(),
1007
+ receiverDcbTecu
1008
+ };
1009
+ }
1010
+
869
1011
  // src/rinex/crx.ts
870
1012
  function crxRepair(old, diff) {
871
1013
  const chars = old.split("");
@@ -1687,5 +1829,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
1687
1829
  CycleSlipAccumulator,
1688
1830
  IonoAccumulator,
1689
1831
  MultipathAccumulator,
1690
- analyzeQuality
1832
+ analyzeQuality,
1833
+ applyIonoDcb,
1834
+ parseSinexBiasDcb
1691
1835
  });
@@ -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 };
@@ -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-EJQ6NG5U.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
  };
@@ -516,13 +516,15 @@ var IonoAccumulator = class {
516
516
  obsIndices;
517
517
  gloChannels;
518
518
  pairLabel = /* @__PURE__ */ new Map();
519
+ pairCodes = /* @__PURE__ */ new Map();
520
+ pairMeta = /* @__PURE__ */ new Map();
519
521
  constructor(header) {
520
522
  this.interval = header.interval ?? 30;
521
523
  this.obsIndices = buildObsIndices(header);
522
524
  this.gloChannels = buildGloChannelMap(header.glonassSlots);
523
525
  }
524
526
  /** Observation callback — wire this into parseRinexStream. */
525
- onObservation = (time, prn, _codes, values) => {
527
+ onObservation = (time, prn, codes, values) => {
526
528
  const sys = prn[0];
527
529
  const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
528
530
  if (!bandMap) return;
@@ -533,7 +535,12 @@ var IonoAccumulator = class {
533
535
  const lVal = values[L];
534
536
  const freq = getFreq(this.gloChannels, prn, band);
535
537
  if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
536
- bandData.set(band, { C: cVal, L: lVal, f: freq });
538
+ bandData.set(band, {
539
+ C: cVal,
540
+ L: lVal,
541
+ f: freq,
542
+ code: codes[C] ?? `C${band}`
543
+ });
537
544
  }
538
545
  }
539
546
  if (bandData.size < 2) return;
@@ -552,7 +559,9 @@ var IonoAccumulator = class {
552
559
  const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
553
560
  const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
554
561
  this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
562
+ this.pairCodes.set(`${sys}:${pairKey}`, [di.code, dj.code]);
555
563
  }
564
+ this.pairMeta.set(`${prn}:${pairKey}`, { fi: di.f, gamma });
556
565
  this.push(prn, pairKey, time, l4, p4, di.f, gamma);
557
566
  break;
558
567
  }
@@ -653,10 +662,14 @@ var IonoAccumulator = class {
653
662
  count++;
654
663
  if (p.stec > maxStec) maxStec = p.stec;
655
664
  }
665
+ const meta = this.pairMeta.get(`${prn}:${bestKey}`);
666
+ const tecuPerNs = meta ? C_LIGHT * 1e-9 / (meta.gamma - 1) * (meta.fi * meta.fi / TEC_FACTOR) : 0;
656
667
  series.push({
657
668
  prn,
658
669
  system: sys,
659
670
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
671
+ codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
672
+ tecuPerNs,
660
673
  points
661
674
  });
662
675
  }
@@ -665,6 +678,133 @@ var IonoAccumulator = class {
665
678
  }
666
679
  };
667
680
 
681
+ // src/analysis/dcb.ts
682
+ var PRN_RE = /^[A-Z]\d{2}$/;
683
+ var SVN_RE = /^[A-Z]\d{3}$/;
684
+ var OBS_RE = /^[CL]\d[A-Z]$/;
685
+ function sinexEpochMs(s) {
686
+ const [y, d, sec] = s.split(":").map(Number);
687
+ if (!y) return Infinity;
688
+ return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
689
+ }
690
+ function parseSinexBiasDcb(text, epochMs) {
691
+ const rows = /* @__PURE__ */ new Map();
692
+ for (const line of text.split("\n")) {
693
+ if (!line.startsWith(" DSB") && !line.startsWith("DSB")) continue;
694
+ const t = line.trim().split(/\s+/);
695
+ if (t.length < 9 || !SVN_RE.test(t[1]) || !PRN_RE.test(t[2]) || !OBS_RE.test(t[3]) || !OBS_RE.test(t[4])) {
696
+ continue;
697
+ }
698
+ const unitIdx = t.indexOf("ns");
699
+ if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
700
+ const value = parseFloat(t[unitIdx + 1]);
701
+ if (!isFinite(value)) continue;
702
+ const start = sinexEpochMs(t[5] ?? "");
703
+ const end = sinexEpochMs(t[6] ?? "");
704
+ const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
705
+ const prn = t[2];
706
+ const pair = `${t[3]}-${t[4]}`;
707
+ let sat = rows.get(prn);
708
+ if (!sat) {
709
+ sat = /* @__PURE__ */ new Map();
710
+ rows.set(prn, sat);
711
+ }
712
+ const prev = sat.get(pair);
713
+ if (!prev || covers && !prev.covers || covers === prev.covers && end > prev.end) {
714
+ sat.set(pair, { value, end, covers });
715
+ }
716
+ }
717
+ const out = /* @__PURE__ */ new Map();
718
+ for (const [prn, sat] of rows) {
719
+ const m = /* @__PURE__ */ new Map();
720
+ for (const [pair, row] of sat) m.set(pair, row.value);
721
+ out.set(prn, m);
722
+ }
723
+ return out;
724
+ }
725
+ function percentile(values, p) {
726
+ const s = [...values].sort((a, b) => a - b);
727
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
728
+ }
729
+ function applyIonoDcb(iono, satDcb) {
730
+ const missing = [];
731
+ let corrected = 0;
732
+ const dcbFor = (s) => {
733
+ const sat = satDcb.get(s.prn);
734
+ if (!sat) return null;
735
+ const fwd = sat.get(`${s.codes[0]}-${s.codes[1]}`);
736
+ if (fwd !== void 0) return fwd;
737
+ const rev = sat.get(`${s.codes[1]}-${s.codes[0]}`);
738
+ if (rev !== void 0) return -rev;
739
+ return null;
740
+ };
741
+ const series = iono.series.map((s) => {
742
+ const dcbNs = dcbFor(s);
743
+ if (dcbNs === null) {
744
+ missing.push(s.prn);
745
+ return s;
746
+ }
747
+ corrected++;
748
+ const shift = dcbNs * s.tecuPerNs;
749
+ return {
750
+ ...s,
751
+ points: s.points.map((p) => ({ time: p.time, stec: p.stec + shift }))
752
+ };
753
+ });
754
+ const missingSet = new Set(missing);
755
+ const receiverDcbTecu = {};
756
+ const groups = /* @__PURE__ */ new Map();
757
+ for (const s of series) {
758
+ const key = `${s.system} ${s.label}`;
759
+ let g = groups.get(key);
760
+ if (!g) {
761
+ g = [];
762
+ groups.set(key, g);
763
+ }
764
+ g.push(s);
765
+ }
766
+ const out = [];
767
+ for (const [key, group] of groups) {
768
+ const samples = [];
769
+ for (const s of group) {
770
+ if (missingSet.has(s.prn)) continue;
771
+ for (const p of s.points) samples.push(p.stec);
772
+ }
773
+ if (samples.length >= 100) {
774
+ const rx = percentile(samples, 0.01);
775
+ receiverDcbTecu[key] = rx;
776
+ for (const s of group) {
777
+ out.push({
778
+ ...s,
779
+ points: s.points.map((p) => ({
780
+ time: p.time,
781
+ stec: p.stec - rx
782
+ }))
783
+ });
784
+ }
785
+ } else {
786
+ out.push(...group);
787
+ }
788
+ }
789
+ out.sort((a, b) => a.prn.localeCompare(b.prn));
790
+ let sum = 0;
791
+ let count = 0;
792
+ let maxStec = 0;
793
+ for (const s of out) {
794
+ for (const p of s.points) {
795
+ sum += p.stec;
796
+ count++;
797
+ if (p.stec > maxStec) maxStec = p.stec;
798
+ }
799
+ }
800
+ return {
801
+ result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
802
+ satellitesCorrected: corrected,
803
+ satellitesMissing: [...new Set(missing)].sort(),
804
+ receiverDcbTecu
805
+ };
806
+ }
807
+
668
808
  // src/analysis/quality-analysis.ts
669
809
  async function analyzeQuality(file, header, onProgress, signal) {
670
810
  const mpAccum = new MultipathAccumulator(header);
@@ -700,5 +840,7 @@ export {
700
840
  CycleSlipAccumulator,
701
841
  CompletenessAccumulator,
702
842
  IonoAccumulator,
843
+ parseSinexBiasDcb,
844
+ applyIonoDcb,
703
845
  analyzeQuality
704
846
  };
package/dist/index.cjs CHANGED
@@ -105,6 +105,7 @@ __export(src_exports, {
105
105
  WarningAccumulator: () => WarningAccumulator,
106
106
  analyzeQuality: () => analyzeQuality,
107
107
  applyHelmert: () => applyHelmert,
108
+ applyIonoDcb: () => applyIonoDcb,
108
109
  buildGloChannelMap: () => buildGloChannelMap,
109
110
  buildObsIndices: () => buildObsIndices,
110
111
  clampUnit: () => clampUnit,
@@ -207,6 +208,7 @@ __export(src_exports, {
207
208
  parseNavFile: () => parseNavFile,
208
209
  parseNmeaFile: () => parseNmeaFile,
209
210
  parseRinexStream: () => parseRinexStream,
211
+ parseSinexBiasDcb: () => parseSinexBiasDcb,
210
212
  parseSourcetable: () => parseSourcetable,
211
213
  phiAltBOC: () => phiAltBOC,
212
214
  phiBOCc: () => phiBOCc,
@@ -5545,13 +5547,15 @@ var IonoAccumulator = class {
5545
5547
  obsIndices;
5546
5548
  gloChannels;
5547
5549
  pairLabel = /* @__PURE__ */ new Map();
5550
+ pairCodes = /* @__PURE__ */ new Map();
5551
+ pairMeta = /* @__PURE__ */ new Map();
5548
5552
  constructor(header) {
5549
5553
  this.interval = header.interval ?? 30;
5550
5554
  this.obsIndices = buildObsIndices(header);
5551
5555
  this.gloChannels = buildGloChannelMap(header.glonassSlots);
5552
5556
  }
5553
5557
  /** Observation callback — wire this into parseRinexStream. */
5554
- onObservation = (time, prn, _codes, values) => {
5558
+ onObservation = (time, prn, codes, values) => {
5555
5559
  const sys = prn[0];
5556
5560
  const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
5557
5561
  if (!bandMap) return;
@@ -5562,7 +5566,12 @@ var IonoAccumulator = class {
5562
5566
  const lVal = values[L];
5563
5567
  const freq = getFreq(this.gloChannels, prn, band);
5564
5568
  if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
5565
- bandData.set(band, { C: cVal, L: lVal, f: freq });
5569
+ bandData.set(band, {
5570
+ C: cVal,
5571
+ L: lVal,
5572
+ f: freq,
5573
+ code: codes[C] ?? `C${band}`
5574
+ });
5566
5575
  }
5567
5576
  }
5568
5577
  if (bandData.size < 2) return;
@@ -5581,7 +5590,9 @@ var IonoAccumulator = class {
5581
5590
  const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
5582
5591
  const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
5583
5592
  this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
5593
+ this.pairCodes.set(`${sys}:${pairKey}`, [di.code, dj.code]);
5584
5594
  }
5595
+ this.pairMeta.set(`${prn}:${pairKey}`, { fi: di.f, gamma });
5585
5596
  this.push(prn, pairKey, time, l4, p4, di.f, gamma);
5586
5597
  break;
5587
5598
  }
@@ -5682,10 +5693,14 @@ var IonoAccumulator = class {
5682
5693
  count++;
5683
5694
  if (p.stec > maxStec) maxStec = p.stec;
5684
5695
  }
5696
+ const meta = this.pairMeta.get(`${prn}:${bestKey}`);
5697
+ const tecuPerNs = meta ? C_LIGHT * 1e-9 / (meta.gamma - 1) * (meta.fi * meta.fi / TEC_FACTOR) : 0;
5685
5698
  series.push({
5686
5699
  prn,
5687
5700
  system: sys,
5688
5701
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
5702
+ codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
5703
+ tecuPerNs,
5689
5704
  points
5690
5705
  });
5691
5706
  }
@@ -5694,6 +5709,133 @@ var IonoAccumulator = class {
5694
5709
  }
5695
5710
  };
5696
5711
 
5712
+ // src/analysis/dcb.ts
5713
+ var PRN_RE = /^[A-Z]\d{2}$/;
5714
+ var SVN_RE = /^[A-Z]\d{3}$/;
5715
+ var OBS_RE = /^[CL]\d[A-Z]$/;
5716
+ function sinexEpochMs(s) {
5717
+ const [y, d, sec] = s.split(":").map(Number);
5718
+ if (!y) return Infinity;
5719
+ return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
5720
+ }
5721
+ function parseSinexBiasDcb(text, epochMs) {
5722
+ const rows = /* @__PURE__ */ new Map();
5723
+ for (const line of text.split("\n")) {
5724
+ if (!line.startsWith(" DSB") && !line.startsWith("DSB")) continue;
5725
+ const t = line.trim().split(/\s+/);
5726
+ if (t.length < 9 || !SVN_RE.test(t[1]) || !PRN_RE.test(t[2]) || !OBS_RE.test(t[3]) || !OBS_RE.test(t[4])) {
5727
+ continue;
5728
+ }
5729
+ const unitIdx = t.indexOf("ns");
5730
+ if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
5731
+ const value = parseFloat(t[unitIdx + 1]);
5732
+ if (!isFinite(value)) continue;
5733
+ const start = sinexEpochMs(t[5] ?? "");
5734
+ const end = sinexEpochMs(t[6] ?? "");
5735
+ const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
5736
+ const prn = t[2];
5737
+ const pair = `${t[3]}-${t[4]}`;
5738
+ let sat = rows.get(prn);
5739
+ if (!sat) {
5740
+ sat = /* @__PURE__ */ new Map();
5741
+ rows.set(prn, sat);
5742
+ }
5743
+ const prev = sat.get(pair);
5744
+ if (!prev || covers && !prev.covers || covers === prev.covers && end > prev.end) {
5745
+ sat.set(pair, { value, end, covers });
5746
+ }
5747
+ }
5748
+ const out = /* @__PURE__ */ new Map();
5749
+ for (const [prn, sat] of rows) {
5750
+ const m = /* @__PURE__ */ new Map();
5751
+ for (const [pair, row] of sat) m.set(pair, row.value);
5752
+ out.set(prn, m);
5753
+ }
5754
+ return out;
5755
+ }
5756
+ function percentile(values, p) {
5757
+ const s = [...values].sort((a, b) => a - b);
5758
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
5759
+ }
5760
+ function applyIonoDcb(iono, satDcb) {
5761
+ const missing = [];
5762
+ let corrected = 0;
5763
+ const dcbFor = (s) => {
5764
+ const sat = satDcb.get(s.prn);
5765
+ if (!sat) return null;
5766
+ const fwd = sat.get(`${s.codes[0]}-${s.codes[1]}`);
5767
+ if (fwd !== void 0) return fwd;
5768
+ const rev = sat.get(`${s.codes[1]}-${s.codes[0]}`);
5769
+ if (rev !== void 0) return -rev;
5770
+ return null;
5771
+ };
5772
+ const series = iono.series.map((s) => {
5773
+ const dcbNs = dcbFor(s);
5774
+ if (dcbNs === null) {
5775
+ missing.push(s.prn);
5776
+ return s;
5777
+ }
5778
+ corrected++;
5779
+ const shift = dcbNs * s.tecuPerNs;
5780
+ return {
5781
+ ...s,
5782
+ points: s.points.map((p) => ({ time: p.time, stec: p.stec + shift }))
5783
+ };
5784
+ });
5785
+ const missingSet = new Set(missing);
5786
+ const receiverDcbTecu = {};
5787
+ const groups = /* @__PURE__ */ new Map();
5788
+ for (const s of series) {
5789
+ const key = `${s.system} ${s.label}`;
5790
+ let g = groups.get(key);
5791
+ if (!g) {
5792
+ g = [];
5793
+ groups.set(key, g);
5794
+ }
5795
+ g.push(s);
5796
+ }
5797
+ const out = [];
5798
+ for (const [key, group] of groups) {
5799
+ const samples = [];
5800
+ for (const s of group) {
5801
+ if (missingSet.has(s.prn)) continue;
5802
+ for (const p of s.points) samples.push(p.stec);
5803
+ }
5804
+ if (samples.length >= 100) {
5805
+ const rx = percentile(samples, 0.01);
5806
+ receiverDcbTecu[key] = rx;
5807
+ for (const s of group) {
5808
+ out.push({
5809
+ ...s,
5810
+ points: s.points.map((p) => ({
5811
+ time: p.time,
5812
+ stec: p.stec - rx
5813
+ }))
5814
+ });
5815
+ }
5816
+ } else {
5817
+ out.push(...group);
5818
+ }
5819
+ }
5820
+ out.sort((a, b) => a.prn.localeCompare(b.prn));
5821
+ let sum = 0;
5822
+ let count = 0;
5823
+ let maxStec = 0;
5824
+ for (const s of out) {
5825
+ for (const p of s.points) {
5826
+ sum += p.stec;
5827
+ count++;
5828
+ if (p.stec > maxStec) maxStec = p.stec;
5829
+ }
5830
+ }
5831
+ return {
5832
+ result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
5833
+ satellitesCorrected: corrected,
5834
+ satellitesMissing: [...new Set(missing)].sort(),
5835
+ receiverDcbTecu
5836
+ };
5837
+ }
5838
+
5697
5839
  // src/analysis/quality-analysis.ts
5698
5840
  async function analyzeQuality(file, header, onProgress, signal) {
5699
5841
  const mpAccum = new MultipathAccumulator(header);
@@ -7000,6 +7142,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7000
7142
  WarningAccumulator,
7001
7143
  analyzeQuality,
7002
7144
  applyHelmert,
7145
+ applyIonoDcb,
7003
7146
  buildGloChannelMap,
7004
7147
  buildObsIndices,
7005
7148
  clampUnit,
@@ -7102,6 +7245,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7102
7245
  parseNavFile,
7103
7246
  parseNmeaFile,
7104
7247
  parseRinexStream,
7248
+ parseSinexBiasDcb,
7105
7249
  parseSourcetable,
7106
7250
  phiAltBOC,
7107
7251
  phiBOCc,
package/dist/index.d.cts CHANGED
@@ -13,7 +13,7 @@ export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPositi
13
13
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.cjs';
14
14
  export { SppOptions, SppSolution, ionoFree, satClockCorrection, solveSpp } from './positioning.cjs';
15
15
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.cjs';
16
- export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, analyzeQuality } from './analysis.cjs';
16
+ export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb } from './analysis.cjs';
17
17
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.cjs';
18
18
  export { NmeaFix, NmeaStats, NmeaTrack, computeStats, nmeaCoordToDecimal, parseNmeaFile, verifyChecksum } from './nmea.cjs';
19
19
  export { BAND_PRESETS, BandPreset, CONSTELLATIONS, ConstellationRow, DELTA_GLO_L1, DELTA_GLO_L2, FREQ_BDS_B1A, FREQ_BDS_B1C, FREQ_BDS_B1I, FREQ_BDS_B2I, FREQ_BDS_B3A, FREQ_BDS_B3I, FREQ_GAL_E1, FREQ_GAL_E5, FREQ_GAL_E5a, FREQ_GAL_E5b, FREQ_GAL_E6, FREQ_GLO_L1, FREQ_GLO_L1OC, FREQ_GLO_L2, FREQ_GLO_L2OC, FREQ_GLO_L3OC, FREQ_GPS_L1, FREQ_GPS_L2, FREQ_GPS_L5, FREQ_NAVIC_L5, FREQ_QZS_L1, FREQ_QZS_L2, FREQ_QZS_L5, FREQ_QZS_L6, Modulation, SignalDef, computePsdDb, phiAltBOC, phiBOCc, phiBOCs, phiBPSK } from './signals.cjs';
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPositi
13
13
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.js';
14
14
  export { SppOptions, SppSolution, ionoFree, satClockCorrection, solveSpp } from './positioning.js';
15
15
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.js';
16
- export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, analyzeQuality } from './analysis.js';
16
+ export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb } from './analysis.js';
17
17
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.js';
18
18
  export { NmeaFix, NmeaStats, NmeaTrack, computeStats, nmeaCoordToDecimal, parseNmeaFile, verifyChecksum } from './nmea.js';
19
19
  export { BAND_PRESETS, BandPreset, CONSTELLATIONS, ConstellationRow, DELTA_GLO_L1, DELTA_GLO_L2, FREQ_BDS_B1A, FREQ_BDS_B1C, FREQ_BDS_B1I, FREQ_BDS_B2I, FREQ_BDS_B3A, FREQ_BDS_B3I, FREQ_GAL_E1, FREQ_GAL_E5, FREQ_GAL_E5a, FREQ_GAL_E5b, FREQ_GAL_E6, FREQ_GLO_L1, FREQ_GLO_L1OC, FREQ_GLO_L2, FREQ_GLO_L2OC, FREQ_GLO_L3OC, FREQ_GPS_L1, FREQ_GPS_L2, FREQ_GPS_L5, FREQ_NAVIC_L5, FREQ_QZS_L1, FREQ_QZS_L2, FREQ_QZS_L5, FREQ_QZS_L6, Modulation, SignalDef, computePsdDb, phiAltBOC, phiBOCc, phiBOCs, phiBPSK } from './signals.js';
package/dist/index.js CHANGED
@@ -8,8 +8,10 @@ import {
8
8
  CycleSlipAccumulator,
9
9
  IonoAccumulator,
10
10
  MultipathAccumulator,
11
- analyzeQuality
12
- } from "./chunk-UQ7D53R2.js";
11
+ analyzeQuality,
12
+ applyIonoDcb,
13
+ parseSinexBiasDcb
14
+ } from "./chunk-EJQ6NG5U.js";
13
15
  import {
14
16
  frequencyLabel,
15
17
  parseAntex
@@ -332,6 +334,7 @@ export {
332
334
  WarningAccumulator,
333
335
  analyzeQuality,
334
336
  applyHelmert,
337
+ applyIonoDcb,
335
338
  buildGloChannelMap,
336
339
  buildObsIndices,
337
340
  clampUnit,
@@ -434,6 +437,7 @@ export {
434
437
  parseNavFile,
435
438
  parseNmeaFile,
436
439
  parseRinexStream,
440
+ parseSinexBiasDcb,
437
441
  parseSourcetable,
438
442
  phiAltBOC,
439
443
  phiBOCc,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Comprehensive GNSS library for JavaScript — time scales, coordinates, RINEX parsing, RTCM3 decoding, orbit computation, and signal analysis",
5
5
  "type": "module",
6
6
  "sideEffects": false,