gnss-js 1.5.1 → 1.6.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/dist/analysis.cjs CHANGED
@@ -26,6 +26,8 @@ __export(analysis_exports, {
26
26
  MultipathAccumulator: () => MultipathAccumulator,
27
27
  analyzeQuality: () => analyzeQuality,
28
28
  applyIonoDcb: () => applyIonoDcb,
29
+ computeIonoRate: () => computeIonoRate,
30
+ detrendIonoArcs: () => detrendIonoArcs,
29
31
  parseSinexBiasDcb: () => parseSinexBiasDcb
30
32
  });
31
33
  module.exports = __toCommonJS(analysis_exports);
@@ -715,6 +717,7 @@ var GF_JUMP_M = 0.15;
715
717
  var IonoAccumulator = class {
716
718
  state = /* @__PURE__ */ new Map();
717
719
  closed = /* @__PURE__ */ new Map();
720
+ closedArcStarts = /* @__PURE__ */ new Map();
718
721
  interval;
719
722
  obsIndices;
720
723
  gloChannels;
@@ -827,6 +830,17 @@ var IonoAccumulator = class {
827
830
  points = [];
828
831
  satArcs.set(pairKey, points);
829
832
  }
833
+ let satStarts = this.closedArcStarts.get(prn);
834
+ if (!satStarts) {
835
+ satStarts = /* @__PURE__ */ new Map();
836
+ this.closedArcStarts.set(prn, satStarts);
837
+ }
838
+ let starts = satStarts.get(pairKey);
839
+ if (!starts) {
840
+ starts = [];
841
+ satStarts.set(pairKey, starts);
842
+ }
843
+ starts.push(points.length);
830
844
  for (let k = 0; k < arc.times.length; k++) {
831
845
  points.push({
832
846
  time: arc.times[k],
@@ -858,7 +872,7 @@ var IonoAccumulator = class {
858
872
  }
859
873
  if (!bestKey) continue;
860
874
  const points = satArcs.get(bestKey);
861
- points.sort((a, b) => a.time - b.time);
875
+ const arcStarts = this.closedArcStarts.get(prn)?.get(bestKey) ?? [0];
862
876
  const sys = prn[0];
863
877
  for (const p of points) {
864
878
  sum += p.stec;
@@ -873,7 +887,8 @@ var IonoAccumulator = class {
873
887
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
874
888
  codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
875
889
  tecuPerNs,
876
- points
890
+ points,
891
+ arcStarts
877
892
  });
878
893
  }
879
894
  series.sort((a, b) => a.prn.localeCompare(b.prn));
@@ -884,6 +899,87 @@ var IonoAccumulator = class {
884
899
  };
885
900
  }
886
901
  };
902
+ function computeIonoRate(result, intervalSec, order = 1) {
903
+ const out = [];
904
+ for (const s of result.series) {
905
+ const pts = [];
906
+ const bounds = [...s.arcStarts, s.points.length];
907
+ for (let a = 0; a < bounds.length - 1; a++) {
908
+ const lo = bounds[a];
909
+ const hi = bounds[a + 1];
910
+ if (order === 2) {
911
+ for (let i = lo + 2; i < hi; i++) {
912
+ const dt1 = s.points[i - 1].time - s.points[i - 2].time;
913
+ const dt2 = s.points[i].time - s.points[i - 1].time;
914
+ if (Math.abs(dt1 - dt2) > 0.1 * Math.max(dt1, dt2)) continue;
915
+ pts.push({
916
+ time: s.points[i].time,
917
+ value: s.points[i].stec - 2 * s.points[i - 1].stec + s.points[i - 2].stec
918
+ });
919
+ }
920
+ continue;
921
+ }
922
+ if (intervalSec === void 0) {
923
+ for (let i = lo + 1; i < hi; i++) {
924
+ const dtMin = (s.points[i].time - s.points[i - 1].time) / 6e4;
925
+ if (dtMin <= 0) continue;
926
+ pts.push({
927
+ time: s.points[i].time,
928
+ value: (s.points[i].stec - s.points[i - 1].stec) / dtMin
929
+ });
930
+ }
931
+ } else {
932
+ const targetMs = intervalSec * 1e3;
933
+ let j = lo;
934
+ for (let i = lo; i < hi; i++) {
935
+ if (j <= i) j = i + 1;
936
+ while (j < hi && s.points[j].time - s.points[i].time < targetMs)
937
+ j++;
938
+ if (j >= hi) break;
939
+ const dtMs = s.points[j].time - s.points[i].time;
940
+ if (dtMs > 1.5 * targetMs) continue;
941
+ pts.push({
942
+ time: s.points[j].time,
943
+ value: (s.points[j].stec - s.points[i].stec) / dtMs * 6e4
944
+ });
945
+ }
946
+ }
947
+ }
948
+ if (pts.length > 0) out.push({ prn: s.prn, system: s.system, points: pts });
949
+ }
950
+ return out;
951
+ }
952
+ function detrendIonoArcs(result) {
953
+ let sum = 0;
954
+ let count = 0;
955
+ let maxStec = -Infinity;
956
+ const series = result.series.map((s) => {
957
+ const bounds = [...s.arcStarts, s.points.length];
958
+ const points = new Array(s.points.length);
959
+ for (let a = 0; a < bounds.length - 1; a++) {
960
+ const lo = bounds[a];
961
+ const hi = bounds[a + 1];
962
+ const anchor = s.points[lo]?.stec ?? 0;
963
+ for (let i = lo; i < hi; i++) {
964
+ points[i] = {
965
+ time: s.points[i].time,
966
+ stec: s.points[i].stec - anchor
967
+ };
968
+ }
969
+ }
970
+ for (const p of points) {
971
+ sum += p.stec;
972
+ count++;
973
+ if (p.stec > maxStec) maxStec = p.stec;
974
+ }
975
+ return { ...s, points };
976
+ });
977
+ return {
978
+ series,
979
+ maxStec: count > 0 ? maxStec : 0,
980
+ meanStec: count > 0 ? sum / count : 0
981
+ };
982
+ }
887
983
 
888
984
  // src/analysis/dcb.ts
889
985
  var PRN_RE = /^[A-Z]\d{2}$/;
@@ -1835,5 +1931,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
1835
1931
  MultipathAccumulator,
1836
1932
  analyzeQuality,
1837
1933
  applyIonoDcb,
1934
+ computeIonoRate,
1935
+ detrendIonoArcs,
1838
1936
  parseSinexBiasDcb
1839
1937
  });
