gnss-js 1.3.2 → 1.4.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
@@ -22,6 +22,7 @@ var analysis_exports = {};
22
22
  __export(analysis_exports, {
23
23
  CompletenessAccumulator: () => CompletenessAccumulator,
24
24
  CycleSlipAccumulator: () => CycleSlipAccumulator,
25
+ IonoAccumulator: () => IonoAccumulator,
25
26
  MultipathAccumulator: () => MultipathAccumulator,
26
27
  analyzeQuality: () => analyzeQuality
27
28
  });
@@ -693,6 +694,171 @@ var CompletenessAccumulator = class {
693
694
  }
694
695
  };
695
696
 
697
+ // src/analysis/ionosphere.ts
698
+ var TEC_FACTOR = 403e15;
699
+ var GF_JUMP_M = 0.15;
700
+ var MIN_ARC_LENGTH2 = 10;
701
+ function median2(values) {
702
+ const s = [...values].sort((a, b) => a - b);
703
+ const m = s.length >> 1;
704
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
705
+ }
706
+ var IonoAccumulator = class {
707
+ state = /* @__PURE__ */ new Map();
708
+ closed = /* @__PURE__ */ new Map();
709
+ interval;
710
+ obsIndices;
711
+ gloChannels;
712
+ pairLabel = /* @__PURE__ */ new Map();
713
+ constructor(header) {
714
+ this.interval = header.interval ?? 30;
715
+ this.obsIndices = buildObsIndices(header);
716
+ this.gloChannels = buildGloChannelMap(header.glonassSlots);
717
+ }
718
+ /** Observation callback — wire this into parseRinexStream. */
719
+ onObservation = (time, prn, _codes, values) => {
720
+ const sys = prn[0];
721
+ const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
722
+ if (!bandMap) return;
723
+ const bandData = /* @__PURE__ */ new Map();
724
+ for (const [band, { C, L }] of bandMap) {
725
+ if (C === null) continue;
726
+ const cVal = values[C];
727
+ const lVal = values[L];
728
+ const freq = getFreq(this.gloChannels, prn, band);
729
+ if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
730
+ bandData.set(band, { C: cVal, L: lVal, f: freq });
731
+ }
732
+ }
733
+ if (bandData.size < 2) return;
734
+ const pairs = DUAL_FREQ_PAIRS[sys] ?? [];
735
+ for (const [bi, bj] of pairs) {
736
+ const di = bandData.get(bi);
737
+ const dj = bandData.get(bj);
738
+ if (!di || !dj) continue;
739
+ const \u03BBi = C_LIGHT / di.f;
740
+ const \u03BBj = C_LIGHT / dj.f;
741
+ const gamma = di.f * di.f / (dj.f * dj.f);
742
+ const l4 = di.L * \u03BBi - dj.L * \u03BBj;
743
+ const p4 = dj.C - di.C;
744
+ const pairKey = `${bi}-${bj}`;
745
+ if (!this.pairLabel.has(`${sys}:${pairKey}`)) {
746
+ const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
747
+ const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
748
+ this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
749
+ }
750
+ this.push(prn, pairKey, time, l4, p4, di.f, gamma);
751
+ break;
752
+ }
753
+ };
754
+ push(prn, pairKey, time, l4, p4, fi, gamma) {
755
+ if (!isFinite(l4) || !isFinite(p4)) return;
756
+ let satStates = this.state.get(prn);
757
+ if (!satStates) {
758
+ satStates = /* @__PURE__ */ new Map();
759
+ this.state.set(prn, satStates);
760
+ }
761
+ let ps = satStates.get(pairKey);
762
+ if (!ps) {
763
+ ps = {
764
+ arc: { times: [], l4: [], p4: [], fi, gamma },
765
+ lastTime: 0,
766
+ lastL4: 0
767
+ };
768
+ satStates.set(pairKey, ps);
769
+ }
770
+ const gap = ps.lastTime > 0 ? (time - ps.lastTime) / 1e3 : 0;
771
+ if (ps.lastTime > 0 && gap > this.interval * ARC_GAP_FACTOR) {
772
+ this.closeArc(prn, pairKey, ps);
773
+ } else if (ps.arc.times.length > 0 && Math.abs(l4 - ps.lastL4) > GF_JUMP_M) {
774
+ this.closeArc(prn, pairKey, ps);
775
+ }
776
+ if (ps.arc.times.length === 0) {
777
+ ps.arc.fi = fi;
778
+ ps.arc.gamma = gamma;
779
+ }
780
+ ps.arc.times.push(time);
781
+ ps.arc.l4.push(l4);
782
+ ps.arc.p4.push(p4);
783
+ ps.lastTime = time;
784
+ ps.lastL4 = l4;
785
+ }
786
+ /** External cycle-slip notification — close affected arcs. */
787
+ notifySlip(_time, prn, bands) {
788
+ const satStates = this.state.get(prn);
789
+ if (!satStates) return;
790
+ for (const [pairKey, ps] of satStates) {
791
+ const [bi, bj] = pairKey.split("-");
792
+ if (bands.has(bi) || bands.has(bj)) {
793
+ this.closeArc(prn, pairKey, ps);
794
+ }
795
+ }
796
+ }
797
+ closeArc(prn, pairKey, ps) {
798
+ const arc = ps.arc;
799
+ if (arc.times.length >= MIN_ARC_LENGTH2) {
800
+ const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
801
+ const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
802
+ let satArcs = this.closed.get(prn);
803
+ if (!satArcs) {
804
+ satArcs = /* @__PURE__ */ new Map();
805
+ this.closed.set(prn, satArcs);
806
+ }
807
+ let points = satArcs.get(pairKey);
808
+ if (!points) {
809
+ points = [];
810
+ satArcs.set(pairKey, points);
811
+ }
812
+ for (let k = 0; k < arc.times.length; k++) {
813
+ points.push({
814
+ time: arc.times[k],
815
+ stec: (arc.l4[k] - level) * toTecu
816
+ });
817
+ }
818
+ }
819
+ ps.arc = { times: [], l4: [], p4: [], fi: arc.fi, gamma: arc.gamma };
820
+ }
821
+ /** Finalize: close remaining arcs, keep one pair per satellite. */
822
+ finalize() {
823
+ for (const [prn, satStates] of this.state) {
824
+ for (const [pairKey, ps] of satStates) {
825
+ this.closeArc(prn, pairKey, ps);
826
+ }
827
+ }
828
+ const series = [];
829
+ let sum = 0;
830
+ let count = 0;
831
+ let maxStec = 0;
832
+ for (const [prn, satArcs] of this.closed) {
833
+ let bestKey = null;
834
+ let bestLen = 0;
835
+ for (const [pairKey, points2] of satArcs) {
836
+ if (points2.length > bestLen) {
837
+ bestLen = points2.length;
838
+ bestKey = pairKey;
839
+ }
840
+ }
841
+ if (!bestKey) continue;
842
+ const points = satArcs.get(bestKey);
843
+ points.sort((a, b) => a.time - b.time);
844
+ const sys = prn[0];
845
+ for (const p of points) {
846
+ sum += p.stec;
847
+ count++;
848
+ if (p.stec > maxStec) maxStec = p.stec;
849
+ }
850
+ series.push({
851
+ prn,
852
+ system: sys,
853
+ label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
854
+ points
855
+ });
856
+ }
857
+ series.sort((a, b) => a.prn.localeCompare(b.prn));
858
+ return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
859
+ }
860
+ };
861
+
696
862
  // src/rinex/crx.ts
