gnss-js 1.5.0 → 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.
package/README.md CHANGED
@@ -94,12 +94,16 @@ const itrf = transformFrame(
94
94
  ```ts
95
95
  import { parseRinexStream, parseNavFile } from 'gnss-js/rinex';
96
96
 
97
- // Streaming observation parser (works with ReadableStream)
98
- const result = await parseRinexStream(stream, {
99
- onObservation: (time, prn, codes, values) => {
100
- // process each epoch
101
- },
102
- });
97
+ // Streaming observation parser (positional arguments)
98
+ const result = await parseRinexStream(
99
+ file, // File (plain, Hatanaka .crx, or .gz)
100
+ (percent) => {}, // optional progress callback
101
+ undefined, // optional AbortSignal
102
+ (time, prn, codes, values) => {
103
+ // per-satellite observation callback
104
+ }
105
+ );
106
+ // result.header: RinexHeader
103
107
 
104
108
  // Navigation file parser
105
109
  const nav = parseNavFile(navFileText);
@@ -125,28 +129,36 @@ for (const frame of decoder) {
125
129
  import { computeAllPositions, computeDop } from 'gnss-js/orbit';
126
130
 
127
131
  const positions = computeAllPositions(ephemerides, times, receiverPosition);
128
- // positions.prns: string[], positions.times: number[], positions.az/el/lat/lon: Float64Array
132
+ // positions.prns: string[], positions.times: number[]
133
+ // positions.positions[prn][epochIdx]: { lat, lon, az, el } | null
129
134
  ```
130
135
 
131
136
  ### Signal analysis
132
137
 
133
138
  ```ts
134
- import {
135
- MultipathAccumulator,
136
- CycleSlipAccumulator,
137
- CompletenessAccumulator,
138
- } from 'gnss-js/analysis';
139
-
140
- const mp = new MultipathAccumulator(header);
141
- const cs = new CycleSlipAccumulator(header);
142
- await parseRinexStream(stream, {
143
- onObservation: (time, prn, codes, values) => {
144
- mp.onObservation(time, prn, codes, values);
145
- cs.onObservation(time, prn, codes, values);
146
- },
147
- });
148
- const multipathResult = mp.finalize();
149
- const cycleSlipResult = cs.finalize();
139
+ import { analyzeQuality } from 'gnss-js/analysis';
140
+
141
+ // One re-parse pass: multipath (per code band, Anubis-style), cycle
142
+ // slips, completeness, and slant-TEC ionosphere series.
143
+ const q = await analyzeQuality(file, header);
144
+ // q.multipath, q.cycleSlips, q.completeness, q.iono
145
+
146
+ // The accumulators (MultipathAccumulator, CycleSlipAccumulator,
147
+ // CompletenessAccumulator, IonoAccumulator) are also exported for
148
+ // wiring into a custom parseRinexStream pass.
149
+ ```
150
+
151
+ ### Ionosphere and DCBs
152
+
153
+ ```ts
154
+ import { parseSinexBiasDcb, applyIonoDcb } from 'gnss-js/analysis';
155
+
156
+ // q.iono: slant TEC per satellite from the geometry-free phase
157
+ // levelled to the code — DCB-biased until calibrated:
158
+ const satDcb = parseSinexBiasDcb(sinexBiasText, obsEpochMs); // ESA .BIA / CAS .BSX
159
+ const { result, receiverDcbTecu } = applyIonoDcb(q.iono, satDcb);
160
+ // result.series[n].points: calibrated STEC (TECU); receiver bias
161
+ // estimated from the night-time floor per system/signal pair.
150
162
  ```
151
163
 
152
164
  ### NTRIP client
@@ -173,6 +185,17 @@ const { reader, abort } = await connectToMountpoint(
173
185
  );
174
186
  ```
175
187
 
188
+ ### NMEA and ANTEX
189
+
190
+ ```ts
191
+ import { parseNmeaFile, computeStats } from 'gnss-js/nmea';
192
+ import { parseAntex } from 'gnss-js/antex';
193
+
194
+ const track = parseNmeaFile(nmeaText); // fixes, satellites, per-sentence records
195
+ const stats = computeStats(track.fixes); // position/HDOP/speed statistics
196
+ const antex = parseAntex(antexText); // antenna PCO/PCV calibrations
197
+ ```
198
+
176
199
  ### Constants
177
200
 
178
201
  ```ts
package/dist/analysis.cjs CHANGED
@@ -30,6 +30,18 @@ __export(analysis_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(analysis_exports);
32
32
 
33
+ // src/analysis/stats-util.ts
34
+ var MIN_ARC_LENGTH = 10;
35
+ function median(values) {
36
+ const s = [...values].sort((a, b) => a - b);
37
+ const m = s.length >> 1;
38
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
39
+ }
40
+ function percentile(values, p) {
41
+ const s = [...values].sort((a, b) => a - b);
42
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
43
+ }
44
+
33
45
  // src/constants/gnss.ts
34
46
  var SYSTEM_NAMES = {
35
47
  G: "GPS",
@@ -217,15 +229,9 @@ function buildObsIndices(header) {
217
229
  var ARC_GAP_FACTOR = 5;
218
230
 
219
231
  // src/analysis/multipath.ts
220
- var MIN_ARC_LENGTH = 10;
221
232
  var MP_JUMP_M = 1.25;
222
233
  var MP_JUMP_WINDOW = 5;
223
234
  var MP_EDIT_SIGMA = 3;
224
- function median(values) {
225
- const s = [...values].sort((a, b) => a - b);
226
- const m = s.length >> 1;
227
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
228
- }
229
235
  var MultipathAccumulator = class {
230
236
  state = /* @__PURE__ */ new Map();
231
237
  results = [];
@@ -706,12 +712,6 @@ var CompletenessAccumulator = class {
706
712
  // src/analysis/ionosphere.ts
707
713
  var TEC_FACTOR = 403e15;
708
714
  var GF_JUMP_M = 0.15;
709
- var MIN_ARC_LENGTH2 = 10;
710
- function median2(values) {
711
- const s = [...values].sort((a, b) => a - b);
712
- const m = s.length >> 1;
713
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
714
- }
715
715
  var IonoAccumulator = class {
716
716
  state = /* @__PURE__ */ new Map();
717
717
  closed = /* @__PURE__ */ new Map();
@@ -814,8 +814,8 @@ var IonoAccumulator = class {
814
814
  }
815
815
  closeArc(prn, pairKey, ps) {
816
816
  const arc = ps.arc;
817
- if (arc.times.length >= MIN_ARC_LENGTH2) {
818
- const level = median2(arc.l4.map((v, k) => v - arc.p4[k]));
817
+ if (arc.times.length >= MIN_ARC_LENGTH) {
818
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
819
819
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
820
820
  let satArcs = this.closed.get(prn);
821
821
  if (!satArcs) {
@@ -846,7 +846,7 @@ var IonoAccumulator = class {
846
846
  const series = [];
847
847
  let sum = 0;
848
848
  let count = 0;
849
- let maxStec = 0;
849
+ let maxStec = -Infinity;
850
850
  for (const [prn, satArcs] of this.closed) {
851
851
  let bestKey = null;
852
852
  let bestLen = 0;
@@ -877,7 +877,11 @@ var IonoAccumulator = class {
877
877
  });
878
878
  }
879
879
  series.sort((a, b) => a.prn.localeCompare(b.prn));
880
- return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
880
+ return {
881
+ series,
882
+ maxStec: count > 0 ? maxStec : 0,
883
+ meanStec: count > 0 ? sum / count : 0
884
+ };
881
885
  }
882
886
  };
883
887
 
@@ -885,9 +889,9 @@ var IonoAccumulator = class {
885
889
  var PRN_RE = /^[A-Z]\d{2}$/;
886
890
  var SVN_RE = /^[A-Z]\d{3}$/;
887
891
  var OBS_RE = /^[CL]\d[A-Z]$/;
888
- function sinexEpochMs(s) {
892
+ function sinexEpochMs(s, openValue) {
889
893
  const [y, d, sec] = s.split(":").map(Number);
890
- if (!y) return Infinity;
894
+ if (!y) return openValue;
891
895
  return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
892
896
  }
893
897
  function parseSinexBiasDcb(text, epochMs) {
@@ -902,8 +906,8 @@ function parseSinexBiasDcb(text, epochMs) {
902
906
  if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
903
907
  const value = parseFloat(t[unitIdx + 1]);
904
908
  if (!isFinite(value)) continue;
905
- const start = sinexEpochMs(t[5] ?? "");
906
- const end = sinexEpochMs(t[6] ?? "");
909
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
910
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
907
911
  const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
908
912
  const prn = t[2];
909
913
  const pair = `${t[3]}-${t[4]}`;
@@ -925,10 +929,6 @@ function parseSinexBiasDcb(text, epochMs) {
925
929
  }
926
930
  return out;
927
931
  }
928
- function percentile(values, p) {
929
- const s = [...values].sort((a, b) => a - b);
930
- return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
931
- }
932
932
  function applyIonoDcb(iono, satDcb) {
933
933
  const missing = [];
934
934
  let corrected = 0;
@@ -992,7 +992,7 @@ function applyIonoDcb(iono, satDcb) {
992
992
  out.sort((a, b) => a.prn.localeCompare(b.prn));
993
993
  let sum = 0;
994
994
  let count = 0;
995
- let maxStec = 0;
995
+ let maxStec = -Infinity;
996
996
  for (const s of out) {
997
997
  for (const p of s.points) {
998
998
  sum += p.stec;
@@ -1001,7 +1001,11 @@ function applyIonoDcb(iono, satDcb) {
1001
1001
  }
1002
1002
  }
1003
1003
  return {
1004
- result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
1004
+ result: {
1005
+ series: out,
1006
+ maxStec: count > 0 ? maxStec : 0,
1007
+ meanStec: count > 0 ? sum / count : 0
1008
+ },
1005
1009
  satellitesCorrected: corrected,
1006
1010
  satellitesMissing: [...new Set(missing)].sort(),
1007
1011
  receiverDcbTecu
@@ -26,7 +26,7 @@ interface MultipathSeries {
26
26
  band: string;
27
27
  /** Reference band digit used in dual-frequency combination, e.g. "2". */
28
28
  refBand: string;
29
- /** Human-readable label, e.g. "MP1 L1-L2". */
29
+ /** Human-readable label, e.g. "G01 MP L1-L2". */
30
30
  label: string;
31
31
  /** Time series of multipath values. */
32
32
  points: MultipathPoint[];
@@ -34,7 +34,7 @@ interface MultipathSeries {
34
34
  rms: number;
35
35
  }
36
36
  interface MultipathSignalStat {
37
- /** Human-readable label, e.g. "MP1 L1-L2 (GPS)". */
37
+ /** Human-readable label, e.g. "MP L1-L2 (GPS)". */
38
38
  label: string;
39
39
  /** System letter, e.g. "G". */
40
40
  system: string;
@@ -26,7 +26,7 @@ interface MultipathSeries {
26
26
  band: string;
27
27
  /** Reference band digit used in dual-frequency combination, e.g. "2". */
28
28
  refBand: string;
29
- /** Human-readable label, e.g. "MP1 L1-L2". */
29
+ /** Human-readable label, e.g. "G01 MP L1-L2". */
30
30
  label: string;
31
31
  /** Time series of multipath values. */
32
32
  points: MultipathPoint[];
@@ -34,7 +34,7 @@ interface MultipathSeries {
34
34
  rms: number;
35
35
  }
36
36
  interface MultipathSignalStat {
37
- /** Human-readable label, e.g. "MP1 L1-L2 (GPS)". */
37
+ /** Human-readable label, e.g. "MP L1-L2 (GPS)". */
38
38
  label: string;
39
39
  /** System letter, e.g. "G". */
40
40
  system: string;
package/dist/analysis.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  analyzeQuality,
7
7
  applyIonoDcb,
8
8
  parseSinexBiasDcb
9
- } from "./chunk-EJQ6NG5U.js";
9
+ } from "./chunk-GDGHTC3E.js";
10
10
  import "./chunk-3U5AX7PY.js";
11
11
  import "./chunk-FIEWO4J4.js";
12
12
  export {
@@ -219,20 +219,10 @@ function invert4x4(m) {
219
219
  return result;
220
220
  }
221
221
  function selectEphemeris(ephemerides, prn, timeMs) {
222
- let best = null;
223
- let bestDt = Infinity;
224
- for (const eph of ephemerides) {
225
- if (eph.prn !== prn) continue;
226
- const ephTime = eph.tocDate.getTime();
227
- const dt = Math.abs(timeMs - ephTime);
228
- const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
229
- if (dt > maxAge) continue;
230
- if (dt < bestDt) {
231
- bestDt = dt;
232
- best = eph;
233
- }
234
- }
235
- return best;
222
+ return selectBest(
223
+ ephemerides.filter((e) => e.prn === prn),
224
+ timeMs
225
+ );
236
226
  }
237
227
  function computeSatPosition(eph, timeMs) {
238
228
  if (eph.system === "R" || eph.system === "S") {
@@ -325,8 +315,9 @@ function selectBest(ephs, timeMs) {
325
315
  return best;
326
316
  }
327
317
  var GPS_EPOCH_MS = START_GPS_TIME.getTime();
318
+ var MS_PER_WEEK = 7 * 864e5;
328
319
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
329
- var GAL_EPOCH_MS = GPS_EPOCH_MS;
320
+ var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK;
330
321
  function ephInfoToEphemeris(info) {
331
322
  const sys = info.prn.charAt(0);
332
323
  if (sys === "R") {
@@ -370,6 +361,7 @@ function ephInfoToEphemeris(info) {
370
361
  return null;
371
362
  }
372
363
  let epochMs;
364
+ let week = info.week;
373
365
  const tocSec = info.toc ?? info.toe;
374
366
  if (sys === "C") {
375
367
  epochMs = BDS_EPOCH_MS;
@@ -377,8 +369,10 @@ function ephInfoToEphemeris(info) {
377
369
  epochMs = GAL_EPOCH_MS;
378
370
  } else {
379
371
  epochMs = GPS_EPOCH_MS;
372
+ const weeksNow = (info.lastReceived - GPS_EPOCH_MS) / MS_PER_WEEK;
373
+ week += 1024 * Math.round((weeksNow - week) / 1024);
380
374
  }
381
- const tocDate = new Date(epochMs + info.week * 7 * 864e5 + tocSec * 1e3);
375
+ const tocDate = new Date(epochMs + week * MS_PER_WEEK + tocSec * 1e3);
382
376
  return {
383
377
  system: sys,
384
378
  prn: info.prn,
@@ -404,7 +398,7 @@ function ephInfoToEphemeris(info) {
404
398
  omega: info.argPerigee,
405
399
  omegaDot: info.omegaDot ?? 0,
406
400
  idot: info.idot ?? 0,
407
- week: info.week,
401
+ week,
408
402
  svHealth: info.health,
409
403
  tgd: 0
410
404
  };
@@ -1345,6 +1345,11 @@ function rtcm3Constellation(msgType) {
1345
1345
  if (msgType >= 1111 && msgType <= 1117) return "QZSS";
1346
1346
  if (msgType >= 1121 && msgType <= 1127) return "BeiDou";
1347
1347
  if (msgType >= 1131 && msgType <= 1137) return "NavIC";
1348
+ if (msgType >= 1015 && msgType <= 1017) return "GPS";
1349
+ if (msgType === 1030) return "GPS";
1350
+ if (msgType === 1031) return "GLONASS";
1351
+ if (msgType >= 1057 && msgType <= 1062) return "GPS";
1352
+ if (msgType >= 1063 && msgType <= 1068) return "GLONASS";
1348
1353
  if (msgType === 1230) return "GLONASS";
1349
1354
  return null;
1350
1355
  }
@@ -1384,7 +1389,6 @@ function updateStreamStats(stats, frames, rawBytes) {
1384
1389
  entry.totalBytes += frame.length + 6;
1385
1390
  const msmEpoch = decodeMsmFull(frame);
1386
1391
  if (msmEpoch) {
1387
- const now2 = Date.now();
1388
1392
  for (const obs of msmEpoch.observations) {
1389
1393
  const signals = [];
1390
1394
  let bestCn0 = 0;
@@ -1399,7 +1403,7 @@ function updateStreamStats(stats, frames, rawBytes) {
1399
1403
  prn: obs.prn,
1400
1404
  system: obs.system,
1401
1405
  cn0: bestCn0,
1402
- lastSeen: now2,
1406
+ lastSeen: now,
1403
1407
  signals
1404
1408
  });
1405
1409
  }
@@ -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();
@@ -611,8 +611,8 @@ var IonoAccumulator = class {
611
611
  }
612
612
  closeArc(prn, pairKey, ps) {
613
613
  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]));
614
+ if (arc.times.length >= MIN_ARC_LENGTH) {
615
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
616
616
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
617
617
  let satArcs = this.closed.get(prn);
618
618
  if (!satArcs) {
@@ -643,7 +643,7 @@ var IonoAccumulator = class {
643
643
  const series = [];
644
644
  let sum = 0;
645
645
  let count = 0;
646
- let maxStec = 0;
646
+ let maxStec = -Infinity;
647
647
  for (const [prn, satArcs] of this.closed) {
648
648
  let bestKey = null;
649
649
  let bestLen = 0;
@@ -674,7 +674,11 @@ var IonoAccumulator = class {
674
674
  });
675
675
  }
676
676
  series.sort((a, b) => a.prn.localeCompare(b.prn));
677
- 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
+ };
678
682
  }
679
683
  };
680
684
 
@@ -682,9 +686,9 @@ var IonoAccumulator = class {
682
686
  var PRN_RE = /^[A-Z]\d{2}$/;
683
687
  var SVN_RE = /^[A-Z]\d{3}$/;
684
688
  var OBS_RE = /^[CL]\d[A-Z]$/;
685
- function sinexEpochMs(s) {
689
+ function sinexEpochMs(s, openValue) {
686
690
  const [y, d, sec] = s.split(":").map(Number);
687
- if (!y) return Infinity;
691
+ if (!y) return openValue;
688
692
  return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
689
693
  }
690
694
  function parseSinexBiasDcb(text, epochMs) {
@@ -699,8 +703,8 @@ function parseSinexBiasDcb(text, epochMs) {
699
703
  if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
700
704
  const value = parseFloat(t[unitIdx + 1]);
701
705
  if (!isFinite(value)) continue;
702
- const start = sinexEpochMs(t[5] ?? "");
703
- const end = sinexEpochMs(t[6] ?? "");
706
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
707
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
704
708
  const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
705
709
  const prn = t[2];
706
710
  const pair = `${t[3]}-${t[4]}`;
@@ -722,10 +726,6 @@ function parseSinexBiasDcb(text, epochMs) {
722
726
  }
723
727
  return out;
724
728
  }
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
729
  function applyIonoDcb(iono, satDcb) {
730
730
  const missing = [];
731
731
  let corrected = 0;
@@ -789,7 +789,7 @@ function applyIonoDcb(iono, satDcb) {
789
789
  out.sort((a, b) => a.prn.localeCompare(b.prn));
790
790
  let sum = 0;
791
791
  let count = 0;
792
- let maxStec = 0;
792
+ let maxStec = -Infinity;
793
793
  for (const s of out) {
794
794
  for (const p of s.points) {
795
795
  sum += p.stec;
@@ -798,7 +798,11 @@ function applyIonoDcb(iono, satDcb) {
798
798
  }
799
799
  }
800
800
  return {
801
- result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
801
+ result: {
802
+ series: out,
803
+ maxStec: count > 0 ? maxStec : 0,
804
+ meanStec: count > 0 ? sum / count : 0
805
+ },
802
806
  satellitesCorrected: corrected,
803
807
  satellitesMissing: [...new Set(missing)].sort(),
804
808
  receiverDcbTecu
@@ -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
@@ -3906,6 +3906,11 @@ function rtcm3Constellation(msgType) {
3906
3906
  if (msgType >= 1111 && msgType <= 1117) return "QZSS";
3907
3907
  if (msgType >= 1121 && msgType <= 1127) return "BeiDou";
3908
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";
3909
3914
  if (msgType === 1230) return "GLONASS";
3910
3915
  return null;
3911
3916
  }
@@ -3945,7 +3950,6 @@ function updateStreamStats(stats, frames, rawBytes) {
3945
3950
  entry.totalBytes += frame.length + 6;
3946
3951
  const msmEpoch = decodeMsmFull(frame);
3947
3952
  if (msmEpoch) {
3948
- const now2 = Date.now();
3949
3953
  for (const obs of msmEpoch.observations) {
3950
3954
  const signals = [];
3951
3955
  let bestCn0 = 0;
@@ -3960,7 +3964,7 @@ function updateStreamStats(stats, frames, rawBytes) {
3960
3964
  prn: obs.prn,
3961
3965
  system: obs.system,
3962
3966
  cn0: bestCn0,
3963
- lastSeen: now2,
3967
+ lastSeen: now,
3964
3968
  signals
3965
3969
  });
3966
3970
  }
@@ -4203,20 +4207,10 @@ function invert4x4(m) {
4203
4207
  return result;
4204
4208
  }
4205
4209
  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;
4210
+ return selectBest(
4211
+ ephemerides.filter((e) => e.prn === prn),
4212
+ timeMs
4213
+ );
4220
4214
  }
4221
4215
  function computeSatPosition(eph, timeMs) {
4222
4216
  if (eph.system === "R" || eph.system === "S") {
@@ -4309,8 +4303,9 @@ function selectBest(ephs, timeMs) {
4309
4303
  return best;
4310
4304
  }
4311
4305
  var GPS_EPOCH_MS = START_GPS_TIME.getTime();
4306
+ var MS_PER_WEEK2 = 7 * 864e5;
4312
4307
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
4313
- var GAL_EPOCH_MS = GPS_EPOCH_MS;
4308
+ var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK2;
4314
4309
  function ephInfoToEphemeris(info) {
4315
4310
  const sys = info.prn.charAt(0);
4316
4311
  if (sys === "R") {
@@ -4354,6 +4349,7 @@ function ephInfoToEphemeris(info) {
4354
4349
  return null;
4355
4350
  }
4356
4351
  let epochMs;
4352
+ let week = info.week;
4357
4353
  const tocSec = info.toc ?? info.toe;
4358
4354
  if (sys === "C") {
4359
4355
  epochMs = BDS_EPOCH_MS;
@@ -4361,8 +4357,10 @@ function ephInfoToEphemeris(info) {
4361
4357
  epochMs = GAL_EPOCH_MS;
4362
4358
  } else {
4363
4359
  epochMs = GPS_EPOCH_MS;
4360
+ const weeksNow = (info.lastReceived - GPS_EPOCH_MS) / MS_PER_WEEK2;
4361
+ week += 1024 * Math.round((weeksNow - week) / 1024);
4364
4362
  }
4365
- const tocDate = new Date(epochMs + info.week * 7 * 864e5 + tocSec * 1e3);
4363
+ const tocDate = new Date(epochMs + week * MS_PER_WEEK2 + tocSec * 1e3);
4366
4364
  return {
4367
4365
  system: sys,
4368
4366
  prn: info.prn,
@@ -4388,7 +4386,7 @@ function ephInfoToEphemeris(info) {
4388
4386
  omega: info.argPerigee,
4389
4387
  omegaDot: info.omegaDot ?? 0,
4390
4388
  idot: info.idot ?? 0,
4391
- week: info.week,
4389
+ week,
4392
4390
  svHealth: info.health,
4393
4391
  tgd: 0
4394
4392
  };
@@ -5044,16 +5042,22 @@ async function connectToMountpoint(proxyUrl, info, signal) {
5044
5042
  };
5045
5043
  }
5046
5044
 
5047
- // src/analysis/multipath.ts
5045
+ // src/analysis/stats-util.ts
5048
5046
  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
5047
  function median(values) {
5053
5048
  const s = [...values].sort((a, b) => a - b);
5054
5049
  const m = s.length >> 1;
5055
5050
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
5056
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;
5057
5061
  var MultipathAccumulator = class {
5058
5062
  state = /* @__PURE__ */ new Map();
5059
5063
  results = [];
@@ -5534,12 +5538,6 @@ var CompletenessAccumulator = class {
5534
5538
  // src/analysis/ionosphere.ts
5535
5539
  var TEC_FACTOR = 403e15;
5536
5540
  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
5541
  var IonoAccumulator = class {
5544
5542
  state = /* @__PURE__ */ new Map();
5545
5543
  closed = /* @__PURE__ */ new Map();
@@ -5642,8 +5640,8 @@ var IonoAccumulator = class {
5642
5640
  }
5643
5641
  closeArc(prn, pairKey, ps) {
5644
5642
  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]));
5643
+ if (arc.times.length >= MIN_ARC_LENGTH) {
5644
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
5647
5645
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
5648
5646
  let satArcs = this.closed.get(prn);
5649
5647
  if (!satArcs) {
@@ -5674,7 +5672,7 @@ var IonoAccumulator = class {
5674
5672
  const series = [];
5675
5673
  let sum = 0;
5676
5674
  let count = 0;
5677
- let maxStec = 0;
5675
+ let maxStec = -Infinity;
5678
5676
  for (const [prn, satArcs] of this.closed) {
5679
5677
  let bestKey = null;
5680
5678
  let bestLen = 0;
@@ -5705,7 +5703,11 @@ var IonoAccumulator = class {
5705
5703
  });
5706
5704
  }
5707
5705
  series.sort((a, b) => a.prn.localeCompare(b.prn));
5708
- 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
+ };
5709
5711
  }
5710
5712
  };
5711
5713
 
@@ -5713,9 +5715,9 @@ var IonoAccumulator = class {
5713
5715
  var PRN_RE = /^[A-Z]\d{2}$/;
5714
5716
  var SVN_RE = /^[A-Z]\d{3}$/;
5715
5717
  var OBS_RE = /^[CL]\d[A-Z]$/;
5716
- function sinexEpochMs(s) {
5718
+ function sinexEpochMs(s, openValue) {
5717
5719
  const [y, d, sec] = s.split(":").map(Number);
5718
- if (!y) return Infinity;
5720
+ if (!y) return openValue;
5719
5721
  return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
5720
5722
  }
5721
5723
  function parseSinexBiasDcb(text, epochMs) {
@@ -5730,8 +5732,8 @@ function parseSinexBiasDcb(text, epochMs) {
5730
5732
  if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
5731
5733
  const value = parseFloat(t[unitIdx + 1]);
5732
5734
  if (!isFinite(value)) continue;
5733
- const start = sinexEpochMs(t[5] ?? "");
5734
- const end = sinexEpochMs(t[6] ?? "");
5735
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
5736
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
5735
5737
  const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
5736
5738
  const prn = t[2];
5737
5739
  const pair = `${t[3]}-${t[4]}`;
@@ -5753,10 +5755,6 @@ function parseSinexBiasDcb(text, epochMs) {
5753
5755
  }
5754
5756
  return out;
5755
5757
  }
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
5758
  function applyIonoDcb(iono, satDcb) {
5761
5759
  const missing = [];
5762
5760
  let corrected = 0;
@@ -5820,7 +5818,7 @@ function applyIonoDcb(iono, satDcb) {
5820
5818
  out.sort((a, b) => a.prn.localeCompare(b.prn));
5821
5819
  let sum = 0;
5822
5820
  let count = 0;
5823
- let maxStec = 0;
5821
+ let maxStec = -Infinity;
5824
5822
  for (const s of out) {
5825
5823
  for (const p of s.points) {
5826
5824
  sum += p.stec;
@@ -5829,7 +5827,11 @@ function applyIonoDcb(iono, satDcb) {
5829
5827
  }
5830
5828
  }
5831
5829
  return {
5832
- result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
5830
+ result: {
5831
+ series: out,
5832
+ maxStec: count > 0 ? maxStec : 0,
5833
+ meanStec: count > 0 ? sum / count : 0
5834
+ },
5833
5835
  satellitesCorrected: corrected,
5834
5836
  satellitesMissing: [...new Set(missing)].sort(),
5835
5837
  receiverDcbTecu
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  analyzeQuality,
12
12
  applyIonoDcb,
13
13
  parseSinexBiasDcb
14
- } from "./chunk-EJQ6NG5U.js";
14
+ } from "./chunk-GDGHTC3E.js";
15
15
  import {
16
16
  frequencyLabel,
17
17
  parseAntex
@@ -100,7 +100,7 @@ import {
100
100
  ionoFree,
101
101
  satClockCorrection,
102
102
  solveSpp
103
- } from "./chunk-PX7TPPTQ.js";
103
+ } from "./chunk-SOJNWF5T.js";
104
104
  import {
105
105
  computeAllPositions,
106
106
  computeDop,
@@ -113,7 +113,7 @@ import {
113
113
  keplerPosition,
114
114
  navTimesFromEph,
115
115
  selectEphemeris
116
- } from "./chunk-PQYBTE42.js";
116
+ } from "./chunk-CD5YSYCG.js";
117
117
  import {
118
118
  getBdsTime,
119
119
  getDateFromBdsTime,
@@ -199,7 +199,7 @@ import {
199
199
  setRtcm3DebugHandler,
200
200
  updateStationMeta,
201
201
  updateStreamStats
202
- } from "./chunk-65CQINSB.js";
202
+ } from "./chunk-FGRC3PB3.js";
203
203
  import {
204
204
  DAYS_MJD2000_MJD,
205
205
  DAYS_MJD_JULIAN,
package/dist/orbit.cjs CHANGED
@@ -372,20 +372,10 @@ function invert4x4(m) {
372
372
  return result;
373
373
  }
374
374
  function selectEphemeris(ephemerides, prn, timeMs) {
375
- let best = null;
376
- let bestDt = Infinity;
377
- for (const eph of ephemerides) {
378
- if (eph.prn !== prn) continue;
379
- const ephTime = eph.tocDate.getTime();
380
- const dt = Math.abs(timeMs - ephTime);
381
- const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
382
- if (dt > maxAge) continue;
383
- if (dt < bestDt) {
384
- bestDt = dt;
385
- best = eph;
386
- }
387
- }
388
- return best;
375
+ return selectBest(
376
+ ephemerides.filter((e) => e.prn === prn),
377
+ timeMs
378
+ );
389
379
  }
390
380
  function computeSatPosition(eph, timeMs) {
391
381
  if (eph.system === "R" || eph.system === "S") {
@@ -478,8 +468,9 @@ function selectBest(ephs, timeMs) {
478
468
  return best;
479
469
  }
480
470
  var GPS_EPOCH_MS = START_GPS_TIME.getTime();
471
+ var MS_PER_WEEK = 7 * 864e5;
481
472
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
482
- var GAL_EPOCH_MS = GPS_EPOCH_MS;
473
+ var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK;
483
474
  function ephInfoToEphemeris(info) {
484
475
  const sys = info.prn.charAt(0);
485
476
  if (sys === "R") {
@@ -523,6 +514,7 @@ function ephInfoToEphemeris(info) {
523
514
  return null;
524
515
  }
525
516
  let epochMs;
517
+ let week = info.week;
526
518
  const tocSec = info.toc ?? info.toe;
527
519
  if (sys === "C") {
528
520
  epochMs = BDS_EPOCH_MS;
@@ -530,8 +522,10 @@ function ephInfoToEphemeris(info) {
530
522
  epochMs = GAL_EPOCH_MS;
531
523
  } else {
532
524
  epochMs = GPS_EPOCH_MS;
525
+ const weeksNow = (info.lastReceived - GPS_EPOCH_MS) / MS_PER_WEEK;
526
+ week += 1024 * Math.round((weeksNow - week) / 1024);
533
527
  }
534
- const tocDate = new Date(epochMs + info.week * 7 * 864e5 + tocSec * 1e3);
528
+ const tocDate = new Date(epochMs + week * MS_PER_WEEK + tocSec * 1e3);
535
529
  return {
536
530
  system: sys,
537
531
  prn: info.prn,
@@ -557,7 +551,7 @@ function ephInfoToEphemeris(info) {
557
551
  omega: info.argPerigee,
558
552
  omegaDot: info.omegaDot ?? 0,
559
553
  idot: info.idot ?? 0,
560
- week: info.week,
554
+ week,
561
555
  svHealth: info.health,
562
556
  tgd: 0
563
557
  };
package/dist/orbit.d.cts CHANGED
@@ -131,7 +131,7 @@ interface VisibilityPass {
131
131
  interface VisibilityResult {
132
132
  /** Sample epochs (Unix ms). */
133
133
  times: number[];
134
- /** Elevation (radians) per PRN per epoch; null when below −0.05 rad / no eph. */
134
+ /** Elevation (radians) per PRN per epoch; null when no valid ephemeris. */
135
135
  elevation: Record<string, (number | null)[]>;
136
136
  /** Number of satellites at or above the mask, per epoch. */
137
137
  visibleCount: number[];
package/dist/orbit.d.ts CHANGED
@@ -131,7 +131,7 @@ interface VisibilityPass {
131
131
  interface VisibilityResult {
132
132
  /** Sample epochs (Unix ms). */
133
133
  times: number[];
134
- /** Elevation (radians) per PRN per epoch; null when below −0.05 rad / no eph. */
134
+ /** Elevation (radians) per PRN per epoch; null when no valid ephemeris. */
135
135
  elevation: Record<string, (number | null)[]>;
136
136
  /** Number of satellites at or above the mask, per epoch. */
137
137
  visibleCount: number[];
package/dist/orbit.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  keplerPosition,
11
11
  navTimesFromEph,
12
12
  selectEphemeris
13
- } from "./chunk-PQYBTE42.js";
13
+ } from "./chunk-CD5YSYCG.js";
14
14
  import "./chunk-HVXYFUCB.js";
15
15
  import {
16
16
  ecefToGeodetic,
@@ -367,7 +367,9 @@ function computeSatPosition(eph, timeMs) {
367
367
  return keplerPosition(eph, tow);
368
368
  }
369
369
  var GPS_EPOCH_MS = START_GPS_TIME.getTime();
370
+ var MS_PER_WEEK = 7 * 864e5;
370
371
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
372
+ var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK;
371
373
 
372
374
  // src/positioning/index.ts
373
375
  var GM_GPS2 = 3986005e8;
@@ -2,8 +2,8 @@ import {
2
2
  ionoFree,
3
3
  satClockCorrection,
4
4
  solveSpp
5
- } from "./chunk-PX7TPPTQ.js";
6
- import "./chunk-PQYBTE42.js";
5
+ } from "./chunk-SOJNWF5T.js";
6
+ import "./chunk-CD5YSYCG.js";
7
7
  import "./chunk-HVXYFUCB.js";
8
8
  import "./chunk-37QNKGTC.js";
9
9
  import "./chunk-6FAL6P4G.js";
package/dist/rtcm3.cjs CHANGED
@@ -1406,6 +1406,11 @@ function rtcm3Constellation(msgType) {
1406
1406
  if (msgType >= 1111 && msgType <= 1117) return "QZSS";
1407
1407
  if (msgType >= 1121 && msgType <= 1127) return "BeiDou";
1408
1408
  if (msgType >= 1131 && msgType <= 1137) return "NavIC";
1409
+ if (msgType >= 1015 && msgType <= 1017) return "GPS";
1410
+ if (msgType === 1030) return "GPS";
1411
+ if (msgType === 1031) return "GLONASS";
1412
+ if (msgType >= 1057 && msgType <= 1062) return "GPS";
1413
+ if (msgType >= 1063 && msgType <= 1068) return "GLONASS";
1409
1414
  if (msgType === 1230) return "GLONASS";
1410
1415
  return null;
1411
1416
  }
@@ -1445,7 +1450,6 @@ function updateStreamStats(stats, frames, rawBytes) {
1445
1450
  entry.totalBytes += frame.length + 6;
1446
1451
  const msmEpoch = decodeMsmFull(frame);
1447
1452
  if (msmEpoch) {
1448
- const now2 = Date.now();
1449
1453
  for (const obs of msmEpoch.observations) {
1450
1454
  const signals = [];
1451
1455
  let bestCn0 = 0;
@@ -1460,7 +1464,7 @@ function updateStreamStats(stats, frames, rawBytes) {
1460
1464
  prn: obs.prn,
1461
1465
  system: obs.system,
1462
1466
  cn0: bestCn0,
1463
- lastSeen: now2,
1467
+ lastSeen: now,
1464
1468
  signals
1465
1469
  });
1466
1470
  }
package/dist/rtcm3.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  setRtcm3DebugHandler,
16
16
  updateStationMeta,
17
17
  updateStreamStats
18
- } from "./chunk-65CQINSB.js";
18
+ } from "./chunk-FGRC3PB3.js";
19
19
  import "./chunk-LEEU5OIO.js";
20
20
  import "./chunk-FIEWO4J4.js";
21
21
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.5.0",
3
+ "version": "1.5.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,
@@ -106,10 +106,10 @@
106
106
  "positioning"
107
107
  ],
108
108
  "author": "Miguel González <work@miguel.es>",
109
- "license": "AGPL-3.0-only",
109
+ "license": "AGPL-3.0-only OR LicenseRef-Commercial",
110
110
  "repository": {
111
111
  "type": "git",
112
- "url": "https://github.com/MiguelPuntoEs/gnss-js"
112
+ "url": "git+https://github.com/MiguelPuntoEs/gnss-js.git"
113
113
  },
114
114
  "homepage": "https://github.com/MiguelPuntoEs/gnss-js#readme",
115
115
  "bugs": {