@@ -239,6 +239,12 @@ interface IonoSeries {
239
239
  tecuPerNs: number;
240
240
  /** Time series of slant TEC values (TECU, DCB-biased). */
241
241
  points: IonoPoint[];
242
+ /**
243
+ * Indices into `points` where a new continuous arc begins (gap,
244
+ * cycle slip, or geometry-free jump). Differencing and per-arc
245
+ * detrending must not cross these boundaries.
246
+ */
247
+ arcStarts: number[];
242
248
  }
243
249
  interface IonoResult {
244
250
  /** Per-satellite slant TEC time series. */
@@ -251,6 +257,7 @@ interface IonoResult {
251
257
  declare class IonoAccumulator {
252
258
  private state;
253
259
  private closed;
260
+ private closedArcStarts;
254
261
  private interval;
255
262
  private obsIndices;
256
263
  private gloChannels;
@@ -267,6 +274,39 @@ declare class IonoAccumulator {
267
274
  /** Finalize: close remaining arcs, keep one pair per satellite. */
268
275
  finalize(): IonoResult;
269
276
  }
277
+ interface IonoRatePoint {
278
+ /** Epoch time (ms) — the later epoch of the differenced pair. */
279
+ time: number;
280
+ /** ROT in TECU/min (order 1) or undivided ΔΔSTEC in TECU (order 2). */
281
+ value: number;
282
+ }
283
+ interface IonoRateSeries {
284
+ prn: string;
285
+ system: string;
286
+ points: IonoRatePoint[];
287
+ }
288
+ /**
289
+ * Sequential time differences of the slant TEC series — the biases
290
+ * (ambiguities, DCBs) cancel, leaving ionospheric rate of TEC, phase
291
+ * noise, and scintillation. Differences never cross arc boundaries,
292
+ * so cycle slips do not appear as outliers (arcs split there).
293
+ *
294
+ * @param intervalSec Standardized differencing baseline in seconds.
295
+ * Omit for native-rate sequential differences; set e.g. 60 for a
296
+ * sample-rate-independent picture (all pairs ~1 min apart are used).
297
+ * @param order 1 (default) = first difference in TECU/min;
298
+ * 2 = second undivided difference in TECU (native rate only,
299
+ * gradients removed, noise and scintillation amplified).
300
+ */
301
+ declare function computeIonoRate(result: IonoResult, intervalSec?: number, order?: 1 | 2): IonoRateSeries[];
302
+ /**
303
+ * Remove the per-arc bias from a slant TEC result — each arc is
304
+ * shifted so its first observation reads zero, leaving only the TEC
305
+ * *variation* along the arc (Hans van der Marel's suggestion; using
306
+ * the first observation keeps the shape untouched at the small risk
307
+ * of anchoring on an outlier).
308
+ */
309
+ declare function detrendIonoArcs(result: IonoResult): IonoResult;
270
310
 
271
311
  /**
272
312
  * Differential code bias (DCB) products and their application to the
@@ -323,4 +363,4 @@ interface QualityResult {
323
363
  }
324
364
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
325
365
 
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 };
366
+ export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoRatePoint, type IonoRateSeries, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb };
@@ -239,6 +239,12 @@ interface IonoSeries {
239
239
  tecuPerNs: number;
240
240
  /** Time series of slant TEC values (TECU, DCB-biased). */
241
241
  points: IonoPoint[];
242
+ /**
243
+ * Indices into `points` where a new continuous arc begins (gap,
244
+ * cycle slip, or geometry-free jump). Differencing and per-arc
245
+ * detrending must not cross these boundaries.
246
+ */
247
+ arcStarts: number[];
242
248
  }
243
249
  interface IonoResult {
244
250
  /** Per-satellite slant TEC time series. */
@@ -251,6 +257,7 @@ interface IonoResult {
251
257
  declare class IonoAccumulator {
252
258
  private state;
253
259
  private closed;
260
+ private closedArcStarts;
254
261
  private interval;
255
262
  private obsIndices;
256
263
  private gloChannels;
@@ -267,6 +274,39 @@ declare class IonoAccumulator {
267
274
  /** Finalize: close remaining arcs, keep one pair per satellite. */
268
275
  finalize(): IonoResult;
269
276
  }
277
+ interface IonoRatePoint {
278
+ /** Epoch time (ms) — the later epoch of the differenced pair. */
279
+ time: number;
280
+ /** ROT in TECU/min (order 1) or undivided ΔΔSTEC in TECU (order 2). */
281
+ value: number;
282
+ }
283
+ interface IonoRateSeries {
284
+ prn: string;
285
+ system: string;
286
+ points: IonoRatePoint[];
287
+ }
288
+ /**
289
+ * Sequential time differences of the slant TEC series — the biases
290
+ * (ambiguities, DCBs) cancel, leaving ionospheric rate of TEC, phase
291
+ * noise, and scintillation. Differences never cross arc boundaries,
292
+ * so cycle slips do not appear as outliers (arcs split there).
293
+ *
294
+ * @param intervalSec Standardized differencing baseline in seconds.
295
+ * Omit for native-rate sequential differences; set e.g. 60 for a
296
+ * sample-rate-independent picture (all pairs ~1 min apart are used).
297
+ * @param order 1 (default) = first difference in TECU/min;
298
+ * 2 = second undivided difference in TECU (native rate only,
299
+ * gradients removed, noise and scintillation amplified).
300
+ */
301
+ declare function computeIonoRate(result: IonoResult, intervalSec?: number, order?: 1 | 2): IonoRateSeries[];
302
+ /**
303
+ * Remove the per-arc bias from a slant TEC result — each arc is
304
+ * shifted so its first observation reads zero, leaving only the TEC
305
+ * *variation* along the arc (Hans van der Marel's suggestion; using
306
+ * the first observation keeps the shape untouched at the small risk
307
+ * of anchoring on an outlier).
308
+ */
309
+ declare function detrendIonoArcs(result: IonoResult): IonoResult;
270
310
 
271
311
  /**
272
312
  * Differential code bias (DCB) products and their application to the
@@ -323,4 +363,4 @@ interface QualityResult {
323
363
  }
324
364
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
325
365
 
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 };
366
+ export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoRatePoint, type IonoRateSeries, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb };
package/dist/analysis.js CHANGED
@@ -5,8 +5,10 @@ import {
5
5
  MultipathAccumulator,
6
6
  analyzeQuality,
7
7
  applyIonoDcb,
8
+ computeIonoRate,
9
+ detrendIonoArcs,
8
10
  parseSinexBiasDcb
9
- } from "./chunk-GDGHTC3E.js";
11
+ } from "./chunk-Z5UZUNCB.js";
10
12
  import "./chunk-3U5AX7PY.js";
11
13
  import "./chunk-FIEWO4J4.js";
12
14
  export {
@@ -16,5 +18,7 @@ export {
16
18
  MultipathAccumulator,
17
19
  analyzeQuality,
18
20
  applyIonoDcb,
21
+ computeIonoRate,
22
+ detrendIonoArcs,
19
23
  parseSinexBiasDcb
20
24
  };
@@ -2,7 +2,7 @@ import {
2
2
  computeDop,
3
3
  computeSatPosition,
4
4
  ecefToAzEl
5
- } from "./chunk-CD5YSYCG.js";
5
+ } from "./chunk-JG2HDJGK.js";
6
6
  import {
7
7
  C_LIGHT,
8
8
  OMEGA_E
@@ -98,7 +98,7 @@ function gloDerivatives(state, acc) {
98
98
  const r3 = r2 * r;
99
99
  const r5 = r2 * r3;
100
100
  const mu_r3 = GM_GLO / r3;
101
- const j2_term = 1.5 * J2_GLO * AE_GLO * AE_GLO / r5;
101
+ const j2_term = 1.5 * J2_GLO * GM_GLO * AE_GLO * AE_GLO / r5;
102
102
  const dvx = -mu_r3 * x + j2_term * x * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * x + 2 * OMEGA_E * vy + ax;
103
103
  const dvy = -mu_r3 * y + j2_term * y * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * y - 2 * OMEGA_E * vx + ay;
104
104
  const dvz = -mu_r3 * z + j2_term * z * (5 * z * z / r2 - 3) + az;
@@ -512,6 +512,7 @@ var GF_JUMP_M = 0.15;
512
512
  var IonoAccumulator = class {
513
513
  state = /* @__PURE__ */ new Map();
514
514
  closed = /* @__PURE__ */ new Map();
515
+ closedArcStarts = /* @__PURE__ */ new Map();
515
516
  interval;
516
517
  obsIndices;
517
518
  gloChannels;
@@ -624,6 +625,17 @@ var IonoAccumulator = class {
624
625
  points = [];
625
626
  satArcs.set(pairKey, points);
626
627
  }
628
+ let satStarts = this.closedArcStarts.get(prn);
629
+ if (!satStarts) {
630
+ satStarts = /* @__PURE__ */ new Map();
631
+ this.closedArcStarts.set(prn, satStarts);
632
+ }
633
+ let starts = satStarts.get(pairKey);
634
+ if (!starts) {
635
+ starts = [];
636
+ satStarts.set(pairKey, starts);
637
+ }
638
+ starts.push(points.length);
627
639
  for (let k = 0; k < arc.times.length; k++) {
628
640
  points.push({
629
641
  time: arc.times[k],
@@ -655,7 +667,7 @@ var IonoAccumulator = class {
655
667
  }
656
668
  if (!bestKey) continue;
657
669
  const points = satArcs.get(bestKey);
658
- points.sort((a, b) => a.time - b.time);
670
+ const arcStarts = this.closedArcStarts.get(prn)?.get(bestKey) ?? [0];
659
671
  const sys = prn[0];
660
672
  for (const p of points) {
661
673
  sum += p.stec;
@@ -670,7 +682,8 @@ var IonoAccumulator = class {
670
682
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
671
683
  codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
672
684
  tecuPerNs,
673
- points
685
+ points,
686
+ arcStarts
674
687
  });
675
688
  }
676
689
  series.sort((a, b) => a.prn.localeCompare(b.prn));
@@ -681,6 +694,87 @@ var IonoAccumulator = class {
681
694
  };
682
695
  }
683
696
  };
697
+ function computeIonoRate(result, intervalSec, order = 1) {
698
+ const out = [];
699
+ for (const s of result.series) {
700
+ const pts = [];
701
+ const bounds = [...s.arcStarts, s.points.length];
702
+ for (let a = 0; a < bounds.length - 1; a++) {
703
+ const lo = bounds[a];
704
+ const hi = bounds[a + 1];
705
+ if (order === 2) {
706
+ for (let i = lo + 2; i < hi; i++) {
707
+ const dt1 = s.points[i - 1].time - s.points[i - 2].time;
708
+ const dt2 = s.points[i].time - s.points[i - 1].time;
709
+ if (Math.abs(dt1 - dt2) > 0.1 * Math.max(dt1, dt2)) continue;
710
+ pts.push({
711
+ time: s.points[i].time,
712
+ value: s.points[i].stec - 2 * s.points[i - 1].stec + s.points[i - 2].stec
713
+ });
714
+ }
715
+ continue;
716
+ }
717
+ if (intervalSec === void 0) {
718
+ for (let i = lo + 1; i < hi; i++) {
719
+ const dtMin = (s.points[i].time - s.points[i - 1].time) / 6e4;
720
+ if (dtMin <= 0) continue;
721
+ pts.push({
722
+ time: s.points[i].time,
723
+ value: (s.points[i].stec - s.points[i - 1].stec) / dtMin
724
+ });
725
+ }
726
+ } else {
727
+ const targetMs = intervalSec * 1e3;
728
+ let j = lo;
729
+ for (let i = lo; i < hi; i++) {
730
+ if (j <= i) j = i + 1;
731
+ while (j < hi && s.points[j].time - s.points[i].time < targetMs)
732
+ j++;
733
+ if (j >= hi) break;
734
+ const dtMs = s.points[j].time - s.points[i].time;
735
+ if (dtMs > 1.5 * targetMs) continue;
736
+ pts.push({
737
+ time: s.points[j].time,
738
+ value: (s.points[j].stec - s.points[i].stec) / dtMs * 6e4
739
+ });
740
+ }
741
+ }
742
+ }
743
+ if (pts.length > 0) out.push({ prn: s.prn, system: s.system, points: pts });
744
+ }
745
+ return out;
746
+ }
747
+ function detrendIonoArcs(result) {
748
+ let sum = 0;
749
+ let count = 0;
750
+ let maxStec = -Infinity;
751
+ const series = result.series.map((s) => {
752
+ const bounds = [...s.arcStarts, s.points.length];
753
+ const points = new Array(s.points.length);
754
+ for (let a = 0; a < bounds.length - 1; a++) {
755
+ const lo = bounds[a];
756
+ const hi = bounds[a + 1];
757
+ const anchor = s.points[lo]?.stec ?? 0;
758
+ for (let i = lo; i < hi; i++) {
759
+ points[i] = {
760
+ time: s.points[i].time,
761
+ stec: s.points[i].stec - anchor
762
+ };
763
+ }
764
+ }
765
+ for (const p of points) {
766
+ sum += p.stec;
767
+ count++;
768
+ if (p.stec > maxStec) maxStec = p.stec;
769
+ }
770
+ return { ...s, points };
771
+ });
772
+ return {
773
+ series,
774
+ maxStec: count > 0 ? maxStec : 0,
775
+ meanStec: count > 0 ? sum / count : 0
776
+ };
777
+ }
684
778
 
685
779
  // src/analysis/dcb.ts
686
780
  var PRN_RE = /^[A-Z]\d{2}$/;
@@ -844,6 +938,8 @@ export {
844
938
  CycleSlipAccumulator,
845
939
  CompletenessAccumulator,
846
940
  IonoAccumulator,
941
+ computeIonoRate,
942
+ detrendIonoArcs,
847
943
  parseSinexBiasDcb,
848
944
  applyIonoDcb,
849
945
  analyzeQuality
package/dist/index.cjs CHANGED
@@ -111,6 +111,7 @@ __export(src_exports, {
111
111
  clampUnit: () => clampUnit,
112
112
  computeAllPositions: () => computeAllPositions,
113
113
  computeDop: () => computeDop,
114
+ computeIonoRate: () => computeIonoRate,
114
115
  computeLiveSkyPositions: () => computeLiveSkyPositions,
115
116
  computePsdDb: () => computePsdDb,
116
117
  computeSatPosition: () => computeSatPosition,
@@ -126,6 +127,7 @@ __export(src_exports, {
126
127
  decodeMsmFull: () => decodeMsmFull,
127
128
  deg2dms: () => deg2dms,
128
129
  deg2rad: () => deg2rad,
130
+ detrendIonoArcs: () => detrendIonoArcs,
129
131
  ecefToAzEl: () => ecefToAzEl,
130
132
  ecefToGeodetic: () => ecefToGeodetic,
131
133
  ephInfoToEphemeris: () => ephInfoToEphemeris,
@@ -4086,7 +4088,7 @@ function gloDerivatives(state, acc) {
4086
4088
  const r3 = r2 * r;
4087
4089
  const r5 = r2 * r3;
4088
4090
  const mu_r3 = GM_GLO / r3;
4089
- const j2_term = 1.5 * J2_GLO * AE_GLO * AE_GLO / r5;
4091
+ const j2_term = 1.5 * J2_GLO * GM_GLO * AE_GLO * AE_GLO / r5;
4090
4092
  const dvx = -mu_r3 * x + j2_term * x * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * x + 2 * OMEGA_E * vy + ax;
4091
4093
  const dvy = -mu_r3 * y + j2_term * y * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * y - 2 * OMEGA_E * vx + ay;
4092
4094
  const dvz = -mu_r3 * z + j2_term * z * (5 * z * z / r2 - 3) + az;
@@ -5541,6 +5543,7 @@ var GF_JUMP_M = 0.15;
5541
5543
  var IonoAccumulator = class {
5542
5544
  state = /* @__PURE__ */ new Map();
5543
5545
  closed = /* @__PURE__ */ new Map();
5546
+ closedArcStarts = /* @__PURE__ */ new Map();
5544
5547
  interval;
5545
5548
  obsIndices;
5546
5549
  gloChannels;
@@ -5653,6 +5656,17 @@ var IonoAccumulator = class {
5653
5656
  points = [];
5654
5657
  satArcs.set(pairKey, points);
5655
5658
  }
5659
+ let satStarts = this.closedArcStarts.get(prn);
5660
+ if (!satStarts) {
5661
+ satStarts = /* @__PURE__ */ new Map();
5662
+ this.closedArcStarts.set(prn, satStarts);
5663
+ }
5664
+ let starts = satStarts.get(pairKey);
5665
+ if (!starts) {
5666
+ starts = [];
5667
+ satStarts.set(pairKey, starts);
5668
+ }
5669
+ starts.push(points.length);
5656
5670
  for (let k = 0; k < arc.times.length; k++) {
5657
5671
  points.push({
5658
5672
  time: arc.times[k],
@@ -5684,7 +5698,7 @@ var IonoAccumulator = class {
5684
5698
  }
5685
5699
  if (!bestKey) continue;
5686
5700
  const points = satArcs.get(bestKey);
5687
- points.sort((a, b) => a.time - b.time);
5701
+ const arcStarts = this.closedArcStarts.get(prn)?.get(bestKey) ?? [0];
5688
5702
  const sys = prn[0];
5689
5703
  for (const p of points) {
5690
5704
  sum += p.stec;
@@ -5699,7 +5713,8 @@ var IonoAccumulator = class {
5699
5713
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
5700
5714
  codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
5701
5715
  tecuPerNs,
5702
- points
5716
+ points,
5717
+ arcStarts
5703
5718
  });
5704
5719
  }
5705
5720
  series.sort((a, b) => a.prn.localeCompare(b.prn));
@@ -5710,6 +5725,87 @@ var IonoAccumulator = class {
5710
5725
  };
5711
5726
  }
5712
5727
  };
5728
+ function computeIonoRate(result, intervalSec, order = 1) {
5729
+ const out = [];
5730
+ for (const s of result.series) {
5731
+ const pts = [];
5732
+ const bounds = [...s.arcStarts, s.points.length];
5733
+ for (let a = 0; a < bounds.length - 1; a++) {
5734
+ const lo = bounds[a];
5735
+ const hi = bounds[a + 1];
5736
+ if (order === 2) {
5737
+ for (let i = lo + 2; i < hi; i++) {
5738
+ const dt1 = s.points[i - 1].time - s.points[i - 2].time;
5739
+ const dt2 = s.points[i].time - s.points[i - 1].time;
5740
+ if (Math.abs(dt1 - dt2) > 0.1 * Math.max(dt1, dt2)) continue;
5741
+ pts.push({
5742
+ time: s.points[i].time,
5743
+ value: s.points[i].stec - 2 * s.points[i - 1].stec + s.points[i - 2].stec
5744
+ });
5745
+ }
5746
+ continue;
5747
+ }
5748
+ if (intervalSec === void 0) {
5749
+ for (let i = lo + 1; i < hi; i++) {
5750
+ const dtMin = (s.points[i].time - s.points[i - 1].time) / 6e4;
5751
+ if (dtMin <= 0) continue;
5752
+ pts.push({
5753
+ time: s.points[i].time,
5754
+ value: (s.points[i].stec - s.points[i - 1].stec) / dtMin
5755
+ });
5756
+ }
5757
+ } else {
5758
+ const targetMs = intervalSec * 1e3;
5759
+ let j = lo;
5760
+ for (let i = lo; i < hi; i++) {
5761
+ if (j <= i) j = i + 1;
5762
+ while (j < hi && s.points[j].time - s.points[i].time < targetMs)
5763
+ j++;
5764
+ if (j >= hi) break;
5765
+ const dtMs = s.points[j].time - s.points[i].time;
5766
+ if (dtMs > 1.5 * targetMs) continue;
5767
+ pts.push({
5768
+ time: s.points[j].time,
5769
+ value: (s.points[j].stec - s.points[i].stec) / dtMs * 6e4
5770
+ });
5771
+ }
5772
+ }
5773
+ }
5774
+ if (pts.length > 0) out.push({ prn: s.prn, system: s.system, points: pts });
5775
+ }
5776
+ return out;
5777
+ }
5778
+ function detrendIonoArcs(result) {
5779
+ let sum = 0;
5780
+ let count = 0;
5781
+ let maxStec = -Infinity;
5782
+ const series = result.series.map((s) => {
5783
+ const bounds = [...s.arcStarts, s.points.length];
5784
+ const points = new Array(s.points.length);
5785
+ for (let a = 0; a < bounds.length - 1; a++) {
5786
+ const lo = bounds[a];
5787
+ const hi = bounds[a + 1];
5788
+ const anchor = s.points[lo]?.stec ?? 0;
5789
+ for (let i = lo; i < hi; i++) {
5790
+ points[i] = {
5791
+ time: s.points[i].time,
5792
+ stec: s.points[i].stec - anchor
5793
+ };
5794
+ }
5795
+ }
5796
+ for (const p of points) {
5797
+ sum += p.stec;
5798
+ count++;
5799
+ if (p.stec > maxStec) maxStec = p.stec;
5800
+ }
5801
+ return { ...s, points };
5802
+ });
5803
+ return {
5804
+ series,
5805
+ maxStec: count > 0 ? maxStec : 0,
5806
+ meanStec: count > 0 ? sum / count : 0
5807
+ };
5808
+ }
5713
5809
 
5714
5810
  // src/analysis/dcb.ts
5715
5811
  var PRN_RE = /^[A-Z]\d{2}$/;
@@ -7150,6 +7246,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7150
7246
  clampUnit,
7151
7247
  computeAllPositions,
7152
7248
  computeDop,
7249
+ computeIonoRate,
7153
7250
  computeLiveSkyPositions,
7154
7251
  computePsdDb,
7155
7252
  computeSatPosition,
@@ -7165,6 +7262,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7165
7262
  decodeMsmFull,
7166
7263
  deg2dms,
7167
7264
  deg2rad,
7265
+ detrendIonoArcs,
7168
7266
  ecefToAzEl,
7169
7267
  ecefToGeodetic,
7170
7268
  ephInfoToEphemeris,
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, IonoDcbResult, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb } from './analysis.cjs';
16
+ export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoRatePoint, IonoRateSeries, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, 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, IonoDcbResult, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb } from './analysis.js';
16
+ export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoRatePoint, IonoRateSeries, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, 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
@@ -10,8 +10,10 @@ import {
10
10
  MultipathAccumulator,
11
11
  analyzeQuality,
12
12
  applyIonoDcb,
13
+ computeIonoRate,
14
+ detrendIonoArcs,
13
15
  parseSinexBiasDcb
14
- } from "./chunk-GDGHTC3E.js";
16
+ } from "./chunk-Z5UZUNCB.js";
15
17
  import {
16
18
  frequencyLabel,
17
19
  parseAntex
@@ -100,7 +102,7 @@ import {
100
102
  ionoFree,
101
103
  satClockCorrection,
102
104
  solveSpp
103
- } from "./chunk-SOJNWF5T.js";
105
+ } from "./chunk-4PXLRDK6.js";
104
106
  import {
105
107
  computeAllPositions,
106
108
  computeDop,
@@ -113,7 +115,7 @@ import {
113
115
  keplerPosition,
114
116
  navTimesFromEph,
115
117
  selectEphemeris
116
- } from "./chunk-CD5YSYCG.js";
118
+ } from "./chunk-JG2HDJGK.js";
117
119
  import {
118
120
  getBdsTime,
119
121
  getDateFromBdsTime,
@@ -340,6 +342,7 @@ export {
340
342
  clampUnit,
341
343
  computeAllPositions,
342
344
  computeDop,
345
+ computeIonoRate,
343
346
  computeLiveSkyPositions,
344
347
  computePsdDb,
345
348
  computeSatPosition,
@@ -355,6 +358,7 @@ export {
355
358
  decodeMsmFull,
356
359
  deg2dms,
357
360
  deg2rad,
361
+ detrendIonoArcs,
358
362
  ecefToAzEl,
359
363
  ecefToGeodetic,
360
364
  ephInfoToEphemeris,
package/dist/orbit.cjs CHANGED
@@ -251,7 +251,7 @@ function gloDerivatives(state, acc) {
251
251
  const r3 = r2 * r;
252
252
  const r5 = r2 * r3;
253
253
  const mu_r3 = GM_GLO / r3;
254
- const j2_term = 1.5 * J2_GLO * AE_GLO * AE_GLO / r5;
254
+ const j2_term = 1.5 * J2_GLO * GM_GLO * AE_GLO * AE_GLO / r5;
255
255
  const dvx = -mu_r3 * x + j2_term * x * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * x + 2 * OMEGA_E * vy + ax;
256
256
  const dvy = -mu_r3 * y + j2_term * y * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * y - 2 * OMEGA_E * vx + ay;
257
257
  const dvz = -mu_r3 * z + j2_term * z * (5 * z * z / r2 - 3) + az;
package/dist/orbit.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  keplerPosition,
11
11
  navTimesFromEph,
12
12
  selectEphemeris
13
- } from "./chunk-CD5YSYCG.js";
13
+ } from "./chunk-JG2HDJGK.js";
14
14
  import "./chunk-HVXYFUCB.js";
15
15
  import {
16
16
  ecefToGeodetic,
@@ -235,7 +235,7 @@ function gloDerivatives(state, acc) {
235
235
  const r3 = r2 * r;
236
236
  const r5 = r2 * r3;
237
237
  const mu_r3 = GM_GLO / r3;
238
- const j2_term = 1.5 * J2_GLO * AE_GLO * AE_GLO / r5;
238
+ const j2_term = 1.5 * J2_GLO * GM_GLO * AE_GLO * AE_GLO / r5;
239
239
  const dvx = -mu_r3 * x + j2_term * x * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * x + 2 * OMEGA_E * vy + ax;
240
240
  const dvy = -mu_r3 * y + j2_term * y * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * y - 2 * OMEGA_E * vx + ay;
241
241
  const dvz = -mu_r3 * z + j2_term * z * (5 * z * z / r2 - 3) + az;
@@ -2,8 +2,8 @@ import {
2
2
  ionoFree,
3
3
  satClockCorrection,
4
4
  solveSpp
5
- } from "./chunk-SOJNWF5T.js";
6
- import "./chunk-CD5YSYCG.js";
5
+ } from "./chunk-4PXLRDK6.js";
6
+ import "./chunk-JG2HDJGK.js";
7
7
  import "./chunk-HVXYFUCB.js";
8
8
  import "./chunk-37QNKGTC.js";
9
9
  import "./chunk-6FAL6P4G.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
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,
@@ -118,14 +118,14 @@
118
118
  "devDependencies": {
119
119
  "@eslint/js": "^10.0.1",
120
120
  "@types/node": "^20.11.17",
121
- "eslint": "^10.1.0",
121
+ "eslint": "^10.7.0",
122
122
  "husky": "^9.1.7",
123
123
  "lint-staged": "^16.4.0",
124
- "prettier": "^3.5.3",
124
+ "prettier": "^3.9.5",
125
125
  "tsup": "^8.0.2",
126
126
  "typescript": "^5.8.3",
127
- "typescript-eslint": "^8.57.2",
128
- "vitest": "^4.0.0"
127
+ "typescript-eslint": "^8.64.0",
128
+ "vitest": "^4.1.10"
129
129
  },
130
130
  "lint-staged": {
131
131
  "*.{ts,js}": [