gnss-js 2.0.0 → 2.1.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.
@@ -21,8 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var positioning_exports = {};
22
22
  __export(positioning_exports, {
23
23
  IONO_L1_M_PER_TECU: () => IONO_L1_M_PER_TECU,
24
+ PppEngine: () => PppEngine,
24
25
  RtkFloatEngine: () => RtkFloatEngine,
25
26
  SbasProcessor: () => SbasProcessor,
27
+ Sp3EphemerisSource: () => Sp3EphemerisSource,
28
+ applyOrbitClock: () => applyOrbitClock,
26
29
  buildPppAntenna: () => buildPppAntenna,
27
30
  createPppCorrections: () => createPppCorrections,
28
31
  estimateNarrowlaneFcb: () => estimateNarrowlaneFcb,
@@ -33,6 +36,7 @@ __export(positioning_exports, {
33
36
  gimSlantIonoDelayL1: () => gimSlantIonoDelayL1,
34
37
  gimVerticalTec: () => gimVerticalTec,
35
38
  gmst: () => gmst,
39
+ hasToOrbitClock: () => hasToOrbitClock,
36
40
  ionoFree: () => ionoFree,
37
41
  klobucharDelay: () => klobucharDelay,
38
42
  lambdaReduction: () => lambdaReduction,
@@ -2394,6 +2398,55 @@ var SbasProcessor = class {
2394
2398
  }
2395
2399
  };
2396
2400
 
2401
+ // src/positioning/ssr-apply.ts
2402
+ var cross = (a, b) => [
2403
+ a[1] * b[2] - a[2] * b[1],
2404
+ a[2] * b[0] - a[0] * b[2],
2405
+ a[0] * b[1] - a[1] * b[0]
2406
+ ];
2407
+ var norm = (a) => Math.hypot(a[0], a[1], a[2]);
2408
+ var unit = (a) => {
2409
+ const n = norm(a);
2410
+ return n > 0 ? [a[0] / n, a[1] / n, a[2] / n] : [0, 0, 0];
2411
+ };
2412
+ function applyOrbitClock(eph, corr, timeMs) {
2413
+ if (eph.system === "R" || eph.system === "S") return null;
2414
+ if (eph.iode !== corr.iod) return null;
2415
+ const age = (timeMs - corr.refTimeMs) / 1e3;
2416
+ if (corr.validitySeconds != null && (age < -1 || age > corr.validitySeconds))
2417
+ return null;
2418
+ const s = computeSatPosition(eph, timeMs);
2419
+ if (!Number.isFinite(s.x)) return null;
2420
+ const p = [s.x, s.y, s.z];
2421
+ const v = [s.vx, s.vy, s.vz];
2422
+ const er = unit(p);
2423
+ const en = unit(cross(p, v));
2424
+ const ea = cross(en, er);
2425
+ const dr = corr.radial + (corr.dotRadial ?? 0) * age;
2426
+ const da = corr.along + (corr.dotAlong ?? 0) * age;
2427
+ const dc = corr.cross + (corr.dotCross ?? 0) * age;
2428
+ const x = s.x + dr * er[0] + da * ea[0] + dc * en[0];
2429
+ const y = s.y + dr * er[1] + da * ea[1] + dc * en[1];
2430
+ const z = s.z + dr * er[2] + da * ea[2] + dc * en[2];
2431
+ const clockM = corr.clockC0 + (corr.clockC1 ?? 0) * age + (corr.clockC2 ?? 0) * age * age + (corr.highRateClock ?? 0);
2432
+ return { x, y, z, clockOffsetS: clockM / C_LIGHT };
2433
+ }
2434
+ function hasToOrbitClock(orbit, clock, refTimeMs, validitySeconds) {
2435
+ if (orbit.deltaRadial == null || orbit.deltaInTrack == null || orbit.deltaCrossTrack == null)
2436
+ return null;
2437
+ const clockC0 = clock && clock.deltaClock != null && !clock.notUsable ? clock.deltaClock : 0;
2438
+ return {
2439
+ prn: orbit.prn,
2440
+ iod: orbit.gnssIod,
2441
+ refTimeMs,
2442
+ radial: orbit.deltaRadial,
2443
+ along: orbit.deltaInTrack,
2444
+ cross: orbit.deltaCrossTrack,
2445
+ clockC0,
2446
+ validitySeconds
2447
+ };
2448
+ }
2449
+
2397
2450
  // src/rinex/sp3.ts
2398
2451
  function sp3Position(sp3, prn, tMs, order = 9) {
2399
2452
  const samples = sp3.satellites[prn];
@@ -2723,14 +2776,14 @@ function buildPppAntenna(antex) {
2723
2776
  }
2724
2777
  };
2725
2778
  }
2726
- function cross(a, b) {
2779
+ function cross2(a, b) {
2727
2780
  return [
2728
2781
  a[1] * b[2] - a[2] * b[1],
2729
2782
  a[2] * b[0] - a[0] * b[2],
2730
2783
  a[0] * b[1] - a[1] * b[0]
2731
2784
  ];
2732
2785
  }
2733
- function norm(a) {
2786
+ function norm2(a) {
2734
2787
  const n = Math.hypot(a[0], a[1], a[2]) || 1;
2735
2788
  return [a[0] / n, a[1] / n, a[2] / n];
2736
2789
  }
@@ -2738,14 +2791,14 @@ function dot(a, b) {
2738
2791
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2739
2792
  }
2740
2793
  function satBodyFrame(satEcef, sunEcef2) {
2741
- const z = norm([-satEcef[0], -satEcef[1], -satEcef[2]]);
2742
- const eSun = norm([
2794
+ const z = norm2([-satEcef[0], -satEcef[1], -satEcef[2]]);
2795
+ const eSun = norm2([
2743
2796
  sunEcef2[0] - satEcef[0],
2744
2797
  sunEcef2[1] - satEcef[1],
2745
2798
  sunEcef2[2] - satEcef[2]
2746
2799
  ]);
2747
- const y = norm(cross(z, eSun));
2748
- const x = norm(cross(y, z));
2800
+ const y = norm2(cross2(z, eSun));
2801
+ const x = norm2(cross2(y, z));
2749
2802
  return { x, y, z };
2750
2803
  }
2751
2804
  function satPcoToEcef(pcoBody, frame) {
@@ -2786,7 +2839,7 @@ function phaseWindup(prn, losRcvToSat, satFrame, latRad, lonRad, state) {
2786
2839
  const kx = satFrame.x;
2787
2840
  const ky = satFrame.y;
2788
2841
  const kdotx = dot(k, kx);
2789
- const kcrossY = cross(k, ky);
2842
+ const kcrossY = cross2(k, ky);
2790
2843
  const dS = [
2791
2844
  kx[0] - k[0] * kdotx - kcrossY[0],
2792
2845
  kx[1] - k[1] * kdotx - kcrossY[1],
@@ -2795,7 +2848,7 @@ function phaseWindup(prn, losRcvToSat, satFrame, latRad, lonRad, state) {
2795
2848
  const north = enuToEcef([0, 1, 0], latRad, lonRad);
2796
2849
  const east = enuToEcef([1, 0, 0], latRad, lonRad);
2797
2850
  const kdotn = dot(k, north);
2798
- const kcrossE = cross(k, east);
2851
+ const kcrossE = cross2(k, east);
2799
2852
  const dR = [
2800
2853
  north[0] - k[0] * kdotn + kcrossE[0],
2801
2854
  north[1] - k[1] * kdotn + kcrossE[1],
@@ -2805,7 +2858,7 @@ function phaseWindup(prn, losRcvToSat, satFrame, latRad, lonRad, state) {
2805
2858
  const nR = Math.hypot(dR[0], dR[1], dR[2]) || 1;
2806
2859
  let cosPhi = dot(dS, dR) / (nS * nR);
2807
2860
  cosPhi = Math.max(-1, Math.min(1, cosPhi));
2808
- const zeta = cross(dS, dR);
2861
+ const zeta = cross2(dS, dR);
2809
2862
  const sign = dot(k, zeta) >= 0 ? 1 : -1;
2810
2863
  const dphi = sign * Math.acos(cosPhi) / (2 * Math.PI);
2811
2864
  const prev = state.get(prn);
@@ -2943,13 +2996,35 @@ function ifCoeffs(f1, f2) {
2943
2996
  const lambdaIf = C_LIGHT / (f1 + f2);
2944
2997
  return { g, lambdaIf };
2945
2998
  }
2999
+ var Sp3EphemerisSource = class {
3000
+ constructor(sp3, clk) {
3001
+ this.sp3 = sp3;
3002
+ this.clk = clk;
3003
+ }
3004
+ satState(prn, tEmitMs) {
3005
+ const p = sp3Position(this.sp3, prn, tEmitMs);
3006
+ if (!p || p.clk == null) return null;
3007
+ const pPlus = sp3Position(this.sp3, prn, tEmitMs + 500);
3008
+ const pMinus = sp3Position(this.sp3, prn, tEmitMs - 500);
3009
+ let vx = 0;
3010
+ let vy = 0;
3011
+ let vz = 0;
3012
+ if (pPlus && pMinus) {
3013
+ vx = pPlus.x - pMinus.x;
3014
+ vy = pPlus.y - pMinus.y;
3015
+ vz = pPlus.z - pMinus.z;
3016
+ }
3017
+ const clkS = (this.clk ? clkBias(this.clk, prn, tEmitMs) : null) ?? p.clk;
3018
+ return { x: p.x, y: p.y, z: p.z, vx, vy, vz, clkS };
3019
+ }
3020
+ };
2946
3021
  function sagnacRotate(x, y, z, travelS) {
2947
3022
  const theta = OMEGA_E * travelS;
2948
3023
  const c = Math.cos(theta);
2949
3024
  const s = Math.sin(theta);
2950
3025
  return [x * c + y * s, -x * s + y * c, z];
2951
3026
  }
2952
- function satStateAtEmission(sp3, prn, recvTimeMs, rcv, clk) {
3027
+ function satStateAtEmission(source, prn, recvTimeMs, rcv) {
2953
3028
  let travelS = 0.075;
2954
3029
  let sx = 0;
2955
3030
  let sy = 0;
@@ -2958,18 +3033,11 @@ function satStateAtEmission(sp3, prn, recvTimeMs, rcv, clk) {
2958
3033
  let velDotPos = 0;
2959
3034
  for (let iter = 0; iter < 3; iter++) {
2960
3035
  const tEmit = recvTimeMs - travelS * 1e3;
2961
- const p = sp3Position(sp3, prn, tEmit);
2962
- if (!p || p.clk == null) return null;
2963
- const pPlus = sp3Position(sp3, prn, tEmit + 500);
2964
- const pMinus = sp3Position(sp3, prn, tEmit - 500);
2965
- if (pPlus && pMinus) {
2966
- const vx = (pPlus.x - pMinus.x) / 1;
2967
- const vy = (pPlus.y - pMinus.y) / 1;
2968
- const vz = (pPlus.z - pMinus.z) / 1;
2969
- velDotPos = p.x * vx + p.y * vy + p.z * vz;
2970
- }
2971
- clkS = (clk && clkBias(clk, prn, tEmit)) ?? p.clk;
2972
- const [rx, ry, rz] = sagnacRotate(p.x, p.y, p.z, travelS);
3036
+ const s = source.satState(prn, tEmit);
3037
+ if (!s) return null;
3038
+ velDotPos = s.x * s.vx + s.y * s.vy + s.z * s.vz;
3039
+ clkS = s.clkS;
3040
+ const [rx, ry, rz] = sagnacRotate(s.x, s.y, s.z, travelS);
2973
3041
  sx = rx;
2974
3042
  sy = ry;
2975
3043
  sz = rz;
@@ -2991,39 +3059,72 @@ var IDX_ZWD = 3;
2991
3059
  var NBASE = 4;
2992
3060
  var CLK_VAR = 1e10;
2993
3061
  var POS_VAR = 60 * 60;
2994
- function solvePpp(epochs, sp3, opts) {
2995
- const elevMask = (opts.elevationMaskDeg ?? 10) * Math.PI / 180;
2996
- const codeSigma = opts.codeSigma ?? 3;
2997
- const phaseSigma = opts.phaseSigma ?? 0.01;
2998
- const ztdQ = opts.ztdProcessNoise ?? 1e-8;
2999
- const kinematic = opts.mode === "kinematic";
3000
- const corrections = opts.corrections;
3001
- const x = [
3002
- opts.aprioriPos[0],
3003
- opts.aprioriPos[1],
3004
- opts.aprioriPos[2],
3005
- 0.1
3006
- // zenith wet delay (m) a priori
3007
- ];
3008
- const P = [];
3009
- for (let i = 0; i < NBASE; i++) {
3010
- P.push(new Array(NBASE).fill(0));
3062
+ var PppEngine = class {
3063
+ source;
3064
+ opts;
3065
+ elevMask;
3066
+ codeSigma;
3067
+ phaseSigma;
3068
+ ztdQ;
3069
+ kinematic;
3070
+ corrections;
3071
+ truth;
3072
+ // Filter state (reset() initialises; mutated in place across epochs).
3073
+ x;
3074
+ P;
3075
+ clkIdx;
3076
+ clockSeeded;
3077
+ ambIdx;
3078
+ lastSeen;
3079
+ arcs;
3080
+ arcSnap;
3081
+ mwState;
3082
+ series;
3083
+ ei = 0;
3084
+ t0 = 0;
3085
+ convergenceSec = null;
3086
+ convergedStreak = 0;
3087
+ constructor(opts, source) {
3088
+ this.opts = opts;
3089
+ this.source = source;
3090
+ this.elevMask = (opts.elevationMaskDeg ?? 10) * Math.PI / 180;
3091
+ this.codeSigma = opts.codeSigma ?? 3;
3092
+ this.phaseSigma = opts.phaseSigma ?? 0.01;
3093
+ this.ztdQ = opts.ztdProcessNoise ?? 1e-8;
3094
+ this.kinematic = opts.mode === "kinematic";
3095
+ this.corrections = opts.corrections;
3096
+ this.truth = opts.groundTruth;
3097
+ this.reset();
3098
+ }
3099
+ /** Reinitialise the filter (fresh session). */
3100
+ reset() {
3101
+ const { opts } = this;
3102
+ this.x = [opts.aprioriPos[0], opts.aprioriPos[1], opts.aprioriPos[2], 0.1];
3103
+ const P = [];
3104
+ for (let i = 0; i < NBASE; i++) P.push(new Array(NBASE).fill(0));
3105
+ P[IDX_X][IDX_X] = 100 * 100;
3106
+ P[IDX_Y][IDX_Y] = 100 * 100;
3107
+ P[IDX_Z][IDX_Z] = 100 * 100;
3108
+ P[IDX_ZWD][IDX_ZWD] = 0.5 * 0.5;
3109
+ this.P = P;
3110
+ this.clkIdx = /* @__PURE__ */ new Map();
3111
+ this.clockSeeded = /* @__PURE__ */ new Set();
3112
+ this.ambIdx = /* @__PURE__ */ new Map();
3113
+ this.lastSeen = /* @__PURE__ */ new Map();
3114
+ this.arcs = [];
3115
+ this.arcSnap = /* @__PURE__ */ new Map();
3116
+ this.mwState = /* @__PURE__ */ new Map();
3117
+ this.series = [];
3118
+ this.ei = 0;
3119
+ this.t0 = 0;
3120
+ this.convergenceSec = null;
3121
+ this.convergedStreak = 0;
3011
3122
  }
3012
- P[IDX_X][IDX_X] = 100 * 100;
3013
- P[IDX_Y][IDX_Y] = 100 * 100;
3014
- P[IDX_Z][IDX_Z] = 100 * 100;
3015
- P[IDX_ZWD][IDX_ZWD] = 0.5 * 0.5;
3016
- const clkIdx = /* @__PURE__ */ new Map();
3017
- const clockSeeded = /* @__PURE__ */ new Set();
3018
- const ambIdx = /* @__PURE__ */ new Map();
3019
- const lastSeen = /* @__PURE__ */ new Map();
3020
- const arcs = [];
3021
- const arcSnap = /* @__PURE__ */ new Map();
3022
- const flushArc = (prn, minEpochs = 10) => {
3023
- const s = arcSnap.get(prn);
3024
- arcSnap.delete(prn);
3123
+ flushArc(prn, minEpochs = 10) {
3124
+ const s = this.arcSnap.get(prn);
3125
+ this.arcSnap.delete(prn);
3025
3126
  if (!s || s.nEpochs < minEpochs) return;
3026
- arcs.push({
3127
+ this.arcs.push({
3027
3128
  prn,
3028
3129
  aIF: s.aIF,
3029
3130
  mwCyc: s.mwCyc,
@@ -3034,64 +3135,88 @@ function solvePpp(epochs, sp3, opts) {
3034
3135
  startMs: s.startMs,
3035
3136
  endMs: s.endMs
3036
3137
  });
3037
- };
3038
- const mwState = /* @__PURE__ */ new Map();
3039
- const detectSlip = (o, ei) => {
3138
+ }
3139
+ /** Detect a cycle slip via the Melbourne–Wübbena wide-lane ambiguity. */
3140
+ detectSlip(o, ei) {
3040
3141
  const lamW = C_LIGHT / (o.f1 - o.f2);
3041
3142
  const lw = o.l1 - o.l2;
3042
3143
  const pn = (o.f1 * o.c1 + o.f2 * o.c2) / (o.f1 + o.f2);
3043
3144
  const mw = lw - pn / lamW;
3044
- const st = mwState.get(o.prn);
3145
+ const st = this.mwState.get(o.prn);
3045
3146
  let slip = o.slip;
3046
3147
  if (!st || ei - st.lastEi > 2)
3047
3148
  slip = true;
3048
3149
  else if (Math.abs(mw - st.mean) > 4) slip = true;
3049
- if (slip) mwState.set(o.prn, { mean: mw, n: 1, lastEi: ei });
3150
+ if (slip) this.mwState.set(o.prn, { mean: mw, n: 1, lastEi: ei });
3050
3151
  else {
3051
3152
  const n = st.n + 1;
3052
- mwState.set(o.prn, {
3153
+ this.mwState.set(o.prn, {
3053
3154
  mean: st.mean + (mw - st.mean) / Math.min(n, 100),
3054
3155
  n,
3055
3156
  lastEi: ei
3056
3157
  });
3057
3158
  }
3058
3159
  return slip;
3059
- };
3060
- const growState = (initVal, initVar) => {
3061
- const idx = x.length;
3062
- x.push(initVal);
3063
- for (const row of P) row.push(0);
3064
- P.push(new Array(x.length).fill(0));
3065
- P[idx][idx] = initVar;
3160
+ }
3161
+ growState(initVal, initVar) {
3162
+ const idx = this.x.length;
3163
+ this.x.push(initVal);
3164
+ for (const row of this.P) row.push(0);
3165
+ this.P.push(new Array(this.x.length).fill(0));
3166
+ this.P[idx][idx] = initVar;
3066
3167
  return idx;
3067
- };
3068
- const dropState = (idx) => {
3069
- x.splice(idx, 1);
3070
- P.splice(idx, 1);
3071
- for (const row of P) row.splice(idx, 1);
3072
- for (const [prn, i] of ambIdx) {
3073
- if (i === idx) ambIdx.delete(prn);
3074
- else if (i > idx) ambIdx.set(prn, i - 1);
3168
+ }
3169
+ dropState(idx) {
3170
+ this.x.splice(idx, 1);
3171
+ this.P.splice(idx, 1);
3172
+ for (const row of this.P) row.splice(idx, 1);
3173
+ for (const [prn, i] of this.ambIdx) {
3174
+ if (i === idx) this.ambIdx.delete(prn);
3175
+ else if (i > idx) this.ambIdx.set(prn, i - 1);
3075
3176
  }
3076
- for (const [sys, i] of clkIdx) {
3077
- if (i > idx) clkIdx.set(sys, i - 1);
3177
+ for (const [sys, i] of this.clkIdx) {
3178
+ if (i > idx) this.clkIdx.set(sys, i - 1);
3078
3179
  }
3079
- };
3080
- const clockOf = (sys) => {
3081
- let i = clkIdx.get(sys);
3180
+ }
3181
+ /** State index of a constellation's receiver clock, creating it lazily. */
3182
+ clockOf(sys) {
3183
+ let i = this.clkIdx.get(sys);
3082
3184
  if (i === void 0) {
3083
- i = growState(0, CLK_VAR);
3084
- clkIdx.set(sys, i);
3185
+ i = this.growState(0, CLK_VAR);
3186
+ this.clkIdx.set(sys, i);
3085
3187
  }
3086
3188
  return i;
3087
- };
3088
- const series = [];
3089
- const truth = opts.groundTruth;
3090
- const t0 = epochs[0]?.timeMs ?? 0;
3091
- let convergenceSec = null;
3092
- let convergedStreak = 0;
3093
- for (let ei = 0; ei < epochs.length; ei++) {
3094
- const epoch = epochs[ei];
3189
+ }
3190
+ /** Process one epoch, carrying the filter forward; returns its result. */
3191
+ process(epoch) {
3192
+ const {
3193
+ source,
3194
+ opts,
3195
+ elevMask,
3196
+ codeSigma,
3197
+ phaseSigma,
3198
+ ztdQ,
3199
+ kinematic,
3200
+ corrections,
3201
+ truth,
3202
+ x,
3203
+ P,
3204
+ clkIdx,
3205
+ clockSeeded,
3206
+ ambIdx,
3207
+ lastSeen,
3208
+ arcSnap,
3209
+ mwState,
3210
+ series
3211
+ } = this;
3212
+ const flushArc = this.flushArc.bind(this);
3213
+ const detectSlip = this.detectSlip.bind(this);
3214
+ const growState = this.growState.bind(this);
3215
+ const dropState = this.dropState.bind(this);
3216
+ const clockOf = this.clockOf.bind(this);
3217
+ const ei = this.ei;
3218
+ if (ei === 0) this.t0 = epoch.timeMs;
3219
+ const t0 = this.t0;
3095
3220
  if (kinematic && ei > 0) {
3096
3221
  for (const p of [IDX_X, IDX_Y, IDX_Z]) {
3097
3222
  for (let j = 0; j < x.length; j++) {
@@ -3112,7 +3237,7 @@ function solvePpp(epochs, sp3, opts) {
3112
3237
  for (const o of epoch.obs) {
3113
3238
  if (o.c1 === 0 || o.c2 === 0 || o.l1 === 0 || o.l2 === 0) continue;
3114
3239
  const slip = detectSlip(o, ei);
3115
- const sat = satStateAtEmission(sp3, o.prn, epoch.timeMs, rcv, opts.clk);
3240
+ const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv);
3116
3241
  if (!sat) continue;
3117
3242
  const [elRad, azRad] = getAer(
3118
3243
  sat.state.x,
@@ -3132,7 +3257,9 @@ function solvePpp(epochs, sp3, opts) {
3132
3257
  const lam1 = C_LIGHT / o.f1;
3133
3258
  const lam2 = C_LIGHT / o.f2;
3134
3259
  if (o.c1 === 0 || o.c2 === 0 || o.l1 === 0 || o.l2 === 0) continue;
3135
- const pIf = g * o.c1 - (g - 1) * o.c2;
3260
+ const b1 = source.codeBias?.(o.prn, o.code1 ?? "", epoch.timeMs) ?? 0;
3261
+ const b2 = source.codeBias?.(o.prn, o.code2 ?? "", epoch.timeMs) ?? 0;
3262
+ const pIf = g * (o.c1 - b1) - (g - 1) * (o.c2 - b2);
3136
3263
  const lIf = g * (o.l1 * lam1) - (g - 1) * (o.l2 * lam2);
3137
3264
  if (!Number.isFinite(pIf) || !Number.isFinite(lIf)) continue;
3138
3265
  const trop = opts.troposphere ?? true ? niellMapping(elRad, latRad, heightM, epoch.timeMs) : { zhd: 0, mh: 0, mw: 0 };
@@ -3319,16 +3446,16 @@ function solvePpp(epochs, sp3, opts) {
3319
3446
  pos[2] - truth[2]
3320
3447
  );
3321
3448
  if (error3d < 0.1) {
3322
- convergedStreak++;
3323
- if (convergedStreak >= 10 && convergenceSec == null) {
3324
- convergenceSec = (epoch.timeMs - t0) / 1e3;
3449
+ this.convergedStreak++;
3450
+ if (this.convergedStreak >= 10 && this.convergenceSec == null) {
3451
+ this.convergenceSec = (epoch.timeMs - t0) / 1e3;
3325
3452
  }
3326
3453
  } else {
3327
- convergedStreak = 0;
3328
- convergenceSec = null;
3454
+ this.convergedStreak = 0;
3455
+ this.convergenceSec = null;
3329
3456
  }
3330
3457
  }
3331
- series.push({
3458
+ const result = {
3332
3459
  timeMs: epoch.timeMs,
3333
3460
  enu,
3334
3461
  error3d,
@@ -3337,50 +3464,68 @@ function solvePpp(epochs, sp3, opts) {
3337
3464
  ztdWet: x[IDX_ZWD],
3338
3465
  ztdHydrostatic: epochZhd,
3339
3466
  phaseResRms: nPhaseRes > 0 ? Math.sqrt(sumPhaseResSq / nPhaseRes) : 0
3340
- });
3467
+ };
3468
+ series.push(result);
3469
+ this.ei = ei + 1;
3470
+ return result;
3341
3471
  }
3342
- let finalState;
3343
- if (opts.exposeState) {
3344
- const ambs = [];
3345
- for (const [prn, index] of ambIdx) {
3346
- const s = arcSnap.get(prn);
3347
- if (!s || s.nEpochs < 10) continue;
3348
- ambs.push({
3349
- prn,
3350
- index,
3351
- aIF: x[index],
3352
- mwCyc: mwState.get(prn)?.mean ?? s.mwCyc,
3353
- f1: s.f1,
3354
- f2: s.f2,
3355
- elevDeg: s.elevSum / s.nEpochs
3356
- });
3472
+ /** Finalise the session: capture the AR state, flush open arcs, assemble. */
3473
+ solution() {
3474
+ const { opts, x, P, ambIdx, arcSnap, mwState, arcs, series, truth } = this;
3475
+ const flushArc = this.flushArc.bind(this);
3476
+ let finalState;
3477
+ if (opts.exposeState) {
3478
+ const ambs = [];
3479
+ for (const [prn, index] of ambIdx) {
3480
+ const s = arcSnap.get(prn);
3481
+ if (!s || s.nEpochs < 10) continue;
3482
+ ambs.push({
3483
+ prn,
3484
+ index,
3485
+ aIF: x[index],
3486
+ mwCyc: mwState.get(prn)?.mean ?? s.mwCyc,
3487
+ f1: s.f1,
3488
+ f2: s.f2,
3489
+ elevDeg: s.elevSum / s.nEpochs
3490
+ });
3491
+ }
3492
+ finalState = {
3493
+ position: [x[IDX_X], x[IDX_Y], x[IDX_Z]],
3494
+ covariance: P.map((row) => row.slice()),
3495
+ ambiguities: ambs
3496
+ };
3357
3497
  }
3358
- finalState = {
3359
- position: [x[IDX_X], x[IDX_Y], x[IDX_Z]],
3360
- covariance: P.map((row) => row.slice()),
3361
- ambiguities: ambs
3498
+ for (const prn of [...arcSnap.keys()]) flushArc(prn);
3499
+ const finalPos = [
3500
+ x[IDX_X],
3501
+ x[IDX_Y],
3502
+ x[IDX_Z]
3503
+ ];
3504
+ const llh = ecefToGeodetic(finalPos[0], finalPos[1], finalPos[2]);
3505
+ const finalError3d = truth ? Math.hypot(
3506
+ finalPos[0] - truth[0],
3507
+ finalPos[1] - truth[1],
3508
+ finalPos[2] - truth[2]
3509
+ ) : null;
3510
+ return {
3511
+ position: finalPos,
3512
+ llh,
3513
+ series,
3514
+ ztdWet: x[IDX_ZWD],
3515
+ ztdHydrostatic: series.length ? series[series.length - 1].ztdHydrostatic : 0,
3516
+ epochsUsed: series.length,
3517
+ convergenceSec: this.convergenceSec,
3518
+ finalError3d,
3519
+ arcs,
3520
+ finalState
3362
3521
  };
3363
3522
  }
3364
- for (const prn of [...arcSnap.keys()]) flushArc(prn);
3365
- const finalPos = [x[IDX_X], x[IDX_Y], x[IDX_Z]];
3366
- const llh = ecefToGeodetic(finalPos[0], finalPos[1], finalPos[2]);
3367
- const finalError3d = truth ? Math.hypot(
3368
- finalPos[0] - truth[0],
3369
- finalPos[1] - truth[1],
3370
- finalPos[2] - truth[2]
3371
- ) : null;
3372
- return {
3373
- position: finalPos,
3374
- llh,
3375
- series,
3376
- ztdWet: x[IDX_ZWD],
3377
- ztdHydrostatic: series.length ? series[series.length - 1].ztdHydrostatic : 0,
3378
- epochsUsed: epochs.length,
3379
- convergenceSec,
3380
- finalError3d,
3381
- arcs,
3382
- finalState
3383
- };
3523
+ };
3524
+ function solvePpp(epochs, sp3OrSource, opts) {
3525
+ const source = "satState" in sp3OrSource ? sp3OrSource : new Sp3EphemerisSource(sp3OrSource, opts.clk);
3526
+ const eng = new PppEngine(opts, source);
3527
+ for (const epoch of epochs) eng.process(epoch);
3528
+ return eng.solution();
3384
3529
  }
3385
3530
 
3386
3531
  // src/positioning/ppp-ar.ts
@@ -4116,8 +4261,11 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4116
4261
  // Annotate the CommonJS export names for ESM import in node:
4117
4262
  0 && (module.exports = {
4118
4263
  IONO_L1_M_PER_TECU,
4264
+ PppEngine,
4119
4265
  RtkFloatEngine,
4120
4266
  SbasProcessor,
4267
+ Sp3EphemerisSource,
4268
+ applyOrbitClock,
4121
4269
  buildPppAntenna,
4122
4270
  createPppCorrections,
4123
4271
  estimateNarrowlaneFcb,
@@ -4128,6 +4276,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4128
4276
  gimSlantIonoDelayL1,
4129
4277
  gimVerticalTec,
4130
4278
  gmst,
4279
+ hasToOrbitClock,
4131
4280
  ionoFree,
4132
4281
  klobucharDelay,
4133
4282
  lambdaReduction,