gnss-js 1.22.0 → 1.24.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.
@@ -34,6 +34,405 @@ interface KlobucharCoeffs {
34
34
  */
35
35
  declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad: number, azRad: number, elRad: number, gpsTowSec: number): number;
36
36
 
37
+ /**
38
+ * RTK positioning, stage 1: epoch-wise double-differenced (DD)
39
+ * processing of a base/rover receiver pair.
40
+ *
41
+ * Two solvers share the same DD construction:
42
+ *
43
+ * - `solveDgnss` — code-based DGNSS: between-receiver single
44
+ * differences on pseudoranges, double-differenced against a
45
+ * reference satellite (highest elevation per signal group), iterated
46
+ * weighted least squares for the rover position with the full DD
47
+ * covariance (DDs sharing a reference satellite are correlated).
48
+ *
49
+ * - `RtkFloatEngine` — a stateful extended Kalman filter over DD
50
+ * carrier phase + code with one float DD ambiguity state per
51
+ * satellite/signal. An EKF (rather than epoch-wise iterated least
52
+ * squares with appended ambiguity columns) was chosen because the
53
+ * float ambiguities must persist across epochs anyway, and stage 2
54
+ * (LAMBDA) needs the joint float ambiguity covariance — which the
55
+ * EKF maintains natively. This mirrors RTKLIB's float-filter
56
+ * structure (rtkpos.c), which the implementation was validated
57
+ * against.
58
+ *
59
+ * Stage 2 — integer ambiguity resolution (opt-in via
60
+ * `ambiguityResolution: 'instant'`): after each float update the
61
+ * engine extracts the DD ambiguity subvector and its joint covariance
62
+ * from the filter, runs MLAMBDA (`lambdaSearch`, a port of RTKLIB's
63
+ * lambda.c) and validates the best candidate with the ratio test
64
+ * (s₂/s₁ ≥ `ratioThreshold`, default 3.0 — RTKLIB's default). On
65
+ * acceptance the rover position is conditioned on the integer
66
+ * ambiguities (xa = x − P_xb·Q_b⁻¹·(b − ǎ), RTKLIB rtkpos.c
67
+ * `resamb_LAMBDA`) and reported as a FIXED solution; the filter state
68
+ * itself is *not* modified (instant, per-epoch fixing — RTKLIB's
69
+ * `armode=instantaneous`; continuous AR/fix-and-hold, which feed the
70
+ * fix back into the filter, are stage-3 continuity options). When the
71
+ * full set fails the ratio test, partial fixing retries after
72
+ * dropping the lowest-elevation ambiguity, one at a time, down to
73
+ * half the candidate set (and never below `minFixAmbiguities`) — an
74
+ * elevation-ordered subset walk in the spirit of RTKLIB demo5's
75
+ * satellite exclusion, floored so an unconverged float cannot buy a
76
+ * lucky ratio on a tiny subset. GLONASS ambiguities are excluded from
77
+ * fixing by
78
+ * default (DD IFB is only integer-safe across same-model receivers;
79
+ * enable with `glonassAr`).
80
+ *
81
+ * Differencing model: for satellites s and reference r observed by
82
+ * rover R and base B, the DD pseudorange/phase cancels satellite
83
+ * clocks (single difference) and receiver clocks (double difference).
84
+ * On short baselines the ionospheric and most of the tropospheric
85
+ * delay cancel too; the residual troposphere is modelled by the same
86
+ * simple elevation-mapped zenith model `solveSpp` uses, applied
87
+ * differentially. DDs are formed per *signal group* (constellation
88
+ * letter + RINEX code, e.g. `G1C`, `C2I`) so that only like signals
89
+ * are differenced.
90
+ *
91
+ * GLONASS (FDMA): DD phase is formed in metres with per-satellite
92
+ * wavelengths (λ_s·SDφ_s − λ_r·SDφ_r). The common receiver clock
93
+ * term cancels exactly (λ·f = c on every channel); the per-channel
94
+ * receiver phase bias (inter-frequency bias, IFB) does not, but it is
95
+ * constant while lock holds and is absorbed into the float DD
96
+ * ambiguity — the standard reason GLONASS float works while GLONASS
97
+ * integer fixing is hard. DD *code* retains the differential
98
+ * inter-channel code bias between the two receivers, which is small
99
+ * (well below the code noise) for same-model receivers — IFB-safe
100
+ * enough for stage 1, and documented as such.
101
+ *
102
+ * Time convention: `timeMs` is GPS-scale epoch milliseconds, exactly
103
+ * as produced by `parseNovatelRange` / the RINEX and RTCM parsers —
104
+ * the same convention `solveSpp` uses. No system clock is read.
105
+ *
106
+ * Adapting decoder output (see `toRtkEpoch`):
107
+ * - NovAtel: `parseNovatelRange(bytes).epochs[i]` gives
108
+ * `{ timeMs, meas }`; `toRtkEpoch(meas)` picks each satellite's
109
+ * L1-band measurement. When a log carries both RANGE and RANGECMP
110
+ * the same epoch appears twice — keep one epoch per `timeMs`.
111
+ * - RTCM MSM: for each satellite/signal cell build
112
+ * `{ code, pr: pseudorangeM, cp: phaserangeM / lambda, lockTimeMs }`
113
+ * (MSM phaseranges are metres; divide by the carrier wavelength to
114
+ * get cycles) and pass a `prn → measurement` map for the epoch.
115
+ */
116
+
117
+ /** One satellite's measurement on one signal, for one epoch. */
118
+ interface RtkMeasurement {
119
+ /** RINEX band+attribute, e.g. "1C", "2I". */
120
+ code: string;
121
+ /** Pseudorange (m). */
122
+ pr: number | null;
123
+ /** Carrier phase (cycles, RINEX sign), optional — DGNSS needs none. */
124
+ cp?: number | null;
125
+ /** Continuous lock time (ms) for cycle-slip detection, optional. */
126
+ lockTimeMs?: number;
127
+ /** GLONASS frequency channel k (−7…+6), for FDMA wavelengths. */
128
+ gloChannel?: number | null;
129
+ }
130
+ /** One epoch of measurements: PRN → measurement (one signal per PRN). */
131
+ type RtkEpochMeasurements = ReadonlyMap<string, RtkMeasurement>;
132
+ /** Decoder-shaped observation (structurally `NovatelMeasurement`). */
133
+ interface RawObservation {
134
+ prn: string;
135
+ code: string;
136
+ pr: number | null;
137
+ cp?: number | null;
138
+ lockTimeS?: number;
139
+ gloChannel?: number | null;
140
+ }
141
+ /**
142
+ * Adapt one epoch of decoder observations (e.g.
143
+ * `parseNovatelRange(...).epochs[i].meas`) to the RTK input map:
144
+ * keeps each satellite's preferred L1-band code measurement with a
145
+ * valid pseudorange, drops unsupported systems (SBAS, NavIC).
146
+ */
147
+ declare function toRtkEpoch(meas: readonly RawObservation[]): Map<string, RtkMeasurement>;
148
+ /** Ephemeris source: a per-PRN map, or a list searched per epoch. */
149
+ type EphemerisSource = ReadonlyMap<string, Ephemeris> | readonly Ephemeris[];
150
+ interface DgnssOptions {
151
+ /** Elevation mask at the base, degrees. Default 10. */
152
+ elevationMaskDeg?: number;
153
+ /** Model the differential troposphere. Default true. */
154
+ troposphere?: boolean;
155
+ /** Maximum Gauss-Newton iterations. Default 15. */
156
+ maxIterations?: number;
157
+ /** Convergence threshold on the position update (m). Default 1e-4. */
158
+ convergenceM?: number;
159
+ /** Zenith code sigma per receiver (m). Default 0.3. */
160
+ codeSigmaM?: number;
161
+ /** Worst-first DD residual rejection threshold (m). Default 20. */
162
+ rejectThresholdM?: number;
163
+ /** Initial rover position; defaults to the base position. */
164
+ initialPosition?: [number, number, number];
165
+ }
166
+ interface DgnssSolution {
167
+ /** Rover position, ECEF metres. */
168
+ position: [number, number, number];
169
+ /** Baseline rover − base, ECEF metres. */
170
+ baseline: [number, number, number];
171
+ /** Formal position sigmas from the LSQ covariance, ECEF metres. */
172
+ sigmas: [number, number, number];
173
+ /** Satellites used (including references). */
174
+ usedSatellites: string[];
175
+ /** Reference satellite per signal group. */
176
+ refSatellites: Record<string, string>;
177
+ /** Post-fit DD residual (m) per non-reference PRN. */
178
+ residuals: Record<string, number>;
179
+ /** Satellites rejected by the residual screen. */
180
+ rejectedSatellites: string[];
181
+ nSats: number;
182
+ iterations: number;
183
+ converged: boolean;
184
+ }
185
+ /**
186
+ * Solve the rover position from one epoch of DD pseudoranges.
187
+ *
188
+ * @param rover Rover measurements (PRN → measurement).
189
+ * @param base Base measurements (PRN → measurement, same codes).
190
+ * @param basePos Known base position, ECEF metres.
191
+ * @param ephemerides Broadcast ephemerides (per-PRN map, or a list
192
+ * searched by closest epoch — pass `parseNovatelNav(...).ephemerides`
193
+ * or `parseNavFile(...).ephemerides` directly).
194
+ * @param timeMs Receiver epoch, GPS-scale milliseconds.
195
+ */
196
+ declare function solveDgnss(rover: RtkEpochMeasurements, base: RtkEpochMeasurements, basePos: readonly [number, number, number], ephemerides: EphemerisSource, timeMs: number, opts?: DgnssOptions): DgnssSolution | null;
197
+ interface RtkFloatOptions {
198
+ /**
199
+ * 'static': the rover position is a constant state (random walk
200
+ * `processNoisePosM`·√s, default 0). 'kinematic': the position is
201
+ * re-initialised each epoch with `kinematicSigmaM` uncertainty
202
+ * (no velocity states at stage 1 — RTKLIB's default kinematic
203
+ * handling without doppler). Default 'kinematic'.
204
+ */
205
+ mode?: 'static' | 'kinematic';
206
+ /** Elevation mask at the base, degrees. Default 10. */
207
+ elevationMaskDeg?: number;
208
+ /** Model the differential troposphere. Default true. */
209
+ troposphere?: boolean;
210
+ /** Zenith code sigma per receiver (m). Default 0.3. */
211
+ codeSigmaM?: number;
212
+ /** Zenith phase sigma per receiver (m). Default 0.003. */
213
+ phaseSigmaM?: number;
214
+ /** Static-mode position random walk (m/√s). Default 0. */
215
+ processNoisePosM?: number;
216
+ /** Kinematic per-epoch position reset sigma (m). Default 30. */
217
+ kinematicSigmaM?: number;
218
+ /** Ambiguity random walk (cycles/√s). Default 1e-4. */
219
+ ambProcessNoiseCycles?: number;
220
+ /** New float ambiguity sigma (cycles). Default 30. */
221
+ ambInitSigmaCycles?: number;
222
+ /** Drop ambiguity states unseen for this long (ms). Default 10000. */
223
+ maxGapMs?: number;
224
+ /** DD code innovation gate (m): drop the satellite. Default 30. */
225
+ codeGateM?: number;
226
+ /**
227
+ * Phase-minus-code innovation divergence gate (m): treat as an
228
+ * undetected cycle slip and re-initialise the ambiguity. Default 5.
229
+ */
230
+ slipGateM?: number;
231
+ /** Measurement-update relinearisations (IEKF). Default 2. */
232
+ updateIterations?: number;
233
+ /**
234
+ * Integer ambiguity resolution: 'off' keeps the stage-1 float-only
235
+ * behaviour; 'instant' attempts a per-epoch LAMBDA fix after each
236
+ * float update (no state feedback — RTKLIB `armode=instantaneous`).
237
+ * Default 'off'.
238
+ */
239
+ ambiguityResolution?: 'off' | 'instant';
240
+ /** Ratio-test acceptance threshold (s₂/s₁). Default 3.0 (RTKLIB). */
241
+ ratioThreshold?: number;
242
+ /**
243
+ * Additional elevation mask (deg) for ambiguities entering the fix
244
+ * (RTKLIB `elmaskar`); 0 = use every measured ambiguity. Default 0.
245
+ */
246
+ arElevationMaskDeg?: number;
247
+ /**
248
+ * Include GLONASS FDMA ambiguities in the integer fix. Only sound
249
+ * across same-model receivers (differential IFB). Default false.
250
+ */
251
+ glonassAr?: boolean;
252
+ /**
253
+ * Partial fixing: when the full ambiguity set fails the ratio test,
254
+ * drop the lowest-elevation ambiguity and retry (elevation-ordered
255
+ * subset walk), never below half the candidates. Default true.
256
+ */
257
+ partialFixing?: boolean;
258
+ /** Minimum DD ambiguities to attempt/accept a fix. Default 4. */
259
+ minFixAmbiguities?: number;
260
+ }
261
+ interface RtkFloatSolution {
262
+ /** Epoch, GPS-scale milliseconds. */
263
+ timeMs: number;
264
+ /**
265
+ * Rover position, ECEF metres — the ambiguity-fixed position when
266
+ * `status === 'fixed'`, the float filter position otherwise.
267
+ */
268
+ position: [number, number, number];
269
+ /** Float baseline rover − base, ECEF metres (always the float). */
270
+ floatBaseline: [number, number, number];
271
+ /**
272
+ * Solution quality: 'fixed' when LAMBDA passed the ratio test,
273
+ * 'float' when carrier ambiguities contributed but no (accepted)
274
+ * fix, 'dgnss' when the epoch was updated from code DDs only.
275
+ */
276
+ status: 'fixed' | 'float' | 'dgnss';
277
+ /** Satellites contributing DD rows this epoch (incl. references). */
278
+ nSats: number;
279
+ /**
280
+ * Ambiguity-validation ratio (s₂/s₁ of the LAMBDA candidates,
281
+ * capped at 999.9 as RTKLIB): the accepted subset's ratio when
282
+ * fixed, the full-set ratio otherwise. Undefined when no fix was
283
+ * attempted (`ambiguityResolution: 'off'` or no eligible set).
284
+ */
285
+ ratio?: number;
286
+ /** Number of integer-fixed DD ambiguities (when `status==='fixed'`). */
287
+ nFixed?: number;
288
+ /** Fixed baseline rover − base, ECEF metres (when fixed). */
289
+ fixedBaseline?: [number, number, number];
290
+ /** Integer DD ambiguity (cycles) per fixed PRN (when fixed). */
291
+ fixedAmbiguities?: Record<string, number>;
292
+ /** Formal position sigmas (filter covariance), ECEF metres. */
293
+ sigmas: [number, number, number];
294
+ /** Float DD ambiguity (cycles) per non-reference PRN. */
295
+ ambiguities: Record<string, number>;
296
+ /** Reference satellite per signal group. */
297
+ refSatellites: Record<string, string>;
298
+ }
299
+ /**
300
+ * Epoch-by-epoch float RTK filter: EKF states = rover position
301
+ * (static or kinematic) + one float DD ambiguity (cycles) per
302
+ * satellite/signal. Cycle slips are handled via lock-time regressions
303
+ * (LLI-style resets) and a phase/code divergence gate; reference-
304
+ * satellite switches re-map the DD ambiguity states algebraically
305
+ * (N_i' = N_i − N_r', old reference gains −N_r') so no information is
306
+ * lost. Feed epochs in time order via `process`.
307
+ */
308
+ declare class RtkFloatEngine {
309
+ private readonly basePos;
310
+ private readonly ephemerides;
311
+ private readonly o;
312
+ /** State vector [x, y, z, N₁…] and covariance; null before init. */
313
+ private x;
314
+ private P;
315
+ private amb;
316
+ private refs;
317
+ private track;
318
+ private lastMs;
319
+ constructor(basePos: readonly [number, number, number], ephemerides: EphemerisSource, opts?: RtkFloatOptions);
320
+ /** Clear all filter state (position, ambiguities, lock history). */
321
+ reset(): void;
322
+ private ambIndex;
323
+ /** Remove one ambiguity state (row/col) from x and P. */
324
+ private dropAmb;
325
+ /** Append an ambiguity state with the given value and variance. */
326
+ private addAmb;
327
+ /**
328
+ * Re-map a group's DD ambiguities from the old reference r to the
329
+ * new reference r' (which must hold a state): N_i' = N_i − N_r' and
330
+ * the old reference's slot becomes N_r = −N_r'. Exact linear
331
+ * transform of state and covariance (P' = T P Tᵀ).
332
+ */
333
+ private retarget;
334
+ /**
335
+ * Try to fix the ambiguity subset `idxs` (indices into `this.amb`):
336
+ * extract the float subvector b and covariance Q_b from the filter,
337
+ * run MLAMBDA and, without touching the filter state, condition the
338
+ * position on the best integer candidate,
339
+ * xa = x − P_xb·Q_b⁻¹·(b − ǎ)
340
+ * (RTKLIB rtkpos.c `resamb_LAMBDA`). Returns null when Q_b is not
341
+ * positive definite / invertible or the search fails; ratio
342
+ * validation is the caller's job.
343
+ */
344
+ private tryFix;
345
+ /**
346
+ * Process one synchronized epoch (same nominal `timeMs` for both
347
+ * receivers). Returns null until a first position can be
348
+ * initialised (via an internal DGNSS solve) or when the epoch has
349
+ * fewer than 3 usable DD rows.
350
+ */
351
+ process(rover: RtkEpochMeasurements, base: RtkEpochMeasurements, timeMs: number): RtkFloatSolution | null;
352
+ }
353
+
354
+ /**
355
+ * Integer ambiguity resolution: LAMBDA decorrelation + MLAMBDA
356
+ * integer least-squares search.
357
+ *
358
+ * Port of RTKLIB's `lambda.c` (demo5 / rtklibexplorer fork,
359
+ * src/lambda.c, Copyright (c) 2007-2008 T. Takasu, BSD-2-Clause),
360
+ * cross-checked against the reference papers where the C comments are
361
+ * thin:
362
+ *
363
+ * - [1] P.J.G. Teunissen, "The least-squares ambiguity decorrelation
364
+ * adjustment: a method for fast GPS ambiguity estimation",
365
+ * J. Geodesy 70, 65-82, 1995.
366
+ * - [2] X.-W. Chang, X. Yang, T. Zhou, "MLAMBDA: A modified LAMBDA
367
+ * method for integer least-squares estimation", J. Geodesy 79,
368
+ * 552-565, 2005.
369
+ *
370
+ * Pipeline (identical to RTKLIB's `lambda()`):
371
+ * 1. LᵀDL factorization of the float covariance, Q = Lᵀ·diag(D)·L
372
+ * with L unit lower triangular (RTKLIB's `LD`).
373
+ * 2. LAMBDA reduction [1]: integer Gauss transformations interleaved
374
+ * with symmetric permutations until the transformed D is
375
+ * (approximately) descending — producing a unimodular Z with
376
+ * Qz = ZᵀQZ = Lᵀ·diag(D)·L far better conditioned than Q.
377
+ * 3. MLAMBDA search [2] in the decorrelated space: depth-first
378
+ * enumeration over a shrinking ellipsoid, keeping the `m` smallest
379
+ * residual quadratic forms (z−ž)ᵀQz⁻¹(z−ž).
380
+ * 4. Back-transform the candidates to the original space, a = Z⁻ᵀ·ž
381
+ * (RTKLIB solves ZᵀF = E; Z is unimodular so F is exactly integer).
382
+ *
383
+ * Deviations from lambda.c:
384
+ * - matrices are `Float64Array` in the same column-major layout, with
385
+ * allocation-failure paths dropped (JS throws instead),
386
+ * - failures (non-positive-definite Q, search loop overflow) return
387
+ * `null` instead of a status code + stderr message,
388
+ * - back-transformed candidates are rounded to the nearest integer:
389
+ * F = Z⁻ᵀ·ž is exactly integer in ℤ arithmetic and the rounding only
390
+ * removes the ~1e-12 fuzz of the floating-point triangular solve,
391
+ * - the ratio s₂/s₁ used by the RTKLIB caller (`rtkpos.c`) is returned
392
+ * alongside the candidates (∞ when s₁ = 0, i.e. noise-free input).
393
+ */
394
+ /** Result of an integer least-squares search. */
395
+ interface LambdaResult {
396
+ /**
397
+ * The `m` best integer candidate vectors (length n each), sorted by
398
+ * ascending residual quadratic form; `candidates[0]` is the integer
399
+ * least-squares minimizer.
400
+ */
401
+ candidates: Float64Array[];
402
+ /**
403
+ * Residual quadratic forms sᵢ = (a−ǎᵢ)ᵀQ⁻¹(a−ǎᵢ) matching
404
+ * `candidates`, ascending.
405
+ */
406
+ residuals: Float64Array;
407
+ /**
408
+ * Ratio-test statistic s₂/s₁ (second-best over best); `Infinity`
409
+ * when the best residual is exactly 0 (noise-free input). Callers
410
+ * typically cap it (RTKLIB uses 999.9) before thresholding.
411
+ */
412
+ ratio: number;
413
+ }
414
+ /**
415
+ * LAMBDA/MLAMBDA integer least-squares estimation (RTKLIB `lambda`):
416
+ * decorrelate the float ambiguities, search the `m` best integer
417
+ * vectors and back-transform them to the original space.
418
+ *
419
+ * @param a Float ambiguities (length ≥ n).
420
+ * @param Q Their covariance, n×n (symmetric — row/column-major agree).
421
+ * @param n Number of ambiguities.
422
+ * @param m Number of candidates to return (default 2: best +
423
+ * second-best, what the ratio test needs).
424
+ * @returns Candidates + residuals + ratio, or null when Q is not
425
+ * positive definite or the search does not terminate.
426
+ */
427
+ declare function lambdaSearch(a: Float64Array, Q: Float64Array, n: number, m?: number): LambdaResult | null;
428
+ /**
429
+ * LAMBDA reduction only (RTKLIB `lambda_reduction`): the unimodular
430
+ * decorrelation matrix Z (n×n, column-major) such that Qz = ZᵀQZ is
431
+ * (near-)diagonally dominant. Exposed for diagnostics/tests; returns
432
+ * null when Q is not positive definite.
433
+ */
434
+ declare function lambdaReduction(Q: Float64Array, n: number): Float64Array | null;
435
+
37
436
  /**
38
437
  * Single-point positioning (SPP) from pseudoranges and broadcast
39
438
  * ephemerides.
@@ -114,4 +513,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
114
513
  */
