gnss-js 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,12 +509,6 @@ 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();
@@ -516,13 +516,15 @@ var IonoAccumulator = class {
516
516
  obsIndices;
517
517
  gloChannels;
518
518
  pairLabel = /* @__PURE__ */ new Map();
519
+ pairCodes = /* @__PURE__ */ new Map();
520
+ pairMeta = /* @__PURE__ */ new Map();
519
521
  constructor(header) {
520
522
  this.interval = header.interval ?? 30;
521
523
  this.obsIndices = buildObsIndices(header);
522
524
  this.gloChannels = buildGloChannelMap(header.glonassSlots);
523
525
  }
524
526
  /** Observation callback — wire this into parseRinexStream. */
525
- onObservation = (time, prn, _codes, values) => {
527
+ onObservation = (time, prn, codes, values) => {
526
528
  const sys = prn[0];
527
529
  const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
528
530
  if (!bandMap) return;
@@ -533,7 +535,12 @@ var IonoAccumulator = class {
533
535
  const lVal = values[L];
534
536
  const freq = getFreq(this.gloChannels, prn, band);
535
537
  if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
536
- bandData.set(band, { C: cVal, L: lVal, f: freq });
538
+ bandData.set(band, {
539
+ C: cVal,
540
+ L: lVal,
541
+ f: freq,
542
+ code: codes[C] ?? `C${band}`
543
+ });
537
544
  }
538
545
  }
539
546
  if (bandData.size < 2) return;
@@ -552,7 +559,9 @@ var IonoAccumulator = class {
552
559
  const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
553
560
  const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
554
561
  this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
562
+ this.pairCodes.set(`${sys}:${pairKey}`, [di.code, dj.code]);
555
563
  }
564
+ this.pairMeta.set(`${prn}:${pairKey}`, { fi: di.f, gamma });
556
565
  this.push(prn, pairKey, time, l4, p4, di.f, gamma);
557
566
  break;
558
567
  }
@@ -602,8 +611,8 @@ var IonoAccumulator = class {
602
611
  }
603
612
  closeArc(prn, pairKey, ps) {
604
613
  const arc = ps.arc;
605
- if (arc.times.length >= MIN_ARC_LENGTH2) {
606
- const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
614
+ if (arc.times.length >= MIN_ARC_LENGTH) {
615
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
607
616
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
608
617
  let satArcs = this.closed.get(prn);
609
618
  if (!satArcs) {
@@ -634,7 +643,7 @@ var IonoAccumulator = class {
634
643
  const series = [];
635
644
  let sum = 0;
636
645
  let count = 0;
637
- let maxStec = 0;
646
+ let maxStec = -Infinity;
638
647
  for (const [prn, satArcs] of this.closed) {
639
648
  let bestKey = null;
640
649
  let bestLen = 0;
@@ -653,18 +662,153 @@ var IonoAccumulator = class {
653
662
  count++;
654
663
  if (p.stec > maxStec) maxStec = p.stec;
655
664
  }
665
+ const meta = this.pairMeta.get(`${prn}:${bestKey}`);
666
+ const tecuPerNs = meta ? C_LIGHT * 1e-9 / (meta.gamma - 1) * (meta.fi * meta.fi / TEC_FACTOR) : 0;
656
667
  series.push({
657
668
  prn,
658
669
  system: sys,
659
670
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
671
+ codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
672
+ tecuPerNs,
660
673
  points
661
674
  });
662
675
  }
663
676
  series.sort((a, b) => a.prn.localeCompare(b.prn));
664
- return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
677
+ return {
678
+ series,
679
+ maxStec: count > 0 ? maxStec : 0,
680
+ meanStec: count > 0 ? sum / count : 0
681
+ };
665
682
  }
666
683
  };
667
684
 
685
+ // src/analysis/dcb.ts
686
+ var PRN_RE = /^[A-Z]\d{2}$/;
687
+ var SVN_RE = /^[A-Z]\d{3}$/;
688
+ var OBS_RE = /^[CL]\d[A-Z]$/;
689
+ function sinexEpochMs(s, openValue) {
690
+ const [y, d, sec] = s.split(":").map(Number);
691
+ if (!y) return openValue;
692
+ return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
693
+ }
694
+ function parseSinexBiasDcb(text, epochMs) {
695
+ const rows = /* @__PURE__ */ new Map();
696
+ for (const line of text.split("\n")) {
697
+ if (!line.startsWith(" DSB") && !line.startsWith("DSB")) continue;
698
+ const t = line.trim().split(/\s+/);
699
+ if (t.length < 9 || !SVN_RE.test(t[1]) || !PRN_RE.test(t[2]) || !OBS_RE.test(t[3]) || !OBS_RE.test(t[4])) {
700
+ continue;
701
+ }
702
+ const unitIdx = t.indexOf("ns");
703
+ if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
704
+ const value = parseFloat(t[unitIdx + 1]);
705
+ if (!isFinite(value)) continue;
706
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
707
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
708
+ const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
709
+ const prn = t[2];
710
+ const pair = `${t[3]}-${t[4]}`;
711
+ let sat = rows.get(prn);
712
+ if (!sat) {
713
+ sat = /* @__PURE__ */ new Map();
714
+ rows.set(prn, sat);
715
+ }
716
+ const prev = sat.get(pair);
717
+ if (!prev || covers && !prev.covers || covers === prev.covers && end > prev.end) {
718
+ sat.set(pair, { value, end, covers });
719
+ }
720
+ }
721
+ const out = /* @__PURE__ */ new Map();
722
+ for (const [prn, sat] of rows) {
723
+ const m = /* @__PURE__ */ new Map();
724
+ for (const [pair, row] of sat) m.set(pair, row.value);
725
+ out.set(prn, m);
726
+ }
727
+ return out;
728
+ }
729
+ function applyIonoDcb(iono, satDcb) {
730
+ const missing = [];
731
+ let corrected = 0;
732
+ const dcbFor = (s) => {
733
+ const sat = satDcb.get(s.prn);
734
+ if (!sat) return null;
735
+ const fwd = sat.get(`${s.codes[0]}-${s.codes[1]}`);
736
+ if (fwd !== void 0) return fwd;
737
+ const rev = sat.get(`${s.codes[1]}-${s.codes[0]}`);
738
+ if (rev !== void 0) return -rev;
739
+ return null;
740
+ };
741
+ const series = iono.series.map((s) => {
742
+ const dcbNs = dcbFor(s);
743
+ if (dcbNs === null) {
744
+ missing.push(s.prn);
745
+ return s;
746
+ }
747
+ corrected++;
748
+ const shift = dcbNs * s.tecuPerNs;
749
+ return {
750
+ ...s,
751
+ points: s.points.map((p) => ({ time: p.time, stec: p.stec + shift }))
752
+ };
753
+ });
754
+ const missingSet = new Set(missing);
755
+ const receiverDcbTecu = {};
756
+ const groups = /* @__PURE__ */ new Map();
757
+ for (const s of series) {
758
+ const key = `${s.system} ${s.label}`;
759
+ let g = groups.get(key);
760
+ if (!g) {
761
+ g = [];
762
+ groups.set(key, g);
763
+ }
764
+ g.push(s);
765
+ }
766
+ const out = [];
767
+ for (const [key, group] of groups) {
768
+ const samples = [];
769
+ for (const s of group) {
770
+ if (missingSet.has(s.prn)) continue;
771
+ for (const p of s.points) samples.push(p.stec);
772
+ }
773
+ if (samples.length >= 100) {
774
+ const rx = percentile(samples, 0.01);
775
+ receiverDcbTecu[key] = rx;
776
+ for (const s of group) {
777
+ out.push({
778
+ ...s,
779
+ points: s.points.map((p) => ({
780
+ time: p.time,
781
+ stec: p.stec - rx
782
+ }))
783
+ });
784
+ }
785
+ } else {
786
+ out.push(...group);
787
+ }
788
+ }
789
+ out.sort((a, b) => a.prn.localeCompare(b.prn));
790
+ let sum = 0;
791
+ let count = 0;
792
+ let maxStec = -Infinity;
793
+ for (const s of out) {
794
+ for (const p of s.points) {
795
+ sum += p.stec;
796
+ count++;
797
+ if (p.stec > maxStec) maxStec = p.stec;
798
+ }
799
+ }
800
+ return {
801
+ result: {
802
+ series: out,
803
+ maxStec: count > 0 ? maxStec : 0,
804
+ meanStec: count > 0 ? sum / count : 0
805
+ },
806
+ satellitesCorrected: corrected,
807
+ satellitesMissing: [...new Set(missing)].sort(),
808
+ receiverDcbTecu
809
+ };
810
+ }
811
+
668
812
  // src/analysis/quality-analysis.ts
669
813
  async function analyzeQuality(file, header, onProgress, signal) {
670
814
  const mpAccum = new MultipathAccumulator(header);
@@ -700,5 +844,7 @@ export {
700
844
  CycleSlipAccumulator,
701
845
  CompletenessAccumulator,
702
846
  IonoAccumulator,
847
+ parseSinexBiasDcb,
848
+ applyIonoDcb,
703
849
  analyzeQuality
704
850
  };
@@ -2,7 +2,7 @@ import {
2
2
  computeDop,
3
3
  computeSatPosition,
4
4
  ecefToAzEl
5
- } from "./chunk-PQYBTE42.js";
5
+ } from "./chunk-CD5YSYCG.js";
6
6
  import {
7
7
  C_LIGHT,
8
8
  OMEGA_E
package/dist/index.cjs CHANGED
@@ -105,6 +105,7 @@ __export(src_exports, {
105
105
  WarningAccumulator: () => WarningAccumulator,
106
106
  analyzeQuality: () => analyzeQuality,
107
107
  applyHelmert: () => applyHelmert,
108
+ applyIonoDcb: () => applyIonoDcb,
108
109
  buildGloChannelMap: () => buildGloChannelMap,
109
110
  buildObsIndices: () => buildObsIndices,
110
111
  clampUnit: () => clampUnit,
@@ -207,6 +208,7 @@ __export(src_exports, {
207
208
  parseNavFile: () => parseNavFile,
208
209
  parseNmeaFile: () => parseNmeaFile,
209
210
  parseRinexStream: () => parseRinexStream,
211
+ parseSinexBiasDcb: () => parseSinexBiasDcb,
210
212
  parseSourcetable: () => parseSourcetable,
211
213
  phiAltBOC: () => phiAltBOC,
212
214
  phiBOCc: () => phiBOCc,
@@ -3904,6 +3906,11 @@ function rtcm3Constellation(msgType) {
3904
3906
  if (msgType >= 1111 && msgType <= 1117) return "QZSS";
3905
3907
  if (msgType >= 1121 && msgType <= 1127) return "BeiDou";
3906
3908
  if (msgType >= 1131 && msgType <= 1137) return "NavIC";
3909
+ if (msgType >= 1015 && msgType <= 1017) return "GPS";
3910
+ if (msgType === 1030) return "GPS";
3911
+ if (msgType === 1031) return "GLONASS";
3912
+ if (msgType >= 1057 && msgType <= 1062) return "GPS";
3913
+ if (msgType >= 1063 && msgType <= 1068) return "GLONASS";
3907
3914
  if (msgType === 1230) return "GLONASS";
3908
3915
  return null;
3909
3916
  }
@@ -3943,7 +3950,6 @@ function updateStreamStats(stats, frames, rawBytes) {
3943
3950
  entry.totalBytes += frame.length + 6;
3944
3951
  const msmEpoch = decodeMsmFull(frame);
3945
3952
  if (msmEpoch) {
3946
- const now2 = Date.now();
3947
3953
  for (const obs of msmEpoch.observations) {
3948
3954
  const signals = [];
3949
3955
  let bestCn0 = 0;
@@ -3958,7 +3964,7 @@ function updateStreamStats(stats, frames, rawBytes) {
3958
3964
  prn: obs.prn,
3959
3965
  system: obs.system,
3960
3966
  cn0: bestCn0,
3961
- lastSeen: now2,
3967
+ lastSeen: now,
3962
3968
  signals
3963
3969
  });
3964
3970
  }
@@ -4201,20 +4207,10 @@ function invert4x4(m) {
4201
4207
  return result;
4202
4208
  }
4203
4209
  function selectEphemeris(ephemerides, prn, timeMs) {
4204
- let best = null;
4205
- let bestDt = Infinity;
4206
- for (const eph of ephemerides) {
4207
- if (eph.prn !== prn) continue;
4208
- const ephTime = eph.tocDate.getTime();
4209
- const dt = Math.abs(timeMs - ephTime);
4210
- const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
4211
- if (dt > maxAge) continue;
4212
- if (dt < bestDt) {
4213
- bestDt = dt;
4214
- best = eph;
4215
- }
4216
- }
4217
- return best;
4210
+ return selectBest(
4211
+ ephemerides.filter((e) => e.prn === prn),
4212
+ timeMs
4213
+ );
4218
4214
  }
4219
4215
  function computeSatPosition(eph, timeMs) {
4220
4216
  if (eph.system === "R" || eph.system === "S") {
@@ -4307,8 +4303,9 @@ function selectBest(ephs, timeMs) {
4307
4303
  return best;
4308
4304
  }
4309
4305
  var GPS_EPOCH_MS = START_GPS_TIME.getTime();
4306
+ var MS_PER_WEEK2 = 7 * 864e5;
4310
4307
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
4311
- var GAL_EPOCH_MS = GPS_EPOCH_MS;
4308
+ var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK2;
4312
4309
  function ephInfoToEphemeris(info) {
4313
4310
  const sys = info.prn.charAt(0);
4314
4311
  if (sys === "R") {
@@ -4352,6 +4349,7 @@ function ephInfoToEphemeris(info) {
4352
4349
  return null;
4353
4350
  }
4354
4351
  let epochMs;
4352
+ let week = info.week;
4355
4353
  const tocSec = info.toc ?? info.toe;
4356
4354
  if (sys === "C") {
4357
4355
  epochMs = BDS_EPOCH_MS;
@@ -4359,8 +4357,10 @@ function ephInfoToEphemeris(info) {
4359
4357
  epochMs = GAL_EPOCH_MS;
4360
4358
  } else {
4361
4359
  epochMs = GPS_EPOCH_MS;
4360
+ const weeksNow = (info.lastReceived - GPS_EPOCH_MS) / MS_PER_WEEK2;
4361
+ week += 1024 * Math.round((weeksNow - week) / 1024);
4362
4362
  }
4363
- const tocDate = new Date(epochMs + info.week * 7 * 864e5 + tocSec * 1e3);
4363
+ const tocDate = new Date(epochMs + week * MS_PER_WEEK2 + tocSec * 1e3);
4364
4364
  return {
4365
4365
  system: sys,
4366
4366
  prn: info.prn,
@@ -4386,7 +4386,7 @@ function ephInfoToEphemeris(info) {
4386
4386
  omega: info.argPerigee,
4387
4387
  omegaDot: info.omegaDot ?? 0,
4388
4388
  idot: info.idot ?? 0,
4389
- week: info.week,
4389
+ week,
4390
4390
  svHealth: info.health,
4391
4391
  tgd: 0
4392
4392
  };
@@ -5042,16 +5042,22 @@ async function connectToMountpoint(proxyUrl, info, signal) {
5042
5042
  };
5043
5043
  }
5044
5044
 
5045
- // src/analysis/multipath.ts
5045
+ // src/analysis/stats-util.ts
5046
5046
  var MIN_ARC_LENGTH = 10;
5047
- var MP_JUMP_M = 1.25;
5048
- var MP_JUMP_WINDOW = 5;
5049
- var MP_EDIT_SIGMA = 3;
5050
5047
  function median(values) {
5051
5048
  const s = [...values].sort((a, b) => a - b);
5052
5049
  const m = s.length >> 1;
5053
5050
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
5054
5051
  }
5052
+ function percentile(values, p) {
5053
+ const s = [...values].sort((a, b) => a - b);
5054
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
5055
+ }
5056
+
5057
+ // src/analysis/multipath.ts
5058
+ var MP_JUMP_M = 1.25;
5059
+ var MP_JUMP_WINDOW = 5;
5060
+ var MP_EDIT_SIGMA = 3;
5055
5061
  var MultipathAccumulator = class {
5056
5062
  state = /* @__PURE__ */ new Map();
5057
5063
  results = [];
@@ -5532,12 +5538,6 @@ var CompletenessAccumulator = class {
5532
5538
  // src/analysis/ionosphere.ts
5533
5539
  var TEC_FACTOR = 403e15;
5534
5540
  var GF_JUMP_M = 0.15;
5535
- var MIN_ARC_LENGTH2 = 10;
5536
- function median2(values) {
5537
- const s = [...values].sort((a, b) => a - b);
5538
- const m = s.length >> 1;
5539
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
5540
- }
5541
5541
  var IonoAccumulator = class {
5542
5542
  state = /* @__PURE__ */ new Map();
5543
5543
  closed = /* @__PURE__ */ new Map();
@@ -5545,13 +5545,15 @@ var IonoAccumulator = class {
5545
5545
  obsIndices;
5546
5546
  gloChannels;
5547
5547
  pairLabel = /* @__PURE__ */ new Map();
5548
+ pairCodes = /* @__PURE__ */ new Map();
5549
+ pairMeta = /* @__PURE__ */ new Map();
5548
5550
  constructor(header) {
5549
5551
  this.interval = header.interval ?? 30;
5550
5552
  this.obsIndices = buildObsIndices(header);
5551
5553
  this.gloChannels = buildGloChannelMap(header.glonassSlots);
5552
5554
  }
5553
5555
  /** Observation callback — wire this into parseRinexStream. */
5554
- onObservation = (time, prn, _codes, values) => {
5556
+ onObservation = (time, prn, codes, values) => {
5555
5557
  const sys = prn[0];
5556
5558
  const bandMap = this.obsIndices.get(sys) ?? this.obsIndices.get("_v2");
5557
5559
  if (!bandMap) return;
@@ -5562,7 +5564,12 @@ var IonoAccumulator = class {
5562
5564
  const lVal = values[L];
5563
5565
  const freq = getFreq(this.gloChannels, prn, band);
5564
5566
  if (cVal != null && cVal !== 0 && lVal != null && lVal !== 0 && freq) {
5565
- bandData.set(band, { C: cVal, L: lVal, f: freq });
5567
+ bandData.set(band, {
5568
+ C: cVal,
5569
+ L: lVal,
5570
+ f: freq,
5571
+ code: codes[C] ?? `C${band}`
5572
+ });
5566
5573
  }
5567
5574
  }
5568
5575
  if (bandData.size < 2) return;
@@ -5581,7 +5588,9 @@ var IonoAccumulator = class {
5581
5588
  const bLabel = BAND_LABELS[sys]?.[bi] ?? bi;
5582
5589
  const rLabel = BAND_LABELS[sys]?.[bj] ?? bj;
5583
5590
  this.pairLabel.set(`${sys}:${pairKey}`, `${bLabel}-${rLabel}`);
5591
+ this.pairCodes.set(`${sys}:${pairKey}`, [di.code, dj.code]);
5584
5592
  }
5593
+ this.pairMeta.set(`${prn}:${pairKey}`, { fi: di.f, gamma });
5585
5594
  this.push(prn, pairKey, time, l4, p4, di.f, gamma);
5586
5595
  break;
5587
5596
  }
@@ -5631,8 +5640,8 @@ var IonoAccumulator = class {
5631
5640
  }
5632
5641
  closeArc(prn, pairKey, ps) {
5633
5642
  const arc = ps.arc;
5634
- if (arc.times.length >= MIN_ARC_LENGTH2) {
5635
- const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
5643
+ if (arc.times.length >= MIN_ARC_LENGTH) {
5644
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
5636
5645
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
5637
5646
  let satArcs = this.closed.get(prn);
5638
5647
  if (!satArcs) {
@@ -5663,7 +5672,7 @@ var IonoAccumulator = class {
5663
5672
  const series = [];
5664
5673
  let sum = 0;
5665
5674
  let count = 0;
5666
- let maxStec = 0;
5675
+ let maxStec = -Infinity;
5667
5676
  for (const [prn, satArcs] of this.closed) {
5668
5677
  let bestKey = null;
5669
5678
  let bestLen = 0;
@@ -5682,18 +5691,153 @@ var IonoAccumulator = class {
5682
5691
  count++;
5683
5692
  if (p.stec > maxStec) maxStec = p.stec;
5684
5693
  }
5694
+ const meta = this.pairMeta.get(`${prn}:${bestKey}`);
5695
+ const tecuPerNs = meta ? C_LIGHT * 1e-9 / (meta.gamma - 1) * (meta.fi * meta.fi / TEC_FACTOR) : 0;
5685
5696
  series.push({
5686
5697
  prn,
5687
5698
  system: sys,
5688
5699
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
5700
+ codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
5701
+ tecuPerNs,
5689
5702
  points
5690
5703
  });
5691
5704
  }
5692
5705
  series.sort((a, b) => a.prn.localeCompare(b.prn));
5693
- return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
5706
+ return {
5707
+ series,
5708
+ maxStec: count > 0 ? maxStec : 0,
5709
+ meanStec: count > 0 ? sum / count : 0
5710
+ };
5694
5711
  }
5695
5712
  };
5696
5713
 
5714
+ // src/analysis/dcb.ts
5715
+ var PRN_RE = /^[A-Z]\d{2}$/;
5716
+ var SVN_RE = /^[A-Z]\d{3}$/;
5717
+ var OBS_RE = /^[CL]\d[A-Z]$/;
5718
+ function sinexEpochMs(s, openValue) {
5719
+ const [y, d, sec] = s.split(":").map(Number);
5720
+ if (!y) return openValue;
5721
+ return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
5722
+ }
5723
+ function parseSinexBiasDcb(text, epochMs) {
5724
+ const rows = /* @__PURE__ */ new Map();
5725
+ for (const line of text.split("\n")) {
5726
+ if (!line.startsWith(" DSB") && !line.startsWith("DSB")) continue;
5727
+ const t = line.trim().split(/\s+/);
5728
+ if (t.length < 9 || !SVN_RE.test(t[1]) || !PRN_RE.test(t[2]) || !OBS_RE.test(t[3]) || !OBS_RE.test(t[4])) {
5729
+ continue;
5730
+ }
5731
+ const unitIdx = t.indexOf("ns");
5732
+ if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
5733
+ const value = parseFloat(t[unitIdx + 1]);
5734
+ if (!isFinite(value)) continue;
5735
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
5736
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
5737
+ const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
5738
+ const prn = t[2];
5739
+ const pair = `${t[3]}-${t[4]}`;
5740
+ let sat = rows.get(prn);
5741
+ if (!sat) {
5742
+ sat = /* @__PURE__ */ new Map();
5743
+ rows.set(prn, sat);
5744
+ }
5745
+ const prev = sat.get(pair);
5746
+ if (!prev || covers && !prev.covers || covers === prev.covers && end > prev.end) {
5747
+ sat.set(pair, { value, end, covers });
5748
+ }
5749
+ }
5750
+ const out = /* @__PURE__ */ new Map();
5751
+ for (const [prn, sat] of rows) {
5752
+ const m = /* @__PURE__ */ new Map();
5753
+ for (const [pair, row] of sat) m.set(pair, row.value);
5754
+ out.set(prn, m);
5755
+ }
5756
+ return out;
5757
+ }
5758
+ function applyIonoDcb(iono, satDcb) {
5759
+ const missing = [];
5760
+ let corrected = 0;
5761
+ const dcbFor = (s) => {
5762
+ const sat = satDcb.get(s.prn);
5763
+ if (!sat) return null;
5764
+ const fwd = sat.get(`${s.codes[0]}-${s.codes[1]}`);
5765
+ if (fwd !== void 0) return fwd;
5766
+ const rev = sat.get(`${s.codes[1]}-${s.codes[0]}`);
5767
+ if (rev !== void 0) return -rev;
5768
+ return null;
5769
+ };
5770
+ const series = iono.series.map((s) => {
5771
+ const dcbNs = dcbFor(s);
5772
+ if (dcbNs === null) {
5773
+ missing.push(s.prn);
5774
+ return s;
5775
+ }
5776
+ corrected++;
5777
+ const shift = dcbNs * s.tecuPerNs;
5778
+ return {
5779
+ ...s,
5780
+ points: s.points.map((p) => ({ time: p.time, stec: p.stec + shift }))
5781
+ };
5782
+ });
5783
+ const missingSet = new Set(missing);
5784
+ const receiverDcbTecu = {};
5785
+ const groups = /* @__PURE__ */ new Map();
5786
+ for (const s of series) {
5787
+ const key = `${s.system} ${s.label}`;
5788
+ let g = groups.get(key);
5789
+ if (!g) {
5790
+ g = [];
5791
+ groups.set(key, g);
5792
+ }
5793
+ g.push(s);
5794
+ }
5795
+ const out = [];
5796
+ for (const [key, group] of groups) {
5797
+ const samples = [];
5798
+ for (const s of group) {
5799
+ if (missingSet.has(s.prn)) continue;
5800
+ for (const p of s.points) samples.push(p.stec);
5801
+ }
5802
+ if (samples.length >= 100) {
5803
+ const rx = percentile(samples, 0.01);
5804
+ receiverDcbTecu[key] = rx;
5805
+ for (const s of group) {
5806
+ out.push({
5807
+ ...s,
5808
+ points: s.points.map((p) => ({
5809
+ time: p.time,
5810
+ stec: p.stec - rx
5811
+ }))
5812
+ });
5813
+ }
5814
+ } else {
5815
+ out.push(...group);
5816
+ }
5817
+ }
5818
+ out.sort((a, b) => a.prn.localeCompare(b.prn));
5819
+ let sum = 0;
5820
+ let count = 0;
5821
+ let maxStec = -Infinity;
5822
+ for (const s of out) {
5823
+ for (const p of s.points) {
5824
+ sum += p.stec;
5825
+ count++;
5826
+ if (p.stec > maxStec) maxStec = p.stec;
5827
+ }
5828
+ }
5829
+ return {
5830
+ result: {
5831
+ series: out,
5832
+ maxStec: count > 0 ? maxStec : 0,
5833
+ meanStec: count > 0 ? sum / count : 0
5834
+ },
5835
+ satellitesCorrected: corrected,
5836
+ satellitesMissing: [...new Set(missing)].sort(),
5837
+ receiverDcbTecu
5838
+ };
5839
+ }
5840
+
5697
5841
  // src/analysis/quality-analysis.ts
5698
5842
  async function analyzeQuality(file, header, onProgress, signal) {
5699
5843
  const mpAccum = new MultipathAccumulator(header);
@@ -7000,6 +7144,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7000
7144
  WarningAccumulator,
7001
7145
  analyzeQuality,
7002
7146
  applyHelmert,
7147
+ applyIonoDcb,
7003
7148
  buildGloChannelMap,
7004
7149
  buildObsIndices,
7005
7150
  clampUnit,
@@ -7102,6 +7247,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7102
7247
  parseNavFile,
7103
7248
  parseNmeaFile,
7104
7249
  parseRinexStream,
7250
+ parseSinexBiasDcb,
7105
7251
  parseSourcetable,
7106
7252
  phiAltBOC,
7107
7253
  phiBOCc,
package/dist/index.d.cts CHANGED
@@ -13,7 +13,7 @@ export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPositi
13
13
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.cjs';
14
14
  export { SppOptions, SppSolution, ionoFree, satClockCorrection, solveSpp } from './positioning.cjs';
15
15
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.cjs';
16
- export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, analyzeQuality } from './analysis.cjs';
16
+ export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb } from './analysis.cjs';
17
17
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.cjs';
18
18
  export { NmeaFix, NmeaStats, NmeaTrack, computeStats, nmeaCoordToDecimal, parseNmeaFile, verifyChecksum } from './nmea.cjs';
19
19
  export { BAND_PRESETS, BandPreset, CONSTELLATIONS, ConstellationRow, DELTA_GLO_L1, DELTA_GLO_L2, FREQ_BDS_B1A, FREQ_BDS_B1C, FREQ_BDS_B1I, FREQ_BDS_B2I, FREQ_BDS_B3A, FREQ_BDS_B3I, FREQ_GAL_E1, FREQ_GAL_E5, FREQ_GAL_E5a, FREQ_GAL_E5b, FREQ_GAL_E6, FREQ_GLO_L1, FREQ_GLO_L1OC, FREQ_GLO_L2, FREQ_GLO_L2OC, FREQ_GLO_L3OC, FREQ_GPS_L1, FREQ_GPS_L2, FREQ_GPS_L5, FREQ_NAVIC_L5, FREQ_QZS_L1, FREQ_QZS_L2, FREQ_QZS_L5, FREQ_QZS_L6, Modulation, SignalDef, computePsdDb, phiAltBOC, phiBOCc, phiBOCs, phiBPSK } from './signals.cjs';
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPositi
13
13
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.js';
14
14
  export { SppOptions, SppSolution, ionoFree, satClockCorrection, solveSpp } from './positioning.js';
15
15
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.js';
16
- export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, analyzeQuality } from './analysis.js';
16
+ export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb } from './analysis.js';
17
17
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.js';
18
18
  export { NmeaFix, NmeaStats, NmeaTrack, computeStats, nmeaCoordToDecimal, parseNmeaFile, verifyChecksum } from './nmea.js';
19
19
  export { BAND_PRESETS, BandPreset, CONSTELLATIONS, ConstellationRow, DELTA_GLO_L1, DELTA_GLO_L2, FREQ_BDS_B1A, FREQ_BDS_B1C, FREQ_BDS_B1I, FREQ_BDS_B2I, FREQ_BDS_B3A, FREQ_BDS_B3I, FREQ_GAL_E1, FREQ_GAL_E5, FREQ_GAL_E5a, FREQ_GAL_E5b, FREQ_GAL_E6, FREQ_GLO_L1, FREQ_GLO_L1OC, FREQ_GLO_L2, FREQ_GLO_L2OC, FREQ_GLO_L3OC, FREQ_GPS_L1, FREQ_GPS_L2, FREQ_GPS_L5, FREQ_NAVIC_L5, FREQ_QZS_L1, FREQ_QZS_L2, FREQ_QZS_L5, FREQ_QZS_L6, Modulation, SignalDef, computePsdDb, phiAltBOC, phiBOCc, phiBOCs, phiBPSK } from './signals.js';