gnss-js 1.9.0 → 1.11.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.
@@ -9,6 +9,7 @@ import {
9
9
  START_GPS_TIME
10
10
  } from "./chunk-LEEU5OIO.js";
11
11
  import {
12
+ C_LIGHT,
12
13
  OMEGA_E
13
14
  } from "./chunk-FIEWO4J4.js";
14
15
 
@@ -27,6 +28,7 @@ function gmForSystem(sys) {
27
28
  if (sys === "C") return GM_BDS;
28
29
  return GM_GPS;
29
30
  }
31
+ var geoVelocityPass = false;
30
32
  function keplerPosition(eph, t) {
31
33
  const GM = gmForSystem(eph.system);
32
34
  const a = eph.sqrtA * eph.sqrtA;
@@ -54,8 +56,16 @@ function keplerPosition(eph, t) {
54
56
  const uk = phik + duk;
55
57
  const rk = a * (1 - eph.e * cosE) + drk;
56
58
  const ik = eph.i0 + dik + eph.idot * tk;
59
+ const EkDot = n / (1 - eph.e * cosE);
60
+ const vkDot = EkDot * Math.sqrt(1 - eph.e * eph.e) / (1 - eph.e * cosE);
61
+ const dukDot = 2 * vkDot * (eph.cus * cos2phi - eph.cuc * sin2phi);
62
+ const drkDot = a * eph.e * sinE * EkDot + 2 * vkDot * (eph.crs * cos2phi - eph.crc * sin2phi);
63
+ const dikDot = eph.idot + 2 * vkDot * (eph.cis * cos2phi - eph.cic * sin2phi);
64
+ const ukDot = vkDot + dukDot;
57
65
  const xp = rk * Math.cos(uk);
58
66
  const yp = rk * Math.sin(uk);
67
+ const xpDot = drkDot * Math.cos(uk) - rk * ukDot * Math.sin(uk);
68
+ const ypDot = drkDot * Math.sin(uk) + rk * ukDot * Math.cos(uk);
59
69
  const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < BDS_GEO_MAX_INCLINATION_RAD && a > BDS_GEO_MIN_SEMIMAJOR_AXIS_M;
60
70
  if (isBdsGeo) {
61
71
  const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
@@ -71,23 +81,43 @@ function keplerPosition(eph, t) {
71
81
  const sinPhi = Math.sin(phi);
72
82
  const COS5 = Math.cos(-5 * Math.PI / 180);
73
83
  const SIN5 = Math.sin(-5 * Math.PI / 180);
84
+ const px = xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5;
85
+ const py = -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5;
86
+ const pz = -yg * SIN5 + zg * COS5;
87
+ if (geoVelocityPass) {
88
+ return { prn: eph.prn, x: px, y: py, z: pz, vx: 0, vy: 0, vz: 0 };
89
+ }
90
+ geoVelocityPass = true;
91
+ const h = 0.5;
92
+ const pm = keplerPosition(eph, t - h);
93
+ const pp = keplerPosition(eph, t + h);
94
+ geoVelocityPass = false;
74
95
  return {
75
96
  prn: eph.prn,
76
- x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
77
- y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
78
- z: -yg * SIN5 + zg * COS5
97
+ x: px,
98
+ y: py,
99
+ z: pz,
100
+ vx: (pp.x - pm.x) / (2 * h),
101
+ vy: (pp.y - pm.y) / (2 * h),
102
+ vz: (pp.z - pm.z) / (2 * h)
79
103
  };
80
104
  }
81
105
  const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
106
+ const omegakDot = eph.omegaDot - OMEGA_E;
82
107
  const cosO = Math.cos(omegak);
83
108
  const sinO = Math.sin(omegak);
84
109
  const cosI = Math.cos(ik);
85
110
  const sinI = Math.sin(ik);
111
+ const x = xp * cosO - yp * cosI * sinO;
112
+ const y = xp * sinO + yp * cosI * cosO;
86
113
  return {
87
114
  prn: eph.prn,
88
- x: xp * cosO - yp * cosI * sinO,
89
- y: xp * sinO + yp * cosI * cosO,
90
- z: yp * sinI
115
+ x,
116
+ y,
117
+ z: yp * sinI,
118
+ vx: xpDot * cosO - ypDot * cosI * sinO + yp * sinI * sinO * dikDot - omegakDot * y,
119
+ vy: xpDot * sinO + ypDot * cosI * cosO - yp * sinI * cosO * dikDot + omegakDot * x,
120
+ vz: ypDot * sinI + yp * cosI * dikDot
91
121
  };
92
122
  }
93
123
  function gloDerivatives(state, acc) {
@@ -141,7 +171,25 @@ function glonassPosition(eph, tUtc) {
141
171
  if (Math.abs(remainder) > 1e-3) {
142
172
  state = rk4Step(state, acc, remainder);
143
173
  }
144
- return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
174
+ return {
175
+ prn: eph.prn,
176
+ x: state[0],
177
+ y: state[1],
178
+ z: state[2],
179
+ vx: state[3],
180
+ vy: state[4],
181
+ vz: state[5]
182
+ };
183
+ }
184
+ function rangeRate(sat, rx, rxVel = [0, 0, 0]) {
185
+ const dx = sat.x - rx[0];
186
+ const dy = sat.y - rx[1];
187
+ const dz = sat.z - rx[2];
188
+ const rho = Math.hypot(dx, dy, dz);
189
+ return ((sat.vx - rxVel[0]) * dx + (sat.vy - rxVel[1]) * dy + (sat.vz - rxVel[2]) * dz) / rho;
190
+ }
191
+ function dopplerHz(rangeRateMs, freqHz) {
192
+ return -rangeRateMs * freqHz / C_LIGHT;
145
193
  }
146
194
  function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
147
195
  const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
@@ -465,6 +513,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
465
513
  const subLat = {};
466
514
  const subLon = {};
467
515
  const visibleCount = new Array(times.length).fill(0);
516
+ const visibleBySystem = {};
468
517
  const pdop = new Array(times.length).fill(null);
469
518
  const gdop = new Array(times.length).fill(null);
470
519
  const hdop = new Array(times.length).fill(null);
@@ -480,6 +529,8 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
480
529
  const p = series[i];
481
530
  if (p && p.el >= maskRadFor(p.az)) {
482
531
  visibleCount[i]++;
532
+ const sys = prn[0];
533
+ (visibleBySystem[sys] ??= new Array(times.length).fill(0))[i]++;
483
534
  visiblePerEpoch[i].push({ az: p.az, el: p.el });
484
535
  }
485
536
  }
@@ -531,6 +582,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
531
582
  subLat,
532
583
  subLon,
533
584
  visibleCount,
585
+ visibleBySystem,
534
586
  pdop,
535
587
  gdop,
536
588
  hdop,
@@ -542,6 +594,8 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
542
594
  export {
543
595
  keplerPosition,
544
596
  glonassPosition,
597
+ rangeRate,
598
+ dopplerHz,
545
599
  ecefToAzEl,
546
600
  computeDop,
547
601
  selectEphemeris,
@@ -2,12 +2,46 @@ import {
2
2
  computeDop,
3
3
  computeSatPosition,
4
4
  ecefToAzEl
5
- } from "./chunk-647YMBJV.js";
5
+ } from "./chunk-DMNFB7Q6.js";
6
+ import {
7
+ ecefToGeodetic
8
+ } from "./chunk-37QNKGTC.js";
6
9
  import {
7
10
  C_LIGHT,
8
11
  OMEGA_E
9
12
  } from "./chunk-FIEWO4J4.js";
10
13
 
14
+ // src/positioning/klobuchar.ts
15
+ function klobucharDelay(coeffs, latRad, lonRad, azRad, elRad, gpsTowSec) {
16
+ const { alpha, beta } = coeffs;
17
+ if (alpha.length < 4 || beta.length < 4) return 0;
18
+ const E = elRad / Math.PI;
19
+ const phiU = latRad / Math.PI;
20
+ const lambdaU = lonRad / Math.PI;
21
+ const psi = 0.0137 / (E + 0.11) - 0.022;
22
+ let phiI = phiU + psi * Math.cos(azRad);
23
+ if (phiI > 0.416) phiI = 0.416;
24
+ else if (phiI < -0.416) phiI = -0.416;
25
+ const lambdaI = lambdaU + psi * Math.sin(azRad) / Math.cos(phiI * Math.PI);
26
+ const phiM = phiI + 0.064 * Math.cos((lambdaI - 1.617) * Math.PI);
27
+ let t = 43200 * lambdaI + gpsTowSec;
28
+ t %= 86400;
29
+ if (t < 0) t += 86400;
30
+ let amp = 0;
31
+ let per = 0;
32
+ for (let n = 3; n >= 0; n--) {
33
+ amp = amp * phiM + alpha[n];
34
+ per = per * phiM + beta[n];
35
+ }
36
+ if (amp < 0) amp = 0;
37
+ if (per < 72e3) per = 72e3;
38
+ const F = 1 + 16 * Math.pow(0.53 - E, 3);
39
+ const x = 2 * Math.PI * (t - 50400) / per;
40
+ if (Math.abs(x) >= 1.57) return F * 5e-9;
41
+ const x2 = x * x;
42
+ return F * (5e-9 + amp * (1 - x2 / 2 + x2 * x2 / 24));
43
+ }
44
+
11
45
  // src/positioning/index.ts
12
46
  var GM_GPS = 3986005e8;
13
47
  var F_REL = -4442807633e-19;
@@ -34,6 +68,16 @@ function ionoFree(p1, p2, f1, f2) {
34
68
  const g = f1 * f1 / (f1 * f1 - f2 * f2);
35
69
  return g * p1 - (g - 1) * p2;
36
70
  }
71
+ var GPS_EPOCH_MS_SPP = Date.UTC(1980, 0, 6);
72
+ var PRIMARY_FREQ_HZ = {
73
+ G: 157542e4,
74
+ E: 157542e4,
75
+ J: 157542e4,
76
+ S: 157542e4,
77
+ C: 1561098e3,
78
+ R: 1602e6
79
+ };
80
+ var F_L1 = 157542e4;
37
81
  function tropoDelay(elevationRad) {
38
82
  const sinEl = Math.sin(elevationRad);
39
83
  return 2.47 / (sinEl + 0.0121);
@@ -68,8 +112,10 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
68
112
  troposphere = true,
69
113
  maxIterations = 15,
70
114
  convergenceM = 1e-4,
71
- tgd = true
115
+ tgd = true,
116
+ iono
72
117
  } = opts;
118
+ const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
73
119
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
74
120
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
75
121
  if (all.length < minSats(all)) return null;
@@ -93,6 +139,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
93
139
  const Htv = new Array(dim).fill(0);
94
140
  let rows = 0;
95
141
  for (const k of Object.keys(residuals2)) delete residuals2[k];
142
+ const positionSaneIter = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
143
+ const [rxLat, rxLon] = positionSaneIter ? ecefToGeodetic(x2, y2, z2) : [0, 0];
96
144
  for (const prn of prns) {
97
145
  const psr = pseudoranges.get(prn);
98
146
  const eph = ephemerides.get(prn);
@@ -111,17 +159,24 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
111
159
  const uy = (y2 - sy) / rho;
112
160
  const uz = (z2 - sz) / rho;
113
161
  let elev = Math.PI / 2;
162
+ let azim = 0;
114
163
  let weight = 1;
115
- const positionSane = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
164
+ const positionSane = positionSaneIter;
116
165
  if (positionSane) {
117
166
  const azel = ecefToAzEl(x2, y2, z2, sx, sy, sz);
118
167
  elev = azel.el;
168
+ azim = azel.az;
119
169
  if (iter >= 2 && elev < elevationMaskDeg * Math.PI / 180) continue;
120
170
  const sinEl = Math.max(Math.sin(elev), 0.1);
121
171
  weight = sinEl * sinEl;
122
172
  }
123
173
  const tropo = troposphere && positionSane ? tropoDelay(elev) : 0;
124
- const predicted = rho + clk - C_LIGHT * dts + tropo;
174
+ let ionoM = 0;
175
+ if (iono && positionSane) {
176
+ const f = PRIMARY_FREQ_HZ[sys] ?? F_L1;
177
+ ionoM = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow) * (F_L1 / f * (F_L1 / f));
178
+ }
179
+ const predicted = rho + clk - C_LIGHT * dts + tropo + ionoM;
125
180
  const v = psr - predicted;
126
181
  const h = new Array(dim).fill(0);
127
182
  h[0] = ux;
@@ -200,6 +255,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
200
255
  }
201
256
 
202
257
  export {
258
+ klobucharDelay,
203
259
  satClockCorrection,
204
260
  ionoFree,
205
261
  solveSpp
package/dist/index.cjs CHANGED
@@ -128,6 +128,7 @@ __export(src_exports, {
128
128
  deg2dms: () => deg2dms,
129
129
  deg2rad: () => deg2rad,
130
130
  detrendIonoArcs: () => detrendIonoArcs,
131
+ dopplerHz: () => dopplerHz,
131
132
  ecefToAzEl: () => ecefToAzEl,
132
133
  ecefToGeodetic: () => ecefToGeodetic,
133
134
  ephInfoToEphemeris: () => ephInfoToEphemeris,
@@ -200,6 +201,7 @@ __export(src_exports, {
200
201
  horizonDistance: () => horizonDistance,
201
202
  ionoFree: () => ionoFree,
202
203
  keplerPosition: () => keplerPosition,
204
+ klobucharDelay: () => klobucharDelay,
203
205
  maskRadForAzimuth: () => maskRadForAzimuth,
204
206
  msmEpochToDate: () => msmEpochToDate,
205
207
  navTimesFromEph: () => navTimesFromEph,
@@ -219,6 +221,7 @@ __export(src_exports, {
219
221
  phiBOCs: () => phiBOCs,
220
222
  phiBPSK: () => phiBPSK,
221
223
  rad2deg: () => rad2deg,
224
+ rangeRate: () => rangeRate,
222
225
  readString: () => readString,
223
226
  reportDecodeError: () => reportDecodeError,
224
227
  resetGloFreqCache: () => resetGloFreqCache,
@@ -4104,6 +4107,7 @@ function gmForSystem(sys) {
4104
4107
  if (sys === "C") return GM_BDS;
4105
4108
  return GM_GPS;
4106
4109
  }
4110
+ var geoVelocityPass = false;
4107
4111
  function keplerPosition(eph, t) {
4108
4112
  const GM = gmForSystem(eph.system);
4109
4113
  const a = eph.sqrtA * eph.sqrtA;
@@ -4131,8 +4135,16 @@ function keplerPosition(eph, t) {
4131
4135
  const uk = phik + duk;
4132
4136
  const rk = a * (1 - eph.e * cosE) + drk;
4133
4137
  const ik = eph.i0 + dik + eph.idot * tk;
4138
+ const EkDot = n / (1 - eph.e * cosE);
4139
+ const vkDot = EkDot * Math.sqrt(1 - eph.e * eph.e) / (1 - eph.e * cosE);
4140
+ const dukDot = 2 * vkDot * (eph.cus * cos2phi - eph.cuc * sin2phi);
4141
+ const drkDot = a * eph.e * sinE * EkDot + 2 * vkDot * (eph.crs * cos2phi - eph.crc * sin2phi);
4142
+ const dikDot = eph.idot + 2 * vkDot * (eph.cis * cos2phi - eph.cic * sin2phi);
4143
+ const ukDot = vkDot + dukDot;
4134
4144
  const xp = rk * Math.cos(uk);
4135
4145
  const yp = rk * Math.sin(uk);
4146
+ const xpDot = drkDot * Math.cos(uk) - rk * ukDot * Math.sin(uk);
4147
+ const ypDot = drkDot * Math.sin(uk) + rk * ukDot * Math.cos(uk);
4136
4148
  const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < BDS_GEO_MAX_INCLINATION_RAD && a > BDS_GEO_MIN_SEMIMAJOR_AXIS_M;
4137
4149
  if (isBdsGeo) {
4138
4150
  const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
@@ -4148,23 +4160,43 @@ function keplerPosition(eph, t) {
4148
4160
  const sinPhi = Math.sin(phi);
4149
4161
  const COS5 = Math.cos(-5 * Math.PI / 180);
4150
4162
  const SIN5 = Math.sin(-5 * Math.PI / 180);
4163
+ const px = xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5;
4164
+ const py = -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5;
4165
+ const pz = -yg * SIN5 + zg * COS5;
4166
+ if (geoVelocityPass) {
4167
+ return { prn: eph.prn, x: px, y: py, z: pz, vx: 0, vy: 0, vz: 0 };
4168
+ }
4169
+ geoVelocityPass = true;
4170
+ const h = 0.5;
4171
+ const pm = keplerPosition(eph, t - h);
4172
+ const pp = keplerPosition(eph, t + h);
4173
+ geoVelocityPass = false;
4151
4174
  return {
4152
4175
  prn: eph.prn,
4153
- x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
4154
- y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
4155
- z: -yg * SIN5 + zg * COS5
4176
+ x: px,
4177
+ y: py,
4178
+ z: pz,
4179
+ vx: (pp.x - pm.x) / (2 * h),
4180
+ vy: (pp.y - pm.y) / (2 * h),
4181
+ vz: (pp.z - pm.z) / (2 * h)
4156
4182
  };
4157
4183
  }
4158
4184
  const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
4185
+ const omegakDot = eph.omegaDot - OMEGA_E;
4159
4186
  const cosO = Math.cos(omegak);
4160
4187
  const sinO = Math.sin(omegak);
4161
4188
  const cosI = Math.cos(ik);
4162
4189
  const sinI = Math.sin(ik);
4190
+ const x = xp * cosO - yp * cosI * sinO;
4191
+ const y = xp * sinO + yp * cosI * cosO;
4163
4192
  return {
4164
4193
  prn: eph.prn,
4165
- x: xp * cosO - yp * cosI * sinO,
4166
- y: xp * sinO + yp * cosI * cosO,
4167
- z: yp * sinI
4194
+ x,
4195
+ y,
4196
+ z: yp * sinI,
4197
+ vx: xpDot * cosO - ypDot * cosI * sinO + yp * sinI * sinO * dikDot - omegakDot * y,
4198
+ vy: xpDot * sinO + ypDot * cosI * cosO - yp * sinI * cosO * dikDot + omegakDot * x,
4199
+ vz: ypDot * sinI + yp * cosI * dikDot
4168
4200
  };
4169
4201
  }
4170
4202
  function gloDerivatives(state, acc) {
@@ -4218,7 +4250,25 @@ function glonassPosition(eph, tUtc) {
4218
4250
  if (Math.abs(remainder) > 1e-3) {
4219
4251
  state = rk4Step(state, acc, remainder);
4220
4252
  }
4221
- return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
4253
+ return {
4254
+ prn: eph.prn,
4255
+ x: state[0],
4256
+ y: state[1],
4257
+ z: state[2],
4258
+ vx: state[3],
4259
+ vy: state[4],
4260
+ vz: state[5]
4261
+ };
4262
+ }
4263
+ function rangeRate(sat, rx, rxVel = [0, 0, 0]) {
4264
+ const dx = sat.x - rx[0];
4265
+ const dy = sat.y - rx[1];
4266
+ const dz = sat.z - rx[2];
4267
+ const rho = Math.hypot(dx, dy, dz);
4268
+ return ((sat.vx - rxVel[0]) * dx + (sat.vy - rxVel[1]) * dy + (sat.vz - rxVel[2]) * dz) / rho;
4269
+ }
4270
+ function dopplerHz(rangeRateMs, freqHz) {
4271
+ return -rangeRateMs * freqHz / C_LIGHT;
4222
4272
  }
4223
4273
  function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
4224
4274
  const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
@@ -4542,6 +4592,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
4542
4592
  const subLat = {};
4543
4593
  const subLon = {};
4544
4594
  const visibleCount = new Array(times.length).fill(0);
4595
+ const visibleBySystem = {};
4545
4596
  const pdop = new Array(times.length).fill(null);
4546
4597
  const gdop = new Array(times.length).fill(null);
4547
4598
  const hdop = new Array(times.length).fill(null);
@@ -4557,6 +4608,8 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
4557
4608
  const p = series[i];
4558
4609
  if (p && p.el >= maskRadFor(p.az)) {
4559
4610
  visibleCount[i]++;
4611
+ const sys = prn[0];
4612
+ (visibleBySystem[sys] ??= new Array(times.length).fill(0))[i]++;
4560
4613
  visiblePerEpoch[i].push({ az: p.az, el: p.el });
4561
4614
  }
4562
4615
  }
@@ -4608,6 +4661,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
4608
4661
  subLat,
4609
4662
  subLon,
4610
4663
  visibleCount,
4664
+ visibleBySystem,
4611
4665
  pdop,
4612
4666
  gdop,
4613
4667
  hdop,
@@ -4833,6 +4887,37 @@ function dateToEpoch(date) {
4833
4887
  return year + (date.getTime() - start) / (end - start);
4834
4888
  }
4835
4889
 
4890
+ // src/positioning/klobuchar.ts
4891
+ function klobucharDelay(coeffs, latRad, lonRad, azRad, elRad, gpsTowSec) {
4892
+ const { alpha, beta } = coeffs;
4893
+ if (alpha.length < 4 || beta.length < 4) return 0;
4894
+ const E = elRad / Math.PI;
4895
+ const phiU = latRad / Math.PI;
4896
+ const lambdaU = lonRad / Math.PI;
4897
+ const psi = 0.0137 / (E + 0.11) - 0.022;
4898
+ let phiI = phiU + psi * Math.cos(azRad);
4899
+ if (phiI > 0.416) phiI = 0.416;
4900
+ else if (phiI < -0.416) phiI = -0.416;
4901
+ const lambdaI = lambdaU + psi * Math.sin(azRad) / Math.cos(phiI * Math.PI);
4902
+ const phiM = phiI + 0.064 * Math.cos((lambdaI - 1.617) * Math.PI);
4903
+ let t = 43200 * lambdaI + gpsTowSec;
4904
+ t %= 86400;
4905
+ if (t < 0) t += 86400;
4906
+ let amp = 0;
4907
+ let per = 0;
4908
+ for (let n = 3; n >= 0; n--) {
4909
+ amp = amp * phiM + alpha[n];
4910
+ per = per * phiM + beta[n];
4911
+ }
4912
+ if (amp < 0) amp = 0;
4913
+ if (per < 72e3) per = 72e3;
4914
+ const F = 1 + 16 * Math.pow(0.53 - E, 3);
4915
+ const x = 2 * Math.PI * (t - 50400) / per;
4916
+ if (Math.abs(x) >= 1.57) return F * 5e-9;
4917
+ const x2 = x * x;
4918
+ return F * (5e-9 + amp * (1 - x2 / 2 + x2 * x2 / 24));
4919
+ }
4920
+
4836
4921
  // src/positioning/index.ts
4837
4922
  var GM_GPS2 = 3986005e8;
4838
4923
  var F_REL = -4442807633e-19;
@@ -4859,6 +4944,16 @@ function ionoFree(p1, p2, f1, f2) {
4859
4944
  const g = f1 * f1 / (f1 * f1 - f2 * f2);
4860
4945
  return g * p1 - (g - 1) * p2;
4861
4946
  }
4947
+ var GPS_EPOCH_MS_SPP = Date.UTC(1980, 0, 6);
4948
+ var PRIMARY_FREQ_HZ = {
4949
+ G: 157542e4,
4950
+ E: 157542e4,
4951
+ J: 157542e4,
4952
+ S: 157542e4,
4953
+ C: 1561098e3,
4954
+ R: 1602e6
4955
+ };
4956
+ var F_L1 = 157542e4;
4862
4957
  function tropoDelay(elevationRad) {
4863
4958
  const sinEl = Math.sin(elevationRad);
4864
4959
  return 2.47 / (sinEl + 0.0121);
@@ -4893,8 +4988,10 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4893
4988
  troposphere = true,
4894
4989
  maxIterations = 15,
4895
4990
  convergenceM = 1e-4,
4896
- tgd = true
4991
+ tgd = true,
4992
+ iono
4897
4993
  } = opts;
4994
+ const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
4898
4995
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
4899
4996
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
4900
4997
  if (all.length < minSats(all)) return null;
@@ -4918,6 +5015,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4918
5015
  const Htv = new Array(dim).fill(0);
4919
5016
  let rows = 0;
4920
5017
  for (const k of Object.keys(residuals2)) delete residuals2[k];
5018
+ const positionSaneIter = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
5019
+ const [rxLat, rxLon] = positionSaneIter ? ecefToGeodetic(x2, y2, z2) : [0, 0];
4921
5020
  for (const prn of prns) {
4922
5021
  const psr = pseudoranges.get(prn);
4923
5022
  const eph = ephemerides.get(prn);
@@ -4936,17 +5035,24 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4936
5035
  const uy = (y2 - sy) / rho;
4937
5036
  const uz = (z2 - sz) / rho;
4938
5037
  let elev = Math.PI / 2;
5038
+ let azim = 0;
4939
5039
  let weight = 1;
4940
- const positionSane = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
5040
+ const positionSane = positionSaneIter;
4941
5041
  if (positionSane) {
4942
5042
  const azel = ecefToAzEl(x2, y2, z2, sx, sy, sz);
4943
5043
  elev = azel.el;
5044
+ azim = azel.az;
4944
5045
  if (iter >= 2 && elev < elevationMaskDeg * Math.PI / 180) continue;
4945
5046
  const sinEl = Math.max(Math.sin(elev), 0.1);
4946
5047
  weight = sinEl * sinEl;
4947
5048
  }
4948
5049
  const tropo = troposphere && positionSane ? tropoDelay(elev) : 0;
4949
- const predicted = rho + clk - C_LIGHT * dts + tropo;
5050
+ let ionoM = 0;
5051
+ if (iono && positionSane) {
5052
+ const f = PRIMARY_FREQ_HZ[sys] ?? F_L1;
5053
+ ionoM = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow) * (F_L1 / f * (F_L1 / f));
5054
+ }
5055
+ const predicted = rho + clk - C_LIGHT * dts + tropo + ionoM;
4950
5056
  const v = psr - predicted;
4951
5057
  const h = new Array(dim).fill(0);
4952
5058
  h[0] = ux;
@@ -7392,6 +7498,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7392
7498
  deg2dms,
7393
7499
  deg2rad,
7394
7500
  detrendIonoArcs,
7501
+ dopplerHz,
7395
7502
  ecefToAzEl,
7396
7503
  ecefToGeodetic,
7397
7504
  ephInfoToEphemeris,
@@ -7464,6 +7571,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7464
7571
  horizonDistance,
7465
7572
  ionoFree,
7466
7573
  keplerPosition,
7574
+ klobucharDelay,
7467
7575
  maskRadForAzimuth,
7468
7576
  msmEpochToDate,
7469
7577
  navTimesFromEph,
@@ -7483,6 +7591,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7483
7591
  phiBOCs,
7484
7592
  phiBPSK,
7485
7593
  rad2deg,
7594
+ rangeRate,
7486
7595
  readString,
7487
7596
  reportDecodeError,
7488
7597
  resetGloFreqCache,
package/dist/index.d.cts CHANGED
@@ -9,9 +9,9 @@ export { E as EpochSummary, P as ParseOptions, R as RinexHeader, a as RinexResul
9
9
  export { E as Ephemeris, G as GlonassEphemeris, K as KeplerEphemeris, N as NavHeader, a as NavResult, p as parseNavFile } from './nav-BAI1a9vK.cjs';
10
10
  export { B as BitReader, E as EphemerisInfo, R as Rtcm3DebugHandler, a as Rtcm3Decoder, b as Rtcm3Frame, d as decodeEphemeris, r as readString, c as reportDecodeError, s as setRtcm3DebugHandler } from './ephemeris-Ohl6NAfw.cjs';
11
11
  export { MessageTypeStats, MsmEpoch, MsmSatObs, MsmSignal, RTCM3_MESSAGE_NAMES, SatCn0, SignalCn0, StationMeta, StreamStats, createStationMeta, createStreamStats, decodeMsmFull, msmEpochToDate, resetGloFreqCache, rtcm3Constellation, setGloFreqNumber, updateStationMeta, updateStreamStats } from './rtcm3.cjs';
12
- export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPosition, VisibilityPass, VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, selectEphemeris } from './orbit.cjs';
12
+ export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPosition, VisibilityPass, VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, dopplerHz, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, rangeRate, selectEphemeris } from './orbit.cjs';
13
13
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.cjs';
14
- export { SppOptions, SppSolution, ionoFree, satClockCorrection, solveSpp } from './positioning.cjs';
14
+ export { KlobucharCoeffs, SppOptions, SppSolution, ionoFree, klobucharDelay, satClockCorrection, solveSpp } from './positioning.cjs';
15
15
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.cjs';
16
16
  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';
17
17
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.cjs';
package/dist/index.d.ts CHANGED
@@ -9,9 +9,9 @@ export { E as EpochSummary, P as ParseOptions, R as RinexHeader, a as RinexResul
9
9
  export { E as Ephemeris, G as GlonassEphemeris, K as KeplerEphemeris, N as NavHeader, a as NavResult, p as parseNavFile } from './nav-BAI1a9vK.js';
10
10
  export { B as BitReader, E as EphemerisInfo, R as Rtcm3DebugHandler, a as Rtcm3Decoder, b as Rtcm3Frame, d as decodeEphemeris, r as readString, c as reportDecodeError, s as setRtcm3DebugHandler } from './ephemeris-Ohl6NAfw.js';
11
11
  export { MessageTypeStats, MsmEpoch, MsmSatObs, MsmSignal, RTCM3_MESSAGE_NAMES, SatCn0, SignalCn0, StationMeta, StreamStats, createStationMeta, createStreamStats, decodeMsmFull, msmEpochToDate, resetGloFreqCache, rtcm3Constellation, setGloFreqNumber, updateStationMeta, updateStreamStats } from './rtcm3.js';
12
- export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPosition, VisibilityPass, VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, selectEphemeris } from './orbit.js';
12
+ export { AllPositionsData, DopValues, EpochSkyData, SatAzEl, SatPoint, SatPosition, VisibilityPass, VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, dopplerHz, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, rangeRate, selectEphemeris } from './orbit.js';
13
13
  export { Helmert14, REFERENCE_FRAMES, ReferenceFrame, applyHelmert, dateToEpoch, transformFrame } from './frames.js';
14
- export { SppOptions, SppSolution, ionoFree, satClockCorrection, solveSpp } from './positioning.js';
14
+ export { KlobucharCoeffs, SppOptions, SppSolution, ionoFree, klobucharDelay, satClockCorrection, solveSpp } from './positioning.js';
15
15
  export { NtripCaster, NtripConnectionInfo, NtripNetwork, NtripStream, NtripStreamConnection, NtripVersion, Sourcetable, SourcetableEntry, connectToMountpoint, fetchSourcetable, parseSourcetable } from './ntrip.js';
16
16
  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';
17
17
  export { AntennaEntry, AntexFile, FrequencyData, frequencyLabel, parseAntex } from './antex.js';
package/dist/index.js CHANGED
@@ -100,23 +100,26 @@ import {
100
100
  } from "./chunk-4KOXMHUW.js";
101
101
  import {
102
102
  ionoFree,
103
+ klobucharDelay,
103
104
  satClockCorrection,
104
105
  solveSpp
105
- } from "./chunk-ZRBRVNHK.js";
106
+ } from "./chunk-LKFSXVIZ.js";
106
107
  import {
107
108
  computeAllPositions,
108
109
  computeDop,
109
110
  computeLiveSkyPositions,
110
111
  computeSatPosition,
111
112
  computeVisibility,
113
+ dopplerHz,
112
114
  ecefToAzEl,
113
115
  ephInfoToEphemeris,
114
116
  glonassPosition,
115
117
  keplerPosition,
116
118
  maskRadForAzimuth,
117
119
  navTimesFromEph,
120
+ rangeRate,
118
121
  selectEphemeris
119
- } from "./chunk-647YMBJV.js";
122
+ } from "./chunk-DMNFB7Q6.js";
120
123
  import {
121
124
  getBdsTime,
122
125
  getDateFromBdsTime,
@@ -361,6 +364,7 @@ export {
361
364
  deg2dms,
362
365
  deg2rad,
363
366
  detrendIonoArcs,
367
+ dopplerHz,
364
368
  ecefToAzEl,
365
369
  ecefToGeodetic,
366
370
  ephInfoToEphemeris,
@@ -433,6 +437,7 @@ export {
433
437
  horizonDistance,
434
438
  ionoFree,
435
439
  keplerPosition,
440
+ klobucharDelay,
436
441
  maskRadForAzimuth,
437
442
  msmEpochToDate,
438
443
  navTimesFromEph,
@@ -452,6 +457,7 @@ export {
452
457
  phiBOCs,
453
458
  phiBPSK,
454
459
  rad2deg,
460
+ rangeRate,
455
461
  readString,
456
462
  reportDecodeError,
457
463
  resetGloFreqCache,
package/dist/orbit.cjs CHANGED
@@ -25,6 +25,7 @@ __export(orbit_exports, {
25
25
  computeLiveSkyPositions: () => computeLiveSkyPositions,
26
26
  computeSatPosition: () => computeSatPosition,
27
27
  computeVisibility: () => computeVisibility,
28
+ dopplerHz: () => dopplerHz,
28
29
  ecefToAzEl: () => ecefToAzEl,
29
30
  ecefToGeodetic: () => ecefToGeodetic,
30
31
  ephInfoToEphemeris: () => ephInfoToEphemeris,
@@ -33,6 +34,7 @@ __export(orbit_exports, {
33
34
  keplerPosition: () => keplerPosition,
34
35
  maskRadForAzimuth: () => maskRadForAzimuth,
35
36
  navTimesFromEph: () => navTimesFromEph,
37
+ rangeRate: () => rangeRate,
36
38
  selectEphemeris: () => selectEphemeris
37
39
  });
38
40
  module.exports = __toCommonJS(orbit_exports);
@@ -82,6 +84,7 @@ var START_BDS_TIME = /* @__PURE__ */ new Date("2006-01-01T00:00:14Z");
82
84
  var START_NTP_TIME = /* @__PURE__ */ new Date("1900-01-01T00:00:00Z");
83
85
 
84
86
  // src/constants/gnss.ts
87
+ var C_LIGHT = 299792458;
85
88
  var OMEGA_E = 72921151467e-15;
86
89
 
87
90
  // src/time/gnss.ts
@@ -181,6 +184,7 @@ function gmForSystem(sys) {
181
184
  if (sys === "C") return GM_BDS;
182
185
  return GM_GPS;
183
186
  }
187
+ var geoVelocityPass = false;
184
188
  function keplerPosition(eph, t) {
185
189
  const GM = gmForSystem(eph.system);
186
190
  const a = eph.sqrtA * eph.sqrtA;
@@ -208,8 +212,16 @@ function keplerPosition(eph, t) {
208
212
  const uk = phik + duk;
209
213
  const rk = a * (1 - eph.e * cosE) + drk;
210
214
  const ik = eph.i0 + dik + eph.idot * tk;
215
+ const EkDot = n / (1 - eph.e * cosE);
216
+ const vkDot = EkDot * Math.sqrt(1 - eph.e * eph.e) / (1 - eph.e * cosE);
217
+ const dukDot = 2 * vkDot * (eph.cus * cos2phi - eph.cuc * sin2phi);
218
+ const drkDot = a * eph.e * sinE * EkDot + 2 * vkDot * (eph.crs * cos2phi - eph.crc * sin2phi);
219
+ const dikDot = eph.idot + 2 * vkDot * (eph.cis * cos2phi - eph.cic * sin2phi);
220
+ const ukDot = vkDot + dukDot;
211
221
  const xp = rk * Math.cos(uk);
212
222
  const yp = rk * Math.sin(uk);
223
+ const xpDot = drkDot * Math.cos(uk) - rk * ukDot * Math.sin(uk);
224
+ const ypDot = drkDot * Math.sin(uk) + rk * ukDot * Math.cos(uk);
213
225
  const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < BDS_GEO_MAX_INCLINATION_RAD && a > BDS_GEO_MIN_SEMIMAJOR_AXIS_M;
214
226
  if (isBdsGeo) {
215
227
  const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
@@ -225,23 +237,43 @@ function keplerPosition(eph, t) {
225
237
  const sinPhi = Math.sin(phi);
226
238
  const COS5 = Math.cos(-5 * Math.PI / 180);
227
239
  const SIN5 = Math.sin(-5 * Math.PI / 180);
240
+ const px = xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5;
241
+ const py = -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5;
242
+ const pz = -yg * SIN5 + zg * COS5;
243
+ if (geoVelocityPass) {
244
+ return { prn: eph.prn, x: px, y: py, z: pz, vx: 0, vy: 0, vz: 0 };
245
+ }
246
+ geoVelocityPass = true;
247
+ const h = 0.5;
248
+ const pm = keplerPosition(eph, t - h);
249
+ const pp = keplerPosition(eph, t + h);
250
+ geoVelocityPass = false;
228
251
  return {
229
252
  prn: eph.prn,
230
- x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
231
- y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
232
- z: -yg * SIN5 + zg * COS5
253
+ x: px,
254
+ y: py,
255
+ z: pz,
256
+ vx: (pp.x - pm.x) / (2 * h),
257
+ vy: (pp.y - pm.y) / (2 * h),
258
+ vz: (pp.z - pm.z) / (2 * h)
233
259
  };
234
260
  }
235
261
  const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
262
+ const omegakDot = eph.omegaDot - OMEGA_E;
236
263
  const cosO = Math.cos(omegak);
237
264
  const sinO = Math.sin(omegak);
238
265
  const cosI = Math.cos(ik);
239
266
  const sinI = Math.sin(ik);
267
+ const x = xp * cosO - yp * cosI * sinO;
268
+ const y = xp * sinO + yp * cosI * cosO;
240
269
  return {
241
270
  prn: eph.prn,
242
- x: xp * cosO - yp * cosI * sinO,
243
- y: xp * sinO + yp * cosI * cosO,
244
- z: yp * sinI
271
+ x,
272
+ y,
273
+ z: yp * sinI,
274
+ vx: xpDot * cosO - ypDot * cosI * sinO + yp * sinI * sinO * dikDot - omegakDot * y,
275
+ vy: xpDot * sinO + ypDot * cosI * cosO - yp * sinI * cosO * dikDot + omegakDot * x,
276
+ vz: ypDot * sinI + yp * cosI * dikDot
245
277
  };
246
278
  }
247
279
  function gloDerivatives(state, acc) {
@@ -295,7 +327,25 @@ function glonassPosition(eph, tUtc) {
295
327
  if (Math.abs(remainder) > 1e-3) {
296
328
  state = rk4Step(state, acc, remainder);
297
329
  }
298
- return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
330
+ return {
331
+ prn: eph.prn,
332
+ x: state[0],
333
+ y: state[1],
334
+ z: state[2],
335
+ vx: state[3],
336
+ vy: state[4],
337
+ vz: state[5]
338
+ };
339
+ }
340
+ function rangeRate(sat, rx, rxVel = [0, 0, 0]) {
341
+ const dx = sat.x - rx[0];
342
+ const dy = sat.y - rx[1];
343
+ const dz = sat.z - rx[2];
344
+ const rho = Math.hypot(dx, dy, dz);
345
+ return ((sat.vx - rxVel[0]) * dx + (sat.vy - rxVel[1]) * dy + (sat.vz - rxVel[2]) * dz) / rho;
346
+ }
347
+ function dopplerHz(rangeRateMs, freqHz) {
348
+ return -rangeRateMs * freqHz / C_LIGHT;
299
349
  }
300
350
  function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
301
351
  const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
@@ -619,6 +669,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
619
669
  const subLat = {};
620
670
  const subLon = {};
621
671
  const visibleCount = new Array(times.length).fill(0);
672
+ const visibleBySystem = {};
622
673
  const pdop = new Array(times.length).fill(null);
623
674
  const gdop = new Array(times.length).fill(null);
624
675
  const hdop = new Array(times.length).fill(null);
@@ -634,6 +685,8 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
634
685
  const p = series[i];
635
686
  if (p && p.el >= maskRadFor(p.az)) {
636
687
  visibleCount[i]++;
688
+ const sys = prn[0];
689
+ (visibleBySystem[sys] ??= new Array(times.length).fill(0))[i]++;
637
690
  visiblePerEpoch[i].push({ az: p.az, el: p.el });
638
691
  }
639
692
  }
@@ -685,6 +738,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
685
738
  subLat,
686
739
  subLon,
687
740
  visibleCount,
741
+ visibleBySystem,
688
742
  pdop,
689
743
  gdop,
690
744
  hdop,
@@ -699,6 +753,7 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
699
753
  computeLiveSkyPositions,
700
754
  computeSatPosition,
701
755
  computeVisibility,
756
+ dopplerHz,
702
757
  ecefToAzEl,
703
758
  ecefToGeodetic,
704
759
  ephInfoToEphemeris,
@@ -707,5 +762,6 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
707
762
  keplerPosition,
708
763
  maskRadForAzimuth,
709
764
  navTimesFromEph,
765
+ rangeRate,
710
766
  selectEphemeris
711
767
  });
package/dist/orbit.d.cts CHANGED
@@ -12,6 +12,9 @@ interface SatPosition {
12
12
  x: number;
13
13
  y: number;
14
14
  z: number;
15
+ vx: number;
16
+ vy: number;
17
+ vz: number;
15
18
  }
16
19
  interface SatAzEl {
17
20
  prn: string;
@@ -28,7 +31,7 @@ interface DopValues {
28
31
  vdop: number;
29
32
  }
30
33
  /**
31
- * Compute satellite ECEF position from Keplerian ephemeris.
34
+ * Compute satellite ECEF position and velocity from Keplerian ephemeris.
32
35
  * @param eph Broadcast ephemeris
33
36
  * @param t GPS time of week in seconds
34
37
  */
@@ -39,6 +42,13 @@ declare function keplerPosition(eph: KeplerEphemeris, t: number): SatPosition;
39
42
  * @param tUtc Target time as UTC seconds since Unix epoch
40
43
  */
41
44
  declare function glonassPosition(eph: GlonassEphemeris, tUtc: number): SatPosition;
45
+ /**
46
+ * Geometric range rate (m/s, positive = receding) between a satellite
47
+ * with ECEF velocity and a receiver (static by default).
48
+ */
49
+ declare function rangeRate(sat: SatPosition, rx: [number, number, number], rxVel?: [number, number, number]): number;
50
+ /** Predicted carrier Doppler (Hz) from a range rate at frequency f. */
51
+ declare function dopplerHz(rangeRateMs: number, freqHz: number): number;
42
52
  /**
43
53
  * Compute azimuth and elevation from receiver ECEF to satellite ECEF.
44
54
  * @param rxX Receiver X in meters (ECEF)
@@ -141,6 +151,8 @@ interface VisibilityResult {
141
151
  subLon: Record<string, (number | null)[]>;
142
152
  /** Number of satellites at or above the mask, per epoch. */
143
153
  visibleCount: number[];
154
+ /** Per-system visible counts per epoch (system letter → counts). */
155
+ visibleBySystem: Record<string, number[]>;
144
156
  /** PDOP per epoch (null when < 4 satellites above the mask). */
145
157
  pdop: (number | null)[];
146
158
  /** GDOP per epoch (null when < 4 satellites above the mask). */
@@ -174,4 +186,4 @@ declare function maskRadForAzimuth(maskDeg: number | number[]): (azRad: number)
174
186
  */
175
187
  declare function computeVisibility(ephemerides: Ephemeris[], rxPos: [number, number, number], startMs: number, endMs: number, stepSec?: number, elevationMaskDeg?: number | number[]): VisibilityResult;
176
188
 
177
- export { type AllPositionsData, type DopValues, type EpochSkyData, type SatAzEl, type SatPoint, type SatPosition, type VisibilityPass, type VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, selectEphemeris };
189
+ export { type AllPositionsData, type DopValues, type EpochSkyData, type SatAzEl, type SatPoint, type SatPosition, type VisibilityPass, type VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, dopplerHz, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, rangeRate, selectEphemeris };
package/dist/orbit.d.ts CHANGED
@@ -12,6 +12,9 @@ interface SatPosition {
12
12
  x: number;
13
13
  y: number;
14
14
  z: number;
15
+ vx: number;
16
+ vy: number;
17
+ vz: number;
15
18
  }
16
19
  interface SatAzEl {
17
20
  prn: string;
@@ -28,7 +31,7 @@ interface DopValues {
28
31
  vdop: number;
29
32
  }
30
33
  /**
31
- * Compute satellite ECEF position from Keplerian ephemeris.
34
+ * Compute satellite ECEF position and velocity from Keplerian ephemeris.
32
35
  * @param eph Broadcast ephemeris
33
36
  * @param t GPS time of week in seconds
34
37
  */
@@ -39,6 +42,13 @@ declare function keplerPosition(eph: KeplerEphemeris, t: number): SatPosition;
39
42
  * @param tUtc Target time as UTC seconds since Unix epoch
40
43
  */
41
44
  declare function glonassPosition(eph: GlonassEphemeris, tUtc: number): SatPosition;
45
+ /**
46
+ * Geometric range rate (m/s, positive = receding) between a satellite
47
+ * with ECEF velocity and a receiver (static by default).
48
+ */
49
+ declare function rangeRate(sat: SatPosition, rx: [number, number, number], rxVel?: [number, number, number]): number;
50
+ /** Predicted carrier Doppler (Hz) from a range rate at frequency f. */
51
+ declare function dopplerHz(rangeRateMs: number, freqHz: number): number;
42
52
  /**
43
53
  * Compute azimuth and elevation from receiver ECEF to satellite ECEF.
44
54
  * @param rxX Receiver X in meters (ECEF)
@@ -141,6 +151,8 @@ interface VisibilityResult {
141
151
  subLon: Record<string, (number | null)[]>;
142
152
  /** Number of satellites at or above the mask, per epoch. */
143
153
  visibleCount: number[];
154
+ /** Per-system visible counts per epoch (system letter → counts). */
155
+ visibleBySystem: Record<string, number[]>;
144
156
  /** PDOP per epoch (null when < 4 satellites above the mask). */
145
157
  pdop: (number | null)[];
146
158
  /** GDOP per epoch (null when < 4 satellites above the mask). */
@@ -174,4 +186,4 @@ declare function maskRadForAzimuth(maskDeg: number | number[]): (azRad: number)
174
186
  */
175
187
  declare function computeVisibility(ephemerides: Ephemeris[], rxPos: [number, number, number], startMs: number, endMs: number, stepSec?: number, elevationMaskDeg?: number | number[]): VisibilityResult;
176
188
 
177
- export { type AllPositionsData, type DopValues, type EpochSkyData, type SatAzEl, type SatPoint, type SatPosition, type VisibilityPass, type VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, selectEphemeris };
189
+ export { type AllPositionsData, type DopValues, type EpochSkyData, type SatAzEl, type SatPoint, type SatPosition, type VisibilityPass, type VisibilityResult, computeAllPositions, computeDop, computeLiveSkyPositions, computeSatPosition, computeVisibility, dopplerHz, ecefToAzEl, ephInfoToEphemeris, glonassPosition, keplerPosition, maskRadForAzimuth, navTimesFromEph, rangeRate, selectEphemeris };
package/dist/orbit.js CHANGED
@@ -4,14 +4,16 @@ import {
4
4
  computeLiveSkyPositions,
5
5
  computeSatPosition,
6
6
  computeVisibility,
7
+ dopplerHz,
7
8
  ecefToAzEl,
8
9
  ephInfoToEphemeris,
9
10
  glonassPosition,
10
11
  keplerPosition,
11
12
  maskRadForAzimuth,
12
13
  navTimesFromEph,
14
+ rangeRate,
13
15
  selectEphemeris
14
- } from "./chunk-647YMBJV.js";
16
+ } from "./chunk-DMNFB7Q6.js";
15
17
  import "./chunk-HVXYFUCB.js";
16
18
  import {
17
19
  ecefToGeodetic,
@@ -26,6 +28,7 @@ export {
26
28
  computeLiveSkyPositions,
27
29
  computeSatPosition,
28
30
  computeVisibility,
31
+ dopplerHz,
29
32
  ecefToAzEl,
30
33
  ecefToGeodetic,
31
34
  ephInfoToEphemeris,
@@ -34,5 +37,6 @@ export {
34
37
  keplerPosition,
35
38
  maskRadForAzimuth,
36
39
  navTimesFromEph,
40
+ rangeRate,
37
41
  selectEphemeris
38
42
  };
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var positioning_exports = {};
22
22
  __export(positioning_exports, {
23
23
  ionoFree: () => ionoFree,
24
+ klobucharDelay: () => klobucharDelay,
24
25
  satClockCorrection: () => satClockCorrection,
25
26
  solveSpp: () => solveSpp
26
27
  });
@@ -164,6 +165,7 @@ function gmForSystem(sys) {
164
165
  if (sys === "C") return GM_BDS;
165
166
  return GM_GPS;
166
167
  }
168
+ var geoVelocityPass = false;
167
169
  function keplerPosition(eph, t) {
168
170
  const GM = gmForSystem(eph.system);
169
171
  const a = eph.sqrtA * eph.sqrtA;
@@ -191,8 +193,16 @@ function keplerPosition(eph, t) {
191
193
  const uk = phik + duk;
192
194
  const rk = a * (1 - eph.e * cosE) + drk;
193
195
  const ik = eph.i0 + dik + eph.idot * tk;
196
+ const EkDot = n / (1 - eph.e * cosE);
197
+ const vkDot = EkDot * Math.sqrt(1 - eph.e * eph.e) / (1 - eph.e * cosE);
198
+ const dukDot = 2 * vkDot * (eph.cus * cos2phi - eph.cuc * sin2phi);
199
+ const drkDot = a * eph.e * sinE * EkDot + 2 * vkDot * (eph.crs * cos2phi - eph.crc * sin2phi);
200
+ const dikDot = eph.idot + 2 * vkDot * (eph.cis * cos2phi - eph.cic * sin2phi);
201
+ const ukDot = vkDot + dukDot;
194
202
  const xp = rk * Math.cos(uk);
195
203
  const yp = rk * Math.sin(uk);
204
+ const xpDot = drkDot * Math.cos(uk) - rk * ukDot * Math.sin(uk);
205
+ const ypDot = drkDot * Math.sin(uk) + rk * ukDot * Math.cos(uk);
196
206
  const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < BDS_GEO_MAX_INCLINATION_RAD && a > BDS_GEO_MIN_SEMIMAJOR_AXIS_M;
197
207
  if (isBdsGeo) {
198
208
  const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
@@ -208,23 +218,43 @@ function keplerPosition(eph, t) {
208
218
  const sinPhi = Math.sin(phi);
209
219
  const COS5 = Math.cos(-5 * Math.PI / 180);
210
220
  const SIN5 = Math.sin(-5 * Math.PI / 180);
221
+ const px = xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5;
222
+ const py = -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5;
223
+ const pz = -yg * SIN5 + zg * COS5;
224
+ if (geoVelocityPass) {
225
+ return { prn: eph.prn, x: px, y: py, z: pz, vx: 0, vy: 0, vz: 0 };
226
+ }
227
+ geoVelocityPass = true;
228
+ const h = 0.5;
229
+ const pm = keplerPosition(eph, t - h);
230
+ const pp = keplerPosition(eph, t + h);
231
+ geoVelocityPass = false;
211
232
  return {
212
233
  prn: eph.prn,
213
- x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
214
- y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
215
- z: -yg * SIN5 + zg * COS5
234
+ x: px,
235
+ y: py,
236
+ z: pz,
237
+ vx: (pp.x - pm.x) / (2 * h),
238
+ vy: (pp.y - pm.y) / (2 * h),
239
+ vz: (pp.z - pm.z) / (2 * h)
216
240
  };
217
241
  }
218
242
  const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
243
+ const omegakDot = eph.omegaDot - OMEGA_E;
219
244
  const cosO = Math.cos(omegak);
220
245
  const sinO = Math.sin(omegak);
221
246
  const cosI = Math.cos(ik);
222
247
  const sinI = Math.sin(ik);
248
+ const x = xp * cosO - yp * cosI * sinO;
249
+ const y = xp * sinO + yp * cosI * cosO;
223
250
  return {
224
251
  prn: eph.prn,
225
- x: xp * cosO - yp * cosI * sinO,
226
- y: xp * sinO + yp * cosI * cosO,
227
- z: yp * sinI
252
+ x,
253
+ y,
254
+ z: yp * sinI,
255
+ vx: xpDot * cosO - ypDot * cosI * sinO + yp * sinI * sinO * dikDot - omegakDot * y,
256
+ vy: xpDot * sinO + ypDot * cosI * cosO - yp * sinI * cosO * dikDot + omegakDot * x,
257
+ vz: ypDot * sinI + yp * cosI * dikDot
228
258
  };
229
259
  }
230
260
  function gloDerivatives(state, acc) {
@@ -278,7 +308,15 @@ function glonassPosition(eph, tUtc) {
278
308
  if (Math.abs(remainder) > 1e-3) {
279
309
  state = rk4Step(state, acc, remainder);
280
310
  }
281
- return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
311
+ return {
312
+ prn: eph.prn,
313
+ x: state[0],
314
+ y: state[1],
315
+ z: state[2],
316
+ vx: state[3],
317
+ vy: state[4],
318
+ vz: state[5]
319
+ };
282
320
  }
283
321
  function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
284
322
  const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
@@ -371,6 +409,37 @@ var MS_PER_WEEK = 7 * 864e5;
371
409
  var BDS_EPOCH_MS = START_BDS_TIME.getTime();
372
410
  var GAL_EPOCH_MS = GPS_EPOCH_MS + 1024 * MS_PER_WEEK;
373
411
 
412
+ // src/positioning/klobuchar.ts
413
+ function klobucharDelay(coeffs, latRad, lonRad, azRad, elRad, gpsTowSec) {
414
+ const { alpha, beta } = coeffs;
415
+ if (alpha.length < 4 || beta.length < 4) return 0;
416
+ const E = elRad / Math.PI;
417
+ const phiU = latRad / Math.PI;
418
+ const lambdaU = lonRad / Math.PI;
419
+ const psi = 0.0137 / (E + 0.11) - 0.022;
420
+ let phiI = phiU + psi * Math.cos(azRad);
421
+ if (phiI > 0.416) phiI = 0.416;
422
+ else if (phiI < -0.416) phiI = -0.416;
423
+ const lambdaI = lambdaU + psi * Math.sin(azRad) / Math.cos(phiI * Math.PI);
424
+ const phiM = phiI + 0.064 * Math.cos((lambdaI - 1.617) * Math.PI);
425
+ let t = 43200 * lambdaI + gpsTowSec;
426
+ t %= 86400;
427
+ if (t < 0) t += 86400;
428
+ let amp = 0;
429
+ let per = 0;
430
+ for (let n = 3; n >= 0; n--) {
431
+ amp = amp * phiM + alpha[n];
432
+ per = per * phiM + beta[n];
433
+ }
434
+ if (amp < 0) amp = 0;
435
+ if (per < 72e3) per = 72e3;
436
+ const F = 1 + 16 * Math.pow(0.53 - E, 3);
437
+ const x = 2 * Math.PI * (t - 50400) / per;
438
+ if (Math.abs(x) >= 1.57) return F * 5e-9;
439
+ const x2 = x * x;
440
+ return F * (5e-9 + amp * (1 - x2 / 2 + x2 * x2 / 24));
441
+ }
442
+
374
443
  // src/positioning/index.ts
375
444
  var GM_GPS2 = 3986005e8;
376
445
  var F_REL = -4442807633e-19;
@@ -397,6 +466,16 @@ function ionoFree(p1, p2, f1, f2) {
397
466
  const g = f1 * f1 / (f1 * f1 - f2 * f2);
398
467
  return g * p1 - (g - 1) * p2;
399
468
  }
469
+ var GPS_EPOCH_MS_SPP = Date.UTC(1980, 0, 6);
470
+ var PRIMARY_FREQ_HZ = {
471
+ G: 157542e4,
472
+ E: 157542e4,
473
+ J: 157542e4,
474
+ S: 157542e4,
475
+ C: 1561098e3,
476
+ R: 1602e6
477
+ };
478
+ var F_L1 = 157542e4;
400
479
  function tropoDelay(elevationRad) {
401
480
  const sinEl = Math.sin(elevationRad);
402
481
  return 2.47 / (sinEl + 0.0121);
@@ -431,8 +510,10 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
431
510
  troposphere = true,
432
511
  maxIterations = 15,
433
512
  convergenceM = 1e-4,
434
- tgd = true
513
+ tgd = true,
514
+ iono
435
515
  } = opts;
516
+ const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
436
517
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
437
518
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
438
519
  if (all.length < minSats(all)) return null;
@@ -456,6 +537,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
456
537
  const Htv = new Array(dim).fill(0);
457
538
  let rows = 0;
458
539
  for (const k of Object.keys(residuals2)) delete residuals2[k];
540
+ const positionSaneIter = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
541
+ const [rxLat, rxLon] = positionSaneIter ? ecefToGeodetic(x2, y2, z2) : [0, 0];
459
542
  for (const prn of prns) {
460
543
  const psr = pseudoranges.get(prn);
461
544
  const eph = ephemerides.get(prn);
@@ -474,17 +557,24 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
474
557
  const uy = (y2 - sy) / rho;
475
558
  const uz = (z2 - sz) / rho;
476
559
  let elev = Math.PI / 2;
560
+ let azim = 0;
477
561
  let weight = 1;
478
- const positionSane = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
562
+ const positionSane = positionSaneIter;
479
563
  if (positionSane) {
480
564
  const azel = ecefToAzEl(x2, y2, z2, sx, sy, sz);
481
565
  elev = azel.el;
566
+ azim = azel.az;
482
567
  if (iter >= 2 && elev < elevationMaskDeg * Math.PI / 180) continue;
483
568
  const sinEl = Math.max(Math.sin(elev), 0.1);
484
569
  weight = sinEl * sinEl;
485
570
  }
486
571
  const tropo = troposphere && positionSane ? tropoDelay(elev) : 0;
487
- const predicted = rho + clk - C_LIGHT * dts + tropo;
572
+ let ionoM = 0;
573
+ if (iono && positionSane) {
574
+ const f = PRIMARY_FREQ_HZ[sys] ?? F_L1;
575
+ ionoM = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow) * (F_L1 / f * (F_L1 / f));
576
+ }
577
+ const predicted = rho + clk - C_LIGHT * dts + tropo + ionoM;
488
578
  const v = psr - predicted;
489
579
  const h = new Array(dim).fill(0);
490
580
  h[0] = ux;
@@ -564,6 +654,7 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
564
654
  // Annotate the CommonJS export names for ESM import in node:
565
655
  0 && (module.exports = {
566
656
  ionoFree,
657
+ klobucharDelay,
567
658
  satClockCorrection,
568
659
  solveSpp
569
660
  });
@@ -3,6 +3,36 @@ import { DopValues } from './orbit.cjs';
3
3
  import './ephemeris-Ohl6NAfw.cjs';
4
4
  import './ecef-CF0uAysr.cjs';
5
5
 
6
+ /**
7
+ * Klobuchar broadcast ionospheric model (IS-GPS-200, 20.3.3.5.2.5).
8
+ *
9
+ * Evaluates the single-frequency vertical→slant ionospheric delay from
10
+ * the eight coefficients broadcast in the GPS navigation message (and
11
+ * carried in RINEX nav headers as GPSA/GPSB, or ION ALPHA/BETA in
12
+ * RINEX 2). The model captures ~50% RMS of the actual delay — crude,
13
+ * but exactly what a single-frequency receiver applies, which makes it
14
+ * both a useful SPP correction and an honest "what broadcast users
15
+ * get" reference against measured or GIM TEC.
16
+ */
17
+ /** The eight Klobuchar coefficients from the navigation message. */
18
+ interface KlobucharCoeffs {
19
+ /** GPSA: alpha0..alpha3 (s, s/sc, s/sc², s/sc³). */
20
+ alpha: number[];
21
+ /** GPSB: beta0..beta3 (s, s/sc, s/sc², s/sc³). */
22
+ beta: number[];
23
+ }
24
+ /**
25
+ * Slant ionospheric group delay on GPS L1, in seconds.
26
+ *
27
+ * @param coeffs Broadcast alpha/beta coefficients
28
+ * @param latRad Receiver geodetic latitude (radians)
29
+ * @param lonRad Receiver geodetic longitude (radians)
30
+ * @param azRad Satellite azimuth (radians)
31
+ * @param elRad Satellite elevation (radians)
32
+ * @param gpsTowSec GPS time of week (seconds)
33
+ */
34
+ declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad: number, azRad: number, elRad: number, gpsTowSec: number): number;
35
+
6
36
  /**
7
37
  * Single-point positioning (SPP) from pseudoranges and broadcast
8
38
  * ephemerides.
@@ -11,9 +41,11 @@ import './ecef-CF0uAysr.cjs';
11
41
  * constellation (absorbing inter-system time offsets), satellite clock
12
42
  * polynomial + relativistic correction, broadcast group delay
13
43
  * (TGD/BGD), Sagnac (Earth-rotation) correction, elevation
14
- * masking/weighting, and a simple tropospheric model. Ionospheric delay is NOT modelled — use the iono-free
15
- * combination (ionoFree helper) with dual-frequency pseudoranges for
16
- * metre-level results, or expect ~2–10 m of iono bias on L1-only.
44
+ * masking/weighting, and a simple tropospheric model. Ionospheric
45
+ * delay can be modelled from the broadcast Klobuchar coefficients
46
+ * (`iono` option) for single-frequency measurements; for metre-level
47
+ * results prefer the iono-free combination (ionoFree helper) with
48
+ * dual-frequency pseudoranges.
17
49
  */
18
50
 
19
51
  interface SppOptions {
@@ -25,6 +57,14 @@ interface SppOptions {
25
57
  maxIterations?: number;
26
58
  /** Convergence threshold on the position update (m). Default 1e-4. */
27
59
  convergenceM?: number;
60
+ /**
61
+ * Broadcast Klobuchar coefficients (RINEX nav header GPSA/GPSB —
62
+ * `NavResult.header.ionoCorrections`). When given, the modelled
63
+ * slant delay is removed from every single-frequency pseudorange,
64
+ * scaled to each system's primary frequency. Omit when feeding
65
+ * iono-free combinations.
66
+ */
67
+ iono?: KlobucharCoeffs;
28
68
  /**
29
69
  * Apply the broadcast group-delay correction (GPS TGD, Galileo
30
70
  * BGD E5a/E1, BeiDou TGD1) to the satellite clock. Correct for
@@ -73,4 +113,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
73
113
  */
74
114
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
75
115
 
76
- export { type SppOptions, type SppSolution, ionoFree, satClockCorrection, solveSpp };
116
+ export { type KlobucharCoeffs, type SppOptions, type SppSolution, ionoFree, klobucharDelay, satClockCorrection, solveSpp };
@@ -3,6 +3,36 @@ import { DopValues } from './orbit.js';
3
3
  import './ephemeris-Ohl6NAfw.js';
4
4
  import './ecef-CF0uAysr.js';
5
5
 
6
+ /**
7
+ * Klobuchar broadcast ionospheric model (IS-GPS-200, 20.3.3.5.2.5).
8
+ *
9
+ * Evaluates the single-frequency vertical→slant ionospheric delay from
10
+ * the eight coefficients broadcast in the GPS navigation message (and
11
+ * carried in RINEX nav headers as GPSA/GPSB, or ION ALPHA/BETA in
12
+ * RINEX 2). The model captures ~50% RMS of the actual delay — crude,
13
+ * but exactly what a single-frequency receiver applies, which makes it
14
+ * both a useful SPP correction and an honest "what broadcast users
15
+ * get" reference against measured or GIM TEC.
16
+ */
17
+ /** The eight Klobuchar coefficients from the navigation message. */
18
+ interface KlobucharCoeffs {
19
+ /** GPSA: alpha0..alpha3 (s, s/sc, s/sc², s/sc³). */
20
+ alpha: number[];
21
+ /** GPSB: beta0..beta3 (s, s/sc, s/sc², s/sc³). */
22
+ beta: number[];
23
+ }
24
+ /**
25
+ * Slant ionospheric group delay on GPS L1, in seconds.
26
+ *
27
+ * @param coeffs Broadcast alpha/beta coefficients
28
+ * @param latRad Receiver geodetic latitude (radians)
29
+ * @param lonRad Receiver geodetic longitude (radians)
30
+ * @param azRad Satellite azimuth (radians)
31
+ * @param elRad Satellite elevation (radians)
32
+ * @param gpsTowSec GPS time of week (seconds)
33
+ */
34
+ declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad: number, azRad: number, elRad: number, gpsTowSec: number): number;
35
+
6
36
  /**
7
37
  * Single-point positioning (SPP) from pseudoranges and broadcast
8
38
  * ephemerides.
@@ -11,9 +41,11 @@ import './ecef-CF0uAysr.js';
11
41
  * constellation (absorbing inter-system time offsets), satellite clock
12
42
  * polynomial + relativistic correction, broadcast group delay
13
43
  * (TGD/BGD), Sagnac (Earth-rotation) correction, elevation
14
- * masking/weighting, and a simple tropospheric model. Ionospheric delay is NOT modelled — use the iono-free
15
- * combination (ionoFree helper) with dual-frequency pseudoranges for
16
- * metre-level results, or expect ~2–10 m of iono bias on L1-only.
44
+ * masking/weighting, and a simple tropospheric model. Ionospheric
45
+ * delay can be modelled from the broadcast Klobuchar coefficients
46
+ * (`iono` option) for single-frequency measurements; for metre-level
47
+ * results prefer the iono-free combination (ionoFree helper) with
48
+ * dual-frequency pseudoranges.
17
49
  */
18
50
 
19
51
  interface SppOptions {
@@ -25,6 +57,14 @@ interface SppOptions {
25
57
  maxIterations?: number;
26
58
  /** Convergence threshold on the position update (m). Default 1e-4. */
27
59
  convergenceM?: number;
60
+ /**
61
+ * Broadcast Klobuchar coefficients (RINEX nav header GPSA/GPSB —
62
+ * `NavResult.header.ionoCorrections`). When given, the modelled
63
+ * slant delay is removed from every single-frequency pseudorange,
64
+ * scaled to each system's primary frequency. Omit when feeding
65
+ * iono-free combinations.
66
+ */
67
+ iono?: KlobucharCoeffs;
28
68
  /**
29
69
  * Apply the broadcast group-delay correction (GPS TGD, Galileo
30
70
  * BGD E5a/E1, BeiDou TGD1) to the satellite clock. Correct for
@@ -73,4 +113,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
73
113
  */
74
114
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
75
115
 
76
- export { type SppOptions, type SppSolution, ionoFree, satClockCorrection, solveSpp };
116
+ export { type KlobucharCoeffs, type SppOptions, type SppSolution, ionoFree, klobucharDelay, satClockCorrection, solveSpp };
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  ionoFree,
3
+ klobucharDelay,
3
4
  satClockCorrection,
4
5
  solveSpp
5
- } from "./chunk-ZRBRVNHK.js";
6
- import "./chunk-647YMBJV.js";
6
+ } from "./chunk-LKFSXVIZ.js";
7
+ import "./chunk-DMNFB7Q6.js";
7
8
  import "./chunk-HVXYFUCB.js";
8
9
  import "./chunk-37QNKGTC.js";
9
10
  import "./chunk-6FAL6P4G.js";
@@ -11,6 +12,7 @@ import "./chunk-LEEU5OIO.js";
11
12
  import "./chunk-FIEWO4J4.js";
12
13
  export {
13
14
  ionoFree,
15
+ klobucharDelay,
14
16
  satClockCorrection,
15
17
  solveSpp
16
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnss-js",
3
- "version": "1.9.0",
3
+ "version": "1.11.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,