gnss-js 1.29.0 → 1.31.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.
@@ -5,6 +5,9 @@ import {
5
5
  import {
6
6
  GPS_PI
7
7
  } from "./chunk-IQ5OIMQD.js";
8
+ import {
9
+ C_LIGHT
10
+ } from "./chunk-FIEWO4J4.js";
8
11
 
9
12
  // src/rinex/crx-writer.ts
10
13
  var ARC_ORDER = 3;
@@ -1417,6 +1420,81 @@ function parseIonex(text) {
1417
1420
  return { epochs, lats, lons, maps };
1418
1421
  }
1419
1422
 
1423
+ // src/rinex/bias-sinex.ts
1424
+ function parseEpoch(s, openTo) {
1425
+ const m = /^(\d{4}):(\d{3}):(\d{5})$/.exec(s.trim());
1426
+ if (!m) return openTo;
1427
+ const y = +m[1];
1428
+ if (y === 0) return openTo;
1429
+ const doy = +m[2];
1430
+ const sod = +m[3];
1431
+ return Date.UTC(y, 0, 1) + (doy - 1) * 864e5 + sod * 1e3;
1432
+ }
1433
+ function parseBiasSinex(text) {
1434
+ const records = [];
1435
+ let inBlock = false;
1436
+ for (const line of text.split("\n")) {
1437
+ if (line.startsWith("+BIAS/SOLUTION")) {
1438
+ inBlock = true;
1439
+ continue;
1440
+ }
1441
+ if (line.startsWith("-BIAS/SOLUTION")) {
1442
+ inBlock = false;
1443
+ continue;
1444
+ }
1445
+ if (!inBlock || line.length < 65 || line.startsWith("*")) continue;
1446
+ const type = line.slice(1, 4).trim();
1447
+ if (type !== "DSB" && type !== "OSB" && type !== "ISB") continue;
1448
+ const prn = line.slice(11, 14).trim();
1449
+ const station = line.slice(15, 24).trim();
1450
+ const obs1 = line.slice(25, 29).trim();
1451
+ const obs2 = line.slice(30, 34).trim();
1452
+ const startMs = parseEpoch(line.slice(35, 49), -Infinity);
1453
+ const endMs = parseEpoch(line.slice(50, 64), Infinity);
1454
+ const tail = line.slice(65).trim().split(/\s+/);
1455
+ if (tail.length < 2) continue;
1456
+ const unit = tail[0];
1457
+ const value = Number(tail[1]);
1458
+ if (!Number.isFinite(value)) continue;
1459
+ records.push({
1460
+ type,
1461
+ prn,
1462
+ station,
1463
+ obs1,
1464
+ obs2,
1465
+ startMs,
1466
+ endMs,
1467
+ value,
1468
+ unit
1469
+ });
1470
+ }
1471
+ const bySat = /* @__PURE__ */ new Map();
1472
+ for (const r of records) {
1473
+ if (!r.prn || r.station) continue;
1474
+ let a = bySat.get(r.prn);
1475
+ if (!a) {
1476
+ a = [];
1477
+ bySat.set(r.prn, a);
1478
+ }
1479
+ a.push(r);
1480
+ }
1481
+ return { records, bySat };
1482
+ }
1483
+ function biasMetres(r) {
1484
+ return r.unit === "ns" ? r.value * 1e-9 * C_LIGHT : r.value;
1485
+ }
1486
+ function findSatBias(bias, prn, obs1, obs2, tMs) {
1487
+ const recs = bias.bySat.get(prn);
1488
+ if (!recs) return null;
1489
+ for (const r of recs) {
1490
+ if (r.obs1 !== obs1) continue;
1491
+ if (obs2 === void 0 ? r.obs2 !== "" : r.obs2 !== obs2) continue;
1492
+ if (tMs != null && (tMs < r.startMs || tMs > r.endMs)) continue;
1493
+ return r;
1494
+ }
1495
+ return null;
1496
+ }
1497
+
1420
1498
  // src/rinex/obs-writer-common.ts
1421
1499
  function createGzipLineSink() {
1422
1500
  const encoder = new TextEncoder();
@@ -1804,6 +1882,9 @@ export {
1804
1882
  writeRinexNav,
1805
1883
  writeRinexNav4,
1806
1884
  parseIonex,
1885
+ parseBiasSinex,
1886
+ biasMetres,
1887
+ findSatBias,
1807
1888
  createGzipLineSink,
1808
1889
  stationHeaderLines,
1809
1890
  writeRinexObsBlob,
@@ -10,8 +10,9 @@ import {
10
10
  getEnuDifference
11
11
  } from "./chunk-37QNKGTC.js";
12
12
  import {
13
+ clkBias,
13
14
  sp3Position
14
- } from "./chunk-4KMWWCUR.js";
15
+ } from "./chunk-GPEP67TG.js";
15
16
  import {
16
17
  C_LIGHT,
17
18
  OMEGA_E
@@ -1582,7 +1583,7 @@ function sagnacRotate(x, y, z, travelS) {
1582
1583
  const s = Math.sin(theta);
1583
1584
  return [x * c + y * s, -x * s + y * c, z];
1584
1585
  }
1585
- function satStateAtEmission(sp3, prn, recvTimeMs, rcv) {
1586
+ function satStateAtEmission(sp3, prn, recvTimeMs, rcv, clk) {
1586
1587
  let travelS = 0.075;
1587
1588
  let sx = 0;
1588
1589
  let sy = 0;
@@ -1601,7 +1602,7 @@ function satStateAtEmission(sp3, prn, recvTimeMs, rcv) {
1601
1602
  const vz = (pPlus.z - pMinus.z) / 1;
1602
1603
  velDotPos = p.x * vx + p.y * vy + p.z * vz;
1603
1604
  }
1604
- clkS = p.clk;
1605
+ clkS = (clk && clkBias(clk, prn, tEmit)) ?? p.clk;
1605
1606
  const [rx, ry, rz] = sagnacRotate(p.x, p.y, p.z, travelS);
1606
1607
  sx = rx;
1607
1608
  sy = ry;
@@ -1715,7 +1716,7 @@ function solvePpp(epochs, sp3, opts) {
1715
1716
  for (const o of epoch.obs) {
1716
1717
  if (o.c1 === 0 || o.c2 === 0 || o.l1 === 0 || o.l2 === 0) continue;
1717
1718
  const slip = detectSlip(o, ei);
1718
- const sat = satStateAtEmission(sp3, o.prn, epoch.timeMs, rcv);
1719
+ const sat = satStateAtEmission(sp3, o.prn, epoch.timeMs, rcv, opts.clk);
1719
1720
  if (!sat) continue;
1720
1721
  const [elRad, azRad] = getAer(
1721
1722
  sat.state.x,
@@ -1910,6 +1911,96 @@ function solvePpp(epochs, sp3, opts) {
1910
1911
  };
1911
1912
  }
1912
1913
 
1914
+ // src/positioning/ppp-ar.ts
1915
+ function wlWavelength(f1, f2) {
1916
+ return C_LIGHT / (f1 - f2);
1917
+ }
1918
+ function nlWavelength(f1, f2) {
1919
+ return C_LIGHT / (f1 + f2);
1920
+ }
1921
+ function resolvePppAmbiguities(sats, Qaif, opts = {}) {
1922
+ const wlThreshold = opts.wlThreshold ?? 0.25;
1923
+ const ratioThreshold = opts.ratioThreshold ?? 3;
1924
+ const kept = [];
1925
+ const nWl = [];
1926
+ const rejectedWl = [];
1927
+ for (let i = 0; i < sats.length; i++) {
1928
+ const s = sats[i];
1929
+ const wlFloat = s.mwCyc - (s.wlBiasCyc ?? 0);
1930
+ const r = Math.round(wlFloat);
1931
+ if (Math.abs(wlFloat - r) <= wlThreshold) {
1932
+ kept.push(i);
1933
+ nWl.push(r);
1934
+ } else {
1935
+ rejectedWl.push(s.prn);
1936
+ }
1937
+ }
1938
+ if (kept.length < 2) {
1939
+ return {
1940
+ fixed: false,
1941
+ ratio: 0,
1942
+ refPrn: null,
1943
+ sats: [],
1944
+ rejectedWl
1945
+ };
1946
+ }
1947
+ const n1Float = [];
1948
+ const lamNl = [];
1949
+ for (let k = 0; k < kept.length; k++) {
1950
+ const s = sats[kept[k]];
1951
+ const lNl = nlWavelength(s.f1, s.f2);
1952
+ lamNl.push(lNl);
1953
+ const n1 = s.aIF / lNl - s.f2 / (s.f1 - s.f2) * nWl[k] - (s.nlBiasCyc ?? 0);
1954
+ n1Float.push(n1);
1955
+ }
1956
+ let ref = 0;
1957
+ for (let k = 1; k < kept.length; k++) {
1958
+ const e = sats[kept[k]].elevDeg ?? -1;
1959
+ if (e > (sats[kept[ref]].elevDeg ?? -1)) ref = k;
1960
+ }
1961
+ const m = kept.length - 1;
1962
+ const sd = new Float64Array(m);
1963
+ const Qsd = new Float64Array(m * m);
1964
+ const others = [];
1965
+ for (let k = 0; k < kept.length; k++) if (k !== ref) others.push(k);
1966
+ const n = sats.length;
1967
+ const qN1 = (ka, kb) => Qaif[kept[ka] * n + kept[kb]] / (lamNl[ka] * lamNl[kb]);
1968
+ for (let a = 0; a < m; a++) {
1969
+ const ka = others[a];
1970
+ sd[a] = n1Float[ka] - n1Float[ref];
1971
+ for (let b = 0; b < m; b++) {
1972
+ const kb = others[b];
1973
+ Qsd[a * m + b] = qN1(ka, kb) - qN1(ka, ref) - qN1(ref, kb) + qN1(ref, ref);
1974
+ }
1975
+ }
1976
+ const res = lambdaSearch(sd, Qsd, m, 2);
1977
+ const ratio = res ? Math.min(res.ratio, 999.9) : 0;
1978
+ const fixed = !!res && ratio >= ratioThreshold;
1979
+ const refN1 = Math.round(n1Float[ref]);
1980
+ const sdFix = res?.candidates[0];
1981
+ const outSats = [];
1982
+ for (let k = 0; k < kept.length; k++) {
1983
+ const s = sats[kept[k]];
1984
+ let n1;
1985
+ if (k === ref) {
1986
+ n1 = refN1;
1987
+ } else {
1988
+ const a = others.indexOf(k);
1989
+ n1 = fixed && sdFix ? refN1 + Math.round(sdFix[a]) : Math.round(n1Float[k]);
1990
+ }
1991
+ const lNl = lamNl[k];
1992
+ const aIF = lNl * n1 + lNl * (s.f2 / (s.f1 - s.f2)) * nWl[k];
1993
+ outSats.push({ prn: s.prn, nWl: nWl[k], n1, aIF });
1994
+ }
1995
+ return {
1996
+ fixed,
1997
+ ratio,
1998
+ refPrn: sats[kept[ref]].prn,
1999
+ sats: outSats,
2000
+ rejectedWl
2001
+ };
2002
+ }
2003
+
1913
2004
  // src/positioning/index.ts
1914
2005
  var GM_GPS = 3986005e8;
1915
2006
  var F_REL = -4442807633e-19;
@@ -2156,6 +2247,9 @@ export {
2156
2247
  buildPppAntenna,
2157
2248
  createPppCorrections,
2158
2249
  solvePpp,
2250
+ wlWavelength,
2251
+ nlWavelength,
2252
+ resolvePppAmbiguities,
2159
2253
  satClockCorrection,
2160
2254
  ionoFree,
2161
2255
  solveSpp
@@ -114,7 +114,69 @@ function sp3Position(sp3, prn, tMs, order = 9) {
114
114
  return { x, y, z, clk };
115
115
  }
116
116
 
117
+ // src/rinex/clk.ts
118
+ var GPS_EPOCH_MS = Date.UTC(1980, 0, 6);
119
+ function parseClk(text) {
120
+ const sats = {};
121
+ let inData = false;
122
+ for (const line of text.split("\n")) {
123
+ if (!inData) {
124
+ if (line.includes("END OF HEADER")) inData = true;
125
+ continue;
126
+ }
127
+ if (line.length < 3 || line[0] !== "A" || line[1] !== "S") continue;
128
+ const p = line.slice(2).trim().split(/\s+/);
129
+ if (p.length < 9) continue;
130
+ const prn = p[0];
131
+ const y = +p[1];
132
+ const mo = +p[2];
133
+ const d = +p[3];
134
+ const h = +p[4];
135
+ const mi = +p[5];
136
+ const s = +p[6];
137
+ const bias = +p[8];
138
+ if (!Number.isFinite(bias)) continue;
139
+ const t = Date.UTC(y, mo - 1, d, h, mi, Math.floor(s)) + s % 1 * 1e3;
140
+ let e = sats[prn];
141
+ if (!e) {
142
+ e = { t: [], c: [] };
143
+ sats[prn] = e;
144
+ }
145
+ e.t.push(t);
146
+ e.c.push(bias);
147
+ }
148
+ let intervalSec = 30;
149
+ for (const e of Object.values(sats)) {
150
+ if (e.t.length >= 2) {
151
+ intervalSec = Math.round((e.t[1] - e.t[0]) / 1e3);
152
+ break;
153
+ }
154
+ }
155
+ return { sats, intervalSec };
156
+ }
157
+ function clkBias(clk, prn, tMs) {
158
+ const e = clk.sats[prn];
159
+ if (!e || e.t.length === 0) return null;
160
+ const { t, c } = e;
161
+ let lo = 0;
162
+ let hi = t.length - 1;
163
+ if (tMs < t[0] || tMs > t[hi]) return null;
164
+ while (lo < hi) {
165
+ const mid = lo + hi + 1 >> 1;
166
+ if (t[mid] <= tMs) lo = mid;
167
+ else hi = mid - 1;
168
+ }
169
+ if (t[lo] === tMs || lo === t.length - 1) return c[lo];
170
+ const t0 = t[lo];
171
+ const t1 = t[lo + 1];
172
+ if (t1 - t0 > clk.intervalSec * 1e3 * 4) return null;
173
+ const f = (tMs - t0) / (t1 - t0);
174
+ return c[lo] + f * (c[lo + 1] - c[lo]);
175
+ }
176
+
117
177
  export {
118
178
  parseSp3,
119
- sp3Position
179
+ sp3Position,
180
+ parseClk,
181
+ clkBias
120
182
  };
@@ -67,4 +67,30 @@ declare function sp3Position(sp3: Sp3File, prn: string, tMs: number, order?: num
67
67
  clk: number | null;
68
68
  } | null;
69
69
 
70
- export { type IonexGrid as I, type Sp3File as S, type Sp3Sample as a, parseSp3 as b, parseIonex as p, sp3Position as s };
70
+ /**
71
+ * RINEX CLOCK (.CLK) parser — high-rate (typically 30 s) precise satellite
72
+ * clock corrections, the accuracy-limiting term for PPP that SP3's 5-minute
73
+ * (linearly interpolated) clocks cannot match.
74
+ *
75
+ * Only `AS` (satellite) records are kept; `AR` (station) records — the bulk
76
+ * of a combined file — are skipped. Clock bias is in seconds.
77
+ */
78
+ interface ClkFile {
79
+ /** prn → time-sorted samples { t: GPS-scale ms, c: clock bias (s) }. */
80
+ sats: Record<string, {
81
+ t: number[];
82
+ c: number[];
83
+ }>;
84
+ /** Nominal sampling interval (s), inferred from the first satellite. */
85
+ intervalSec: number;
86
+ }
87
+ /** Parse a RINEX CLOCK file. */
88
+ declare function parseClk(text: string): ClkFile;
89
+ /**
90
+ * Satellite clock bias (seconds) at an arbitrary time by linear
91
+ * interpolation between the bracketing samples. Returns null outside the
92
+ * span or across a gap larger than 4 sampling intervals.
93
+ */
94
+ declare function clkBias(clk: ClkFile, prn: string, tMs: number): number | null;
95
+
96
+ export { type ClkFile as C, type IonexGrid as I, type Sp3File as S, type Sp3Sample as a, parseIonex as b, clkBias as c, parseSp3 as d, parseClk as p, sp3Position as s };
@@ -67,4 +67,30 @@ declare function sp3Position(sp3: Sp3File, prn: string, tMs: number, order?: num
67
67
  clk: number | null;
68
68
  } | null;
69
69
 
70
- export { type IonexGrid as I, type Sp3File as S, type Sp3Sample as a, parseSp3 as b, parseIonex as p, sp3Position as s };
70
+ /**
71
+ * RINEX CLOCK (.CLK) parser — high-rate (typically 30 s) precise satellite
72
+ * clock corrections, the accuracy-limiting term for PPP that SP3's 5-minute
73
+ * (linearly interpolated) clocks cannot match.
74
+ *
75
+ * Only `AS` (satellite) records are kept; `AR` (station) records — the bulk
76
+ * of a combined file — are skipped. Clock bias is in seconds.
77
+ */
78
+ interface ClkFile {
79
+ /** prn → time-sorted samples { t: GPS-scale ms, c: clock bias (s) }. */
80
+ sats: Record<string, {
81
+ t: number[];
82
+ c: number[];
83
+ }>;
84
+ /** Nominal sampling interval (s), inferred from the first satellite. */
85
+ intervalSec: number;
86
+ }
87
+ /** Parse a RINEX CLOCK file. */
88
+ declare function parseClk(text: string): ClkFile;
89
+ /**
90
+ * Satellite clock bias (seconds) at an arbitrary time by linear
91
+ * interpolation between the bracketing samples. Returns null outside the
92
+ * span or across a gap larger than 4 sampling intervals.
93
+ */
94
+ declare function clkBias(clk: ClkFile, prn: string, tMs: number): number | null;
95
+
96
+ export { type ClkFile as C, type IonexGrid as I, type Sp3File as S, type Sp3Sample as a, parseIonex as b, clkBias as c, parseSp3 as d, parseClk as p, sp3Position as s };