gnss-js 1.50.0 → 1.52.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.
@@ -705,7 +705,7 @@ function parseSbfGloNav(data) {
705
705
  }
706
706
 
707
707
  // src/sbf/rawnav-sbas.ts
708
- function feedGeoBlock(view, b) {
708
+ function feedGeoBlock(view, b, onSbasMessage) {
709
709
  const tow = view.getUint32(b + 8, true);
710
710
  const wnc = view.getUint16(b + 12, true);
711
711
  const svid = view.getUint8(b + 14);
@@ -721,10 +721,11 @@ function feedGeoBlock(view, b) {
721
721
  }
722
722
  if (!sbasCrcOk(msg)) return { badCrc: true };
723
723
  const prn = Number(prnStr.slice(1)) + 100;
724
+ onSbasMessage?.(msg, prn, wnc, tow / 1e3);
724
725
  const eph = decodeSbasGeoNav(msg, prn, wnc, tow / 1e3);
725
726
  return eph ? { eph } : {};
726
727
  }
727
- function parseSbfGeoNav(data) {
728
+ function parseSbfGeoNav(data, opts = {}) {
728
729
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
729
730
  const ephemerides = [];
730
731
  const seen = /* @__PURE__ */ new Set();
@@ -733,7 +734,7 @@ function parseSbfGeoNav(data) {
733
734
  scanSbfFrames(data, view, (id, b, len) => {
734
735
  if (id !== 4020 || len < 52) return;
735
736
  messages++;
736
- const r = feedGeoBlock(view, b);
737
+ const r = feedGeoBlock(view, b, opts.onSbasMessage);
737
738
  if (r.eph) {
738
739
  const key = `${r.eph.prn}|${r.eph.tocDate.getTime()}`;
739
740
  if (!seen.has(key)) {
@@ -753,7 +754,7 @@ function ephKey(e) {
753
754
  const source = "source" in e ? e.source : "";
754
755
  return `${e.prn}|${e.tocDate.getTime()}|${source}`;
755
756
  }
756
- function decodeSbfNavigation(data) {
757
+ function decodeSbfNavigation(data, opts = {}) {
757
758
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
758
759
  const ephemerides = [];
759
760
  const seen = /* @__PURE__ */ new Set();
@@ -874,7 +875,7 @@ function decodeSbfNavigation(data) {
874
875
  case 4020: {
875
876
  if (len < 52) return;
876
877
  counts.sbasRaw++;
877
- const r = feedGeoBlock(view, b);
878
+ const r = feedGeoBlock(view, b, opts.onSbasMessage);
878
879
  if (r.eph) addEph(r.eph);
879
880
  else if (r.badCrc) counts.badFrames++;
880
881
  return;
@@ -3048,6 +3048,65 @@ function sagnac2(pos, travelTimeS) {
3048
3048
  const s = Math.sin(theta);
3049
3049
  return [pos.x * c + pos.y * s, -pos.x * s + pos.y * c, pos.z];
3050
3050
  }
3051
+ function sbasAirTropoVar(elRad) {
3052
+ const elDeg = elRad * 180 / Math.PI;
3053
+ const sigMp = 0.13 + 0.53 * Math.exp(-elDeg / 10);
3054
+ const sigNoise = 0.15 + 0.43 * Math.exp(-elDeg / 6.9);
3055
+ const m = 1.001 / Math.sqrt(2001e-6 + Math.sin(elRad) * Math.sin(elRad));
3056
+ const sigTropo = 0.12 * m;
3057
+ return sigMp * sigMp + sigNoise * sigNoise + sigTropo * sigTropo;
3058
+ }
3059
+ function invertMatrix(A) {
3060
+ const n = A.length;
3061
+ const M = A.map((row, i) => [
3062
+ ...row,
3063
+ ...Array.from({ length: n }, (_, j) => i === j ? 1 : 0)
3064
+ ]);
3065
+ for (let col = 0; col < n; col++) {
3066
+ let piv = col;
3067
+ for (let r = col + 1; r < n; r++)
3068
+ if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
3069
+ if (Math.abs(M[piv][col]) < 1e-12) return null;
3070
+ [M[col], M[piv]] = [M[piv], M[col]];
3071
+ const d = M[col][col];
3072
+ for (let j = 0; j < 2 * n; j++) M[col][j] /= d;
3073
+ for (let r = 0; r < n; r++) {
3074
+ if (r === col) continue;
3075
+ const f = M[r][col];
3076
+ for (let j = 0; j < 2 * n; j++) M[r][j] -= f * M[col][j];
3077
+ }
3078
+ }
3079
+ return M.map((row) => row.slice(n));
3080
+ }
3081
+ function sbasProtectionLevels(sats, opts = {}) {
3082
+ const kH = opts.kH ?? 6;
3083
+ const kV = opts.kV ?? 5.33;
3084
+ if (sats.length < 4) return null;
3085
+ const N = [0, 1, 2, 3].map(() => [0, 0, 0, 0]);
3086
+ for (const s of sats) {
3087
+ if (!(s.variance > 0)) continue;
3088
+ const ce = Math.cos(s.elRad);
3089
+ const g = [
3090
+ ce * Math.sin(s.azRad),
3091
+ ce * Math.cos(s.azRad),
3092
+ Math.sin(s.elRad),
3093
+ 1
3094
+ ];
3095
+ const w = 1 / s.variance;
3096
+ for (let i = 0; i < 4; i++)
3097
+ for (let j = 0; j < 4; j++) N[i][j] += w * g[i] * g[j];
3098
+ }
3099
+ const cov = invertMatrix(N);
3100
+ if (!cov) return null;
3101
+ const dE2 = cov[0][0];
3102
+ const dN2 = cov[1][1];
3103
+ const dEN = cov[0][1];
3104
+ const dU = Math.sqrt(Math.max(cov[2][2], 0));
3105
+ const dMajor = Math.sqrt(
3106
+ (dE2 + dN2) / 2 + Math.sqrt(((dE2 - dN2) / 2) ** 2 + dEN * dEN)
3107
+ );
3108
+ return { hpl: kH * dMajor, vpl: kV * dU, dMajor, dU };
3109
+ }
3051
3110
  function solveLinear(A, b) {
3052
3111
  const n = b.length;
3053
3112
  const M = A.map((row, i) => [...row, b[i]]);
@@ -3074,9 +3133,11 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3074
3133
  convergenceM = 1e-4,
3075
3134
  tgd = true,
3076
3135
  iono,
3077
- gim
3136
+ gim,
3137
+ sbas
3078
3138
  } = opts;
3079
3139
  const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
3140
+ const gpsWeek = Math.floor((timeMs - GPS_EPOCH_MS_SPP) / 1e3 / 604800);
3080
3141
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
3081
3142
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
3082
3143
  if (all.length < minSats(all)) return null;
@@ -3102,7 +3163,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3102
3163
  const sysRows = new Map(systems.map((s) => [s, 0]));
3103
3164
  for (const k of Object.keys(residuals2)) delete residuals2[k];
3104
3165
  const positionSaneIter = x9 * x9 + y2 * y2 + z2 * z2 > 1e12;
3105
- const [rxLat, rxLon, rxHgt] = positionSaneIter ? ecefToGeodetic(x9, y2, z2) : [0, 0, 0];
3166
+ const [rxLat2, rxLon2, rxHgt2] = positionSaneIter ? ecefToGeodetic(x9, y2, z2) : [0, 0, 0];
3106
3167
  for (const prn of prns) {
3107
3168
  const psr = pseudoranges.get(prn);
3108
3169
  const eph = ephemerides.get(prn);
@@ -3113,7 +3174,16 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3113
3174
  const sat = computeSatPosition(eph, tTx - dtsClock * 1e3);
3114
3175
  if (!Number.isFinite(sat.x)) continue;
3115
3176
  const isKepler = eph.system !== "R" && eph.system !== "S";
3116
- const dts = dtsClock - (tgd && isKepler ? eph.tgd : 0);
3177
+ let dts = dtsClock - (tgd && isKepler ? eph.tgd : 0);
3178
+ if (sbas) {
3179
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
3180
+ if (sc) {
3181
+ sat.x += sc.dPos[0];
3182
+ sat.y += sc.dPos[1];
3183
+ sat.z += sc.dPos[2];
3184
+ dts += sc.dClkS;
3185
+ }
3186
+ }
3117
3187
  const travel = Math.hypot(sat.x - x9, sat.y - y2, sat.z - z2) / C_LIGHT;
3118
3188
  const [sx, sy, sz] = sagnac2(sat, travel);
3119
3189
  const rho = Math.hypot(sx - x9, sy - y2, sz - z2);
@@ -3132,14 +3202,26 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3132
3202
  const sinEl = Math.max(Math.sin(elev), 0.1);
3133
3203
  weight = sinEl * sinEl;
3134
3204
  }
3135
- const tropo = troposphere && positionSane ? tropoDelay2(elev, rxLat, rxHgt) : 0;
3205
+ const tropo = troposphere && positionSane ? tropoDelay2(elev, rxLat2, rxHgt2) : 0;
3136
3206
  let ionoM = 0;
3137
- if ((gim || iono) && positionSane) {
3207
+ if ((sbas || gim || iono) && positionSane) {
3138
3208
  let l1M = null;
3139
- if (gim)
3140
- l1M = gimSlantIonoDelayL1(gim, rxLat, rxLon, azim, elev, timeMs);
3209
+ if (sbas) {
3210
+ const si = sbas.ionoDelay(
3211
+ gpsWeek,
3212
+ gpsTow,
3213
+ rxLat2,
3214
+ rxLon2,
3215
+ rxHgt2,
3216
+ azim,
3217
+ elev
3218
+ );
3219
+ if (si) l1M = si.delayM;
3220
+ }
3221
+ if (l1M === null && gim)
3222
+ l1M = gimSlantIonoDelayL1(gim, rxLat2, rxLon2, azim, elev, timeMs);
3141
3223
  if (l1M === null && iono) {
3142
- l1M = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow);
3224
+ l1M = C_LIGHT * klobucharDelay(iono, rxLat2, rxLon2, azim, elev, gpsTow);
3143
3225
  }
3144
3226
  if (l1M !== null) {
3145
3227
  const f = PRIMARY_FREQ_HZ[sys] ?? F_L12;
@@ -3212,6 +3294,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3212
3294
  const { x, y, z, clock, residuals, iterations, converged } = inner;
3213
3295
  const used = [];
3214
3296
  const azels = [];
3297
+ const [rxLat, rxLon, rxHgt] = ecefToGeodetic(x, y, z);
3298
+ const sbasRows = [];
3215
3299
  for (const prn of candidates) {
3216
3300
  if (!(prn in residuals)) continue;
3217
3301
  const eph = ephemerides.get(prn);
@@ -3221,8 +3305,27 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3221
3305
  if (azel.el >= elevationMaskDeg * Math.PI / 180) {
3222
3306
  used.push(prn);
3223
3307
  azels.push(azel);
3308
+ if (sbas) {
3309
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
3310
+ const si = sbas.ionoDelay(
3311
+ gpsWeek,
3312
+ gpsTow,
3313
+ rxLat,
3314
+ rxLon,
3315
+ rxHgt,
3316
+ azel.az,
3317
+ azel.el
3318
+ );
3319
+ if (sc && si)
3320
+ sbasRows.push({
3321
+ azRad: azel.az,
3322
+ elRad: azel.el,
3323
+ variance: sc.varM2 + si.varM2 + sbasAirTropoVar(azel.el)
3324
+ });
3325
+ }
3224
3326
  }
3225
3327
  }
3328
+ const pl = sbas ? sbasProtectionLevels(sbasRows) : null;
3226
3329
  return {
3227
3330
  position: [x, y, z],
3228
3331
  clockBias: Object.fromEntries(clock),
@@ -3231,7 +3334,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3231
3334
  residuals,
3232
3335
  dop: computeDop(azels),
3233
3336
  iterations,
3234
- converged
3337
+ converged,
3338
+ ...pl ? { hpl: pl.hpl, vpl: pl.vpl, sbasSats: sbasRows.length } : sbas ? { sbasSats: sbasRows.length } : {}
3235
3339
  };
3236
3340
  }
3237
3341
 
@@ -3265,5 +3369,6 @@ export {
3265
3369
  estimateNetworkFcbs,
3266
3370
  satClockCorrection,
3267
3371
  ionoFree,
3372
+ sbasProtectionLevels,
3268
3373
  solveSpp
3269
3374
  };
@@ -261,7 +261,7 @@ function decodeGpsQzssEphemeris(view, p, sys) {
261
261
  tgd: view.getFloat64(p + 172, true)
262
262
  };
263
263
  }
264
- function parseNovatelNav(data) {
264
+ function parseNovatelNav(data, opts = {}) {
265
265
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
266
266
  const stats = { badCrc: 0 };
267
267
  const ephemerides = [];
@@ -334,6 +334,7 @@ function parseNovatelNav(data) {
334
334
  const prn = view.getUint32(p + 4, true);
335
335
  const msg = new Uint8Array(32);
336
336
  msg.set(data.subarray(p + 12, p + 41));
337
+ opts.onSbasMessage?.(msg, prn, frame.week, frame.towMs / 1e3);
337
338
  const eph = decodeSbasGeoNav(msg, prn, frame.week, frame.towMs / 1e3);
338
339
  if (!eph) continue;
339
340
  const prev = lastGlo.get(eph.prn);
package/dist/index.cjs CHANGED
@@ -294,6 +294,7 @@ __export(src_exports, {
294
294
  rhumbLine: () => rhumbLine,
295
295
  rtcm3Constellation: () => rtcm3Constellation,
296
296
  satClockCorrection: () => satClockCorrection,
297
+ sbasProtectionLevels: () => sbasProtectionLevels,
297
298
  sbfCrc16: () => crc16,
298
299
  scanSbfFrames: () => scanSbfFrames,
299
300
  selectEphemeris: () => selectEphemeris,
@@ -7727,7 +7728,7 @@ function parseSbfGloNav(data) {
7727
7728
  }
7728
7729
 
7729
7730
  // src/sbf/rawnav-sbas.ts
7730
- function feedGeoBlock(view, b) {
7731
+ function feedGeoBlock(view, b, onSbasMessage) {
7731
7732
  const tow = view.getUint32(b + 8, true);
7732
7733
  const wnc = view.getUint16(b + 12, true);
7733
7734
  const svid = view.getUint8(b + 14);
@@ -7743,10 +7744,11 @@ function feedGeoBlock(view, b) {
7743
7744
  }
7744
7745
  if (!sbasCrcOk(msg)) return { badCrc: true };
7745
7746
  const prn = Number(prnStr.slice(1)) + 100;
7747
+ onSbasMessage?.(msg, prn, wnc, tow / 1e3);
7746
7748
  const eph = decodeSbasGeoNav(msg, prn, wnc, tow / 1e3);
7747
7749
  return eph ? { eph } : {};
7748
7750
  }
7749
- function parseSbfGeoNav(data) {
7751
+ function parseSbfGeoNav(data, opts = {}) {
7750
7752
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
7751
7753
  const ephemerides = [];
7752
7754
  const seen = /* @__PURE__ */ new Set();
@@ -7755,7 +7757,7 @@ function parseSbfGeoNav(data) {
7755
7757
  scanSbfFrames(data, view, (id, b, len) => {
7756
7758
  if (id !== 4020 || len < 52) return;
7757
7759
  messages++;
7758
- const r = feedGeoBlock(view, b);
7760
+ const r = feedGeoBlock(view, b, opts.onSbasMessage);
7759
7761
  if (r.eph) {
7760
7762
  const key = `${r.eph.prn}|${r.eph.tocDate.getTime()}`;
7761
7763
  if (!seen.has(key)) {
@@ -7775,7 +7777,7 @@ function ephKey(e) {
7775
7777
  const source = "source" in e ? e.source : "";
7776
7778
  return `${e.prn}|${e.tocDate.getTime()}|${source}`;
7777
7779
  }
7778
- function decodeSbfNavigation(data) {
7780
+ function decodeSbfNavigation(data, opts = {}) {
7779
7781
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
7780
7782
  const ephemerides = [];
7781
7783
  const seen = /* @__PURE__ */ new Set();
@@ -7896,7 +7898,7 @@ function decodeSbfNavigation(data) {
7896
7898
  case 4020: {
7897
7899
  if (len < 52) return;
7898
7900
  counts.sbasRaw++;
7899
- const r = feedGeoBlock(view, b);
7901
+ const r = feedGeoBlock(view, b, opts.onSbasMessage);
7900
7902
  if (r.eph) addEph(r.eph);
7901
7903
  else if (r.badCrc) counts.badFrames++;
7902
7904
  return;
@@ -9368,7 +9370,7 @@ function decodeGpsQzssEphemeris(view, p, sys) {
9368
9370
  tgd: view.getFloat64(p + 172, true)
9369
9371
  };
9370
9372
  }
9371
- function parseNovatelNav(data) {
9373
+ function parseNovatelNav(data, opts = {}) {
9372
9374
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
9373
9375
  const stats = { badCrc: 0 };
9374
9376
  const ephemerides = [];
@@ -9441,6 +9443,7 @@ function parseNovatelNav(data) {
9441
9443
  const prn = view.getUint32(p + 4, true);
9442
9444
  const msg = new Uint8Array(32);
9443
9445
  msg.set(data.subarray(p + 12, p + 41));
9446
+ opts.onSbasMessage?.(msg, prn, frame.week, frame.towMs / 1e3);
9444
9447
  const eph = decodeSbasGeoNav(msg, prn, frame.week, frame.towMs / 1e3);
9445
9448
  if (!eph) continue;
9446
9449
  const prev = lastGlo.get(eph.prn);
@@ -14976,6 +14979,65 @@ function sagnac2(pos, travelTimeS) {
14976
14979
  const s = Math.sin(theta);
14977
14980
  return [pos.x * c + pos.y * s, -pos.x * s + pos.y * c, pos.z];
14978
14981
  }
14982
+ function sbasAirTropoVar(elRad) {
14983
+ const elDeg = elRad * 180 / Math.PI;
14984
+ const sigMp = 0.13 + 0.53 * Math.exp(-elDeg / 10);
14985
+ const sigNoise = 0.15 + 0.43 * Math.exp(-elDeg / 6.9);
14986
+ const m = 1.001 / Math.sqrt(2001e-6 + Math.sin(elRad) * Math.sin(elRad));
14987
+ const sigTropo = 0.12 * m;
14988
+ return sigMp * sigMp + sigNoise * sigNoise + sigTropo * sigTropo;
14989
+ }
14990
+ function invertMatrix(A) {
14991
+ const n = A.length;
14992
+ const M = A.map((row, i) => [
14993
+ ...row,
14994
+ ...Array.from({ length: n }, (_, j) => i === j ? 1 : 0)
14995
+ ]);
14996
+ for (let col = 0; col < n; col++) {
14997
+ let piv = col;
14998
+ for (let r = col + 1; r < n; r++)
14999
+ if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
15000
+ if (Math.abs(M[piv][col]) < 1e-12) return null;
15001
+ [M[col], M[piv]] = [M[piv], M[col]];
15002
+ const d = M[col][col];
15003
+ for (let j = 0; j < 2 * n; j++) M[col][j] /= d;
15004
+ for (let r = 0; r < n; r++) {
15005
+ if (r === col) continue;
15006
+ const f = M[r][col];
15007
+ for (let j = 0; j < 2 * n; j++) M[r][j] -= f * M[col][j];
15008
+ }
15009
+ }
15010
+ return M.map((row) => row.slice(n));
15011
+ }
15012
+ function sbasProtectionLevels(sats, opts = {}) {
15013
+ const kH = opts.kH ?? 6;
15014
+ const kV = opts.kV ?? 5.33;
15015
+ if (sats.length < 4) return null;
15016
+ const N = [0, 1, 2, 3].map(() => [0, 0, 0, 0]);
15017
+ for (const s of sats) {
15018
+ if (!(s.variance > 0)) continue;
15019
+ const ce = Math.cos(s.elRad);
15020
+ const g = [
15021
+ ce * Math.sin(s.azRad),
15022
+ ce * Math.cos(s.azRad),
15023
+ Math.sin(s.elRad),
15024
+ 1
15025
+ ];
15026
+ const w = 1 / s.variance;
15027
+ for (let i = 0; i < 4; i++)
15028
+ for (let j = 0; j < 4; j++) N[i][j] += w * g[i] * g[j];
15029
+ }
15030
+ const cov = invertMatrix(N);
15031
+ if (!cov) return null;
15032
+ const dE2 = cov[0][0];
15033
+ const dN2 = cov[1][1];
15034
+ const dEN = cov[0][1];
15035
+ const dU = Math.sqrt(Math.max(cov[2][2], 0));
15036
+ const dMajor = Math.sqrt(
15037
+ (dE2 + dN2) / 2 + Math.sqrt(((dE2 - dN2) / 2) ** 2 + dEN * dEN)
15038
+ );
15039
+ return { hpl: kH * dMajor, vpl: kV * dU, dMajor, dU };
15040
+ }
14979
15041
  function solveLinear(A, b) {
14980
15042
  const n = b.length;
14981
15043
  const M = A.map((row, i) => [...row, b[i]]);
@@ -15002,9 +15064,11 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15002
15064
  convergenceM = 1e-4,
15003
15065
  tgd = true,
15004
15066
  iono,
15005
- gim
15067
+ gim,
15068
+ sbas
15006
15069
  } = opts;
15007
15070
  const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
15071
+ const gpsWeek = Math.floor((timeMs - GPS_EPOCH_MS_SPP) / 1e3 / 604800);
15008
15072
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
15009
15073
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
15010
15074
  if (all.length < minSats(all)) return null;
@@ -15030,7 +15094,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15030
15094
  const sysRows = new Map(systems.map((s) => [s, 0]));
15031
15095
  for (const k of Object.keys(residuals2)) delete residuals2[k];
15032
15096
  const positionSaneIter = x9 * x9 + y2 * y2 + z2 * z2 > 1e12;
15033
- const [rxLat, rxLon, rxHgt] = positionSaneIter ? ecefToGeodetic(x9, y2, z2) : [0, 0, 0];
15097
+ const [rxLat2, rxLon2, rxHgt2] = positionSaneIter ? ecefToGeodetic(x9, y2, z2) : [0, 0, 0];
15034
15098
  for (const prn of prns) {
15035
15099
  const psr = pseudoranges.get(prn);
15036
15100
  const eph = ephemerides.get(prn);
@@ -15041,7 +15105,16 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15041
15105
  const sat = computeSatPosition(eph, tTx - dtsClock * 1e3);
15042
15106
  if (!Number.isFinite(sat.x)) continue;
15043
15107
  const isKepler3 = eph.system !== "R" && eph.system !== "S";
15044
- const dts = dtsClock - (tgd && isKepler3 ? eph.tgd : 0);
15108
+ let dts = dtsClock - (tgd && isKepler3 ? eph.tgd : 0);
15109
+ if (sbas) {
15110
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
15111
+ if (sc) {
15112
+ sat.x += sc.dPos[0];
15113
+ sat.y += sc.dPos[1];
15114
+ sat.z += sc.dPos[2];
15115
+ dts += sc.dClkS;
15116
+ }
15117
+ }
15045
15118
  const travel = Math.hypot(sat.x - x9, sat.y - y2, sat.z - z2) / C_LIGHT;
15046
15119
  const [sx, sy, sz] = sagnac2(sat, travel);
15047
15120
  const rho = Math.hypot(sx - x9, sy - y2, sz - z2);
@@ -15060,14 +15133,26 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15060
15133
  const sinEl = Math.max(Math.sin(elev), 0.1);
15061
15134
  weight = sinEl * sinEl;
15062
15135
  }
15063
- const tropo = troposphere && positionSane ? tropoDelay2(elev, rxLat, rxHgt) : 0;
15136
+ const tropo = troposphere && positionSane ? tropoDelay2(elev, rxLat2, rxHgt2) : 0;
15064
15137
  let ionoM = 0;
15065
- if ((gim || iono) && positionSane) {
15138
+ if ((sbas || gim || iono) && positionSane) {
15066
15139
  let l1M = null;
15067
- if (gim)
15068
- l1M = gimSlantIonoDelayL1(gim, rxLat, rxLon, azim, elev, timeMs);
15140
+ if (sbas) {
15141
+ const si = sbas.ionoDelay(
15142
+ gpsWeek,
15143
+ gpsTow,
15144
+ rxLat2,
15145
+ rxLon2,
15146
+ rxHgt2,
15147
+ azim,
15148
+ elev
15149
+ );
15150
+ if (si) l1M = si.delayM;
15151
+ }
15152
+ if (l1M === null && gim)
15153
+ l1M = gimSlantIonoDelayL1(gim, rxLat2, rxLon2, azim, elev, timeMs);
15069
15154
  if (l1M === null && iono) {
15070
- l1M = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow);
15155
+ l1M = C_LIGHT * klobucharDelay(iono, rxLat2, rxLon2, azim, elev, gpsTow);
15071
15156
  }
15072
15157
  if (l1M !== null) {
15073
15158
  const f = PRIMARY_FREQ_HZ[sys] ?? F_L12;
@@ -15140,6 +15225,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15140
15225
  const { x, y, z, clock, residuals, iterations, converged } = inner;
15141
15226
  const used = [];
15142
15227
  const azels = [];
15228
+ const [rxLat, rxLon, rxHgt] = ecefToGeodetic(x, y, z);
15229
+ const sbasRows = [];
15143
15230
  for (const prn of candidates) {
15144
15231
  if (!(prn in residuals)) continue;
15145
15232
  const eph = ephemerides.get(prn);
@@ -15149,8 +15236,27 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15149
15236
  if (azel.el >= elevationMaskDeg * Math.PI / 180) {
15150
15237
  used.push(prn);
15151
15238
  azels.push(azel);
15239
+ if (sbas) {
15240
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
15241
+ const si = sbas.ionoDelay(
15242
+ gpsWeek,
15243
+ gpsTow,
15244
+ rxLat,
15245
+ rxLon,
15246
+ rxHgt,
15247
+ azel.az,
15248
+ azel.el
15249
+ );
15250
+ if (sc && si)
15251
+ sbasRows.push({
15252
+ azRad: azel.az,
15253
+ elRad: azel.el,
15254
+ variance: sc.varM2 + si.varM2 + sbasAirTropoVar(azel.el)
15255
+ });
15256
+ }
15152
15257
  }
15153
15258
  }
15259
+ const pl = sbas ? sbasProtectionLevels(sbasRows) : null;
15154
15260
  return {
15155
15261
  position: [x, y, z],
15156
15262
  clockBias: Object.fromEntries(clock),
@@ -15159,7 +15265,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15159
15265
  residuals,
15160
15266
  dop: computeDop(azels),
15161
15267
  iterations,
15162
- converged
15268
+ converged,
15269
+ ...pl ? { hpl: pl.hpl, vpl: pl.vpl, sbasSats: sbasRows.length } : sbas ? { sbasSats: sbasRows.length } : {}
15163
15270
  };
15164
15271
  }
15165
15272
 
@@ -17697,6 +17804,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
17697
17804
  rhumbLine,
17698
17805
  rtcm3Constellation,
17699
17806
  satClockCorrection,
17807
+ sbasProtectionLevels,
17700
17808
  sbfCrc16,
17701
17809
  scanSbfFrames,
17702
17810
  selectEphemeris,
package/dist/index.d.cts CHANGED
@@ -17,7 +17,7 @@ export { TrimbleEpoch, TrimbleFrame, TrimbleMeasurement, TrimbleNavResult, Trimb
17
17
  export { BinexEpoch, BinexMeasurement, BinexParseResult, BinexRecord, binexCrc16, binexCrc32, binexCsum8, binexRecords, decodeBinexEph, getBnxi, parseBinex } from './binex.cjs';
18
18
  export { AllPositionsData, AlmanacPosition, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPosition, VisibilityPass, VisibilityResult, almanacEpochMs, almanacSatPosition, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, computeVisibilityFromPositions, dopplerHz, ecefToAzEl, ephInfoToEphemeris, glonassAlmanacEpochMs, glonassAlmanacPosition, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, rangeRate, selectEphemeris } from './orbit.cjs';
19
19
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.cjs';
20
- export { AntennaOffset, ArFixedSat, ArOptions, ArResult, ArSat, CorrectionResult, DgnssOptions, DgnssSolution, EphemerisSource, FixedPositionResult, IONO_L1_M_PER_TECU, KlobucharCoeffs, LambdaResult, NetworkFcbResult, NetworkFcbSummary, NlArc, NlFcbResult, PppAntennaModel, PppArc, PppCorrectionConfig, PppCorrections, PppEpoch, PppEpochContext, PppEpochResult, PppFixState, PppOptions, PppSatObs, PppSolution, RawObservation, RtkEpochMeasurements, RtkFloatEngine, RtkFloatOptions, RtkFloatSolution, RtkMeasurement, RtkObsEpoch, RtkObsInput, RtkPostProcessOptions, RtkPostProcessResult, RtkTrackPoint, SatGeom, SbasIonoDelay, SbasProcessor, SbasSatCorrection, SppOptions, SppSolution, TropoMapping, WlArc, WlFcbOptions, WlFcbResult, WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength } from './positioning.cjs';
20
+ export { AntennaOffset, ArFixedSat, ArOptions, ArResult, ArSat, CorrectionResult, DgnssOptions, DgnssSolution, EphemerisSource, FixedPositionResult, IONO_L1_M_PER_TECU, KlobucharCoeffs, LambdaResult, NetworkFcbResult, NetworkFcbSummary, NlArc, NlFcbResult, PppAntennaModel, PppArc, PppCorrectionConfig, PppCorrections, PppEpoch, PppEpochContext, PppEpochResult, PppFixState, PppOptions, PppSatObs, PppSolution, RawObservation, RtkEpochMeasurements, RtkFloatEngine, RtkFloatOptions, RtkFloatSolution, RtkMeasurement, RtkObsEpoch, RtkObsInput, RtkPostProcessOptions, RtkPostProcessResult, RtkTrackPoint, SatGeom, SbasCorrectionSource, SbasIonoDelay, SbasProcessor, SbasSatCorrection, SppOptions, SppSolution, TropoMapping, WlArc, WlFcbOptions, WlFcbResult, WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, sbasProtectionLevels, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength } from './positioning.cjs';
21
21
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.cjs';
22
22
  export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoRatePoint, IonoRateSeries, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb } from './analysis.cjs';
23
23
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.cjs';
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { TrimbleEpoch, TrimbleFrame, TrimbleMeasurement, TrimbleNavResult, Trimb
17
17
  export { BinexEpoch, BinexMeasurement, BinexParseResult, BinexRecord, binexCrc16, binexCrc32, binexCsum8, binexRecords, decodeBinexEph, getBnxi, parseBinex } from './binex.js';
18
18
  export { AllPositionsData, AlmanacPosition, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPosition, VisibilityPass, VisibilityResult, almanacEpochMs, almanacSatPosition, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, computeVisibilityFromPositions, dopplerHz, ecefToAzEl, ephInfoToEphemeris, glonassAlmanacEpochMs, glonassAlmanacPosition, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, rangeRate, selectEphemeris } from './orbit.js';
19
19
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.js';
20
- export { AntennaOffset, ArFixedSat, ArOptions, ArResult, ArSat, CorrectionResult, DgnssOptions, DgnssSolution, EphemerisSource, FixedPositionResult, IONO_L1_M_PER_TECU, KlobucharCoeffs, LambdaResult, NetworkFcbResult, NetworkFcbSummary, NlArc, NlFcbResult, PppAntennaModel, PppArc, PppCorrectionConfig, PppCorrections, PppEpoch, PppEpochContext, PppEpochResult, PppFixState, PppOptions, PppSatObs, PppSolution, RawObservation, RtkEpochMeasurements, RtkFloatEngine, RtkFloatOptions, RtkFloatSolution, RtkMeasurement, RtkObsEpoch, RtkObsInput, RtkPostProcessOptions, RtkPostProcessResult, RtkTrackPoint, SatGeom, SbasIonoDelay, SbasProcessor, SbasSatCorrection, SppOptions, SppSolution, TropoMapping, WlArc, WlFcbOptions, WlFcbResult, WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength } from './positioning.js';
20
+ export { AntennaOffset, ArFixedSat, ArOptions, ArResult, ArSat, CorrectionResult, DgnssOptions, DgnssSolution, EphemerisSource, FixedPositionResult, IONO_L1_M_PER_TECU, KlobucharCoeffs, LambdaResult, NetworkFcbResult, NetworkFcbSummary, NlArc, NlFcbResult, PppAntennaModel, PppArc, PppCorrectionConfig, PppCorrections, PppEpoch, PppEpochContext, PppEpochResult, PppFixState, PppOptions, PppSatObs, PppSolution, RawObservation, RtkEpochMeasurements, RtkFloatEngine, RtkFloatOptions, RtkFloatSolution, RtkMeasurement, RtkObsEpoch, RtkObsInput, RtkPostProcessOptions, RtkPostProcessResult, RtkTrackPoint, SatGeom, SbasCorrectionSource, SbasIonoDelay, SbasProcessor, SbasSatCorrection, SppOptions, SppSolution, TropoMapping, WlArc, WlFcbOptions, WlFcbResult, WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, sbasProtectionLevels, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength } from './positioning.js';
21
21
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.js';
22
22
  export { CompleteSatSignal, CompleteSignalStats, CompletenessAccumulator, CompletenessResult, CycleSlipAccumulator, CycleSlipEvent, CycleSlipResult, CycleSlipSignalStats, IonoAccumulator, IonoDcbResult, IonoPoint, IonoRatePoint, IonoRateSeries, IonoResult, IonoSeries, MultipathAccumulator, MultipathPoint, MultipathResult, MultipathSeries, MultipathSignalStat, QualityResult, SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb } from './analysis.js';
23
23
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.js';
package/dist/index.js CHANGED
@@ -68,7 +68,7 @@ import {
68
68
  parseSbfNav,
69
69
  parseSbfReceiverSetup,
70
70
  scanSbfFrames
71
- } from "./chunk-XFPDTGSV.js";
71
+ } from "./chunk-DE5IUULR.js";
72
72
  import "./chunk-FA2UTLEQ.js";
73
73
  import {
74
74
  OEM4_HLEN,
@@ -76,7 +76,7 @@ import {
76
76
  oem4Frames,
77
77
  parseNovatelNav,
78
78
  parseNovatelRange
79
- } from "./chunk-2IM5JYKP.js";
79
+ } from "./chunk-ZS5RLFWI.js";
80
80
  import {
81
81
  parseTrimble,
82
82
  parseTrimbleNav,
@@ -157,6 +157,7 @@ import {
157
157
  postProcessRtk,
158
158
  resolvePppAmbiguities,
159
159
  satClockCorrection,
160
+ sbasProtectionLevels,
160
161
  solidEarthTide,
161
162
  solveDgnss,
162
163
  solvePpp,
@@ -164,7 +165,7 @@ import {
164
165
  sunEcef,
165
166
  toRtkEpoch,
166
167
  wlWavelength
167
- } from "./chunk-VYC2FRDV.js";
168
+ } from "./chunk-U32IU7RO.js";
168
169
  import "./chunk-WQX354KD.js";
169
170
  import {
170
171
  almanacEpochMs,
@@ -630,6 +631,7 @@ export {
630
631
  rhumbLine,
631
632
  rtcm3Constellation,
632
633
  satClockCorrection,
634
+ sbasProtectionLevels,
633
635
  crc16 as sbfCrc16,
634
636
  scanSbfFrames,
635
637
  selectEphemeris,
package/dist/novatel.cjs CHANGED
@@ -729,7 +729,7 @@ function decodeGpsQzssEphemeris(view, p, sys) {
729
729
  tgd: view.getFloat64(p + 172, true)
730
730
  };
731
731
  }
732
- function parseNovatelNav(data) {
732
+ function parseNovatelNav(data, opts = {}) {
733
733
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
734
734
  const stats = { badCrc: 0 };
735
735
  const ephemerides = [];
@@ -802,6 +802,7 @@ function parseNovatelNav(data) {
802
802
  const prn = view.getUint32(p + 4, true);
803
803
  const msg = new Uint8Array(32);
804
804
  msg.set(data.subarray(p + 12, p + 41));
805
+ opts.onSbasMessage?.(msg, prn, frame.week, frame.towMs / 1e3);
805
806
  const eph = decodeSbasGeoNav(msg, prn, frame.week, frame.towMs / 1e3);
806
807
  if (!eph) continue;
807
808
  const prev = lastGlo.get(eph.prn);
@@ -105,7 +105,13 @@ interface NovatelNavResult {
105
105
  * Galileo by IODNav + toe + toc per data source, BeiDou by toe + toc,
106
106
  * GLONASS by reference time and health.
107
107
  */
108
- declare function parseNovatelNav(data: Uint8Array): NovatelNavResult;
108
+ declare function parseNovatelNav(data: Uint8Array, opts?: {
109
+ /** Called for each SBAS L1 message (all types) from RAWSBASFRAME. The
110
+ * frame carries 232 bits — the correction fields fit, but the inner SBAS
111
+ * CRC does not; the outer OEM4 CRC-32 guarantees the bytes. Feed an
112
+ * {@link ../positioning/sbas.SbasProcessor}. */
113
+ onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void;
114
+ }): NovatelNavResult;
109
115
 
110
116
  /**
111
117
  * NovAtel OEM4/6/7 raw-measurement decoding (RANGE log, message 43).
package/dist/novatel.d.ts CHANGED
@@ -105,7 +105,13 @@ interface NovatelNavResult {
105
105
  * Galileo by IODNav + toe + toc per data source, BeiDou by toe + toc,
106
106
  * GLONASS by reference time and health.
107
107
  */
108
- declare function parseNovatelNav(data: Uint8Array): NovatelNavResult;
108
+ declare function parseNovatelNav(data: Uint8Array, opts?: {
109
+ /** Called for each SBAS L1 message (all types) from RAWSBASFRAME. The
110
+ * frame carries 232 bits — the correction fields fit, but the inner SBAS
111
+ * CRC does not; the outer OEM4 CRC-32 guarantees the bytes. Feed an
112
+ * {@link ../positioning/sbas.SbasProcessor}. */
113
+ onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void;
114
+ }): NovatelNavResult;
109
115
 
110
116
  /**
111
117
  * NovAtel OEM4/6/7 raw-measurement decoding (RANGE log, message 43).
package/dist/novatel.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  oem4Frames,
5
5
  parseNovatelNav,
6
6
  parseNovatelRange
7
- } from "./chunk-2IM5JYKP.js";
7
+ } from "./chunk-ZS5RLFWI.js";
8
8
  import "./chunk-WQX354KD.js";
9
9
  import "./chunk-YUW33VXK.js";
10
10
  import "./chunk-V2D5KUKH.js";
@@ -43,6 +43,7 @@ __export(positioning_exports, {
43
43
  postProcessRtk: () => postProcessRtk,
44
44
  resolvePppAmbiguities: () => resolvePppAmbiguities,
45
45
  satClockCorrection: () => satClockCorrection,
46
+ sbasProtectionLevels: () => sbasProtectionLevels,
46
47
  solidEarthTide: () => solidEarthTide,
47
48
  solveDgnss: () => solveDgnss,
48
49
  solvePpp: () => solvePpp,
@@ -3593,6 +3594,65 @@ function sagnac2(pos, travelTimeS) {
3593
3594
  const s = Math.sin(theta);
3594
3595
  return [pos.x * c + pos.y * s, -pos.x * s + pos.y * c, pos.z];
3595
3596
  }
3597
+ function sbasAirTropoVar(elRad) {
3598
+ const elDeg = elRad * 180 / Math.PI;
3599
+ const sigMp = 0.13 + 0.53 * Math.exp(-elDeg / 10);
3600
+ const sigNoise = 0.15 + 0.43 * Math.exp(-elDeg / 6.9);
3601
+ const m = 1.001 / Math.sqrt(2001e-6 + Math.sin(elRad) * Math.sin(elRad));
3602
+ const sigTropo = 0.12 * m;
3603
+ return sigMp * sigMp + sigNoise * sigNoise + sigTropo * sigTropo;
3604
+ }
3605
+ function invertMatrix(A) {
3606
+ const n = A.length;
3607
+ const M = A.map((row, i) => [
3608
+ ...row,
3609
+ ...Array.from({ length: n }, (_, j) => i === j ? 1 : 0)
3610
+ ]);
3611
+ for (let col = 0; col < n; col++) {
3612
+ let piv = col;
3613
+ for (let r = col + 1; r < n; r++)
3614
+ if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
3615
+ if (Math.abs(M[piv][col]) < 1e-12) return null;
3616
+ [M[col], M[piv]] = [M[piv], M[col]];
3617
+ const d = M[col][col];
3618
+ for (let j = 0; j < 2 * n; j++) M[col][j] /= d;
3619
+ for (let r = 0; r < n; r++) {
3620
+ if (r === col) continue;
3621
+ const f = M[r][col];
3622
+ for (let j = 0; j < 2 * n; j++) M[r][j] -= f * M[col][j];
3623
+ }
3624
+ }
3625
+ return M.map((row) => row.slice(n));
3626
+ }
3627
+ function sbasProtectionLevels(sats, opts = {}) {
3628
+ const kH = opts.kH ?? 6;
3629
+ const kV = opts.kV ?? 5.33;
3630
+ if (sats.length < 4) return null;
3631
+ const N = [0, 1, 2, 3].map(() => [0, 0, 0, 0]);
3632
+ for (const s of sats) {
3633
+ if (!(s.variance > 0)) continue;
3634
+ const ce = Math.cos(s.elRad);
3635
+ const g = [
3636
+ ce * Math.sin(s.azRad),
3637
+ ce * Math.cos(s.azRad),
3638
+ Math.sin(s.elRad),
3639
+ 1
3640
+ ];
3641
+ const w = 1 / s.variance;
3642
+ for (let i = 0; i < 4; i++)
3643
+ for (let j = 0; j < 4; j++) N[i][j] += w * g[i] * g[j];
3644
+ }
3645
+ const cov = invertMatrix(N);
3646
+ if (!cov) return null;
3647
+ const dE2 = cov[0][0];
3648
+ const dN2 = cov[1][1];
3649
+ const dEN = cov[0][1];
3650
+ const dU = Math.sqrt(Math.max(cov[2][2], 0));
3651
+ const dMajor = Math.sqrt(
3652
+ (dE2 + dN2) / 2 + Math.sqrt(((dE2 - dN2) / 2) ** 2 + dEN * dEN)
3653
+ );
3654
+ return { hpl: kH * dMajor, vpl: kV * dU, dMajor, dU };
3655
+ }
3596
3656
  function solveLinear(A, b) {
3597
3657
  const n = b.length;
3598
3658
  const M = A.map((row, i) => [...row, b[i]]);
@@ -3619,9 +3679,11 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3619
3679
  convergenceM = 1e-4,
3620
3680
  tgd = true,
3621
3681
  iono,
3622
- gim
3682
+ gim,
3683
+ sbas
3623
3684
  } = opts;
3624
3685
  const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
3686
+ const gpsWeek = Math.floor((timeMs - GPS_EPOCH_MS_SPP) / 1e3 / 604800);
3625
3687
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
3626
3688
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
3627
3689
  if (all.length < minSats(all)) return null;
@@ -3647,7 +3709,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3647
3709
  const sysRows = new Map(systems.map((s) => [s, 0]));
3648
3710
  for (const k of Object.keys(residuals2)) delete residuals2[k];
3649
3711
  const positionSaneIter = x9 * x9 + y2 * y2 + z2 * z2 > 1e12;
3650
- const [rxLat, rxLon, rxHgt] = positionSaneIter ? ecefToGeodetic(x9, y2, z2) : [0, 0, 0];
3712
+ const [rxLat2, rxLon2, rxHgt2] = positionSaneIter ? ecefToGeodetic(x9, y2, z2) : [0, 0, 0];
3651
3713
  for (const prn of prns) {
3652
3714
  const psr = pseudoranges.get(prn);
3653
3715
  const eph = ephemerides.get(prn);
@@ -3658,7 +3720,16 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3658
3720
  const sat = computeSatPosition(eph, tTx - dtsClock * 1e3);
3659
3721
  if (!Number.isFinite(sat.x)) continue;
3660
3722
  const isKepler = eph.system !== "R" && eph.system !== "S";
3661
- const dts = dtsClock - (tgd && isKepler ? eph.tgd : 0);
3723
+ let dts = dtsClock - (tgd && isKepler ? eph.tgd : 0);
3724
+ if (sbas) {
3725
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
3726
+ if (sc) {
3727
+ sat.x += sc.dPos[0];
3728
+ sat.y += sc.dPos[1];
3729
+ sat.z += sc.dPos[2];
3730
+ dts += sc.dClkS;
3731
+ }
3732
+ }
3662
3733
  const travel = Math.hypot(sat.x - x9, sat.y - y2, sat.z - z2) / C_LIGHT;
3663
3734
  const [sx, sy, sz] = sagnac2(sat, travel);
3664
3735
  const rho = Math.hypot(sx - x9, sy - y2, sz - z2);
@@ -3677,14 +3748,26 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3677
3748
  const sinEl = Math.max(Math.sin(elev), 0.1);
3678
3749
  weight = sinEl * sinEl;
3679
3750
  }
3680
- const tropo = troposphere && positionSane ? tropoDelay2(elev, rxLat, rxHgt) : 0;
3751
+ const tropo = troposphere && positionSane ? tropoDelay2(elev, rxLat2, rxHgt2) : 0;
3681
3752
  let ionoM = 0;
3682
- if ((gim || iono) && positionSane) {
3753
+ if ((sbas || gim || iono) && positionSane) {
3683
3754
  let l1M = null;
3684
- if (gim)
3685
- l1M = gimSlantIonoDelayL1(gim, rxLat, rxLon, azim, elev, timeMs);
3755
+ if (sbas) {
3756
+ const si = sbas.ionoDelay(
3757
+ gpsWeek,
3758
+ gpsTow,
3759
+ rxLat2,
3760
+ rxLon2,
3761
+ rxHgt2,
3762
+ azim,
3763
+ elev
3764
+ );
3765
+ if (si) l1M = si.delayM;
3766
+ }
3767
+ if (l1M === null && gim)
3768
+ l1M = gimSlantIonoDelayL1(gim, rxLat2, rxLon2, azim, elev, timeMs);
3686
3769
  if (l1M === null && iono) {
3687
- l1M = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow);
3770
+ l1M = C_LIGHT * klobucharDelay(iono, rxLat2, rxLon2, azim, elev, gpsTow);
3688
3771
  }
3689
3772
  if (l1M !== null) {
3690
3773
  const f = PRIMARY_FREQ_HZ[sys] ?? F_L12;
@@ -3757,6 +3840,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3757
3840
  const { x, y, z, clock, residuals, iterations, converged } = inner;
3758
3841
  const used = [];
3759
3842
  const azels = [];
3843
+ const [rxLat, rxLon, rxHgt] = ecefToGeodetic(x, y, z);
3844
+ const sbasRows = [];
3760
3845
  for (const prn of candidates) {
3761
3846
  if (!(prn in residuals)) continue;
3762
3847
  const eph = ephemerides.get(prn);
@@ -3766,8 +3851,27 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3766
3851
  if (azel.el >= elevationMaskDeg * Math.PI / 180) {
3767
3852
  used.push(prn);
3768
3853
  azels.push(azel);
3854
+ if (sbas) {
3855
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
3856
+ const si = sbas.ionoDelay(
3857
+ gpsWeek,
3858
+ gpsTow,
3859
+ rxLat,
3860
+ rxLon,
3861
+ rxHgt,
3862
+ azel.az,
3863
+ azel.el
3864
+ );
3865
+ if (sc && si)
3866
+ sbasRows.push({
3867
+ azRad: azel.az,
3868
+ elRad: azel.el,
3869
+ variance: sc.varM2 + si.varM2 + sbasAirTropoVar(azel.el)
3870
+ });
3871
+ }
3769
3872
  }
3770
3873
  }
3874
+ const pl = sbas ? sbasProtectionLevels(sbasRows) : null;
3771
3875
  return {
3772
3876
  position: [x, y, z],
3773
3877
  clockBias: Object.fromEntries(clock),
@@ -3776,7 +3880,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3776
3880
  residuals,
3777
3881
  dop: computeDop(azels),
3778
3882
  iterations,
3779
- converged
3883
+ converged,
3884
+ ...pl ? { hpl: pl.hpl, vpl: pl.vpl, sbasSats: sbasRows.length } : sbas ? { sbasSats: sbasRows.length } : {}
3780
3885
  };
3781
3886
  }
3782
3887
  // Annotate the CommonJS export names for ESM import in node:
@@ -3804,6 +3909,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3804
3909
  postProcessRtk,
3805
3910
  resolvePppAmbiguities,
3806
3911
  satClockCorrection,
3912
+ sbasProtectionLevels,
3807
3913
  solidEarthTide,
3808
3914
  solveDgnss,
3809
3915
  solvePpp,
@@ -1223,6 +1223,28 @@ interface SppOptions {
1223
1223
  * reference the broadcast clock already matches. Default true.
1224
1224
  */
1225
1225
  tgd?: boolean;
1226
+ /**
1227
+ * SBAS wide-area corrections (WAAS/EGNOS/…). When given, each satellite in
1228
+ * the SBAS PRN mask has its position + clock adjusted by the fast/long-term
1229
+ * corrections, and — where the ionospheric pierce point is covered by the
1230
+ * SBAS grid — the SBAS slant ionosphere replaces the Klobuchar/GIM model.
1231
+ * Pass an {@link ./sbas.SbasProcessor} (or anything with these two methods).
1232
+ * Combine with single-frequency pseudoranges, not iono-free input.
1233
+ */
1234
+ sbas?: SbasCorrectionSource;
1235
+ }
1236
+ /** The two SBAS corrections `solveSpp` consumes — {@link ./sbas.SbasProcessor}
1237
+ * satisfies this structurally. */
1238
+ interface SbasCorrectionSource {
1239
+ satCorrection(prn: string, week: number, tow: number): {
1240
+ dPos: [number, number, number];
1241
+ dClkS: number;
1242
+ varM2: number;
1243
+ } | null;
1244
+ ionoDelay(week: number, tow: number, latRad: number, lonRad: number, heightM: number, azRad: number, elRad: number): {
1245
+ delayM: number;
1246
+ varM2: number;
1247
+ } | null;
1226
1248
  }
1227
1249
  interface SppSolution {
1228
1250
  /** Receiver position, ECEF meters. */
@@ -1239,6 +1261,15 @@ interface SppSolution {
1239
1261
  dop: DopValues | null;
1240
1262
  iterations: number;
1241
1263
  converged: boolean;
1264
+ /**
1265
+ * SBAS integrity, present only when the `sbas` option was given and enough
1266
+ * corrected satellites were available. Horizontal/vertical protection levels
1267
+ * (m) — the DO-229 statistical error bounds — and the count of SBAS-corrected
1268
+ * satellites they were computed from.
1269
+ */
1270
+ hpl?: number;
1271
+ vpl?: number;
1272
+ sbasSats?: number;
1242
1273
  }
1243
1274
  /**
1244
1275
  * Broadcast satellite clock correction in seconds at time `tMs`
@@ -1251,6 +1282,26 @@ declare function satClockCorrection(eph: Ephemeris, tMs: number): number;
1251
1282
  * Removes the first-order ionospheric delay.
1252
1283
  */
1253
1284
  declare function ionoFree(p1: number, p2: number, f1: number, f2: number): number;
1285
+ /**
1286
+ * SBAS horizontal/vertical protection levels (m) from the per-satellite ENU
1287
+ * geometry and total error variances. HPL = K_H·d_major, VPL = K_V·d_U, where
1288
+ * d_major is the semi-major axis of the horizontal position-error ellipse and
1289
+ * d_U the vertical std, both from the variance-weighted least-squares
1290
+ * covariance (DO-229 §2.1.4.10). Needs ≥ 4 satellites.
1291
+ */
1292
+ declare function sbasProtectionLevels(sats: readonly {
1293
+ azRad: number;
1294
+ elRad: number;
1295
+ variance: number;
1296
+ }[], opts?: {
1297
+ kH?: number;
1298
+ kV?: number;
1299
+ }): {
1300
+ hpl: number;
1301
+ vpl: number;
1302
+ dMajor: number;
1303
+ dU: number;
1304
+ } | null;
1254
1305
  /**
1255
1306
  * Solve receiver position from one epoch of pseudoranges.
1256
1307
  *
@@ -1263,4 +1314,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
1263
1314
  */
1264
1315
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
1265
1316
 
1266
- export { type AntennaOffset, type ArFixedSat, type ArOptions, type ArResult, type ArSat, type CorrectionResult, type DgnssOptions, type DgnssSolution, type EphemerisSource, type FixedPositionResult, IONO_L1_M_PER_TECU, type KlobucharCoeffs, type LambdaResult, type NetworkFcbResult, type NetworkFcbSummary, type NlArc, type NlFcbResult, type PppAntennaModel, type PppArc, type PppCorrectionConfig, type PppCorrections, type PppEpoch, type PppEpochContext, type PppEpochResult, type PppFixState, type PppOptions, type PppSatObs, type PppSolution, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type RtkObsEpoch, type RtkObsInput, type RtkPostProcessOptions, type RtkPostProcessResult, type RtkTrackPoint, type SatGeom, type SbasIonoDelay, SbasProcessor, type SbasSatCorrection, type SppOptions, type SppSolution, type TropoMapping, type WlArc, type WlFcbOptions, type WlFcbResult, type WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength };
1317
+ export { type AntennaOffset, type ArFixedSat, type ArOptions, type ArResult, type ArSat, type CorrectionResult, type DgnssOptions, type DgnssSolution, type EphemerisSource, type FixedPositionResult, IONO_L1_M_PER_TECU, type KlobucharCoeffs, type LambdaResult, type NetworkFcbResult, type NetworkFcbSummary, type NlArc, type NlFcbResult, type PppAntennaModel, type PppArc, type PppCorrectionConfig, type PppCorrections, type PppEpoch, type PppEpochContext, type PppEpochResult, type PppFixState, type PppOptions, type PppSatObs, type PppSolution, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type RtkObsEpoch, type RtkObsInput, type RtkPostProcessOptions, type RtkPostProcessResult, type RtkTrackPoint, type SatGeom, type SbasCorrectionSource, type SbasIonoDelay, SbasProcessor, type SbasSatCorrection, type SppOptions, type SppSolution, type TropoMapping, type WlArc, type WlFcbOptions, type WlFcbResult, type WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, sbasProtectionLevels, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength };
@@ -1223,6 +1223,28 @@ interface SppOptions {
1223
1223
  * reference the broadcast clock already matches. Default true.
1224
1224
  */
1225
1225
  tgd?: boolean;
1226
+ /**
1227
+ * SBAS wide-area corrections (WAAS/EGNOS/…). When given, each satellite in
1228
+ * the SBAS PRN mask has its position + clock adjusted by the fast/long-term
1229
+ * corrections, and — where the ionospheric pierce point is covered by the
1230
+ * SBAS grid — the SBAS slant ionosphere replaces the Klobuchar/GIM model.
1231
+ * Pass an {@link ./sbas.SbasProcessor} (or anything with these two methods).
1232
+ * Combine with single-frequency pseudoranges, not iono-free input.
1233
+ */
1234
+ sbas?: SbasCorrectionSource;
1235
+ }
1236
+ /** The two SBAS corrections `solveSpp` consumes — {@link ./sbas.SbasProcessor}
1237
+ * satisfies this structurally. */
1238
+ interface SbasCorrectionSource {
1239
+ satCorrection(prn: string, week: number, tow: number): {
1240
+ dPos: [number, number, number];
1241
+ dClkS: number;
1242
+ varM2: number;
1243
+ } | null;
1244
+ ionoDelay(week: number, tow: number, latRad: number, lonRad: number, heightM: number, azRad: number, elRad: number): {
1245
+ delayM: number;
1246
+ varM2: number;
1247
+ } | null;
1226
1248
  }
1227
1249
  interface SppSolution {
1228
1250
  /** Receiver position, ECEF meters. */
@@ -1239,6 +1261,15 @@ interface SppSolution {
1239
1261
  dop: DopValues | null;
1240
1262
  iterations: number;
1241
1263
  converged: boolean;
1264
+ /**
1265
+ * SBAS integrity, present only when the `sbas` option was given and enough
1266
+ * corrected satellites were available. Horizontal/vertical protection levels
1267
+ * (m) — the DO-229 statistical error bounds — and the count of SBAS-corrected
1268
+ * satellites they were computed from.
1269
+ */
1270
+ hpl?: number;
1271
+ vpl?: number;
1272
+ sbasSats?: number;
1242
1273
  }
1243
1274
  /**
1244
1275
  * Broadcast satellite clock correction in seconds at time `tMs`
@@ -1251,6 +1282,26 @@ declare function satClockCorrection(eph: Ephemeris, tMs: number): number;
1251
1282
  * Removes the first-order ionospheric delay.
1252
1283
  */
1253
1284
  declare function ionoFree(p1: number, p2: number, f1: number, f2: number): number;
1285
+ /**
1286
+ * SBAS horizontal/vertical protection levels (m) from the per-satellite ENU
1287
+ * geometry and total error variances. HPL = K_H·d_major, VPL = K_V·d_U, where
1288
+ * d_major is the semi-major axis of the horizontal position-error ellipse and
1289
+ * d_U the vertical std, both from the variance-weighted least-squares
1290
+ * covariance (DO-229 §2.1.4.10). Needs ≥ 4 satellites.
1291
+ */
1292
+ declare function sbasProtectionLevels(sats: readonly {
1293
+ azRad: number;
1294
+ elRad: number;
1295
+ variance: number;
1296
+ }[], opts?: {
1297
+ kH?: number;
1298
+ kV?: number;
1299
+ }): {
1300
+ hpl: number;
1301
+ vpl: number;
1302
+ dMajor: number;
1303
+ dU: number;
1304
+ } | null;
1254
1305
  /**
1255
1306
  * Solve receiver position from one epoch of pseudoranges.
1256
1307
  *
@@ -1263,4 +1314,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
1263
1314
  */
1264
1315
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
1265
1316
 
1266
- export { type AntennaOffset, type ArFixedSat, type ArOptions, type ArResult, type ArSat, type CorrectionResult, type DgnssOptions, type DgnssSolution, type EphemerisSource, type FixedPositionResult, IONO_L1_M_PER_TECU, type KlobucharCoeffs, type LambdaResult, type NetworkFcbResult, type NetworkFcbSummary, type NlArc, type NlFcbResult, type PppAntennaModel, type PppArc, type PppCorrectionConfig, type PppCorrections, type PppEpoch, type PppEpochContext, type PppEpochResult, type PppFixState, type PppOptions, type PppSatObs, type PppSolution, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type RtkObsEpoch, type RtkObsInput, type RtkPostProcessOptions, type RtkPostProcessResult, type RtkTrackPoint, type SatGeom, type SbasIonoDelay, SbasProcessor, type SbasSatCorrection, type SppOptions, type SppSolution, type TropoMapping, type WlArc, type WlFcbOptions, type WlFcbResult, type WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength };
1317
+ export { type AntennaOffset, type ArFixedSat, type ArOptions, type ArResult, type ArSat, type CorrectionResult, type DgnssOptions, type DgnssSolution, type EphemerisSource, type FixedPositionResult, IONO_L1_M_PER_TECU, type KlobucharCoeffs, type LambdaResult, type NetworkFcbResult, type NetworkFcbSummary, type NlArc, type NlFcbResult, type PppAntennaModel, type PppArc, type PppCorrectionConfig, type PppCorrections, type PppEpoch, type PppEpochContext, type PppEpochResult, type PppFixState, type PppOptions, type PppSatObs, type PppSolution, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type RtkObsEpoch, type RtkObsInput, type RtkPostProcessOptions, type RtkPostProcessResult, type RtkTrackPoint, type SatGeom, type SbasCorrectionSource, type SbasIonoDelay, SbasProcessor, type SbasSatCorrection, type SppOptions, type SppSolution, type TropoMapping, type WlArc, type WlFcbOptions, type WlFcbResult, type WlObs, buildPppAntenna, createPppCorrections, estimateNarrowlaneFcb, estimateNetworkFcbs, estimateWidelaneFcb, extractWidelaneArcs, fixPppPosition, gimSlantIonoDelayL1, gimVerticalTec, gmst, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, moonEcef, niellMapping, nlWavelength, postProcessRtk, resolvePppAmbiguities, satClockCorrection, sbasProtectionLevels, solidEarthTide, solveDgnss, solvePpp, solveSpp, sunEcef, toRtkEpoch, wlWavelength };
@@ -22,6 +22,7 @@ import {
22
22
  postProcessRtk,
23
23
  resolvePppAmbiguities,
24
24
  satClockCorrection,
25
+ sbasProtectionLevels,
25
26
  solidEarthTide,
26
27
  solveDgnss,
27
28
  solvePpp,
@@ -29,7 +30,7 @@ import {
29
30
  sunEcef,
30
31
  toRtkEpoch,
31
32
  wlWavelength
32
- } from "./chunk-VYC2FRDV.js";
33
+ } from "./chunk-U32IU7RO.js";
33
34
  import "./chunk-WQX354KD.js";
34
35
  import "./chunk-MHGNKSSH.js";
35
36
  import "./chunk-MIIM4LDY.js";
@@ -65,6 +66,7 @@ export {
65
66
  postProcessRtk,
66
67
  resolvePppAmbiguities,
67
68
  satClockCorrection,
69
+ sbasProtectionLevels,
68
70
  solidEarthTide,
69
71
  solveDgnss,
70
72
  solvePpp,
package/dist/sbf.cjs CHANGED
@@ -1981,7 +1981,7 @@ function decodeSbasGeoNav(msg, prn, week, tow) {
1981
1981
  }
1982
1982
 
1983
1983
  // src/sbf/rawnav-sbas.ts
1984
- function feedGeoBlock(view, b) {
1984
+ function feedGeoBlock(view, b, onSbasMessage) {
1985
1985
  const tow = view.getUint32(b + 8, true);
1986
1986
  const wnc = view.getUint16(b + 12, true);
1987
1987
  const svid = view.getUint8(b + 14);
@@ -1997,10 +1997,11 @@ function feedGeoBlock(view, b) {
1997
1997
  }
1998
1998
  if (!sbasCrcOk(msg)) return { badCrc: true };
1999
1999
  const prn = Number(prnStr.slice(1)) + 100;
2000
+ onSbasMessage?.(msg, prn, wnc, tow / 1e3);
2000
2001
  const eph = decodeSbasGeoNav(msg, prn, wnc, tow / 1e3);
2001
2002
  return eph ? { eph } : {};
2002
2003
  }
2003
- function parseSbfGeoNav(data) {
2004
+ function parseSbfGeoNav(data, opts = {}) {
2004
2005
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
2005
2006
  const ephemerides = [];
2006
2007
  const seen = /* @__PURE__ */ new Set();
@@ -2009,7 +2010,7 @@ function parseSbfGeoNav(data) {
2009
2010
  scanSbfFrames(data, view, (id, b, len) => {
2010
2011
  if (id !== 4020 || len < 52) return;
2011
2012
  messages++;
2012
- const r = feedGeoBlock(view, b);
2013
+ const r = feedGeoBlock(view, b, opts.onSbasMessage);
2013
2014
  if (r.eph) {
2014
2015
  const key = `${r.eph.prn}|${r.eph.tocDate.getTime()}`;
2015
2016
  if (!seen.has(key)) {
@@ -2029,7 +2030,7 @@ function ephKey(e) {
2029
2030
  const source = "source" in e ? e.source : "";
2030
2031
  return `${e.prn}|${e.tocDate.getTime()}|${source}`;
2031
2032
  }
2032
- function decodeSbfNavigation(data) {
2033
+ function decodeSbfNavigation(data, opts = {}) {
2033
2034
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
2034
2035
  const ephemerides = [];
2035
2036
  const seen = /* @__PURE__ */ new Set();
@@ -2150,7 +2151,7 @@ function decodeSbfNavigation(data) {
2150
2151
  case 4020: {
2151
2152
  if (len < 52) return;
2152
2153
  counts.sbasRaw++;
2153
- const r = feedGeoBlock(view, b);
2154
+ const r = feedGeoBlock(view, b, opts.onSbasMessage);
2154
2155
  if (r.eph) addEph(r.eph);
2155
2156
  else if (r.badCrc) counts.badFrames++;
2156
2157
  return;
package/dist/sbf.d.cts CHANGED
@@ -265,7 +265,11 @@ interface SbfNavigation {
265
265
  * F/NAV, GLONASS strings, BeiDou D1/D2, and the Klobuchar/NeQuick iono +
266
266
  * GPS-UTC leap-second blocks. See the module header for merge semantics.
267
267
  */
268
- declare function decodeSbfNavigation(data: Uint8Array): SbfNavigation;
268
+ declare function decodeSbfNavigation(data: Uint8Array, opts?: {
269
+ /** Called for every CRC-valid SBAS L1 message (all types) — feed an
270
+ * {@link ../positioning/sbas.SbasProcessor} to build SBAS corrections. */
271
+ onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void;
272
+ }): SbfNavigation;
269
273
 
270
274
  /**
271
275
  * Septentrio SBF raw Galileo navigation-page blocks: GALRawINAV (4023)
@@ -372,7 +376,7 @@ declare function parseSbfGloNav(data: Uint8Array): SbfGloNavResult;
372
376
  * message type 9. Returns the ephemeris, `{ badCrc: true }` on a failed
373
377
  * CRC, or `{}` for any other (non–type-9) message.
374
378
  */
375
- declare function feedGeoBlock(view: DataView, b: number): {
379
+ declare function feedGeoBlock(view: DataView, b: number, onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void): {
376
380
  eph?: GlonassEphemeris;
377
381
  badCrc?: boolean;
378
382
  };
@@ -391,7 +395,9 @@ interface SbfGeoNavResult {
391
395
  * the same blocks through {@link feedGeoBlock}; this standalone parser
392
396
  * mirrors the per-constellation `parseSbf*` helpers.
393
397
  */
394
- declare function parseSbfGeoNav(data: Uint8Array): SbfGeoNavResult;
398
+ declare function parseSbfGeoNav(data: Uint8Array, opts?: {
399
+ onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void;
400
+ }): SbfGeoNavResult;
395
401
 
396
402
  /** Constellations HAS corrects: GPS (GNSS ID 0), Galileo (GNSS ID 2). */
397
403
  type HasSystem = 'G' | 'E';
package/dist/sbf.d.ts CHANGED
@@ -265,7 +265,11 @@ interface SbfNavigation {
265
265
  * F/NAV, GLONASS strings, BeiDou D1/D2, and the Klobuchar/NeQuick iono +
266
266
  * GPS-UTC leap-second blocks. See the module header for merge semantics.
267
267
  */
268
- declare function decodeSbfNavigation(data: Uint8Array): SbfNavigation;
268
+ declare function decodeSbfNavigation(data: Uint8Array, opts?: {
269
+ /** Called for every CRC-valid SBAS L1 message (all types) — feed an
270
+ * {@link ../positioning/sbas.SbasProcessor} to build SBAS corrections. */
271
+ onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void;
272
+ }): SbfNavigation;
269
273
 
270
274
  /**
271
275
  * Septentrio SBF raw Galileo navigation-page blocks: GALRawINAV (4023)
@@ -372,7 +376,7 @@ declare function parseSbfGloNav(data: Uint8Array): SbfGloNavResult;
372
376
  * message type 9. Returns the ephemeris, `{ badCrc: true }` on a failed
373
377
  * CRC, or `{}` for any other (non–type-9) message.
374
378
  */
375
- declare function feedGeoBlock(view: DataView, b: number): {
379
+ declare function feedGeoBlock(view: DataView, b: number, onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void): {
376
380
  eph?: GlonassEphemeris;
377
381
  badCrc?: boolean;
378
382
  };
@@ -391,7 +395,9 @@ interface SbfGeoNavResult {
391
395
  * the same blocks through {@link feedGeoBlock}; this standalone parser
392
396
  * mirrors the per-constellation `parseSbf*` helpers.
393
397
  */
394
- declare function parseSbfGeoNav(data: Uint8Array): SbfGeoNavResult;
398
+ declare function parseSbfGeoNav(data: Uint8Array, opts?: {
399
+ onSbasMessage?: (msg: Uint8Array, prn: number, week: number, tow: number) => void;
400
+ }): SbfGeoNavResult;
395
401
 
396
402
  /** Constellations HAS corrects: GPS (GNSS ID 0), Galileo (GNSS ID 2). */
397
403
  type HasSystem = 'G' | 'E';
package/dist/sbf.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  parseSbfNav,
16
16
  parseSbfReceiverSetup,
17
17
  scanSbfFrames
18
- } from "./chunk-XFPDTGSV.js";
18
+ } from "./chunk-DE5IUULR.js";
19
19
  import "./chunk-FA2UTLEQ.js";
20
20
  import "./chunk-WQX354KD.js";
21
21
  import "./chunk-37QNKGTC.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.50.0",
3
+ "version": "1.52.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,