697
863
  function crxRepair(old, diff) {
698
864
  const chars = old.split("");
@@ -1482,8 +1648,10 @@ function computeStats(header, epochs, satellitesSeen) {
1482
1648
  // src/analysis/quality-analysis.ts
1483
1649
  async function analyzeQuality(file, header, onProgress, signal) {
1484
1650
  const mpAccum = new MultipathAccumulator(header);
1651
+ const ionoAccum = new IonoAccumulator(header);
1485
1652
  const csAccum = new CycleSlipAccumulator(header, (time, prn, bands) => {
1486
1653
  mpAccum.notifySlip(time, prn, bands);
1654
+ ionoAccum.notifySlip(time, prn, bands);
1487
1655
  });
1488
1656
  const compAccum = new CompletenessAccumulator(header);
1489
1657
  await parseRinexStream(
@@ -1493,6 +1661,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
1493
1661
  (time, prn, codes, values) => {
1494
1662
  csAccum.onObservation(time, prn, codes, values);
1495
1663
  mpAccum.onObservation(time, prn, codes, values);
1664
+ ionoAccum.onObservation(time, prn, codes, values);
1496
1665
  compAccum.onObservation(time, prn, codes, values);
1497
1666
  },
1498
1667
  true
@@ -1501,13 +1670,15 @@ async function analyzeQuality(file, header, onProgress, signal) {
1501
1670
  return {
1502
1671
  cycleSlips: csAccum.finalize(),
1503
1672
  completeness: compAccum.finalize(),
1504
- multipath: mpAccum.finalize()
1673
+ multipath: mpAccum.finalize(),
1674
+ iono: ionoAccum.finalize()
1505
1675
  };
1506
1676
  }
1507
1677
  // Annotate the CommonJS export names for ESM import in node:
1508
1678
  0 && (module.exports = {
1509
1679
  CompletenessAccumulator,
1510
1680
  CycleSlipAccumulator,
1681
+ IonoAccumulator,
1511
1682
  MultipathAccumulator,
1512
1683
  analyzeQuality
1513
1684
  });
@@ -196,16 +196,81 @@ declare class CompletenessAccumulator {
196
196
  finalize(): CompletenessResult;
197
197
  }
198
198
 
199
+ /**
200
+ * Slant ionospheric delay from dual-frequency observations.
201
+ *
202
+ * Geometry-free phase combination levelled to the geometry-free code,
203
+ * per continuous arc:
204
+ * L4 = λi·Li − λj·Lj = (γ−1)·I_i + B (phase; B = ambiguity bias)
205
+ * P4 = C_j − C_i = (γ−1)·I_i + DCBs (code)
206
+ * with γ = (f_i/f_j)² and I_i the slant ionospheric delay on band i.
207
+ * The arc levelling constant B̂ = median(L4 − P4) transfers the code's
208
+ * absolute level onto the low-noise phase; slant TEC follows as
209
+ * STEC [TECU] = (L4 − B̂)/(γ−1) · f_i² / 40.3e16.
210
+ *
211
+ * The result carries the receiver+satellite differential code biases
212
+ * (not removed here — several TECU per satellite), so series are
213
+ * DCB-biased but shape-faithful: diurnal variation, gradients and
214
+ * disturbances read correctly.
215
+ *
216
+ * The geometry-free phase is also a sensitive cycle-slip detector:
217
+ * arcs split on epoch-to-epoch L4 jumps beyond GF_JUMP_M, in addition
218
+ * to time gaps and external slip notifications.
219
+ */
220
+
221
+ interface IonoPoint {
222
+ /** Epoch time in milliseconds. */
223
+ time: number;
224
+ /** Slant TEC in TECU (DCB-biased). */
225
+ stec: number;
226
+ }
227
+ interface IonoSeries {
228
+ /** Satellite PRN, e.g. "G01". */
229
+ prn: string;
230
+ /** System letter, e.g. "G". */
231
+ system: string;
232
+ /** Band pair used, e.g. "L1-L2". */
233
+ label: string;
234
+ /** Time series of slant TEC values (TECU, DCB-biased). */
235
+ points: IonoPoint[];
236
+ }
237
+ interface IonoResult {
238
+ /** Per-satellite slant TEC time series. */
239
+ series: IonoSeries[];
240
+ /** Highest single STEC sample across all series (TECU). */
241
+ maxStec: number;
242
+ /** Mean STEC across all samples (TECU). */
243
+ meanStec: number;
244
+ }
245
+ declare class IonoAccumulator {
246
+ private state;
247
+ private closed;
248
+ private interval;
249
+ private obsIndices;
250
+ private gloChannels;
251
+ private pairLabel;
252
+ constructor(header: RinexHeader);
253
+ /** Observation callback — wire this into parseRinexStream. */
254
+ onObservation: (time: number, prn: string, _codes: string[], values: (number | null)[]) => void;
255
+ private push;
256
+ /** External cycle-slip notification — close affected arcs. */
257
+ notifySlip(_time: number, prn: string, bands: Set<string>): void;
258
+ private closeArc;
259
+ /** Finalize: close remaining arcs, keep one pair per satellite. */
260
+ finalize(): IonoResult;
261
+ }
262
+
199
263
  /**
200
264
  * Combined quality analysis — runs cycle slip detection, data completeness,
201
- * and multipath analysis in a single file re-parse pass.
265
+ * multipath and ionosphere analysis in a single file re-parse pass.
202
266
  */
203
267
 
204
268
  interface QualityResult {
205
269
  cycleSlips: CycleSlipResult;
206
270
  completeness: CompletenessResult;
207
271
  multipath: MultipathResult;
272
+ iono: IonoResult;
208
273
  }
209
274
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
210
275
 
211
- export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, analyzeQuality };
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 };
@@ -196,16 +196,81 @@ declare class CompletenessAccumulator {
196
196
  finalize(): CompletenessResult;
197
197
  }
198
198
 
199
+ /**
200
+ * Slant ionospheric delay from dual-frequency observations.
201
+ *
202
+ * Geometry-free phase combination levelled to the geometry-free code,
203
+ * per continuous arc:
204
+ * L4 = λi·Li − λj·Lj = (γ−1)·I_i + B (phase; B = ambiguity bias)
205
+ * P4 = C_j − C_i = (γ−1)·I_i + DCBs (code)
206
+ * with γ = (f_i/f_j)² and I_i the slant ionospheric delay on band i.
207
+ * The arc levelling constant B̂ = median(L4 − P4) transfers the code's
208
+ * absolute level onto the low-noise phase; slant TEC follows as
209
+ * STEC [TECU] = (L4 − B̂)/(γ−1) · f_i² / 40.3e16.
210
+ *
211
+ * The result carries the receiver+satellite differential code biases
212
+ * (not removed here — several TECU per satellite), so series are
213
+ * DCB-biased but shape-faithful: diurnal variation, gradients and
214
+ * disturbances read correctly.
215
+ *
216
+ * The geometry-free phase is also a sensitive cycle-slip detector:
217
+ * arcs split on epoch-to-epoch L4 jumps beyond GF_JUMP_M, in addition
218
+ * to time gaps and external slip notifications.
219
+ */
220
+
221
+ interface IonoPoint {
222
+ /** Epoch time in milliseconds. */
223
+ time: number;
224
+ /** Slant TEC in TECU (DCB-biased). */
225
+ stec: number;
226
+ }
227
+ interface IonoSeries {
228
+ /** Satellite PRN, e.g. "G01". */
229
+ prn: string;
230
+ /** System letter, e.g. "G". */
231
+ system: string;
232
+ /** Band pair used, e.g. "L1-L2". */
233
+ label: string;
234
+ /** Time series of slant TEC values (TECU, DCB-biased). */
235
+ points: IonoPoint[];
236
+ }
237
+ interface IonoResult {
238
+ /** Per-satellite slant TEC time series. */
239
+ series: IonoSeries[];
240
+ /** Highest single STEC sample across all series (TECU). */
241
+ maxStec: number;
242
+ /** Mean STEC across all samples (TECU). */
243
+ meanStec: number;
244
+ }
245
+ declare class IonoAccumulator {
246
+ private state;
247
+ private closed;
248
+ private interval;
249
+ private obsIndices;
250
+ private gloChannels;
251
+ private pairLabel;
252
+ constructor(header: RinexHeader);
253
+ /** Observation callback — wire this into parseRinexStream. */
254
+ onObservation: (time: number, prn: string, _codes: string[], values: (number | null)[]) => void;
255
+ private push;
256
+ /** External cycle-slip notification — close affected arcs. */
257
+ notifySlip(_time: number, prn: string, bands: Set<string>): void;
258
+ private closeArc;
259
+ /** Finalize: close remaining arcs, keep one pair per satellite. */
260
+ finalize(): IonoResult;
261
+ }
262
+
199
263
  /**
200
264
  * Combined quality analysis — runs cycle slip detection, data completeness,
201
- * and multipath analysis in a single file re-parse pass.
265
+ * multipath and ionosphere analysis in a single file re-parse pass.
202
266
  */
203
267
 
204
268
  interface QualityResult {
205
269
  cycleSlips: CycleSlipResult;
206
270
  completeness: CompletenessResult;
207
271
  multipath: MultipathResult;
272
+ iono: IonoResult;
208
273
  }
209
274
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
210
275
 
211
- export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, analyzeQuality };
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 };
package/dist/analysis.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  CompletenessAccumulator,
3
3
  CycleSlipAccumulator,
