ootk 7.0.3 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -21,6 +21,8 @@ var ValidationError = class extends OotkError {
21
21
  this.value = value;
22
22
  this.name = "ValidationError";
23
23
  }
24
+ field;
25
+ value;
24
26
  };
25
27
  var ParseError = class extends OotkError {
26
28
  /**
@@ -35,6 +37,8 @@ var ParseError = class extends OotkError {
35
37
  this.line = line;
36
38
  this.name = "ParseError";
37
39
  }
40
+ format;
41
+ line;
38
42
  };
39
43
  var PropagationError = class extends OotkError {
40
44
  /**
@@ -47,6 +51,7 @@ var PropagationError = class extends OotkError {
47
51
  this.epoch = epoch;
48
52
  this.name = "PropagationError";
49
53
  }
54
+ epoch;
50
55
  };
51
56
  var OrbitDeterminationError = class extends OotkError {
52
57
  /**
@@ -59,6 +64,7 @@ var OrbitDeterminationError = class extends OotkError {
59
64
  this.algorithm = algorithm;
60
65
  this.name = "OrbitDeterminationError";
61
66
  }
67
+ algorithm;
62
68
  };
63
69
 
64
70
  // src/enums/AngularDiameterMethod.ts
@@ -153,6 +159,7 @@ var SensorType = /* @__PURE__ */ ((SensorType2) => {
153
159
  SensorType2["LASER_RANGING"] = "LASER_RANGING";
154
160
  SensorType2["PASSIVE_RF"] = "PASSIVE_RF";
155
161
  SensorType2["BISTATIC_RADIO_TELESCOPE"] = "BISTATIC_RADIO_TELESCOPE";
162
+ SensorType2["TT_C_ANTENNA"] = "TT_C_ANTENNA";
156
163
  return SensorType2;
157
164
  })(SensorType || {});
158
165
 
@@ -223,6 +230,7 @@ var SpaceObjectType = /* @__PURE__ */ ((SpaceObjectType2) => {
223
230
  SpaceObjectType2[SpaceObjectType2["DWARF_PLANET"] = 36] = "DWARF_PLANET";
224
231
  SpaceObjectType2[SpaceObjectType2["MOON"] = 37] = "MOON";
225
232
  SpaceObjectType2[SpaceObjectType2["DYNAMIC_GROUND_OBJECT"] = 38] = "DYNAMIC_GROUND_OBJECT";
233
+ SpaceObjectType2[SpaceObjectType2["ASTEROID"] = 39] = "ASTEROID";
226
234
  SpaceObjectType2[SpaceObjectType2["MAX_SPACE_OBJECT_TYPE"] = 40] = "MAX_SPACE_OBJECT_TYPE";
227
235
  return SpaceObjectType2;
228
236
  })(SpaceObjectType || {});
@@ -276,6 +284,7 @@ var Epoch = class {
276
284
  throw new ValidationError("Epoch posix time must be a valid number", "posix", posix);
277
285
  }
278
286
  }
287
+ posix;
279
288
  toString() {
280
289
  return this.toDateTime().toISOString();
281
290
  }
@@ -438,7 +447,8 @@ function matchHalfPlane(angle, match) {
438
447
  return Math.abs(d1) < Math.abs(d2) ? a1 : a2;
439
448
  }
