gnss-js 1.5.0 → 1.6.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.
@@ -13,16 +13,22 @@ import {
13
13
  getFreq
14
14
  } from "./chunk-FIEWO4J4.js";
15
15
 
16
- // src/analysis/multipath.ts
16
+ // src/analysis/stats-util.ts
17
17
  var MIN_ARC_LENGTH = 10;
18
- var MP_JUMP_M = 1.25;
19
- var MP_JUMP_WINDOW = 5;
20
- var MP_EDIT_SIGMA = 3;
21
18
  function median(values) {
22
19
  const s = [...values].sort((a, b) => a - b);
23
20
  const m = s.length >> 1;
24
21
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
25
22
  }
23
+ function percentile(values, p) {
24
+ const s = [...values].sort((a, b) => a - b);
25
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
26
+ }
27
+
28
+ // src/analysis/multipath.ts
29
+ var MP_JUMP_M = 1.25;
30
+ var MP_JUMP_WINDOW = 5;
31
+ var MP_EDIT_SIGMA = 3;
26
32
  var MultipathAccumulator = class {
27
33
  state = /* @__PURE__ */ new Map();
28
34
  results = [];
@@ -503,15 +509,10 @@ var CompletenessAccumulator = class {
503
509
  // src/analysis/ionosphere.ts
504
510
  var TEC_FACTOR = 403e15;
505
511
  var GF_JUMP_M = 0.15;
506
- var MIN_ARC_LENGTH2 = 10;
507
- function median2(values) {
508
- const s = [...values].sort((a, b) => a - b);
509
- const m = s.length >> 1;
510
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
511
- }
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;
@@ -611,8 +612,8 @@ var IonoAccumulator = class {
611
612
  }
612
613
  closeArc(prn, pairKey, ps) {
613
614
  const arc = ps.arc;
614
- if (arc.times.length >= MIN_ARC_LENGTH2) {
615
- const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
615
+ if (arc.times.length >= MIN_ARC_LENGTH) {
616
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
616
617
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
617
618
  let satArcs = this.closed.get(prn);
618
619
  if (!satArcs) {
@@ -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],
@@ -643,7 +655,7 @@ var IonoAccumulator = class {
643
655
  const series = [];
644
656
  let sum = 0;
645
657
  let count = 0;
646
- let maxStec = 0;
658
+ let maxStec = -Infinity;
647
659
  for (const [prn, satArcs] of this.closed) {
648
660
  let bestKey = null;
649
661
  let bestLen = 0;
@@ -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,21 +682,107 @@ 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));
677
- return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
690
+ return {
691
+ series,
692
+ maxStec: count > 0 ? maxStec : 0,
693
+ meanStec: count > 0 ? sum / count : 0
694
+ };
678
695
  }
679
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
+ }
680
778
 
681
779
  // src/analysis/dcb.ts
682
780
  var PRN_RE = /^[A-Z]\d{2}$/;
683
781
  var SVN_RE = /^[A-Z]\d{3}$/;
684
782
  var OBS_RE = /^[CL]\d[A-Z]$/;
685
- function sinexEpochMs(s) {
783
+ function sinexEpochMs(s, openValue) {
686
784
  const [y, d, sec] = s.split(":").map(Number);
687
- if (!y) return Infinity;
785
+ if (!y) return openValue;
688
786
  return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
689
787
  }
690
788
  function parseSinexBiasDcb(text, epochMs) {
@@ -699,8 +797,8 @@ function parseSinexBiasDcb(text, epochMs) {
699
797
  if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
700
798
  const value = parseFloat(t[unitIdx + 1]);
701
799
  if (!isFinite(value)) continue;
702
- const start = sinexEpochMs(t[5] ?? "");
703
- const end = sinexEpochMs(t[6] ?? "");
800
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
801
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
704
802
  const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
705
803
  const prn = t[2];
706
804
  const pair = `${t[3]}-${t[4]}`;
@@ -722,10 +820,6 @@ function parseSinexBiasDcb(text, epochMs) {
722
820
  }
723
821
  return out;
724
822
  }
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
823
  function applyIonoDcb(iono, satDcb) {
730
824
  const missing = [];
731
825
  let corrected = 0;
@@ -789,7 +883,7 @@ function applyIonoDcb(iono, satDcb) {
789
883
  out.sort((a, b) => a.prn.localeCompare(b.prn));
790
884
  let sum = 0;
791
885
  let count = 0;
792
- let maxStec = 0;
886
+ let maxStec = -Infinity;
793
887
  for (const s of out) {
794
888
  for (const p of s.points) {
795
889
  sum += p.stec;
@@ -798,7 +892,11 @@ function applyIonoDcb(iono, satDcb) {
798
892
  }
799
893
  }
800
894
  return {
801
- result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
895
+ result: {
896
+ series: out,
897
+ maxStec: count > 0 ? maxStec : 0,
898
+ meanStec: count > 0 ? sum / count : 0
899
+ },
802
900
  satellitesCorrected: corrected,
803
901
  satellitesMissing: [...new Set(missing)].sort(),
804
902
  receiverDcbTecu
@@ -840,6 +938,8 @@ export {
840
938
  CycleSlipAccumulator,
841
939
  CompletenessAccumulator,
842
940
  IonoAccumulator,
941
+ computeIonoRate,
942
+ detrendIonoArcs,
843
943
  parseSinexBiasDcb,
844
944
  applyIonoDcb,
845
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,
@@ -3906,6 +3908,11 @@ function rtcm3Constellation(msgType) {
3906
3908
  if (msgType >= 1111 && msgType <= 1117) return "QZSS";
3907
3909
  if (msgType >= 1121 && msgType <= 1127) return "BeiDou";
3908
3910
  if (msgType >= 1131 && msgType <= 1137) return "NavIC";
3911
+ if (msgType >= 1015 && msgType <= 1017) return "GPS";
3912
+ if (msgType === 1030) return "GPS";
3913
+ if (msgType === 1031) return "GLONASS";
3914
+ if (msgType >= 1057 && msgType <= 1062) return "GPS";
3915
+ if (msgType >= 1063 && msgType <= 1068) return "GLONASS";
3909
3916
  if (msgType === 1230) return "GLONASS";
3910
3917
  return null;
3911
3918
  }
@@ -3945,7 +3952,6 @@ function updateStreamStats(stats, frames, rawBytes) {
3945
3952
  entry.totalBytes += frame.length + 6;
3946
3953
  const msmEpoch = decodeMsmFull(frame);
3947
3954
  if (msmEpoch) {
3948
- const now2 = Date.now();
3949
3955
  for (const obs of msmEpoch.observations) {
3950
3956
  const signals = [];
3951
3957
  let bestCn0 = 0;
@@ -3960,7 +3966,7 @@ function updateStreamStats(stats, frames, rawBytes) {
3960
3966
  prn: obs.prn,
3961
3967
  system: obs.system,
3962
3968
  cn0: bestCn0,
3963
- lastSeen: now2,
3969
+ lastSeen: now,
3964
3970
  signals
3965
3971
  });
3966
3972
  }
@@ -4203,20 +4209,10 @@ function invert4x4(m) {
4203
4209
  return result;
4204
4210
  }
4205
4211
  function selectEphemeris(ephemerides, prn, timeMs) {
4206
- let best = null;
4207
- let bestDt = Infinity;
4208
- for (const eph of ephemerides) {
4209
- if (eph.prn !== prn) continue;
4210
- const ephTime = eph.tocDate.getTime();
4211
- const dt = Math.abs(timeMs - ephTime);
4212
- const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
4213
- if (dt > maxAge) continue;
4214
- if (dt < bestDt) {
4215
- bestDt = dt;
4216
- best = eph;
4217
- }
4218
- }
4219
- return best;
4212
+ return selectBest(
4213
+ ephemerides.filter((e) => e.prn === prn),
4214
+ timeMs
4215
+ );
4220
4216
  }
4221
4217
  function computeSatPosition(eph, timeMs) {
4222
4218
  if (eph.system === "R" || eph.system === "S") {
@@ -4309,8 +4305,9 @@ function selectBest(ephs, timeMs) {
4309
4305
  return best;
4310
4306
  }
4311
4307
  var GPS_EPOCH_MS = START_GPS_TIME.getTime();
4308
+ var MS_PER_WEEK2 = 7 * 864e5;
4312
4309
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
4313
- var GAL_EPOCH_MS = GPS_EPOCH_MS;
4310
+ var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK2;
4314
4311
  function ephInfoToEphemeris(info) {
4315
4312
  const sys = info.prn.charAt(0);
4316
4313
  if (sys === "R") {
@@ -4354,6 +4351,7 @@ function ephInfoToEphemeris(info) {
4354
4351
  return null;
4355
4352
  }
4356
4353
  let epochMs;
4354
+ let week = info.week;
4357
4355
  const tocSec = info.toc ?? info.toe;
4358
4356
  if (sys === "C") {
4359
4357
  epochMs = BDS_EPOCH_MS;
@@ -4361,8 +4359,10 @@ function ephInfoToEphemeris(info) {
4361
4359
  epochMs = GAL_EPOCH_MS;
4362
4360
  } else {
4363
4361
  epochMs = GPS_EPOCH_MS;
4362
+ const weeksNow = (info.lastReceived - GPS_EPOCH_MS) / MS_PER_WEEK2;
4363
+ week += 1024 * Math.round((weeksNow - week) / 1024);
4364
4364
  }
4365
- const tocDate = new Date(epochMs + info.week * 7 * 864e5 + tocSec * 1e3);
4365
+ const tocDate = new Date(epochMs + week * MS_PER_WEEK2 + tocSec * 1e3);
4366
4366
  return {
4367
4367
  system: sys,
4368
4368
  prn: info.prn,
@@ -4388,7 +4388,7 @@ function ephInfoToEphemeris(info) {
4388
4388
  omega: info.argPerigee,
4389
4389
  omegaDot: info.omegaDot ?? 0,
4390
4390
  idot: info.idot ?? 0,
4391
- week: info.week,
4391
+ week,
4392
4392
  svHealth: info.health,
4393
4393
  tgd: 0
4394
4394
  };
@@ -5044,16 +5044,22 @@ async function connectToMountpoint(proxyUrl, info, signal) {
5044
5044
  };
5045
5045
  }
5046
5046
 
5047
- // src/analysis/multipath.ts
5047
+ // src/analysis/stats-util.ts
5048
5048
  var MIN_ARC_LENGTH = 10;
5049
- var MP_JUMP_M = 1.25;
5050
- var MP_JUMP_WINDOW = 5;
5051
- var MP_EDIT_SIGMA = 3;
5052
5049
  function median(values) {
5053
5050
  const s = [...values].sort((a, b) => a - b);
5054
5051
  const m = s.length >> 1;
5055
5052
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
5056
5053
  }
5054
+ function percentile(values, p) {
5055
+ const s = [...values].sort((a, b) => a - b);
5056
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
5057
+ }
5058
+
5059
+ // src/analysis/multipath.ts
5060
+ var MP_JUMP_M = 1.25;
5061
+ var MP_JUMP_WINDOW = 5;
5062
+ var MP_EDIT_SIGMA = 3;
5057
5063
  var MultipathAccumulator = class {
5058
5064
  state = /* @__PURE__ */ new Map();
5059
5065
  results = [];
@@ -5534,15 +5540,10 @@ var CompletenessAccumulator = class {
5534
5540
  // src/analysis/ionosphere.ts
5535
5541
  var TEC_FACTOR = 403e15;
5536
5542
  var GF_JUMP_M = 0.15;
5537
- var MIN_ARC_LENGTH2 = 10;
5538
- function median2(values) {
5539
- const s = [...values].sort((a, b) => a - b);
5540
- const m = s.length >> 1;
5541
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
5542
- }
5543
5543
  var IonoAccumulator = class {
5544
5544
  state = /* @__PURE__ */ new Map();
5545
5545
  closed = /* @__PURE__ */ new Map();
5546
+ closedArcStarts = /* @__PURE__ */ new Map();
5546
5547
  interval;
5547
5548
  obsIndices;
5548
5549
  gloChannels;
@@ -5642,8 +5643,8 @@ var IonoAccumulator = class {
5642
5643
  }
5643
5644
  closeArc(prn, pairKey, ps) {
5644
5645
  const arc = ps.arc;
5645
- if (arc.times.length >= MIN_ARC_LENGTH2) {
5646
- const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
5646
+ if (arc.times.length >= MIN_ARC_LENGTH) {
5647
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
5647
5648
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
5648
5649
  let satArcs = this.closed.get(prn);
5649
5650
  if (!satArcs) {
@@ -5655,6 +5656,17 @@ var IonoAccumulator = class {
5655
5656
  points = [];
5656
5657
  satArcs.set(pairKey, points);
5657
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);
5658
5670
  for (let k = 0; k < arc.times.length; k++) {
5659
5671
  points.push({
5660
5672
  time: arc.times[k],
@@ -5674,7 +5686,7 @@ var IonoAccumulator = class {
5674
5686
  const series = [];
5675
5687
  let sum = 0;
5676
5688
  let count = 0;
5677
- let maxStec = 0;
5689
+ let maxStec = -Infinity;
5678
5690
  for (const [prn, satArcs] of this.closed) {
5679
5691
  let bestKey = null;
5680
5692
  let bestLen = 0;
@@ -5686,7 +5698,7 @@ var IonoAccumulator = class {
5686
5698
  }
5687
5699
  if (!bestKey) continue;
5688
5700
  const points = satArcs.get(bestKey);
5689
- points.sort((a, b) => a.time - b.time);
5701
+ const arcStarts = this.closedArcStarts.get(prn)?.get(bestKey) ?? [0];
5690
5702
  const sys = prn[0];
5691
5703
  for (const p of points) {
5692
5704
  sum += p.stec;
@@ -5701,21 +5713,107 @@ var IonoAccumulator = class {
5701
5713
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
5702
5714
  codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
5703
5715
  tecuPerNs,
5704
- points
5716
+ points,
5717
+ arcStarts
5705
5718
  });
5706
5719
  }
5707
5720
  series.sort((a, b) => a.prn.localeCompare(b.prn));
5708
- return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
5721
+ return {
5722
+ series,
5723
+ maxStec: count > 0 ? maxStec : 0,
5724
+ meanStec: count > 0 ? sum / count : 0
5725
+ };
5709
5726
  }
5710
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
+ }
5711
5809
 
5712
5810
  // src/analysis/dcb.ts
5713
5811
  var PRN_RE = /^[A-Z]\d{2}$/;
5714
5812
  var SVN_RE = /^[A-Z]\d{3}$/;
5715
5813
  var OBS_RE = /^[CL]\d[A-Z]$/;
5716
- function sinexEpochMs(s) {
5814
+ function sinexEpochMs(s, openValue) {
5717
5815
  const [y, d, sec] = s.split(":").map(Number);
5718
- if (!y) return Infinity;
5816
+ if (!y) return openValue;
5719
5817
  return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
5720
5818
  }
5721
5819
  function parseSinexBiasDcb(text, epochMs) {
@@ -5730,8 +5828,8 @@ function parseSinexBiasDcb(text, epochMs) {
5730
5828
  if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
5731
5829
  const value = parseFloat(t[unitIdx + 1]);
5732
5830
  if (!isFinite(value)) continue;
5733
- const start = sinexEpochMs(t[5] ?? "");
5734
- const end = sinexEpochMs(t[6] ?? "");
5831
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
5832
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
5735
5833
  const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
5736
5834
  const prn = t[2];
5737
5835
  const pair = `${t[3]}-${t[4]}`;
@@ -5753,10 +5851,6 @@ function parseSinexBiasDcb(text, epochMs) {
5753
5851
  }
5754
5852
  return out;
5755
5853
  }
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
5854
  function applyIonoDcb(iono, satDcb) {
5761
5855
  const missing = [];
5762
5856
  let corrected = 0;
@@ -5820,7 +5914,7 @@ function applyIonoDcb(iono, satDcb) {
5820
5914
  out.sort((a, b) => a.prn.localeCompare(b.prn));
5821
5915
  let sum = 0;
5822
5916
  let count = 0;
5823
- let maxStec = 0;
5917
+ let maxStec = -Infinity;
5824
5918
  for (const s of out) {
5825
5919
  for (const p of s.points) {
5826
5920
  sum += p.stec;
@@ -5829,7 +5923,11 @@ function applyIonoDcb(iono, satDcb) {
5829
5923
  }
5830
5924
  }
5831
5925
  return {
5832
- result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
5926
+ result: {
5927
+ series: out,
5928
+ maxStec: count > 0 ? maxStec : 0,
5929
+ meanStec: count > 0 ? sum / count : 0
5930
+ },
5833
5931
  satellitesCorrected: corrected,
5834
5932
  satellitesMissing: [...new Set(missing)].sort(),
5835
5933
  receiverDcbTecu
@@ -7148,6 +7246,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7148
7246
  clampUnit,
7149
7247
  computeAllPositions,
7150
7248
  computeDop,
7249
+ computeIonoRate,
7151
7250
  computeLiveSkyPositions,
7152
7251
  computePsdDb,
7153
7252
  computeSatPosition,
@@ -7163,6 +7262,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7163
7262
  decodeMsmFull,
7164
7263
  deg2dms,
7165
7264
  deg2rad,
7265
+ detrendIonoArcs,
7166
7266
  ecefToAzEl,
7167
7267
  ecefToGeodetic,
7168
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-EJQ6NG5U.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-PX7TPPTQ.js";
105
+ } from "./chunk-SOJNWF5T.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-PQYBTE42.js";
118
+ } from "./chunk-CD5YSYCG.js";
117
119
  import {
118
120
  getBdsTime,
119
121
  getDateFromBdsTime,
@@ -199,7 +201,7 @@ import {
199
201
  setRtcm3DebugHandler,
200
202
  updateStationMeta,
201
203
  updateStreamStats
202
- } from "./chunk-65CQINSB.js";
204
+ } from "./chunk-FGRC3PB3.js";
203
205
  import {
204
206
  DAYS_MJD2000_MJD,
205
207
  DAYS_MJD_JULIAN,
@@ -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,