4
+ IonoAccumulator,
4
5
  MultipathAccumulator,
5
6
  analyzeQuality
6
- } from "./chunk-AW6BORTZ.js";
7
+ } from "./chunk-3ZAFWOT6.js";
7
8
  import "./chunk-3U5AX7PY.js";
8
9
  import "./chunk-FIEWO4J4.js";
9
10
  export {
10
11
  CompletenessAccumulator,
11
12
  CycleSlipAccumulator,
13
+ IonoAccumulator,
12
14
  MultipathAccumulator,
13
15
  analyzeQuality
14
16
  };
@@ -493,11 +493,178 @@ var CompletenessAccumulator = class {
493
493
  }
494
494
  };
495
495
 
496
+ // src/analysis/ionosphere.ts
497
+ var TEC_FACTOR = 403e15;
498
+ var GF_JUMP_M = 0.15;
499
+ var MIN_ARC_LENGTH2 = 10;
500
+ function median2(values) {
501
+ const s = [...values].sort((a, b) => a - b);
502
+ const m = s.length >> 1;
503
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
504
+ }
505
+ var IonoAccumulator = class {
506
+ state = /* @__PURE__ */ new Map();
507
+ closed = /* @__PURE__ */ new Map();
508
+ interval;
509
+ obsIndices;
510
+ gloChannels;
511
+ pairLabel = /* @__PURE__ */ new Map();
512
+ constructor(header) {
513
+ this.interval = header.interval ?? 30;
514
+ this.obsIndices = buildObsIndices(header);
515
+ this.gloChannels = buildGloChannelMap(header.glonassSlots);
516
+ }
517
+ /** Observation callback — wire this into parseRinexStream. */
518
+ onObservation = (time, prn, _codes, values) => {
519
+ const sys = prn[0];
520
+ const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
521
+ if (!bandMap) return;
522
+ const bandData = /* @__PURE__ */ new Map();
523
+ for (const [band, { C, L }] of bandMap) {
524
+ if (C === null) continue;
525
+ const cVal = values[C];
526
+ const lVal = values[L];
527
+ const freq = getFreq(this.gloChannels, prn, band);
528
+ if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
529
+ bandData.set(band, { C: cVal, L: lVal, f: freq });
530
+ }
531
+ }
532
+ if (bandData.size < 2) return;
533
+ const pairs = DUAL_FREQ_PAIRS[sys] ?? [];
534
+ for (const [bi, bj] of pairs) {
535
+ const di = bandData.get(bi);
536
+ const dj = bandData.get(bj);
537
+ if (!di || !dj) continue;
538
+ const \u03BBi = C_LIGHT / di.f;
539
+ const \u03BBj = C_LIGHT / dj.f;
540
+ const gamma = di.f * di.f / (dj.f * dj.f);
541
+ const l4 = di.L * \u03BBi - dj.L * \u03BBj;
542
+ const p4 = dj.C - di.C;
543
+ const pairKey = `${bi}-${bj}`;
544
+ if (!this.pairLabel.has(`${sys}:${pairKey}`)) {
545
+ const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
546
+ const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
547
+ this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
548
+ }
549
+ this.push(prn, pairKey, time, l4, p4, di.f, gamma);
550
+ break;
551
+ }
552
+ };
553
+ push(prn, pairKey, time, l4, p4, fi, gamma) {
554
+ if (!isFinite(l4) || !isFinite(p4)) return;
555
+ let satStates = this.state.get(prn);
556
+ if (!satStates) {
557
+ satStates = /* @__PURE__ */ new Map();
558
+ this.state.set(prn, satStates);
559
+ }
560
+ let ps = satStates.get(pairKey);
561
+ if (!ps) {
562
+ ps = {
563
+ arc: { times: [], l4: [], p4: [], fi, gamma },
564
+ lastTime: 0,
565
+ lastL4: 0
566
+ };
567
+ satStates.set(pairKey, ps);
568
+ }
569
+ const gap = ps.lastTime > 0 ? (time - ps.lastTime) / 1e3 : 0;
570
+ if (ps.lastTime > 0 && gap > this.interval * ARC_GAP_FACTOR) {
571
+ this.closeArc(prn, pairKey, ps);
572
+ } else if (ps.arc.times.length > 0 && Math.abs(l4 - ps.lastL4) > GF_JUMP_M) {
573
+ this.closeArc(prn, pairKey, ps);
574
+ }
575
+ if (ps.arc.times.length === 0) {
576
+ ps.arc.fi = fi;
577
+ ps.arc.gamma = gamma;
578
+ }
579
+ ps.arc.times.push(time);
580
+ ps.arc.l4.push(l4);
581
+ ps.arc.p4.push(p4);
582
+ ps.lastTime = time;
583
+ ps.lastL4 = l4;
584
+ }
585
+ /** External cycle-slip notification — close affected arcs. */
586
+ notifySlip(_time, prn, bands) {
587
+ const satStates = this.state.get(prn);
588
+ if (!satStates) return;
589
+ for (const [pairKey, ps] of satStates) {
590
+ const [bi, bj] = pairKey.split("-");
591
+ if (bands.has(bi) || bands.has(bj)) {
592
+ this.closeArc(prn, pairKey, ps);
593
+ }
594
+ }
595
+ }
596
+ closeArc(prn, pairKey, ps) {
597
+ const arc = ps.arc;
598
+ if (arc.times.length >= MIN_ARC_LENGTH2) {
599
+ const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
600
+ const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
601
+ let satArcs = this.closed.get(prn);
602
+ if (!satArcs) {
603
+ satArcs = /* @__PURE__ */ new Map();
604
+ this.closed.set(prn, satArcs);
605
+ }
606
+ let points = satArcs.get(pairKey);
607
+ if (!points) {
608
+ points = [];
609
+ satArcs.set(pairKey, points);
610
+ }
611
+ for (let k = 0; k < arc.times.length; k++) {
612
+ points.push({
613
+ time: arc.times[k],
614
+ stec: (arc.l4[k] - level) * toTecu
615
+ });
616
+ }
617
+ }
618
+ ps.arc = { times: [], l4: [], p4: [], fi: arc.fi, gamma: arc.gamma };
619
+ }
620
+ /** Finalize: close remaining arcs, keep one pair per satellite. */
621
+ finalize() {
622
+ for (const [prn, satStates] of this.state) {
623
+ for (const [pairKey, ps] of satStates) {
624
+ this.closeArc(prn, pairKey, ps);
625
+ }
626
+ }
627
+ const series = [];
628
+ let sum = 0;
629
+ let count = 0;
630
+ let maxStec = 0;
631
+ for (const [prn, satArcs] of this.closed) {
632
+ let bestKey = null;
633
+ let bestLen = 0;
634
+ for (const [pairKey, points2] of satArcs) {
635
+ if (points2.length > bestLen) {
636
+ bestLen = points2.length;
637
+ bestKey = pairKey;
638
+ }
639
+ }
640
+ if (!bestKey) continue;
641
+ const points = satArcs.get(bestKey);
642
+ points.sort((a, b) => a.time - b.time);
643
+ const sys = prn[0];
644
+ for (const p of points) {
645
+ sum += p.stec;
646
+ count++;
647
+ if (p.stec > maxStec) maxStec = p.stec;
648
+ }
649
+ series.push({
650
+ prn,
651
+ system: sys,
652
+ label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
653
+ points
654
+ });
655
+ }
656
+ series.sort((a, b) => a.prn.localeCompare(b.prn));
657
+ return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
658
+ }
659
+ };
660
+
496
661
  // src/analysis/quality-analysis.ts
