gnss-js 1.2.0 → 1.3.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
@@ -51,6 +51,16 @@ const { distance } = vincenty(
51
51
  // distance in meters, angles in radians
52
52
  ```
53
53
 
54
+ ### Visibility / pass prediction
55
+
56
+ ```ts
57
+ import { computeVisibility } from 'gnss-js/orbit';
58
+
59
+ const vis = computeVisibility(ephemerides, rxEcef, startMs, endMs, 300, 10);
60
+ // vis.passes: rise/peak/set per satellite; vis.pdop / vis.visibleCount
61
+ // per epoch; vis.elevation[prn]: elevation timeline for plotting.
62
+ ```
63
+
54
64
  ### Single-point positioning
55
65
 
56
66
  ```ts
@@ -0,0 +1,223 @@
1
+ // src/frames/index.ts
2
+ var H = (t, d, r, tDot, dDot, rDot, epoch) => ({ t, d, r, tDot, dDot, rDot, epoch });
3
+ var Z = [0, 0, 0];
4
+ var FROM_ITRF2020 = {
5
+ ITRF2014: H([-1.4, -0.9, 1.4], -0.42, Z, [0, -0.1, 0.2], 0, Z, 2015),
6
+ ITRF2008: H([0.2, 1, 3.3], -0.29, Z, [0, -0.1, 0.1], 0.03, Z, 2015),
7
+ ITRF2005: H([2.7, 0.1, -1.4], 0.65, Z, [0.3, -0.1, 0.1], 0.03, Z, 2015),
8
+ ITRF2000: H([-0.2, 0.8, -34.2], 2.25, Z, [0.1, 0, -1.7], 0.11, Z, 2015),
9
+ ITRF97: H(
10
+ [6.5, -3.9, -77.9],
11
+ 3.98,
12
+ [0, 0, 0.36],
13
+ [0.1, -0.6, -3.1],
14
+ 0.12,
15
+ [0, 0, 0.02],
16
+ 2015
17
+ ),
18
+ ITRF96: H(
19
+ [6.5, -3.9, -77.9],
20
+ 3.98,
21
+ [0, 0, 0.36],
22
+ [0.1, -0.6, -3.1],
23
+ 0.12,
24
+ [0, 0, 0.02],
25
+ 2015
26
+ ),
27
+ ITRF94: H(
28
+ [6.5, -3.9, -77.9],
29
+ 3.98,
30
+ [0, 0, 0.36],
31
+ [0.1, -0.6, -3.1],
32
+ 0.12,
33
+ [0, 0, 0.02],
34
+ 2015
35
+ ),
36
+ ITRF93: H(
37
+ [-65.8, 1.9, -71.3],
38
+ 4.47,
39
+ [-3.36, -4.33, 0.75],
40
+ [-2.8, -0.2, -2.3],
41
+ 0.12,
42
+ [-0.11, -0.19, 0.07],
43
+ 2015
44
+ ),
45
+ ITRF92: H(
46
+ [14.5, -1.9, -85.9],
47
+ 3.27,
48
+ [0, 0, 0.36],
49
+ [0.1, -0.6, -3.1],
50
+ 0.12,
51
+ [0, 0, 0.02],
52
+ 2015
53
+ ),
54
+ ITRF91: H(
55
+ [26.5, 12.1, -91.9],
56
+ 4.67,
57
+ [0, 0, 0.36],
58
+ [0.1, -0.6, -3.1],
59
+ 0.12,
60
+ [0, 0, 0.02],
61
+ 2015
62
+ ),
63
+ ITRF90: H(
64
+ [24.5, 8.1, -107.9],
65
+ 4.97,
66
+ [0, 0, 0.36],
67
+ [0.1, -0.6, -3.1],
68
+ 0.12,
69
+ [0, 0, 0.02],
70
+ 2015
71
+ ),
72
+ ITRF89: H(
73
+ [29.5, 32.1, -145.9],
74
+ 8.37,
75
+ [0, 0, 0.36],
76
+ [0.1, -0.6, -3.1],
77
+ 0.12,
78
+ [0, 0, 0.02],
79
+ 2015
80
+ ),
81
+ ITRF88: H(
82
+ [24.5, -3.9, -169.9],
83
+ 11.47,
84
+ [0.1, 0, 0.36],
85
+ [0.1, -0.6, -3.1],
86
+ 0.12,
87
+ [0, 0, 0.02],
88
+ 2015
89
+ ),
90
+ /** ITRF2020 → ETRF2020, EPSG:10572 (epoch-1989 variant) */
91
+ ETRF2020: H(Z, 0, Z, Z, 0, [0.086, 0.519, -0.753], 1989)
92
+ };
93
+ var ITRF2014_TO_ETRF2014 = H(Z, 0, Z, Z, 0, [0.085, 0.531, -0.77], 1989);
94
+ var ITRF2014_TO_ETRF2000 = H(
95
+ [54.7, 52.2, -74.1],
96
+ 2.12,
97
+ [1.701, 10.29, -16.632],
98
+ [0.1, 0.1, -1.9],
99
+ 0.11,
100
+ [0.081, 0.49, -0.792],
101
+ 2010
102
+ );
103
+ var ITRF2014_TO_NAD83_2011 = H(
104
+ [1005.3, -1909.21, -541.57],
105
+ 0.36891,
106
+ [-26.78138, 0.42027, -10.93206],
107
+ [0.79, -0.6, -1.44],
108
+ -0.07201,
109
+ [-0.06667, 0.75744, 0.05133],
110
+ 2010
111
+ );
112
+ var MAS_TO_RAD = Math.PI / (180 * 3600 * 1e3);
113
+ function applyHelmert(xyz, p, epoch, inverse = false) {
114
+ const dt = epoch - p.epoch;
115
+ const sign = inverse ? -1 : 1;
116
+ const tx = sign * (p.t[0] + p.tDot[0] * dt) / 1e3;
117
+ const ty = sign * (p.t[1] + p.tDot[1] * dt) / 1e3;
118
+ const tz = sign * (p.t[2] + p.tDot[2] * dt) / 1e3;
119
+ const d = sign * (p.d + p.dDot * dt) * 1e-9;
120
+ const rx = sign * (p.r[0] + p.rDot[0] * dt) * MAS_TO_RAD;
121
+ const ry = sign * (p.r[1] + p.rDot[1] * dt) * MAS_TO_RAD;
122
+ const rz = sign * (p.r[2] + p.rDot[2] * dt) * MAS_TO_RAD;
123
+ const [x, y, z] = xyz;
124
+ return [
125
+ x + tx + d * x - rz * y + ry * z,
126
+ y + ty + rz * x + d * y - rx * z,
127
+ z + tz - ry * x + rx * y + d * z
128
+ ];
129
+ }
130
+ var EDGES = [
131
+ ...Object.entries(FROM_ITRF2020).map(([to, p]) => ({
132
+ from: "ITRF2020",
133
+ to,
134
+ p
135
+ })),
136
+ { from: "ITRF2014", to: "ETRF2014", p: ITRF2014_TO_ETRF2014 },
137
+ { from: "ITRF2014", to: "ETRF2000", p: ITRF2014_TO_ETRF2000 },
138
+ { from: "ITRF2014", to: "NAD83(2011)", p: ITRF2014_TO_NAD83_2011 }
139
+ ];
140
+ var REFERENCE_FRAMES = [
141
+ "WGS84",
142
+ "ITRF2020",
143
+ "ITRF2014",
144
+ "ITRF2008",
145
+ "ITRF2005",
146
+ "ITRF2000",
147
+ "ITRF97",
148
+ "ITRF96",
149
+ "ITRF94",
150
+ "ITRF93",
151
+ "ITRF92",
152
+ "ITRF91",
153
+ "ITRF90",
154
+ "ITRF89",
155
+ "ITRF88",
156
+ "ETRF2020",
157
+ "ETRF2014",
158
+ "ETRF2000",
159
+ "NAD83(2011)"
160
+ ];
161
+ function resolveAlias(f) {
162
+ return f === "WGS84" ? "ITRF2020" : f;
163
+ }
164
+ function findPath(from, to) {
165
+ if (from === to) return [];
166
+ const queue = [from];
167
+ const cameFrom = /* @__PURE__ */ new Map();
168
+ const seen = /* @__PURE__ */ new Set([from]);
169
+ while (queue.length > 0) {
170
+ const cur = queue.shift();
171
+ for (const edge of EDGES) {
172
+ let next = null;
173
+ let inverse = false;
174
+ if (edge.from === cur) {
175
+ next = edge.to;
176
+ } else if (edge.to === cur) {
177
+ next = edge.from;
178
+ inverse = true;
179
+ }
180
+ if (next === null || seen.has(next)) continue;
181
+ seen.add(next);
182
+ cameFrom.set(next, { prev: cur, edge, inverse });
183
+ if (next === to) {
184
+ const path = [];
185
+ let node = to;
186
+ while (node !== from) {
187
+ const step = cameFrom.get(node);
188
+ path.unshift({ edge: step.edge, inverse: step.inverse });
189
+ node = step.prev;
190
+ }
191
+ return path;
192
+ }
193
+ queue.push(next);
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ function transformFrame(xyz, from, to, epoch) {
199
+ const src = resolveAlias(from);
200
+ const dst = resolveAlias(to);
201
+ const path = findPath(src, dst);
202
+ if (!path) {
203
+ throw new Error(`No transformation path from ${from} to ${to}`);
204
+ }
205
+ let out = [xyz[0], xyz[1], xyz[2]];
206
+ for (const { edge, inverse } of path) {
207
+ out = applyHelmert(out, edge.p, epoch, inverse);
208
+ }
209
+ return out;
210
+ }
211
+ function dateToEpoch(date) {
212
+ const year = date.getUTCFullYear();
213
+ const start = Date.UTC(year, 0, 1);
214
+ const end = Date.UTC(year + 1, 0, 1);
215
+ return year + (date.getTime() - start) / (end - start);
216
+ }
217
+
218
+ export {
219
+ applyHelmert,
220
+ REFERENCE_FRAMES,
221
+ transformFrame,
222
+ dateToEpoch
223
+ };
@@ -223,7 +223,8 @@ function selectEphemeris(ephemerides, prn, timeMs) {
223
223
  if (eph.prn !== prn) continue;
224
224
  const ephTime = eph.tocDate.getTime();
225
225
  const dt = Math.abs(timeMs - ephTime);
226
- if (dt > 4 * 3600 * 1e3) continue;
226
+ const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
227
+ if (dt > maxAge) continue;
227
228
  if (dt < bestDt) {
228
229
  bestDt = dt;
229
230
  best = eph;
@@ -331,13 +332,13 @@ function ephInfoToEphemeris(info) {
331
332
  return null;
332
333
  if (info.vx === void 0 || info.vy === void 0 || info.vz === void 0)
333
334
  return null;
334
- const now = new Date(info.lastReceived);
335
+ const moscow = new Date(info.lastReceived + 3 * 3600 * 1e3);
335
336
  const utcMidnight = Date.UTC(
336
- now.getUTCFullYear(),
337
- now.getUTCMonth(),
338
- now.getUTCDate()
337
+ moscow.getUTCFullYear(),
338
+ moscow.getUTCMonth(),
339
+ moscow.getUTCDate()
339
340
  );
340
- const tbSec = (info.tb ?? 0) * 900;
341
+ const tbSec = (info.tb ?? 0) * 60;
341
342
  const moscowOffset = 3 * 3600;
342
343
  const tocMs = utcMidnight + (tbSec - moscowOffset) * 1e3;
343
344
  return {
@@ -440,6 +441,65 @@ function computeLiveSkyPositions(ephemerides, rxPos, cn0Map) {
440
441
  }
441
442
  return result;
442
443
  }
444
+ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, elevationMaskDeg = 10) {
445
+ const stepMs = stepSec * 1e3;
446
+ const times = [];
447
+ for (let t = startMs; t <= endMs; t += stepMs) times.push(t);
448
+ const maskRad = elevationMaskDeg * Math.PI / 180;
449
+ const all = computeAllPositions(ephemerides, times, rxPos);
450
+ const elevation = {};
451
+ const visibleCount = new Array(times.length).fill(0);
452
+ const pdop = new Array(times.length).fill(null);
453
+ const visiblePerEpoch = times.map(() => []);
454
+ for (const prn of all.prns) {
455
+ const series = all.positions[prn];
456
+ elevation[prn] = series.map((p) => p ? p.el : null);
457
+ for (let i = 0; i < series.length; i++) {
458
+ const p = series[i];
459
+ if (p && p.el >= maskRad) {
460
+ visibleCount[i]++;
461
+ visiblePerEpoch[i].push({ az: p.az, el: p.el });
462
+ }
463
+ }
464
+ }
465
+ for (let i = 0; i < times.length; i++) {
466
+ const dop = computeDop(visiblePerEpoch[i]);
467
+ pdop[i] = dop ? dop.pdop : null;
468
+ }
469
+ const passes = [];
470
+ for (const prn of all.prns) {
471
+ const els = elevation[prn];
472
+ let start = -1;
473
+ let peakEl = -Infinity;
474
+ let peakTime = 0;
475
+ for (let i = 0; i <= els.length; i++) {
476
+ const el = i < els.length ? els[i] : null;
477
+ const above = el !== null && el !== void 0 && el >= maskRad;
478
+ if (above) {
479
+ if (start === -1) {
480
+ start = i;
481
+ peakEl = -Infinity;
482
+ }
483
+ if (el > peakEl) {
484
+ peakEl = el;
485
+ peakTime = times[i];
486
+ }
487
+ } else if (start !== -1) {
488
+ passes.push({
489
+ prn,
490
+ system: prn[0],
491
+ rise: times[start],
492
+ set: times[i - 1],
493
+ peakTime,
494
+ peakEl
495
+ });
496
+ start = -1;
497
+ }
498
+ }
499
+ }
500
+ passes.sort((a, b) => a.rise - b.rise);
501
+ return { times, elevation, visibleCount, pdop, passes };
502
+ }
443
503
 
444
504
  export {
445
505
  keplerPosition,
@@ -450,5 +510,7 @@ export {
450
510
  computeSatPosition,
451
511
  navTimesFromEph,
452
512
  computeAllPositions,
453
- computeLiveSkyPositions
513
+ ephInfoToEphemeris,
514
+ computeLiveSkyPositions,
515
+ computeVisibility
454
516
  };
@@ -0,0 +1,206 @@
1
+ import {
2
+ computeDop,
3
+ computeSatPosition,
4
+ ecefToAzEl
5
+ } from "./chunk-CC2R4JGS.js";
6
+ import {
7
+ C_LIGHT
8
+ } from "./chunk-W5WKEV7U.js";
9
+
10
+ // src/positioning/index.ts
11
+ var GM_GPS = 3986005e8;
12
+ var F_REL = -4442807633e-19;
13
+ function satClockCorrection(eph, tMs) {
14
+ if (eph.system === "R" || eph.system === "S") {
15
+ const dt2 = (tMs - eph.tocDate.getTime()) / 1e3;
16
+ return eph.tauN + eph.gammaN * dt2;
17
+ }
18
+ const k = eph;
19
+ const dt = (tMs - k.tocDate.getTime()) / 1e3;
20
+ const poly = k.af0 + k.af1 * dt + k.af2 * dt * dt;
21
+ const a = k.sqrtA * k.sqrtA;
22
+ const n = Math.sqrt(GM_GPS / (a * a * a)) + k.deltaN;
23
+ const tk = dt;
24
+ const Mk = k.m0 + n * tk;
25
+ let Ek = Mk;
26
+ for (let i = 0; i < 8; i++) {
27
+ Ek = Mk + k.e * Math.sin(Ek);
28
+ }
29
+ const rel = F_REL * k.e * k.sqrtA * Math.sin(Ek);
30
+ return poly + rel;
31
+ }
32
+ function ionoFree(p1, p2, f1, f2) {
33
+ const g = f1 * f1 / (f1 * f1 - f2 * f2);
34
+ return g * p1 - (g - 1) * p2;
35
+ }
36
+ function tropoDelay(elevationRad) {
37
+ const sinEl = Math.sin(elevationRad);
38
+ return 2.47 / (sinEl + 0.0121);
39
+ }
40
+ function sagnac(pos, travelTimeS) {
41
+ const OMEGA_E = 72921151467e-15;
42
+ const theta = OMEGA_E * travelTimeS;
43
+ const c = Math.cos(theta);
44
+ const s = Math.sin(theta);
45
+ return [pos.x * c + pos.y * s, -pos.x * s + pos.y * c, pos.z];
46
+ }
47
+ function solveLinear(A, b) {
48
+ const n = b.length;
49
+ const M = A.map((row, i) => [...row, b[i]]);
50
+ for (let col = 0; col < n; col++) {
51
+ let pivot = col;
52
+ for (let r = col + 1; r < n; r++) {
53
+ if (Math.abs(M[r][col]) > Math.abs(M[pivot][col])) pivot = r;
54
+ }
55
+ if (Math.abs(M[pivot][col]) < 1e-12) return null;
56
+ [M[col], M[pivot]] = [M[pivot], M[col]];
57
+ for (let r = 0; r < n; r++) {
58
+ if (r === col) continue;
59
+ const f = M[r][col] / M[col][col];
60
+ for (let c = col; c <= n; c++) M[r][c] -= f * M[col][c];
61
+ }
62
+ }
63
+ return M.map((row, i) => row[n] / M[i][i]);
64
+ }
65
+ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
66
+ const {
67
+ elevationMaskDeg = 10,
68
+ troposphere = true,
69
+ maxIterations = 15,
70
+ convergenceM = 1e-4,
71
+ tgd = true
72
+ } = opts;
73
+ const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
74
+ const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
75
+ if (all.length < minSats(all)) return null;
76
+ const gaussNewton = (prns) => {
77
+ const systems = [...new Set(prns.map((p) => p[0]))].sort();
78
+ const dim = 3 + systems.length;
79
+ const sysIndex = new Map(systems.map((s, i) => [s, 3 + i]));
80
+ let x2 = 0;
81
+ let y2 = 0;
82
+ let z2 = 0;
83
+ const clock2 = new Map(systems.map((s) => [s, 0]));
84
+ const residuals2 = {};
85
+ let iterations2 = 0;
86
+ let converged2 = false;
87
+ for (let iter = 0; iter < maxIterations; iter++) {
88
+ iterations2 = iter + 1;
89
+ const HtH = Array.from(
90
+ { length: dim },
91
+ () => new Array(dim).fill(0)
92
+ );
93
+ const Htv = new Array(dim).fill(0);
94
+ let rows = 0;
95
+ for (const k of Object.keys(residuals2)) delete residuals2[k];
96
+ for (const prn of prns) {
97
+ const psr = pseudoranges.get(prn);
98
+ const eph = ephemerides.get(prn);
99
+ const sys = prn[0];
100
+ const clk = clock2.get(sys);
101
+ const tTx = timeMs - psr / C_LIGHT * 1e3;
102
+ const dtsClock = satClockCorrection(eph, tTx);
103
+ const sat = computeSatPosition(eph, tTx - dtsClock * 1e3);
104
+ if (!Number.isFinite(sat.x)) continue;
105
+ const isKepler = eph.system !== "R" && eph.system !== "S";
106
+ const dts = dtsClock - (tgd && isKepler ? eph.tgd : 0);
107
+ const travel = Math.hypot(sat.x - x2, sat.y - y2, sat.z - z2) / C_LIGHT;
108
+ const [sx, sy, sz] = sagnac(sat, travel);
109
+ const rho = Math.hypot(sx - x2, sy - y2, sz - z2);
110
+ const ux = (x2 - sx) / rho;
111
+ const uy = (y2 - sy) / rho;
112
+ const uz = (z2 - sz) / rho;
113
+ let elev = Math.PI / 2;
114
+ let weight = 1;
115
+ const positionSane = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
116
+ if (positionSane) {
117
+ const azel = ecefToAzEl(x2, y2, z2, sx, sy, sz);
118
+ elev = azel.el;
119
+ if (iter >= 2 && elev < elevationMaskDeg * Math.PI / 180) continue;
120
+ const sinEl = Math.max(Math.sin(elev), 0.1);
121
+ weight = sinEl * sinEl;
122
+ }
123
+ const tropo = troposphere && positionSane ? tropoDelay(elev) : 0;
124
+ const predicted = rho + clk - C_LIGHT * dts + tropo;
125
+ const v = psr - predicted;
126
+ const h = new Array(dim).fill(0);
127
+ h[0] = ux;
128
+ h[1] = uy;
129
+ h[2] = uz;
130
+ h[sysIndex.get(sys)] = 1;
131
+ for (let i = 0; i < dim; i++) {
132
+ for (let j = 0; j < dim; j++) HtH[i][j] += weight * h[i] * h[j];
133
+ Htv[i] += weight * h[i] * v;
134
+ }
135
+ residuals2[prn] = v;
136
+ rows++;
137
+ }
138
+ if (rows < dim) return null;
139
+ const dx = solveLinear(HtH, Htv);
140
+ if (!dx) return null;
141
+ x2 += dx[0];
142
+ y2 += dx[1];
143
+ z2 += dx[2];
144
+ for (const s of systems)
145
+ clock2.set(s, clock2.get(s) + dx[sysIndex.get(s)]);
146
+ if (Math.hypot(dx[0], dx[1], dx[2]) < convergenceM) {
147
+ converged2 = true;
148
+ break;
149
+ }
150
+ }
151
+ return { x: x2, y: y2, z: z2, clock: clock2, residuals: residuals2, iterations: iterations2, converged: converged2 };
152
+ };
153
+ const REJECT_THRESHOLD_M = 50;
154
+ let candidates = all;
155
+ const rejected = [];
156
+ let inner = null;
157
+ for (let round = 0; round <= all.length; round++) {
158
+ inner = gaussNewton(candidates);
159
+ if (!inner) return null;
160
+ const vals = Object.entries(inner.residuals);
161
+ let worst = null;
162
+ let worstAbs = REJECT_THRESHOLD_M;
163
+ for (const [prn, v] of vals) {
164
+ if (Math.abs(v) > worstAbs) {
165
+ worstAbs = Math.abs(v);
166
+ worst = prn;
167
+ }
168
+ }
169
+ if (!worst) break;
170
+ const remaining = candidates.filter((p) => p !== worst);
171
+ if (remaining.length < minSats(remaining)) break;
172
+ rejected.push(worst);
173
+ candidates = remaining;
174
+ }
175
+ if (!inner) return null;
176
+ const { x, y, z, clock, residuals, iterations, converged } = inner;
177
+ const used = [];
178
+ const azels = [];
179
+ for (const prn of candidates) {
180
+ if (!(prn in residuals)) continue;
181
+ const eph = ephemerides.get(prn);
182
+ const sat = computeSatPosition(eph, timeMs);
183
+ if (!Number.isFinite(sat.x)) continue;
184
+ const azel = ecefToAzEl(x, y, z, sat.x, sat.y, sat.z);
185
+ if (azel.el >= elevationMaskDeg * Math.PI / 180) {
186
+ used.push(prn);
187
+ azels.push(azel);
188
+ }
189
+ }
190
+ return {
191
+ position: [x, y, z],
192
+ clockBias: Object.fromEntries(clock),
193
+ usedSatellites: used,
194
+ rejectedSatellites: rejected,
195
+ residuals,
196
+ dop: computeDop(azels),
197
+ iterations,
198
+ converged
199
+ };
200
+ }
201
+
202
+ export {
203
+ satClockCorrection,
204
+ ionoFree,
205
+ solveSpp
206
+ };