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.
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
@@ -26,10 +26,24 @@ __export(analysis_exports, {
26
26
  MultipathAccumulator: () => MultipathAccumulator,
27
27
  analyzeQuality: () => analyzeQuality,
28
28
  applyIonoDcb: () => applyIonoDcb,
29
+ computeIonoRate: () => computeIonoRate,
30
+ detrendIonoArcs: () => detrendIonoArcs,
29
31
  parseSinexBiasDcb: () => parseSinexBiasDcb
30
32
  });
31
33
  module.exports = __toCommonJS(analysis_exports);
32
34
 
35
+ // src/analysis/stats-util.ts
36
+ var MIN_ARC_LENGTH = 10;
37
+ function median(values) {
38
+ const s = [...values].sort((a, b) => a - b);
39
+ const m = s.length >> 1;
40
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
41
+ }
42
+ function percentile(values, p) {
43
+ const s = [...values].sort((a, b) => a - b);
44
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
45
+ }
46
+
33
47
  // src/constants/gnss.ts
34
48
  var SYSTEM_NAMES = {
35
49
  G: "GPS",
@@ -217,15 +231,9 @@ function buildObsIndices(header) {
217
231
  var ARC_GAP_FACTOR = 5;
218
232
 
219
233
  // src/analysis/multipath.ts
220
- var MIN_ARC_LENGTH = 10;
221
234
  var MP_JUMP_M = 1.25;
222
235
  var MP_JUMP_WINDOW = 5;
223
236
  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
237
  var MultipathAccumulator = class {
230
238
  state = /* @__PURE__ */ new Map();
231
239
  results = [];
@@ -706,15 +714,10 @@ var CompletenessAccumulator = class {
706
714
  // src/analysis/ionosphere.ts
707
715
  var TEC_FACTOR = 403e15;
708
716
  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
717
  var IonoAccumulator = class {
716
718
  state = /* @__PURE__ */ new Map();
717
719
  closed = /* @__PURE__ */ new Map();
720
+ closedArcStarts = /* @__PURE__ */ new Map();
718
721
  interval;
719
722
  obsIndices;
720
723
  gloChannels;
@@ -814,8 +817,8 @@ var IonoAccumulator = class {
814
817
  }
815
818
  closeArc(prn, pairKey, ps) {
816
819
  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]));
820
+ if (arc.times.length >= MIN_ARC_LENGTH) {
821
+ const level = median(arc.l4.map((v, k) => v - arc.p4[k]));
819
822
  const toTecu = arc.fi * arc.fi / TEC_FACTOR / (arc.gamma - 1);
820
823
  let satArcs = this.closed.get(prn);
821
824
  if (!satArcs) {
@@ -827,6 +830,17 @@ var IonoAccumulator = class {
827
830
  points = [];
828
831
  satArcs.set(pairKey, points);
829
832
  }
833
+ let satStarts = this.closedArcStarts.get(prn);
834
+ if (!satStarts) {
835
+ satStarts = /* @__PURE__ */ new Map();
836
+ this.closedArcStarts.set(prn, satStarts);
837
+ }
838
+ let starts = satStarts.get(pairKey);
839
+ if (!starts) {
840
+ starts = [];
841
+ satStarts.set(pairKey, starts);
842
+ }
843
+ starts.push(points.length);
830
844
  for (let k = 0; k < arc.times.length; k++) {
831
845
  points.push({
832
846
  time: arc.times[k],
@@ -846,7 +860,7 @@ var IonoAccumulator = class {
846
860
  const series = [];
847
861
  let sum = 0;
848
862
  let count = 0;
849
- let maxStec = 0;
863
+ let maxStec = -Infinity;
850
864
  for (const [prn, satArcs] of this.closed) {
851
865
  let bestKey = null;
852
866
  let bestLen = 0;
@@ -858,7 +872,7 @@ var IonoAccumulator = class {
858
872
  }
859
873
  if (!bestKey) continue;
860
874
  const points = satArcs.get(bestKey);
861
- points.sort((a, b) => a.time - b.time);
875
+ const arcStarts = this.closedArcStarts.get(prn)?.get(bestKey) ?? [0];
862
876
  const sys = prn[0];
863
877
  for (const p of points) {
864
878
  sum += p.stec;
@@ -873,21 +887,107 @@ var IonoAccumulator = class {
873
887
  label: this.pairLabel.get(`${sys}:${bestKey}`) ?? bestKey,
874
888
  codes: this.pairCodes.get(`${sys}:${bestKey}`) ?? ["", ""],
875
889
  tecuPerNs,
876
- points
890
+ points,
891
+ arcStarts
877
892
  });
878
893
  }
879
894
  series.sort((a, b) => a.prn.localeCompare(b.prn));
880
- return { series, maxStec, meanStec: count > 0 ? sum / count : 0 };
895
+ return {
896
+ series,
897
+ maxStec: count > 0 ? maxStec : 0,
898
+ meanStec: count > 0 ? sum / count : 0
899
+ };
881
900
  }
882
901
  };
902
+ function computeIonoRate(result, intervalSec, order = 1) {
903
+ const out = [];
904
+ for (const s of result.series) {
905
+ const pts = [];
906
+ const bounds = [...s.arcStarts, s.points.length];
907
+ for (let a = 0; a < bounds.length - 1; a++) {
908
+ const lo = bounds[a];
909
+ const hi = bounds[a + 1];
910
+ if (order === 2) {
911
+ for (let i = lo + 2; i < hi; i++) {
912
+ const dt1 = s.points[i - 1].time - s.points[i - 2].time;
913
+ const dt2 = s.points[i].time - s.points[i - 1].time;
914
+ if (Math.abs(dt1 - dt2) > 0.1 * Math.max(dt1, dt2)) continue;
915
+ pts.push({
916
+ time: s.points[i].time,
917
+ value: s.points[i].stec - 2 * s.points[i - 1].stec + s.points[i - 2].stec
918
+ });
919
+ }
920
+ continue;
921
+ }
922
+ if (intervalSec === void 0) {
923
+ for (let i = lo + 1; i < hi; i++) {
924
+ const dtMin = (s.points[i].time - s.points[i - 1].time) / 6e4;
925
+ if (dtMin <= 0) continue;
926
+ pts.push({
927
+ time: s.points[i].time,
928
+ value: (s.points[i].stec - s.points[i - 1].stec) / dtMin
929
+ });
930
+ }
931
+ } else {
932
+ const targetMs = intervalSec * 1e3;
933
+ let j = lo;
934
+ for (let i = lo; i < hi; i++) {
935
+ if (j <= i) j = i + 1;
936
+ while (j < hi && s.points[j].time - s.points[i].time < targetMs)
937
+ j++;
938
+ if (j >= hi) break;
939
+ const dtMs = s.points[j].time - s.points[i].time;
940
+ if (dtMs > 1.5 * targetMs) continue;
941
+ pts.push({
942
+ time: s.points[j].time,
943
+ value: (s.points[j].stec - s.points[i].stec) / dtMs * 6e4
944
+ });
945
+ }
946
+ }
947
+ }
948
+ if (pts.length > 0) out.push({ prn: s.prn, system: s.system, points: pts });
949
+ }
950
+ return out;
951
+ }
952
+ function detrendIonoArcs(result) {
953
+ let sum = 0;
954
+ let count = 0;
955
+ let maxStec = -Infinity;
956
+ const series = result.series.map((s) => {
957
+ const bounds = [...s.arcStarts, s.points.length];
958
+ const points = new Array(s.points.length);
959
+ for (let a = 0; a < bounds.length - 1; a++) {
960
+ const lo = bounds[a];
961
+ const hi = bounds[a + 1];
962
+ const anchor = s.points[lo]?.stec ?? 0;
963
+ for (let i = lo; i < hi; i++) {
964
+ points[i] = {
965
+ time: s.points[i].time,
966
+ stec: s.points[i].stec - anchor
967
+ };
968
+ }
969
+ }
970
+ for (const p of points) {
971
+ sum += p.stec;
972
+ count++;
973
+ if (p.stec > maxStec) maxStec = p.stec;
974
+ }
975
+ return { ...s, points };
976
+ });
977
+ return {
978
+ series,
979
+ maxStec: count > 0 ? maxStec : 0,
980
+ meanStec: count > 0 ? sum / count : 0
981
+ };
982
+ }
883
983
 
884
984
  // src/analysis/dcb.ts
885
985
  var PRN_RE = /^[A-Z]\d{2}$/;
886
986
  var SVN_RE = /^[A-Z]\d{3}$/;
887
987
  var OBS_RE = /^[CL]\d[A-Z]$/;
888
- function sinexEpochMs(s) {
988
+ function sinexEpochMs(s, openValue) {
889
989
  const [y, d, sec] = s.split(":").map(Number);
890
- if (!y) return Infinity;
990
+ if (!y) return openValue;
891
991
  return Date.UTC(y, 0, 1) + ((d ?? 1) - 1) * 864e5 + (sec ?? 0) * 1e3;
892
992
  }
893
993
  function parseSinexBiasDcb(text, epochMs) {
@@ -902,8 +1002,8 @@ function parseSinexBiasDcb(text, epochMs) {
902
1002
  if (unitIdx < 0 || unitIdx + 1 >= t.length) continue;
903
1003
  const value = parseFloat(t[unitIdx + 1]);
904
1004
  if (!isFinite(value)) continue;
905
- const start = sinexEpochMs(t[5] ?? "");
906
- const end = sinexEpochMs(t[6] ?? "");
1005
+ const start = sinexEpochMs(t[5] ?? "", -Infinity);
1006
+ const end = sinexEpochMs(t[6] ?? "", Infinity);
907
1007
  const covers = epochMs !== void 0 && epochMs >= start && epochMs < end;
908
1008
  const prn = t[2];
909
1009
  const pair = `${t[3]}-${t[4]}`;
@@ -925,10 +1025,6 @@ function parseSinexBiasDcb(text, epochMs) {
925
1025
  }
926
1026
  return out;
927
1027
  }
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
1028
  function applyIonoDcb(iono, satDcb) {
933
1029
  const missing = [];
934
1030
  let corrected = 0;
@@ -992,7 +1088,7 @@ function applyIonoDcb(iono, satDcb) {
992
1088
  out.sort((a, b) => a.prn.localeCompare(b.prn));
993
1089
  let sum = 0;
994
1090
  let count = 0;
995
- let maxStec = 0;
1091
+ let maxStec = -Infinity;
996
1092
  for (const s of out) {
997
1093
  for (const p of s.points) {
998
1094
  sum += p.stec;
@@ -1001,7 +1097,11 @@ function applyIonoDcb(iono, satDcb) {
1001
1097
  }
1002
1098
  }
1003
1099
  return {
1004
- result: { series: out, maxStec, meanStec: count > 0 ? sum / count : 0 },
1100
+ result: {
1101
+ series: out,
1102
+ maxStec: count > 0 ? maxStec : 0,
1103
+ meanStec: count > 0 ? sum / count : 0
1104
+ },
1005
1105
  satellitesCorrected: corrected,
1006
1106
  satellitesMissing: [...new Set(missing)].sort(),
1007
1107
  receiverDcbTecu
@@ -1831,5 +1931,7 @@ async function analyzeQuality(file, header, onProgress, signal) {
1831
1931
  MultipathAccumulator,
1832
1932
  analyzeQuality,
1833
1933
  applyIonoDcb,
1934
+ computeIonoRate,
1935
+ detrendIonoArcs,
1834
1936
  parseSinexBiasDcb
1835
1937
  });
@@ -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;
@@ -239,6 +239,12 @@ interface IonoSeries {
239
239
  tecuPerNs: number;
240
240
  /** Time series of slant TEC values (TECU, DCB-biased). */
241
241
  points: IonoPoint[];
242
+ /**
243
+ * Indices into `points` where a new continuous arc begins (gap,
244
+ * cycle slip, or geometry-free jump). Differencing and per-arc
245
+ * detrending must not cross these boundaries.
246
+ */
247
+ arcStarts: number[];
242
248
  }
243
249
  interface IonoResult {
244
250
  /** Per-satellite slant TEC time series. */
@@ -251,6 +257,7 @@ interface IonoResult {
251
257
  declare class IonoAccumulator {
252
258
  private state;
253
259
  private closed;
260
+ private closedArcStarts;
254
261
  private interval;
255
262
  private obsIndices;
256
263
  private gloChannels;
@@ -267,6 +274,39 @@ declare class IonoAccumulator {
267
274
  /** Finalize: close remaining arcs, keep one pair per satellite. */
268
275
  finalize(): IonoResult;
269
276
  }
277
+ interface IonoRatePoint {
278
+ /** Epoch time (ms) — the later epoch of the differenced pair. */
279
+ time: number;
280
+ /** ROT in TECU/min (order 1) or undivided ΔΔSTEC in TECU (order 2). */
281
+ value: number;
282
+ }
283
+ interface IonoRateSeries {
284
+ prn: string;
285
+ system: string;
286
+ points: IonoRatePoint[];
287
+ }
288
+ /**
289
+ * Sequential time differences of the slant TEC series — the biases
290
+ * (ambiguities, DCBs) cancel, leaving ionospheric rate of TEC, phase
291
+ * noise, and scintillation. Differences never cross arc boundaries,
292
+ * so cycle slips do not appear as outliers (arcs split there).
293
+ *
294
+ * @param intervalSec Standardized differencing baseline in seconds.
295
+ * Omit for native-rate sequential differences; set e.g. 60 for a
296
+ * sample-rate-independent picture (all pairs ~1 min apart are used).
297
+ * @param order 1 (default) = first difference in TECU/min;
298
+ * 2 = second undivided difference in TECU (native rate only,
299
+ * gradients removed, noise and scintillation amplified).
300
+ */
301
+ declare function computeIonoRate(result: IonoResult, intervalSec?: number, order?: 1 | 2): IonoRateSeries[];
302
+ /**
303
+ * Remove the per-arc bias from a slant TEC result — each arc is
304
+ * shifted so its first observation reads zero, leaving only the TEC
305
+ * *variation* along the arc (Hans van der Marel's suggestion; using
306
+ * the first observation keeps the shape untouched at the small risk
307
+ * of anchoring on an outlier).
308
+ */
309
+ declare function detrendIonoArcs(result: IonoResult): IonoResult;
270
310
 
271
311
  /**
272
312
  * Differential code bias (DCB) products and their application to the
@@ -323,4 +363,4 @@ interface QualityResult {
323
363
  }
324
364
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
325
365
 
326
- export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb };
366
+ export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoRatePoint, type IonoRateSeries, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb };
@@ -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;
@@ -239,6 +239,12 @@ interface IonoSeries {
239
239
  tecuPerNs: number;
240
240
  /** Time series of slant TEC values (TECU, DCB-biased). */
241
241
  points: IonoPoint[];
242
+ /**
243
+ * Indices into `points` where a new continuous arc begins (gap,
244
+ * cycle slip, or geometry-free jump). Differencing and per-arc
245
+ * detrending must not cross these boundaries.
246
+ */
247
+ arcStarts: number[];
242
248
  }
243
249
  interface IonoResult {
244
250
  /** Per-satellite slant TEC time series. */
@@ -251,6 +257,7 @@ interface IonoResult {
251
257
  declare class IonoAccumulator {
252
258
  private state;
253
259
  private closed;
260
+ private closedArcStarts;
254
261
  private interval;
255
262
  private obsIndices;
256
263
  private gloChannels;
@@ -267,6 +274,39 @@ declare class IonoAccumulator {
267
274
  /** Finalize: close remaining arcs, keep one pair per satellite. */
268
275
  finalize(): IonoResult;
269
276
  }
277
+ interface IonoRatePoint {
278
+ /** Epoch time (ms) — the later epoch of the differenced pair. */
279
+ time: number;
280
+ /** ROT in TECU/min (order 1) or undivided ΔΔSTEC in TECU (order 2). */
281
+ value: number;
282
+ }
283
+ interface IonoRateSeries {
284
+ prn: string;
285
+ system: string;
286
+ points: IonoRatePoint[];
287
+ }
288
+ /**
289
+ * Sequential time differences of the slant TEC series — the biases
290
+ * (ambiguities, DCBs) cancel, leaving ionospheric rate of TEC, phase
291
+ * noise, and scintillation. Differences never cross arc boundaries,
292
+ * so cycle slips do not appear as outliers (arcs split there).
293
+ *
294
+ * @param intervalSec Standardized differencing baseline in seconds.
295
+ * Omit for native-rate sequential differences; set e.g. 60 for a
296
+ * sample-rate-independent picture (all pairs ~1 min apart are used).
297
+ * @param order 1 (default) = first difference in TECU/min;
298
+ * 2 = second undivided difference in TECU (native rate only,
299
+ * gradients removed, noise and scintillation amplified).
300
+ */
301
+ declare function computeIonoRate(result: IonoResult, intervalSec?: number, order?: 1 | 2): IonoRateSeries[];
302
+ /**
303
+ * Remove the per-arc bias from a slant TEC result — each arc is
304
+ * shifted so its first observation reads zero, leaving only the TEC
305
+ * *variation* along the arc (Hans van der Marel's suggestion; using
306
+ * the first observation keeps the shape untouched at the small risk
307
+ * of anchoring on an outlier).
308
+ */
309
+ declare function detrendIonoArcs(result: IonoResult): IonoResult;
270
310
 
271
311
  /**
272
312
  * Differential code bias (DCB) products and their application to the
@@ -323,4 +363,4 @@ interface QualityResult {
323
363
  }
324
364
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
325
365
 
326
- export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, parseSinexBiasDcb };
366
+ export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoRatePoint, type IonoRateSeries, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb };
package/dist/analysis.js CHANGED
@@ -5,8 +5,10 @@ import {
5
5
  MultipathAccumulator,
6
6
  analyzeQuality,
7
7
  applyIonoDcb,
8
+ computeIonoRate,
9
+ detrendIonoArcs,
8
10
  parseSinexBiasDcb
9
- } from "./chunk-EJQ6NG5U.js";
11
+ } from "./chunk-Z5UZUNCB.js";
10
12
  import "./chunk-3U5AX7PY.js";
11
13
  import "./chunk-FIEWO4J4.js";
12
14
  export {
@@ -16,5 +18,7 @@ export {
16
18
  MultipathAccumulator,
17
19
  analyzeQuality,
18
20
  applyIonoDcb,
21
+ computeIonoRate,
22
+ detrendIonoArcs,
19
23
  parseSinexBiasDcb
20
24
  };
@@ -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
  }
@@ -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