115
514
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
116
515
 
117
- export { type KlobucharCoeffs, type SppOptions, type SppSolution, ionoFree, klobucharDelay, satClockCorrection, solveSpp };
516
+ export { type DgnssOptions, type DgnssSolution, type EphemerisSource, type KlobucharCoeffs, type LambdaResult, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type SppOptions, type SppSolution, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, satClockCorrection, solveDgnss, solveSpp, toRtkEpoch };
@@ -1,18 +1,28 @@
1
1
  import {
2
+ RtkFloatEngine,
2
3
  ionoFree,
3
4
  klobucharDelay,
5
+ lambdaReduction,
6
+ lambdaSearch,
4
7
  satClockCorrection,
5
- solveSpp
6
- } from "./chunk-B5LU4746.js";
7
- import "./chunk-JNFV5BYB.js";
8
+ solveDgnss,
9
+ solveSpp,
10
+ toRtkEpoch
11
+ } from "./chunk-T2ZMNRCY.js";
12
+ import "./chunk-MHGNKSSH.js";
8
13
  import "./chunk-37QNKGTC.js";
9
14
  import "./chunk-6FAL6P4G.js";
10
15
  import "./chunk-HVXYFUCB.js";
11
16
  import "./chunk-LEEU5OIO.js";
12
17
  import "./chunk-FIEWO4J4.js";
13
18
  export {
19
+ RtkFloatEngine,
14
20
  ionoFree,
15
21
  klobucharDelay,
22
+ lambdaReduction,
23
+ lambdaSearch,
16
24
  satClockCorrection,
17
- solveSpp
25
+ solveDgnss,
26
+ solveSpp,
27
+ toRtkEpoch
18
28
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.22.0",
3
+ "version": "1.24.0",
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,