440
449
  function wrapAngle(theta) {
441
- const result = (theta + Math.PI) % (2 * Math.PI) - Math.PI;
450
+ const tau = 2 * Math.PI;
451
+ const result = ((theta + Math.PI) % tau + tau) % tau - Math.PI;
442
452
  if (result === -Math.PI) {
443
453
  return Math.PI;
444
454
  }
@@ -447,7 +457,7 @@ function wrapAngle(theta) {
447
457
  function angularDistanceCosine_(lam1, phi1, lam2, phi2) {
448
458
  const a = Math.sin(phi1) * Math.sin(phi2);
449
459
  const b = Math.cos(phi1) * Math.cos(phi2) * Math.cos(lam2 - lam1);
450
- return Math.acos(a + b);
460
+ return Math.acos(Math.min(1, Math.max(-1, a + b)));
451
461
  }
452
462
  function angularDistanceHaversine_(lam1, phi1, lam2, phi2) {
453
463
  const dlam = lam2 - lam1;
@@ -632,6 +642,7 @@ var spaceObjTypeStrMap_ = {
632
642
  [36 /* DWARF_PLANET */]: "Dwarf Planet",
633
643
  [37 /* MOON */]: "Moon",
634
644
  [38 /* DYNAMIC_GROUND_OBJECT */]: "Dynamic Ground Object",
645
+ [39 /* ASTEROID */]: "Asteroid",
635
646
  [40 /* MAX_SPACE_OBJECT_TYPE */]: "Max Space Object Type"
636
647
  };
637
648
  var spaceObjType2Str = (spaceObjType) => spaceObjTypeStrMap_[spaceObjType] || "Unknown";
@@ -1813,6 +1824,8 @@ var EpochGPS = class _EpochGPS {
1813
1824
  throw new ValidationError("GPS seconds must be between 0 and 604799", "seconds", seconds);
1814
1825
  }
1815
1826
  }
1827
+ week;
1828
+ seconds;
1816
1829
  /** Cached GPS reference epoch (1980-01-06T00:00:00.000Z) */
1817
1830
  static reference_ = null;
1818
1831
  /**
@@ -1948,6 +1961,7 @@ var Vector = class _Vector {
1948
1961
  this.elements = elements;
1949
1962
  this.length = elements.length;
1950
1963
  }
1964
+ elements;
1951
1965
  /**
1952
1966
  * The length of the vector.
1953
1967
  */
@@ -2711,6 +2725,9 @@ var Vector3D = class _Vector3D {
2711
2725
  this.y = y;
2712
2726
  this.z = z;
2713
2727
  }
2728
+ x;
2729
+ y;
2730
+ z;
2714
2731
  /**
2715
2732
  * Create a new Vector3D object from the first three elements of a Vector
2716
2733
  * object.
@@ -3218,6 +3235,131 @@ var Sgp4Error = class _Sgp4Error extends Error {
3218
3235
  }
3219
3236
  };
3220
3237
 
3238
+ // src/sgp4/sgp4-wasm-backend.ts
3239
+ var WASM_KEY_FAILED = -1n;
3240
+ var defineHidden_ = (obj, key, value) => {
3241
+ Object.defineProperty(obj, key, {
3242
+ value,
3243
+ writable: true,
3244
+ configurable: true,
3245
+ enumerable: false
3246
+ });
3247
+ };
3248
+ var backend = null;
3249
+ var satKeyCache = /* @__PURE__ */ new Map();
3250
+ var syntheticSatnumCounter = 0;
3251
+ var wasmAttachedCount = 0;
3252
+ var wasmFallbackCount = 0;
3253
+ var SYNTHETIC_SATNUM_LETTERS = "VWXYZ";
3254
+ var synthesizeSatnum_ = () => {
3255
+ const n = syntheticSatnumCounter++;
3256
+ const letterIdx = Math.floor(n / 1e4);
3257
+ if (letterIdx >= SYNTHETIC_SATNUM_LETTERS.length) {
3258
+ return null;
3259
+ }
3260
+ return `${SYNTHETIC_SATNUM_LETTERS[letterIdx]}${(n % 1e4).toString().padStart(4, "0")}`;
3261
+ };
3262
+ var tryAddWithSyntheticSatnum_ = (wasm, line1, line2) => {
3263
+ const satnumField = line1.substring(2, 7);
3264
+ const classification = line1.charAt(7);
3265
+ const isSatnumMeaningless = satnumField.trim() === "" || satnumField === "00000";
3266
+ const isNonStandardClassification = classification !== "U" && classification !== "C" && classification !== "S";
3267
+ if (!isSatnumMeaningless && !isNonStandardClassification) {
3268
+ return null;
3269
+ }
3270
+ const satnum = synthesizeSatnum_();
3271
+ if (satnum === null) {
3272
+ return null;
3273
+ }
3274
+ const syntheticLine1 = `${line1.slice(0, 2)}${satnum}U${line1.slice(8)}`;
3275
+ const syntheticLine2 = `${line2.slice(0, 2)}${satnum}${line2.slice(7)}`;
3276
+ try {
3277
+ const satKey = wasm.addSat(syntheticLine1, syntheticLine2);
3278
+ wasm.initSats([satKey]);
3279
+ return satKey;
3280
+ } catch {
3281
+ return null;
3282
+ }
3283
+ };
3284
+ var setSgp4WasmBackend = (wasm) => {
3285
+ if (!wasm.isLoaded) {
3286
+ throw new Error("Sgp4 wasm backend must be loaded before use. Call load() first.");
3287
+ }
3288
+ backend = wasm;
3289
+ };
3290
+ var clearSgp4WasmBackend = () => {
3291
+ backend = null;
3292
+ satKeyCache.clear();
3293
+ syntheticSatnumCounter = 0;
3294
+ wasmAttachedCount = 0;
3295
+ wasmFallbackCount = 0;
3296
+ };
3297
+ var getSgp4WasmBackendStats = () => ({
3298
+ attached: wasmAttachedCount,
3299
+ fallback: wasmFallbackCount
3300
+ });
3301
+ var isSgp4WasmBackendActive = () => backend !== null;
3302
+ var stashTleLines = (satrec, line1, line2) => {
3303
+ defineHidden_(satrec, "wasmTleLine1", line1);
3304
+ defineHidden_(satrec, "wasmTleLine2", line2);
3305
+ };
3306
+ var ensureSatKey_ = (wasm, satrec) => {
3307
+ const existing = satrec.wasmSatKey;
3308
+ if (existing !== void 0) {
3309
+ return existing > 0n ? existing : null;
3310
+ }
3311
+ const line1 = satrec.wasmTleLine1;
3312
+ const line2 = satrec.wasmTleLine2;
3313
+ if (!line1 || !line2) {
3314
+ defineHidden_(satrec, "wasmSatKey", WASM_KEY_FAILED);
3315
+ wasmFallbackCount++;
3316
+ return null;
3317
+ }
3318
+ const cacheKey = `${line1}
3319
+ ${line2}`;
3320
+ const cached = satKeyCache.get(cacheKey);
3321
+ if (cached !== void 0) {
3322
+ defineHidden_(satrec, "wasmSatKey", cached);
3323
+ return cached;
3324
+ }
3325
+ try {
3326
+ const satKey = wasm.addSat(line1, line2);
3327
+ wasm.initSats([satKey]);
3328
+ satKeyCache.set(cacheKey, satKey);
3329
+ defineHidden_(satrec, "wasmSatKey", satKey);
3330
+ wasmAttachedCount++;
3331
+ return satKey;
3332
+ } catch {
3333
+ const syntheticKey = tryAddWithSyntheticSatnum_(wasm, line1, line2);
3334
+ if (syntheticKey !== null) {
3335
+ satKeyCache.set(cacheKey, syntheticKey);
3336
+ defineHidden_(satrec, "wasmSatKey", syntheticKey);
3337
+ wasmAttachedCount++;
3338
+ return syntheticKey;
3339
+ }
3340
+ defineHidden_(satrec, "wasmSatKey", WASM_KEY_FAILED);
3341
+ wasmFallbackCount++;
3342
+ return null;
3343
+ }
3344
+ };
3345
+ var tryPropagateWasm = (satrec, tsince) => {
3346
+ if (!backend || satrec.init) {
3347
+ return null;
3348
+ }
3349
+ const satKey = ensureSatKey_(backend, satrec);
3350
+ if (satKey === null) {
3351
+ return null;
3352
+ }
3353
+ const result = backend.propagateOnePosVelFast(satKey, tsince);
3354
+ if (result.err !== 0) {
3355
+ return { position: false, velocity: false };
3356
+ }
3357
+ return {
3358
+ position: result.position,
3359
+ velocity: result.velocity
3360
+ };
3361
+ };
3362
+
3221
3363
  // src/sgp4/sgp4.ts
3222
3364
  var fasx2 = 0.13130908;
3223
3365
  var fasx4 = 2.8843198;
@@ -3232,6 +3374,42 @@ var stepp = 720;
3232
3374
  var stepn = -720;
3233
3375
  var step2 = 259200;
3234
3376
  var Sgp4 = class _Sgp4 {
3377
+ /**
3378
+ * Routes all subsequent `createSatrec`/`propagate` calls in this JavaScript
3379
+ * context through the USSF Astro Standards SGP4 wasm build instead of the
3380
+ * pure-TypeScript implementation. Pass a loaded `Sgp4Wasm` or `Sgp4XpWasm`
3381
+ * instance (see `src/external/`).
3382
+ *
3383
+ * satKeys are attached to satrecs lazily on first propagation, so the
3384
+ * backend can be activated before or after satrecs are created. Satrecs
3385
+ * without TLE lines (e.g. built from an OMM) and TLEs the wasm library
3386
+ * rejects fall back to the TypeScript implementation permanently.
3387
+ * @param wasm A loaded wasm SGP4 instance.
3388
+ */
3389
+ static useWasmBackend(wasm) {
3390
+ setSgp4WasmBackend(wasm);
3391
+ }
3392
+ /**
3393
+ * Restores the pure-TypeScript SGP4 implementation.
3394
+ */
3395
+ static clearWasmBackend() {
3396
+ clearSgp4WasmBackend();
3397
+ }
3398
+ /**
3399
+ * Whether propagation is currently routed through a wasm backend.
3400
+ */
3401
+ static get isWasmBackendActive() {
3402
+ return isSgp4WasmBackendActive();
3403
+ }
3404
+ /**
3405
+ * Diagnostic counters for the wasm backend: distinct TLEs attached to the
3406
+ * wasm registry vs permanently fallen back to the TypeScript
3407
+ * implementation. A growing fallback count means part of the catalog is
3408
+ * still propagating in TypeScript.
3409
+ */
3410
+ static get wasmBackendStats() {
3411
+ return getSgp4WasmBackendStats();
3412
+ }
3235
3413
  /*
3236
3414
  * -----------------------------------------------------------------------------
3237
3415
  *
@@ -3391,6 +3569,7 @@ var Sgp4 = class _Sgp4 {
3391
3569
  xno: satrec.no,
3392
3570
  xnodeo: satrec.nodeo
3393
3571
  });
3572
+ stashTleLines(satrec, tleLine1, tleLine2);
3394
3573
  return satrec;
3395
3574
  }
3396
3575
  static createSatrecFromOmm(omm, whichconst = "wgs72" /* wgs72 */, opsmode = "i" /* IMPROVED */) {
@@ -3908,6 +4087,10 @@ var Sgp4 = class _Sgp4 {
3908
4087
  *----------------------------------------------------------------------------
3909
4088
  */
3910
4089
  static propagate(satrec, tsince) {
4090
+ const wasmResult = tryPropagateWasm(satrec, tsince);
4091
+ if (wasmResult !== null) {
4092
+ return wasmResult;
4093
+ }
3911
4094
  const { xke, j2, j3oj2, vkmpersec } = satrec;
3912
4095
  satrec.t = tsince;
3913
4096
  satrec.error = 0 /* NO_ERROR */;
@@ -6318,7 +6501,7 @@ function uv2azel(u, v, coneHalfAngle) {
6318
6501
  throw new ValidationError("v must be between -1 and 1", "v", v);
6319
6502
  }
6320
6503
  const alpha = Math.asin(u);
6321
- const beta = Math.asin(v);
6504
+ const beta = Math.asin(0 - v);
6322
6505
  const az = alpha / 90 * (coneHalfAngle * RAD2DEG);
6323
6506
  const el = beta / 90 * (coneHalfAngle * RAD2DEG);
6324
6507
  return { az, el };
@@ -6352,6 +6535,8 @@ var StateCovariance = class _StateCovariance {
6352
6535
  this.matrix = matrix;
6353
6536
  this.frame = frame;
6354
6537
  }
6538
+ matrix;
6539
+ frame;
6355
6540
  // / Create a new [StateCovariance] object from 1-sigma values.
6356
6541
  static fromSigmas(sigmas, frame) {
6357
6542
  const n = sigmas.length;
@@ -6554,7 +6739,7 @@ var ClassicalElements = class _ClassicalElements {
6554
6739
  }
6555
6740
  const pos = state.position;
6556
6741
  const vel = state.velocity;
6557
- const a = state.semimajorAxis;
6742
+ const a = 1 / (2 / pos.magnitude() - vel.magnitude() ** 2 / mu);
6558
6743
  const eVecA = pos.scale(vel.magnitude() ** 2 - mu / pos.magnitude());
6559
6744
  const eVecB = vel.scale(pos.dot(vel));
6560
6745
  const eVec = eVecA.subtract(eVecB).scale(1 / mu);
@@ -6683,7 +6868,7 @@ var ClassicalElements = class _ClassicalElements {
6683
6868
  */
6684
6869
  getOrbitRegime() {
6685
6870
  const n = this.revsPerDay;
6686
- const p = this.period * sec2min;
6871
+ const p = this.period;
6687
6872
  if (n >= 0.99 && n <= 1.01 && this.eccentricity < 0.01) {
6688
6873
  return "Geosynchronous Orbit" /* GEO */;
6689
6874
  }
@@ -6751,13 +6936,14 @@ var ClassicalElements = class _ClassicalElements {
6751
6936
  let maInit = eaInit - this.eccentricity * Math.sin(eaInit);
6752
6937
  maInit = matchHalfPlane(maInit, eaInit);
6753
6938
  const maFinal = (maInit + n * delta) % TAU;
6754
- let eaFinal = maFinal;
6755
- for (let iter = 0; iter < 32; iter++) {
6756
- const eaTemp = maFinal + this.eccentricity * Math.sin(eaFinal);
6757
- if (Math.abs(eaTemp - eaFinal) < 1e-12) {
6939
+ const ecc = this.eccentricity;
6940
+ let eaFinal = ecc > 0.8 ? Math.PI * Math.sign(maFinal || 1) : maFinal;
6941
+ for (let iter = 0; iter < 50; iter++) {
6942
+ const step = (eaFinal - ecc * Math.sin(eaFinal) - maFinal) / (1 - ecc * Math.cos(eaFinal));
6943
+ eaFinal -= step;
6944
+ if (Math.abs(step) < 1e-12) {
6758
6945
  break;
6759
6946
  }
6760
- eaFinal = eaTemp;
6761
6947
  }
6762
6948
  const cosEaFinal = Math.cos(eaFinal);
6763
6949
  let vFinal = Math.acos(clamp((cosEaFinal - this.eccentricity) / (1 - this.eccentricity * cosEaFinal), -1, 1));
@@ -7187,13 +7373,21 @@ var GroundObject = class extends BaseObject {
7187
7373
  }
7188
7374
  /**
7189
7375
  * Calculates the Earth-Centered Inertial (ECI) position vector of the ground object at a given date.
7190
- * @variation optimized version of this.toGeodetic().toITRF().toJ2000().position;
7376
+ *
7377
+ * Uses the WGS84 ellipsoid (the same {@link llaRad2ecef} position that `ecef()` and
7378
+ * `Satellite.rae()` use), rotated into TEME by GMST. It deliberately does not use the
7379
+ * spherical-Earth `lla2eci()`: at mid latitudes that puts the observer ~20 km away from
7380
+ * its WGS84 position, which skewed `Satellite.dopplerFactor()` by up to ~0.2 km/s of range
7381
+ * rate near closest approach.
7382
+ *
7383
+ * The result is in TEME (GMST rotation only), the frame `Satellite.eci()` returns. For J2000
7384
+ * use {@link toJ2000}, which also applies precession and nutation.
7191
7385
  * @param date The date for which to calculate the ECI position vector. Defaults to the current date.
7192
7386
  * @returns The ECI position vector of the ground object.
7193
7387
  */
7194
7388
  eci(date = /* @__PURE__ */ new Date()) {
7195
7389
  const { gmst } = calcGmst(date);
7196
- return lla2eci(this.toGeodetic(), gmst);
7390
+ return ecef2eci(llaRad2ecef(this.llaRad()), gmst);
7197
7391
  }
7198
7392
  /**
7199
7393
  * Returns the latitude, longitude, and altitude of the GroundObject.
@@ -7234,18 +7428,16 @@ var GroundObject = class extends BaseObject {
7234
7428
  }
7235
7429
  /**
7236
7430
  * Converts the ground position to J2000 inertial coordinates.
7237
- * Ground objects have zero velocity in the inertial frame (ignoring Earth rotation).
7431
+ *
7432
+ * The site is placed on the WGS84 ellipsoid in ITRF and rotated into J2000 with the full
7433
+ * precession, nutation and sidereal-time chain ({@link ITRF.toJ2000}), so it is in the same
7434
+ * frame as `Satellite.toJ2000()`. The velocity is the Earth-rotation velocity (omega x r,
7435
+ * ~0.46 km/s at the equator): a ground site is fixed in ITRF, not in inertial space.
7238
7436
  * @param date - The date for the conversion (defaults to now)
7239
7437
  * @returns J2000 state vector
7240
7438
  */
7241
7439
  toJ2000(date = /* @__PURE__ */ new Date()) {
7242
- const { gmst } = calcGmst(date);
7243
- const position = lla2eci(this.llaRad(), gmst);
7244
- return new J2000(
7245
- EpochUTC.fromDateTime(date),
7246
- new Vector3D(position.x, position.y, position.z),
7247
- new Vector3D(0, 0, 0)
7248
- );
7440
+ return this.toGeodetic().toITRF(EpochUTC.fromDateTime(date)).toJ2000();
7249
7441
  }
7250
7442
  // ==================== Component Management ====================
7251
7443
  /**
@@ -7792,7 +7984,7 @@ var TEME = class _TEME extends StateVector {
7792
7984
  }
7793
7985
  };
7794
7986
 
7795
- // node_modules/astronomy-engine/esm/astronomy.js
7987
+ // node_modules/.pnpm/astronomy-engine@2.1.19/node_modules/astronomy-engine/esm/astronomy.js
7796
7988
  var C_AUDAY = 173.1446326846693;
7797
7989
  var KM_PER_AU2 = 14959787069098932e-8;
7798
7990
  var DEG2RAD2 = 0.017453292519943295;
@@ -9859,7 +10051,7 @@ function CalcPluto(time, helio) {
9859
10051
  r = ra.mul(1 - ramp).add(rb.mul(ramp));
9860
10052
  v = va.mul(1 - ramp).add(vb.mul(ramp));
9861
10053
  }
9862
- if (helio) {
10054
+ {
9863
10055
  if (!bary)
9864
10056
  bary = new major_bodies_t(time.tt);
9865
10057
  r = r.sub(bary.Sun.r);
@@ -9872,7 +10064,7 @@ function HelioVector(body, date) {
9872
10064
  if (body in vsop)
9873
10065
  return CalcVsop(vsop[body], time);
9874
10066
  if (body === Body.Pluto) {
9875
- const p = CalcPluto(time, true);
10067
+ const p = CalcPluto(time);
9876
10068
  return new Vector2(p.x, p.y, p.z, time);
9877
10069
  }
9878
10070
  if (body === Body.Sun)
@@ -9968,38 +10160,6 @@ function GeoVector(body, date, aberration) {
9968
10160
  function ExportState(terse, time) {
9969
10161
  return new StateVector2(terse.r.x, terse.r.y, terse.r.z, terse.v.x, terse.v.y, terse.v.z, time);
9970
10162
  }
9971
- function BaryState(body, date) {
9972
- const time = MakeTime(date);
9973
- if (body === Body.SSB) {
9974
- return new StateVector2(0, 0, 0, 0, 0, 0, time);
9975
- }
9976
- if (body === Body.Pluto) {
9977
- return CalcPluto(time, false);
9978
- }
9979
- const bary = new major_bodies_t(time.tt);
9980
- switch (body) {
9981
- case Body.Sun:
9982
- return ExportState(bary.Sun, time);
9983
- case Body.Jupiter:
9984
- return ExportState(bary.Jupiter, time);
9985
- case Body.Saturn:
9986
- return ExportState(bary.Saturn, time);
9987
- case Body.Uranus:
9988
- return ExportState(bary.Uranus, time);
9989
- case Body.Neptune:
9990
- return ExportState(bary.Neptune, time);
9991
- case Body.Moon:
9992
- case Body.EMB:
9993
- const earth = CalcVsopPosVel(vsop[Body.Earth], time.tt);
9994
- const state = body === Body.Moon ? GeoMoonState(time) : GeoEmbState(time);
9995
- return new StateVector2(state.x + bary.Sun.r.x + earth.r.x, state.y + bary.Sun.r.y + earth.r.y, state.z + bary.Sun.r.z + earth.r.z, state.vx + bary.Sun.v.x + earth.v.x, state.vy + bary.Sun.v.y + earth.v.y, state.vz + bary.Sun.v.z + earth.v.z, time);
9996
- }
9997
- if (body in vsop) {
9998
- const planet = CalcVsopPosVel(vsop[body], time.tt);
9999
- return new StateVector2(bary.Sun.r.x + planet.r.x, bary.Sun.r.y + planet.r.y, bary.Sun.r.z + planet.r.z, bary.Sun.v.x + planet.v.x, bary.Sun.v.y + planet.v.y, bary.Sun.v.z + planet.v.z, time);
10000
- }
10001
- throw `BaryState: Unsupported body "${body}"`;
10002
- }
10003
10163
  function HelioState(body, date) {
10004
10164
  const time = MakeTime(date);
10005
10165
  switch (body) {
@@ -10019,7 +10179,7 @@ function HelioState(body, date) {
10019
10179
  const planet = CalcVsopPosVel(vsop[body], time.tt);
10020
10180
  return ExportState(planet, time);
10021
10181
  case Body.Pluto:
10022
- return CalcPluto(time, true);
10182
+ return CalcPluto(time);
10023
10183
  case Body.Moon:
10024
10184
  case Body.EMB:
10025
10185
  const earth = CalcVsopPosVel(vsop.Earth, time.tt);
@@ -10340,6 +10500,11 @@ function SearchRiseSet(body, observer, direction, dateStart, limitDays, metersAb
10340
10500
  const altitude = dip - REFRACTION_NEAR_HORIZON * atmos.density;
10341
10501
  return InternalSearchAltitude(body, observer, direction, dateStart, limitDays, body_radius_au, altitude);
10342
10502
  }
10503
+ function SearchAltitude(body, observer, direction, dateStart, limitDays, altitude) {
10504
+ if (!Number.isFinite(altitude) || altitude < -90 || altitude > 90)
10505
+ throw `Invalid altitude angle: ${altitude}`;
10506
+ return InternalSearchAltitude(body, observer, direction, dateStart, limitDays, 0, altitude);
10507
+ }
10343
10508
  var AscentInfo = class {
10344
10509
  constructor(tx, ty, ax, ay) {
10345
10510
  this.tx = tx;
@@ -10602,7 +10767,9 @@ var CelestialBody = class extends BaseObject {
10602
10767
  * Gets rise, transit, and set times for this body as seen from a ground location.
10603
10768
  * @param observer - The ground observer location
10604
10769
  * @param date - The starting date for the search
10605
- * @param minElevation - Minimum elevation angle in degrees (default 0)
10770
+ * @param minElevation - Elevation threshold in degrees (default 0). At 0 the times are the
10771
+ * usual rise/set (upper limb on the horizon, with refraction); otherwise they are when the
10772
+ * body's centre crosses that geometric elevation.
10606
10773
  * @returns Rise, transit, and set times (null if body doesn't rise/set)
10607
10774
  */
10608
10775
  getRiseSetTimes(observer, date = /* @__PURE__ */ new Date(), minElevation = 0) {
@@ -10611,8 +10778,10 @@ var CelestialBody = class extends BaseObject {
10611
10778
  }
10612
10779
  const obs = new Observer(observer.lat, observer.lon, observer.alt * 1e3);
10613
10780
  const time = MakeTime(date);
10614
- const riseTime = SearchRiseSet(this.astronomyBody_, obs, 1, time, 1, minElevation);
10615
- const setTime = SearchRiseSet(this.astronomyBody_, obs, -1, time, 1, minElevation);
10781
+ const body = this.astronomyBody_;
10782
+ const search = (direction) => minElevation === 0 ? SearchRiseSet(body, obs, direction, time, 1) : SearchAltitude(body, obs, direction, time, 1, minElevation);
10783
+ const riseTime = search(1);
10784
+ const setTime = search(-1);
10616
10785
  let transitTime = null;
10617
10786
  if (riseTime && setTime) {
10618
10787
  const riseMs = riseTime.date.getTime();
@@ -10631,14 +10800,17 @@ var CelestialBody = class extends BaseObject {
10631
10800
  }
10632
10801
  /**
10633
10802
  * Calculates the angular diameter of this body as seen from a given position.
10634
- * @param observerPos - The observer's position in km
10803
+ * @param observerPos - The observer's position in km (J2000, at `date`)
10804
+ * @param date - The time of `observerPos` (defaults to now). The body is placed at this
10805
+ * time; without it a past or future observer position was paired with the body's
10806
+ * position now.
10635
10807
  * @returns Angular diameter in radians
10636
10808
  */
10637
- getAngularDiameter(observerPos) {
10809
+ getAngularDiameter(observerPos, date = /* @__PURE__ */ new Date()) {
10638
10810
  if (!this.radius) {
10639
10811
  throw new Error(`${this.name} does not have a defined radius`);
10640
10812
  }
10641
- const distance = observerPos.subtract(this.eci()).magnitude();
10813
+ const distance = observerPos.subtract(this.eci(date)).magnitude();
10642
10814
  return 2 * Math.atan(this.radius / distance);
10643
10815
  }
10644
10816
  /**
@@ -10779,16 +10951,16 @@ var SunBody = class _SunBody extends CelestialBody {
10779
10951
  return this.geoVectorToKm(Body.Sun, date);
10780
10952
  }
10781
10953
  /**
10782
- * Gets the Sun's apparent position (corrected for light travel time).
10954
+ * Gets the Sun's apparent position (corrected for light travel time and aberration).
10955
+ *
10956
+ * Same as {@link eci}: astronomy-engine's `GeoVector` already applies the light-time and
10957
+ * aberration corrections. (This used to roll the time back by another ~499 s, putting the
10958
+ * Sun a further ~20 arcsec along its path.)
10783
10959
  * @param date - The date/time for the position calculation
10784
- * @returns Position vector in kilometers
10960
+ * @returns Position vector in kilometers (J2000)
10785
10961
  */
10786
10962
  eciApparent(date = /* @__PURE__ */ new Date()) {
10787
- const pos = this.eci(date);
10788
- const distance = pos.magnitude();
10789
- const lightTimeSec = distance / cKmPerSec;
10790
- const adjustedDate = new Date(date.getTime() - lightTimeSec * 1e3);
10791
- return this.eci(adjustedDate);
10963
+ return this.eci(date);
10792
10964
  }
10793
10965
  /**
10794
10966
  * Gets the Sun's position in heliocentric coordinates.
@@ -10853,8 +11025,13 @@ var SunBody = class _SunBody extends CelestialBody {
10853
11025
  // ==================== Satellite Shadow Methods ====================
10854
11026
  /**
10855
11027
  * Determines if a satellite is in Earth's shadow.
11028
+ *
11029
+ * The test is a cylinder-plus-cone that tracks the umbra boundary closely (within ~0.01 deg
11030
+ * of the `lightingRatio` umbra edge in LEO); a satellite in the penumbra counts as lit. Use
11031
+ * {@link lightingRatio} when partial illumination matters.
10856
11032
  * @param epoch - The epoch for the calculation
10857
- * @param satPos - The satellite's ECI position in kilometers
11033
+ * @param satPos - The satellite's position in J2000 (the frame of `eci()`), km; a TEME
11034
+ * position from `Satellite.eci()` is ~0.3 deg off in 2026
10858
11035
  * @example
10859
11036
  * ```typescript
10860
11037
  * import { Sun, Satellite, EpochUTC, Vector3D, Kilometers, Seconds } from 'ootk';
@@ -10863,14 +11040,8 @@ var SunBody = class _SunBody extends CelestialBody {
10863
11040
  * const now = new Date();
10864
11041
  * const epoch = new EpochUTC((now.getTime() / 1000) as Seconds);
10865
11042
  *
10866
- * const pv = satellite.eci(now);
10867
- * if (pv) {
10868
- * const satPos = new Vector3D<Kilometers>(
10869
- * pv.position.x as Kilometers,
10870
- * pv.position.y as Kilometers,
10871
- * pv.position.z as Kilometers
10872
- * );
10873
- *
11043
+ * const satPos = satellite.toJ2000(now).position; // J2000, like Sun.eci()
11044
+ * {
10874
11045
  * const inShadow = Sun.shadow(epoch, satPos);
10875
11046
  * console.log(inShadow ? 'Satellite is in eclipse' : 'Satellite is sunlit');
10876
11047
  *
@@ -10946,11 +11117,12 @@ var SunBody = class _SunBody extends CelestialBody {
10946
11117
  }
10947
11118
  /**
10948
11119
  * Calculates the Sun's angular diameter as seen from a position.
10949
- * @param obsPos - Observer position in kilometers
11120
+ * @param obsPos - Observer position in kilometers (J2000, at `date`)
11121
+ * @param date - The time of `obsPos` (defaults to now)
10950
11122
  * @returns Angular diameter in radians
10951
11123
  */
10952
- diameter(obsPos) {
10953
- const sunPos = this.eci();
11124
+ diameter(obsPos, date = /* @__PURE__ */ new Date()) {
11125
+ const sunPos = this.eci(date);
10954
11126
  const distance = obsPos.subtract(sunPos).magnitude();
10955
11127
  return 2 * Math.asin(_SunBody.RADIUS / distance);
10956
11128
  }
@@ -10995,7 +11167,7 @@ var SunBody = class _SunBody extends CelestialBody {
10995
11167
  if (isNaN(lon)) {
10996
11168
  throw new Error("longitude missing");
10997
11169
  }
10998
- const date = dateVal instanceof Date ? dateVal : new Date(dateVal);
11170
+ const date = new Date(dateVal instanceof Date ? dateVal.getTime() : dateVal);
10999
11171
  if (isUtc) {
11000
11172
  date.setUTCHours(12, 0, 0, 0);
11001
11173
  } else {
@@ -11019,15 +11191,19 @@ var SunBody = class _SunBody extends CelestialBody {
11019
11191
  }
11020
11192
  /**
11021
11193
  * Gets sunrise and sunset times using astronomy-engine.
11194
+ *
11195
+ * The times are when the Sun's centre crosses `minElevation` (geometric altitude). The
11196
+ * default -0.833 deg is the standard sunrise/sunset definition (refraction plus the solar
11197
+ * semidiameter); -6, -12 and -18 give civil, nautical and astronomical twilight.
11022
11198
  * @param observer - Ground observer location
11023
11199
  * @param date - The date for calculation
11024
- * @param minElevation - Minimum elevation in degrees (default -0.833 for standard sunrise)
11200
+ * @param minElevation - Elevation of the Sun's centre in degrees (default -0.833 for standard sunrise)
11025
11201
  */
11026
11202
  getSunriseSunset(observer, date = /* @__PURE__ */ new Date(), minElevation = -0.833) {
11027
11203
  const obs = new Observer(observer.lat, observer.lon, observer.alt * 1e3);
11028
11204
  const time = MakeTime(date);
11029
- const sunrise = SearchRiseSet(Body.Sun, obs, 1, time, 1, minElevation);
11030
- const sunset = SearchRiseSet(Body.Sun, obs, -1, time, 1, minElevation);
11205
+ const sunrise = SearchAltitude(Body.Sun, obs, 1, time, 1, minElevation);
11206
+ const sunset = SearchAltitude(Body.Sun, obs, -1, time, 1, minElevation);
11031
11207
  return {
11032
11208
  sunrise: sunrise?.date ?? null,
11033
11209
  sunset: sunset?.date ?? null
@@ -11306,6 +11482,9 @@ var SolarRadiationPressure = class _SolarRadiationPressure extends Force {
11306
11482
  this.area = area;
11307
11483
  this.reflectCoeff = reflectCoeff;
11308
11484
  }
11485
+ mass;
11486
+ area;
11487
+ reflectCoeff;
11309
11488
  // / Solar pressure _(N/m²)_;
11310
11489
  static _kRef = 456e-8 * KM_PER_AU ** 2;
11311
11490
  acceleration(state) {
@@ -11540,7 +11719,7 @@ var MoonBody = class _MoonBody extends CelestialBody {
11540
11719
  if (!phase) {
11541
11720
  phase = _MoonBody.moonCycles_[0];
11542
11721
  }
11543
- let type = "";
11722
+ let type;
11544
11723
  if (next === nextNewMoon) {
11545
11724
  type = "newMoon";
11546
11725
  } else if (next === nextFirstQuarter) {
@@ -11688,6 +11867,8 @@ var ThirdBodyGravity = class _ThirdBodyGravity {
11688
11867
  this.moon = moon;
11689
11868
  this.sun = sun;
11690
11869
  }
11870
+ moon;
11871
+ sun;
11691
11872
  static moonGravity_(state) {
11692
11873
  const rMoon = Moon.eci(state.epoch.toDateTime());
11693
11874
  const aNum = rMoon.subtract(state.position);
@@ -12094,29 +12275,40 @@ var FormatTle = class _FormatTle {
12094
12275
  * Format a value in TLE exponential notation for BSTAR or mean motion ddot.
12095
12276
  * Format: "sNNNNN±N" (8 chars) where mantissa has implied leading decimal point.
12096
12277
  * Example: 0.00017507 → " 17507-3" (i.e., .17507 × 10^-3)
12278
+ *
12279
+ * The result is ALWAYS exactly 8 characters. The field has room for a 5-digit
12280
+ * mantissa and a single-digit exponent, so values needing more are folded into
12281
+ * what the format can express: magnitudes below 1e-10 become zero (the usual
12282
+ * convention for a drag term too small to write), and the 1e9-and-up case is
12283
+ * saturated. Anything longer would push every later column of TLE line 1 to the
12284
+ * right, which silently corrupts ephemeris type, element number and checksum.
12097
12285
  * @param value - The value to format
12098
12286
  * @returns Formatted 8-character string
12099
12287
  */
12100
12288
  static formatTleExponential(value) {
12101
- if (value === 0) {
12289
+ if (value === 0 || !Number.isFinite(value)) {
12102
12290
  return " 00000+0";
12103
12291
  }
12104
12292
  const sign2 = value >= 0 ? " " : "-";
12105
12293
  const absVal = Math.abs(value);
12106
12294
  let exponent = Math.floor(Math.log10(absVal)) + 1;
12107
- let mantissa = absVal / 10 ** exponent;
12108
- if (mantissa >= 1) {
12109
- mantissa /= 10;
12295
+ let mantissa = Math.round(absVal / 10 ** exponent * 1e5);
12296
+ while (mantissa < 1e4) {
12297
+ exponent--;
12298
+ mantissa = Math.round(absVal / 10 ** exponent * 1e5);
12299
+ }
12300
+ while (mantissa >= 1e5) {
12110
12301
  exponent++;
12302
+ mantissa = Math.round(absVal / 10 ** exponent * 1e5);
12111
12303
  }
12112
- if (mantissa < 0.1 && mantissa > 0) {
12113
- mantissa *= 10;
12114
- exponent--;
12304
+ if (exponent < -9) {
12305
+ return " 00000+0";
12306
+ }
12307
+ if (exponent > 9) {
12308
+ return `${sign2}99999+9`;
12115
12309
  }
12116
- const mantissaStr = Math.round(mantissa * 1e5).toString().padStart(5, "0");
12117
12310
  const expSign = exponent >= 0 ? "+" : "-";
12118
- const expStr = Math.abs(exponent).toString();
12119
- return sign2 + mantissaStr + expSign + expStr;
12311
+ return sign2 + mantissa.toString() + expSign + Math.abs(exponent).toString();
12120
12312
  }
12121
12313
  /**
12122
12314
  * Compute TLE line checksum (modulo 10 sum of digits, '-' counts as 1).
@@ -12293,20 +12485,8 @@ ${this.line2}`;
12293
12485
  }
12294
12486
  static calcElsetAge(tle1, nowInput, outputUnits = "days") {
12295
12487
  nowInput ??= /* @__PURE__ */ new Date();
12296
- const currentYearFull = nowInput.getUTCFullYear();
12297
- const currentYearShort = currentYearFull % 100;
12298
- const epochYearShort = parseInt(tle1.substring(18, 20), 10);
12299
- const epochDayOfYear = parseFloat(tle1.substring(20, 32));
12300
- let epochYearFull;
12301
- if (epochYearShort <= currentYearShort) {
12302
- epochYearFull = 2e3 + epochYearShort;
12303
- } else {
12304
- epochYearFull = 1900 + epochYearShort;
12305
- }
12306
- const epochJday = epochDayOfYear + epochYearFull * 365;
12307
- const currentJday = getDayOfYear(nowInput) + currentYearFull * 365;
12308
- const currentTime = (nowInput.getUTCHours() * 3600 + nowInput.getUTCMinutes() * 60 + nowInput.getUTCSeconds()) / 86400;
12309
- const daysOld = currentJday + currentTime - epochJday;
12488
+ const epochMs = _Tle.parseEpoch_(tle1.substring(18, 32)).toDateTime().getTime();
12489
+ const daysOld = (nowInput.getTime() - epochMs) / 864e5;
12310
12490
  switch (outputUnits) {
12311
12491
  case "hours":
12312
12492
  return daysOld * 24;
@@ -12989,6 +13169,8 @@ var RkCheckpoint = class {
12989
13169
  this.cacheState = cacheState;
12990
13170
  this.stepSize = stepSize;
12991
13171
  }
13172
+ cacheState;
13173
+ stepSize;
12992
13174
  };
12993
13175
 
12994
13176
  // src/propagator/RkResult.ts
@@ -12999,6 +13181,9 @@ var RkResult = class {
12999
13181
  this.error = error;
13000
13182
  this.newStep = newStep;
13001
13183
  }
13184
+ state;
13185
+ error;
13186
+ newStep;
13002
13187
  };
13003
13188
 
13004
13189
  // src/interpolator/CubicSpline.ts
@@ -13012,6 +13197,12 @@ var CubicSpline = class {
13012
13197
  this.p1 = p1;
13013
13198
  this.m1 = m1;
13014
13199
  }
13200
+ t0;
13201
+ p0;
13202
+ m0;
13203
+ t1;
13204
+ p1;
13205
+ m1;
13015
13206
  // / Interpolate position at the provided time [t] _(POSIX seconds)_.
13016
13207
  position_(t) {
13017
13208
  const t2 = t * t;
@@ -13050,9 +13241,8 @@ var Interpolator = class {
13050
13241
  * cached value range.
13051
13242
  */
13052
13243
  inWindow(epoch) {
13053
- const start = this.window().start;
13054
- const stop = this.window().end;
13055
- return start <= epoch && epoch <= stop;
13244
+ const window = this.window();
13245
+ return window.start.posix <= epoch.posix && epoch.posix <= window.end.posix;
13056
13246
  }
13057
13247
  /*
13058
13248
  * Calculate the start/stop epoch between this and another [Interpolator].
@@ -13060,13 +13250,15 @@ var Interpolator = class {
13060
13250
  * Returns `null` if there is no overlap between interpolators.
13061
13251
  */
13062
13252
  overlap(interpolator) {
13063
- const x1 = this.window().start;
13064
- const x2 = this.window().end;
13065
- const y1 = interpolator.window().start;
13066
- const y2 = interpolator.window().end;
13253
+ const a = this.window();
13254
+ const b = interpolator.window();
13255
+ const x1 = a.start.posix;
13256
+ const x2 = a.end.posix;
13257
+ const y1 = b.start.posix;
13258
+ const y2 = b.end.posix;
13067
13259
  if (x1 <= y2 && y1 <= x2) {
13068
- const e1 = new EpochUTC(Math.max(x1.posix, y1.posix));
13069
- const e2 = new EpochUTC(Math.min(x2.posix, y2.posix));
13260
+ const e1 = new EpochUTC(Math.max(x1, y1));
13261
+ const e2 = new EpochUTC(Math.min(x2, y2));
13070
13262
  return new EpochWindow(e1, e2);
13071
13263
  }
13072
13264
  return null;
@@ -13087,6 +13279,7 @@ var CubicSplineInterpolator = class _CubicSplineInterpolator extends StateInterp
13087
13279
  super();
13088
13280
  this.splines_ = splines_;
13089
13281
  }
13282
+ splines_;
13090
13283
  static fromEphemeris(ephemeris) {
13091
13284
  const splines = [];
13092
13285
  for (let i = 0; i < ephemeris.length - 1; i++) {
@@ -13286,6 +13479,7 @@ var VerletBlendInterpolator = class _VerletBlendInterpolator extends StateInterp
13286
13479
  super();
13287
13480
  this.ephemeris = ephemeris;
13288
13481
  }
13482
+ ephemeris;
13289
13483
  get sizeBytes() {
13290
13484
  return 64 * 7 * this.ephemeris.length / 8;
13291
13485
  }
@@ -13481,7 +13675,7 @@ var Propagator = class {
13481
13675
  ephemeris(start, stop, interval = 60) {
13482
13676
  const output = [this.propagate(start)];
13483
13677
  let tempEpoch = start;
13484
- while (tempEpoch <= stop) {
13678
+ while (tempEpoch.posix <= stop.posix) {
13485
13679
  tempEpoch = tempEpoch.roll(interval);
13486
13680
  output.push(this.propagate(tempEpoch));
13487
13681
  }
@@ -13655,6 +13849,9 @@ var RungeKuttaAdaptive = class _RungeKuttaAdaptive extends Propagator {
13655
13849
  this._cacheState = this.initState_;
13656
13850
  this.tolerance_ = Math.max(_RungeKuttaAdaptive._minTolerance, Math.abs(tolerance_));
13657
13851
  }
13852
+ initState_;
13853
+ forceModel_;
13854
+ tolerance_;
13658
13855
  // / Initial state vector.
13659
13856
  _cacheState;
13660
13857
  _checkpoints = [];
@@ -13774,7 +13971,7 @@ var RungeKuttaAdaptive = class _RungeKuttaAdaptive extends Propagator {
13774
13971
  let tState = this.propagate(maneuver.start);
13775
13972
  this.forceModel_.loadManeuver(maneuver);
13776
13973
  const ephemeris = [tState];
13777
- while (tState.epoch < maneuver.stop) {
13974
+ while (tState.epoch.posix < maneuver.stop.posix) {
13778
13975
  const step = Math.min(maneuver.stop.difference(tState.epoch), interval);
13779
13976
  tState = this.propagate(tState.epoch.roll(step));
13780
13977
  ephemeris.push(tState);
@@ -13783,13 +13980,13 @@ var RungeKuttaAdaptive = class _RungeKuttaAdaptive extends Propagator {
13783
13980
  return ephemeris;
13784
13981
  }
13785
13982
  ephemerisManeuver(start, finish, maneuvers, interval = 60) {
13786
- const tMvr = maneuvers.filter((mvr) => mvr.start >= start || mvr.stop <= finish);
13983
+ const tMvr = maneuvers.filter((mvr) => mvr.start.posix >= start.posix || mvr.stop.posix <= finish.posix);
13787
13984
  const ephemeris = [];
13788
- if (tMvr[0].start > start) {
13985
+ if (tMvr[0].start.posix > start.posix) {
13789
13986
  ephemeris.push(this.propagate(start));
13790
13987
  }
13791
13988
  for (const mvr of tMvr) {
13792
- while (this._cacheState.epoch < mvr.start) {
13989
+ while (this._cacheState.epoch.posix < mvr.start.posix) {
13793
13990
  const step = Math.min(mvr.start.difference(this._cacheState.epoch), interval);
13794
13991
  this.propagate(this._cacheState.epoch.roll(step));
13795
13992
  if (this._cacheState.epoch.posix !== mvr.start.posix) {
@@ -14388,6 +14585,7 @@ var AccessWindow = class _AccessWindow {
14388
14585
  };
14389
14586
 
14390
14587
  // src/objects/AccessCalculator.ts
14588
+ var CIVIL_TWILIGHT_DEG = -6;
14391
14589
  var AccessCalculator = class _AccessCalculator {
14392
14590
  /** Default calculation time step in milliseconds (10 seconds) */
14393
14591
  static DEFAULT_STEP_MS_ = 1e4;
@@ -14528,31 +14726,20 @@ var AccessCalculator = class _AccessCalculator {
14528
14726
  return false;
14529
14727
  }
14530
14728
  if (constraints.requireSunlit) {
14531
- const eci = target.eci(date);
14532
- if (!eci) {
14729
+ let satPos;
14730
+ try {
14731
+ satPos = target.toJ2000(date).position;
14732
+ } catch {
14533
14733
  return false;
14534
14734
  }
14535
14735
  const epoch = EpochUTC.fromDateTime(date);
14536
- const satPos = new Vector3D(
14537
- eci.position.x,
14538
- eci.position.y,
14539
- eci.position.z
14540
- );
14541
14736
  if (Sun.shadow(epoch, satPos)) {
14542
14737
  return false;
14543
14738
  }
14544
14739
  }
14545
14740
  if (constraints.requireObserverDark) {
14546
- const lla = observer.lla();
14547
- const altMeters = lla.alt * 1e3;
14548
- const sunTimes = Sun.getTimes(date, lla.lat, lla.lon, altMeters);
14549
- const currentTime = date.getTime();
14550
- const civilDusk = sunTimes.civilDusk?.getTime();
14551
- const civilDawn = sunTimes.civilDawn?.getTime();
14552
- if (civilDusk && civilDawn) {
14553
- if (currentTime >= civilDawn && currentTime <= civilDusk) {
14554
- return false;
14555
- }
14741
+ if (Sun.getAzEl(observer, date, false).el >= CIVIL_TWILIGHT_DEG) {
14742
+ return false;
14556
14743
  }
14557
14744
  }
14558
14745
  return true;
@@ -14732,38 +14919,35 @@ var DynamicGroundObject = class _DynamicGroundObject extends GroundObject {
14732
14919
  }
14733
14920
  /**
14734
14921
  * Gets the ECI (Earth-Centered Inertial) position at a specific time.
14922
+ *
14923
+ * The WGS84 position from {@link getEcef} rotated into TEME by GMST, the same model
14924
+ * `GroundObject.eci()` uses (not the spherical-Earth `lla2eci()`).
14735
14925
  * @param time - The time to get position for
14736
- * @returns ECI position vector, or null if time is outside waypoint range
14926
+ * @returns ECI (TEME) position vector, or null if time is outside waypoint range
14737
14927
  */
14738
14928
  getEci(time) {
14739
- const lla = this.getLLA(time);
14740
- if (!lla) {
14929
+ const ecef = this.getEcef(time);
14930
+ if (!ecef) {
14741
14931
  return null;
14742
14932
  }
14743
- const llaRad = {
14744
- lat: lla.lat * DEG2RAD,
14745
- lon: lla.lon * DEG2RAD,
14746
- alt: lla.alt
14747
- };
14748
14933
  const { gmst } = calcGmst(time);
14749
- return lla2eci(llaRad, gmst);
14934
+ return ecef2eci(ecef, gmst);
14750
14935
  }
14751
14936
  /**
14752
14937
  * Converts position at a specific time to J2000 inertial coordinates.
14753
- * Ground objects have zero velocity in the inertial frame (ignoring Earth rotation).
14938
+ *
14939
+ * The WGS84 position is rotated from ITRF into J2000 with precession, nutation and
14940
+ * sidereal time, as in `GroundObject.toJ2000()`. The velocity is the Earth-rotation
14941
+ * velocity (omega x r); the platform's own motion between waypoints is not included.
14754
14942
  * @param time - The time for the conversion
14755
14943
  * @returns J2000 state vector, or null if time is outside waypoint range
14756
14944
  */
14757
14945
  getJ2000(time) {
14758
- const eci = this.getEci(time);
14759
- if (!eci) {
14946
+ const geodetic = this.getGeodetic(time);
14947
+ if (!geodetic) {
14760
14948
  return null;
14761
14949
  }
14762
- return new J2000(
14763
- EpochUTC.fromDateTime(time),
14764
- new Vector3D(eci.x, eci.y, eci.z),
14765
- new Vector3D(0, 0, 0)
14766
- );
14950
+ return geodetic.toITRF(EpochUTC.fromDateTime(time)).toJ2000();
14767
14951
  }
14768
14952
  /**
14769
14953
  * Converts position at a specific time to Geodetic coordinates.
@@ -16123,10 +16307,17 @@ var RAE = class _RAE {
16123
16307
  this.azRateRad = azRateRad;
16124
16308
  this.elRateRad = elRateRad;
16125
16309
  }
16310
+ epoch;
16311
+ rng;
16312
+ azRad;
16313
+ elRad;
16314
+ rngRate;
16315
+ azRateRad;
16316
+ elRateRad;
16126
16317
  // / Create a new [Razel] object, using degrees for the angular values.
16127
16318
  static fromDegrees(epoch, range, azimuth, elevation, rangeRate, azimuthRate, elevationRate) {
16128
- const azimuthRateRad = azimuthRate ? azimuthRate * DEG2RAD : void 0;
16129
- const elevationRateRad = elevationRate ? elevationRate * DEG2RAD : void 0;
16319
+ const azimuthRateRad = typeof azimuthRate === "number" ? azimuthRate * DEG2RAD : void 0;
16320
+ const elevationRateRad = typeof elevationRate === "number" ? elevationRate * DEG2RAD : void 0;
16130
16321
  return new _RAE(
16131
16322
  epoch,
16132
16323
  range,
@@ -16199,14 +16390,14 @@ var RAE = class _RAE {
16199
16390
  * @returns The azimuth rate in degrees per second, or undefined if it is not available.
16200
16391
  */
16201
16392
  get azRate() {
16202
- return this.azRateRad ? this.azRateRad * RAD2DEG : void 0;
16393
+ return typeof this.azRateRad === "number" ? this.azRateRad * RAD2DEG : void 0;
16203
16394
  }
16204
16395
  /**
16205
16396
  * Gets the elevation rate in degrees per second.
16206
16397
  * @returns The elevation rate in degrees per second, or undefined if the elevation rate is not set.
16207
16398
  */
16208
16399
  get elRate() {
16209
- return this.elRateRad ? this.elRateRad * RAD2DEG : void 0;
16400
+ return typeof this.elRateRad === "number" ? this.elRateRad * RAD2DEG : void 0;
16210
16401
  }
16211
16402
  toString() {
16212
16403
  return [
@@ -16378,13 +16569,13 @@ var KeplerPropagator = class extends Propagator {
16378
16569
  return [this.cacheState_];
16379
16570
  }
16380
16571
  ephemerisManeuver(start, finish, maneuvers, interval = 60) {
16381
- const tMvr = maneuvers.slice(0).filter((mvr) => mvr.center >= start || mvr.center <= finish);
16572
+ const tMvr = maneuvers.slice(0).filter((mvr) => mvr.center.posix >= start.posix && mvr.center.posix <= finish.posix);
16382
16573
  const ephemeris = [];
16383
- if (tMvr[0].start > start) {
16574
+ if (tMvr.length === 0 || tMvr[0].start.posix > start.posix) {
16384
16575
  ephemeris.push(this.propagate(start));
16385
16576
  }
16386
16577
  for (const mvr of tMvr) {
16387
- while (this.cacheState_.epoch < mvr.center) {
16578
+ while (this.cacheState_.epoch.posix < mvr.center.posix) {
16388
16579
  const step = Math.min(mvr.center.difference(this.cacheState_.epoch), interval);
16389
16580
  this.propagate(this.cacheState_.epoch.roll(step));
16390
16581
  if (this.cacheState_.epoch.posix !== mvr.center.posix) {
@@ -16393,7 +16584,7 @@ var KeplerPropagator = class extends Propagator {
16393
16584
  }
16394
16585
  ephemeris.push(...this.maneuver(mvr));
16395
16586
  }
16396
- while (this.cacheState_.epoch < finish) {
16587
+ while (this.cacheState_.epoch.posix < finish.posix) {
16397
16588
  const step = Math.min(finish.difference(this.cacheState_.epoch), interval);
16398
16589
  this.propagate(this.cacheState_.epoch.roll(step));
16399
16590
  ephemeris.push(this.cacheState_);
@@ -16409,6 +16600,7 @@ var KeplerPropagator = class extends Propagator {
16409
16600
  }
16410
16601
  restore(index) {
16411
16602
  this.cacheState_ = this.checkpoints_[index];
16603
+ this.elements_ = this.cacheState_.toClassicalElements();
16412
16604
  }
16413
16605
  };
16414
16606
 
@@ -16432,6 +16624,11 @@ var RungeKutta4Propagator = class extends Propagator {
16432
16624
  this.checkpoints_ = checkpoints_;
16433
16625
  this.stepSize_ = Math.abs(stepSize_);
16434
16626
  }
16627
+ initState_;
16628
+ forceModel_;
16629
+ stepSize_;
16630
+ cacheState_;
16631
+ checkpoints_;
16435
16632
  // / Set the integrator step size to the provided number of [seconds].
16436
16633
  setStepSize(seconds) {
16437
16634
  this.stepSize_ = Math.abs(seconds);
@@ -16441,13 +16638,13 @@ var RungeKutta4Propagator = class extends Propagator {
16441
16638
  this.forceModel_ = forceModel;
16442
16639
  }
16443
16640
  ephemerisManeuver(start, finish, maneuvers, interval = 60) {
16444
- const tMvr = maneuvers.slice(0).filter((mvr) => mvr.start <= finish && mvr.stop >= start);
16641
+ const tMvr = maneuvers.slice(0).filter((mvr) => mvr.start.posix <= finish.posix && mvr.stop.posix >= start.posix);
16445
16642
  const ephemeris = [];
16446
- if (tMvr.length === 0 || tMvr[0].start > start) {
16643
+ if (tMvr.length === 0 || tMvr[0].start.posix > start.posix) {
16447
16644
  ephemeris.push(this.propagate(start));
16448
16645
  }
16449
16646
  for (const mvr of tMvr) {
16450
- while (this.cacheState_.epoch < mvr.start) {
16647
+ while (this.cacheState_.epoch.posix < mvr.start.posix) {
16451
16648
  const step = Math.min(mvr.start.difference(this.cacheState_.epoch), interval);
16452
16649
  this.propagate(this.cacheState_.epoch.roll(step));
16453
16650
  if (this.cacheState_.epoch.posix !== mvr.start.posix) {
@@ -16471,7 +16668,7 @@ var RungeKutta4Propagator = class extends Propagator {
16471
16668
  let tState = this.propagate(maneuver.start);
16472
16669
  this.forceModel_.loadManeuver(maneuver);
16473
16670
  const ephemeris = [tState];
16474
- while (tState.epoch < maneuver.stop) {
16671
+ while (tState.epoch.posix < maneuver.stop.posix) {
16475
16672
  const step = Math.min(maneuver.stop.difference(tState.epoch), interval);
16476
16673
  tState = this.propagate(tState.epoch.roll(step));
16477
16674
  ephemeris.push(tState);
@@ -16543,6 +16740,7 @@ var Sgp4Propagator = class extends Propagator {
16543
16740
  this.tle_ = tle_;
16544
16741
  this.cacheState_ = tle_.state.toJ2000();
16545
16742
  }
16743
+ tle_;
16546
16744
  cacheState_;
16547
16745
  checkpoints_ = [];
16548
16746
  /**
@@ -17026,8 +17224,13 @@ var Satellite = class _Satellite extends SpaceObject {
17026
17224
  return rae.az;
17027
17225
  }
17028
17226
  /**
17029
- * Calculates the RAE (Range, Azimuth, Elevation) values for a given sensor and date. If no date is provided, the
17030
- * current time is used.
17227
+ * Calculates the RAE (Range, Azimuth, Elevation) values and their rates for a given sensor
17228
+ * and date. If no date is provided, the current time is used.
17229
+ *
17230
+ * The rates are central differences of `rae()` over +/-0.5 s, stored in the units the
17231
+ * {@link RAE} class expects: km/s for range rate and rad/s for the angle rates (read them
17232
+ * in deg/s with `azRate` / `elRate`). The azimuth change is wrapped so a pass through
17233
+ * north does not produce a 360 deg jump.
17031
17234
  * @variation expanded
17032
17235
  * @param observer - The observer's position on the ground.
17033
17236
  * @param date - The date at which to calculate the RAE values. Optional, defaults to the current date.
@@ -17038,14 +17241,18 @@ var Satellite = class _Satellite extends SpaceObject {
17038
17241
  if (!rae) {
17039
17242
  return null;
17040
17243
  }
17041
- const rae2 = this.rae(observer, new Date(date.getTime() + 1e3));
17042
- if (!rae2) {
17244
+ const halfStepMs = 500;
17245
+ const raeBefore = this.rae(observer, new Date(date.getTime() - halfStepMs));
17246
+ const raeAfter = this.rae(observer, new Date(date.getTime() + halfStepMs));
17247
+ if (!raeBefore || !raeAfter) {
17043
17248
  return null;
17044
17249
  }
17250
+ const dt = 2 * halfStepMs / 1e3;
17251
+ const rangeRate = (raeAfter.rng - raeBefore.rng) / dt;
17252
+ const dAz = ((raeAfter.az - raeBefore.az + 180) % 360 + 360) % 360 - 180;
17253
+ const azimuthRate = dAz / dt * DEG2RAD;
17254
+ const elevationRate = (raeAfter.el - raeBefore.el) / dt * DEG2RAD;
17045
17255
  const epoch = new EpochUTC(date.getTime() / 1e3);
17046
- const rangeRate = rae2.rng - rae.rng;
17047
- const azimuthRate = rae2.az - rae.az;
17048
- const elevationRate = rae2.el - rae.el;
17049
17256
  return new RAE(
17050
17257
  epoch,
17051
17258
  rae.rng,
@@ -17709,11 +17916,12 @@ var Satellite = class _Satellite extends SpaceObject {
17709
17916
  /**
17710
17917
  * Calculates the angle between this satellite, another satellite, and the Sun.
17711
17918
  *
17712
- * Returns the angle at this satellite between the vector to the other satellite
17713
- * and the vector to the Sun.
17919
+ * Returns the angle at the OTHER satellite between its vector to the Sun and its vector
17920
+ * to this satellite: the solar phase angle of `other` as seen from this satellite
17921
+ * (0 = fully lit face toward this satellite, pi = backlit).
17714
17922
  *
17715
17923
  * @param other - The other satellite.
17716
- * @param sunPosition - The Sun's ECI position vector.
17924
+ * @param sunPosition - The Sun's position in TEME (the frame of `eci()`), km.
17717
17925
  * @param date - The date for the calculation.
17718
17926
  * @returns The angle in radians.
17719
17927
  * @throws Error if positions are undefined.
@@ -17756,11 +17964,12 @@ var Satellite = class _Satellite extends SpaceObject {
17756
17964
  * ```
17757
17965
  */
17758
17966
  getSunStatus(date = /* @__PURE__ */ new Date()) {
17759
- const pv = this.eci(date);
17760
- if (!pv) {
17967
+ let satPos;
17968
+ try {
17969
+ satPos = this.toJ2000(date).position;
17970
+ } catch {
17761
17971
  return -1 /* UNKNOWN */;
17762
17972
  }
17763
- const satPos = new Vector3D(pv.position.x, pv.position.y, pv.position.z);
17764
17973
  const sunPos = Sun.eci(date);
17765
17974
  const ratio = Sun.lightingRatio(satPos, sunPos);
17766
17975
  if (ratio === 0) {
@@ -18228,6 +18437,43 @@ var FieldOfView = class _FieldOfView {
18228
18437
  const targetDir = direction.normalize();
18229
18438
  return this.isDirectionWithinCone(targetDir);
18230
18439
  }
18440
+ /**
18441
+ * Samples a unit direction on or inside the FOV cone.
18442
+ *
18443
+ * The cone cross-section is parameterized like a standard 2D ellipse in
18444
+ * angular (gnomonic) space: x = halfAngle * cos(t), y = minorHalfAngle * sin(t),
18445
+ * where x is the off-boresight angle along the major axis (u) and y along the
18446
+ * minor axis (v). Uniform steps in t produce well-distributed boundary points
18447
+ * even for highly eccentric fans (e.g., an 80° x 1° fence radar), unlike
18448
+ * uniform steps in the geometric position angle which clump at the fan tips.
18449
+ *
18450
+ * Points sampled with radialFraction < 1 always satisfy the elliptical cone
18451
+ * containment check; radialFraction = 1 lies exactly on the boundary.
18452
+ *
18453
+ * @param t - Parametric ellipse angle in radians. t=0 points along the major
18454
+ * axis (+u), t=PI/2 along the minor axis (+v). Note this is NOT the
18455
+ * geometric position angle around the boresight.
18456
+ * @param radialFraction - 0 = boresight, 1 = cone boundary (default: 1)
18457
+ * @returns Unit direction vector in the FOV's reference frame (ENU for topocentric)
18458
+ */
18459
+ directionAt(t, radialFraction = 1) {
18460
+ const x = radialFraction * this.halfAngleRad_ * Math.cos(t);
18461
+ const y = radialFraction * this.minorHalfAngleRad_ * Math.sin(t);
18462
+ const theta = Math.hypot(x, y);
18463
+ const { b, u, v } = this.boresightFrame_;
18464
+ if (theta < EPS) {
18465
+ return b;
18466
+ }
18467
+ const cosTheta = Math.cos(theta);
18468
+ const sinTheta = Math.sin(theta);
18469
+ const rx = x / theta;
18470
+ const ry = y / theta;
18471
+ return new Vector3D(
18472
+ cosTheta * b.x + sinTheta * (rx * u.x + ry * v.x),
18473
+ cosTheta * b.y + sinTheta * (rx * u.y + ry * v.y),
18474
+ cosTheta * b.z + sinTheta * (rx * u.z + ry * v.z)
18475
+ );
18476
+ }
18231
18477
  /**
18232
18478
  * Gets the effective minimum elevation at a given azimuth.
18233
18479
  * Considers all applicable elevation masks and returns the most restrictive.
@@ -18637,13 +18883,13 @@ var VisualizationHelpers = class {
18637
18883
  const points = [];
18638
18884
  for (let i = 0; i < samples; i++) {
18639
18885
  const phi = 2 * Math.PI * i / samples;
18640
- const theta = Math.sqrt(
18641
- (fov.halfAngle * Math.cos(phi)) ** 2 + (fov.minorHalfAngle * Math.sin(phi)) ** 2
18642
- ) * DEG2RAD;
18886
+ const x = fov.halfAngle * Math.cos(phi) * DEG2RAD;
18887
+ const y = fov.minorHalfAngle * Math.sin(phi) * DEG2RAD;
18888
+ const theta = Math.hypot(x, y);
18643
18889
  const cosTheta = Math.cos(theta);
18644
18890
  const sinTheta = Math.sin(theta);
18645
- const cosPhi = Math.cos(phi);
18646
- const sinPhi = Math.sin(phi);
18891
+ const cosPhi = theta > 0 ? x / theta : 1;
18892
+ const sinPhi = theta > 0 ? y / theta : 0;
18647
18893
  const dir = {
18648
18894
  x: b.x * cosTheta + u.x * sinTheta * cosPhi + v.x * sinTheta * sinPhi,
18649
18895
  y: b.y * cosTheta + u.y * sinTheta * cosPhi + v.y * sinTheta * sinPhi,
@@ -18833,7 +19079,7 @@ var PlanetBody = class _PlanetBody extends CelestialBody {
18833
19079
  */
18834
19080
  velocity(date = /* @__PURE__ */ new Date()) {
18835
19081
  const time = MakeTime(date);
18836
- const state = BaryState(this.planetData_.body, time);
19082
+ const state = HelioState(this.planetData_.body, time);
18837
19083
  return new Vector3D(
18838
19084
  state.vx * AU_PER_DAY_TO_KM_PER_S,
18839
19085
  state.vy * AU_PER_DAY_TO_KM_PER_S,
@@ -18912,7 +19158,7 @@ var EphemerisBody = class _EphemerisBody extends CelestialBody {
18912
19158
  if (!params.ephemeris || params.ephemeris.length === 0) {
18913
19159
  throw new Error("Ephemeris array cannot be empty");
18914
19160
  }
18915
- let spaceObjectType = 0 /* UNKNOWN */;
19161
+ let spaceObjectType;
18916
19162
  switch (params.bodyType) {
18917
19163
  case "dwarf_planet" /* DWARF_PLANET */:
18918
19164
  spaceObjectType = 36 /* DWARF_PLANET */;
@@ -19287,6 +19533,11 @@ var Thrust = class {
19287
19533
  crosstrack * 1e-3
19288
19534
  );
19289
19535
  }
19536
+ center;
19537
+ radial;
19538
+ intrack;
19539
+ crosstrack;
19540
+ durationRate;
19290
19541
  deltaV;
19291
19542
  get magnitude() {
19292
19543
  return this.deltaV.magnitude() * 1e3;
@@ -19325,6 +19576,9 @@ var Hill = class _Hill {
19325
19576
  this.semimajorAxis_ = semimajorAxis;
19326
19577
  this.meanMotion_ = Earth.smaToMeanMotion(this.semimajorAxis_);
19327
19578
  }
19579
+ epoch;
19580
+ position;
19581
+ velocity;
19328
19582
  semimajorAxis_;
19329
19583
  meanMotion_;
19330
19584
  static fromState(origin, radialPosition, intrackPosition, nodeVelocity, nodeOffsetTime) {
@@ -19501,7 +19755,7 @@ var Hill = class _Hill {
19501
19755
  postManeuvers = postManeuvers.slice();
19502
19756
  let output = preManeuvers;
19503
19757
  output.sort((a, b) => a.center.difference(b.center));
19504
- output = output.filter((mvr) => mvr.center >= this.epoch && mvr.center >= pivot);
19758
+ output = output.filter((mvr) => mvr.center.posix >= this.epoch.posix && mvr.center.posix >= pivot.posix);
19505
19759
  for (const mvr of output) {
19506
19760
  state = state.maneuver(mvr);
19507
19761
  }
@@ -19611,6 +19865,13 @@ var RadecTopocentric = class _RadecTopocentric {
19611
19865
  this.declinationRate = declinationRate;
19612
19866
  this.rangeRate = rangeRate;
19613
19867
  }
19868
+ epoch;
19869
+ rightAscension;
19870
+ declination;
19871
+ range;
19872
+ rightAscensionRate;
19873
+ declinationRate;
19874
+ rangeRate;
19614
19875
  /**
19615
19876
  * Create a new RadecTopocentric object, using degrees for the angular values.
19616
19877
  * @param epoch UTC epoch.
@@ -19623,8 +19884,8 @@ var RadecTopocentric = class _RadecTopocentric {
19623
19884
  * @returns A new RadecTopocentric object.
19624
19885
  */
19625
19886
  static fromDegrees(epoch, rightAscensionDegrees, declinationDegrees, range, rightAscensionRateDegrees, declinationRateDegrees, rangeRate) {
19626
- const rightAscensionRate = rightAscensionRateDegrees ? rightAscensionRateDegrees * DEG2RAD : null;
19627
- const declinationRate = declinationRateDegrees ? declinationRateDegrees * DEG2RAD : null;
19887
+ const rightAscensionRate = typeof rightAscensionRateDegrees === "number" ? rightAscensionRateDegrees * DEG2RAD : null;
19888
+ const declinationRate = typeof declinationRateDegrees === "number" ? declinationRateDegrees * DEG2RAD : null;
19628
19889
  const normalizedRA = ((rightAscensionDegrees * DEG2RAD + Math.PI) % TAU + TAU) % TAU - Math.PI;
19629
19890
  return new _RadecTopocentric(
19630
19891
  epoch,
@@ -19694,14 +19955,14 @@ var RadecTopocentric = class _RadecTopocentric {
19694
19955
  * @returns The right ascension rate in degrees per second, or null if it is not available.
19695
19956
  */
19696
19957
  get rightAscensionRateDegrees() {
19697
- return this.rightAscensionRate ? this.rightAscensionRate * RAD2DEG : null;
19958
+ return typeof this.rightAscensionRate === "number" ? this.rightAscensionRate * RAD2DEG : null;
19698
19959
  }
19699
19960
  /**
19700
19961
  * Gets the rate of change of declination in degrees per second.
19701
19962
  * @returns The rate of change of declination in degrees per second, or null if the declination rate is not defined.
19702
19963
  */
19703
19964
  get declinationRateDegrees() {
19704
- return this.declinationRate ? this.declinationRate * RAD2DEG : null;
19965
+ return typeof this.declinationRate === "number" ? this.declinationRate * RAD2DEG : null;
19705
19966
  }
19706
19967
  /**
19707
19968
  * Return the position relative to the observer site.
@@ -19725,7 +19986,7 @@ var RadecTopocentric = class _RadecTopocentric {
19725
19986
  * @returns A Vector3D object.
19726
19987
  */
19727
19988
  velocity(site, range, rangeRate) {
19728
- if (!this.rightAscensionRate || !this.declinationRate) {
19989
+ if (typeof this.rightAscensionRate !== "number" || typeof this.declinationRate !== "number") {
19729
19990
  throw new Error("Velocity unsolvable, missing ra/dec rates.");
19730
19991
  }
19731
19992
  const r = range ?? this.range ?? 1;
@@ -19780,6 +20041,9 @@ var ObservationOptical = class _ObservationOptical extends Observation {
19780
20041
  this.observation = observation;
19781
20042
  this.noise_ = noise_;
19782
20043
  }
20044
+ site_;
20045
+ observation;
20046
+ noise_;
19783
20047
  // / Inertial site location.
19784
20048
  get site() {
19785
20049
  return this.site_;
@@ -19870,6 +20134,9 @@ var ObservationRadar = class _ObservationRadar extends Observation {
19870
20134
  this.observation = observation;
19871
20135
  this.noise_ = noise_;
19872
20136
  }
20137
+ site_;
20138
+ observation;
20139
+ noise_;
19873
20140
  // / Default noise matrix.
19874
20141
  static defaultNoise = _ObservationRadar.noiseFromSigmas(0.32, 0.015 * DEG2RAD, 0.015 * DEG2RAD);
19875
20142
  get epoch() {
@@ -19950,6 +20217,13 @@ var RadecGeocentric = class _RadecGeocentric {
19950
20217
  this.declinationRate = declinationRate;
19951
20218
  this.rangeRate = rangeRate;
19952
20219
  }
20220
+ epoch;
20221
+ rightAscension;
20222
+ declination;
20223
+ range;
20224
+ rightAscensionRate;
20225
+ declinationRate;
20226
+ rangeRate;
19953
20227
  /**
19954
20228
  * Creates a RadecGeocentric object from the given parameters in degrees.
19955
20229
  * @param epoch - The epoch in UTC.
@@ -19962,8 +20236,8 @@ var RadecGeocentric = class _RadecGeocentric {
19962
20236
  * @returns A new RadecGeocentric object.
19963
20237
  */
19964
20238
  static fromDegrees(epoch, rightAscensionDegrees, declinationDegrees, range, rightAscensionRateDegrees, declinationRateDegrees, rangeRate) {
19965
- const rightAscensionRate = rightAscensionRateDegrees ? rightAscensionRateDegrees * DEG2RAD : null;
19966
- const declinationRate = declinationRateDegrees ? declinationRateDegrees * DEG2RAD : null;
20239
+ const rightAscensionRate = typeof rightAscensionRateDegrees === "number" ? rightAscensionRateDegrees * DEG2RAD : null;
20240
+ const declinationRate = typeof declinationRateDegrees === "number" ? declinationRateDegrees * DEG2RAD : null;
19967
20241
  return new _RadecGeocentric(
19968
20242
  epoch,
19969
20243
  rightAscensionDegrees * DEG2RAD,
@@ -20000,7 +20274,7 @@ var RadecGeocentric = class _RadecGeocentric {
20000
20274
  const declinationRate = (vK - rangeRate * (rK / rMag)) / rIJMag;
20001
20275
  return new _RadecGeocentric(
20002
20276
  state.epoch,
20003
- rightAscension % TAU,
20277
+ (rightAscension % TAU + TAU) % TAU,
20004
20278
  declination,
20005
20279
  rMag,
20006
20280
  rightAscensionRate,
@@ -20013,7 +20287,8 @@ var RadecGeocentric = class _RadecGeocentric {
20013
20287
  * @returns The right ascension in degrees.
20014
20288
  */
20015
20289
  get rightAscensionDegrees() {
20016
- return this.rightAscension * RAD2DEG;
20290
+ const deg = this.rightAscension * RAD2DEG;
20291
+ return (deg % 360 + 360) % 360;
20017
20292
  }
20018
20293
  /**
20019
20294
  * Gets the declination in degrees.
@@ -20027,14 +20302,14 @@ var RadecGeocentric = class _RadecGeocentric {
20027
20302
  * @returns The right ascension rate in degrees per second, or null if it is not available.
20028
20303
  */
20029
20304
  get rightAscensionRateDegrees() {
20030
- return this.rightAscensionRate ? this.rightAscensionRate * RAD2DEG : null;
20305
+ return typeof this.rightAscensionRate === "number" ? this.rightAscensionRate * RAD2DEG : null;
20031
20306
  }
20032
20307
  /**
20033
20308
  * Gets the rate of change of declination in degrees per second.
20034
20309
  * @returns The rate of change of declination in degrees per second, or null if not available.
20035
20310
  */
20036
20311
  get declinationRateDegrees() {
20037
- return this.declinationRate ? this.declinationRate * RAD2DEG : null;
20312
+ return typeof this.declinationRate === "number" ? this.declinationRate * RAD2DEG : null;
20038
20313
  }
20039
20314
  /**
20040
20315
  * Calculates the position vector in geocentric coordinates.
@@ -20054,7 +20329,7 @@ var RadecGeocentric = class _RadecGeocentric {
20054
20329
  * @throws Error if the right ascension rate or declination rate is missing.
20055
20330
  */
20056
20331
  velocity(range, rangeRate) {
20057
- if (!this.rightAscensionRate || !this.declinationRate) {
20332
+ if (typeof this.rightAscensionRate !== "number" || typeof this.declinationRate !== "number") {
20058
20333
  throw new Error("Velocity unsolvable, missing ra/dec rates.");
20059
20334
  }
20060
20335
  const r = range ?? this.range ?? 1;
@@ -20472,12 +20747,143 @@ var RandomGaussianSource = class {
20472
20747
  }
20473
20748
  };
20474
20749
 
20750
+ // src/external/Sgp4WasmTypes.ts
20751
+ var Sgp4WasmLogLevel = /* @__PURE__ */ ((Sgp4WasmLogLevel3) => {
20752
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["None"] = 0] = "None";
20753
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["Info"] = 1] = "Info";
20754
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["Warn"] = 2] = "Warn";
20755
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["Error"] = 3] = "Error";
20756
+ return Sgp4WasmLogLevel3;
20757
+ })(Sgp4WasmLogLevel || {});
20758
+ var Sgp4WasmError = class extends Error {
20759
+ constructor(message, code) {
20760
+ super(message);
20761
+ this.code = code;
20762
+ this.name = "Sgp4WasmError";
20763
+ }
20764
+ code;
20765
+ };
20766
+ var SGP4_WASM_DYN_ARR_ERRORS = {
20767
+ [-1]: "Error allocating space for the dynamic array",
20768
+ [-2]: "Dynamic array is already initialized",
20769
+ [-3]: "The requested size is too large",
20770
+ [-4]: "The requested size is negative"
20771
+ };
20772
+ var SGP4_WASM_ADD_SAT_ERRORS = {
20773
+ [-1]: "Dynamic array has not been allocated",
20774
+ [-2]: "Dynamic array is out of space (call reallocDynArr)",
20775
+ [-3]: "Failed to determine satKey from line 1",
20776
+ [-4]: "Failed to parse line 1 or line 2"
20777
+ };
20778
+
20779
+ // src/propagator/Sgp4WasmPropagator.ts
20780
+ var Sgp4WasmPropagator = class extends Propagator {
20781
+ constructor(wasm_, tle_) {
20782
+ super();
20783
+ this.wasm_ = wasm_;
20784
+ this.tle_ = tle_;
20785
+ if (!wasm_.isLoaded) {
20786
+ throw new Sgp4WasmError("Sgp4WasmPropagator requires a loaded Sgp4Wasm instance. Call load() first.");
20787
+ }
20788
+ this.satKey_ = wasm_.addSat(tle_.line1, tle_.line2);
20789
+ wasm_.initSats([this.satKey_]);
20790
+ this.cacheState_ = this.propagate(tle_.epoch);
20791
+ }
20792
+ wasm_;
20793
+ tle_;
20794
+ satKey_;
20795
+ cacheState_;
20796
+ checkpoints_ = [];
20797
+ /**
20798
+ * The Astro Standards satKey backing this propagator.
20799
+ */
20800
+ get satKey() {
20801
+ return this.satKey_;
20802
+ }
20803
+ /**
20804
+ * Gets the state of the propagator in the J2000 coordinate system.
20805
+ * @returns The J2000 state of the propagator.
20806
+ */
20807
+ get state() {
20808
+ return this.cacheState_;
20809
+ }
20810
+ /**
20811
+ * Calculates the ephemeris maneuver using the SGP4 propagator.
20812
+ * @param start The start epoch in UTC.
20813
+ * @param finish The finish epoch in UTC.
20814
+ * @param maneuvers The array of thrust maneuvers.
20815
+ * @param interval The time interval in seconds.
20816
+ */
20817
+ ephemerisManeuver(_start, _finish, _maneuvers, _interval = 60) {
20818
+ throw new Error("Maneuvers cannot be modelled with SGP4.");
20819
+ }
20820
+ /**
20821
+ * Performs a maneuver with the given thrust.
20822
+ * @param maneuver - The thrust maneuver to perform.
20823
+ * @param interval - The time interval for the maneuver (default: 60.0 seconds).
20824
+ * @throws Error if maneuvers cannot be modeled with SGP4.
20825
+ */
20826
+ maneuver(_maneuver, _interval = 60) {
20827
+ throw new Error("Maneuvers cannot be modelled with SGP4.");
20828
+ }
20829
+ /**
20830
+ * Propagates the state to a specified epoch in J2000 coordinates.
20831
+ * @param epoch - The epoch in UTC format.
20832
+ * @returns The propagated state in J2000 coordinates.
20833
+ */
20834
+ propagate(epoch) {
20835
+ const state = this.wasm_.propagateEpoch(this.satKey_, epoch);
20836
+ if (state.err !== 0) {
20837
+ throw new Sgp4WasmError(`SGP4 propagation failed for satKey ${this.satKey_} (code ${state.err})`, state.err);
20838
+ }
20839
+ this.cacheState_ = new TEME(epoch, state.position, state.velocity).toJ2000();
20840
+ return this.cacheState_;
20841
+ }
20842
+ /**
20843
+ * Resets the state of the propagator to the TLE epoch state.
20844
+ */
20845
+ reset() {
20846
+ this.cacheState_ = this.propagate(this.tle_.epoch);
20847
+ }
20848
+ /**
20849
+ * Saves the current state of the propagator and returns the index of the checkpoint.
20850
+ * @returns The index of the checkpoint.
20851
+ */
20852
+ checkpoint() {
20853
+ this.checkpoints_.push(this.cacheState_);
20854
+ return this.checkpoints_.length - 1;
20855
+ }
20856
+ /**
20857
+ * Clears all the checkpoints in the propagator.
20858
+ */
20859
+ clearCheckpoints() {
20860
+ this.checkpoints_ = [];
20861
+ }
20862
+ /**
20863
+ * Restores the state of the propagator to a previously saved checkpoint.
20864
+ * @param index - The index of the checkpoint to restore.
20865
+ */
20866
+ restore(index) {
20867
+ this.cacheState_ = this.checkpoints_[index];
20868
+ }
20869
+ /**
20870
+ * Removes the satellite from the backing wasm instance's SGP4 and TLE
20871
+ * registries.
20872
+ */
20873
+ dispose() {
20874
+ this.wasm_.uninitSats([this.satKey_]);
20875
+ this.wasm_.removeSats([this.satKey_]);
20876
+ }
20877
+ };
20878
+
20475
20879
  // src/observation/PropagatorPairs.ts
20476
20880
  var PropagatorPairs = class {
20477
20881
  constructor(posStep_, velStep_) {
20478
20882
  this.posStep_ = posStep_;
20479
20883
  this.velStep_ = velStep_;
20480
20884
  }
20885
+ posStep_;
20886
+ velStep_;
20481
20887
  _high = Array(6).fill(null);
20482
20888
  _low = Array(6).fill(null);
20483
20889
  set(index, high, low) {
@@ -20511,6 +20917,9 @@ var BatchLeastSquaresResult = class {
20511
20917
  this.covariance = covariance2;
20512
20918
  this.rms = rms;
20513
20919
  }
20920
+ state;
20921
+ covariance;
20922
+ rms;
20514
20923
  };
20515
20924
 
20516
20925
  // src/orbit-determination/BatchLeastSquaresOD.ts
@@ -20541,6 +20950,12 @@ var BatchLeastSquaresOD = class _BatchLeastSquaresOD {
20541
20950
  this.propagator_ = new RungeKutta89Propagator(this.apriori_, this.forceModel_);
20542
20951
  this.nominal_ = this.propagator_.propagate(this.start_);
20543
20952
  }
20953
+ observations_;
20954
+ apriori_;
20955
+ forceModel_;
20956
+ posStep_;
20957
+ velStep_;
20958
+ fastDerivatives_;
20544
20959
  /** Propagator pair cache, for generating observation Jacobians. */
20545
20960
  propPairs_;
20546
20961
  /** Nominal state propagator. */
@@ -20641,6 +21056,7 @@ var GibbsIOD = class _GibbsIOD {
20641
21056
  constructor(mu = Earth.mu) {
20642
21057
  this.mu = mu;
20643
21058
  }
21059
+ mu;
20644
21060
  /** Abort solve if position plane exceeds this value. */
20645
21061
  static coplanarThreshold_ = 5 * DEG2RAD;
20646
21062
  /**
@@ -20699,6 +21115,7 @@ var HerrickGibbsIOD = class {
20699
21115
  constructor(mu = Earth.mu) {
20700
21116
  this.mu = mu;
20701
21117
  }
21118
+ mu;
20702
21119
  // / Attempt to create a state estimate from three inertial position vectors.
20703
21120
  solve(r1, t1, r2, t2, r3, t3) {
20704
21121
  const dt31 = t3.difference(t1);
@@ -20959,6 +21376,7 @@ var LambertIOD = class _LambertIOD {
20959
21376
  constructor(mu = Earth.mu) {
20960
21377
  this.mu = mu;
20961
21378
  }
21379
+ mu;
20962
21380
  /**
20963
21381
  * Try to guess the short path argument given an [interceptor] and
20964
21382
  * [target] state.
@@ -21214,10 +21632,9 @@ var GoodingIOD = class _GoodingIOD {
21214
21632
  * @returns Orbit estimate referenced to the epoch of the second observation
21215
21633
  */
21216
21634
  estimate(o1, o2, o3, rho1init = null, rho3init = null, nRev = 0, direction = true) {
21217
- let orbit = null;
21218
21635
  if (rho1init === null || rho3init === null) {
21219
21636
  const gaussIod = new GaussIOD(this._mu);
21220
- orbit = gaussIod.estimate(o1, o2, o3);
21637
+ const orbit = gaussIod.estimate(o1, o2, o3);
21221
21638
  if (orbit === null) {
21222
21639
  throw new OrbitDeterminationError("Gauss IOD failed to provide initial estimate for Gooding IOD", "Gooding");
21223
21640
  }
@@ -21646,6 +22063,11 @@ var LevenbergMarquardtResult = class {
21646
22063
  this.iterations = iterations;
21647
22064
  this.converged = converged;
21648
22065
  }
22066
+ state;
22067
+ covariance;
22068
+ rms;
22069
+ iterations;
22070
+ converged;
21649
22071
  };
21650
22072
 
21651
22073
  // src/orbit-determination/LevenbergMarquardtOD.ts
@@ -21676,6 +22098,12 @@ var LevenbergMarquardtOD = class _LevenbergMarquardtOD {
21676
22098
  const propagator = new RungeKutta89Propagator(this.apriori_, this.forceModel_);
21677
22099
  this.nominal_ = propagator.propagate(this.start_);
21678
22100
  }
22101
+ observations_;
22102
+ apriori_;
22103
+ forceModel_;
22104
+ posStep_;
22105
+ velStep_;
22106
+ fastDerivatives_;
21679
22107
  /** Propagator pair cache, for generating observation Jacobians. */
21680
22108
  propPairs_;
21681
22109
  /** State estimate during solve. */
@@ -21834,6 +22262,8 @@ var SimplexEntry = class _SimplexEntry {
21834
22262
  this.x_ = new Vector(points);
21835
22263
  this.score = this.f_(points);
21836
22264
  }
22265
+ f_;
22266
+ points;
21837
22267
  /**
21838
22268
  * The cost function score at this vertex.
21839
22269
  *
@@ -22112,6 +22542,18 @@ var ModifiedGoodingIOD = class {
22112
22542
  };
22113
22543
  return scoreFn;
22114
22544
  }
22545
+ /**
22546
+ * Solves for a J2000 state at the middle observation's epoch: a Gooding three-line-of-sight
22547
+ * estimate refined by a downhill simplex over all observations with a two-body force model.
22548
+ *
22549
+ * `tolerance` ends the simplex once the spread of the summed line-of-sight error (radians)
22550
+ * or of the vertices falls below it.
22551
+ * @param observations - Optical observations, in time order (at least 3)
22552
+ * @param r0 - Range guess for the first observation (km)
22553
+ * @param rN - Range guess for the last observation (km)
22554
+ * @param options - Solver options
22555
+ * @returns The J2000 state at the middle observation's epoch
22556
+ */
22115
22557
  solve(observations, r0, rN, {
22116
22558
  nRev = 0,
22117
22559
  direction = true,
@@ -22575,6 +23017,8 @@ var ConjunctionAssessment = class {
22575
23017
  this.primaryProp = this.createPropagator(primary, false);
22576
23018
  this.secondaryProp = this.createPropagator(secondary, false);
22577
23019
  }
23020
+ primary;
23021
+ secondary;
22578
23022
  primaryProp;
22579
23023
  secondaryProp;
22580
23024
  primaryCovSample;
@@ -23809,7 +24253,7 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23809
24253
  this.validateFace_(face);
23810
24254
  const azRad = az * DEG2RAD;
23811
24255
  const elRad = el * DEG2RAD;
23812
- const azDiff = azRad - this.boresightAzRad(face);
24256
+ const azDiff = wrapAngle(azRad - this.boresightAzRad(face));
23813
24257
  const elDiff = elRad - this.boresightElRad(face);
23814
24258
  return azel2uv(azDiff, elDiff, this.beamwidthRad);
23815
24259
  }
@@ -23903,9 +24347,7 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23903
24347
  getVisibleFaces(az, el, maxAngle = 60) {
23904
24348
  const visibleFaces = [];
23905
24349
  for (let face = 0; face < this.faceCount; face++) {
23906
- const azDiff = Math.abs(az - this.boresightAz[face]);
23907
- const elDiff = Math.abs(el - this.boresightEl[face]);
23908
- const angularDist = Math.sqrt(azDiff * azDiff + elDiff * elDiff);
24350
+ const angularDist = this.angleFromBoresight_(az, el, face);
23909
24351
  if (angularDist <= maxAngle) {
23910
24352
  visibleFaces.push(face);
23911
24353
  }
@@ -23922,9 +24364,7 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23922
24364
  let bestFace = -1;
23923
24365
  let minDist = Infinity;
23924
24366
  for (let face = 0; face < this.faceCount; face++) {
23925
- const azDiff = az - this.boresightAz[face];
23926
- const elDiff = el - this.boresightEl[face];
23927
- const dist = Math.sqrt(azDiff * azDiff + elDiff * elDiff);
24367
+ const dist = this.angleFromBoresight_(az, el, face);
23928
24368
  if (dist < minDist && dist < 90) {
23929
24369
  minDist = dist;
23930
24370
  bestFace = face;
@@ -23932,6 +24372,24 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23932
24372
  }
23933
24373
  return bestFace;
23934
24374
  }
24375
+ /**
24376
+ * Great-circle angle between a look direction and a face's boresight, in degrees.
24377
+ * (The face checks used sqrt(dAz^2 + dEl^2) with no azimuth wrap: 359 deg missed a 0 deg
24378
+ * face, and azimuth differences were not shrunk by cos(elevation).)
24379
+ * @param az - Target azimuth in degrees
24380
+ * @param el - Target elevation in degrees
24381
+ * @param face - Face index
24382
+ * @returns Angle from boresight in degrees
24383
+ */
24384
+ angleFromBoresight_(az, el, face) {
24385
+ return angularDistance(
24386
+ az * DEG2RAD,
24387
+ el * DEG2RAD,
24388
+ this.boresightAz[face] * DEG2RAD,
24389
+ this.boresightEl[face] * DEG2RAD,
24390
+ 1 /* Haversine */
24391
+ ) * RAD2DEG;
24392
+ }
23935
24393
  // ==================== Serialization ====================
23936
24394
  serializeSpecific() {
23937
24395
  return {
@@ -24442,21 +24900,98 @@ var PassiveRFSensor = class _PassiveRFSensor extends Sensor {
24442
24900
  }
24443
24901
  };
24444
24902
 
24445
- // src/parsers/HorizonsParser.ts
24446
- function julianDateToDate(jd) {
24447
- const unixMs = (jd - 24405875e-1) * 864e5;
24448
- return new Date(unixMs);
24449
- }
24450
- var HorizonsParser = class {
24451
- constructor() {
24903
+ // src/sensor/TtcAntenna.ts
24904
+ var TtcAntenna = class _TtcAntenna extends Sensor {
24905
+ /** Antenna dish diameter in meters */
24906
+ antennaDiameter;
24907
+ /** Antenna gain in dB */
24908
+ antennaGain;
24909
+ /** Whether the station supports two-way ranging */
24910
+ hasRanging;
24911
+ /** Whether the station can uplink commands */
24912
+ hasUplink;
24913
+ constructor(params) {
24914
+ const paramsWithType = {
24915
+ ...params,
24916
+ sensorType: "TT_C_ANTENNA" /* TT_C_ANTENNA */
24917
+ };
24918
+ super(paramsWithType);
24919
+ this.antennaDiameter = params.antennaDiameter;
24920
+ this.antennaGain = params.antennaGain;
24921
+ this.hasRanging = params.hasRanging ?? true;
24922
+ this.hasUplink = params.hasUplink ?? true;
24452
24923
  }
24453
24924
  /**
24454
- * Parses NASA Horizons vector table format.
24455
- *
24456
- * Expects output from Horizons with settings:
24457
- * - VECTORS (type 2 or 3)
24458
- * - Output in km and km/s
24459
- *
24925
+ * Creates an angle observation (Ra/Dec) of a cooperative target.
24926
+ * Angles come from antenna autotrack while locked to the target's downlink.
24927
+ * @param target - The space object to observe
24928
+ * @param date - Time of observation (defaults to now)
24929
+ * @returns ObservationOptical or null if target not in FOV
24930
+ */
24931
+ observe(target, date = /* @__PURE__ */ new Date()) {
24932
+ if (!this.canObserve(target, date)) {
24933
+ return null;
24934
+ }
24935
+ const sensorJ2000 = this.getJ2000(date);
24936
+ const targetJ2000 = target.toJ2000(date);
24937
+ const radec = RadecTopocentric.fromStateVector(targetJ2000, sensorJ2000);
24938
+ return new ObservationOptical(sensorJ2000, radec);
24939
+ }
24940
+ serializeSpecific() {
24941
+ return {
24942
+ antennaDiameter: this.antennaDiameter,
24943
+ antennaGain: this.antennaGain,
24944
+ hasRanging: this.hasRanging,
24945
+ hasUplink: this.hasUplink
24946
+ };
24947
+ }
24948
+ /**
24949
+ * Creates a deep copy of this TT&C antenna.
24950
+ * The cloned sensor will not have a parent assigned.
24951
+ * @returns A new TtcAntenna instance with the same properties
24952
+ */
24953
+ clone() {
24954
+ return new _TtcAntenna({
24955
+ id: this.id,
24956
+ name: this.name,
24957
+ sensorType: this.sensorType,
24958
+ fieldOfView: this.fieldOfView.serialize(),
24959
+ antennaDiameter: this.antennaDiameter,
24960
+ antennaGain: this.antennaGain,
24961
+ hasRanging: this.hasRanging,
24962
+ hasUplink: this.hasUplink,
24963
+ shortName: this.shortName,
24964
+ system: this.system,
24965
+ country: this.country,
24966
+ operator: this.operator,
24967
+ dwellTime: this.dwellTime,
24968
+ freqBand: this.freqBand,
24969
+ isVolumetric: this.isVolumetric,
24970
+ url: this.url,
24971
+ metadata: this.metadata ? { ...this.metadata } : void 0
24972
+ });
24973
+ }
24974
+ };
24975
+
24976
+ // src/parsers/HorizonsParser.ts
24977
+ function julianDateToDate(jd) {
24978
+ const unixMs = (jd - 24405875e-1) * 864e5;
24979
+ return new Date(unixMs);
24980
+ }
24981
+ function tdbDateToEpochUtc(tdb) {
24982
+ const j2000Posix = 946728e3;
24983
+ return EpochUTC.fromJ2000TTSeconds(tdb.getTime() / 1e3 - j2000Posix);
24984
+ }
24985
+ var HorizonsParser = class {
24986
+ constructor() {
24987
+ }
24988
+ /**
24989
+ * Parses NASA Horizons vector table format.
24990
+ *
24991
+ * Expects output from Horizons with settings:
24992
+ * - VECTORS (type 2 or 3)
24993
+ * - Output in km and km/s
24994
+ *
24460
24995
  * @param data - Raw Horizons output text
24461
24996
  * @returns Parsed ephemeris data
24462
24997
  */
@@ -24478,22 +25013,34 @@ var HorizonsParser = class {
24478
25013
  let referenceFrame = "";
24479
25014
  for (let i = 0; i < lines.length; i++) {
24480
25015
  const line = lines[i].trim();
24481
- if (line.startsWith("Target body name:")) {
24482
- targetName = this.extractTargetName_(line, "Target body name:");
24483
- metadata.targetName = targetName;
24484
- } else if (line.startsWith("Center body name:")) {
24485
- centerBody = this.extractValue_(line, "Center body name:");
24486
- metadata.centerBody = centerBody;
24487
- } else if (line.startsWith("Center-site name:") && !centerBody) {
24488
- centerBody = this.extractValue_(line, "Center-site name:");
24489
- metadata.centerBody = centerBody;
24490
- } else if (line.startsWith("Output type:")) {
24491
- metadata.outputType = this.extractValue_(line, "Output type:");
24492
- } else if (line.startsWith("Reference frame:")) {
24493
- referenceFrame = this.extractValue_(line, "Reference frame:");
24494
- metadata.referenceFrame = referenceFrame;
24495
- } else if (line.startsWith("Output units:")) {
24496
- metadata.outputUnits = this.extractValue_(line, "Output units:");
25016
+ const meta = this.matchMetadataLine_(line);
25017
+ if (meta) {
25018
+ switch (meta.label) {
25019
+ case "Target body name":
25020
+ targetName = this.stripNameSuffixes_(meta.value);
25021
+ metadata.targetName = targetName;
25022
+ break;
25023
+ case "Center body name":
25024
+ centerBody = meta.value;
25025
+ metadata.centerBody = centerBody;
25026
+ break;
25027
+ case "Center-site name":
25028
+ if (!centerBody) {
25029
+ centerBody = meta.value;
25030
+ metadata.centerBody = centerBody;
25031
+ }
25032
+ break;
25033
+ case "Output type":
25034
+ metadata.outputType = meta.value;
25035
+ break;
25036
+ case "Reference frame":
25037
+ referenceFrame = meta.value;
25038
+ metadata.referenceFrame = referenceFrame;
25039
+ break;
25040
+ case "Output units":
25041
+ metadata.outputUnits = meta.value;
25042
+ break;
25043
+ }
24497
25044
  }
24498
25045
  if (line === "$$SOE") {
24499
25046
  inData = true;
@@ -24538,10 +25085,12 @@ var HorizonsParser = class {
24538
25085
  let observerLocation = "";
24539
25086
  for (let i = 0; i < lines.length; i++) {
24540
25087
  const line = lines[i].trim();
24541
- if (line.startsWith("Target body name:")) {
24542
- targetName = this.extractTargetName_(line, "Target body name:");
24543
- } else if (line.startsWith("Center-site name:")) {
24544
- observerLocation = this.extractValue_(line, "Center-site name:");
25088
+ const targetRaw = this.extractLabeledValue_(line, "Target body name");
25089
+ const siteRaw = this.extractLabeledValue_(line, "Center-site name");
25090
+ if (targetRaw !== null) {
25091
+ targetName = this.stripNameSuffixes_(targetRaw);
25092
+ } else if (siteRaw !== null) {
25093
+ observerLocation = siteRaw;
24545
25094
  }
24546
25095
  if (line === "$$SOE") {
24547
25096
  inData = true;
@@ -24565,23 +25114,51 @@ var HorizonsParser = class {
24565
25114
  };
24566
25115
  }
24567
25116
  // ==================== Private Helper Methods ====================
25117
+ /** Metadata labels recognized in Horizons output headers. */
25118
+ static metadataLabels_ = [
25119
+ "Target body name",
25120
+ "Center body name",
25121
+ "Center-site name",
25122
+ "Output type",
25123
+ "Reference frame",
25124
+ "Output units"
25125
+ ];
24568
25126
  /**
24569
- * Extracts a value from a key: value line.
25127
+ * Extracts a metadata value from a `Label : value` line, tolerating the
25128
+ * column-aligned padding Horizons inserts before the colon (e.g.
25129
+ * "Output units : KM-S"). Returns null if the line does not carry the
25130
+ * given label.
24570
25131
  */
24571
- static extractValue_(line, key) {
24572
- const value = line.substring(key.length).trim();
24573
- return value.split(/\s{2,}/u)[0].trim();
25132
+ static extractLabeledValue_(line, label) {
25133
+ if (!line.startsWith(label)) {
25134
+ return null;
25135
+ }
25136
+ const rest = line.substring(label.length);
25137
+ const colonIdx = rest.indexOf(":");
25138
+ if (colonIdx === -1 || rest.substring(0, colonIdx).trim().length > 0) {
25139
+ return null;
25140
+ }
25141
+ return rest.substring(colonIdx + 1).trim().split(/\s{2,}/u)[0].trim();
25142
+ }
25143
+ /** Returns the first recognized metadata label/value on the line, if any. */
25144
+ static matchMetadataLine_(line) {
25145
+ for (const label of this.metadataLabels_) {
25146
+ const value = this.extractLabeledValue_(line, label);
25147
+ if (value !== null) {
25148
+ return { label, value };
25149
+ }
25150
+ }
25151
+ return null;
24574
25152
  }
24575
25153
  /**
24576
- * Extracts the target body name, stripping parenthetical descriptors and IDs.
25154
+ * Strips parenthetical descriptors and IDs from a target body name.
24577
25155
  *
24578
25156
  * Horizons format examples:
24579
- * - "1 Ceres {source: astDys}" → "1 Ceres"
24580
- * - "Artemis II (spacecraft) (-1024) {source: ...}" → "Artemis II"
24581
- * - "Mars (499)" → "Mars"
25157
+ * - "1 Ceres" → "1 Ceres"
25158
+ * - "Artemis II (spacecraft) (-1024)" → "Artemis II"
25159
+ * - "Mars (499)" → "Mars"
24582
25160
  */
24583
- static extractTargetName_(line, key) {
24584
- const raw = this.extractValue_(line, key);
25161
+ static stripNameSuffixes_(raw) {
24585
25162
  const parenIdx = raw.search(/\s\(/u);
24586
25163
  if (parenIdx > 0) {
24587
25164
  return raw.substring(0, parenIdx).trim();
@@ -24602,7 +25179,7 @@ var HorizonsParser = class {
24602
25179
  if (isNaN(jd)) {
24603
25180
  return null;
24604
25181
  }
24605
- const epoch2 = EpochUTC.fromDateTime(julianDateToDate(jd));
25182
+ const epoch2 = tdbDateToEpochUtc(julianDateToDate(jd));
24606
25183
  const x2 = parseFloat(parts[1]);
24607
25184
  const y2 = parseFloat(parts[2]);
24608
25185
  const z2 = parseFloat(parts[3]);
@@ -24650,7 +25227,8 @@ var HorizonsParser = class {
24650
25227
  };
24651
25228
  const month = monthMap[monthStr] ?? 0;
24652
25229
  const date = new Date(Date.UTC(year, month, day, hour, minute, Math.floor(second), second % 1 * 1e3));
24653
- const epoch = EpochUTC.fromDateTime(date);
25230
+ const isUt = /\bUTC?\b/u.test(line.slice(dateMatch[0].length));
25231
+ const epoch = isUt ? EpochUTC.fromDateTime(date) : tdbDateToEpochUtc(date);
24654
25232
  let nextIndex = currentIndex + 1;
24655
25233
  if (nextIndex >= allLines.length) {
24656
25234
  return null;
@@ -26568,7 +27146,7 @@ var Transmitter = class _Transmitter extends CommunicationDevice {
26568
27146
  const distance = this.getDistanceTo(receiver, date);
26569
27147
  const fspl = calculateFspl(distance, this.frequency);
26570
27148
  const receivedPower = this.eirp - fspl + receiver.antenna.gain - receiver.lineLoss;
26571
- const noiseFloorDbw = -204 + 10 * Math.log10(this.bandwidth) + receiver.noiseFigure;
27149
+ const noiseFloorDbw = receiver.noiseFloor;
26572
27150
  const snr = receivedPower - noiseFloorDbw;
26573
27151
  return {
26574
27152
  eirp: this.eirp,
@@ -27308,6 +27886,11 @@ var ChebyshevCoefficients = class _ChebyshevCoefficients {
27308
27886
  this.cyd_ = _ChebyshevCoefficients._derivative(a, b, cy_);
27309
27887
  this.czd_ = _ChebyshevCoefficients._derivative(a, b, cz_);
27310
27888
  }
27889
+ a;
27890
+ b;
27891
+ cx_;
27892
+ cy_;
27893
+ cz_;
27311
27894
  cxd_;
27312
27895
  cyd_;
27313
27896
  czd_;
@@ -27639,6 +28222,43 @@ var PlaneChangeBurn = class _PlaneChangeBurn {
27639
28222
  }
27640
28223
  };
27641
28224
 
28225
+ // src/maneuver/PropellantBudget.ts
28226
+ var PropellantBudget = class _PropellantBudget {
28227
+ /** Standard gravity used by the specific-impulse definition (m/s^2). */
28228
+ static g0 = 9.80665;
28229
+ /**
28230
+ * Mass remaining after an impulsive burn: `m * exp(-dv / (isp * g0))`.
28231
+ * @param massKg Total mass before the burn. (kg)
28232
+ * @param deltaVMs Delta-V of the burn. (m/s)
28233
+ * @param ispS Specific impulse of the propulsion system. (s)
28234
+ * @returns Total mass after the burn. (kg)
28235
+ */
28236
+ static massAfterBurn(massKg, deltaVMs, ispS) {
28237
+ return massKg * Math.exp(-deltaVMs / (ispS * _PropellantBudget.g0));
28238
+ }
28239
+ /**
28240
+ * Propellant consumed by an impulsive burn.
28241
+ * @param massKg Total mass before the burn. (kg)
28242
+ * @param deltaVMs Delta-V of the burn. (m/s)
28243
+ * @param ispS Specific impulse of the propulsion system. (s)
28244
+ * @returns Propellant mass consumed. (kg)
28245
+ */
28246
+ static propellantUsed(massKg, deltaVMs, ispS) {
28247
+ return massKg - _PropellantBudget.massAfterBurn(massKg, deltaVMs, ispS);
28248
+ }
28249
+ /**
28250
+ * Delta-V available before the propellant is exhausted:
28251
+ * `isp * g0 * ln(m / dry)`.
28252
+ * @param massKg Current total mass. (kg)
28253
+ * @param dryMassKg Dry mass (no propellant). (kg)
28254
+ * @param ispS Specific impulse of the propulsion system. (s)
28255
+ * @returns Remaining delta-V. (m/s)
28256
+ */
28257
+ static deltaVRemaining(massKg, dryMassKg, ispS) {
28258
+ return ispS * _PropellantBudget.g0 * Math.log(massKg / dryMassKg);
28259
+ }
28260
+ };
28261
+
27642
28262
  // src/maneuver/Waypoint.ts
27643
28263
  var Waypoint = class _Waypoint {
27644
28264
  epoch;
@@ -27827,6 +28447,11 @@ var TwoBurnOrbitTransfer = class _TwoBurnOrbitTransfer {
27827
28447
  this.vTransB = vTransB;
27828
28448
  this.tTrans = tTrans;
27829
28449
  }
28450
+ vInit;
28451
+ vFinal;
28452
+ vTransA;
28453
+ vTransB;
28454
+ tTrans;
27830
28455
  /**
27831
28456
  * Calculates the parameters for a Hohmann transfer orbit between two circular orbits.
27832
28457
  * @param rInit The initial radius of the orbit. (km)
@@ -27841,6 +28466,55 @@ var TwoBurnOrbitTransfer = class _TwoBurnOrbitTransfer {
27841
28466
  const tTrans = Math.PI * Math.sqrt((rInit + rFinal) ** 3 / (8 * Earth.mu));
27842
28467
  return new _TwoBurnOrbitTransfer(vInit, vFinal, vTransA, vTransB, tTrans);
27843
28468
  }
28469
+ /**
28470
+ * Calculates a Hohmann transfer between two circular orbits with an optional
28471
+ * inclination change folded into one of the two burns as a single combined
28472
+ * impulse.
28473
+ *
28474
+ * The plane change is cheapest where the orbital velocity is lowest. For a
28475
+ * raising transfer that is the second burn (at rFinal); for a lowering
28476
+ * transfer it is the first burn (at rInit). The plane change is auto-assigned
28477
+ * to the cheaper burn and the assignment is reported in the result. A
28478
+ * combined burn costs `sqrt(v1^2 + v2^2 - 2 * v1 * v2 * cos(deltaInc))`
28479
+ * (law of cosines between the pre- and post-burn velocity vectors).
28480
+ * @param rInit The initial orbit radius. (km)
28481
+ * @param rFinal The final orbit radius. (km)
28482
+ * @param deltaIncRad The inclination change. (rad)
28483
+ * @returns Per-burn delta-V magnitudes, total, transfer time, and the plane-change assignment.
28484
+ */
28485
+ static hohmannTransferWithPlaneChange(rInit, rFinal, deltaIncRad) {
28486
+ const vInit = Math.sqrt(Earth.mu / rInit);
28487
+ const vFinal = Math.sqrt(Earth.mu / rFinal);
28488
+ const vTransInit = Math.sqrt(Earth.mu * (2 / rInit - 2 / (rInit + rFinal)));
28489
+ const vTransFinal = Math.sqrt(Earth.mu * (2 / rFinal - 2 / (rInit + rFinal)));
28490
+ const tTrans = Math.PI * Math.sqrt((rInit + rFinal) ** 3 / (8 * Earth.mu));
28491
+ const combined = (v1, v2) => Math.sqrt(v1 ** 2 + v2 ** 2 - 2 * v1 * v2 * Math.cos(deltaIncRad));
28492
+ let planeChangeBurn;
28493
+ let deltaV1;
28494
+ let deltaV2;
28495
+ if (deltaIncRad === 0) {
28496
+ planeChangeBurn = "none";
28497
+ deltaV1 = Math.abs(vTransInit - vInit);
28498
+ deltaV2 = Math.abs(vFinal - vTransFinal);
28499
+ } else if (rFinal >= rInit) {
28500
+ planeChangeBurn = "burn2";
28501
+ deltaV1 = Math.abs(vTransInit - vInit);
28502
+ deltaV2 = combined(vTransFinal, vFinal);
28503
+ } else {
28504
+ planeChangeBurn = "burn1";
28505
+ deltaV1 = combined(vInit, vTransInit);
28506
+ deltaV2 = Math.abs(vFinal - vTransFinal);
28507
+ }
28508
+ return {
28509
+ vInit,
28510
+ vFinal,
28511
+ deltaV1,
28512
+ deltaV2,
28513
+ deltaVTotal: deltaV1 + deltaV2,
28514
+ tTrans,
28515
+ planeChangeBurn
28516
+ };
28517
+ }
27844
28518
  // / Return the total delta-velocity magnitude for both maneuvers _(km/s)_.
27845
28519
  get deltaV() {
27846
28520
  return Math.abs(this.vTransA) + Math.abs(this.vTransB);
@@ -27879,6 +28553,7 @@ var ChebyshevCompressor = class _ChebyshevCompressor {
27879
28553
  constructor(interpolator_) {
27880
28554
  this.interpolator_ = interpolator_;
27881
28555
  }
28556
+ interpolator_;
27882
28557
  /** Returns the cosine of π times x. */
27883
28558
  static cosPi_(x) {
27884
28559
  return Math.cos(Math.PI * x);
@@ -28119,6 +28794,8 @@ var SimpleLinearRegression = class _SimpleLinearRegression {
28119
28794
  this.ys = ys;
28120
28795
  this.update();
28121
28796
  }
28797
+ xs;
28798
+ ys;
28122
28799
  /** Line slope (m in y = mx + b). */
28123
28800
  slope_ = 0;
28124
28801
  /** Y-axis intercept (b in y = mx + b). */
@@ -29317,8 +29994,7 @@ var LaunchTrajectoryGenerator = class _LaunchTrajectoryGenerator {
29317
29994
  azimuthRad,
29318
29995
  dr
29319
29996
  );
29320
- const gmst = epoch.gmstAngle();
29321
- const pos = _LaunchTrajectoryGenerator.geodeticToEci_(lat, lon, alt, gmst);
29997
+ const pos = _LaunchTrajectoryGenerator.geodeticToJ2000_(lat, lon, alt, epoch);
29322
29998
  positions.push({ epoch, pos });
29323
29999
  }
29324
30000
  for (let i = 0; i < positions.length; i++) {
@@ -29416,20 +30092,17 @@ var LaunchTrajectoryGenerator = class _LaunchTrajectoryGenerator {
29416
30092
  return { lat, lon };
29417
30093
  }
29418
30094
  /**
29419
- * Convert geodetic coordinates to ECI position.
29420
- * Uses spherical Earth approximation (consistent with lla2eci in transforms.ts).
30095
+ * Convert geodetic coordinates to a J2000 position.
30096
+ *
30097
+ * The point is placed on the WGS84 ellipsoid in ITRF and rotated into J2000 with
30098
+ * precession, nutation and sidereal time, so the trajectory starts on the same launch
30099
+ * site a `GroundObject` at those coordinates reports. (A spherical Earth rotated by GMST
30100
+ * alone, the earlier model, is a TEME position on the wrong surface: the pad sat ~17 km
30101
+ * off on the sphere plus up to ~40 km of precession and nutation since J2000.)
29421
30102
  */
29422
- static geodeticToEci_(latRad, lonRad, altKm, gmst) {
29423
- const cosLat = Math.cos(latRad);
29424
- const sinLat = Math.sin(latRad);
29425
- const cosLon = Math.cos(lonRad + gmst);
29426
- const sinLon = Math.sin(lonRad + gmst);
29427
- const r = Earth.radiusMean + altKm;
29428
- return new Vector3D(
29429
- r * cosLat * cosLon,
29430
- r * cosLat * sinLon,
29431
- r * sinLat
29432
- );
30103
+ static geodeticToJ2000_(latRad, lonRad, altKm, epoch) {
30104
+ const lonWrapped = Math.atan2(Math.sin(lonRad), Math.cos(lonRad));
30105
+ return new Geodetic(latRad, lonWrapped, altKm).toITRF(epoch).toJ2000().position;
29433
30106
  }
29434
30107
  /**
29435
30108
  * Compute the orbital velocity vector at the insertion point.
@@ -29550,389 +30223,174 @@ var LaunchTrajectoryGenerator = class _LaunchTrajectoryGenerator {
29550
30223
  }
29551
30224
  };
29552
30225
 
29553
- // src/orbit-design/OrbitFinder.ts
29554
- var OrbitFinder = class _OrbitFinder {
29555
- static MAX_LAT_ERROR = 0.025;
29556
- static MAX_LON_ERROR = 0.025;
29557
- static MAX_ALT_ERROR = 10;
29558
- static MAX_ITERATIONS = 5e3;
29559
- static COARSE_STEP = 1;
29560
- // degrees
29561
- static FINE_STEP = 5e-3;
29562
- // degrees
29563
- sat_;
29564
- goalParams_;
29565
- now_;
29566
- goalDirection_;
29567
- debugCallback_;
29568
- currentParams_;
29569
- lastLatitude_ = null;
29570
- currentDirection_ = null;
29571
- constructor(sat, goalLat, goalLon, goalDirection, now, goalAlt, raanOffset = 0, options) {
29572
- this.sat_ = sat;
29573
- this.now_ = now;
29574
- this.goalDirection_ = goalDirection;
29575
- this.debugCallback_ = options?.debugCallback;
29576
- this.goalParams_ = {
29577
- meanAnomaly: 0,
29578
- argOfPerigee: sat.argOfPerigee,
29579
- raan: sat.rightAscension + raanOffset,
29580
- altitude: goalAlt || 0,
29581
- latitude: goalLat,
29582
- longitude: goalLon
30226
+ // src/orbit-design/LaunchWindow.ts
30227
+ var LaunchWindowFinder = class _LaunchWindowFinder {
30228
+ options_;
30229
+ constructor(options) {
30230
+ this.options_ = {
30231
+ searchHours: 24,
30232
+ coarseStepSec: 60,
30233
+ fineStepSec: 1,
30234
+ ...options
29583
30235
  };
29584
- this.currentParams_ = this.getCurrentOrbitParams_();
29585
30236
  }
29586
30237
  /**
29587
- * Rotates the orbit to pass over the target latitude/longitude at the specified time.
29588
- * @returns A tuple of [TleLine1, TleLine2] on success, or ['Error', message] on failure.
30238
+ * Whether the site can inject directly into the desired inclination: the
30239
+ * orbit plane must reach the site's latitude, i.e. |sin(lat)| <= sin(inc).
29589
30240
  */
29590
- rotateOrbitToLatLon() {
29591
- try {
29592
- if (this.goalParams_.altitude > 0) {
29593
- const perigeeParams = this.findPerigeePosition_();
29594
- this.debug_(`Original perigee position: ${perigeeParams.latitude} - ${perigeeParams.longitude} - ${perigeeParams.altitude}`);
29595
- this.updateOrbitWithoutDirectionCheck_({
29596
- meanAnomaly: 0
29597
- });
29598
- this.debug_(`Positioned at initial perigee: ${this.currentParams_.latitude} - ${this.currentParams_.longitude} - ${this.currentParams_.altitude}`);
29599
- let bestArgPerigee = this.currentParams_.argOfPerigee;
29600
- let bestLatError = Infinity;
29601
- for (let argPer = 0; argPer < 360; argPer += 0.25) {
29602
- this.updateOrbitWithoutDirectionCheck_({
29603
- meanAnomaly: 0,
29604
- argOfPerigee: argPer
29605
- });
29606
- const latError = Math.abs(this.currentParams_.latitude - perigeeParams.latitude);
29607
- if (latError < bestLatError) {
29608
- bestLatError = latError;
29609
- bestArgPerigee = argPer;
29610
- this.debug_(`New best arg perigee: ${argPer} with lat error ${latError}`);
29611
- }
29612
- if (latError <= _OrbitFinder.MAX_LAT_ERROR) {
29613
- break;
29614
- }
29615
- }
29616
- this.updateOrbitWithoutDirectionCheck_({
29617
- meanAnomaly: 0,
29618
- argOfPerigee: bestArgPerigee
29619
- });
29620
- this.debug_(`After arg perigee adjustment: ${this.currentParams_.latitude} - ${this.currentParams_.longitude} - ${this.currentParams_.altitude}`);
29621
- this.lastLatitude_ = null;
29622
- this.currentDirection_ = null;
29623
- const finalMeanAResult = this.meanACalcLoop_(this.goalDirection_);
29624
- if (finalMeanAResult !== 1 /* Success */) {
29625
- return ["Error", "Failed to find final target position"];
29626
- }
29627
- const successfulDirection = this.currentDirection_;
29628
- const newRaan = this.linearSearchRaan_();
29629
- this.currentDirection_ = successfulDirection;
29630
- const finalSuccess = this.updateOrbit_({
29631
- meanAnomaly: this.currentParams_.meanAnomaly,
29632
- raan: newRaan,
29633
- argOfPerigee: bestArgPerigee
29634
- });
29635
- if (!finalSuccess) {
29636
- return ["Error", "Final position adjustment failed"];
29637
- }
29638
- this.debug_(`Final position: ${this.currentParams_.latitude}, ${this.currentParams_.longitude}, ${this.currentParams_.altitude}`);
29639
- this.debug_(`Target altitude: ${this.goalParams_.altitude}, Current altitude: ${this.currentParams_.altitude}`);
29640
- } else {
29641
- const result = this.meanACalcLoop_(this.goalDirection_);
29642
- if (result !== 1 /* Success */) {
29643
- return ["Error", `Failed to find solution with ${this.goalDirection_} bound direction`];
29644
- }
29645
- const meanAnomalySuccess = this.updateOrbit_({
29646
- meanAnomaly: this.currentParams_.meanAnomaly
29647
- });
29648
- if (!meanAnomalySuccess) {
29649
- return ["Error", "Mean anomaly adjustment resulted in incorrect direction"];
29650
- }
29651
- const successfulDirection = this.currentDirection_;
29652
- const newRaan = this.linearSearchRaan_();
29653
- this.currentDirection_ = successfulDirection;
29654
- const combinedSuccess = this.updateOrbit_({
29655
- meanAnomaly: this.currentParams_.meanAnomaly,
29656
- raan: newRaan
29657
- });
29658
- if (!combinedSuccess) {
29659
- return ["Error", "Combined mean anomaly and RAAN adjustment failed"];
29660
- }
29661
- }
29662
- return [
29663
- this.generateTle1_(),
29664
- this.generateTle2_(this.currentParams_)
29665
- ];
29666
- } catch (error) {
29667
- this.debug_(`Error in rotateOrbitToLatLon: ${error.message}`);
29668
- return ["Error", error.message];
29669
- }
29670
- }
29671
- debug_(message) {
29672
- if (this.debugCallback_) {
29673
- this.debugCallback_(message);
30241
+ isGeometryPossible() {
30242
+ const sinInc = Math.sin(this.options_.inclination * DEG2RAD);
30243
+ if (sinInc === 0) {
30244
+ return this.options_.siteLat === 0;
29674
30245
  }
30246
+ return Math.abs(Math.sin(this.options_.siteLat * DEG2RAD) / sinInc) <= 1;
29675
30247
  }
29676
- calculateTimeVariables_(date, satrec) {
29677
- const j = jday(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()) + date.getUTCMilliseconds() * MILLISECONDS_TO_DAYS;
29678
- const gmst = Sgp4.gstime(j);
29679
- const m = satrec ? (j - satrec.jdsatepoch) * MINUTES_PER_DAY : null;
29680
- return { gmst, m, j };
29681
- }
29682
- getCurrentOrbitParams_() {
29683
- if (!this.sat_?.satrec) {
29684
- throw new Error("Satellite data is not available");
29685
- }
29686
- const { m, gmst } = this.calculateTimeVariables_(this.now_, this.sat_.satrec);
29687
- if (m === null) {
29688
- throw new Error("Invalid time variables");
29689
- }
29690
- const positionEci = Sgp4.propagate(this.sat_.satrec, m).position;
29691
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29692
- return {
29693
- meanAnomaly: this.sat_.meanAnomaly,
29694
- argOfPerigee: this.sat_.argOfPerigee,
29695
- raan: this.sat_.rightAscension,
29696
- altitude: alt,
29697
- latitude: lat,
29698
- longitude: lon
29699
- };
30248
+ /**
30249
+ * RAAN of the (only) plane with the configured inclination that contains the
30250
+ * launch site at the given time on the configured leg. (deg, 0-360)
30251
+ */
30252
+ achievableRaan(time) {
30253
+ const inc = this.options_.inclination * DEG2RAD;
30254
+ const lat = this.options_.siteLat * DEG2RAD;
30255
+ const sinU = Math.sin(lat) / Math.sin(inc);
30256
+ const u = this.options_.direction === "N" ? Math.asin(sinU) : Math.PI - Math.asin(sinU);
30257
+ const nodeToSite = Math.atan2(Math.sin(u) * Math.cos(inc), Math.cos(u));
30258
+ const gmst = Sgp4.gstime(_LaunchWindowFinder.julianDate_(time));
30259
+ const lst = gmst + this.options_.siteLon * DEG2RAD;
30260
+ return _LaunchWindowFinder.normalizeDeg_((lst - nodeToSite) * RAD2DEG);
29700
30261
  }
29701
- determineDirection_(newLat) {
29702
- if (this.lastLatitude_ === null) {
29703
- this.debug_(`Initial latitude: ${newLat}`);
29704
- this.lastLatitude_ = newLat;
30262
+ /**
30263
+ * Scans the search window for the launch time minimizing |ΔRAAN| to the
30264
+ * target plane, then refines around the best coarse sample. Returns null
30265
+ * when the geometry is impossible (inclination below site latitude).
30266
+ */
30267
+ findBestLaunchTime() {
30268
+ if (!this.isGeometryPossible()) {
29705
30269
  return null;
29706
30270
  }
29707
- if (this.currentDirection_ && Math.abs(newLat - this.lastLatitude_) < 0.01) {
29708
- return this.currentDirection_;
29709
- }
29710
- const direction = newLat > this.lastLatitude_ ? "N" : "S";
29711
- this.debug_(`Current latitude: ${this.lastLatitude_} - New latitude: ${newLat} - Direction: ${direction}`);
29712
- this.lastLatitude_ = newLat;
29713
- this.debug_(`New direction: ${direction}`);
29714
- return direction;
29715
- }
29716
- isCorrectDirection_() {
29717
- this.debug_(`Current direction: ${this.currentDirection_} - Goal direction: ${this.goalDirection_}`);
29718
- return this.currentDirection_ === this.goalDirection_;
29719
- }
29720
- updateOrbit_(newParams) {
29721
- const tle1 = this.generateTle1_();
29722
- const tle2 = this.generateTle2_(newParams);
29723
- const satrec = Sgp4.createSatrec(tle1, tle2);
29724
- if (!satrec) {
29725
- throw new Error("Invalid orbit parameters");
29726
- }
29727
- const { m, gmst } = this.calculateTimeVariables_(this.now_, satrec);
29728
- if (m === null) {
29729
- throw new Error("Invalid time variables");
29730
- }
29731
- const positionEci = Sgp4.propagate(satrec, m).position;
29732
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29733
- const newDirection = this.determineDirection_(lat);
29734
- if (newDirection !== null) {
29735
- this.currentDirection_ = newDirection;
29736
- }
29737
- this.currentParams_ = {
29738
- ...this.currentParams_,
29739
- ...newParams,
29740
- latitude: lat,
29741
- longitude: lon,
29742
- altitude: alt
29743
- };
29744
- return this.isCorrectDirection_();
29745
- }
29746
- meanACalcLoop_(direction) {
29747
- const startVal = direction === "N" ? 0 : 180;
29748
- const endVal = direction === "N" ? 360 : 540;
29749
- for (let posVal = startVal * 10; posVal < endVal * 10; posVal += 0.25) {
29750
- const normalizedVal = posVal % (360 * 10);
29751
- const result = this.meanACalc_(normalizedVal, this.now_);
29752
- if (result === 1 /* Success */) {
29753
- if (this.currentDirection_ !== direction) {
29754
- posVal += 20;
29755
- } else {
29756
- return 1 /* Success */;
29757
- }
29758
- }
29759
- if (result === 3 /* Far */) {
29760
- posVal += 100;
29761
- }
29762
- if (result === 2 /* Error */) {
29763
- return 2 /* Error */;
29764
- }
29765
- }
29766
- return 0 /* Near */;
29767
- }
29768
- meanACalc_(meana, now) {
29769
- meana /= 10;
29770
- meana %= 360;
29771
- const tle1 = this.generateTle1_();
29772
- const tle2 = this.generateTle2_({ meanAnomaly: meana });
29773
- const satrec = Sgp4.createSatrec(tle1, tle2);
29774
- if (!satrec) {
29775
- return 2 /* Error */;
29776
- }
29777
- const { m, gmst } = this.calculateTimeVariables_(now, satrec);
29778
- if (m === null) {
29779
- return 2 /* Error */;
29780
- }
29781
- const positionEci = Sgp4.propagate(satrec, m).position;
29782
- const { lat } = eci2lla(positionEci, gmst);
29783
- if (this.lastLatitude_ !== null) {
29784
- if (Math.abs(lat - this.lastLatitude_) > 1e-3) {
29785
- this.currentDirection_ = lat > this.lastLatitude_ ? "N" : "S";
29786
- }
29787
- }
29788
- this.lastLatitude_ = lat;
29789
- if (Math.abs(lat - this.goalParams_.latitude) <= _OrbitFinder.MAX_LAT_ERROR) {
29790
- if (this.currentDirection_ === this.goalDirection_) {
29791
- this.currentParams_.meanAnomaly = meana;
29792
- return 1 /* Success */;
29793
- }
29794
- }
29795
- if (Math.abs(lat - this.goalParams_.latitude) > 11) {
29796
- return 3 /* Far */;
29797
- }
29798
- return 0 /* Near */;
29799
- }
29800
- linearSearchRaan_() {
29801
- let bestValue = this.currentParams_.raan;
30271
+ const startMs = this.options_.startTime.getTime();
30272
+ const endMs = startMs + this.options_.searchHours * 3600 * 1e3;
30273
+ const coarseMs = this.options_.coarseStepSec * 1e3;
30274
+ let bestMs = startMs;
29802
30275
  let bestError = Infinity;
29803
- for (let raan = 0; raan < 360; raan += _OrbitFinder.COARSE_STEP) {
29804
- this.updateOrbit_({ raan });
29805
- const error = Math.abs(this.calculateError_("raan"));
29806
- if (error < Math.abs(bestError)) {
30276
+ for (let ms = startMs; ms <= endMs; ms += coarseMs) {
30277
+ const error = Math.abs(this.raanErrorAt_(new Date(ms)));
30278
+ if (error < bestError) {
29807
30279
  bestError = error;
29808
- bestValue = raan;
29809
- }
29810
- if (error < _OrbitFinder.MAX_LON_ERROR) {
29811
- for (let fineRaan = bestValue - 5; fineRaan <= bestValue + 5; fineRaan += _OrbitFinder.FINE_STEP) {
29812
- const normalizedRaan = (fineRaan % 360 + 360) % 360;
29813
- this.updateOrbit_({ raan: normalizedRaan });
29814
- const fineError = Math.abs(this.calculateError_("raan"));
29815
- if (fineError < error) {
29816
- bestValue = normalizedRaan;
29817
- bestError = fineError;
29818
- }
29819
- }
29820
- return bestValue;
30280
+ bestMs = ms;
29821
30281
  }
29822
30282
  }
29823
- return bestValue;
29824
- }
29825
- normalizeAngleDifference_(angle1, angle2) {
29826
- const diff = (angle1 - angle2) % 360;
29827
- if (diff > 180) {
29828
- return diff - 360;
29829
- } else if (diff < -180) {
29830
- return diff + 360;
29831
- }
29832
- return diff;
29833
- }
29834
- calculateError_(param) {
29835
- switch (param) {
29836
- case "meanAnomaly":
29837
- return this.currentParams_.latitude - this.goalParams_.latitude;
29838
- case "raan":
29839
- return this.normalizeAngleDifference_(
29840
- this.currentParams_.longitude,
29841
- this.goalParams_.longitude
29842
- );
29843
- case "argOfPerigee":
29844
- this.debug_(`Current altitude: ${this.currentParams_.altitude} - Goal altitude: ${this.goalParams_.altitude}`);
29845
- return this.currentParams_.altitude - this.goalParams_.altitude;
29846
- default:
29847
- return 0;
29848
- }
29849
- }
29850
- updateOrbitWithoutDirectionCheck_(newParams) {
29851
- const tle1 = this.generateTle1_();
29852
- const tle2 = this.generateTle2_(newParams);
29853
- const satrec = Sgp4.createSatrec(tle1, tle2);
29854
- if (!satrec) {
29855
- throw new Error("Invalid orbit parameters");
29856
- }
29857
- const { m, gmst } = this.calculateTimeVariables_(this.now_, satrec);
29858
- if (m === null) {
29859
- throw new Error("Invalid time variables");
29860
- }
29861
- const positionEci = Sgp4.propagate(satrec, m).position;
29862
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29863
- this.currentParams_ = {
29864
- ...this.currentParams_,
29865
- ...newParams,
29866
- latitude: lat,
29867
- longitude: lon,
29868
- altitude: alt
29869
- };
29870
- }
29871
- findPerigeePosition_() {
29872
- let lowestAltitude = Infinity;
29873
- let perigeeParams = null;
29874
- for (let meanA = 0; meanA < 360; meanA += 0.05) {
29875
- const tle1 = this.generateTle1_();
29876
- const tle2 = this.generateTle2_({ meanAnomaly: meanA });
29877
- const satrec = Sgp4.createSatrec(tle1, tle2);
29878
- if (!satrec) {
29879
- continue;
29880
- }
29881
- const { m, gmst } = this.calculateTimeVariables_(this.now_, satrec);
29882
- if (m === null) {
29883
- throw new Error("Invalid time variables");
29884
- }
29885
- const positionEci = Sgp4.propagate(satrec, m).position;
29886
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29887
- if (alt < lowestAltitude) {
29888
- lowestAltitude = alt;
29889
- perigeeParams = {
29890
- meanAnomaly: meanA,
29891
- argOfPerigee: this.currentParams_.argOfPerigee,
29892
- raan: this.currentParams_.raan,
29893
- altitude: alt,
29894
- latitude: lat,
29895
- longitude: lon
29896
- };
30283
+ const fineMs = this.options_.fineStepSec * 1e3;
30284
+ const refineStart = Math.max(startMs, bestMs - coarseMs);
30285
+ const refineEnd = Math.min(endMs, bestMs + coarseMs);
30286
+ for (let ms = refineStart; ms <= refineEnd; ms += fineMs) {
30287
+ const error = Math.abs(this.raanErrorAt_(new Date(ms)));
30288
+ if (error < bestError) {
30289
+ bestError = error;
30290
+ bestMs = ms;
29897
30291
  }
29898
30292
  }
29899
- if (!perigeeParams) {
29900
- throw new Error("Failed to find perigee position");
29901
- }
29902
- this.debug_(`Found perigee at altitude ${perigeeParams.altitude} km`);
29903
- return perigeeParams;
30293
+ const time = new Date(bestMs);
30294
+ const achievableRaan = this.achievableRaan(time);
30295
+ const targetRaan = this.targetRaanAt_(time);
30296
+ return {
30297
+ time,
30298
+ achievableRaan,
30299
+ targetRaan,
30300
+ raanError: _LaunchWindowFinder.normalize180_(achievableRaan - targetRaan)
30301
+ };
29904
30302
  }
29905
30303
  /**
29906
- * Returns the 5-char satnum to embed in the synthesized TLEs.
29907
- *
29908
- * Why not {@link Satellite.sccNum}? For extended (7+ digit) IDs sccNum is
29909
- * the full 9-digit canonical value, which would push the TLE past 69 chars
29910
- * and break Sgp4.createSatrec. The input tle1 always has a valid 5-char
29911
- * satnum at columns 2-6, and SGP4 itself doesn't care what value lives
29912
- * there as long as line 1 and line 2 agree — so use that.
30304
+ * Mean RAAN of a satellite at a given time: the TLE epoch value advanced by
30305
+ * the secular nodal drift (J2). Suitable as the `targetRaan` function so the
30306
+ * target plane's regression over the search window is accounted for.
29913
30307
  */
29914
- tleSatNum_() {
29915
- return this.sat_.tle1.substring(2, 7);
30308
+ static meanRaanAt(satrec, time) {
30309
+ const minutesSinceEpoch = (_LaunchWindowFinder.julianDate_(time) - satrec.jdsatepoch) * MINUTES_PER_DAY;
30310
+ return _LaunchWindowFinder.normalizeDeg_((satrec.nodeo + satrec.nodedot * minutesSinceEpoch) * RAD2DEG);
29916
30311
  }
29917
- generateTle1_() {
29918
- return `1 ${this.tleSatNum_()}U ${this.sat_.tle1.substring(9, 17)} ${this.sat_.tle1.substring(18, 32)}${this.sat_.tle1.substring(32, 71)}`;
30312
+ targetRaanAt_(time) {
30313
+ const target = this.options_.targetRaan;
30314
+ return typeof target === "function" ? target(time) : target;
29919
30315
  }
29920
- generateTle2_(newParams) {
29921
- const mergedParams = {
29922
- ...this.currentParams_,
29923
- // Use current parameters as base
29924
- ...newParams
29925
- // Override with any provided parameters
29926
- };
29927
- const inc = this.sat_.inclination.toFixed(4).padStart(8, "0");
29928
- const raan = mergedParams.raan.toFixed(4).padStart(8, "0");
29929
- const ecc = this.sat_.eccentricity.toFixed(7).substring(2, 9);
29930
- const argPer = mergedParams.argOfPerigee.toFixed(4).padStart(8, "0");
29931
- const meanA = mergedParams.meanAnomaly.toFixed(4).padStart(8, "0");
29932
- const meanMo = this.sat_.tle2.substring(52, 63);
29933
- return `2 ${this.tleSatNum_()} ${inc} ${raan} ${ecc} ${argPer} ${meanA} ${meanMo} 10`;
30316
+ raanErrorAt_(time) {
30317
+ return _LaunchWindowFinder.normalize180_(this.achievableRaan(time) - this.targetRaanAt_(time));
30318
+ }
30319
+ static julianDate_(time) {
30320
+ return jday(time.getUTCFullYear(), time.getUTCMonth() + 1, time.getUTCDate(), time.getUTCHours(), time.getUTCMinutes(), time.getUTCSeconds()) + time.getUTCMilliseconds() * MILLISECONDS_TO_DAYS;
30321
+ }
30322
+ static normalizeDeg_(deg) {
30323
+ return (deg % 360 + 360) % 360;
30324
+ }
30325
+ static normalize180_(deg) {
30326
+ const normalized = (deg % 360 + 360) % 360;
30327
+ return normalized >= 180 ? normalized - 360 : normalized;
30328
+ }
30329
+ };
30330
+
30331
+ // src/orbit-design/GroundTrackStateVector.ts
30332
+ var WGS84_A = 6378.137;
30333
+ var WGS84_F = 1 / 298.257223563;
30334
+ var WGS84_E2 = 2 * WGS84_F - WGS84_F * WGS84_F;
30335
+ var MIN_ECCENTRICITY = 1e-3;
30336
+ var groundTrackStateVector = (opts) => {
30337
+ const { semimajorAxisKm: a, inclinationRad: inc, latRad, lonRad, gmstRad, direction } = opts;
30338
+ const e = Math.max(opts.eccentricity, MIN_ECCENTRICITY);
30339
+ const rPerigee = a * (1 - e);
30340
+ const vPerigee = Math.sqrt(Earth.mu / a * ((1 + e) / (1 - e)));
30341
+ const sinLat = Math.sin(latRad);
30342
+ const cosLat = Math.cos(latRad);
30343
+ const nPrime = WGS84_A / Math.sqrt(1 - WGS84_E2 * sinLat * sinLat);
30344
+ const mPrime = nPrime * (1 - WGS84_E2);
30345
+ const c = cosLat * cosLat;
30346
+ const s = sinLat * sinLat;
30347
+ const bCoef = c * nPrime + s * mPrime;
30348
+ const cCoef = c * nPrime * nPrime + s * mPrime * mPrime - rPerigee * rPerigee;
30349
+ const disc = bCoef * bCoef - cCoef;
30350
+ if (disc < 0) {
30351
+ return null;
30352
+ }
30353
+ const height = -bCoef + Math.sqrt(disc);
30354
+ const cosLonE = Math.cos(lonRad);
30355
+ const sinLonE = Math.sin(lonRad);
30356
+ const xEcef = (nPrime + height) * cosLat * cosLonE;
30357
+ const yEcef = (nPrime + height) * cosLat * sinLonE;
30358
+ const zEcef = (mPrime + height) * sinLat;
30359
+ const cosG = Math.cos(gmstRad);
30360
+ const sinG = Math.sin(gmstRad);
30361
+ const position = {
30362
+ x: xEcef * cosG - yEcef * sinG,
30363
+ y: xEcef * sinG + yEcef * cosG,
30364
+ z: zEcef
30365
+ };
30366
+ const rMag = Math.hypot(position.x, position.y, position.z);
30367
+ const rHat = { x: position.x / rMag, y: position.y / rMag, z: position.z / rMag };
30368
+ const geocentricLat = Math.asin(rHat.z);
30369
+ const cosGeoLat = Math.cos(geocentricLat);
30370
+ const sinAz = Math.cos(inc) / cosGeoLat;
30371
+ if (sinAz < -1 || sinAz > 1) {
30372
+ return null;
29934
30373
  }
30374
+ const cosAz = (direction === "N" ? 1 : -1) * Math.sqrt(Math.max(0, 1 - sinAz * sinAz));
30375
+ const eastMag = Math.hypot(rHat.x, rHat.y);
30376
+ const eastHat = eastMag > 1e-9 ? { x: -rHat.y / eastMag, y: rHat.x / eastMag, z: 0 } : { x: 1, y: 0, z: 0 };
30377
+ const northHat = {
30378
+ x: rHat.y * eastHat.z - rHat.z * eastHat.y,
30379
+ y: rHat.z * eastHat.x - rHat.x * eastHat.z,
30380
+ z: rHat.x * eastHat.y - rHat.y * eastHat.x
30381
+ };
30382
+ const velocity = {
30383
+ x: vPerigee * (sinAz * eastHat.x + cosAz * northHat.x),
30384
+ y: vPerigee * (sinAz * eastHat.y + cosAz * northHat.y),
30385
+ z: vPerigee * (sinAz * eastHat.z + cosAz * northHat.z)
30386
+ };
30387
+ return { position, velocity };
30388
+ };
30389
+ var semimajorAxisFromMeanMotion = (meanMotionRevPerDay) => {
30390
+ const n = meanMotionRevPerDay * 2 * Math.PI / 86400;
30391
+ return (Earth.mu / (n * n)) ** (1 / 3);
29935
30392
  };
30393
+ var perigeeSpeed = (semimajorAxisKm, eccentricity) => Math.sqrt(Earth.mu / semimajorAxisKm * ((1 + eccentricity) / (1 - eccentricity)));
29936
30394
 
29937
30395
  // src/orbit-design/RepeatGroundTrack.ts
29938
30396
  var SUN_SYNC_PRECESSION_RAD_PER_SEC = 0.9856 * DEG2RAD / secondsPerDay;
@@ -30220,6 +30678,688 @@ var RepeatGroundTrack = class _RepeatGroundTrack {
30220
30678
  return a;
30221
30679
  }
30222
30680
  };
30681
+
30682
+ // src/external/Sgp4WasmBase.ts
30683
+ var DS50_UTC_JD_EPOCH = 24332815e-1;
30684
+ var ERR_SAT_TEXT_BYTES = 512;
30685
+ var Sgp4WasmBase = class _Sgp4WasmBase {
30686
+ module_ = null;
30687
+ fs_ = null;
30688
+ bindings_ = null;
30689
+ loadPromise_ = null;
30690
+ isDynArrInitialized_ = false;
30691
+ scratchKeyPtr_ = 0;
30692
+ scratchResultPtr_ = 0;
30693
+ /**
30694
+ * Converts a UTC epoch to the Astro Standards ds50UTC time system
30695
+ * (days since 1949 December 31, 00:00 UTC).
30696
+ * @param epoch The epoch to convert.
30697
+ * @returns Days since 1950 UTC.
30698
+ */
30699
+ static toDs50Utc(epoch) {
30700
+ return epoch.toJulianDate() - DS50_UTC_JD_EPOCH;
30701
+ }
30702
+ /**
30703
+ * Whether the wasm runtime has been loaded and initialized.
30704
+ */
30705
+ get isLoaded() {
30706
+ return this.module_ !== null;
30707
+ }
30708
+ /**
30709
+ * Raw Emscripten Module escape hatch (`_malloc`, `ccall`, heap views, ...).
30710
+ * @throws {Sgp4WasmError} If the module is not loaded yet.
30711
+ */
30712
+ get module() {
30713
+ if (!this.module_) {
30714
+ throw new Sgp4WasmError(`${this.constructor.name} is not loaded. Call load() first.`);
30715
+ }
30716
+ return this.module_;
30717
+ }
30718
+ /**
30719
+ * Raw Emscripten virtual filesystem escape hatch.
30720
+ * @throws {Sgp4WasmError} If the module is not loaded yet.
30721
+ */
30722
+ get fs() {
30723
+ if (!this.fs_) {
30724
+ throw new Sgp4WasmError(`${this.constructor.name} is not loaded. Call load() first.`);
30725
+ }
30726
+ return this.fs_;
30727
+ }
30728
+ /**
30729
+ * Loads and initializes the wasm runtime. Idempotent — repeat calls return
30730
+ * the same in-flight or completed load.
30731
+ * @param options Artifact sources and stdout/stderr hooks. In Node both
30732
+ * artifacts default to files next to this module; in the browser both must
30733
+ * be provided (typically URLs to statically served copies).
30734
+ * @returns This instance, once the runtime is initialized.
30735
+ */
30736
+ load(options = {}) {
30737
+ this.loadPromise_ ??= this.loadInternal_(options).catch((err) => {
30738
+ this.loadPromise_ = null;
30739
+ throw err;
30740
+ });
30741
+ return this.loadPromise_;
30742
+ }
30743
+ /**
30744
+ * Frees the dynamic array (if initialized) and drops all references to the
30745
+ * wasm runtime so it can be garbage collected. Note Emscripten linear
30746
+ * memory cannot be truly freed; a subsequent load() evaluates a fresh,
30747
+ * isolated runtime.
30748
+ */
30749
+ dispose() {
30750
+ if (this.isDynArrInitialized_ && this.bindings_) {
30751
+ this.bindings_.freeDynArr();
30752
+ }
30753
+ if (this.module_ && this.scratchKeyPtr_) {
30754
+ this.module_._free(this.scratchKeyPtr_);
30755
+ this.module_._free(this.scratchResultPtr_);
30756
+ }
30757
+ this.scratchKeyPtr_ = 0;
30758
+ this.scratchResultPtr_ = 0;
30759
+ this.isDynArrInitialized_ = false;
30760
+ this.module_ = null;
30761
+ this.fs_ = null;
30762
+ this.bindings_ = null;
30763
+ this.loadPromise_ = null;
30764
+ }
30765
+ // TLE tree management
30766
+ /**
30767
+ * Adds a single TLE to the TLE tree (`TleAddSatFrLines_wasm`).
30768
+ * @param line1 Line 1 of the TLE.
30769
+ * @param line2 Line 2 of the TLE.
30770
+ * @returns The satKey of the added TLE.
30771
+ */
30772
+ addSat(line1, line2) {
30773
+ const satKey = this.fn_().tleAddSatFrLines(line1, line2);
30774
+ if (satKey <= 0n) {
30775
+ throw new Sgp4WasmError(`TleAddSatFrLines_wasm failed for line1 "${line1}" (code ${satKey})`, Number(satKey));
30776
+ }
30777
+ return satKey;
30778
+ }
30779
+ /**
30780
+ * Adds any number of TLEs from newline-separated text
30781
+ * (`TleAddSatsFrLines_wasm`).
30782
+ *
30783
+ * The library adds TLEs individually: entries it fails to parse come back
30784
+ * as `0n` in the returned array (positions match the input order), while
30785
+ * the rest are added normally.
30786
+ * @param tleText Newline-separated TLE lines.
30787
+ * @returns One satKey per input TLE; `0n` marks a failed entry.
30788
+ */
30789
+ addSats(tleText) {
30790
+ const m = this.module;
30791
+ const numSats = _Sgp4WasmBase.countTles_(tleText);
30792
+ if (numSats === 0) {
30793
+ return [];
30794
+ }
30795
+ const textBytes = new TextEncoder().encode(tleText);
30796
+ return this.withMalloc_(textBytes.length + 1, (textPtr) => this.withMalloc_(numSats * 8, (keysPtr) => this.withMalloc_(ERR_SAT_TEXT_BYTES, (errPtr) => {
30797
+ m.HEAPU8.set(textBytes, textPtr);
30798
+ m.HEAPU8[textPtr + textBytes.length] = 0;
30799
+ m.HEAPU8.fill(0, errPtr, errPtr + ERR_SAT_TEXT_BYTES);
30800
+ const ret = this.fn_().tleAddSatsFrLines(textPtr, keysPtr, errPtr);
30801
+ if (ret !== 0) {
30802
+ const errLine = m.UTF8ToString(errPtr, ERR_SAT_TEXT_BYTES);
30803
+ throw new Sgp4WasmError(`TleAddSatsFrLines_wasm failed (code ${ret}); offending TLE line 1: "${errLine}"`, ret);
30804
+ }
30805
+ return this.readI64Array_(keysPtr, numSats);
30806
+ })));
30807
+ }
30808
+ /**
30809
+ * Adds any number of TLEs through the Emscripten virtual filesystem
30810
+ * (`TleLoadFileVFS_wasm`). The text is written to the VFS `inFile`, and
30811
+ * the satKeys the library writes to `outFile` are read back.
30812
+ * @param tleText Newline-separated TLE lines.
30813
+ * @returns The satKeys of all added TLEs.
30814
+ */
30815
+ loadTlesVfs(tleText) {
30816
+ this.fn_();
30817
+ const vfs = this.fs;
30818
+ try {
30819
+ vfs.writeFile("inFile", tleText);
30820
+ const ret = this.fn_().tleLoadFileVfs("inFile");
30821
+ if (ret <= 0) {
30822
+ throw new Sgp4WasmError(`TleLoadFileVFS_wasm failed (code ${ret})`, ret);
30823
+ }
30824
+ const outText = vfs.readFile("outFile", { encoding: "utf8" });
30825
+ return outText.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => BigInt(line));
30826
+ } finally {
30827
+ for (const file of ["inFile", "outFile"]) {
30828
+ if (vfs.analyzePath(file).exists) {
30829
+ vfs.unlink(file);
30830
+ }
30831
+ }
30832
+ }
30833
+ }
30834
+ /**
30835
+ * Removes TLEs from the TLE tree (`TleRemoveSats_wasm`).
30836
+ * @param satKeys The satKeys to remove.
30837
+ */
30838
+ removeSats(satKeys) {
30839
+ this.runKeysWithErrSat_((keysPtr, numKeys, errPtr) => this.fn_().tleRemoveSats(keysPtr, numKeys, errPtr), satKeys, "TleRemoveSats_wasm");
30840
+ }
30841
+ // SGP4 object lifecycle
30842
+ /**
30843
+ * Initializes SGP4 objects for previously added TLEs
30844
+ * (`Sgp4InitSats_wasm`). Must be called after adding TLEs and before
30845
+ * propagating them.
30846
+ * @param satKeys The satKeys to initialize.
30847
+ */
30848
+ initSats(satKeys) {
30849
+ this.runKeysWithErrSat_((keysPtr, numKeys, errPtr) => this.fn_().sgp4InitSats(keysPtr, numKeys, errPtr), satKeys, "Sgp4InitSats_wasm");
30850
+ }
30851
+ /**
30852
+ * Removes initialized SGP4 objects (`Sgp4RemoveSats_wasm`).
30853
+ * @param satKeys The satKeys to remove.
30854
+ */
30855
+ uninitSats(satKeys) {
30856
+ this.runKeysWithErrSat_((keysPtr, numKeys, errPtr) => this.fn_().sgp4RemoveSats(keysPtr, numKeys, errPtr), satKeys, "Sgp4RemoveSats_wasm");
30857
+ }
30858
+ // Propagation by satKey
30859
+ /**
30860
+ * Propagates satKeys with geodetic output (`Sgp4Prop_wasm`).
30861
+ * @param satKeys The satKeys to propagate (must be initialized).
30862
+ * @param startTimeMse Start time in minutes since each TLE's epoch.
30863
+ * @param propsPerSat Number of propagations per satellite.
30864
+ * @param stepSizeMin Step size in minutes between propagations.
30865
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30866
+ */
30867
+ propagate(satKeys, startTimeMse, propsPerSat, stepSizeMin) {
30868
+ const buf = this.propagateKeys_(
30869
+ (...args) => this.fn_().sgp4Prop(...args),
30870
+ satKeys,
30871
+ startTimeMse,
30872
+ propsPerSat,
30873
+ stepSizeMin,
30874
+ 11,
30875
+ "Sgp4Prop_wasm"
30876
+ );
30877
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 11);
30878
+ }
30879
+ /**
30880
+ * Propagates satKeys, position/velocity only (`Sgp4PropPosVel_wasm`).
30881
+ * @param satKeys The satKeys to propagate (must be initialized).
30882
+ * @param startTimeMse Start time in minutes since each TLE's epoch.
30883
+ * @param propsPerSat Number of propagations per satellite.
30884
+ * @param stepSizeMin Step size in minutes between propagations.
30885
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30886
+ */
30887
+ propagatePosVel(satKeys, startTimeMse, propsPerSat, stepSizeMin) {
30888
+ const buf = this.propagateKeys_(
30889
+ (...args) => this.fn_().sgp4PropPosVel(...args),
30890
+ satKeys,
30891
+ startTimeMse,
30892
+ propsPerSat,
30893
+ stepSizeMin,
30894
+ 8,
30895
+ "Sgp4PropPosVel_wasm"
30896
+ );
30897
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 8);
30898
+ }
30899
+ /**
30900
+ * Propagates satKeys from an absolute time with geodetic output
30901
+ * (`Sgp4PropDs50Utc_wasm`).
30902
+ * @param satKeys The satKeys to propagate (must be initialized).
30903
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
30904
+ * @param propsPerSat Number of propagations per satellite.
30905
+ * @param stepSizeMin Step size in minutes between propagations.
30906
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30907
+ */
30908
+ propagateDs50Utc(satKeys, startTimeDs50Utc, propsPerSat, stepSizeMin) {
30909
+ const buf = this.propagateKeys_(
30910
+ (...args) => this.fn_().sgp4PropDs50Utc(...args),
30911
+ satKeys,
30912
+ startTimeDs50Utc,
30913
+ propsPerSat,
30914
+ stepSizeMin,
30915
+ 11,
30916
+ "Sgp4PropDs50Utc_wasm"
30917
+ );
30918
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 11);
30919
+ }
30920
+ /**
30921
+ * Propagates satKeys from an absolute time, position/velocity only
30922
+ * (`Sgp4PropDs50UtcPosVel_wasm`).
30923
+ * @param satKeys The satKeys to propagate (must be initialized).
30924
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
30925
+ * @param propsPerSat Number of propagations per satellite.
30926
+ * @param stepSizeMin Step size in minutes between propagations.
30927
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30928
+ */
30929
+ propagateDs50UtcPosVel(satKeys, startTimeDs50Utc, propsPerSat, stepSizeMin) {
30930
+ const buf = this.propagateKeys_(
30931
+ (...args) => this.fn_().sgp4PropDs50UtcPosVel(...args),
30932
+ satKeys,
30933
+ startTimeDs50Utc,
30934
+ propsPerSat,
30935
+ stepSizeMin,
30936
+ 8,
30937
+ "Sgp4PropDs50UtcPosVel_wasm"
30938
+ );
30939
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 8);
30940
+ }
30941
+ /**
30942
+ * Convenience single-satellite, single-step propagation.
30943
+ * @param satKey The satKey to propagate (must be initialized).
30944
+ * @param minutesSinceEpoch Minutes since the TLE epoch.
30945
+ * @returns The propagated state.
30946
+ */
30947
+ propagateOne(satKey, minutesSinceEpoch) {
30948
+ return this.propagate([satKey], minutesSinceEpoch, 1, 0)[0][0];
30949
+ }
30950
+ /**
30951
+ * Convenience single-satellite propagation to an absolute UTC epoch.
30952
+ * @param satKey The satKey to propagate (must be initialized).
30953
+ * @param epoch The UTC epoch to propagate to.
30954
+ * @returns The propagated state.
30955
+ */
30956
+ propagateEpoch(satKey, epoch) {
30957
+ return this.propagateDs50Utc([satKey], _Sgp4WasmBase.toDs50Utc(epoch), 1, 0)[0][0];
30958
+ }
30959
+ /**
30960
+ * Hot-path single-satellite propagation (`Sgp4PropPosVel_wasm`) that reuses
30961
+ * persistent scratch buffers instead of allocating per call, and returns
30962
+ * plain numbers instead of Vector3D instances. Intended for per-frame use
30963
+ * (e.g. as a drop-in backend for `Sgp4.propagate`).
30964
+ * @param satKey The satKey to propagate (must be initialized).
30965
+ * @param minutesSinceEpoch Minutes since the TLE epoch.
30966
+ * @returns Plain TEME position/velocity plus the per-record error code; a
30967
+ * nonzero function-level failure is reported as `err` too.
30968
+ */
30969
+ propagateOnePosVelFast(satKey, minutesSinceEpoch) {
30970
+ this.fn_();
30971
+ const m = this.module;
30972
+ if (!this.scratchKeyPtr_) {
30973
+ this.scratchKeyPtr_ = m._malloc(8);
30974
+ this.scratchResultPtr_ = m._malloc(8 * 8);
30975
+ }
30976
+ m.HEAP64[this.scratchKeyPtr_ >> 3] = satKey;
30977
+ const ret = m._Sgp4PropPosVel_wasm(this.scratchKeyPtr_, 1, minutesSinceEpoch, 1, 0, this.scratchResultPtr_);
30978
+ if (ret !== 0) {
30979
+ return { err: ret, position: { x: 0, y: 0, z: 0 }, velocity: { x: 0, y: 0, z: 0 } };
30980
+ }
30981
+ const heap = m.HEAPF64;
30982
+ const off = this.scratchResultPtr_ / 8;
30983
+ return {
30984
+ err: heap[off],
30985
+ position: { x: heap[off + 2], y: heap[off + 3], z: heap[off + 4] },
30986
+ velocity: { x: heap[off + 5], y: heap[off + 6], z: heap[off + 7] }
30987
+ };
30988
+ }
30989
+ // Dynamic array batch path
30990
+ /**
30991
+ * Initializes the module-global dynamic array of SGP4 objects
30992
+ * (`InitDynArr_wasm`).
30993
+ * @param initialSize Initial capacity of the array.
30994
+ */
30995
+ initDynArr(initialSize) {
30996
+ const ret = this.fn_().initDynArr(initialSize);
30997
+ if (ret !== 0) {
30998
+ throw new Sgp4WasmError(`InitDynArr_wasm failed (code ${ret}): ${SGP4_WASM_DYN_ARR_ERRORS[ret] ?? "unknown error"}`, ret);
30999
+ }
31000
+ this.isDynArrInitialized_ = true;
31001
+ }
31002
+ /**
31003
+ * Re-allocates the dynamic array to a new capacity
31004
+ * (`ReallocateDynArr_wasm`).
31005
+ * @param newSize New capacity of the array.
31006
+ */
31007
+ reallocDynArr(newSize) {
31008
+ const ret = this.fn_().reallocDynArr(newSize);
31009
+ if (ret !== 0) {
31010
+ throw new Sgp4WasmError(`ReallocateDynArr_wasm failed (code ${ret}): ${SGP4_WASM_DYN_ARR_ERRORS[ret] ?? "unknown error"}`, ret);
31011
+ }
31012
+ }
31013
+ /**
31014
+ * Frees the dynamic array (`FreeDynArr_wasm`).
31015
+ */
31016
+ freeDynArr() {
31017
+ this.fn_().freeDynArr();
31018
+ this.isDynArrInitialized_ = false;
31019
+ }
31020
+ /**
31021
+ * Current size of the dynamic array (`GetDynArrSize_wasm`).
31022
+ */
31023
+ get dynArrSize() {
31024
+ return this.fn_().getDynArrSize();
31025
+ }
31026
+ /**
31027
+ * Adds a TLE to the dynamic array (`AddSatToDynArr_wasm`).
31028
+ * @param line1 Line 1 of the TLE.
31029
+ * @param line2 Line 2 of the TLE.
31030
+ * @returns The dynamic array index of the added satellite.
31031
+ */
31032
+ addSatToDynArr(line1, line2) {
31033
+ const index = this.fn_().addSatToDynArr(line1, line2);
31034
+ if (index < 0) {
31035
+ throw new Sgp4WasmError(`AddSatToDynArr_wasm failed (code ${index}): ${SGP4_WASM_ADD_SAT_ERRORS[index] ?? "unknown error"}`, index);
31036
+ }
31037
+ return index;
31038
+ }
31039
+ /**
31040
+ * Propagates dynamic array entries, position/velocity only
31041
+ * (`Sgp4PropDynArrPosVel_wasm`).
31042
+ * @param indexes Dynamic array indices to propagate.
31043
+ * @param startTimeMse Start time in minutes since each TLE's epoch.
31044
+ * @param propsPerSat Number of propagations per satellite.
31045
+ * @param stepSizeMin Step size in minutes between propagations.
31046
+ * @returns Records indexed as `[satIndex][stepIndex]`.
31047
+ */
31048
+ propagateDynArrPosVel(indexes, startTimeMse, propsPerSat, stepSizeMin) {
31049
+ const buf = this.propagateIndexes_(
31050
+ (...args) => this.fn_().sgp4PropDynArrPosVel(...args),
31051
+ indexes,
31052
+ startTimeMse,
31053
+ propsPerSat,
31054
+ stepSizeMin,
31055
+ 8
31056
+ );
31057
+ return this.parseRecords_(buf, indexes.length, propsPerSat, 8);
31058
+ }
31059
+ /**
31060
+ * Propagates dynamic array entries from an absolute time with geodetic
31061
+ * output (`Sgp4PropDs50UtcDynArr_wasm`).
31062
+ * @param indexes Dynamic array indices to propagate.
31063
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
31064
+ * @param propsPerSat Number of propagations per satellite.
31065
+ * @param stepSizeMin Step size in minutes between propagations.
31066
+ * @returns Records indexed as `[satIndex][stepIndex]`.
31067
+ */
31068
+ propagateDs50UtcDynArr(indexes, startTimeDs50Utc, propsPerSat, stepSizeMin) {
31069
+ const buf = this.propagateIndexes_(
31070
+ (...args) => this.fn_().sgp4PropDs50UtcDynArr(...args),
31071
+ indexes,
31072
+ startTimeDs50Utc,
31073
+ propsPerSat,
31074
+ stepSizeMin,
31075
+ 11
31076
+ );
31077
+ return this.parseRecords_(buf, indexes.length, propsPerSat, 11);
31078
+ }
31079
+ /**
31080
+ * Propagates dynamic array entries from an absolute time,
31081
+ * position/velocity only (`Sgp4PropDs50UtcDynArrPosVel_wasm`).
31082
+ * @param indexes Dynamic array indices to propagate.
31083
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
31084
+ * @param propsPerSat Number of propagations per satellite.
31085
+ * @param stepSizeMin Step size in minutes between propagations.
31086
+ * @returns Records indexed as `[satIndex][stepIndex]`.
31087
+ */
31088
+ propagateDs50UtcDynArrPosVel(indexes, startTimeDs50Utc, propsPerSat, stepSizeMin) {
31089
+ const buf = this.propagateIndexes_(
31090
+ (...args) => this.fn_().sgp4PropDs50UtcDynArrPosVel(...args),
31091
+ indexes,
31092
+ startTimeDs50Utc,
31093
+ propsPerSat,
31094
+ stepSizeMin,
31095
+ 8
31096
+ );
31097
+ return this.parseRecords_(buf, indexes.length, propsPerSat, 8);
31098
+ }
31099
+ // Logging
31100
+ /**
31101
+ * Sets the wasm library's logging level (`SetLogLevel_wasm`).
31102
+ * @param level The new log level.
31103
+ */
31104
+ setLogLevel(level) {
31105
+ const ret = this.fn_().setLogLevel(level);
31106
+ if (ret !== 0) {
31107
+ throw new Sgp4WasmError(`SetLogLevel_wasm failed (code ${ret})`, ret);
31108
+ }
31109
+ }
31110
+ // Internals
31111
+ /**
31112
+ * Counts the TLEs in newline-separated text by counting line-1 rows.
31113
+ * @param tleText Newline-separated TLE lines.
31114
+ * @returns Number of TLEs.
31115
+ */
31116
+ static countTles_(tleText) {
31117
+ return tleText.split(/\r?\n/u).filter((line) => line.trim().startsWith("1 ")).length;
31118
+ }
31119
+ static isHttpUrl_(source) {
31120
+ const href = source instanceof URL ? source.href : source;
31121
+ return /^https?:\/\//u.test(href);
31122
+ }
31123
+ fn_() {
31124
+ if (!this.bindings_) {
31125
+ throw new Sgp4WasmError(`${this.constructor.name} is not loaded. Call load() first.`);
31126
+ }
31127
+ return this.bindings_;
31128
+ }
31129
+ async loadInternal_(options) {
31130
+ const isNode = typeof process !== "undefined" && Boolean(process.versions?.node);
31131
+ const glueText = await this.resolveGlueText_(options.glue, isNode);
31132
+ const wasmBinary = await this.resolveWasmBinary_(options.wasm, isNode);
31133
+ let requireFn;
31134
+ let dirname;
31135
+ if (isNode) {
31136
+ const nodeModule = await import(
31137
+ /* webpackIgnore: true */
31138
+ 'module'
31139
+ );
31140
+ requireFn = nodeModule.createRequire(import.meta.url);
31141
+ dirname = ".";
31142
+ }
31143
+ let markReady = () => void 0;
31144
+ let markFailed = () => void 0;
31145
+ const runtimeReady = new Promise((resolve, reject) => {
31146
+ markReady = resolve;
31147
+ markFailed = reject;
31148
+ });
31149
+ const seed = {
31150
+ wasmBinary,
31151
+ onRuntimeInitialized: () => markReady(),
31152
+ onAbort: (reason) => markFailed(new Sgp4WasmError(`${this.constructor.name} wasm runtime aborted: ${reason}`))
31153
+ };
31154
+ if (options.print) {
31155
+ seed.print = options.print;
31156
+ }
31157
+ if (options.printErr) {
31158
+ seed.printErr = options.printErr;
31159
+ }
31160
+ const evaluateGlue = new Function("Module", "require", "__dirname", `${glueText};
31161
+ return { Module: Module, FS: FS };`);
31162
+ const { Module, FS } = evaluateGlue(seed, requireFn, dirname);
31163
+ await runtimeReady;
31164
+ this.module_ = Module;
31165
+ this.fs_ = FS;
31166
+ this.bindings_ = _Sgp4WasmBase.buildBindings_(Module);
31167
+ return this;
31168
+ }
31169
+ static buildBindings_(m) {
31170
+ const num = "number";
31171
+ return {
31172
+ tleAddSatFrLines: m.cwrap("TleAddSatFrLines_wasm", num, ["string", "string"]),
31173
+ tleAddSatsFrLines: m.cwrap("TleAddSatsFrLines_wasm", num, [num, num, num]),
31174
+ tleLoadFileVfs: m.cwrap("TleLoadFileVFS_wasm", num, ["string"]),
31175
+ tleRemoveSats: m.cwrap("TleRemoveSats_wasm", num, [num, num, num]),
31176
+ sgp4InitSats: m.cwrap("Sgp4InitSats_wasm", num, [num, num, num]),
31177
+ sgp4RemoveSats: m.cwrap("Sgp4RemoveSats_wasm", num, [num, num, num]),
31178
+ sgp4Prop: m.cwrap("Sgp4Prop_wasm", num, [num, num, num, num, num, num]),
31179
+ sgp4PropPosVel: m.cwrap("Sgp4PropPosVel_wasm", num, [num, num, num, num, num, num]),
31180
+ sgp4PropDs50Utc: m.cwrap("Sgp4PropDs50Utc_wasm", num, [num, num, num, num, num, num]),
31181
+ sgp4PropDs50UtcPosVel: m.cwrap("Sgp4PropDs50UtcPosVel_wasm", num, [num, num, num, num, num, num]),
31182
+ initDynArr: m.cwrap("InitDynArr_wasm", num, [num]),
31183
+ reallocDynArr: m.cwrap("ReallocateDynArr_wasm", num, [num]),
31184
+ freeDynArr: m.cwrap("FreeDynArr_wasm", null, []),
31185
+ getDynArrSize: m.cwrap("GetDynArrSize_wasm", num, []),
31186
+ addSatToDynArr: m.cwrap("AddSatToDynArr_wasm", num, ["string", "string"]),
31187
+ sgp4PropDynArrPosVel: m.cwrap("Sgp4PropDynArrPosVel_wasm", null, [num, num, num, num, num, num]),
31188
+ sgp4PropDs50UtcDynArr: m.cwrap("Sgp4PropDs50UtcDynArr_wasm", null, [num, num, num, num, num, num]),
31189
+ sgp4PropDs50UtcDynArrPosVel: m.cwrap("Sgp4PropDs50UtcDynArrPosVel_wasm", null, [num, num, num, num, num, num]),
31190
+ setLogLevel: m.cwrap("SetLogLevel_wasm", num, [num])
31191
+ };
31192
+ }
31193
+ async resolveGlueText_(source, isNode) {
31194
+ if (source && typeof source === "object" && "text" in source) {
31195
+ return source.text;
31196
+ }
31197
+ const resolved = this.resolveDefaultSource_(source, this.defaultGlueFile_, isNode);
31198
+ if (isNode && !_Sgp4WasmBase.isHttpUrl_(resolved)) {
31199
+ return await this.readNodeFile_(resolved, "utf8");
31200
+ }
31201
+ const response = await fetch(resolved);
31202
+ if (!response.ok) {
31203
+ throw this.missingArtifactError_(resolved.toString(), `HTTP ${response.status}`);
31204
+ }
31205
+ return response.text();
31206
+ }
31207
+ async resolveWasmBinary_(source, isNode) {
31208
+ if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
31209
+ return source;
31210
+ }
31211
+ const resolved = this.resolveDefaultSource_(source, this.defaultWasmFile_, isNode);
31212
+ if (isNode && !_Sgp4WasmBase.isHttpUrl_(resolved)) {
31213
+ return await this.readNodeFile_(resolved, null);
31214
+ }
31215
+ const response = await fetch(resolved);
31216
+ if (!response.ok) {
31217
+ throw this.missingArtifactError_(resolved.toString(), `HTTP ${response.status}`);
31218
+ }
31219
+ return response.arrayBuffer();
31220
+ }
31221
+ resolveDefaultSource_(source, defaultFile, isNode) {
31222
+ if (source) {
31223
+ return source;
31224
+ }
31225
+ if (!isNode || typeof import.meta.url !== "string") {
31226
+ throw new Sgp4WasmError(
31227
+ `${this.constructor.name}.load() requires explicit glue/wasm sources in this environment. Serve the ${defaultFile} artifact as a static asset and pass its URL via LoadOptions.`
31228
+ );
31229
+ }
31230
+ return new URL(defaultFile, import.meta.url);
31231
+ }
31232
+ async readNodeFile_(source, encoding) {
31233
+ const fsPromises = await import(
31234
+ /* webpackIgnore: true */
31235
+ 'fs/promises'
31236
+ );
31237
+ try {
31238
+ return encoding ? await fsPromises.readFile(source, encoding) : await fsPromises.readFile(source);
31239
+ } catch (err) {
31240
+ throw this.missingArtifactError_(source.toString(), err.message);
31241
+ }
31242
+ }
31243
+ missingArtifactError_(location, cause) {
31244
+ return new Sgp4WasmError(
31245
+ `Failed to load Sgp4Prop artifact at ${location} (${cause}). The Sgp4Prop WebAssembly artifacts are part of the license-restricted USSF Astro Standards "C Sgp4Prop WebAssembly" distribution available from space-track.org. Download it and place ${this.defaultGlueFile_}/${this.defaultWasmFile_} in ootk's src/external/ directory, or pass explicit sources via LoadOptions.`
31246
+ );
31247
+ }
31248
+ // Memory marshalling
31249
+ withMalloc_(bytes, fn) {
31250
+ const m = this.module;
31251
+ const ptr = m._malloc(Math.max(bytes, 1));
31252
+ try {
31253
+ return fn(ptr);
31254
+ } finally {
31255
+ m._free(ptr);
31256
+ }
31257
+ }
31258
+ withI64Array_(values, fn) {
31259
+ return this.withMalloc_(values.length * 8, (ptr) => {
31260
+ const m = this.module;
31261
+ values.forEach((value, i) => m.setValue(ptr + i * 8, BigInt(value), "i64"));
31262
+ return fn(ptr);
31263
+ });
31264
+ }
31265
+ withI32Array_(values, fn) {
31266
+ return this.withMalloc_(values.length * 4, (ptr) => {
31267
+ const m = this.module;
31268
+ values.forEach((value, i) => m.setValue(ptr + i * 4, value, "i32"));
31269
+ return fn(ptr);
31270
+ });
31271
+ }
31272
+ readI64Array_(ptr, count) {
31273
+ const m = this.module;
31274
+ const out = [];
31275
+ for (let i = 0; i < count; i++) {
31276
+ out.push(m.getValue(ptr + i * 8, "i64"));
31277
+ }
31278
+ return out;
31279
+ }
31280
+ /**
31281
+ * Allocates a result buffer of `count` doubles, runs `fn`, and copies the
31282
+ * buffer out. The HEAPF64 view is re-read from the module after the call
31283
+ * because wasm memory growth invalidates cached views.
31284
+ */
31285
+ withF64Result_(count, fn) {
31286
+ return this.withMalloc_(count * 8, (ptr) => {
31287
+ fn(ptr);
31288
+ return this.module.HEAPF64.slice(ptr / 8, ptr / 8 + count);
31289
+ });
31290
+ }
31291
+ runKeysWithErrSat_(op, satKeys, opName) {
31292
+ this.withI64Array_(satKeys, (keysPtr) => this.withMalloc_(8, (errPtr) => {
31293
+ const m = this.module;
31294
+ m.setValue(errPtr, 0n, "i64");
31295
+ const ret = op(keysPtr, satKeys.length, errPtr);
31296
+ if (ret !== 0) {
31297
+ const errKey = m.getValue(errPtr, "i64");
31298
+ throw new Sgp4WasmError(`${opName} failed (code ${ret}); offending satKey: ${errKey}`, ret);
31299
+ }
31300
+ }));
31301
+ }
31302
+ propagateKeys_(op, satKeys, start, propsPerSat, stepMin, width, opName) {
31303
+ return this.withI64Array_(satKeys, (keysPtr) => this.withF64Result_(satKeys.length * propsPerSat * width, (resultPtr) => {
31304
+ const ret = op(keysPtr, satKeys.length, start, propsPerSat, stepMin, resultPtr);
31305
+ if (ret !== 0) {
31306
+ throw new Sgp4WasmError(`${opName} failed (code ${ret})`, ret);
31307
+ }
31308
+ }));
31309
+ }
31310
+ propagateIndexes_(op, indexes, start, propsPerSat, stepMin, width) {
31311
+ return this.withI32Array_(indexes, (idxPtr) => this.withF64Result_(indexes.length * propsPerSat * width, (resultPtr) => {
31312
+ op(idxPtr, indexes.length, start, propsPerSat, stepMin, resultPtr);
31313
+ }));
31314
+ }
31315
+ /**
31316
+ * Parses a copied-out result buffer into per-satellite, per-step records.
31317
+ * Records are 8 doubles wide (err, time, pos, vel) or 11 doubles wide
31318
+ * (plus lat, lon, height) — the distribution docs' "7 per record"
31319
+ * precondition is stale.
31320
+ */
31321
+ parseRecords_(buf, numKeys, propsPerSat, width) {
31322
+ const results = [];
31323
+ for (let satIndex = 0; satIndex < numKeys; satIndex++) {
31324
+ const satRecords = [];
31325
+ for (let stepIndex = 0; stepIndex < propsPerSat; stepIndex++) {
31326
+ const off = (satIndex * propsPerSat + stepIndex) * width;
31327
+ const record = {
31328
+ err: buf[off],
31329
+ time: buf[off + 1],
31330
+ position: new Vector3D(buf[off + 2], buf[off + 3], buf[off + 4]),
31331
+ velocity: new Vector3D(
31332
+ buf[off + 5],
31333
+ buf[off + 6],
31334
+ buf[off + 7]
31335
+ )
31336
+ };
31337
+ if (width === 11) {
31338
+ record.llh = {
31339
+ lat: buf[off + 8],
31340
+ lon: buf[off + 9],
31341
+ height: buf[off + 10]
31342
+ };
31343
+ }
31344
+ satRecords.push(record);
31345
+ }
31346
+ results.push(satRecords);
31347
+ }
31348
+ return results;
31349
+ }
31350
+ };
31351
+
31352
+ // src/external/Sgp4Wasm.ts
31353
+ var Sgp4Wasm = class extends Sgp4WasmBase {
31354
+ defaultGlueFile_ = "Sgp4Prop.js";
31355
+ defaultWasmFile_ = "Sgp4Prop.wasm";
31356
+ };
31357
+
31358
+ // src/external/Sgp4XpWasm.ts
31359
+ var Sgp4XpWasm = class extends Sgp4WasmBase {
31360
+ defaultGlueFile_ = "Sgp4Prop.xp.js";
31361
+ defaultWasmFile_ = "Sgp4Prop.xp.wasm";
31362
+ };
30223
31363
  /**
30224
31364
  * @author Theodore Kruczek
30225
31365
  * @license AGPL-3.0-or-later
@@ -30393,6 +31533,22 @@ var RepeatGroundTrack = class _RepeatGroundTrack {
30393
31533
  * You should have received a copy of the GNU Affero General Public License along with
30394
31534
  * Orbital Object ToolKit. If not, see <http://www.gnu.org/licenses/>.
30395
31535
  */
31536
+ /**
31537
+ * @author Theodore Kruczek
31538
+ * @license AGPL-3.0-or-later
31539
+ * @copyright (c) 2026 Kruczek Labs LLC
31540
+ *
31541
+ * Orbital Object ToolKit is free software: you can redistribute it and/or modify it under the
31542
+ * terms of the GNU Affero General Public License as published by the Free Software
31543
+ * Foundation, either version 3 of the License, or (at your option) any later version.
31544
+ *
31545
+ * Orbital Object ToolKit is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
31546
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
31547
+ * See the GNU Affero General Public License for more details.
31548
+ *
31549
+ * You should have received a copy of the GNU Affero General Public License along with
31550
+ * Orbital Object ToolKit. If not, see <http://www.gnu.org/licenses/>.
31551
+ */
30396
31552
  /*! Bundled license information:
30397
31553
 
30398
31554
  astronomy-engine/esm/astronomy.js:
@@ -30431,4 +31587,4 @@ astronomy-engine/esm/astronomy.js:
30431
31587
  *)
30432
31588
  */
30433
31589
 
30434
- export { AccessCalculator, AngularDiameterMethod, AngularDistanceMethod, Antenna, AtmosphericDrag, BaseObject, BatchLeastSquaresOD, BatchLeastSquaresResult, Beacon, BoxMuller, CatalogScreener, CatalogSource, CdmExporter, CdmParser, CelestialBody, CelestialBodyType, CenterBody, CenterBodyMu, ChebyshevCoefficients, ChebyshevCompressor, ChebyshevInterpolator, ClassicalElements, CommDeviceType, CommLink, CommunicationDevice, ConjunctionAssessment, ConjunctionEvent, ConstellationGenerator, ContactScheduler, ContactSelectionStrategy, CovarianceFrame, CovarianceRealism, CovarianceSample, CubicSpline, CubicSplineInterpolator, DEFAULT_INTERPOLATOR, DEFAULT_LAGRANGE_ORDER, DEG2RAD, DataHandler, DormandPrince54Propagator, DownhillSimplex, DynamicGroundObject, Earth, EarthGravity, EphemerisBody, EphemerisSatellite, Epoch, EpochGPS, EpochTAI, EpochTDB, EpochTT, EpochUTC, EpochWindow, EquinoctialElements, EulerAngles, FieldInterpolator, FieldOfView, Force, ForceModel, FormatTle, FovFrame, FovShape, Geodetic, GibbsIOD, GoldenSection, GoodingIOD, Gravity, GroundObject, GroundStation, HerrickGibbsIOD, Hill, History, HorizonsParser, ITRF, Interpolator, InterpolatorType, J2000, Jupiter, KM_PER_AU, KeplerPropagator, LagrangeInterpolator, LambertIOD, LandObject, LaserRangingSensor, LaunchTrajectoryGenerator, LevenbergMarquardtOD, LevenbergMarquardtResult, MILLISECONDS_PER_DAY, MILLISECONDS_PER_SECOND, MILLISECONDS_TO_DAYS, MINUTES_PER_DAY, MS_PER_DAY, Marker, Mars, Matrix, MechanicalRadar, Mercury, ModifiedGoodingIOD, ModulationType, Moon, MoonBody, Neptune, Observation, ObservationOptical, ObservationRadar, OdmExporter, OemParser, OmmParser, OotkError, OpticalSensor, OrbitDeterminationError, OrbitFinder, OrbitRegime, PI, ParseError, PassType, PassiveRFSensor, PayloadStatus, PhasedArrayRadar, PlaneChangeBurn, PlanetBody, Pluto, PolynomialRegression, ProbabilityOfCollision, PropagationError, Propagator, PropagatorType, Quaternion, RAD2DEG, RADIUS_OF_EARTH, RAE, RCS_VMAG_ESTIMATE_MAX, RCS_VMAG_ESTIMATE_MIN, RIC, RadarSensor, RadecGeocentric, RadecTopocentric, Random, RandomGaussianSource, Receiver, RelativeState, RepeatGroundTrack, RkCheckpoint, RkResult, RungeKutta4Propagator, RungeKutta89Propagator, RungeKuttaAdaptive, SLR_WAVELENGTHS, SPEED_OF_LIGHT_KM_S, Satellite, Saturn, ScheduledContact, ScreeningFilter, SegmentedLagrangeInterpolator, Sensor, SensorType, Sgp4, Sgp4Error, Sgp4ErrorCode, Sgp4OpsMode, Sgp4Propagator, SimpleLinearRegression, SolarRadiationPressure, SolarSystem, SpaceObject, SpaceObjectType, Star, StateCovariance, StateInterpolator, StateVector, Sun, SunBody, SunStatus, TAU, TEME, ThirdBodyGravity, Thrust, TimeStamped, Tle, Transmitter, Transponder, TwoBurnOrbitTransfer, Uranus, ValidationError, Vector, Vector3D, Venus, VerletBlendInterpolator, VisualizationHelpers, Waypoint, ZoomValue, acoth, acsch, angularDiameter, angularDistance, angularVelocityOfEarth, array2d, asec2rad, asech, azel2uv, bodyTypeLookup, boresightFrameFromAzElRoll, cKmPerMs, cKmPerSec, cMPerSec, calcGmst, calcIncFromAz, calcInertAz, calculateFspl, calculatePropagationDelay, cappedScreeningCovarianceFromTle, clamp, concat, copySign, covariance, createCovarianceFromTle, createSampleCovarianceFromTle, createVec, csch, dbmToDbw, dbwToDbm, dbwToWatts, deg2rad, derivative, dopplerFactor, earthGravityParam, ecef2eci, ecef2enu, ecef2rae, ecefRad2rae, eci2ecef, eci2lla, eci2rae, enu2rf, estimateRcs, estimateVmagFromRcs, evalPoly, factorial, gamma, getDayOfYear, getDegLat, getDegLon, getRadLat, getRadLon, halfPi, isLeapYear, jacobian, jday, linearDistance, linearInterpolate, lla2ecef, lla2eci, lla2sez, llaRad2ecef, log10, mag2db, masec2rad, matchHalfPlane, mean, msec2sec, newtonM, newtonNu, normalizeAngle, observationDerivative, observationNoiseFromSigmas, parseCenterBody, rad2deg, radecToPosition, radecToVelocity, rae2ecef, rae2eci, rae2enu, rae2raeOffBoresight, rae2ruv, rae2sez, relativeVelocity, rv2tle, scaleAndClampRicSigmas, sec2day, sec2deg, sec2min, sech, secondsPerDay, secondsPerSiderealDay, secondsPerWeek, sez2rae, sign, spaceObjType2Str, std, temp4, toPrecision, ttasec2rad, uv2azel, wattsToDbw, wrapAngle, x2o3 };
31590
+ export { AccessCalculator, AngularDiameterMethod, AngularDistanceMethod, Antenna, AtmosphericDrag, BaseObject, BatchLeastSquaresOD, BatchLeastSquaresResult, Beacon, BoxMuller, CatalogScreener, CatalogSource, CdmExporter, CdmParser, CelestialBody, CelestialBodyType, CenterBody, CenterBodyMu, ChebyshevCoefficients, ChebyshevCompressor, ChebyshevInterpolator, ClassicalElements, CommDeviceType, CommLink, CommunicationDevice, ConjunctionAssessment, ConjunctionEvent, ConstellationGenerator, ContactScheduler, ContactSelectionStrategy, CovarianceFrame, CovarianceRealism, CovarianceSample, CubicSpline, CubicSplineInterpolator, DEFAULT_INTERPOLATOR, DEFAULT_LAGRANGE_ORDER, DEG2RAD, DataHandler, DormandPrince54Propagator, DownhillSimplex, DynamicGroundObject, Earth, EarthGravity, EphemerisBody, EphemerisSatellite, Epoch, EpochGPS, EpochTAI, EpochTDB, EpochTT, EpochUTC, EpochWindow, EquinoctialElements, EulerAngles, FieldInterpolator, FieldOfView, Force, ForceModel, FormatTle, FovFrame, FovShape, Geodetic, GibbsIOD, GoldenSection, GoodingIOD, Gravity, GroundObject, GroundStation, HerrickGibbsIOD, Hill, History, HorizonsParser, ITRF, Interpolator, InterpolatorType, J2000, Jupiter, KM_PER_AU, KeplerPropagator, LagrangeInterpolator, LambertIOD, LandObject, LaserRangingSensor, LaunchTrajectoryGenerator, LaunchWindowFinder, LevenbergMarquardtOD, LevenbergMarquardtResult, MILLISECONDS_PER_DAY, MILLISECONDS_PER_SECOND, MILLISECONDS_TO_DAYS, MINUTES_PER_DAY, MS_PER_DAY, Marker, Mars, Matrix, MechanicalRadar, Mercury, ModifiedGoodingIOD, ModulationType, Moon, MoonBody, Neptune, Observation, ObservationOptical, ObservationRadar, OdmExporter, OemParser, OmmParser, OotkError, OpticalSensor, OrbitDeterminationError, OrbitRegime, PI, ParseError, PassType, PassiveRFSensor, PayloadStatus, PhasedArrayRadar, PlaneChangeBurn, PlanetBody, Pluto, PolynomialRegression, ProbabilityOfCollision, PropagationError, Propagator, PropagatorType, PropellantBudget, Quaternion, RAD2DEG, RADIUS_OF_EARTH, RAE, RCS_VMAG_ESTIMATE_MAX, RCS_VMAG_ESTIMATE_MIN, RIC, RadarSensor, RadecGeocentric, RadecTopocentric, Random, RandomGaussianSource, Receiver, RelativeState, RepeatGroundTrack, RkCheckpoint, RkResult, RungeKutta4Propagator, RungeKutta89Propagator, RungeKuttaAdaptive, SGP4_WASM_ADD_SAT_ERRORS, SGP4_WASM_DYN_ARR_ERRORS, SLR_WAVELENGTHS, SPEED_OF_LIGHT_KM_S, Satellite, Saturn, ScheduledContact, ScreeningFilter, SegmentedLagrangeInterpolator, Sensor, SensorType, Sgp4, Sgp4Error, Sgp4ErrorCode, Sgp4OpsMode, Sgp4Propagator, Sgp4Wasm, Sgp4WasmBase, Sgp4WasmError, Sgp4WasmLogLevel, Sgp4WasmPropagator, Sgp4XpWasm, SimpleLinearRegression, SolarRadiationPressure, SolarSystem, SpaceObject, SpaceObjectType, Star, StateCovariance, StateInterpolator, StateVector, Sun, SunBody, SunStatus, TAU, TEME, ThirdBodyGravity, Thrust, TimeStamped, Tle, Transmitter, Transponder, TtcAntenna, TwoBurnOrbitTransfer, Uranus, ValidationError, Vector, Vector3D, Venus, VerletBlendInterpolator, VisualizationHelpers, Waypoint, ZoomValue, acoth, acsch, angularDiameter, angularDistance, angularVelocityOfEarth, array2d, asec2rad, asech, azel2uv, bodyTypeLookup, boresightFrameFromAzElRoll, cKmPerMs, cKmPerSec, cMPerSec, calcGmst, calcIncFromAz, calcInertAz, calculateFspl, calculatePropagationDelay, cappedScreeningCovarianceFromTle, clamp, concat, copySign, covariance, createCovarianceFromTle, createSampleCovarianceFromTle, createVec, csch, dbmToDbw, dbwToDbm, dbwToWatts, deg2rad, derivative, dopplerFactor, earthGravityParam, ecef2eci, ecef2enu, ecef2rae, ecefRad2rae, eci2ecef, eci2lla, eci2rae, enu2rf, estimateRcs, estimateVmagFromRcs, evalPoly, factorial, gamma, getDayOfYear, getDegLat, getDegLon, getRadLat, getRadLon, groundTrackStateVector, halfPi, isLeapYear, jacobian, jday, linearDistance, linearInterpolate, lla2ecef, lla2eci, lla2sez, llaRad2ecef, log10, mag2db, masec2rad, matchHalfPlane, mean, msec2sec, newtonM, newtonNu, normalizeAngle, observationDerivative, observationNoiseFromSigmas, parseCenterBody, perigeeSpeed, rad2deg, radecToPosition, radecToVelocity, rae2ecef, rae2eci, rae2enu, rae2raeOffBoresight, rae2ruv, rae2sez, relativeVelocity, rv2tle, scaleAndClampRicSigmas, sec2day, sec2deg, sec2min, sech, secondsPerDay, secondsPerSiderealDay, secondsPerWeek, semimajorAxisFromMeanMotion, sez2rae, sign, spaceObjType2Str, std, temp4, toPrecision, ttasec2rad, uv2azel, wattsToDbw, wrapAngle, x2o3 };