497
662
  async function analyzeQuality(file, header, onProgress, signal) {
498
663
  const mpAccum = new MultipathAccumulator(header);
664
+ const ionoAccum = new IonoAccumulator(header);
499
665
  const csAccum = new CycleSlipAccumulator(header, (time, prn, bands) => {
500
666
  mpAccum.notifySlip(time, prn, bands);
667
+ ionoAccum.notifySlip(time, prn, bands);
501
668
  });
502
669
  const compAccum = new CompletenessAccumulator(header);
503
670
  await parseRinexStream(
@@ -507,6 +674,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
507
674
  (time, prn, codes, values) => {
508
675
  csAccum.onObservation(time, prn, codes, values);
509
676
  mpAccum.onObservation(time, prn, codes, values);
677
+ ionoAccum.onObservation(time, prn, codes, values);
510
678
  compAccum.onObservation(time, prn, codes, values);
511
679
  },
512
680
  true
@@ -515,7 +683,8 @@ async function analyzeQuality(file, header, onProgress, signal) {
515
683
  return {
516
684
  cycleSlips: csAccum.finalize(),
517
685
  completeness: compAccum.finalize(),
518
- multipath: mpAccum.finalize()
686
+ multipath: mpAccum.finalize(),
687
+ iono: ionoAccum.finalize()
519
688
  };
520
689
  }
521
690
 
@@ -523,5 +692,6 @@ export {
523
692
  MultipathAccumulator,
524
693
  CycleSlipAccumulator,
525
694
  CompletenessAccumulator,
695
+ IonoAccumulator,
526
696
  analyzeQuality
527
697
  };
package/dist/index.cjs CHANGED
@@ -67,6 +67,7 @@ __export(src_exports, {
67
67
  GLO_F2_BASE: () => GLO_F2_BASE,
68
68
  GLO_F2_STEP: () => GLO_F2_STEP,
69
69
  GLO_F3: () => GLO_F3,
70
+ IonoAccumulator: () => IonoAccumulator,
70
71
  MILLISECONDS_GPS_TAI: () => MILLISECONDS_GPS_TAI,
71
72
  MILLISECONDS_IN_DAY: () => MILLISECONDS_IN_DAY,
72
73
  MILLISECONDS_IN_HOUR: () => MILLISECONDS_IN_HOUR,
@@ -5521,11 +5522,178 @@ var CompletenessAccumulator = class {
5521
5522
  }
5522
5523
  };
5523
5524
 
5525
+ // src/analysis/ionosphere.ts
5526
+ var TEC_FACTOR = 403e15;
5527
+ var GF_JUMP_M = 0.15;
5528
+ var MIN_ARC_LENGTH2 = 10;
5529
+ function median2(values) {
5530
+ const s = [...values].sort((a, b) => a - b);
5531
+ const m = s.length >> 1;
5532
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
5533
+ }
5534
+ var IonoAccumulator = class {
5535
+ state = /* @__PURE__ */ new Map();
5536
+ closed = /* @__PURE__ */ new Map();
5537
+ interval;
5538
+ obsIndices;
5539
+ gloChannels;
5540
+ pairLabel = /* @__PURE__ */ new Map();
5541
+ constructor(header) {
5542
+ this.interval = header.interval ?? 30;
5543
+ this.obsIndices = buildObsIndices(header);
5544
+ this.gloChannels = buildGloChannelMap(header.glonassSlots);
5545
+ }
5546
+ /** Observation callback — wire this into parseRinexStream. */
5547
+ onObservation = (time, prn, _codes, values) => {
5548
+ const sys = prn[0];
5549
+ const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
5550
+ if (!bandMap) return;
5551
+ const bandData = /* @__PURE__ */ new Map();
5552
+ for (const [band, { C, L }] of bandMap) {
5553
+ if (C === null) continue;
5554
+ const cVal = values[C];
5555
+ const lVal = values[L];
5556
+ const freq = getFreq(this.gloChannels, prn, band);
5557
+ if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
5558
+ bandData.set(band, { C: cVal, L: lVal, f: freq });
5559
+ }
5560
+ }
5561
+ if (bandData.size < 2) return;
5562
+ const pairs = DUAL_FREQ_PAIRS[sys] ?? [];
5563
+ for (const [bi, bj] of pairs) {
5564
+ const di = bandData.get(bi);
5565
+ const dj = bandData.get(bj);
5566
+ if (!di || !dj) continue;
5567
+ const \u03BBi = C_LIGHT / di.f;
5568
+ const \u03BBj = C_LIGHT / dj.f;
5569
+ const gamma = di.f * di.f / (dj.f * dj.f);
5570
+ const l4 = di.L * \u03BBi - dj.L * \u03BBj;
5571
+ const p4 = dj.C - di.C;
5572
+ const pairKey = `${bi}-${bj}`;
5573
+ if (!this.pairLabel.has(`${sys}:${pairKey}`)) {
5574
+ const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
5575
+ const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
5576
+ this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
5577
+ }
5578
+ this.push(prn, pairKey, time, l4, p4, di.f, gamma);
5579
+ break;
5580
+ }
5581
+ };
5582
+ push(prn, pairKey, time, l4, p4, fi, gamma) {
5583
+ if (!isFinite(l4) || !isFinite(p4)) return;
5584
+ let satStates = this.state.get(prn);
5585
+ if (!satStates) {
5586
+ satStates = /* @__PURE__ */ new Map();
5587
+ this.state.set(prn, satStates);
5588
+ }
5589
+ let ps = satStates.get(pairKey);
5590
+ if (!ps) {
5591
+ ps = {
5592
+ arc: { times: [], l4: [], p4: [], fi, gamma },
5593
+ lastTime: 0,
5594
+ lastL4: 0
5595
+ };
5596
+ satStates.set(pairKey, ps);
5597
+ }
5598
+ const gap = ps.lastTime > 0 ? (time - ps.lastTime) / 1e3 : 0;
5599
+ if (ps.lastTime > 0 && gap > this.interval * ARC_GAP_FACTOR) {
5600
+ this.closeArc(prn, pairKey, ps);
5601
+ } else if (ps.arc.times.length > 0 && Math.abs(l4 - ps.lastL4) > GF_JUMP_M) {
5602
+ this.closeArc(prn, pairKey, ps);
5603
+ }
5604
+ if (ps.arc.times.length === 0) {
5605
+ ps.arc.fi = fi;
5606
+ ps.arc.gamma = gamma;
5607
+ }
5608
+ ps.arc.times.push(time);
5609
+ ps.arc.l4.push(l4);
5610
+ ps.arc.p4.push(p4);
5611
+ ps.lastTime = time;
5612
+ ps.lastL4 = l4;
5613
+ }
5614
+ /** External cycle-slip notification — close affected arcs. */
5615
+ notifySlip(_time, prn, bands) {
5616
+ const satStates = this.state.get(prn);
5617
+ if (!satStates) return;
5618
+ for (const [pairKey, ps] of satStates) {
5619
+ const [bi, bj] = pairKey.split("-");
5620
+ if (bands.has(bi) || bands.has(bj)) {
5621
+ this.closeArc(prn, pairKey, ps);
5622
+ }
5623
+ }
5624
+ }
5625
+ closeArc(prn, pairKey, ps) {
5626
+ const arc = ps.arc;
5627
+ if (arc.times.length >= MIN_ARC_LENGTH2) {
5628
+ const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
5629
+ const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
5630
+ let satArcs = this.closed.get(prn);
5631
+ if (!satArcs) {
5632
+ satArcs = /* @__PURE__ */ new Map();
5633
+ this.closed.set(prn, satArcs);
5634
+ }
5635
+ let points = satArcs.get(pairKey);
5636
+ if (!points) {
5637
+ points = [];
5638
+ satArcs.set(pairKey, points);
5639
+ }
5640
+ for (let k = 0; k < arc.times.length; k++) {
5641
+ points.push({
5642
+ time: arc.times[k],
5643
+ stec: (arc.l4[k] - level) * toTecu
5644
+ });
5645
+ }
5646
+ }
5647
+ ps.arc = { times: [], l4: [], p4: [], fi: arc.fi, gamma: arc.gamma };
5648
+ }
5649
+ /** Finalize: close remaining arcs, keep one pair per satellite. */
5650
+ finalize() {
5651
+ for (const [prn, satStates] of this.state) {
5652
+ for (const [pairKey, ps] of satStates) {
5653
+ this.closeArc(prn, pairKey, ps);
5654
+ }
5655
+ }
5656
+ const series = [];
5657
+ let sum = 0;
5658
+ let count = 0;
5659
+ let maxStec = 0;
5660
+ for (const [prn, satArcs] of this.closed) {
5661
+ let bestKey = null;
5662
+ let bestLen = 0;
5663
+ for (const [pairKey, points2] of satArcs) {
5664
+ if (points2.length > bestLen) {
5665
+ bestLen = points2.length;
5666
+ bestKey = pairKey;
5667
+ }
5668
+ }
5669
+ if (!bestKey) continue;
5670
+ const points = satArcs.get(bestKey);
5671
+ points.sort((a, b) => a.time - b.time);
5672
+ const sys = prn[0];
5673
+ for (const p of points) {
5674
+ sum += p.stec;
5675
+ count++;
5676
+ if (p.stec > maxStec) maxStec = p.stec;
5677
+ }
5678
+ series.push({
5679
+ prn,
5680
+ system: sys,
5681
+ label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
5682
+ points
5683
+ });
5684
+ }
5685
+ series.sort((a, b) => a.prn.localeCompare(b.prn));
5686
+ return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
5687
+ }
5688
+ };
5689
+
5524
5690
  // src/analysis/quality-analysis.ts
5525
5691
  async function analyzeQuality(file, header, onProgress, signal) {
5526
5692
  const mpAccum = new MultipathAccumulator(header);
5693
+ const ionoAccum = new IonoAccumulator(header);
5527
5694
  const csAccum = new CycleSlipAccumulator(header, (time, prn, bands) => {
5528
5695
  mpAccum.notifySlip(time, prn, bands);
5696
+ ionoAccum.notifySlip(time, prn, bands);
5529
5697
  });
5530
5698
  const compAccum = new CompletenessAccumulator(header);
5531
5699
  await parseRinexStream(
@@ -5535,6 +5703,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
5535
5703
  (time, prn, codes, values) => {
5536
5704
  csAccum.onObservation(time, prn, codes, values);
5537
5705
  mpAccum.onObservation(time, prn, codes, values);
5706
+ ionoAccum.onObservation(time, prn, codes, values);
5538
5707
  compAccum.onObservation(time, prn, codes, values);
5539
5708
  },
5540
5709
  true
@@ -5543,7 +5712,8 @@ async function analyzeQuality(file, header, onProgress, signal) {
5543
5712
  return {
5544
5713
  cycleSlips: csAccum.finalize(),
5545
5714
  completeness: compAccum.finalize(),
5546
- multipath: mpAccum.finalize()
5715
+ multipath: mpAccum.finalize(),
5716
+ iono: ionoAccum.finalize()
5547
5717
  };
5548
5718
  }
5549
5719
 
@@ -6785,6 +6955,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
6785
6955
  GLO_F2_BASE,
6786
6956
  GLO_F2_STEP,
6787
6957
  GLO_F3,
6958
+ IonoAccumulator,
6788
6959
  MILLISECONDS_GPS_TAI,
6789
6960
  MILLISECONDS_IN_DAY,
6790
6961
  MILLISECONDS_IN_HOUR,
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, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, analyzeQuality } from './analysis.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';
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, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, analyzeQuality } from './analysis.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';
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
@@ -6,9 +6,10 @@ import {
6
6
  import {
7
7
  CompletenessAccumulator,
8
8
  CycleSlipAccumulator,
9
+ IonoAccumulator,
9
10
  MultipathAccumulator,
10
11
  analyzeQuality
11
- } from "./chunk-AW6BORTZ.js";
12
+ } from "./chunk-3ZAFWOT6.js";
12
13
  import {
13
14
  frequencyLabel,
14
15
  parseAntex
@@ -293,6 +294,7 @@ export {
293
294
  GLO_F2_BASE,
294
295
  GLO_F2_STEP,
295
296
  GLO_F3,
297
+ IonoAccumulator,
296
298
  MILLISECONDS_GPS_TAI,
297
299
  MILLISECONDS_IN_DAY,
298
300
  MILLISECONDS_IN_HOUR,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.3.2",
3
+ "version": "1.4.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,