gnss-js 1.59.0 → 1.61.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.
@@ -1270,6 +1270,44 @@ var DEGF = [
1270
1270
  58e-4
1271
1271
  ];
1272
1272
  var degfcorr = (ai) => ai > 0 && ai <= 15 ? DEGF[ai] : 58e-4;
1273
+ var DELTA_UDRE = [
1274
+ 1,
1275
+ 1.1,
1276
+ 1.25,
1277
+ 1.5,
1278
+ 2,
1279
+ 3,
1280
+ 4,
1281
+ 5,
1282
+ 6,
1283
+ 8,
1284
+ 10,
1285
+ 20,
1286
+ 30,
1287
+ 40,
1288
+ 50,
1289
+ 100
1290
+ ];
1291
+ function pointInRegion(lat, lon, rg) {
1292
+ const poly = rg.square ? [
1293
+ [rg.lat1, rg.lon1],
1294
+ [rg.lat1, rg.lon2],
1295
+ [rg.lat2, rg.lon2],
1296
+ [rg.lat2, rg.lon1]
1297
+ ] : [
1298
+ [rg.lat1, rg.lon1],
1299
+ [rg.lat2, rg.lon2],
1300
+ [rg.lat1, rg.lon2]
1301
+ ];
1302
+ let inside = false;
1303
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
1304
+ const [yi, xi] = poly[i];
1305
+ const [yj, xj] = poly[j];
1306
+ if (yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi)
1307
+ inside = !inside;
1308
+ }
1309
+ return inside;
1310
+ }
1273
1311
  function sbasLongTermDeg(d, vel, ageSec) {
1274
1312
  if (vel) {
1275
1313
  if (ageSec > 0 && ageSec < d.iltcV1) return 0;
@@ -1335,6 +1373,7 @@ var SbasProcessor = class {
1335
1373
  iodp = -1;
1336
1374
  tlat = 0;
1337
1375
  degr = null;
1376
+ mt27 = null;
1338
1377
  ion = [];
1339
1378
  /** GEO PRN(s) whose messages have been ingested. */
1340
1379
  geoPrns = /* @__PURE__ */ new Set();
@@ -1363,6 +1402,8 @@ var SbasProcessor = class {
1363
1402
  return this.decodeDegradation(msg) ? type : -1;
1364
1403
  case 10:
1365
1404
  return this.decodeMt10(msg) ? type : -1;
1405
+ case 27:
1406
+ return this.decodeMt27(msg) ? type : -1;
1366
1407
  case 18:
1367
1408
  return this.decodeIgpMask(msg) ? type : -1;
1368
1409
  case 24:
@@ -1380,14 +1421,17 @@ var SbasProcessor = class {
1380
1421
  }
1381
1422
  }
1382
1423
  decodeMask(msg) {
1383
- const sats = [];
1424
+ const iodp = getBitU(msg, 224, 2);
1425
+ const prns = [];
1384
1426
  for (let i = 1; i <= 210; i++) {
1385
- if (getBitU(msg, 13 + i, 1)) sats.push({ prn: maskPrn(i) });
1427
+ if (getBitU(msg, 13 + i, 1)) prns.push(maskPrn(i));
1386
1428
  }
1387
- this.iodp = getBitU(msg, 224, 2);
1388
- this.sats = sats;
1429
+ if (this.iodp === iodp && this.sats.length === prns.length && this.sats.every((s, k) => s.prn === prns[k]))
1430
+ return true;
1431
+ this.iodp = iodp;
1432
+ this.sats = prns.map((prn) => ({ prn }));
1389
1433
  this.satIdx.clear();
1390
- sats.forEach((s, k) => {
1434
+ this.sats.forEach((s, k) => {
1391
1435
  if (s.prn) this.satIdx.set(s.prn, k);
1392
1436
  });
1393
1437
  return true;
@@ -1455,6 +1499,54 @@ var SbasProcessor = class {
1455
1499
  };
1456
1500
  return true;
1457
1501
  }
1502
+ /** MT27 SBAS service message (DO-229D §A.4.4.13, Table A-20): regions with a
1503
+ * δUDRE that inflates σ_UDRE for users inside/outside them. */
1504
+ decodeMt27(msg) {
1505
+ const iods = getBitU(msg, 14, 3);
1506
+ const svcNum = getBitU(msg, 20, 3) + 1;
1507
+ const nRegions = getBitU(msg, 23, 3);
1508
+ const priority = getBitU(msg, 26, 2);
1509
+ const udreInside = getBitU(msg, 28, 4);
1510
+ const udreOutside = getBitU(msg, 32, 4);
1511
+ if (!this.mt27 || this.mt27.iods !== iods)
1512
+ this.mt27 = { iods, udreOutside, msgs: /* @__PURE__ */ new Map() };
1513
+ this.mt27.udreOutside = udreOutside;
1514
+ const regions = [];
1515
+ for (let r = 0; r < nRegions && r < 5; r++) {
1516
+ const p = 36 + r * 35;
1517
+ regions.push({
1518
+ lat1: getBitS(msg, p, 8),
1519
+ lon1: getBitS(msg, p + 8, 9),
1520
+ lat2: getBitS(msg, p + 17, 8),
1521
+ lon2: getBitS(msg, p + 25, 9),
1522
+ square: getBitU(msg, p + 34, 1) === 1
1523
+ });
1524
+ }
1525
+ this.mt27.msgs.set(svcNum, { priority, udreInside, regions });
1526
+ return true;
1527
+ }
1528
+ /** δUDRE multiplier (DO-229D Table A-21) for a user location from the MT27
1529
+ * set: the highest-priority containing region wins (ties → lower δUDRE),
1530
+ * else the outside value. 1 when no MT27 has been received. */
1531
+ deltaUdre(latDeg, lonDeg) {
1532
+ const s = this.mt27;
1533
+ if (!s) return 1;
1534
+ let best = null;
1535
+ let bestPri = -1;
1536
+ for (const m of s.msgs.values()) {
1537
+ for (const rg of m.regions) {
1538
+ if (!pointInRegion(latDeg, lonDeg, rg)) continue;
1539
+ const val = DELTA_UDRE[m.udreInside] ?? 1;
1540
+ if (m.priority > bestPri) {
1541
+ bestPri = m.priority;
1542
+ best = val;
1543
+ } else if (m.priority === bestPri && best !== null) {
1544
+ best = Math.min(best, val);
1545
+ }
1546
+ }
1547
+ }
1548
+ return best ?? DELTA_UDRE[s.udreOutside] ?? 1;
1549
+ }
1458
1550
  decodeMixed(msg, week, tow) {
1459
1551
  if (this.iodp !== getBitU(msg, 110, 2)) return false;
1460
1552
  const blk = getBitU(msg, 112, 2);
@@ -1559,7 +1651,11 @@ var SbasProcessor = class {
1559
1651
  break;
1560
1652
  }
1561
1653
  }
1562
- this.ion[band] = { iodi: getBitU(msg, 22, 2), igp };
1654
+ const iodi = getBitU(msg, 22, 2);
1655
+ const cur = this.ion[band];
1656
+ if (cur && cur.iodi === iodi && cur.igp.length === igp.length && cur.igp.every((g, k) => g.lat === igp[k].lat && g.lon === igp[k].lon))
1657
+ return true;
1658
+ this.ion[band] = { iodi, igp };
1563
1659
  return true;
1564
1660
  }
1565
1661
  decodeIgpDelay(msg, week, tow) {
@@ -1626,7 +1722,7 @@ var SbasProcessor = class {
1626
1722
  * no valid (unexpired) long-term + fast correction is available. Add `dPos`
1627
1723
  * to the satellite ECEF position and `dClkS` to its clock bias.
1628
1724
  */
1629
- satCorrection(prn, week, tow) {
1725
+ satCorrection(prn, week, tow, userLatRad, userLonRad) {
1630
1726
  const idx = this.satIdx.get(prn);
1631
1727
  if (idx === void 0) return null;
1632
1728
  const s = this.sats[idx];
@@ -1648,7 +1744,11 @@ var SbasProcessor = class {
1648
1744
  const tf = now - s.fcorr.t0s + this.tlat;
1649
1745
  if (Math.abs(tf) > MAXSBSAGEF || s.fcorr.udre >= 15) return null;
1650
1746
  const prc = s.fcorr.prc;
1651
- const sigUdre = Math.sqrt(varfcorr(s.fcorr.udre));
1747
+ const dudre = userLatRad != null && userLonRad != null ? this.deltaUdre(
1748
+ userLatRad * 180 / Math.PI,
1749
+ userLonRad * 180 / Math.PI
1750
+ ) : 1;
1751
+ const sigUdre = Math.sqrt(varfcorr(s.fcorr.udre)) * dudre;
1652
1752
  const efc = degfcorr(s.fcorr.ai) * tf * tf / 2;
1653
1753
  const eltc = this.degr && s.lcorr && s.lcorr.t0s !== 0 ? sbasLongTermDeg(this.degr, s.lcorr.vel, now - s.lcorr.t0s) : 0;
1654
1754
  const varM2 = this.degr?.rssUdre ? sigUdre * sigUdre + efc * efc + eltc * eltc : (sigUdre + efc + eltc) ** 2;
@@ -3387,7 +3487,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3387
3487
  used.push(prn);
3388
3488
  azels.push(azel);
3389
3489
  if (sbas) {
3390
- const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
3490
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow, rxLat, rxLon);
3391
3491
  const si = sbas.ionoDelay(
3392
3492
  gpsWeek,
3393
3493
  gpsTow,
package/dist/index.cjs CHANGED
@@ -13480,6 +13480,44 @@ var DEGF = [
13480
13480
  58e-4
13481
13481
  ];
13482
13482
  var degfcorr = (ai) => ai > 0 && ai <= 15 ? DEGF[ai] : 58e-4;
13483
+ var DELTA_UDRE = [
13484
+ 1,
13485
+ 1.1,
13486
+ 1.25,
13487
+ 1.5,
13488
+ 2,
13489
+ 3,
13490
+ 4,
13491
+ 5,
13492
+ 6,
13493
+ 8,
13494
+ 10,
13495
+ 20,
13496
+ 30,
13497
+ 40,
13498
+ 50,
13499
+ 100
13500
+ ];
13501
+ function pointInRegion(lat, lon, rg) {
13502
+ const poly = rg.square ? [
13503
+ [rg.lat1, rg.lon1],
13504
+ [rg.lat1, rg.lon2],
13505
+ [rg.lat2, rg.lon2],
13506
+ [rg.lat2, rg.lon1]
13507
+ ] : [
13508
+ [rg.lat1, rg.lon1],
13509
+ [rg.lat2, rg.lon2],
13510
+ [rg.lat1, rg.lon2]
13511
+ ];
13512
+ let inside = false;
13513
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
13514
+ const [yi, xi] = poly[i];
13515
+ const [yj, xj] = poly[j];
13516
+ if (yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi)
13517
+ inside = !inside;
13518
+ }
13519
+ return inside;
13520
+ }
13483
13521
  function sbasLongTermDeg(d, vel, ageSec) {
13484
13522
  if (vel) {
13485
13523
  if (ageSec > 0 && ageSec < d.iltcV1) return 0;
@@ -13545,6 +13583,7 @@ var SbasProcessor = class {
13545
13583
  iodp = -1;
13546
13584
  tlat = 0;
13547
13585
  degr = null;
13586
+ mt27 = null;
13548
13587
  ion = [];
13549
13588
  /** GEO PRN(s) whose messages have been ingested. */
13550
13589
  geoPrns = /* @__PURE__ */ new Set();
@@ -13573,6 +13612,8 @@ var SbasProcessor = class {
13573
13612
  return this.decodeDegradation(msg) ? type : -1;
13574
13613
  case 10:
13575
13614
  return this.decodeMt10(msg) ? type : -1;
13615
+ case 27:
13616
+ return this.decodeMt27(msg) ? type : -1;
13576
13617
  case 18:
13577
13618
  return this.decodeIgpMask(msg) ? type : -1;
13578
13619
  case 24:
@@ -13590,14 +13631,17 @@ var SbasProcessor = class {
13590
13631
  }
13591
13632
  }
13592
13633
  decodeMask(msg) {
13593
- const sats = [];
13634
+ const iodp = getBitU(msg, 224, 2);
13635
+ const prns = [];
13594
13636
  for (let i = 1; i <= 210; i++) {
13595
- if (getBitU(msg, 13 + i, 1)) sats.push({ prn: maskPrn(i) });
13637
+ if (getBitU(msg, 13 + i, 1)) prns.push(maskPrn(i));
13596
13638
  }
13597
- this.iodp = getBitU(msg, 224, 2);
13598
- this.sats = sats;
13639
+ if (this.iodp === iodp && this.sats.length === prns.length && this.sats.every((s, k) => s.prn === prns[k]))
13640
+ return true;
13641
+ this.iodp = iodp;
13642
+ this.sats = prns.map((prn) => ({ prn }));
13599
13643
  this.satIdx.clear();
13600
- sats.forEach((s, k) => {
13644
+ this.sats.forEach((s, k) => {
13601
13645
  if (s.prn) this.satIdx.set(s.prn, k);
13602
13646
  });
13603
13647
  return true;
@@ -13665,6 +13709,54 @@ var SbasProcessor = class {
13665
13709
  };
13666
13710
  return true;
13667
13711
  }
13712
+ /** MT27 SBAS service message (DO-229D §A.4.4.13, Table A-20): regions with a
13713
+ * δUDRE that inflates σ_UDRE for users inside/outside them. */
13714
+ decodeMt27(msg) {
13715
+ const iods = getBitU(msg, 14, 3);
13716
+ const svcNum = getBitU(msg, 20, 3) + 1;
13717
+ const nRegions = getBitU(msg, 23, 3);
13718
+ const priority = getBitU(msg, 26, 2);
13719
+ const udreInside = getBitU(msg, 28, 4);
13720
+ const udreOutside = getBitU(msg, 32, 4);
13721
+ if (!this.mt27 || this.mt27.iods !== iods)
13722
+ this.mt27 = { iods, udreOutside, msgs: /* @__PURE__ */ new Map() };
13723
+ this.mt27.udreOutside = udreOutside;
13724
+ const regions = [];
13725
+ for (let r = 0; r < nRegions && r < 5; r++) {
13726
+ const p = 36 + r * 35;
13727
+ regions.push({
13728
+ lat1: getBitS(msg, p, 8),
13729
+ lon1: getBitS(msg, p + 8, 9),
13730
+ lat2: getBitS(msg, p + 17, 8),
13731
+ lon2: getBitS(msg, p + 25, 9),
13732
+ square: getBitU(msg, p + 34, 1) === 1
13733
+ });
13734
+ }
13735
+ this.mt27.msgs.set(svcNum, { priority, udreInside, regions });
13736
+ return true;
13737
+ }
13738
+ /** δUDRE multiplier (DO-229D Table A-21) for a user location from the MT27
13739
+ * set: the highest-priority containing region wins (ties → lower δUDRE),
13740
+ * else the outside value. 1 when no MT27 has been received. */
13741
+ deltaUdre(latDeg, lonDeg) {
13742
+ const s = this.mt27;
13743
+ if (!s) return 1;
13744
+ let best = null;
13745
+ let bestPri = -1;
13746
+ for (const m of s.msgs.values()) {
13747
+ for (const rg of m.regions) {
13748
+ if (!pointInRegion(latDeg, lonDeg, rg)) continue;
13749
+ const val = DELTA_UDRE[m.udreInside] ?? 1;
13750
+ if (m.priority > bestPri) {
13751
+ bestPri = m.priority;
13752
+ best = val;
13753
+ } else if (m.priority === bestPri && best !== null) {
13754
+ best = Math.min(best, val);
13755
+ }
13756
+ }
13757
+ }
13758
+ return best ?? DELTA_UDRE[s.udreOutside] ?? 1;
13759
+ }
13668
13760
  decodeMixed(msg, week, tow) {
13669
13761
  if (this.iodp !== getBitU(msg, 110, 2)) return false;
13670
13762
  const blk = getBitU(msg, 112, 2);
@@ -13769,7 +13861,11 @@ var SbasProcessor = class {
13769
13861
  break;
13770
13862
  }
13771
13863
  }
13772
- this.ion[band] = { iodi: getBitU(msg, 22, 2), igp };
13864
+ const iodi = getBitU(msg, 22, 2);
13865
+ const cur = this.ion[band];
13866
+ if (cur && cur.iodi === iodi && cur.igp.length === igp.length && cur.igp.every((g, k) => g.lat === igp[k].lat && g.lon === igp[k].lon))
13867
+ return true;
13868
+ this.ion[band] = { iodi, igp };
13773
13869
  return true;
13774
13870
  }
13775
13871
  decodeIgpDelay(msg, week, tow) {
@@ -13836,7 +13932,7 @@ var SbasProcessor = class {
13836
13932
  * no valid (unexpired) long-term + fast correction is available. Add `dPos`
13837
13933
  * to the satellite ECEF position and `dClkS` to its clock bias.
13838
13934
  */
13839
- satCorrection(prn, week, tow) {
13935
+ satCorrection(prn, week, tow, userLatRad, userLonRad) {
13840
13936
  const idx = this.satIdx.get(prn);
13841
13937
  if (idx === void 0) return null;
13842
13938
  const s = this.sats[idx];
@@ -13858,7 +13954,11 @@ var SbasProcessor = class {
13858
13954
  const tf = now - s.fcorr.t0s + this.tlat;
13859
13955
  if (Math.abs(tf) > MAXSBSAGEF || s.fcorr.udre >= 15) return null;
13860
13956
  const prc = s.fcorr.prc;
13861
- const sigUdre = Math.sqrt(varfcorr(s.fcorr.udre));
13957
+ const dudre = userLatRad != null && userLonRad != null ? this.deltaUdre(
13958
+ userLatRad * 180 / Math.PI,
13959
+ userLonRad * 180 / Math.PI
13960
+ ) : 1;
13961
+ const sigUdre = Math.sqrt(varfcorr(s.fcorr.udre)) * dudre;
13862
13962
  const efc = degfcorr(s.fcorr.ai) * tf * tf / 2;
13863
13963
  const eltc = this.degr && s.lcorr && s.lcorr.t0s !== 0 ? sbasLongTermDeg(this.degr, s.lcorr.vel, now - s.lcorr.t0s) : 0;
13864
13964
  const varM2 = this.degr?.rssUdre ? sigUdre * sigUdre + efc * efc + eltc * eltc : (sigUdre + efc + eltc) ** 2;
@@ -15597,7 +15697,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
15597
15697
  used.push(prn);
15598
15698
  azels.push(azel);
15599
15699
  if (sbas) {
15600
- const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
15700
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow, rxLat, rxLon);
15601
15701
  const si = sbas.ionoDelay(
15602
15702
  gpsWeek,
15603
15703
  gpsTow,
package/dist/index.js CHANGED
@@ -178,7 +178,7 @@ import {
178
178
  sunEcef,
179
179
  toRtkEpoch,
180
180
  wlWavelength
181
- } from "./chunk-7NZ4SJTJ.js";
181
+ } from "./chunk-GWN2F3PQ.js";
182
182
  import "./chunk-WQX354KD.js";
183
183
  import {
184
184
  almanacEpochMs,
@@ -1746,6 +1746,44 @@ var DEGF = [
1746
1746
  58e-4
1747
1747
  ];
1748
1748
  var degfcorr = (ai) => ai > 0 && ai <= 15 ? DEGF[ai] : 58e-4;
1749
+ var DELTA_UDRE = [
1750
+ 1,
1751
+ 1.1,
1752
+ 1.25,
1753
+ 1.5,
1754
+ 2,
1755
+ 3,
1756
+ 4,
1757
+ 5,
1758
+ 6,
1759
+ 8,
1760
+ 10,
1761
+ 20,
1762
+ 30,
1763
+ 40,
1764
+ 50,
1765
+ 100
1766
+ ];
1767
+ function pointInRegion(lat, lon, rg) {
1768
+ const poly = rg.square ? [
1769
+ [rg.lat1, rg.lon1],
1770
+ [rg.lat1, rg.lon2],
1771
+ [rg.lat2, rg.lon2],
1772
+ [rg.lat2, rg.lon1]
1773
+ ] : [
1774
+ [rg.lat1, rg.lon1],
1775
+ [rg.lat2, rg.lon2],
1776
+ [rg.lat1, rg.lon2]
1777
+ ];
1778
+ let inside = false;
1779
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
1780
+ const [yi, xi] = poly[i];
1781
+ const [yj, xj] = poly[j];
1782
+ if (yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi)
1783
+ inside = !inside;
1784
+ }
1785
+ return inside;
1786
+ }
1749
1787
  function sbasLongTermDeg(d, vel, ageSec) {
1750
1788
  if (vel) {
1751
1789
  if (ageSec > 0 && ageSec < d.iltcV1) return 0;
@@ -1811,6 +1849,7 @@ var SbasProcessor = class {
1811
1849
  iodp = -1;
1812
1850
  tlat = 0;
1813
1851
  degr = null;
1852
+ mt27 = null;
1814
1853
  ion = [];
1815
1854
  /** GEO PRN(s) whose messages have been ingested. */
1816
1855
  geoPrns = /* @__PURE__ */ new Set();
@@ -1839,6 +1878,8 @@ var SbasProcessor = class {
1839
1878
  return this.decodeDegradation(msg) ? type : -1;
1840
1879
  case 10:
1841
1880
  return this.decodeMt10(msg) ? type : -1;
1881
+ case 27:
1882
+ return this.decodeMt27(msg) ? type : -1;
1842
1883
  case 18:
1843
1884
  return this.decodeIgpMask(msg) ? type : -1;
1844
1885
  case 24:
@@ -1856,14 +1897,17 @@ var SbasProcessor = class {
1856
1897
  }
1857
1898
  }
1858
1899
  decodeMask(msg) {
1859
- const sats = [];
1900
+ const iodp = getBitU(msg, 224, 2);
1901
+ const prns = [];
1860
1902
  for (let i = 1; i <= 210; i++) {
1861
- if (getBitU(msg, 13 + i, 1)) sats.push({ prn: maskPrn(i) });
1903
+ if (getBitU(msg, 13 + i, 1)) prns.push(maskPrn(i));
1862
1904
  }
1863
- this.iodp = getBitU(msg, 224, 2);
1864
- this.sats = sats;
1905
+ if (this.iodp === iodp && this.sats.length === prns.length && this.sats.every((s, k) => s.prn === prns[k]))
1906
+ return true;
1907
+ this.iodp = iodp;
1908
+ this.sats = prns.map((prn) => ({ prn }));
1865
1909
  this.satIdx.clear();
1866
- sats.forEach((s, k) => {
1910
+ this.sats.forEach((s, k) => {
1867
1911
  if (s.prn) this.satIdx.set(s.prn, k);
1868
1912
  });
1869
1913
  return true;
@@ -1931,6 +1975,54 @@ var SbasProcessor = class {
1931
1975
  };
1932
1976
  return true;
1933
1977
  }
1978
+ /** MT27 SBAS service message (DO-229D §A.4.4.13, Table A-20): regions with a
1979
+ * δUDRE that inflates σ_UDRE for users inside/outside them. */
1980
+ decodeMt27(msg) {
1981
+ const iods = getBitU(msg, 14, 3);
1982
+ const svcNum = getBitU(msg, 20, 3) + 1;
1983
+ const nRegions = getBitU(msg, 23, 3);
1984
+ const priority = getBitU(msg, 26, 2);
1985
+ const udreInside = getBitU(msg, 28, 4);
1986
+ const udreOutside = getBitU(msg, 32, 4);
1987
+ if (!this.mt27 || this.mt27.iods !== iods)
1988
+ this.mt27 = { iods, udreOutside, msgs: /* @__PURE__ */ new Map() };
1989
+ this.mt27.udreOutside = udreOutside;
1990
+ const regions = [];
1991
+ for (let r = 0; r < nRegions && r < 5; r++) {
1992
+ const p = 36 + r * 35;
1993
+ regions.push({
1994
+ lat1: getBitS(msg, p, 8),
1995
+ lon1: getBitS(msg, p + 8, 9),
1996
+ lat2: getBitS(msg, p + 17, 8),
1997
+ lon2: getBitS(msg, p + 25, 9),
1998
+ square: getBitU(msg, p + 34, 1) === 1
1999
+ });
2000
+ }
2001
+ this.mt27.msgs.set(svcNum, { priority, udreInside, regions });
2002
+ return true;
2003
+ }
2004
+ /** δUDRE multiplier (DO-229D Table A-21) for a user location from the MT27
2005
+ * set: the highest-priority containing region wins (ties → lower δUDRE),
2006
+ * else the outside value. 1 when no MT27 has been received. */
2007
+ deltaUdre(latDeg, lonDeg) {
2008
+ const s = this.mt27;
2009
+ if (!s) return 1;
2010
+ let best = null;
2011
+ let bestPri = -1;
2012
+ for (const m of s.msgs.values()) {
2013
+ for (const rg of m.regions) {
2014
+ if (!pointInRegion(latDeg, lonDeg, rg)) continue;
2015
+ const val = DELTA_UDRE[m.udreInside] ?? 1;
2016
+ if (m.priority > bestPri) {
2017
+ bestPri = m.priority;
2018
+ best = val;
2019
+ } else if (m.priority === bestPri && best !== null) {
2020
+ best = Math.min(best, val);
2021
+ }
2022
+ }
2023
+ }
2024
+ return best ?? DELTA_UDRE[s.udreOutside] ?? 1;
2025
+ }
1934
2026
  decodeMixed(msg, week, tow) {
1935
2027
  if (this.iodp !== getBitU(msg, 110, 2)) return false;
1936
2028
  const blk = getBitU(msg, 112, 2);
@@ -2035,7 +2127,11 @@ var SbasProcessor = class {
2035
2127
  break;
2036
2128
  }
2037
2129
  }
2038
- this.ion[band] = { iodi: getBitU(msg, 22, 2), igp };
2130
+ const iodi = getBitU(msg, 22, 2);
2131
+ const cur = this.ion[band];
2132
+ if (cur && cur.iodi === iodi && cur.igp.length === igp.length && cur.igp.every((g, k) => g.lat === igp[k].lat && g.lon === igp[k].lon))
2133
+ return true;
2134
+ this.ion[band] = { iodi, igp };
2039
2135
  return true;
2040
2136
  }
2041
2137
  decodeIgpDelay(msg, week, tow) {
@@ -2102,7 +2198,7 @@ var SbasProcessor = class {
2102
2198
  * no valid (unexpired) long-term + fast correction is available. Add `dPos`
2103
2199
  * to the satellite ECEF position and `dClkS` to its clock bias.
2104
2200
  */
2105
- satCorrection(prn, week, tow) {
2201
+ satCorrection(prn, week, tow, userLatRad, userLonRad) {
2106
2202
  const idx = this.satIdx.get(prn);
2107
2203
  if (idx === void 0) return null;
2108
2204
  const s = this.sats[idx];
@@ -2124,7 +2220,11 @@ var SbasProcessor = class {
2124
2220
  const tf = now - s.fcorr.t0s + this.tlat;
2125
2221
  if (Math.abs(tf) > MAXSBSAGEF || s.fcorr.udre >= 15) return null;
2126
2222
  const prc = s.fcorr.prc;
2127
- const sigUdre = Math.sqrt(varfcorr(s.fcorr.udre));
2223
+ const dudre = userLatRad != null && userLonRad != null ? this.deltaUdre(
2224
+ userLatRad * 180 / Math.PI,
2225
+ userLonRad * 180 / Math.PI
2226
+ ) : 1;
2227
+ const sigUdre = Math.sqrt(varfcorr(s.fcorr.udre)) * dudre;
2128
2228
  const efc = degfcorr(s.fcorr.ai) * tf * tf / 2;
2129
2229
  const eltc = this.degr && s.lcorr && s.lcorr.t0s !== 0 ? sbasLongTermDeg(this.degr, s.lcorr.vel, now - s.lcorr.t0s) : 0;
2130
2230
  const varM2 = this.degr?.rssUdre ? sigUdre * sigUdre + efc * efc + eltc * eltc : (sigUdre + efc + eltc) ** 2;
@@ -3935,7 +4035,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
3935
4035
  used.push(prn);
3936
4036
  azels.push(azel);
3937
4037
  if (sbas) {
3938
- const sc = sbas.satCorrection(prn, gpsWeek, gpsTow);
4038
+ const sc = sbas.satCorrection(prn, gpsWeek, gpsTow, rxLat, rxLon);
3939
4039
  const si = sbas.ionoDelay(
3940
4040
  gpsWeek,
3941
4041
  gpsTow,
@@ -605,6 +605,7 @@ declare class SbasProcessor {
605
605
  private iodp;
606
606
  private tlat;
607
607
  private degr;
608
+ private mt27;
608
609
  private ion;
609
610
  /** GEO PRN(s) whose messages have been ingested. */
610
611
  readonly geoPrns: Set<string>;
@@ -622,6 +623,13 @@ declare class SbasProcessor {
622
623
  * preamble + 6-bit type (start bit 14), MSB-first, with the listed LSBs.
623
624
  * Iiono / Iltc_v0 of 0 must be read as 1 (Table A-9, Note 3). */
624
625
  private decodeMt10;
626
+ /** MT27 SBAS service message (DO-229D §A.4.4.13, Table A-20): regions with a
627
+ * δUDRE that inflates σ_UDRE for users inside/outside them. */
628
+ private decodeMt27;
629
+ /** δUDRE multiplier (DO-229D Table A-21) for a user location from the MT27
630
+ * set: the highest-priority containing region wins (ties → lower δUDRE),
631
+ * else the outside value. 1 when no MT27 has been received. */
632
+ deltaUdre(latDeg: number, lonDeg: number): number;
625
633
  private decodeMixed;
626
634
  private decodeLongCorrHalf;
627
635
  private decodeLong0;
@@ -657,7 +665,7 @@ declare class SbasProcessor {
657
665
  * no valid (unexpired) long-term + fast correction is available. Add `dPos`
658
666
  * to the satellite ECEF position and `dClkS` to its clock bias.
659
667
  */
660
- satCorrection(prn: string, week: number, tow: number): SbasSatCorrection | null;
668
+ satCorrection(prn: string, week: number, tow: number, userLatRad?: number, userLonRad?: number): SbasSatCorrection | null;
661
669
  /** The last-decoded MT10 degradation factors, or null if none seen. */
662
670
  get degradation(): Readonly<Degradation> | null;
663
671
  /**
@@ -1292,7 +1300,10 @@ interface SppOptions {
1292
1300
  /** The two SBAS corrections `solveSpp` consumes — {@link ./sbas.SbasProcessor}
1293
1301
  * satisfies this structurally. */
1294
1302
  interface SbasCorrectionSource {
1295
- satCorrection(prn: string, week: number, tow: number): {
1303
+ satCorrection(prn: string, week: number, tow: number,
1304
+ /** User geodetic lat/lon (rad) for the MT27 service-region δUDRE, when
1305
+ * known (the final protection-level pass). Omitted during iteration. */
1306
+ userLatRad?: number, userLonRad?: number): {
1296
1307
  dPos: [number, number, number];
1297
1308
  dClkS: number;
1298
1309
  varM2: number;
@@ -605,6 +605,7 @@ declare class SbasProcessor {
605
605
  private iodp;
606
606
  private tlat;
607
607
  private degr;
608
+ private mt27;
608
609
  private ion;
609
610
  /** GEO PRN(s) whose messages have been ingested. */
610
611
  readonly geoPrns: Set<string>;
@@ -622,6 +623,13 @@ declare class SbasProcessor {
622
623
  * preamble + 6-bit type (start bit 14), MSB-first, with the listed LSBs.
623
624
  * Iiono / Iltc_v0 of 0 must be read as 1 (Table A-9, Note 3). */
624
625
  private decodeMt10;
626
+ /** MT27 SBAS service message (DO-229D §A.4.4.13, Table A-20): regions with a
627
+ * δUDRE that inflates σ_UDRE for users inside/outside them. */
628
+ private decodeMt27;
629
+ /** δUDRE multiplier (DO-229D Table A-21) for a user location from the MT27
630
+ * set: the highest-priority containing region wins (ties → lower δUDRE),
631
+ * else the outside value. 1 when no MT27 has been received. */
632
+ deltaUdre(latDeg: number, lonDeg: number): number;
625
633
  private decodeMixed;
626
634
  private decodeLongCorrHalf;
627
635
  private decodeLong0;
@@ -657,7 +665,7 @@ declare class SbasProcessor {
657
665
  * no valid (unexpired) long-term + fast correction is available. Add `dPos`
658
666
  * to the satellite ECEF position and `dClkS` to its clock bias.
659
667
  */
660
- satCorrection(prn: string, week: number, tow: number): SbasSatCorrection | null;
668
+ satCorrection(prn: string, week: number, tow: number, userLatRad?: number, userLonRad?: number): SbasSatCorrection | null;
661
669
  /** The last-decoded MT10 degradation factors, or null if none seen. */
662
670
  get degradation(): Readonly<Degradation> | null;
663
671
  /**
@@ -1292,7 +1300,10 @@ interface SppOptions {
1292
1300
  /** The two SBAS corrections `solveSpp` consumes — {@link ./sbas.SbasProcessor}
1293
1301
  * satisfies this structurally. */
1294
1302
  interface SbasCorrectionSource {
1295
- satCorrection(prn: string, week: number, tow: number): {
1303
+ satCorrection(prn: string, week: number, tow: number,
1304
+ /** User geodetic lat/lon (rad) for the MT27 service-region δUDRE, when
1305
+ * known (the final protection-level pass). Omitted during iteration. */
1306
+ userLatRad?: number, userLonRad?: number): {
1296
1307
  dPos: [number, number, number];
1297
1308
  dClkS: number;
1298
1309
  varM2: number;
@@ -32,7 +32,7 @@ import {
32
32
  sunEcef,
33
33
  toRtkEpoch,
34
34
  wlWavelength
35
- } from "./chunk-7NZ4SJTJ.js";
35
+ } from "./chunk-GWN2F3PQ.js";
36
36
  import "./chunk-WQX354KD.js";
37
37
  import "./chunk-MHGNKSSH.js";
38
38
  import "./chunk-MIIM4LDY.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.59.0",
3
+ "version": "1.61.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,