gnss-js 2.11.0 → 2.12.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/dist/index.cjs CHANGED
@@ -106,6 +106,7 @@ __export(src_exports, {
106
106
  SYSTEM_NAMES: () => SYSTEM_NAMES,
107
107
  SYSTEM_ORDER: () => SYSTEM_ORDER,
108
108
  SYS_SHORT: () => SYS_SHORT,
109
+ SatOsbTable: () => SatOsbTable,
109
110
  SbasProcessor: () => SbasProcessor,
110
111
  Scale: () => Scale,
111
112
  Sp3EphemerisSource: () => Sp3EphemerisSource,
@@ -1374,20 +1375,14 @@ function geodeticToGeohash(lat, lon, precision = 8) {
1374
1375
  // src/rinex/crx.ts
1375
1376
  function crxRepair(old, diff) {
1376
1377
  const chars = old.split("");
1377
- for (let i = 0; i < diff.length; i++) {
1378
+ const n = Math.min(old.length, diff.length);
1379
+ for (let i = 0; i < n; i++) {
1378
1380
  const dc = diff[i];
1379
- if (dc === " ") {
1380
- } else if (dc === "&") {
1381
- chars[i] = " ";
1382
- } else {
1383
- chars[i] = dc;
1384
- }
1385
- }
1386
- if (diff.length > chars.length) {
1387
- for (let i = chars.length; i < diff.length; i++) {
1388
- chars[i] = diff[i] === "&" ? " " : diff[i];
1389
- }
1381
+ if (dc === " ") continue;
1382
+ chars[i] = dc === "&" ? " " : dc;
1390
1383
  }
1384
+ for (let i = old.length; i < diff.length; i++)
1385
+ chars[i] = diff[i] === "&" ? " " : diff[i];
1391
1386
  return chars.join("");
1392
1387
  }
1393
1388
  function parseCrxDataLine(line, ntype) {
@@ -2085,6 +2080,15 @@ function readObsValue(line, index) {
2085
2080
  const v = parseFloat(raw);
2086
2081
  return isNaN(v) ? null : v;
2087
2082
  }
2083
+ function normalizePrn(id) {
2084
+ let sys = id[0];
2085
+ let num = id.substring(1);
2086
+ if (sys === " " || /\d/.test(sys)) {
2087
+ num = id;
2088
+ sys = "G";
2089
+ }
2090
+ return sys + num.replace(/\s/g, "").padStart(2, "0");
2091
+ }
2088
2092
  function parseEpochLine3(line) {
2089
2093
  if (line[0] !== ">") return null;
2090
2094
  const yr = parseInt(line.substring(2, 6));
@@ -2234,7 +2238,7 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
2234
2238
  function processSatLineV3(line) {
2235
2239
  if (!header || line.length < 3) return;
2236
2240
  const sys = line[0];
2237
- const prn = line.substring(0, 3);
2241
+ const prn = normalizePrn(line.substring(0, 3));
2238
2242
  trackSat(sys, prn);
2239
2243
  const obsLine = line.substring(3);
2240
2244
  const codes = header.obsTypes[sys] ?? [];
@@ -2259,10 +2263,8 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
2259
2263
  let v2ObsAccum = [];
2260
2264
  function processSatLineV2(line) {
2261
2265
  if (!header || v2SatIndex >= v2SatIds.length) return;
2262
- const satId = v2SatIds[v2SatIndex];
2263
- let sys = satId[0];
2264
- if (sys === " " || /\d/.test(sys)) sys = "G";
2265
- const prn = sys + satId.substring(1).padStart(2, "0");
2266
+ const prn = normalizePrn(v2SatIds[v2SatIndex]);
2267
+ const sys = prn[0];
2266
2268
  if (v2CurrentSatLine === 0) {
2267
2269
  trackSat(sys, prn);
2268
2270
  if (onSatObs) v2ObsAccum = [];
@@ -2353,6 +2355,7 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
2353
2355
  } else {
2354
2356
  epochLine = crxRepair(crxPrevEpochLine, line);
2355
2357
  }
2358
+ epochLine = epochLine.replace(/ +$/, "");
2356
2359
  if (isV3) {
2357
2360
  if (epochLine[0] !== ">") return;
2358
2361
  const info = parseEpochLine3(epochLine);
@@ -2370,10 +2373,11 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
2370
2373
  return;
2371
2374
  }
2372
2375
  resetEpochAccum(info);
2373
- const satPart = epochLine.substring(41).replace(/\s+$/, "");
2374
2376
  crxSatList = [];
2375
- for (let i = 0; i + 3 <= satPart.length; i += 3) {
2376
- crxSatList.push(satPart.substring(i, i + 3));
2377
+ for (let k = 0; k < info.numSats; k++) {
2378
+ crxSatList.push(
2379
+ normalizePrn(epochLine.substring(41 + 3 * k, 44 + 3 * k))
2380
+ );
2377
2381
  }
2378
2382
  crxSatIndex = 0;
2379
2383
  crxPrevEpochLine = epochLine;
@@ -2394,11 +2398,7 @@ async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode)
2394
2398
  return;
2395
2399
  }
2396
2400
  resetEpochAccum(info);
2397
- crxSatList = (info.satIds ?? []).map((id) => {
2398
- let sys = id[0];
2399
- if (sys === " " || /\d/.test(sys)) sys = "G";
2400
- return sys + id.substring(1).padStart(2, "0");
2401
- });
2401
+ crxSatList = (info.satIds ?? []).slice(0, info.numSats).map(normalizePrn);
2402
2402
  crxSatIndex = 0;
2403
2403
  crxPrevEpochLine = epochLine;
2404
2404
  crxPhase = "clock";
@@ -4608,6 +4608,50 @@ function findSatBias(bias, prn, obs1, obs2, tMs) {
4608
4608
  }
4609
4609
  return null;
4610
4610
  }
4611
+ var SatOsbTable = class {
4612
+ byKey = /* @__PURE__ */ new Map();
4613
+ constructor(bias) {
4614
+ const recs = Array.isArray(bias) ? bias : bias.records;
4615
+ const groups = /* @__PURE__ */ new Map();
4616
+ for (const r of recs) {
4617
+ if (r.type !== "OSB" || !r.prn || r.station) continue;
4618
+ if (r.unit !== "ns") continue;
4619
+ const k = `${r.prn} ${r.obs1}`;
4620
+ let g = groups.get(k);
4621
+ if (!g) groups.set(k, g = []);
4622
+ g.push(r);
4623
+ }
4624
+ for (const [k, g] of groups) {
4625
+ g.sort((a, b) => a.startMs - b.startMs);
4626
+ this.byKey.set(k, {
4627
+ start: Float64Array.from(g, (r) => r.startMs),
4628
+ end: Float64Array.from(g, (r) => r.endMs),
4629
+ m: Float64Array.from(g, biasMetres)
4630
+ });
4631
+ }
4632
+ }
4633
+ /** Whether any phase (L…) OSB is present — i.e. the product supports AR. */
4634
+ get hasPhase() {
4635
+ for (const k of this.byKey.keys())
4636
+ if (k.split(" ")[1][0] === "L") return true;
4637
+ return false;
4638
+ }
4639
+ /** OSB (m) of `prn`'s observable `obs` (e.g. 'C1W', 'L2W') at `tMs`;
4640
+ * null when the product has none valid then. */
4641
+ get(prn, obs, tMs) {
4642
+ const e = this.byKey.get(`${prn} ${obs}`) ?? this.byKey.get(`${prn} ${obs.slice(0, 2)}X`);
4643
+ if (!e) return null;
4644
+ let lo = 0;
4645
+ let hi = e.start.length - 1;
4646
+ if (tMs < e.start[0]) return null;
4647
+ while (lo < hi) {
4648
+ const mid = lo + hi + 1 >> 1;
4649
+ if (e.start[mid] <= tMs) lo = mid;
4650
+ else hi = mid - 1;
4651
+ }
4652
+ return tMs <= e.end[lo] ? e.m[lo] : null;
4653
+ }
4654
+ };
4611
4655
 
4612
4656
  // src/rinex/obs-writer-common.ts
4613
4657
  function createGzipLineSink() {
@@ -14090,6 +14134,12 @@ function solveZt(n, m, Z2, E) {
14090
14134
  for (let cIdx = 0; cIdx < m; cIdx++) X[r + cIdx * n] = M[r * w + n + cIdx];
14091
14135
  return X;
14092
14136
  }
14137
+ function normCdf(x) {
14138
+ const z = Math.abs(x) / Math.SQRT2;
14139
+ const t = 1 / (1 + 0.3275911 * z);
14140
+ const erf = 1 - ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-z * z);
14141
+ return x >= 0 ? 0.5 * (1 + erf) : 0.5 * (1 - erf);
14142
+ }
14093
14143
  function lambdaSearch(a, Q, n, m = 2) {
14094
14144
  if (n <= 0 || m <= 0 || a.length < n || Q.length < n * n) return null;
14095
14145
  const L = new Float64Array(n * n);
@@ -14116,7 +14166,10 @@ function lambdaSearch(a, Q, n, m = 2) {
14116
14166
  candidates.push(v);
14117
14167
  }
14118
14168
  const ratio = m >= 2 && s[0] > 0 ? s[1] / s[0] : Infinity;
14119
- return { candidates, residuals: s, ratio };
14169
+ let successRate = 1;
14170
+ for (let i = 0; i < n; i++)
14171
+ successRate *= 2 * normCdf(1 / (2 * Math.sqrt(D[i]))) - 1;
14172
+ return { candidates, residuals: s, ratio, successRate };
14120
14173
  }
14121
14174
  function lambdaReduction(Q, n) {
14122
14175
  if (n <= 0 || Q.length < n * n) return null;
@@ -14131,7 +14184,7 @@ function lambdaReduction(Q, n) {
14131
14184
 
14132
14185
  // src/positioning/ppp-antenna.ts
14133
14186
  function parseAntexTime(s) {
14134
- const p = s.trim().split(/\s+/).map(Number);
14187
+ const p = s.trim().split(/[\s:-]+/).map(Number);
14135
14188
  if (p.length < 3 || p.some((v) => Number.isNaN(v))) return null;
14136
14189
  return Date.UTC(
14137
14190
  p[0],
@@ -14181,11 +14234,21 @@ function buildPppAntenna(antex) {
14181
14234
  satOffset(prn, dateMs, band1, band2, f1, f2) {
14182
14235
  const list = sats.get(prn);
14183
14236
  if (!list) return null;
14184
- const entry = list.find((e) => {
14185
- const from = parseAntexTime(e.validFrom);
14186
- const until = parseAntexTime(e.validUntil);
14187
- return (from == null || dateMs >= from) && (until == null || dateMs <= until);
14188
- });
14237
+ let entry;
14238
+ let bestDist = Infinity;
14239
+ for (const e of list) {
14240
+ const from = parseAntexTime(e.validFrom) ?? -Infinity;
14241
+ const until = parseAntexTime(e.validUntil) ?? Infinity;
14242
+ if (dateMs >= from && dateMs <= until) {
14243
+ entry = e;
14244
+ break;
14245
+ }
14246
+ const dist = dateMs < from ? from - dateMs : dateMs - until;
14247
+ if (dist < bestDist) {
14248
+ bestDist = dist;
14249
+ entry = e;
14250
+ }
14251
+ }
14189
14252
  if (!entry) return null;
14190
14253
  const a = freqOf(entry, band1);
14191
14254
  const b = freqOf(entry, band2);
@@ -14193,10 +14256,14 @@ function buildPppAntenna(antex) {
14193
14256
  return withZen(entry, combineIf(a, b, f1, f2));
14194
14257
  },
14195
14258
  rcvOffset(antType, band1, band2, f1, f2) {
14196
- const entry = rcvs.get(antType);
14259
+ const entry = rcvs.get(antType) ?? rcvs.get(antType.substring(0, 16).padEnd(16, " ") + "NONE");
14197
14260
  if (!entry) return null;
14198
- const a = freqOf(entry, band1);
14199
- const b = freqOf(entry, band2);
14261
+ let a = freqOf(entry, band1);
14262
+ let b = freqOf(entry, band2);
14263
+ if (!a || !b) {
14264
+ a = freqOf(entry, "G01");
14265
+ b = freqOf(entry, "G02");
14266
+ }
14200
14267
  if (!a || !b) return null;
14201
14268
  return withZen(entry, combineIf(a, b, f1, f2));
14202
14269
  }
@@ -14272,13 +14339,13 @@ function phaseWindup(prn, losRcvToSat, satFrame, latRad, lonRad, state) {
14272
14339
  kx[2] - k[2] * kdotx - kcrossY[2]
14273
14340
  ];
14274
14341
  const north = enuToEcef([0, 1, 0], latRad, lonRad);
14275
- const east = enuToEcef([1, 0, 0], latRad, lonRad);
14342
+ const west = enuToEcef([-1, 0, 0], latRad, lonRad);
14276
14343
  const kdotn = dot(k, north);
14277
- const kcrossE = cross(k, east);
14344
+ const kcrossW = cross(k, west);
14278
14345
  const dR = [
14279
- north[0] - k[0] * kdotn + kcrossE[0],
14280
- north[1] - k[1] * kdotn + kcrossE[1],
14281
- north[2] - k[2] * kdotn + kcrossE[2]
14346
+ north[0] - k[0] * kdotn + kcrossW[0],
14347
+ north[1] - k[1] * kdotn + kcrossW[1],
14348
+ north[2] - k[2] * kdotn + kcrossW[2]
14282
14349
  ];
14283
14350
  const nS = Math.hypot(dS[0], dS[1], dS[2]) || 1;
14284
14351
  const nR = Math.hypot(dR[0], dR[1], dR[2]) || 1;
@@ -14405,9 +14472,11 @@ function buildGeometry(rover, base, basePos, ephemerides, timeMs, maskRad, tropo
14405
14472
  let baseLatRad = 0;
14406
14473
  let baseLonRad = 0;
14407
14474
  if (antenna?.base) {
14408
- const [latDeg, lonDeg] = ecefToGeodetic(basePos[0], basePos[1], basePos[2]);
14409
- baseLatRad = latDeg * Math.PI / 180;
14410
- baseLonRad = lonDeg * Math.PI / 180;
14475
+ [baseLatRad, baseLonRad] = ecefToGeodetic(
14476
+ basePos[0],
14477
+ basePos[1],
14478
+ basePos[2]
14479
+ );
14411
14480
  }
14412
14481
  for (const [prn, mR] of rover) {
14413
14482
  const sys = prn[0];
@@ -14503,12 +14572,12 @@ function roverTerms(g, x, y, z, troposphere) {
14503
14572
  dTropo = tropoDelay(Math.max(elR, 0.05)) - g.tropoB;
14504
14573
  }
14505
14574
  if (g.offR) {
14506
- const [latDeg, lonDeg] = ecefToGeodetic(x, y, z);
14575
+ const [latRad, lonRad] = ecefToGeodetic(x, y, z);
14507
14576
  rho += rcvAntennaRangeM(
14508
14577
  g.offR,
14509
14578
  [-u[0], -u[1], -u[2]],
14510
- latDeg * Math.PI / 180,
14511
- lonDeg * Math.PI / 180,
14579
+ latRad,
14580
+ lonRad,
14512
14581
  elR,
14513
14582
  g.roverDelta
14514
14583
  );
@@ -16323,6 +16392,234 @@ function hasToOrbitClock(orbit, clock2, refTimeMs, validitySeconds) {
16323
16392
  };
16324
16393
  }
16325
16394
 
16395
+ // src/positioning/ppp-ar.ts
16396
+ function wlWavelength(f1, f2) {
16397
+ return C_LIGHT / (f1 - f2);
16398
+ }
16399
+ function nlWavelength(f1, f2) {
16400
+ return C_LIGHT / (f1 + f2);
16401
+ }
16402
+ function resolvePppAmbiguities(sats, Qaif, opts = {}) {
16403
+ const wlThreshold = opts.wlThreshold ?? 0.25;
16404
+ const ratioThreshold = opts.ratioThreshold ?? 3;
16405
+ const minPartial = opts.minPartial ?? 4;
16406
+ const minSuccess = opts.minSuccessRate ?? 0;
16407
+ const n = sats.length;
16408
+ const none = (rejectedWl2, ratio = 0) => ({
16409
+ fixed: false,
16410
+ ratio,
16411
+ refPrn: null,
16412
+ refPrns: [],
16413
+ sats: [],
16414
+ rejectedWl: rejectedWl2
16415
+ });
16416
+ const elev = (i) => sats[i].elevDeg ?? -1;
16417
+ const wlOf = (i) => sats[i].mwCyc - (sats[i].wlBiasCyc ?? 0);
16418
+ const refOfSys = /* @__PURE__ */ new Map();
16419
+ for (let i = 0; i < n; i++) {
16420
+ const sys = sats[i].prn[0];
16421
+ const r = refOfSys.get(sys);
16422
+ if (r === void 0 || elev(i) > elev(r)) refOfSys.set(sys, i);
16423
+ }
16424
+ const nWlOf = /* @__PURE__ */ new Map();
16425
+ const refIdx = /* @__PURE__ */ new Map();
16426
+ const rejectedWl = [];
16427
+ for (const r of refOfSys.values()) {
16428
+ nWlOf.set(r, Math.round(wlOf(r)));
16429
+ refIdx.set(r, r);
16430
+ }
16431
+ for (let i = 0; i < n; i++) {
16432
+ if (refIdx.has(i)) continue;
16433
+ const r = refOfSys.get(sats[i].prn[0]);
16434
+ const sdWl = wlOf(i) - wlOf(r);
16435
+ const k = Math.round(sdWl);
16436
+ if (Math.abs(sdWl - k) <= wlThreshold) {
16437
+ nWlOf.set(i, nWlOf.get(r) + k);
16438
+ refIdx.set(i, r);
16439
+ } else rejectedWl.push(sats[i].prn);
16440
+ }
16441
+ const lamNl = (i) => nlWavelength(sats[i].f1, sats[i].f2);
16442
+ const n1Float = (i) => {
16443
+ const s = sats[i];
16444
+ return s.aIF / lamNl(i) - s.f2 / (s.f1 - s.f2) * nWlOf.get(i) - (s.nlBiasCyc ?? 0);
16445
+ };
16446
+ const qN1 = (a, b) => Qaif[a * n + b] / (lamNl(a) * lamNl(b));
16447
+ let cand = [...refIdx.keys()].filter((i) => refIdx.get(i) !== i).sort((a, b) => elev(b) - elev(a));
16448
+ if (cand.length < 1) return none(rejectedWl);
16449
+ let best = null;
16450
+ let firstRatio = 0;
16451
+ for (; ; ) {
16452
+ const m = cand.length;
16453
+ const sd2 = new Float64Array(m);
16454
+ const Qsd = new Float64Array(m * m);
16455
+ for (let a = 0; a < m; a++) {
16456
+ const ia = cand[a];
16457
+ const ra = refIdx.get(ia);
16458
+ sd2[a] = n1Float(ia) - n1Float(ra);
16459
+ for (let b = 0; b < m; b++) {
16460
+ const ib = cand[b];
16461
+ const rb = refIdx.get(ib);
16462
+ Qsd[a * m + b] = qN1(ia, ib) - qN1(ia, rb) - qN1(ra, ib) + qN1(ra, rb);
16463
+ }
16464
+ }
16465
+ const res = lambdaSearch(sd2, Qsd, m, 2);
16466
+ const ratio = res ? Math.min(res.ratio, 999.9) : 0;
16467
+ if (best === null && firstRatio === 0) firstRatio = ratio;
16468
+ if (res && ratio >= ratioThreshold && res.successRate >= minSuccess) {
16469
+ best = { set: cand, ratio, fix: Float64Array.from(res.candidates[0]) };
16470
+ break;
16471
+ }
16472
+ if (m <= minPartial) break;
16473
+ cand = cand.slice(0, -1);
16474
+ }
16475
+ if (!best) return { ...none(rejectedWl, firstRatio), refPrn: null };
16476
+ const refN1 = /* @__PURE__ */ new Map();
16477
+ for (const r of refOfSys.values()) refN1.set(r, Math.round(n1Float(r)));
16478
+ const out = [];
16479
+ const emit = (i, n1) => {
16480
+ const s = sats[i];
16481
+ const l = lamNl(i);
16482
+ const nWl = nWlOf.get(i);
16483
+ out.push({
16484
+ prn: s.prn,
16485
+ nWl,
16486
+ n1,
16487
+ aIF: l * n1 + l * (s.f2 / (s.f1 - s.f2)) * nWl,
16488
+ ref: sats[refIdx.get(i)].prn
16489
+ });
16490
+ };
16491
+ const usedRefs = new Set(best.set.map((i) => refIdx.get(i)));
16492
+ for (const r of usedRefs) emit(r, refN1.get(r));
16493
+ best.set.forEach(
16494
+ (i, a) => emit(i, refN1.get(refIdx.get(i)) + Math.round(best.fix[a]))
16495
+ );
16496
+ const refPrns = [...usedRefs].map((r) => sats[r].prn);
16497
+ return {
16498
+ fixed: true,
16499
+ ratio: best.ratio,
16500
+ refPrn: refPrns[0] ?? null,
16501
+ refPrns,
16502
+ sats: out,
16503
+ rejectedWl
16504
+ };
16505
+ }
16506
+ function solveSpd(A, b) {
16507
+ const n = b.length;
16508
+ const L = Array.from({ length: n }, () => new Array(n).fill(0));
16509
+ for (let i = 0; i < n; i++) {
16510
+ for (let j = 0; j <= i; j++) {
16511
+ let s = A[i][j];
16512
+ for (let k = 0; k < j; k++) s -= L[i][k] * L[j][k];
16513
+ if (i === j) {
16514
+ if (s <= 0) return null;
16515
+ L[i][j] = Math.sqrt(s);
16516
+ } else {
16517
+ L[i][j] = s / L[j][j];
16518
+ }
16519
+ }
16520
+ }
16521
+ const y = new Array(n).fill(0);
16522
+ for (let i = 0; i < n; i++) {
16523
+ let s = b[i];
16524
+ for (let k = 0; k < i; k++) s -= L[i][k] * y[k];
16525
+ y[i] = s / L[i][i];
16526
+ }
16527
+ const x = new Array(n).fill(0);
16528
+ for (let i = n - 1; i >= 0; i--) {
16529
+ let s = y[i];
16530
+ for (let k = i + 1; k < n; k++) s -= L[k][i] * x[k];
16531
+ x[i] = s / L[i][i];
16532
+ }
16533
+ return x;
16534
+ }
16535
+ function fixPppPosition(state, fcb, opts = {}) {
16536
+ const floatPos = state.position;
16537
+ const noFix = {
16538
+ fixed: false,
16539
+ position: floatPos,
16540
+ floatPosition: floatPos,
16541
+ shift: [0, 0, 0],
16542
+ nFixed: 0,
16543
+ ratio: 0,
16544
+ refPrn: null
16545
+ };
16546
+ const amb = state.ambiguities;
16547
+ if (amb.length < 3) return noFix;
16548
+ const sats = amb.map((a) => ({
16549
+ prn: a.prn,
16550
+ aIF: a.aIF,
16551
+ mwCyc: a.mwCyc,
16552
+ f1: a.f1,
16553
+ f2: a.f2,
16554
+ wlBiasCyc: fcb.satWlFcb.get(a.prn) ?? 0,
16555
+ nlBiasCyc: fcb.satNlFcb.get(a.prn) ?? 0,
16556
+ elevDeg: a.elevDeg
16557
+ }));
16558
+ const n = sats.length;
16559
+ const Qaif = new Float64Array(n * n);
16560
+ for (let i = 0; i < n; i++)
16561
+ for (let j = 0; j < n; j++)
16562
+ Qaif[i * n + j] = state.covariance[amb[i].index][amb[j].index];
16563
+ const res = resolvePppAmbiguities(sats, Qaif, {
16564
+ ...opts,
16565
+ minPartial: opts.minPartial ?? Math.max(4, opts.minFixed ?? 0)
16566
+ });
16567
+ if (!res.fixed || res.refPrn === null) {
16568
+ return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
16569
+ }
16570
+ const idxOfPrn = new Map(amb.map((a) => [a.prn, a.index]));
16571
+ const floatOfPrn = new Map(amb.map((a) => [a.prn, a.aIF]));
16572
+ const fixedOf = new Map(res.sats.map((s) => [s.prn, s.aIF]));
16573
+ const pairs = res.sats.filter(
16574
+ (s) => s.ref !== s.prn && idxOfPrn.has(s.prn) && idxOfPrn.has(s.ref)
16575
+ ).map((s) => ({ prn: s.prn, ref: s.ref }));
16576
+ const m = pairs.length;
16577
+ if (m < Math.max(1, opts.minFixed ?? 1))
16578
+ return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
16579
+ const P = state.covariance;
16580
+ const ix = (prn) => idxOfPrn.get(prn);
16581
+ const qAA = (i, j) => P[ix(i)][ix(j)];
16582
+ const qXA = (r, i) => P[r][ix(i)];
16583
+ const Qzz = Array.from({ length: m }, () => new Array(m).fill(0));
16584
+ const z = new Array(m).fill(0);
16585
+ for (let a = 0; a < m; a++) {
16586
+ const { prn: ia, ref: ra } = pairs[a];
16587
+ z[a] = floatOfPrn.get(ia) - floatOfPrn.get(ra) - (fixedOf.get(ia) - fixedOf.get(ra));
16588
+ for (let b = 0; b < m; b++) {
16589
+ const { prn: ib, ref: rb } = pairs[b];
16590
+ Qzz[a][b] = qAA(ia, ib) - qAA(ia, rb) - qAA(ra, ib) + qAA(ra, rb);
16591
+ }
16592
+ }
16593
+ const Qxz = [0, 1, 2].map(
16594
+ (r) => pairs.map(({ prn, ref }) => qXA(r, prn) - qXA(r, ref))
16595
+ );
16596
+ const y = solveSpd(Qzz, z);
16597
+ if (!y) return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
16598
+ let chi2 = 0;
16599
+ for (let a = 0; a < m; a++) chi2 += z[a] * y[a];
16600
+ if (chi2 / m > (opts.maxChi2 ?? Infinity))
16601
+ return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
16602
+ const shift = [0, 0, 0];
16603
+ for (let r = 0; r < 3; r++) {
16604
+ let s = 0;
16605
+ for (let a = 0; a < m; a++) s += Qxz[r][a] * y[a];
16606
+ shift[r] = -s;
16607
+ }
16608
+ return {
16609
+ fixed: true,
16610
+ position: [
16611
+ floatPos[0] + shift[0],
16612
+ floatPos[1] + shift[1],
16613
+ floatPos[2] + shift[2]
16614
+ ],
16615
+ floatPosition: floatPos,
16616
+ shift,
16617
+ nFixed: m,
16618
+ ratio: res.ratio,
16619
+ refPrn: res.refPrn
16620
+ };
16621
+ }
16622
+
16326
16623
  // src/positioning/ppp-tropo.ts
16327
16624
  var HYD_AVG_A = [
16328
16625
  0.0012769934,
@@ -16530,13 +16827,11 @@ function createPppCorrections(cfg) {
16530
16827
  let rangeM = 0;
16531
16828
  let phaseWindupM = 0;
16532
16829
  const e = geom.los;
16533
- const [latDeg, lonDeg] = ecefToGeodetic(
16830
+ const [latRad, lonRad] = ecefToGeodetic(
16534
16831
  geom.rcvEcef[0],
16535
16832
  geom.rcvEcef[1],
16536
16833
  geom.rcvEcef[2]
16537
16834
  );
16538
- const latRad = latDeg * Math.PI / 180;
16539
- const lonRad = lonDeg * Math.PI / 180;
16540
16835
  const frame = ctx ? satBodyFrame(geom.satEcef, ctx.sun) : null;
16541
16836
  if (useSat && frame && geom.band1 && geom.band2) {
16542
16837
  const off = cfg.antenna.satOffset(
@@ -16594,6 +16889,10 @@ function createPppCorrections(cfg) {
16594
16889
  }
16595
16890
 
16596
16891
  // src/positioning/ppp.ts
16892
+ var NO_FCB = {
16893
+ satWlFcb: /* @__PURE__ */ new Map(),
16894
+ satNlFcb: /* @__PURE__ */ new Map()
16895
+ };
16597
16896
  function matVec(P, h) {
16598
16897
  const n = P.length;
16599
16898
  const out = new Array(n).fill(0);
@@ -16605,6 +16904,34 @@ function matVec(P, h) {
16605
16904
  }
16606
16905
  return out;
16607
16906
  }
16907
+ function tdLsq(qs) {
16908
+ const M = [0, 1, 2, 3].map(() => [0, 0, 0, 0, 0]);
16909
+ for (const q of qs) {
16910
+ const a = [-q.v.e[0], -q.v.e[1], -q.v.e[2], 1];
16911
+ for (let i = 0; i < 4; i++) {
16912
+ M[i][4] += a[i] * q.d;
16913
+ for (let j = 0; j < 4; j++) M[i][j] += a[i] * a[j];
16914
+ }
16915
+ }
16916
+ for (let c = 0; c < 4; c++) {
16917
+ let p = c;
16918
+ for (let r = c + 1; r < 4; r++)
16919
+ if (Math.abs(M[r][c]) > Math.abs(M[p][c])) p = r;
16920
+ if (Math.abs(M[p][c]) < 1e-9) return null;
16921
+ [M[c], M[p]] = [M[p], M[c]];
16922
+ for (let r = 0; r < 4; r++) {
16923
+ if (r === c) continue;
16924
+ const f = M[r][c] / M[c][c];
16925
+ for (let k = c; k < 5; k++) M[r][k] -= f * M[c][k];
16926
+ }
16927
+ }
16928
+ return [0, 1, 2, 3].map((i) => M[i][4] / M[i][i]);
16929
+ }
16930
+ function tdResidual(q, s) {
16931
+ return q.d - (-q.v.e[0] * s[0] - q.v.e[1] * s[1] - q.v.e[2] * s[2] + s[3]);
16932
+ }
16933
+ var TD_SLIP_M = 0.08;
16934
+ var CODE_GATE_M = 15;
16608
16935
  function ekfUpdateScalar(x, P, h, innovation, r, rejectK = 0) {
16609
16936
  const n = x.length;
16610
16937
  const Ph = matVec(P, h);
@@ -16637,10 +16964,17 @@ function ifCoeffs(f1, f2) {
16637
16964
  return { g, lambdaIf };
16638
16965
  }
16639
16966
  var Sp3EphemerisSource = class {
16640
- constructor(sp3, clk) {
16967
+ constructor(sp3, clk, osb) {
16641
16968
  this.sp3 = sp3;
16642
16969
  this.clk = clk;
16970
+ this.osb = osb;
16971
+ if (osb) {
16972
+ this.codeBias = (prn, code, t) => osb.get(prn, "C" + code, t);
16973
+ this.phaseBias = (prn, code, t) => osb.get(prn, "L" + code, t);
16974
+ }
16643
16975
  }
16976
+ codeBias;
16977
+ phaseBias;
16644
16978
  satState(prn, tEmitMs) {
16645
16979
  const p = sp3Position(this.sp3, prn, tEmitMs);
16646
16980
  if (!p || p.clk == null) return null;
@@ -16658,6 +16992,21 @@ var Sp3EphemerisSource = class {
16658
16992
  return { x: p.x, y: p.y, z: p.z, vx, vy, vz, clkS };
16659
16993
  }
16660
16994
  };
16995
+ function applySatBiases(source, o, tMs) {
16996
+ if (!source.codeBias && !source.phaseBias) return o;
16997
+ const b1 = source.codeBias?.(o.prn, o.code1 ?? "", tMs) ?? 0;
16998
+ const b2 = source.codeBias?.(o.prn, o.code2 ?? "", tMs) ?? 0;
16999
+ let l1 = o.l1;
17000
+ let l2 = o.l2;
17001
+ if (source.phaseBias) {
17002
+ const p1 = source.phaseBias(o.prn, o.phase1 ?? o.code1 ?? "", tMs);
17003
+ const p2 = source.phaseBias(o.prn, o.phase2 ?? o.code2 ?? "", tMs);
17004
+ if (p1 == null || p2 == null) return null;
17005
+ l1 -= p1 / (C_LIGHT / o.f1);
17006
+ l2 -= p2 / (C_LIGHT / o.f2);
17007
+ }
17008
+ return { ...o, c1: o.c1 - b1, c2: o.c2 - b2, l1, l2 };
17009
+ }
16661
17010
  function sagnacRotate(x, y, z, travelS) {
16662
17011
  const theta = OMEGA_E * travelS;
16663
17012
  const c = Math.cos(theta);
@@ -16719,6 +17068,16 @@ var PppEngine = class {
16719
17068
  arcs;
16720
17069
  arcSnap;
16721
17070
  mwState;
17071
+ /** Previous epoch's phase terms per satellite, for the time-differenced
17072
+ * (clock-free) cycle-slip test. */
17073
+ tdPrev;
17074
+ /** Consecutive gated-out phase updates per satellite. */
17075
+ phaseRejects;
17076
+ /** PPP-AR: the latest accepted fix (null: float). */
17077
+ lastFix;
17078
+ lastArMs;
17079
+ firstFixMs;
17080
+ fixedEpochs;
16722
17081
  series;
16723
17082
  ei = 0;
16724
17083
  t0 = 0;
@@ -16754,6 +17113,12 @@ var PppEngine = class {
16754
17113
  this.arcs = [];
16755
17114
  this.arcSnap = /* @__PURE__ */ new Map();
16756
17115
  this.mwState = /* @__PURE__ */ new Map();
17116
+ this.tdPrev = /* @__PURE__ */ new Map();
17117
+ this.phaseRejects = /* @__PURE__ */ new Map();
17118
+ this.lastFix = null;
17119
+ this.lastArMs = -Infinity;
17120
+ this.firstFixMs = null;
17121
+ this.fixedEpochs = 0;
16757
17122
  this.series = [];
16758
17123
  this.ei = 0;
16759
17124
  this.t0 = 0;
@@ -16847,7 +17212,9 @@ var PppEngine = class {
16847
17212
  lastSeen,
16848
17213
  arcSnap,
16849
17214
  mwState,
16850
- series
17215
+ series,
17216
+ tdPrev,
17217
+ phaseRejects
16851
17218
  } = this;
16852
17219
  const flushArc = this.flushArc.bind(this);
16853
17220
  const detectSlip = this.detectSlip.bind(this);
@@ -16869,13 +17236,15 @@ var PppEngine = class {
16869
17236
  for (const ci of clkIdx.values()) P[ci][ci] = CLK_VAR;
16870
17237
  P[IDX_ZWD][IDX_ZWD] += ztdQ * (ei > 0 ? 1 : 0);
16871
17238
  const rcv = [x[IDX_X], x[IDX_Y], x[IDX_Z]];
16872
- const [latDeg, , heightM] = ecefToGeodetic(rcv[0], rcv[1], rcv[2]);
16873
- const latRad = latDeg * Math.PI / 180;
17239
+ const [latRad, , heightM] = ecefToGeodetic(rcv[0], rcv[1], rcv[2]);
16874
17240
  const epochCtx = corrections?.epochContext?.(epoch.timeMs, rcv) ?? null;
16875
17241
  const vis = [];
16876
17242
  let epochZhd = 0;
16877
- for (const o of epoch.obs) {
16878
- if (o.c1 === 0 || o.c2 === 0 || o.l1 === 0 || o.l2 === 0) continue;
17243
+ for (const raw of epoch.obs) {
17244
+ if (raw.c1 === 0 || raw.c2 === 0 || raw.l1 === 0 || raw.l2 === 0)
17245
+ continue;
17246
+ const o = applySatBiases(source, raw, epoch.timeMs);
17247
+ if (!o) continue;
16879
17248
  const slip = detectSlip(o, ei);
16880
17249
  const sat = satStateAtEmission(source, o.prn, epoch.timeMs, rcv);
16881
17250
  if (!sat) continue;
@@ -16897,9 +17266,7 @@ var PppEngine = class {
16897
17266
  const lam1 = C_LIGHT / o.f1;
16898
17267
  const lam2 = C_LIGHT / o.f2;
16899
17268
  if (o.c1 === 0 || o.c2 === 0 || o.l1 === 0 || o.l2 === 0) continue;
16900
- const b1 = source.codeBias?.(o.prn, o.code1 ?? "", epoch.timeMs) ?? 0;
16901
- const b2 = source.codeBias?.(o.prn, o.code2 ?? "", epoch.timeMs) ?? 0;
16902
- const pIf = g * (o.c1 - b1) - (g - 1) * (o.c2 - b2);
17269
+ const pIf = g * o.c1 - (g - 1) * o.c2;
16903
17270
  const lIf = g * (o.l1 * lam1) - (g - 1) * (o.l2 * lam2);
16904
17271
  if (!Number.isFinite(pIf) || !Number.isFinite(lIf)) continue;
16905
17272
  const trop = opts.troposphere ?? true ? niellMapping(elRad, latRad, heightM, epoch.timeMs) : { zhd: 0, mh: 0, mw: 0 };
@@ -16932,9 +17299,63 @@ var PppEngine = class {
16932
17299
  windupM: corr.phaseWindupM,
16933
17300
  slip,
16934
17301
  f1: o.f1,
16935
- f2: o.f2
17302
+ f2: o.f2,
17303
+ sat: [sat.state.x, sat.state.y, sat.state.z]
16936
17304
  });
16937
17305
  }
17306
+ {
17307
+ const dq = [];
17308
+ for (const v of vis) {
17309
+ const prev = tdPrev.get(v.prn);
17310
+ const model = -v.satClkM + v.tropoFixed + v.windupM;
17311
+ if (prev && prev.ei === ei - 1 && !v.slip) {
17312
+ const r0 = Math.hypot(
17313
+ prev.sat[0] - rcv[0],
17314
+ prev.sat[1] - rcv[1],
17315
+ prev.sat[2] - rcv[2]
17316
+ );
17317
+ dq.push({
17318
+ v,
17319
+ d: v.lIf - prev.lIf - (v.range - r0) - (model - prev.model)
17320
+ });
17321
+ }
17322
+ tdPrev.set(v.prn, { ei, lIf: v.lIf, sat: v.sat, model });
17323
+ }
17324
+ if (!kinematic) {
17325
+ if (dq.length >= 4) {
17326
+ const sorted = dq.map((q) => q.d).sort((a, b) => a - b);
17327
+ const med = sorted[Math.floor(sorted.length / 2)];
17328
+ for (const q of dq)
17329
+ if (Math.abs(q.d - med) > TD_SLIP_M) q.v.slip = true;
17330
+ }
17331
+ } else if (dq.length >= 6) {
17332
+ let use = dq;
17333
+ for (let iter = 0; iter < 6; iter++) {
17334
+ const sol = use.length >= 6 ? tdLsq(use) : null;
17335
+ if (!sol) {
17336
+ for (const q of dq) q.v.slip = true;
17337
+ break;
17338
+ }
17339
+ let worst = -1;
17340
+ let worstR = 0;
17341
+ use.forEach((q, k) => {
17342
+ const r = Math.abs(tdResidual(q, sol));
17343
+ if (r > worstR) {
17344
+ worstR = r;
17345
+ worst = k;
17346
+ }
17347
+ });
17348
+ if (worstR <= TD_SLIP_M || iter === 5) {
17349
+ for (const q of dq)
17350
+ if (Math.abs(tdResidual(q, sol)) > TD_SLIP_M) q.v.slip = true;
17351
+ break;
17352
+ }
17353
+ use = use.filter((_, k) => k !== worst);
17354
+ }
17355
+ } else {
17356
+ for (const q of dq) q.v.slip = true;
17357
+ }
17358
+ }
16938
17359
  const zwd0 = x[IDX_ZWD];
16939
17360
  const bySys = /* @__PURE__ */ new Map();
16940
17361
  for (const v of vis) {
@@ -16944,6 +17365,20 @@ var PppEngine = class {
16944
17365
  if (arr) arr.push(r);
16945
17366
  else bySys.set(sys, [r]);
16946
17367
  }
17368
+ const posSigma = Math.sqrt(
17369
+ P[IDX_X][IDX_X] + P[IDX_Y][IDX_Y] + P[IDX_Z][IDX_Z]
17370
+ );
17371
+ const codeGate = CODE_GATE_M + 3 * posSigma;
17372
+ const screened = /* @__PURE__ */ new Set();
17373
+ for (const [sys, res] of bySys) {
17374
+ if (res.length < 4) continue;
17375
+ const med = [...res].sort((a, b) => a - b)[Math.floor(res.length / 2)];
17376
+ for (const v of vis) {
17377
+ if (v.prn[0] !== sys) continue;
17378
+ const r = v.pIf - (v.range - v.satClkM + v.tropoFixed + zwd0 * v.mw);
17379
+ if (Math.abs(r - med) > codeGate) screened.add(v.prn);
17380
+ }
17381
+ }
16947
17382
  for (const [sys, res] of bySys) {
16948
17383
  const ci = clockOf(sys);
16949
17384
  if (!clockSeeded.has(sys)) {
@@ -16954,9 +17389,11 @@ var PppEngine = class {
16954
17389
  P[ci][ci] = CLK_VAR;
16955
17390
  }
16956
17391
  let nSats = 0;
16957
- for (const v of vis) {
17392
+ const live = vis.filter((v) => !screened.has(v.prn));
17393
+ const rangeNow = (v) => v.range - (v.e[0] * (x[IDX_X] - rcv[0]) + v.e[1] * (x[IDX_Y] - rcv[1]) + v.e[2] * (x[IDX_Z] - rcv[2]));
17394
+ const prepAmbiguity = (v) => {
16958
17395
  const ci = clockOf(v.prn[0]);
16959
- const common = v.range + x[ci] - v.satClkM + v.tropoFixed + zwd0 * v.mw;
17396
+ const common = rangeNow(v) + x[ci] - v.satClkM + v.tropoFixed + zwd0 * v.mw;
16960
17397
  if (v.slip) flushArc(v.prn);
16961
17398
  let ai = ambIdx.get(v.prn);
16962
17399
  if (ai === void 0 || v.slip) {
@@ -16973,67 +17410,91 @@ var PppEngine = class {
16973
17410
  }
16974
17411
  ambIdx.set(v.prn, ai);
16975
17412
  }
16976
- const n = x.length;
17413
+ return ai;
17414
+ };
17415
+ const codeUpdate = (v) => {
17416
+ const ci = clockOf(v.prn[0]);
17417
+ const common = rangeNow(v) + x[ci] - v.satClkM + v.tropoFixed + x[IDX_ZWD] * v.mw;
17418
+ const sinEl = Math.max(Math.sin(v.elRad), 0.1);
17419
+ const h = new Array(x.length).fill(0);
17420
+ h[IDX_X] = -v.e[0];
17421
+ h[IDX_Y] = -v.e[1];
17422
+ h[IDX_Z] = -v.e[2];
17423
+ h[ci] = 1;
17424
+ h[IDX_ZWD] = v.mw;
17425
+ const innov = v.pIf - common;
17426
+ if (Math.abs(innov) < 1e6)
17427
+ ekfUpdateScalar(x, P, h, innov, (codeSigma / sinEl) ** 2, 4);
17428
+ };
17429
+ const phaseUpdate = (v, ai) => {
17430
+ const ci = clockOf(v.prn[0]);
16977
17431
  const sinEl = Math.max(Math.sin(v.elRad), 0.1);
16978
- const rCode = (codeSigma / sinEl) ** 2;
16979
17432
  const rPhase = (phaseSigma / sinEl) ** 2;
16980
- {
16981
- const h = new Array(n).fill(0);
17433
+ const commonPh = rangeNow(v) + x[ci] - v.satClkM + v.tropoFixed + x[IDX_ZWD] * v.mw;
17434
+ const innov = v.lIf - (commonPh + v.windupM + x[ai]);
17435
+ const reinit = (phaseRejects.get(v.prn) ?? 0) >= 3;
17436
+ let used = false;
17437
+ if (!reinit) {
17438
+ const h = new Array(x.length).fill(0);
16982
17439
  h[IDX_X] = -v.e[0];
16983
17440
  h[IDX_Y] = -v.e[1];
16984
17441
  h[IDX_Z] = -v.e[2];
16985
17442
  h[ci] = 1;
16986
17443
  h[IDX_ZWD] = v.mw;
16987
- const innov = v.pIf - common;
16988
- if (Math.abs(innov) < 1e6) ekfUpdateScalar(x, P, h, innov, rCode, 4);
17444
+ h[ai] = 1;
17445
+ used = ekfUpdateScalar(x, P, h, innov, rPhase, 4);
16989
17446
  }
16990
- {
16991
- const commonPh = v.range + x[ci] - v.satClkM + v.tropoFixed + x[IDX_ZWD] * v.mw;
16992
- const innov = v.lIf - (commonPh + v.windupM + x[ai]);
16993
- if (Math.abs(innov) > 0.15) {
16994
- flushArc(v.prn);
16995
- x[ai] = v.lIf - (commonPh + v.windupM);
16996
- for (let j = 0; j < P.length; j++) {
16997
- P[ai][j] = 0;
16998
- P[j][ai] = 0;
16999
- }
17000
- P[ai][ai] = 100 * 100;
17001
- } else {
17002
- const h = new Array(n).fill(0);
17003
- h[IDX_X] = -v.e[0];
17004
- h[IDX_Y] = -v.e[1];
17005
- h[IDX_Z] = -v.e[2];
17006
- h[ci] = 1;
17007
- h[IDX_ZWD] = v.mw;
17008
- h[ai] = 1;
17009
- ekfUpdateScalar(x, P, h, innov, rPhase, 4);
17010
- }
17011
- }
17012
- {
17013
- const elevDeg = v.elRad * 180 / Math.PI;
17014
- const mwMean = mwState.get(v.prn)?.mean ?? 0;
17015
- const s = arcSnap.get(v.prn);
17016
- if (s) {
17017
- s.aIF = x[ai];
17018
- s.mwCyc = mwMean;
17019
- s.elevSum += elevDeg;
17020
- s.nEpochs++;
17021
- s.endMs = epoch.timeMs;
17022
- } else {
17023
- arcSnap.set(v.prn, {
17024
- aIF: x[ai],
17025
- mwCyc: mwMean,
17026
- f1: v.f1,
17027
- f2: v.f2,
17028
- elevSum: elevDeg,
17029
- nEpochs: 1,
17030
- startMs: epoch.timeMs,
17031
- endMs: epoch.timeMs
17032
- });
17447
+ if (reinit) {
17448
+ flushArc(v.prn);
17449
+ x[ai] = v.lIf - (commonPh + v.windupM);
17450
+ for (let j = 0; j < P.length; j++) {
17451
+ P[ai][j] = 0;
17452
+ P[j][ai] = 0;
17033
17453
  }
17454
+ P[ai][ai] = 100 * 100;
17455
+ phaseRejects.delete(v.prn);
17456
+ } else if (used) phaseRejects.delete(v.prn);
17457
+ else phaseRejects.set(v.prn, (phaseRejects.get(v.prn) ?? 0) + 1);
17458
+ };
17459
+ const snapshot = (v, ai) => {
17460
+ const elevDeg = v.elRad * 180 / Math.PI;
17461
+ const mwMean = mwState.get(v.prn)?.mean ?? 0;
17462
+ const s = arcSnap.get(v.prn);
17463
+ if (s) {
17464
+ s.aIF = x[ai];
17465
+ s.mwCyc = mwMean;
17466
+ s.elevSum += elevDeg;
17467
+ s.nEpochs++;
17468
+ s.endMs = epoch.timeMs;
17469
+ } else {
17470
+ arcSnap.set(v.prn, {
17471
+ aIF: x[ai],
17472
+ mwCyc: mwMean,
17473
+ f1: v.f1,
17474
+ f2: v.f2,
17475
+ elevSum: elevDeg,
17476
+ nEpochs: 1,
17477
+ startMs: epoch.timeMs,
17478
+ endMs: epoch.timeMs
17479
+ });
17034
17480
  }
17035
17481
  lastSeen.set(v.prn, ei);
17036
17482
  nSats++;
17483
+ };
17484
+ if (kinematic) {
17485
+ for (const v of live) codeUpdate(v);
17486
+ for (const v of live) {
17487
+ const ai = prepAmbiguity(v);
17488
+ phaseUpdate(v, ai);
17489
+ snapshot(v, ai);
17490
+ }
17491
+ } else {
17492
+ for (const v of live) {
17493
+ const ai = prepAmbiguity(v);
17494
+ codeUpdate(v);
17495
+ phaseUpdate(v, ai);
17496
+ snapshot(v, ai);
17497
+ }
17037
17498
  }
17038
17499
  let sumPhaseResSq = 0;
17039
17500
  let nPhaseRes = 0;
@@ -17044,7 +17505,7 @@ var PppEngine = class {
17044
17505
  if (ai === void 0) continue;
17045
17506
  const sys = v.prn[0];
17046
17507
  const ci = clockOf(sys);
17047
- const commonPh = v.range + x[ci] - v.satClkM + v.tropoFixed + x[IDX_ZWD] * v.mw;
17508
+ const commonPh = rangeNow(v) + x[ci] - v.satClkM + v.tropoFixed + x[IDX_ZWD] * v.mw;
17048
17509
  const innov = v.lIf - (commonPh + v.windupM + x[ai]);
17049
17510
  if (Math.abs(innov) >= 0.15) continue;
17050
17511
  const arr = bySysRes.get(sys);
@@ -17068,6 +17529,8 @@ var PppEngine = class {
17068
17529
  flushArc(prn);
17069
17530
  }
17070
17531
  }
17532
+ this.tryFix(epoch.timeMs, vis);
17533
+ if (this.lastFix) this.fixedEpochs++;
17071
17534
  const pos = [x[IDX_X], x[IDX_Y], x[IDX_Z]];
17072
17535
  let enu = null;
17073
17536
  let error3d = null;
@@ -17103,12 +17566,75 @@ var PppEngine = class {
17103
17566
  nSats,
17104
17567
  ztdWet: x[IDX_ZWD],
17105
17568
  ztdHydrostatic: epochZhd,
17106
- phaseResRms: nPhaseRes > 0 ? Math.sqrt(sumPhaseResSq / nPhaseRes) : 0
17569
+ phaseResRms: nPhaseRes > 0 ? Math.sqrt(sumPhaseResSq / nPhaseRes) : 0,
17570
+ nFixed: this.lastFix?.nFixed ?? 0,
17571
+ fixedPosition: this.lastFix?.position ?? null
17107
17572
  };
17108
17573
  series.push(result);
17109
17574
  this.ei = ei + 1;
17110
17575
  return result;
17111
17576
  }
17577
+ /**
17578
+ * PPP-AR. Every `everyS`, resolve the converged ambiguities of the
17579
+ * satellites in view (between satellites of one constellation) and, when
17580
+ * the fix passes the ratio, success-rate and χ² tests, condition the
17581
+ * float position on it ({@link fixPppPosition}). The float filter itself
17582
+ * is never constrained — a wrong fix can't contaminate later epochs, and
17583
+ * the next attempt starts clean. Between attempts the last fix carries
17584
+ * over (static) or is dropped (kinematic).
17585
+ */
17586
+ tryFix(tMs, vis) {
17587
+ const ar = this.opts.ar;
17588
+ if (!ar || !this.source.phaseBias) return;
17589
+ const cfg = ar === true ? {} : ar;
17590
+ if (tMs - this.lastArMs < (cfg.everyS ?? 120) * 1e3) {
17591
+ if (this.kinematic) this.lastFix = null;
17592
+ return;
17593
+ }
17594
+ this.lastArMs = tMs;
17595
+ this.lastFix = null;
17596
+ const { x, P, ambIdx, arcSnap, mwState } = this;
17597
+ const minArcMs = (cfg.minArcS ?? 600) * 1e3;
17598
+ const ambiguities = [];
17599
+ for (const v of vis) {
17600
+ const index = ambIdx.get(v.prn);
17601
+ const snap = arcSnap.get(v.prn);
17602
+ const mw = mwState.get(v.prn);
17603
+ if (index === void 0 || !snap || !mw) continue;
17604
+ if (tMs - snap.startMs < minArcMs || snap.nEpochs < 20) continue;
17605
+ ambiguities.push({
17606
+ prn: v.prn,
17607
+ index,
17608
+ aIF: x[index],
17609
+ mwCyc: mw.mean,
17610
+ f1: v.f1,
17611
+ f2: v.f2,
17612
+ elevDeg: v.elRad * 180 / Math.PI
17613
+ });
17614
+ }
17615
+ const fx = fixPppPosition(
17616
+ {
17617
+ position: [x[IDX_X], x[IDX_Y], x[IDX_Z]],
17618
+ covariance: P,
17619
+ ambiguities
17620
+ },
17621
+ NO_FCB,
17622
+ {
17623
+ ratioThreshold: cfg.ratio ?? 3,
17624
+ wlThreshold: cfg.wlThreshold ?? 0.15,
17625
+ minSuccessRate: cfg.successRate ?? 0.999,
17626
+ minFixed: cfg.minFixed ?? 5,
17627
+ maxChi2: cfg.maxChi2 ?? 20
17628
+ }
17629
+ );
17630
+ if (!fx.fixed) return;
17631
+ this.lastFix = {
17632
+ position: fx.position,
17633
+ nFixed: fx.nFixed,
17634
+ ratio: fx.ratio
17635
+ };
17636
+ this.firstFixMs ??= tMs;
17637
+ }
17112
17638
  /** Finalise the session: capture the AR state, flush open arcs, assemble. */
17113
17639
  solution() {
17114
17640
  const { opts, x, P, ambIdx, arcSnap, mwState, arcs, series, truth } = this;
@@ -17141,7 +17667,12 @@ var PppEngine = class {
17141
17667
  x[IDX_Y],
17142
17668
  x[IDX_Z]
17143
17669
  ];
17144
- const llh = ecefToGeodetic(finalPos[0], finalPos[1], finalPos[2]);
17670
+ const geo = ecefToGeodetic(finalPos[0], finalPos[1], finalPos[2]);
17671
+ const llh = [
17672
+ geo[0] * 180 / Math.PI,
17673
+ geo[1] * 180 / Math.PI,
17674
+ geo[2]
17675
+ ];
17145
17676
  const finalError3d = truth ? Math.hypot(
17146
17677
  finalPos[0] - truth[0],
17147
17678
  finalPos[1] - truth[1],
@@ -17157,218 +17688,25 @@ var PppEngine = class {
17157
17688
  convergenceSec: this.convergenceSec,
17158
17689
  finalError3d,
17159
17690
  arcs,
17160
- finalState
17691
+ finalState,
17692
+ ...opts.ar && this.source.phaseBias ? {
17693
+ ar: {
17694
+ firstFixSec: this.firstFixMs === null || !series.length ? null : (this.firstFixMs - series[0].timeMs) / 1e3,
17695
+ nFixedEnd: this.lastFix?.nFixed ?? 0,
17696
+ position: this.lastFix?.position ?? null,
17697
+ fixedEpochShare: series.length ? this.fixedEpochs / series.length : 0
17698
+ }
17699
+ } : {}
17161
17700
  };
17162
17701
  }
17163
17702
  };
17164
17703
  function solvePpp(epochs, sp3OrSource, opts) {
17165
- const source = "satState" in sp3OrSource ? sp3OrSource : new Sp3EphemerisSource(sp3OrSource, opts.clk);
17704
+ const source = "satState" in sp3OrSource ? sp3OrSource : new Sp3EphemerisSource(sp3OrSource, opts.clk, opts.osb);
17166
17705
  const eng = new PppEngine(opts, source);
17167
17706
  for (const epoch of epochs) eng.process(epoch);
17168
17707
  return eng.solution();
17169
17708
  }
17170
17709
 
17171
- // src/positioning/ppp-ar.ts
17172
- function wlWavelength(f1, f2) {
17173
- return C_LIGHT / (f1 - f2);
17174
- }
17175
- function nlWavelength(f1, f2) {
17176
- return C_LIGHT / (f1 + f2);
17177
- }
17178
- function resolvePppAmbiguities(sats, Qaif, opts = {}) {
17179
- const wlThreshold = opts.wlThreshold ?? 0.25;
17180
- const ratioThreshold = opts.ratioThreshold ?? 3;
17181
- const kept = [];
17182
- const nWl = [];
17183
- const rejectedWl = [];
17184
- for (let i = 0; i < sats.length; i++) {
17185
- const s = sats[i];
17186
- const wlFloat = s.mwCyc - (s.wlBiasCyc ?? 0);
17187
- const r = Math.round(wlFloat);
17188
- if (Math.abs(wlFloat - r) <= wlThreshold) {
17189
- kept.push(i);
17190
- nWl.push(r);
17191
- } else {
17192
- rejectedWl.push(s.prn);
17193
- }
17194
- }
17195
- if (kept.length < 2) {
17196
- return {
17197
- fixed: false,
17198
- ratio: 0,
17199
- refPrn: null,
17200
- sats: [],
17201
- rejectedWl
17202
- };
17203
- }
17204
- const n1Float = [];
17205
- const lamNl = [];
17206
- for (let k = 0; k < kept.length; k++) {
17207
- const s = sats[kept[k]];
17208
- const lNl = nlWavelength(s.f1, s.f2);
17209
- lamNl.push(lNl);
17210
- const n1 = s.aIF / lNl - s.f2 / (s.f1 - s.f2) * nWl[k] - (s.nlBiasCyc ?? 0);
17211
- n1Float.push(n1);
17212
- }
17213
- let ref = 0;
17214
- for (let k = 1; k < kept.length; k++) {
17215
- const e = sats[kept[k]].elevDeg ?? -1;
17216
- if (e > (sats[kept[ref]].elevDeg ?? -1)) ref = k;
17217
- }
17218
- const m = kept.length - 1;
17219
- const sd2 = new Float64Array(m);
17220
- const Qsd = new Float64Array(m * m);
17221
- const others = [];
17222
- for (let k = 0; k < kept.length; k++) if (k !== ref) others.push(k);
17223
- const n = sats.length;
17224
- const qN1 = (ka, kb) => Qaif[kept[ka] * n + kept[kb]] / (lamNl[ka] * lamNl[kb]);
17225
- for (let a = 0; a < m; a++) {
17226
- const ka = others[a];
17227
- sd2[a] = n1Float[ka] - n1Float[ref];
17228
- for (let b = 0; b < m; b++) {
17229
- const kb = others[b];
17230
- Qsd[a * m + b] = qN1(ka, kb) - qN1(ka, ref) - qN1(ref, kb) + qN1(ref, ref);
17231
- }
17232
- }
17233
- const res = lambdaSearch(sd2, Qsd, m, 2);
17234
- const ratio = res ? Math.min(res.ratio, 999.9) : 0;
17235
- const fixed = !!res && ratio >= ratioThreshold;
17236
- const refN1 = Math.round(n1Float[ref]);
17237
- const sdFix = res?.candidates[0];
17238
- const outSats = [];
17239
- for (let k = 0; k < kept.length; k++) {
17240
- const s = sats[kept[k]];
17241
- let n1;
17242
- if (k === ref) {
17243
- n1 = refN1;
17244
- } else {
17245
- const a = others.indexOf(k);
17246
- n1 = fixed && sdFix ? refN1 + Math.round(sdFix[a]) : Math.round(n1Float[k]);
17247
- }
17248
- const lNl = lamNl[k];
17249
- const aIF = lNl * n1 + lNl * (s.f2 / (s.f1 - s.f2)) * nWl[k];
17250
- outSats.push({ prn: s.prn, nWl: nWl[k], n1, aIF });
17251
- }
17252
- return {
17253
- fixed,
17254
- ratio,
17255
- refPrn: sats[kept[ref]].prn,
17256
- sats: outSats,
17257
- rejectedWl
17258
- };
17259
- }
17260
- function solveSpd(A, b) {
17261
- const n = b.length;
17262
- const L = Array.from({ length: n }, () => new Array(n).fill(0));
17263
- for (let i = 0; i < n; i++) {
17264
- for (let j = 0; j <= i; j++) {
17265
- let s = A[i][j];
17266
- for (let k = 0; k < j; k++) s -= L[i][k] * L[j][k];
17267
- if (i === j) {
17268
- if (s <= 0) return null;
17269
- L[i][j] = Math.sqrt(s);
17270
- } else {
17271
- L[i][j] = s / L[j][j];
17272
- }
17273
- }
17274
- }
17275
- const y = new Array(n).fill(0);
17276
- for (let i = 0; i < n; i++) {
17277
- let s = b[i];
17278
- for (let k = 0; k < i; k++) s -= L[i][k] * y[k];
17279
- y[i] = s / L[i][i];
17280
- }
17281
- const x = new Array(n).fill(0);
17282
- for (let i = n - 1; i >= 0; i--) {
17283
- let s = y[i];
17284
- for (let k = i + 1; k < n; k++) s -= L[k][i] * x[k];
17285
- x[i] = s / L[i][i];
17286
- }
17287
- return x;
17288
- }
17289
- function fixPppPosition(state, fcb, opts = {}) {
17290
- const floatPos = state.position;
17291
- const noFix = {
17292
- fixed: false,
17293
- position: floatPos,
17294
- floatPosition: floatPos,
17295
- shift: [0, 0, 0],
17296
- nFixed: 0,
17297
- ratio: 0,
17298
- refPrn: null
17299
- };
17300
- const amb = state.ambiguities;
17301
- if (amb.length < 3) return noFix;
17302
- const sats = amb.map((a) => ({
17303
- prn: a.prn,
17304
- aIF: a.aIF,
17305
- mwCyc: a.mwCyc,
17306
- f1: a.f1,
17307
- f2: a.f2,
17308
- wlBiasCyc: fcb.satWlFcb.get(a.prn) ?? 0,
17309
- nlBiasCyc: fcb.satNlFcb.get(a.prn) ?? 0,
17310
- elevDeg: a.elevDeg
17311
- }));
17312
- const n = sats.length;
17313
- const Qaif = new Float64Array(n * n);
17314
- for (let i = 0; i < n; i++)
17315
- for (let j = 0; j < n; j++)
17316
- Qaif[i * n + j] = state.covariance[amb[i].index][amb[j].index];
17317
- const res = resolvePppAmbiguities(sats, Qaif, opts);
17318
- if (!res.fixed || res.refPrn === null) {
17319
- return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
17320
- }
17321
- const idxOfPrn = new Map(amb.map((a) => [a.prn, a.index]));
17322
- const floatOfPrn = new Map(amb.map((a) => [a.prn, a.aIF]));
17323
- const kept = res.sats.filter((s) => idxOfPrn.has(s.prn));
17324
- const refPos = kept.findIndex((s) => s.prn === res.refPrn);
17325
- if (refPos < 0 || kept.length < 2) {
17326
- return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
17327
- }
17328
- const ambIdx = kept.map((s) => idxOfPrn.get(s.prn));
17329
- const aFloat = kept.map((s) => floatOfPrn.get(s.prn));
17330
- const aFixed = kept.map((s) => s.aIF);
17331
- const others = kept.map((_, i) => i).filter((i) => i !== refPos);
17332
- const m = others.length;
17333
- const P = state.covariance;
17334
- const qAA = (i, j) => P[ambIdx[i]][ambIdx[j]];
17335
- const qXA = (r, i) => P[r][ambIdx[i]];
17336
- const Qzz = Array.from({ length: m }, () => new Array(m).fill(0));
17337
- const z = new Array(m).fill(0);
17338
- for (let a = 0; a < m; a++) {
17339
- const ia = others[a];
17340
- z[a] = aFloat[ia] - aFloat[refPos] - (aFixed[ia] - aFixed[refPos]);
17341
- for (let b = 0; b < m; b++) {
17342
- const ib = others[b];
17343
- Qzz[a][b] = qAA(ia, ib) - qAA(ia, refPos) - qAA(refPos, ib) + qAA(refPos, refPos);
17344
- }
17345
- }
17346
- const Qxz = [0, 1, 2].map(
17347
- (r) => others.map((ia) => qXA(r, ia) - qXA(r, refPos))
17348
- );
17349
- const y = solveSpd(Qzz, z);
17350
- if (!y) return { ...noFix, ratio: res.ratio, refPrn: res.refPrn };
17351
- const shift = [0, 0, 0];
17352
- for (let r = 0; r < 3; r++) {
17353
- let s = 0;
17354
- for (let a = 0; a < m; a++) s += Qxz[r][a] * y[a];
17355
- shift[r] = -s;
17356
- }
17357
- return {
17358
- fixed: true,
17359
- position: [
17360
- floatPos[0] + shift[0],
17361
- floatPos[1] + shift[1],
17362
- floatPos[2] + shift[2]
17363
- ],
17364
- floatPosition: floatPos,
17365
- shift,
17366
- nFixed: m,
17367
- ratio: res.ratio,
17368
- refPrn: res.refPrn
17369
- };
17370
- }
17371
-
17372
17710
  // src/positioning/ppp-fcb.ts
17373
17711
  function extractWidelaneArcs(station, epochs, opts = {}) {
17374
17712
  const slipCyc = opts.slipCyc ?? 4;
@@ -20228,6 +20566,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
20228
20566
  SYSTEM_NAMES,
20229
20567
  SYSTEM_ORDER,
20230
20568
  SYS_SHORT,
20569
+ SatOsbTable,
20231
20570
  SbasProcessor,
20232
20571
  Scale,
20233
20572
  Sp3EphemerisSource,