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.cjs CHANGED
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.23_tsx@4.23.1_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
4
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
5
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
6
+
3
7
  // src/errors/index.ts
4
8
  var OotkError = class extends Error {
5
9
  constructor(message) {
@@ -23,6 +27,8 @@ var ValidationError = class extends OotkError {
23
27
  this.value = value;
24
28
  this.name = "ValidationError";
25
29
  }
30
+ field;
31
+ value;
26
32
  };
27
33
  var ParseError = class extends OotkError {
28
34
  /**
@@ -37,6 +43,8 @@ var ParseError = class extends OotkError {
37
43
  this.line = line;
38
44
  this.name = "ParseError";
39
45
  }
46
+ format;
47
+ line;
40
48
  };
41
49
  var PropagationError = class extends OotkError {
42
50
  /**
@@ -49,6 +57,7 @@ var PropagationError = class extends OotkError {
49
57
  this.epoch = epoch;
50
58
  this.name = "PropagationError";
51
59
  }
60
+ epoch;
52
61
  };
53
62
  var OrbitDeterminationError = class extends OotkError {
54
63
  /**
@@ -61,6 +70,7 @@ var OrbitDeterminationError = class extends OotkError {
61
70
  this.algorithm = algorithm;
62
71
  this.name = "OrbitDeterminationError";
63
72
  }
73
+ algorithm;
64
74
  };
65
75
 
66
76
  // src/enums/AngularDiameterMethod.ts
@@ -155,6 +165,7 @@ var SensorType = /* @__PURE__ */ ((SensorType2) => {
155
165
  SensorType2["LASER_RANGING"] = "LASER_RANGING";
156
166
  SensorType2["PASSIVE_RF"] = "PASSIVE_RF";
157
167
  SensorType2["BISTATIC_RADIO_TELESCOPE"] = "BISTATIC_RADIO_TELESCOPE";
168
+ SensorType2["TT_C_ANTENNA"] = "TT_C_ANTENNA";
158
169
  return SensorType2;
159
170
  })(SensorType || {});
160
171
 
@@ -225,6 +236,7 @@ var SpaceObjectType = /* @__PURE__ */ ((SpaceObjectType2) => {
225
236
  SpaceObjectType2[SpaceObjectType2["DWARF_PLANET"] = 36] = "DWARF_PLANET";
226
237
  SpaceObjectType2[SpaceObjectType2["MOON"] = 37] = "MOON";
227
238
  SpaceObjectType2[SpaceObjectType2["DYNAMIC_GROUND_OBJECT"] = 38] = "DYNAMIC_GROUND_OBJECT";
239
+ SpaceObjectType2[SpaceObjectType2["ASTEROID"] = 39] = "ASTEROID";
228
240
  SpaceObjectType2[SpaceObjectType2["MAX_SPACE_OBJECT_TYPE"] = 40] = "MAX_SPACE_OBJECT_TYPE";
229
241
  return SpaceObjectType2;
230
242
  })(SpaceObjectType || {});
@@ -278,6 +290,7 @@ var Epoch = class {
278
290
  throw new ValidationError("Epoch posix time must be a valid number", "posix", posix);
279
291
  }
280
292
  }
293
+ posix;
281
294
  toString() {
282
295
  return this.toDateTime().toISOString();
283
296
  }
@@ -440,7 +453,8 @@ function matchHalfPlane(angle, match) {
440
453
  return Math.abs(d1) < Math.abs(d2) ? a1 : a2;
441
454
  }
442
455
  function wrapAngle(theta) {
443
- const result = (theta + Math.PI) % (2 * Math.PI) - Math.PI;
456
+ const tau = 2 * Math.PI;
457
+ const result = ((theta + Math.PI) % tau + tau) % tau - Math.PI;
444
458
  if (result === -Math.PI) {
445
459
  return Math.PI;
446
460
  }
@@ -449,7 +463,7 @@ function wrapAngle(theta) {
449
463
  function angularDistanceCosine_(lam1, phi1, lam2, phi2) {
450
464
  const a = Math.sin(phi1) * Math.sin(phi2);
451
465
  const b = Math.cos(phi1) * Math.cos(phi2) * Math.cos(lam2 - lam1);
452
- return Math.acos(a + b);
466
+ return Math.acos(Math.min(1, Math.max(-1, a + b)));
453
467
  }
454
468
  function angularDistanceHaversine_(lam1, phi1, lam2, phi2) {
455
469
  const dlam = lam2 - lam1;
@@ -634,6 +648,7 @@ var spaceObjTypeStrMap_ = {
634
648
  [36 /* DWARF_PLANET */]: "Dwarf Planet",
635
649
  [37 /* MOON */]: "Moon",
636
650
  [38 /* DYNAMIC_GROUND_OBJECT */]: "Dynamic Ground Object",
651
+ [39 /* ASTEROID */]: "Asteroid",
637
652
  [40 /* MAX_SPACE_OBJECT_TYPE */]: "Max Space Object Type"
638
653
  };
639
654
  var spaceObjType2Str = (spaceObjType) => spaceObjTypeStrMap_[spaceObjType] || "Unknown";
@@ -1815,6 +1830,8 @@ var EpochGPS = class _EpochGPS {
1815
1830
  throw new ValidationError("GPS seconds must be between 0 and 604799", "seconds", seconds);
1816
1831
  }
1817
1832
  }
1833
+ week;
1834
+ seconds;
1818
1835
  /** Cached GPS reference epoch (1980-01-06T00:00:00.000Z) */
1819
1836
  static reference_ = null;
1820
1837
  /**
@@ -1950,6 +1967,7 @@ var Vector = class _Vector {
1950
1967
  this.elements = elements;
1951
1968
  this.length = elements.length;
1952
1969
  }
1970
+ elements;
1953
1971
  /**
1954
1972
  * The length of the vector.
1955
1973
  */
@@ -2713,6 +2731,9 @@ var Vector3D = class _Vector3D {
2713
2731
  this.y = y;
2714
2732
  this.z = z;
2715
2733
  }
2734
+ x;
2735
+ y;
2736
+ z;
2716
2737
  /**
2717
2738
  * Create a new Vector3D object from the first three elements of a Vector
2718
2739
  * object.
@@ -3220,6 +3241,131 @@ var Sgp4Error = class _Sgp4Error extends Error {
3220
3241
  }
3221
3242
  };
3222
3243
 
3244
+ // src/sgp4/sgp4-wasm-backend.ts
3245
+ var WASM_KEY_FAILED = -1n;
3246
+ var defineHidden_ = (obj, key, value) => {
3247
+ Object.defineProperty(obj, key, {
3248
+ value,
3249
+ writable: true,
3250
+ configurable: true,
3251
+ enumerable: false
3252
+ });
3253
+ };
3254
+ var backend = null;
3255
+ var satKeyCache = /* @__PURE__ */ new Map();
3256
+ var syntheticSatnumCounter = 0;
3257
+ var wasmAttachedCount = 0;
3258
+ var wasmFallbackCount = 0;
3259
+ var SYNTHETIC_SATNUM_LETTERS = "VWXYZ";
3260
+ var synthesizeSatnum_ = () => {
3261
+ const n = syntheticSatnumCounter++;
3262
+ const letterIdx = Math.floor(n / 1e4);
3263
+ if (letterIdx >= SYNTHETIC_SATNUM_LETTERS.length) {
3264
+ return null;
3265
+ }
3266
+ return `${SYNTHETIC_SATNUM_LETTERS[letterIdx]}${(n % 1e4).toString().padStart(4, "0")}`;
3267
+ };
3268
+ var tryAddWithSyntheticSatnum_ = (wasm, line1, line2) => {
3269
+ const satnumField = line1.substring(2, 7);
3270
+ const classification = line1.charAt(7);
3271
+ const isSatnumMeaningless = satnumField.trim() === "" || satnumField === "00000";
3272
+ const isNonStandardClassification = classification !== "U" && classification !== "C" && classification !== "S";
3273
+ if (!isSatnumMeaningless && !isNonStandardClassification) {
3274
+ return null;
3275
+ }
3276
+ const satnum = synthesizeSatnum_();
3277
+ if (satnum === null) {
3278
+ return null;
3279
+ }
3280
+ const syntheticLine1 = `${line1.slice(0, 2)}${satnum}U${line1.slice(8)}`;
3281
+ const syntheticLine2 = `${line2.slice(0, 2)}${satnum}${line2.slice(7)}`;
3282
+ try {
3283
+ const satKey = wasm.addSat(syntheticLine1, syntheticLine2);
3284
+ wasm.initSats([satKey]);
3285
+ return satKey;
3286
+ } catch {
3287
+ return null;
3288
+ }
3289
+ };
3290
+ var setSgp4WasmBackend = (wasm) => {
3291
+ if (!wasm.isLoaded) {
3292
+ throw new Error("Sgp4 wasm backend must be loaded before use. Call load() first.");
3293
+ }
3294
+ backend = wasm;
3295
+ };
3296
+ var clearSgp4WasmBackend = () => {
3297
+ backend = null;
3298
+ satKeyCache.clear();
3299
+ syntheticSatnumCounter = 0;
3300
+ wasmAttachedCount = 0;
3301
+ wasmFallbackCount = 0;
3302
+ };
3303
+ var getSgp4WasmBackendStats = () => ({
3304
+ attached: wasmAttachedCount,
3305
+ fallback: wasmFallbackCount
3306
+ });
3307
+ var isSgp4WasmBackendActive = () => backend !== null;
3308
+ var stashTleLines = (satrec, line1, line2) => {
3309
+ defineHidden_(satrec, "wasmTleLine1", line1);
3310
+ defineHidden_(satrec, "wasmTleLine2", line2);
3311
+ };
3312
+ var ensureSatKey_ = (wasm, satrec) => {
3313
+ const existing = satrec.wasmSatKey;
3314
+ if (existing !== void 0) {
3315
+ return existing > 0n ? existing : null;
3316
+ }
3317
+ const line1 = satrec.wasmTleLine1;
3318
+ const line2 = satrec.wasmTleLine2;
3319
+ if (!line1 || !line2) {
3320
+ defineHidden_(satrec, "wasmSatKey", WASM_KEY_FAILED);
3321
+ wasmFallbackCount++;
3322
+ return null;
3323
+ }
3324
+ const cacheKey = `${line1}
3325
+ ${line2}`;
3326
+ const cached = satKeyCache.get(cacheKey);
3327
+ if (cached !== void 0) {
3328
+ defineHidden_(satrec, "wasmSatKey", cached);
3329
+ return cached;
3330
+ }
3331
+ try {
3332
+ const satKey = wasm.addSat(line1, line2);
3333
+ wasm.initSats([satKey]);
3334
+ satKeyCache.set(cacheKey, satKey);
3335
+ defineHidden_(satrec, "wasmSatKey", satKey);
3336
+ wasmAttachedCount++;
3337
+ return satKey;
3338
+ } catch {
3339
+ const syntheticKey = tryAddWithSyntheticSatnum_(wasm, line1, line2);
3340
+ if (syntheticKey !== null) {
3341
+ satKeyCache.set(cacheKey, syntheticKey);
3342
+ defineHidden_(satrec, "wasmSatKey", syntheticKey);
3343
+ wasmAttachedCount++;
3344
+ return syntheticKey;
3345
+ }
3346
+ defineHidden_(satrec, "wasmSatKey", WASM_KEY_FAILED);
3347
+ wasmFallbackCount++;
3348
+ return null;
3349
+ }
3350
+ };
3351
+ var tryPropagateWasm = (satrec, tsince) => {
3352
+ if (!backend || satrec.init) {
3353
+ return null;
3354
+ }
3355
+ const satKey = ensureSatKey_(backend, satrec);
3356
+ if (satKey === null) {
3357
+ return null;
3358
+ }
3359
+ const result = backend.propagateOnePosVelFast(satKey, tsince);
3360
+ if (result.err !== 0) {
3361
+ return { position: false, velocity: false };
3362
+ }
3363
+ return {
3364
+ position: result.position,
3365
+ velocity: result.velocity
3366
+ };
3367
+ };
3368
+
3223
3369
  // src/sgp4/sgp4.ts
3224
3370
  var fasx2 = 0.13130908;
3225
3371
  var fasx4 = 2.8843198;
@@ -3234,6 +3380,42 @@ var stepp = 720;
3234
3380
  var stepn = -720;
3235
3381
  var step2 = 259200;
3236
3382
  var Sgp4 = class _Sgp4 {
3383
+ /**
3384
+ * Routes all subsequent `createSatrec`/`propagate` calls in this JavaScript
3385
+ * context through the USSF Astro Standards SGP4 wasm build instead of the
3386
+ * pure-TypeScript implementation. Pass a loaded `Sgp4Wasm` or `Sgp4XpWasm`
3387
+ * instance (see `src/external/`).
3388
+ *
3389
+ * satKeys are attached to satrecs lazily on first propagation, so the
3390
+ * backend can be activated before or after satrecs are created. Satrecs
3391
+ * without TLE lines (e.g. built from an OMM) and TLEs the wasm library
3392
+ * rejects fall back to the TypeScript implementation permanently.
3393
+ * @param wasm A loaded wasm SGP4 instance.
3394
+ */
3395
+ static useWasmBackend(wasm) {
3396
+ setSgp4WasmBackend(wasm);
3397
+ }
3398
+ /**
3399
+ * Restores the pure-TypeScript SGP4 implementation.
3400
+ */
3401
+ static clearWasmBackend() {
3402
+ clearSgp4WasmBackend();
3403
+ }
3404
+ /**
3405
+ * Whether propagation is currently routed through a wasm backend.
3406
+ */
3407
+ static get isWasmBackendActive() {
3408
+ return isSgp4WasmBackendActive();
3409
+ }
3410
+ /**
3411
+ * Diagnostic counters for the wasm backend: distinct TLEs attached to the
3412
+ * wasm registry vs permanently fallen back to the TypeScript
3413
+ * implementation. A growing fallback count means part of the catalog is
3414
+ * still propagating in TypeScript.
3415
+ */
3416
+ static get wasmBackendStats() {
3417
+ return getSgp4WasmBackendStats();
3418
+ }
3237
3419
  /*
3238
3420
  * -----------------------------------------------------------------------------
3239
3421
  *
@@ -3393,6 +3575,7 @@ var Sgp4 = class _Sgp4 {
3393
3575
  xno: satrec.no,
3394
3576
  xnodeo: satrec.nodeo
3395
3577
  });
3578
+ stashTleLines(satrec, tleLine1, tleLine2);
3396
3579
  return satrec;
3397
3580
  }
3398
3581
  static createSatrecFromOmm(omm, whichconst = "wgs72" /* wgs72 */, opsmode = "i" /* IMPROVED */) {
@@ -3910,6 +4093,10 @@ var Sgp4 = class _Sgp4 {
3910
4093
  *----------------------------------------------------------------------------
3911
4094
  */
3912
4095
  static propagate(satrec, tsince) {
4096
+ const wasmResult = tryPropagateWasm(satrec, tsince);
4097
+ if (wasmResult !== null) {
4098
+ return wasmResult;
4099
+ }
3913
4100
  const { xke, j2, j3oj2, vkmpersec } = satrec;
3914
4101
  satrec.t = tsince;
3915
4102
  satrec.error = 0 /* NO_ERROR */;
@@ -6320,7 +6507,7 @@ function uv2azel(u, v, coneHalfAngle) {
6320
6507
  throw new ValidationError("v must be between -1 and 1", "v", v);
6321
6508
  }
6322
6509
  const alpha = Math.asin(u);
6323
- const beta = Math.asin(v);
6510
+ const beta = Math.asin(0 - v);
6324
6511
  const az = alpha / 90 * (coneHalfAngle * RAD2DEG);
6325
6512
  const el = beta / 90 * (coneHalfAngle * RAD2DEG);
6326
6513
  return { az, el };
@@ -6354,6 +6541,8 @@ var StateCovariance = class _StateCovariance {
6354
6541
  this.matrix = matrix;
6355
6542
  this.frame = frame;
6356
6543
  }
6544
+ matrix;
6545
+ frame;
6357
6546
  // / Create a new [StateCovariance] object from 1-sigma values.
6358
6547
  static fromSigmas(sigmas, frame) {
6359
6548
  const n = sigmas.length;
@@ -6556,7 +6745,7 @@ var ClassicalElements = class _ClassicalElements {
6556
6745
  }
6557
6746
  const pos = state.position;
6558
6747
  const vel = state.velocity;
6559
- const a = state.semimajorAxis;
6748
+ const a = 1 / (2 / pos.magnitude() - vel.magnitude() ** 2 / mu);
6560
6749
  const eVecA = pos.scale(vel.magnitude() ** 2 - mu / pos.magnitude());
6561
6750
  const eVecB = vel.scale(pos.dot(vel));
6562
6751
  const eVec = eVecA.subtract(eVecB).scale(1 / mu);
@@ -6685,7 +6874,7 @@ var ClassicalElements = class _ClassicalElements {
6685
6874
  */
6686
6875
  getOrbitRegime() {
6687
6876
  const n = this.revsPerDay;
6688
- const p = this.period * sec2min;
6877
+ const p = this.period;
6689
6878
  if (n >= 0.99 && n <= 1.01 && this.eccentricity < 0.01) {
6690
6879
  return "Geosynchronous Orbit" /* GEO */;
6691
6880
  }
@@ -6753,13 +6942,14 @@ var ClassicalElements = class _ClassicalElements {
6753
6942
  let maInit = eaInit - this.eccentricity * Math.sin(eaInit);
6754
6943
  maInit = matchHalfPlane(maInit, eaInit);
6755
6944
  const maFinal = (maInit + n * delta) % TAU;
6756
- let eaFinal = maFinal;
6757
- for (let iter = 0; iter < 32; iter++) {
6758
- const eaTemp = maFinal + this.eccentricity * Math.sin(eaFinal);
6759
- if (Math.abs(eaTemp - eaFinal) < 1e-12) {
6945
+ const ecc = this.eccentricity;
6946
+ let eaFinal = ecc > 0.8 ? Math.PI * Math.sign(maFinal || 1) : maFinal;
6947
+ for (let iter = 0; iter < 50; iter++) {
6948
+ const step = (eaFinal - ecc * Math.sin(eaFinal) - maFinal) / (1 - ecc * Math.cos(eaFinal));
6949
+ eaFinal -= step;
6950
+ if (Math.abs(step) < 1e-12) {
6760
6951
  break;
6761
6952
  }
6762
- eaFinal = eaTemp;
6763
6953
  }
6764
6954
  const cosEaFinal = Math.cos(eaFinal);
6765
6955
  let vFinal = Math.acos(clamp((cosEaFinal - this.eccentricity) / (1 - this.eccentricity * cosEaFinal), -1, 1));
@@ -7189,13 +7379,21 @@ var GroundObject = class extends BaseObject {
7189
7379
  }
7190
7380
  /**
7191
7381
  * Calculates the Earth-Centered Inertial (ECI) position vector of the ground object at a given date.
7192
- * @variation optimized version of this.toGeodetic().toITRF().toJ2000().position;
7382
+ *
7383
+ * Uses the WGS84 ellipsoid (the same {@link llaRad2ecef} position that `ecef()` and
7384
+ * `Satellite.rae()` use), rotated into TEME by GMST. It deliberately does not use the
7385
+ * spherical-Earth `lla2eci()`: at mid latitudes that puts the observer ~20 km away from
7386
+ * its WGS84 position, which skewed `Satellite.dopplerFactor()` by up to ~0.2 km/s of range
7387
+ * rate near closest approach.
7388
+ *
7389
+ * The result is in TEME (GMST rotation only), the frame `Satellite.eci()` returns. For J2000
7390
+ * use {@link toJ2000}, which also applies precession and nutation.
7193
7391
  * @param date The date for which to calculate the ECI position vector. Defaults to the current date.
7194
7392
  * @returns The ECI position vector of the ground object.
7195
7393
  */
7196
7394
  eci(date = /* @__PURE__ */ new Date()) {
7197
7395
  const { gmst } = calcGmst(date);
7198
- return lla2eci(this.toGeodetic(), gmst);
7396
+ return ecef2eci(llaRad2ecef(this.llaRad()), gmst);
7199
7397
  }
7200
7398
  /**
7201
7399
  * Returns the latitude, longitude, and altitude of the GroundObject.
@@ -7236,18 +7434,16 @@ var GroundObject = class extends BaseObject {
7236
7434
  }
7237
7435
  /**
7238
7436
  * Converts the ground position to J2000 inertial coordinates.
7239
- * Ground objects have zero velocity in the inertial frame (ignoring Earth rotation).
7437
+ *
7438
+ * The site is placed on the WGS84 ellipsoid in ITRF and rotated into J2000 with the full
7439
+ * precession, nutation and sidereal-time chain ({@link ITRF.toJ2000}), so it is in the same
7440
+ * frame as `Satellite.toJ2000()`. The velocity is the Earth-rotation velocity (omega x r,
7441
+ * ~0.46 km/s at the equator): a ground site is fixed in ITRF, not in inertial space.
7240
7442
  * @param date - The date for the conversion (defaults to now)
7241
7443
  * @returns J2000 state vector
7242
7444
  */
7243
7445
  toJ2000(date = /* @__PURE__ */ new Date()) {
7244
- const { gmst } = calcGmst(date);
7245
- const position = lla2eci(this.llaRad(), gmst);
7246
- return new J2000(
7247
- EpochUTC.fromDateTime(date),
7248
- new Vector3D(position.x, position.y, position.z),
7249
- new Vector3D(0, 0, 0)
7250
- );
7446
+ return this.toGeodetic().toITRF(EpochUTC.fromDateTime(date)).toJ2000();
7251
7447
  }
7252
7448
  // ==================== Component Management ====================
7253
7449
  /**
@@ -7794,7 +7990,7 @@ var TEME = class _TEME extends StateVector {
7794
7990
  }
7795
7991
  };
7796
7992
 
7797
- // node_modules/astronomy-engine/esm/astronomy.js
7993
+ // node_modules/.pnpm/astronomy-engine@2.1.19/node_modules/astronomy-engine/esm/astronomy.js
7798
7994
  var C_AUDAY = 173.1446326846693;
7799
7995
  var KM_PER_AU2 = 14959787069098932e-8;
7800
7996
  var DEG2RAD2 = 0.017453292519943295;
@@ -9861,7 +10057,7 @@ function CalcPluto(time, helio) {
9861
10057
  r = ra.mul(1 - ramp).add(rb.mul(ramp));
9862
10058
  v = va.mul(1 - ramp).add(vb.mul(ramp));
9863
10059
  }
9864
- if (helio) {
10060
+ {
9865
10061
  if (!bary)
9866
10062
  bary = new major_bodies_t(time.tt);
9867
10063
  r = r.sub(bary.Sun.r);
@@ -9874,7 +10070,7 @@ function HelioVector(body, date) {
9874
10070
  if (body in vsop)
9875
10071
  return CalcVsop(vsop[body], time);
9876
10072
  if (body === Body.Pluto) {
9877
- const p = CalcPluto(time, true);
10073
+ const p = CalcPluto(time);
9878
10074
  return new Vector2(p.x, p.y, p.z, time);
9879
10075
  }
9880
10076
  if (body === Body.Sun)
@@ -9970,38 +10166,6 @@ function GeoVector(body, date, aberration) {
9970
10166
  function ExportState(terse, time) {
9971
10167
  return new StateVector2(terse.r.x, terse.r.y, terse.r.z, terse.v.x, terse.v.y, terse.v.z, time);
9972
10168
  }
9973
- function BaryState(body, date) {
9974
- const time = MakeTime(date);
9975
- if (body === Body.SSB) {
9976
- return new StateVector2(0, 0, 0, 0, 0, 0, time);
9977
- }
9978
- if (body === Body.Pluto) {
9979
- return CalcPluto(time, false);
9980
- }
9981
- const bary = new major_bodies_t(time.tt);
9982
- switch (body) {
9983
- case Body.Sun:
9984
- return ExportState(bary.Sun, time);
9985
- case Body.Jupiter:
9986
- return ExportState(bary.Jupiter, time);
9987
- case Body.Saturn:
9988
- return ExportState(bary.Saturn, time);
9989
- case Body.Uranus:
9990
- return ExportState(bary.Uranus, time);
9991
- case Body.Neptune:
9992
- return ExportState(bary.Neptune, time);
9993
- case Body.Moon:
9994
- case Body.EMB:
9995
- const earth = CalcVsopPosVel(vsop[Body.Earth], time.tt);
9996
- const state = body === Body.Moon ? GeoMoonState(time) : GeoEmbState(time);
9997
- 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);
9998
- }
9999
- if (body in vsop) {
10000
- const planet = CalcVsopPosVel(vsop[body], time.tt);
10001
- 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);
10002
- }
10003
- throw `BaryState: Unsupported body "${body}"`;
10004
- }
10005
10169
  function HelioState(body, date) {
10006
10170
  const time = MakeTime(date);
10007
10171
  switch (body) {
@@ -10021,7 +10185,7 @@ function HelioState(body, date) {
10021
10185
  const planet = CalcVsopPosVel(vsop[body], time.tt);
10022
10186
  return ExportState(planet, time);
10023
10187
  case Body.Pluto:
10024
- return CalcPluto(time, true);
10188
+ return CalcPluto(time);
10025
10189
  case Body.Moon:
10026
10190
  case Body.EMB:
10027
10191
  const earth = CalcVsopPosVel(vsop.Earth, time.tt);
@@ -10342,6 +10506,11 @@ function SearchRiseSet(body, observer, direction, dateStart, limitDays, metersAb
10342
10506
  const altitude = dip - REFRACTION_NEAR_HORIZON * atmos.density;
10343
10507
  return InternalSearchAltitude(body, observer, direction, dateStart, limitDays, body_radius_au, altitude);
10344
10508
  }
10509
+ function SearchAltitude(body, observer, direction, dateStart, limitDays, altitude) {
10510
+ if (!Number.isFinite(altitude) || altitude < -90 || altitude > 90)
10511
+ throw `Invalid altitude angle: ${altitude}`;
10512
+ return InternalSearchAltitude(body, observer, direction, dateStart, limitDays, 0, altitude);
10513
+ }
10345
10514
  var AscentInfo = class {
10346
10515
  constructor(tx, ty, ax, ay) {
10347
10516
  this.tx = tx;
@@ -10604,7 +10773,9 @@ var CelestialBody = class extends BaseObject {
10604
10773
  * Gets rise, transit, and set times for this body as seen from a ground location.
10605
10774
  * @param observer - The ground observer location
10606
10775
  * @param date - The starting date for the search
10607
- * @param minElevation - Minimum elevation angle in degrees (default 0)
10776
+ * @param minElevation - Elevation threshold in degrees (default 0). At 0 the times are the
10777
+ * usual rise/set (upper limb on the horizon, with refraction); otherwise they are when the
10778
+ * body's centre crosses that geometric elevation.
10608
10779
  * @returns Rise, transit, and set times (null if body doesn't rise/set)
10609
10780
  */
10610
10781
  getRiseSetTimes(observer, date = /* @__PURE__ */ new Date(), minElevation = 0) {
@@ -10613,8 +10784,10 @@ var CelestialBody = class extends BaseObject {
10613
10784
  }
10614
10785
  const obs = new Observer(observer.lat, observer.lon, observer.alt * 1e3);
10615
10786
  const time = MakeTime(date);
10616
- const riseTime = SearchRiseSet(this.astronomyBody_, obs, 1, time, 1, minElevation);
10617
- const setTime = SearchRiseSet(this.astronomyBody_, obs, -1, time, 1, minElevation);
10787
+ const body = this.astronomyBody_;
10788
+ const search = (direction) => minElevation === 0 ? SearchRiseSet(body, obs, direction, time, 1) : SearchAltitude(body, obs, direction, time, 1, minElevation);
10789
+ const riseTime = search(1);
10790
+ const setTime = search(-1);
10618
10791
  let transitTime = null;
10619
10792
  if (riseTime && setTime) {
10620
10793
  const riseMs = riseTime.date.getTime();
@@ -10633,14 +10806,17 @@ var CelestialBody = class extends BaseObject {
10633
10806
  }
10634
10807
  /**
10635
10808
  * Calculates the angular diameter of this body as seen from a given position.
10636
- * @param observerPos - The observer's position in km
10809
+ * @param observerPos - The observer's position in km (J2000, at `date`)
10810
+ * @param date - The time of `observerPos` (defaults to now). The body is placed at this
10811
+ * time; without it a past or future observer position was paired with the body's
10812
+ * position now.
10637
10813
  * @returns Angular diameter in radians
10638
10814
  */
10639
- getAngularDiameter(observerPos) {
10815
+ getAngularDiameter(observerPos, date = /* @__PURE__ */ new Date()) {
10640
10816
  if (!this.radius) {
10641
10817
  throw new Error(`${this.name} does not have a defined radius`);
10642
10818
  }
10643
- const distance = observerPos.subtract(this.eci()).magnitude();
10819
+ const distance = observerPos.subtract(this.eci(date)).magnitude();
10644
10820
  return 2 * Math.atan(this.radius / distance);
10645
10821
  }
10646
10822
  /**
@@ -10781,16 +10957,16 @@ var SunBody = class _SunBody extends CelestialBody {
10781
10957
  return this.geoVectorToKm(Body.Sun, date);
10782
10958
  }
10783
10959
  /**
10784
- * Gets the Sun's apparent position (corrected for light travel time).
10960
+ * Gets the Sun's apparent position (corrected for light travel time and aberration).
10961
+ *
10962
+ * Same as {@link eci}: astronomy-engine's `GeoVector` already applies the light-time and
10963
+ * aberration corrections. (This used to roll the time back by another ~499 s, putting the
10964
+ * Sun a further ~20 arcsec along its path.)
10785
10965
  * @param date - The date/time for the position calculation
10786
- * @returns Position vector in kilometers
10966
+ * @returns Position vector in kilometers (J2000)
10787
10967
  */
10788
10968
  eciApparent(date = /* @__PURE__ */ new Date()) {
10789
- const pos = this.eci(date);
10790
- const distance = pos.magnitude();
10791
- const lightTimeSec = distance / cKmPerSec;
10792
- const adjustedDate = new Date(date.getTime() - lightTimeSec * 1e3);
10793
- return this.eci(adjustedDate);
10969
+ return this.eci(date);
10794
10970
  }
10795
10971
  /**
10796
10972
  * Gets the Sun's position in heliocentric coordinates.
@@ -10855,8 +11031,13 @@ var SunBody = class _SunBody extends CelestialBody {
10855
11031
  // ==================== Satellite Shadow Methods ====================
10856
11032
  /**
10857
11033
  * Determines if a satellite is in Earth's shadow.
11034
+ *
11035
+ * The test is a cylinder-plus-cone that tracks the umbra boundary closely (within ~0.01 deg
11036
+ * of the `lightingRatio` umbra edge in LEO); a satellite in the penumbra counts as lit. Use
11037
+ * {@link lightingRatio} when partial illumination matters.
10858
11038
  * @param epoch - The epoch for the calculation
10859
- * @param satPos - The satellite's ECI position in kilometers
11039
+ * @param satPos - The satellite's position in J2000 (the frame of `eci()`), km; a TEME
11040
+ * position from `Satellite.eci()` is ~0.3 deg off in 2026
10860
11041
  * @example
10861
11042
  * ```typescript
10862
11043
  * import { Sun, Satellite, EpochUTC, Vector3D, Kilometers, Seconds } from 'ootk';
@@ -10865,14 +11046,8 @@ var SunBody = class _SunBody extends CelestialBody {
10865
11046
  * const now = new Date();
10866
11047
  * const epoch = new EpochUTC((now.getTime() / 1000) as Seconds);
10867
11048
  *
10868
- * const pv = satellite.eci(now);
10869
- * if (pv) {
10870
- * const satPos = new Vector3D<Kilometers>(
10871
- * pv.position.x as Kilometers,
10872
- * pv.position.y as Kilometers,
10873
- * pv.position.z as Kilometers
10874
- * );
10875
- *
11049
+ * const satPos = satellite.toJ2000(now).position; // J2000, like Sun.eci()
11050
+ * {
10876
11051
  * const inShadow = Sun.shadow(epoch, satPos);
10877
11052
  * console.log(inShadow ? 'Satellite is in eclipse' : 'Satellite is sunlit');
10878
11053
  *
@@ -10948,11 +11123,12 @@ var SunBody = class _SunBody extends CelestialBody {
10948
11123
  }
10949
11124
  /**
10950
11125
  * Calculates the Sun's angular diameter as seen from a position.
10951
- * @param obsPos - Observer position in kilometers
11126
+ * @param obsPos - Observer position in kilometers (J2000, at `date`)
11127
+ * @param date - The time of `obsPos` (defaults to now)
10952
11128
  * @returns Angular diameter in radians
10953
11129
  */
10954
- diameter(obsPos) {
10955
- const sunPos = this.eci();
11130
+ diameter(obsPos, date = /* @__PURE__ */ new Date()) {
11131
+ const sunPos = this.eci(date);
10956
11132
  const distance = obsPos.subtract(sunPos).magnitude();
10957
11133
  return 2 * Math.asin(_SunBody.RADIUS / distance);
10958
11134
  }
@@ -10997,7 +11173,7 @@ var SunBody = class _SunBody extends CelestialBody {
10997
11173
  if (isNaN(lon)) {
10998
11174
  throw new Error("longitude missing");
10999
11175
  }
11000
- const date = dateVal instanceof Date ? dateVal : new Date(dateVal);
11176
+ const date = new Date(dateVal instanceof Date ? dateVal.getTime() : dateVal);
11001
11177
  if (isUtc) {
11002
11178
  date.setUTCHours(12, 0, 0, 0);
11003
11179
  } else {
@@ -11021,15 +11197,19 @@ var SunBody = class _SunBody extends CelestialBody {
11021
11197
  }
11022
11198
  /**
11023
11199
  * Gets sunrise and sunset times using astronomy-engine.
11200
+ *
11201
+ * The times are when the Sun's centre crosses `minElevation` (geometric altitude). The
11202
+ * default -0.833 deg is the standard sunrise/sunset definition (refraction plus the solar
11203
+ * semidiameter); -6, -12 and -18 give civil, nautical and astronomical twilight.
11024
11204
  * @param observer - Ground observer location
11025
11205
  * @param date - The date for calculation
11026
- * @param minElevation - Minimum elevation in degrees (default -0.833 for standard sunrise)
11206
+ * @param minElevation - Elevation of the Sun's centre in degrees (default -0.833 for standard sunrise)
11027
11207
  */
11028
11208
  getSunriseSunset(observer, date = /* @__PURE__ */ new Date(), minElevation = -0.833) {
11029
11209
  const obs = new Observer(observer.lat, observer.lon, observer.alt * 1e3);
11030
11210
  const time = MakeTime(date);
11031
- const sunrise = SearchRiseSet(Body.Sun, obs, 1, time, 1, minElevation);
11032
- const sunset = SearchRiseSet(Body.Sun, obs, -1, time, 1, minElevation);
11211
+ const sunrise = SearchAltitude(Body.Sun, obs, 1, time, 1, minElevation);
11212
+ const sunset = SearchAltitude(Body.Sun, obs, -1, time, 1, minElevation);
11033
11213
  return {
11034
11214
  sunrise: sunrise?.date ?? null,
11035
11215
  sunset: sunset?.date ?? null
@@ -11308,6 +11488,9 @@ var SolarRadiationPressure = class _SolarRadiationPressure extends Force {
11308
11488
  this.area = area;
11309
11489
  this.reflectCoeff = reflectCoeff;
11310
11490
  }
11491
+ mass;
11492
+ area;
11493
+ reflectCoeff;
11311
11494
  // / Solar pressure _(N/m²)_;
11312
11495
  static _kRef = 456e-8 * KM_PER_AU ** 2;
11313
11496
  acceleration(state) {
@@ -11542,7 +11725,7 @@ var MoonBody = class _MoonBody extends CelestialBody {
11542
11725
  if (!phase) {
11543
11726
  phase = _MoonBody.moonCycles_[0];
11544
11727
  }
11545
- let type = "";
11728
+ let type;
11546
11729
  if (next === nextNewMoon) {
11547
11730
  type = "newMoon";
11548
11731
  } else if (next === nextFirstQuarter) {
@@ -11690,6 +11873,8 @@ var ThirdBodyGravity = class _ThirdBodyGravity {
11690
11873
  this.moon = moon;
11691
11874
  this.sun = sun;
11692
11875
  }
11876
+ moon;
11877
+ sun;
11693
11878
  static moonGravity_(state) {
11694
11879
  const rMoon = Moon.eci(state.epoch.toDateTime());
11695
11880
  const aNum = rMoon.subtract(state.position);
@@ -12096,29 +12281,40 @@ var FormatTle = class _FormatTle {
12096
12281
  * Format a value in TLE exponential notation for BSTAR or mean motion ddot.
12097
12282
  * Format: "sNNNNN±N" (8 chars) where mantissa has implied leading decimal point.
12098
12283
  * Example: 0.00017507 → " 17507-3" (i.e., .17507 × 10^-3)
12284
+ *
12285
+ * The result is ALWAYS exactly 8 characters. The field has room for a 5-digit
12286
+ * mantissa and a single-digit exponent, so values needing more are folded into
12287
+ * what the format can express: magnitudes below 1e-10 become zero (the usual
12288
+ * convention for a drag term too small to write), and the 1e9-and-up case is
12289
+ * saturated. Anything longer would push every later column of TLE line 1 to the
12290
+ * right, which silently corrupts ephemeris type, element number and checksum.
12099
12291
  * @param value - The value to format
12100
12292
  * @returns Formatted 8-character string
12101
12293
  */
12102
12294
  static formatTleExponential(value) {
12103
- if (value === 0) {
12295
+ if (value === 0 || !Number.isFinite(value)) {
12104
12296
  return " 00000+0";
12105
12297
  }
12106
12298
  const sign2 = value >= 0 ? " " : "-";
12107
12299
  const absVal = Math.abs(value);
12108
12300
  let exponent = Math.floor(Math.log10(absVal)) + 1;
12109
- let mantissa = absVal / 10 ** exponent;
12110
- if (mantissa >= 1) {
12111
- mantissa /= 10;
12301
+ let mantissa = Math.round(absVal / 10 ** exponent * 1e5);
12302
+ while (mantissa < 1e4) {
12303
+ exponent--;
12304
+ mantissa = Math.round(absVal / 10 ** exponent * 1e5);
12305
+ }
12306
+ while (mantissa >= 1e5) {
12112
12307
  exponent++;
12308
+ mantissa = Math.round(absVal / 10 ** exponent * 1e5);
12113
12309
  }
12114
- if (mantissa < 0.1 && mantissa > 0) {
12115
- mantissa *= 10;
12116
- exponent--;
12310
+ if (exponent < -9) {
12311
+ return " 00000+0";
12312
+ }
12313
+ if (exponent > 9) {
12314
+ return `${sign2}99999+9`;
12117
12315
  }
12118
- const mantissaStr = Math.round(mantissa * 1e5).toString().padStart(5, "0");
12119
12316
  const expSign = exponent >= 0 ? "+" : "-";
12120
- const expStr = Math.abs(exponent).toString();
12121
- return sign2 + mantissaStr + expSign + expStr;
12317
+ return sign2 + mantissa.toString() + expSign + Math.abs(exponent).toString();
12122
12318
  }
12123
12319
  /**
12124
12320
  * Compute TLE line checksum (modulo 10 sum of digits, '-' counts as 1).
@@ -12295,20 +12491,8 @@ ${this.line2}`;
12295
12491
  }
12296
12492
  static calcElsetAge(tle1, nowInput, outputUnits = "days") {
12297
12493
  nowInput ??= /* @__PURE__ */ new Date();
12298
- const currentYearFull = nowInput.getUTCFullYear();
12299
- const currentYearShort = currentYearFull % 100;
12300
- const epochYearShort = parseInt(tle1.substring(18, 20), 10);
12301
- const epochDayOfYear = parseFloat(tle1.substring(20, 32));
12302
- let epochYearFull;
12303
- if (epochYearShort <= currentYearShort) {
12304
- epochYearFull = 2e3 + epochYearShort;
12305
- } else {
12306
- epochYearFull = 1900 + epochYearShort;
12307
- }
12308
- const epochJday = epochDayOfYear + epochYearFull * 365;
12309
- const currentJday = getDayOfYear(nowInput) + currentYearFull * 365;
12310
- const currentTime = (nowInput.getUTCHours() * 3600 + nowInput.getUTCMinutes() * 60 + nowInput.getUTCSeconds()) / 86400;
12311
- const daysOld = currentJday + currentTime - epochJday;
12494
+ const epochMs = _Tle.parseEpoch_(tle1.substring(18, 32)).toDateTime().getTime();
12495
+ const daysOld = (nowInput.getTime() - epochMs) / 864e5;
12312
12496
  switch (outputUnits) {
12313
12497
  case "hours":
12314
12498
  return daysOld * 24;
@@ -12991,6 +13175,8 @@ var RkCheckpoint = class {
12991
13175
  this.cacheState = cacheState;
12992
13176
  this.stepSize = stepSize;
12993
13177
  }
13178
+ cacheState;
13179
+ stepSize;
12994
13180
  };
12995
13181
 
12996
13182
  // src/propagator/RkResult.ts
@@ -13001,6 +13187,9 @@ var RkResult = class {
13001
13187
  this.error = error;
13002
13188
  this.newStep = newStep;
13003
13189
  }
13190
+ state;
13191
+ error;
13192
+ newStep;
13004
13193
  };
13005
13194
 
13006
13195
  // src/interpolator/CubicSpline.ts
@@ -13014,6 +13203,12 @@ var CubicSpline = class {
13014
13203
  this.p1 = p1;
13015
13204
  this.m1 = m1;
13016
13205
  }
13206
+ t0;
13207
+ p0;
13208
+ m0;
13209
+ t1;
13210
+ p1;
13211
+ m1;
13017
13212
  // / Interpolate position at the provided time [t] _(POSIX seconds)_.
13018
13213
  position_(t) {
13019
13214
  const t2 = t * t;
@@ -13052,9 +13247,8 @@ var Interpolator = class {
13052
13247
  * cached value range.
13053
13248
  */
13054
13249
  inWindow(epoch) {
13055
- const start = this.window().start;
13056
- const stop = this.window().end;
13057
- return start <= epoch && epoch <= stop;
13250
+ const window = this.window();
13251
+ return window.start.posix <= epoch.posix && epoch.posix <= window.end.posix;
13058
13252
  }
13059
13253
  /*
13060
13254
  * Calculate the start/stop epoch between this and another [Interpolator].
@@ -13062,13 +13256,15 @@ var Interpolator = class {
13062
13256
  * Returns `null` if there is no overlap between interpolators.
13063
13257
  */
13064
13258
  overlap(interpolator) {
13065
- const x1 = this.window().start;
13066
- const x2 = this.window().end;
13067
- const y1 = interpolator.window().start;
13068
- const y2 = interpolator.window().end;
13259
+ const a = this.window();
13260
+ const b = interpolator.window();
13261
+ const x1 = a.start.posix;
13262
+ const x2 = a.end.posix;
13263
+ const y1 = b.start.posix;
13264
+ const y2 = b.end.posix;
13069
13265
  if (x1 <= y2 && y1 <= x2) {
13070
- const e1 = new EpochUTC(Math.max(x1.posix, y1.posix));
13071
- const e2 = new EpochUTC(Math.min(x2.posix, y2.posix));
13266
+ const e1 = new EpochUTC(Math.max(x1, y1));
13267
+ const e2 = new EpochUTC(Math.min(x2, y2));
13072
13268
  return new EpochWindow(e1, e2);
13073
13269
  }
13074
13270
  return null;
@@ -13089,6 +13285,7 @@ var CubicSplineInterpolator = class _CubicSplineInterpolator extends StateInterp
13089
13285
  super();
13090
13286
  this.splines_ = splines_;
13091
13287
  }
13288
+ splines_;
13092
13289
  static fromEphemeris(ephemeris) {
13093
13290
  const splines = [];
13094
13291
  for (let i = 0; i < ephemeris.length - 1; i++) {
@@ -13288,6 +13485,7 @@ var VerletBlendInterpolator = class _VerletBlendInterpolator extends StateInterp
13288
13485
  super();
13289
13486
  this.ephemeris = ephemeris;
13290
13487
  }
13488
+ ephemeris;
13291
13489
  get sizeBytes() {
13292
13490
  return 64 * 7 * this.ephemeris.length / 8;
13293
13491
  }
@@ -13483,7 +13681,7 @@ var Propagator = class {
13483
13681
  ephemeris(start, stop, interval = 60) {
13484
13682
  const output = [this.propagate(start)];
13485
13683
  let tempEpoch = start;
13486
- while (tempEpoch <= stop) {
13684
+ while (tempEpoch.posix <= stop.posix) {
13487
13685
  tempEpoch = tempEpoch.roll(interval);
13488
13686
  output.push(this.propagate(tempEpoch));
13489
13687
  }
@@ -13657,6 +13855,9 @@ var RungeKuttaAdaptive = class _RungeKuttaAdaptive extends Propagator {
13657
13855
  this._cacheState = this.initState_;
13658
13856
  this.tolerance_ = Math.max(_RungeKuttaAdaptive._minTolerance, Math.abs(tolerance_));
13659
13857
  }
13858
+ initState_;
13859
+ forceModel_;
13860
+ tolerance_;
13660
13861
  // / Initial state vector.
13661
13862
  _cacheState;
13662
13863
  _checkpoints = [];
@@ -13776,7 +13977,7 @@ var RungeKuttaAdaptive = class _RungeKuttaAdaptive extends Propagator {
13776
13977
  let tState = this.propagate(maneuver.start);
13777
13978
  this.forceModel_.loadManeuver(maneuver);
13778
13979
  const ephemeris = [tState];
13779
- while (tState.epoch < maneuver.stop) {
13980
+ while (tState.epoch.posix < maneuver.stop.posix) {
13780
13981
  const step = Math.min(maneuver.stop.difference(tState.epoch), interval);
13781
13982
  tState = this.propagate(tState.epoch.roll(step));
13782
13983
  ephemeris.push(tState);
@@ -13785,13 +13986,13 @@ var RungeKuttaAdaptive = class _RungeKuttaAdaptive extends Propagator {
13785
13986
  return ephemeris;
13786
13987
  }
13787
13988
  ephemerisManeuver(start, finish, maneuvers, interval = 60) {
13788
- const tMvr = maneuvers.filter((mvr) => mvr.start >= start || mvr.stop <= finish);
13989
+ const tMvr = maneuvers.filter((mvr) => mvr.start.posix >= start.posix || mvr.stop.posix <= finish.posix);
13789
13990
  const ephemeris = [];
13790
- if (tMvr[0].start > start) {
13991
+ if (tMvr[0].start.posix > start.posix) {
13791
13992
  ephemeris.push(this.propagate(start));
13792
13993
  }
13793
13994
  for (const mvr of tMvr) {
13794
- while (this._cacheState.epoch < mvr.start) {
13995
+ while (this._cacheState.epoch.posix < mvr.start.posix) {
13795
13996
  const step = Math.min(mvr.start.difference(this._cacheState.epoch), interval);
13796
13997
  this.propagate(this._cacheState.epoch.roll(step));
13797
13998
  if (this._cacheState.epoch.posix !== mvr.start.posix) {
@@ -14390,6 +14591,7 @@ var AccessWindow = class _AccessWindow {
14390
14591
  };
14391
14592
 
14392
14593
  // src/objects/AccessCalculator.ts
14594
+ var CIVIL_TWILIGHT_DEG = -6;
14393
14595
  var AccessCalculator = class _AccessCalculator {
14394
14596
  /** Default calculation time step in milliseconds (10 seconds) */
14395
14597
  static DEFAULT_STEP_MS_ = 1e4;
@@ -14530,31 +14732,20 @@ var AccessCalculator = class _AccessCalculator {
14530
14732
  return false;
14531
14733
  }
14532
14734
  if (constraints.requireSunlit) {
14533
- const eci = target.eci(date);
14534
- if (!eci) {
14735
+ let satPos;
14736
+ try {
14737
+ satPos = target.toJ2000(date).position;
14738
+ } catch {
14535
14739
  return false;
14536
14740
  }
14537
14741
  const epoch = EpochUTC.fromDateTime(date);
14538
- const satPos = new Vector3D(
14539
- eci.position.x,
14540
- eci.position.y,
14541
- eci.position.z
14542
- );
14543
14742
  if (Sun.shadow(epoch, satPos)) {
14544
14743
  return false;
14545
14744
  }
14546
14745
  }
14547
14746
  if (constraints.requireObserverDark) {
14548
- const lla = observer.lla();
14549
- const altMeters = lla.alt * 1e3;
14550
- const sunTimes = Sun.getTimes(date, lla.lat, lla.lon, altMeters);
14551
- const currentTime = date.getTime();
14552
- const civilDusk = sunTimes.civilDusk?.getTime();
14553
- const civilDawn = sunTimes.civilDawn?.getTime();
14554
- if (civilDusk && civilDawn) {
14555
- if (currentTime >= civilDawn && currentTime <= civilDusk) {
14556
- return false;
14557
- }
14747
+ if (Sun.getAzEl(observer, date, false).el >= CIVIL_TWILIGHT_DEG) {
14748
+ return false;
14558
14749
  }
14559
14750
  }
14560
14751
  return true;
@@ -14734,38 +14925,35 @@ var DynamicGroundObject = class _DynamicGroundObject extends GroundObject {
14734
14925
  }
14735
14926
  /**
14736
14927
  * Gets the ECI (Earth-Centered Inertial) position at a specific time.
14928
+ *
14929
+ * The WGS84 position from {@link getEcef} rotated into TEME by GMST, the same model
14930
+ * `GroundObject.eci()` uses (not the spherical-Earth `lla2eci()`).
14737
14931
  * @param time - The time to get position for
14738
- * @returns ECI position vector, or null if time is outside waypoint range
14932
+ * @returns ECI (TEME) position vector, or null if time is outside waypoint range
14739
14933
  */
14740
14934
  getEci(time) {
14741
- const lla = this.getLLA(time);
14742
- if (!lla) {
14935
+ const ecef = this.getEcef(time);
14936
+ if (!ecef) {
14743
14937
  return null;
14744
14938
  }
14745
- const llaRad = {
14746
- lat: lla.lat * DEG2RAD,
14747
- lon: lla.lon * DEG2RAD,
14748
- alt: lla.alt
14749
- };
14750
14939
  const { gmst } = calcGmst(time);
14751
- return lla2eci(llaRad, gmst);
14940
+ return ecef2eci(ecef, gmst);
14752
14941
  }
14753
14942
  /**
14754
14943
  * Converts position at a specific time to J2000 inertial coordinates.
14755
- * Ground objects have zero velocity in the inertial frame (ignoring Earth rotation).
14944
+ *
14945
+ * The WGS84 position is rotated from ITRF into J2000 with precession, nutation and
14946
+ * sidereal time, as in `GroundObject.toJ2000()`. The velocity is the Earth-rotation
14947
+ * velocity (omega x r); the platform's own motion between waypoints is not included.
14756
14948
  * @param time - The time for the conversion
14757
14949
  * @returns J2000 state vector, or null if time is outside waypoint range
14758
14950
  */
14759
14951
  getJ2000(time) {
14760
- const eci = this.getEci(time);
14761
- if (!eci) {
14952
+ const geodetic = this.getGeodetic(time);
14953
+ if (!geodetic) {
14762
14954
  return null;
14763
14955
  }
14764
- return new J2000(
14765
- EpochUTC.fromDateTime(time),
14766
- new Vector3D(eci.x, eci.y, eci.z),
14767
- new Vector3D(0, 0, 0)
14768
- );
14956
+ return geodetic.toITRF(EpochUTC.fromDateTime(time)).toJ2000();
14769
14957
  }
14770
14958
  /**
14771
14959
  * Converts position at a specific time to Geodetic coordinates.
@@ -16125,10 +16313,17 @@ var RAE = class _RAE {
16125
16313
  this.azRateRad = azRateRad;
16126
16314
  this.elRateRad = elRateRad;
16127
16315
  }
16316
+ epoch;
16317
+ rng;
16318
+ azRad;
16319
+ elRad;
16320
+ rngRate;
16321
+ azRateRad;
16322
+ elRateRad;
16128
16323
  // / Create a new [Razel] object, using degrees for the angular values.
16129
16324
  static fromDegrees(epoch, range, azimuth, elevation, rangeRate, azimuthRate, elevationRate) {
16130
- const azimuthRateRad = azimuthRate ? azimuthRate * DEG2RAD : void 0;
16131
- const elevationRateRad = elevationRate ? elevationRate * DEG2RAD : void 0;
16325
+ const azimuthRateRad = typeof azimuthRate === "number" ? azimuthRate * DEG2RAD : void 0;
16326
+ const elevationRateRad = typeof elevationRate === "number" ? elevationRate * DEG2RAD : void 0;
16132
16327
  return new _RAE(
16133
16328
  epoch,
16134
16329
  range,
@@ -16201,14 +16396,14 @@ var RAE = class _RAE {
16201
16396
  * @returns The azimuth rate in degrees per second, or undefined if it is not available.
16202
16397
  */
16203
16398
  get azRate() {
16204
- return this.azRateRad ? this.azRateRad * RAD2DEG : void 0;
16399
+ return typeof this.azRateRad === "number" ? this.azRateRad * RAD2DEG : void 0;
16205
16400
  }
16206
16401
  /**
16207
16402
  * Gets the elevation rate in degrees per second.
16208
16403
  * @returns The elevation rate in degrees per second, or undefined if the elevation rate is not set.
16209
16404
  */
16210
16405
  get elRate() {
16211
- return this.elRateRad ? this.elRateRad * RAD2DEG : void 0;
16406
+ return typeof this.elRateRad === "number" ? this.elRateRad * RAD2DEG : void 0;
16212
16407
  }
16213
16408
  toString() {
16214
16409
  return [
@@ -16380,13 +16575,13 @@ var KeplerPropagator = class extends Propagator {
16380
16575
  return [this.cacheState_];
16381
16576
  }
16382
16577
  ephemerisManeuver(start, finish, maneuvers, interval = 60) {
16383
- const tMvr = maneuvers.slice(0).filter((mvr) => mvr.center >= start || mvr.center <= finish);
16578
+ const tMvr = maneuvers.slice(0).filter((mvr) => mvr.center.posix >= start.posix && mvr.center.posix <= finish.posix);
16384
16579
  const ephemeris = [];
16385
- if (tMvr[0].start > start) {
16580
+ if (tMvr.length === 0 || tMvr[0].start.posix > start.posix) {
16386
16581
  ephemeris.push(this.propagate(start));
16387
16582
  }
16388
16583
  for (const mvr of tMvr) {
16389
- while (this.cacheState_.epoch < mvr.center) {
16584
+ while (this.cacheState_.epoch.posix < mvr.center.posix) {
16390
16585
  const step = Math.min(mvr.center.difference(this.cacheState_.epoch), interval);
16391
16586
  this.propagate(this.cacheState_.epoch.roll(step));
16392
16587
  if (this.cacheState_.epoch.posix !== mvr.center.posix) {
@@ -16395,7 +16590,7 @@ var KeplerPropagator = class extends Propagator {
16395
16590
  }
16396
16591
  ephemeris.push(...this.maneuver(mvr));
16397
16592
  }
16398
- while (this.cacheState_.epoch < finish) {
16593
+ while (this.cacheState_.epoch.posix < finish.posix) {
16399
16594
  const step = Math.min(finish.difference(this.cacheState_.epoch), interval);
16400
16595
  this.propagate(this.cacheState_.epoch.roll(step));
16401
16596
  ephemeris.push(this.cacheState_);
@@ -16411,6 +16606,7 @@ var KeplerPropagator = class extends Propagator {
16411
16606
  }
16412
16607
  restore(index) {
16413
16608
  this.cacheState_ = this.checkpoints_[index];
16609
+ this.elements_ = this.cacheState_.toClassicalElements();
16414
16610
  }
16415
16611
  };
16416
16612
 
@@ -16434,6 +16630,11 @@ var RungeKutta4Propagator = class extends Propagator {
16434
16630
  this.checkpoints_ = checkpoints_;
16435
16631
  this.stepSize_ = Math.abs(stepSize_);
16436
16632
  }
16633
+ initState_;
16634
+ forceModel_;
16635
+ stepSize_;
16636
+ cacheState_;
16637
+ checkpoints_;
16437
16638
  // / Set the integrator step size to the provided number of [seconds].
16438
16639
  setStepSize(seconds) {
16439
16640
  this.stepSize_ = Math.abs(seconds);
@@ -16443,13 +16644,13 @@ var RungeKutta4Propagator = class extends Propagator {
16443
16644
  this.forceModel_ = forceModel;
16444
16645
  }
16445
16646
  ephemerisManeuver(start, finish, maneuvers, interval = 60) {
16446
- const tMvr = maneuvers.slice(0).filter((mvr) => mvr.start <= finish && mvr.stop >= start);
16647
+ const tMvr = maneuvers.slice(0).filter((mvr) => mvr.start.posix <= finish.posix && mvr.stop.posix >= start.posix);
16447
16648
  const ephemeris = [];
16448
- if (tMvr.length === 0 || tMvr[0].start > start) {
16649
+ if (tMvr.length === 0 || tMvr[0].start.posix > start.posix) {
16449
16650
  ephemeris.push(this.propagate(start));
16450
16651
  }
16451
16652
  for (const mvr of tMvr) {
16452
- while (this.cacheState_.epoch < mvr.start) {
16653
+ while (this.cacheState_.epoch.posix < mvr.start.posix) {
16453
16654
  const step = Math.min(mvr.start.difference(this.cacheState_.epoch), interval);
16454
16655
  this.propagate(this.cacheState_.epoch.roll(step));
16455
16656
  if (this.cacheState_.epoch.posix !== mvr.start.posix) {
@@ -16473,7 +16674,7 @@ var RungeKutta4Propagator = class extends Propagator {
16473
16674
  let tState = this.propagate(maneuver.start);
16474
16675
  this.forceModel_.loadManeuver(maneuver);
16475
16676
  const ephemeris = [tState];
16476
- while (tState.epoch < maneuver.stop) {
16677
+ while (tState.epoch.posix < maneuver.stop.posix) {
16477
16678
  const step = Math.min(maneuver.stop.difference(tState.epoch), interval);
16478
16679
  tState = this.propagate(tState.epoch.roll(step));
16479
16680
  ephemeris.push(tState);
@@ -16545,6 +16746,7 @@ var Sgp4Propagator = class extends Propagator {
16545
16746
  this.tle_ = tle_;
16546
16747
  this.cacheState_ = tle_.state.toJ2000();
16547
16748
  }
16749
+ tle_;
16548
16750
  cacheState_;
16549
16751
  checkpoints_ = [];
16550
16752
  /**
@@ -17028,8 +17230,13 @@ var Satellite = class _Satellite extends SpaceObject {
17028
17230
  return rae.az;
17029
17231
  }
17030
17232
  /**
17031
- * Calculates the RAE (Range, Azimuth, Elevation) values for a given sensor and date. If no date is provided, the
17032
- * current time is used.
17233
+ * Calculates the RAE (Range, Azimuth, Elevation) values and their rates for a given sensor
17234
+ * and date. If no date is provided, the current time is used.
17235
+ *
17236
+ * The rates are central differences of `rae()` over +/-0.5 s, stored in the units the
17237
+ * {@link RAE} class expects: km/s for range rate and rad/s for the angle rates (read them
17238
+ * in deg/s with `azRate` / `elRate`). The azimuth change is wrapped so a pass through
17239
+ * north does not produce a 360 deg jump.
17033
17240
  * @variation expanded
17034
17241
  * @param observer - The observer's position on the ground.
17035
17242
  * @param date - The date at which to calculate the RAE values. Optional, defaults to the current date.
@@ -17040,14 +17247,18 @@ var Satellite = class _Satellite extends SpaceObject {
17040
17247
  if (!rae) {
17041
17248
  return null;
17042
17249
  }
17043
- const rae2 = this.rae(observer, new Date(date.getTime() + 1e3));
17044
- if (!rae2) {
17250
+ const halfStepMs = 500;
17251
+ const raeBefore = this.rae(observer, new Date(date.getTime() - halfStepMs));
17252
+ const raeAfter = this.rae(observer, new Date(date.getTime() + halfStepMs));
17253
+ if (!raeBefore || !raeAfter) {
17045
17254
  return null;
17046
17255
  }
17256
+ const dt = 2 * halfStepMs / 1e3;
17257
+ const rangeRate = (raeAfter.rng - raeBefore.rng) / dt;
17258
+ const dAz = ((raeAfter.az - raeBefore.az + 180) % 360 + 360) % 360 - 180;
17259
+ const azimuthRate = dAz / dt * DEG2RAD;
17260
+ const elevationRate = (raeAfter.el - raeBefore.el) / dt * DEG2RAD;
17047
17261
  const epoch = new EpochUTC(date.getTime() / 1e3);
17048
- const rangeRate = rae2.rng - rae.rng;
17049
- const azimuthRate = rae2.az - rae.az;
17050
- const elevationRate = rae2.el - rae.el;
17051
17262
  return new RAE(
17052
17263
  epoch,
17053
17264
  rae.rng,
@@ -17711,11 +17922,12 @@ var Satellite = class _Satellite extends SpaceObject {
17711
17922
  /**
17712
17923
  * Calculates the angle between this satellite, another satellite, and the Sun.
17713
17924
  *
17714
- * Returns the angle at this satellite between the vector to the other satellite
17715
- * and the vector to the Sun.
17925
+ * Returns the angle at the OTHER satellite between its vector to the Sun and its vector
17926
+ * to this satellite: the solar phase angle of `other` as seen from this satellite
17927
+ * (0 = fully lit face toward this satellite, pi = backlit).
17716
17928
  *
17717
17929
  * @param other - The other satellite.
17718
- * @param sunPosition - The Sun's ECI position vector.
17930
+ * @param sunPosition - The Sun's position in TEME (the frame of `eci()`), km.
17719
17931
  * @param date - The date for the calculation.
17720
17932
  * @returns The angle in radians.
17721
17933
  * @throws Error if positions are undefined.
@@ -17758,11 +17970,12 @@ var Satellite = class _Satellite extends SpaceObject {
17758
17970
  * ```
17759
17971
  */
17760
17972
  getSunStatus(date = /* @__PURE__ */ new Date()) {
17761
- const pv = this.eci(date);
17762
- if (!pv) {
17973
+ let satPos;
17974
+ try {
17975
+ satPos = this.toJ2000(date).position;
17976
+ } catch {
17763
17977
  return -1 /* UNKNOWN */;
17764
17978
  }
17765
- const satPos = new Vector3D(pv.position.x, pv.position.y, pv.position.z);
17766
17979
  const sunPos = Sun.eci(date);
17767
17980
  const ratio = Sun.lightingRatio(satPos, sunPos);
17768
17981
  if (ratio === 0) {
@@ -18230,6 +18443,43 @@ var FieldOfView = class _FieldOfView {
18230
18443
  const targetDir = direction.normalize();
18231
18444
  return this.isDirectionWithinCone(targetDir);
18232
18445
  }
18446
+ /**
18447
+ * Samples a unit direction on or inside the FOV cone.
18448
+ *
18449
+ * The cone cross-section is parameterized like a standard 2D ellipse in
18450
+ * angular (gnomonic) space: x = halfAngle * cos(t), y = minorHalfAngle * sin(t),
18451
+ * where x is the off-boresight angle along the major axis (u) and y along the
18452
+ * minor axis (v). Uniform steps in t produce well-distributed boundary points
18453
+ * even for highly eccentric fans (e.g., an 80° x 1° fence radar), unlike
18454
+ * uniform steps in the geometric position angle which clump at the fan tips.
18455
+ *
18456
+ * Points sampled with radialFraction < 1 always satisfy the elliptical cone
18457
+ * containment check; radialFraction = 1 lies exactly on the boundary.
18458
+ *
18459
+ * @param t - Parametric ellipse angle in radians. t=0 points along the major
18460
+ * axis (+u), t=PI/2 along the minor axis (+v). Note this is NOT the
18461
+ * geometric position angle around the boresight.
18462
+ * @param radialFraction - 0 = boresight, 1 = cone boundary (default: 1)
18463
+ * @returns Unit direction vector in the FOV's reference frame (ENU for topocentric)
18464
+ */
18465
+ directionAt(t, radialFraction = 1) {
18466
+ const x = radialFraction * this.halfAngleRad_ * Math.cos(t);
18467
+ const y = radialFraction * this.minorHalfAngleRad_ * Math.sin(t);
18468
+ const theta = Math.hypot(x, y);
18469
+ const { b, u, v } = this.boresightFrame_;
18470
+ if (theta < EPS) {
18471
+ return b;
18472
+ }
18473
+ const cosTheta = Math.cos(theta);
18474
+ const sinTheta = Math.sin(theta);
18475
+ const rx = x / theta;
18476
+ const ry = y / theta;
18477
+ return new Vector3D(
18478
+ cosTheta * b.x + sinTheta * (rx * u.x + ry * v.x),
18479
+ cosTheta * b.y + sinTheta * (rx * u.y + ry * v.y),
18480
+ cosTheta * b.z + sinTheta * (rx * u.z + ry * v.z)
18481
+ );
18482
+ }
18233
18483
  /**
18234
18484
  * Gets the effective minimum elevation at a given azimuth.
18235
18485
  * Considers all applicable elevation masks and returns the most restrictive.
@@ -18639,13 +18889,13 @@ var VisualizationHelpers = class {
18639
18889
  const points = [];
18640
18890
  for (let i = 0; i < samples; i++) {
18641
18891
  const phi = 2 * Math.PI * i / samples;
18642
- const theta = Math.sqrt(
18643
- (fov.halfAngle * Math.cos(phi)) ** 2 + (fov.minorHalfAngle * Math.sin(phi)) ** 2
18644
- ) * DEG2RAD;
18892
+ const x = fov.halfAngle * Math.cos(phi) * DEG2RAD;
18893
+ const y = fov.minorHalfAngle * Math.sin(phi) * DEG2RAD;
18894
+ const theta = Math.hypot(x, y);
18645
18895
  const cosTheta = Math.cos(theta);
18646
18896
  const sinTheta = Math.sin(theta);
18647
- const cosPhi = Math.cos(phi);
18648
- const sinPhi = Math.sin(phi);
18897
+ const cosPhi = theta > 0 ? x / theta : 1;
18898
+ const sinPhi = theta > 0 ? y / theta : 0;
18649
18899
  const dir = {
18650
18900
  x: b.x * cosTheta + u.x * sinTheta * cosPhi + v.x * sinTheta * sinPhi,
18651
18901
  y: b.y * cosTheta + u.y * sinTheta * cosPhi + v.y * sinTheta * sinPhi,
@@ -18835,7 +19085,7 @@ var PlanetBody = class _PlanetBody extends CelestialBody {
18835
19085
  */
18836
19086
  velocity(date = /* @__PURE__ */ new Date()) {
18837
19087
  const time = MakeTime(date);
18838
- const state = BaryState(this.planetData_.body, time);
19088
+ const state = HelioState(this.planetData_.body, time);
18839
19089
  return new Vector3D(
18840
19090
  state.vx * AU_PER_DAY_TO_KM_PER_S,
18841
19091
  state.vy * AU_PER_DAY_TO_KM_PER_S,
@@ -18914,7 +19164,7 @@ var EphemerisBody = class _EphemerisBody extends CelestialBody {
18914
19164
  if (!params.ephemeris || params.ephemeris.length === 0) {
18915
19165
  throw new Error("Ephemeris array cannot be empty");
18916
19166
  }
18917
- let spaceObjectType = 0 /* UNKNOWN */;
19167
+ let spaceObjectType;
18918
19168
  switch (params.bodyType) {
18919
19169
  case "dwarf_planet" /* DWARF_PLANET */:
18920
19170
  spaceObjectType = 36 /* DWARF_PLANET */;
@@ -19289,6 +19539,11 @@ var Thrust = class {
19289
19539
  crosstrack * 1e-3
19290
19540
  );
19291
19541
  }
19542
+ center;
19543
+ radial;
19544
+ intrack;
19545
+ crosstrack;
19546
+ durationRate;
19292
19547
  deltaV;
19293
19548
  get magnitude() {
19294
19549
  return this.deltaV.magnitude() * 1e3;
@@ -19327,6 +19582,9 @@ var Hill = class _Hill {
19327
19582
  this.semimajorAxis_ = semimajorAxis;
19328
19583
  this.meanMotion_ = Earth.smaToMeanMotion(this.semimajorAxis_);
19329
19584
  }
19585
+ epoch;
19586
+ position;
19587
+ velocity;
19330
19588
  semimajorAxis_;
19331
19589
  meanMotion_;
19332
19590
  static fromState(origin, radialPosition, intrackPosition, nodeVelocity, nodeOffsetTime) {
@@ -19503,7 +19761,7 @@ var Hill = class _Hill {
19503
19761
  postManeuvers = postManeuvers.slice();
19504
19762
  let output = preManeuvers;
19505
19763
  output.sort((a, b) => a.center.difference(b.center));
19506
- output = output.filter((mvr) => mvr.center >= this.epoch && mvr.center >= pivot);
19764
+ output = output.filter((mvr) => mvr.center.posix >= this.epoch.posix && mvr.center.posix >= pivot.posix);
19507
19765
  for (const mvr of output) {
19508
19766
  state = state.maneuver(mvr);
19509
19767
  }
@@ -19613,6 +19871,13 @@ var RadecTopocentric = class _RadecTopocentric {
19613
19871
  this.declinationRate = declinationRate;
19614
19872
  this.rangeRate = rangeRate;
19615
19873
  }
19874
+ epoch;
19875
+ rightAscension;
19876
+ declination;
19877
+ range;
19878
+ rightAscensionRate;
19879
+ declinationRate;
19880
+ rangeRate;
19616
19881
  /**
19617
19882
  * Create a new RadecTopocentric object, using degrees for the angular values.
19618
19883
  * @param epoch UTC epoch.
@@ -19625,8 +19890,8 @@ var RadecTopocentric = class _RadecTopocentric {
19625
19890
  * @returns A new RadecTopocentric object.
19626
19891
  */
19627
19892
  static fromDegrees(epoch, rightAscensionDegrees, declinationDegrees, range, rightAscensionRateDegrees, declinationRateDegrees, rangeRate) {
19628
- const rightAscensionRate = rightAscensionRateDegrees ? rightAscensionRateDegrees * DEG2RAD : null;
19629
- const declinationRate = declinationRateDegrees ? declinationRateDegrees * DEG2RAD : null;
19893
+ const rightAscensionRate = typeof rightAscensionRateDegrees === "number" ? rightAscensionRateDegrees * DEG2RAD : null;
19894
+ const declinationRate = typeof declinationRateDegrees === "number" ? declinationRateDegrees * DEG2RAD : null;
19630
19895
  const normalizedRA = ((rightAscensionDegrees * DEG2RAD + Math.PI) % TAU + TAU) % TAU - Math.PI;
19631
19896
  return new _RadecTopocentric(
19632
19897
  epoch,
@@ -19696,14 +19961,14 @@ var RadecTopocentric = class _RadecTopocentric {
19696
19961
  * @returns The right ascension rate in degrees per second, or null if it is not available.
19697
19962
  */
19698
19963
  get rightAscensionRateDegrees() {
19699
- return this.rightAscensionRate ? this.rightAscensionRate * RAD2DEG : null;
19964
+ return typeof this.rightAscensionRate === "number" ? this.rightAscensionRate * RAD2DEG : null;
19700
19965
  }
19701
19966
  /**
19702
19967
  * Gets the rate of change of declination in degrees per second.
19703
19968
  * @returns The rate of change of declination in degrees per second, or null if the declination rate is not defined.
19704
19969
  */
19705
19970
  get declinationRateDegrees() {
19706
- return this.declinationRate ? this.declinationRate * RAD2DEG : null;
19971
+ return typeof this.declinationRate === "number" ? this.declinationRate * RAD2DEG : null;
19707
19972
  }
19708
19973
  /**
19709
19974
  * Return the position relative to the observer site.
@@ -19727,7 +19992,7 @@ var RadecTopocentric = class _RadecTopocentric {
19727
19992
  * @returns A Vector3D object.
19728
19993
  */
19729
19994
  velocity(site, range, rangeRate) {
19730
- if (!this.rightAscensionRate || !this.declinationRate) {
19995
+ if (typeof this.rightAscensionRate !== "number" || typeof this.declinationRate !== "number") {
19731
19996
  throw new Error("Velocity unsolvable, missing ra/dec rates.");
19732
19997
  }
19733
19998
  const r = range ?? this.range ?? 1;
@@ -19782,6 +20047,9 @@ var ObservationOptical = class _ObservationOptical extends Observation {
19782
20047
  this.observation = observation;
19783
20048
  this.noise_ = noise_;
19784
20049
  }
20050
+ site_;
20051
+ observation;
20052
+ noise_;
19785
20053
  // / Inertial site location.
19786
20054
  get site() {
19787
20055
  return this.site_;
@@ -19872,6 +20140,9 @@ var ObservationRadar = class _ObservationRadar extends Observation {
19872
20140
  this.observation = observation;
19873
20141
  this.noise_ = noise_;
19874
20142
  }
20143
+ site_;
20144
+ observation;
20145
+ noise_;
19875
20146
  // / Default noise matrix.
19876
20147
  static defaultNoise = _ObservationRadar.noiseFromSigmas(0.32, 0.015 * DEG2RAD, 0.015 * DEG2RAD);
19877
20148
  get epoch() {
@@ -19952,6 +20223,13 @@ var RadecGeocentric = class _RadecGeocentric {
19952
20223
  this.declinationRate = declinationRate;
19953
20224
  this.rangeRate = rangeRate;
19954
20225
  }
20226
+ epoch;
20227
+ rightAscension;
20228
+ declination;
20229
+ range;
20230
+ rightAscensionRate;
20231
+ declinationRate;
20232
+ rangeRate;
19955
20233
  /**
19956
20234
  * Creates a RadecGeocentric object from the given parameters in degrees.
19957
20235
  * @param epoch - The epoch in UTC.
@@ -19964,8 +20242,8 @@ var RadecGeocentric = class _RadecGeocentric {
19964
20242
  * @returns A new RadecGeocentric object.
19965
20243
  */
19966
20244
  static fromDegrees(epoch, rightAscensionDegrees, declinationDegrees, range, rightAscensionRateDegrees, declinationRateDegrees, rangeRate) {
19967
- const rightAscensionRate = rightAscensionRateDegrees ? rightAscensionRateDegrees * DEG2RAD : null;
19968
- const declinationRate = declinationRateDegrees ? declinationRateDegrees * DEG2RAD : null;
20245
+ const rightAscensionRate = typeof rightAscensionRateDegrees === "number" ? rightAscensionRateDegrees * DEG2RAD : null;
20246
+ const declinationRate = typeof declinationRateDegrees === "number" ? declinationRateDegrees * DEG2RAD : null;
19969
20247
  return new _RadecGeocentric(
19970
20248
  epoch,
19971
20249
  rightAscensionDegrees * DEG2RAD,
@@ -20002,7 +20280,7 @@ var RadecGeocentric = class _RadecGeocentric {
20002
20280
  const declinationRate = (vK - rangeRate * (rK / rMag)) / rIJMag;
20003
20281
  return new _RadecGeocentric(
20004
20282
  state.epoch,
20005
- rightAscension % TAU,
20283
+ (rightAscension % TAU + TAU) % TAU,
20006
20284
  declination,
20007
20285
  rMag,
20008
20286
  rightAscensionRate,
@@ -20015,7 +20293,8 @@ var RadecGeocentric = class _RadecGeocentric {
20015
20293
  * @returns The right ascension in degrees.
20016
20294
  */
20017
20295
  get rightAscensionDegrees() {
20018
- return this.rightAscension * RAD2DEG;
20296
+ const deg = this.rightAscension * RAD2DEG;
20297
+ return (deg % 360 + 360) % 360;
20019
20298
  }
20020
20299
  /**
20021
20300
  * Gets the declination in degrees.
@@ -20029,14 +20308,14 @@ var RadecGeocentric = class _RadecGeocentric {
20029
20308
  * @returns The right ascension rate in degrees per second, or null if it is not available.
20030
20309
  */
20031
20310
  get rightAscensionRateDegrees() {
20032
- return this.rightAscensionRate ? this.rightAscensionRate * RAD2DEG : null;
20311
+ return typeof this.rightAscensionRate === "number" ? this.rightAscensionRate * RAD2DEG : null;
20033
20312
  }
20034
20313
  /**
20035
20314
  * Gets the rate of change of declination in degrees per second.
20036
20315
  * @returns The rate of change of declination in degrees per second, or null if not available.
20037
20316
  */
20038
20317
  get declinationRateDegrees() {
20039
- return this.declinationRate ? this.declinationRate * RAD2DEG : null;
20318
+ return typeof this.declinationRate === "number" ? this.declinationRate * RAD2DEG : null;
20040
20319
  }
20041
20320
  /**
20042
20321
  * Calculates the position vector in geocentric coordinates.
@@ -20056,7 +20335,7 @@ var RadecGeocentric = class _RadecGeocentric {
20056
20335
  * @throws Error if the right ascension rate or declination rate is missing.
20057
20336
  */
20058
20337
  velocity(range, rangeRate) {
20059
- if (!this.rightAscensionRate || !this.declinationRate) {
20338
+ if (typeof this.rightAscensionRate !== "number" || typeof this.declinationRate !== "number") {
20060
20339
  throw new Error("Velocity unsolvable, missing ra/dec rates.");
20061
20340
  }
20062
20341
  const r = range ?? this.range ?? 1;
@@ -20474,12 +20753,143 @@ var RandomGaussianSource = class {
20474
20753
  }
20475
20754
  };
20476
20755
 
20756
+ // src/external/Sgp4WasmTypes.ts
20757
+ var Sgp4WasmLogLevel = /* @__PURE__ */ ((Sgp4WasmLogLevel3) => {
20758
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["None"] = 0] = "None";
20759
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["Info"] = 1] = "Info";
20760
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["Warn"] = 2] = "Warn";
20761
+ Sgp4WasmLogLevel3[Sgp4WasmLogLevel3["Error"] = 3] = "Error";
20762
+ return Sgp4WasmLogLevel3;
20763
+ })(Sgp4WasmLogLevel || {});
20764
+ var Sgp4WasmError = class extends Error {
20765
+ constructor(message, code) {
20766
+ super(message);
20767
+ this.code = code;
20768
+ this.name = "Sgp4WasmError";
20769
+ }
20770
+ code;
20771
+ };
20772
+ var SGP4_WASM_DYN_ARR_ERRORS = {
20773
+ [-1]: "Error allocating space for the dynamic array",
20774
+ [-2]: "Dynamic array is already initialized",
20775
+ [-3]: "The requested size is too large",
20776
+ [-4]: "The requested size is negative"
20777
+ };
20778
+ var SGP4_WASM_ADD_SAT_ERRORS = {
20779
+ [-1]: "Dynamic array has not been allocated",
20780
+ [-2]: "Dynamic array is out of space (call reallocDynArr)",
20781
+ [-3]: "Failed to determine satKey from line 1",
20782
+ [-4]: "Failed to parse line 1 or line 2"
20783
+ };
20784
+
20785
+ // src/propagator/Sgp4WasmPropagator.ts
20786
+ var Sgp4WasmPropagator = class extends Propagator {
20787
+ constructor(wasm_, tle_) {
20788
+ super();
20789
+ this.wasm_ = wasm_;
20790
+ this.tle_ = tle_;
20791
+ if (!wasm_.isLoaded) {
20792
+ throw new Sgp4WasmError("Sgp4WasmPropagator requires a loaded Sgp4Wasm instance. Call load() first.");
20793
+ }
20794
+ this.satKey_ = wasm_.addSat(tle_.line1, tle_.line2);
20795
+ wasm_.initSats([this.satKey_]);
20796
+ this.cacheState_ = this.propagate(tle_.epoch);
20797
+ }
20798
+ wasm_;
20799
+ tle_;
20800
+ satKey_;
20801
+ cacheState_;
20802
+ checkpoints_ = [];
20803
+ /**
20804
+ * The Astro Standards satKey backing this propagator.
20805
+ */
20806
+ get satKey() {
20807
+ return this.satKey_;
20808
+ }
20809
+ /**
20810
+ * Gets the state of the propagator in the J2000 coordinate system.
20811
+ * @returns The J2000 state of the propagator.
20812
+ */
20813
+ get state() {
20814
+ return this.cacheState_;
20815
+ }
20816
+ /**
20817
+ * Calculates the ephemeris maneuver using the SGP4 propagator.
20818
+ * @param start The start epoch in UTC.
20819
+ * @param finish The finish epoch in UTC.
20820
+ * @param maneuvers The array of thrust maneuvers.
20821
+ * @param interval The time interval in seconds.
20822
+ */
20823
+ ephemerisManeuver(_start, _finish, _maneuvers, _interval = 60) {
20824
+ throw new Error("Maneuvers cannot be modelled with SGP4.");
20825
+ }
20826
+ /**
20827
+ * Performs a maneuver with the given thrust.
20828
+ * @param maneuver - The thrust maneuver to perform.
20829
+ * @param interval - The time interval for the maneuver (default: 60.0 seconds).
20830
+ * @throws Error if maneuvers cannot be modeled with SGP4.
20831
+ */
20832
+ maneuver(_maneuver, _interval = 60) {
20833
+ throw new Error("Maneuvers cannot be modelled with SGP4.");
20834
+ }
20835
+ /**
20836
+ * Propagates the state to a specified epoch in J2000 coordinates.
20837
+ * @param epoch - The epoch in UTC format.
20838
+ * @returns The propagated state in J2000 coordinates.
20839
+ */
20840
+ propagate(epoch) {
20841
+ const state = this.wasm_.propagateEpoch(this.satKey_, epoch);
20842
+ if (state.err !== 0) {
20843
+ throw new Sgp4WasmError(`SGP4 propagation failed for satKey ${this.satKey_} (code ${state.err})`, state.err);
20844
+ }
20845
+ this.cacheState_ = new TEME(epoch, state.position, state.velocity).toJ2000();
20846
+ return this.cacheState_;
20847
+ }
20848
+ /**
20849
+ * Resets the state of the propagator to the TLE epoch state.
20850
+ */
20851
+ reset() {
20852
+ this.cacheState_ = this.propagate(this.tle_.epoch);
20853
+ }
20854
+ /**
20855
+ * Saves the current state of the propagator and returns the index of the checkpoint.
20856
+ * @returns The index of the checkpoint.
20857
+ */
20858
+ checkpoint() {
20859
+ this.checkpoints_.push(this.cacheState_);
20860
+ return this.checkpoints_.length - 1;
20861
+ }
20862
+ /**
20863
+ * Clears all the checkpoints in the propagator.
20864
+ */
20865
+ clearCheckpoints() {
20866
+ this.checkpoints_ = [];
20867
+ }
20868
+ /**
20869
+ * Restores the state of the propagator to a previously saved checkpoint.
20870
+ * @param index - The index of the checkpoint to restore.
20871
+ */
20872
+ restore(index) {
20873
+ this.cacheState_ = this.checkpoints_[index];
20874
+ }
20875
+ /**
20876
+ * Removes the satellite from the backing wasm instance's SGP4 and TLE
20877
+ * registries.
20878
+ */
20879
+ dispose() {
20880
+ this.wasm_.uninitSats([this.satKey_]);
20881
+ this.wasm_.removeSats([this.satKey_]);
20882
+ }
20883
+ };
20884
+
20477
20885
  // src/observation/PropagatorPairs.ts
20478
20886
  var PropagatorPairs = class {
20479
20887
  constructor(posStep_, velStep_) {
20480
20888
  this.posStep_ = posStep_;
20481
20889
  this.velStep_ = velStep_;
20482
20890
  }
20891
+ posStep_;
20892
+ velStep_;
20483
20893
  _high = Array(6).fill(null);
20484
20894
  _low = Array(6).fill(null);
20485
20895
  set(index, high, low) {
@@ -20513,6 +20923,9 @@ var BatchLeastSquaresResult = class {
20513
20923
  this.covariance = covariance2;
20514
20924
  this.rms = rms;
20515
20925
  }
20926
+ state;
20927
+ covariance;
20928
+ rms;
20516
20929
  };
20517
20930
 
20518
20931
  // src/orbit-determination/BatchLeastSquaresOD.ts
@@ -20543,6 +20956,12 @@ var BatchLeastSquaresOD = class _BatchLeastSquaresOD {
20543
20956
  this.propagator_ = new RungeKutta89Propagator(this.apriori_, this.forceModel_);
20544
20957
  this.nominal_ = this.propagator_.propagate(this.start_);
20545
20958
  }
20959
+ observations_;
20960
+ apriori_;
20961
+ forceModel_;
20962
+ posStep_;
20963
+ velStep_;
20964
+ fastDerivatives_;
20546
20965
  /** Propagator pair cache, for generating observation Jacobians. */
20547
20966
  propPairs_;
20548
20967
  /** Nominal state propagator. */
@@ -20643,6 +21062,7 @@ var GibbsIOD = class _GibbsIOD {
20643
21062
  constructor(mu = Earth.mu) {
20644
21063
  this.mu = mu;
20645
21064
  }
21065
+ mu;
20646
21066
  /** Abort solve if position plane exceeds this value. */
20647
21067
  static coplanarThreshold_ = 5 * DEG2RAD;
20648
21068
  /**
@@ -20701,6 +21121,7 @@ var HerrickGibbsIOD = class {
20701
21121
  constructor(mu = Earth.mu) {
20702
21122
  this.mu = mu;
20703
21123
  }
21124
+ mu;
20704
21125
  // / Attempt to create a state estimate from three inertial position vectors.
20705
21126
  solve(r1, t1, r2, t2, r3, t3) {
20706
21127
  const dt31 = t3.difference(t1);
@@ -20961,6 +21382,7 @@ var LambertIOD = class _LambertIOD {
20961
21382
  constructor(mu = Earth.mu) {
20962
21383
  this.mu = mu;
20963
21384
  }
21385
+ mu;
20964
21386
  /**
20965
21387
  * Try to guess the short path argument given an [interceptor] and
20966
21388
  * [target] state.
@@ -21216,10 +21638,9 @@ var GoodingIOD = class _GoodingIOD {
21216
21638
  * @returns Orbit estimate referenced to the epoch of the second observation
21217
21639
  */
21218
21640
  estimate(o1, o2, o3, rho1init = null, rho3init = null, nRev = 0, direction = true) {
21219
- let orbit = null;
21220
21641
  if (rho1init === null || rho3init === null) {
21221
21642
  const gaussIod = new GaussIOD(this._mu);
21222
- orbit = gaussIod.estimate(o1, o2, o3);
21643
+ const orbit = gaussIod.estimate(o1, o2, o3);
21223
21644
  if (orbit === null) {
21224
21645
  throw new OrbitDeterminationError("Gauss IOD failed to provide initial estimate for Gooding IOD", "Gooding");
21225
21646
  }
@@ -21648,6 +22069,11 @@ var LevenbergMarquardtResult = class {
21648
22069
  this.iterations = iterations;
21649
22070
  this.converged = converged;
21650
22071
  }
22072
+ state;
22073
+ covariance;
22074
+ rms;
22075
+ iterations;
22076
+ converged;
21651
22077
  };
21652
22078
 
21653
22079
  // src/orbit-determination/LevenbergMarquardtOD.ts
@@ -21678,6 +22104,12 @@ var LevenbergMarquardtOD = class _LevenbergMarquardtOD {
21678
22104
  const propagator = new RungeKutta89Propagator(this.apriori_, this.forceModel_);
21679
22105
  this.nominal_ = propagator.propagate(this.start_);
21680
22106
  }
22107
+ observations_;
22108
+ apriori_;
22109
+ forceModel_;
22110
+ posStep_;
22111
+ velStep_;
22112
+ fastDerivatives_;
21681
22113
  /** Propagator pair cache, for generating observation Jacobians. */
21682
22114
  propPairs_;
21683
22115
  /** State estimate during solve. */
@@ -21836,6 +22268,8 @@ var SimplexEntry = class _SimplexEntry {
21836
22268
  this.x_ = new Vector(points);
21837
22269
  this.score = this.f_(points);
21838
22270
  }
22271
+ f_;
22272
+ points;
21839
22273
  /**
21840
22274
  * The cost function score at this vertex.
21841
22275
  *
@@ -22114,6 +22548,18 @@ var ModifiedGoodingIOD = class {
22114
22548
  };
22115
22549
  return scoreFn;
22116
22550
  }
22551
+ /**
22552
+ * Solves for a J2000 state at the middle observation's epoch: a Gooding three-line-of-sight
22553
+ * estimate refined by a downhill simplex over all observations with a two-body force model.
22554
+ *
22555
+ * `tolerance` ends the simplex once the spread of the summed line-of-sight error (radians)
22556
+ * or of the vertices falls below it.
22557
+ * @param observations - Optical observations, in time order (at least 3)
22558
+ * @param r0 - Range guess for the first observation (km)
22559
+ * @param rN - Range guess for the last observation (km)
22560
+ * @param options - Solver options
22561
+ * @returns The J2000 state at the middle observation's epoch
22562
+ */
22117
22563
  solve(observations, r0, rN, {
22118
22564
  nRev = 0,
22119
22565
  direction = true,
@@ -22577,6 +23023,8 @@ var ConjunctionAssessment = class {
22577
23023
  this.primaryProp = this.createPropagator(primary, false);
22578
23024
  this.secondaryProp = this.createPropagator(secondary, false);
22579
23025
  }
23026
+ primary;
23027
+ secondary;
22580
23028
  primaryProp;
22581
23029
  secondaryProp;
22582
23030
  primaryCovSample;
@@ -23811,7 +24259,7 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23811
24259
  this.validateFace_(face);
23812
24260
  const azRad = az * DEG2RAD;
23813
24261
  const elRad = el * DEG2RAD;
23814
- const azDiff = azRad - this.boresightAzRad(face);
24262
+ const azDiff = wrapAngle(azRad - this.boresightAzRad(face));
23815
24263
  const elDiff = elRad - this.boresightElRad(face);
23816
24264
  return azel2uv(azDiff, elDiff, this.beamwidthRad);
23817
24265
  }
@@ -23905,9 +24353,7 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23905
24353
  getVisibleFaces(az, el, maxAngle = 60) {
23906
24354
  const visibleFaces = [];
23907
24355
  for (let face = 0; face < this.faceCount; face++) {
23908
- const azDiff = Math.abs(az - this.boresightAz[face]);
23909
- const elDiff = Math.abs(el - this.boresightEl[face]);
23910
- const angularDist = Math.sqrt(azDiff * azDiff + elDiff * elDiff);
24356
+ const angularDist = this.angleFromBoresight_(az, el, face);
23911
24357
  if (angularDist <= maxAngle) {
23912
24358
  visibleFaces.push(face);
23913
24359
  }
@@ -23924,9 +24370,7 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23924
24370
  let bestFace = -1;
23925
24371
  let minDist = Infinity;
23926
24372
  for (let face = 0; face < this.faceCount; face++) {
23927
- const azDiff = az - this.boresightAz[face];
23928
- const elDiff = el - this.boresightEl[face];
23929
- const dist = Math.sqrt(azDiff * azDiff + elDiff * elDiff);
24373
+ const dist = this.angleFromBoresight_(az, el, face);
23930
24374
  if (dist < minDist && dist < 90) {
23931
24375
  minDist = dist;
23932
24376
  bestFace = face;
@@ -23934,6 +24378,24 @@ var PhasedArrayRadar = class _PhasedArrayRadar extends RadarSensor {
23934
24378
  }
23935
24379
  return bestFace;
23936
24380
  }
24381
+ /**
24382
+ * Great-circle angle between a look direction and a face's boresight, in degrees.
24383
+ * (The face checks used sqrt(dAz^2 + dEl^2) with no azimuth wrap: 359 deg missed a 0 deg
24384
+ * face, and azimuth differences were not shrunk by cos(elevation).)
24385
+ * @param az - Target azimuth in degrees
24386
+ * @param el - Target elevation in degrees
24387
+ * @param face - Face index
24388
+ * @returns Angle from boresight in degrees
24389
+ */
24390
+ angleFromBoresight_(az, el, face) {
24391
+ return angularDistance(
24392
+ az * DEG2RAD,
24393
+ el * DEG2RAD,
24394
+ this.boresightAz[face] * DEG2RAD,
24395
+ this.boresightEl[face] * DEG2RAD,
24396
+ 1 /* Haversine */
24397
+ ) * RAD2DEG;
24398
+ }
23937
24399
  // ==================== Serialization ====================
23938
24400
  serializeSpecific() {
23939
24401
  return {
@@ -24444,20 +24906,97 @@ var PassiveRFSensor = class _PassiveRFSensor extends Sensor {
24444
24906
  }
24445
24907
  };
24446
24908
 
24447
- // src/parsers/HorizonsParser.ts
24448
- function julianDateToDate(jd) {
24449
- const unixMs = (jd - 24405875e-1) * 864e5;
24450
- return new Date(unixMs);
24451
- }
24452
- var HorizonsParser = class {
24453
- constructor() {
24909
+ // src/sensor/TtcAntenna.ts
24910
+ var TtcAntenna = class _TtcAntenna extends Sensor {
24911
+ /** Antenna dish diameter in meters */
24912
+ antennaDiameter;
24913
+ /** Antenna gain in dB */
24914
+ antennaGain;
24915
+ /** Whether the station supports two-way ranging */
24916
+ hasRanging;
24917
+ /** Whether the station can uplink commands */
24918
+ hasUplink;
24919
+ constructor(params) {
24920
+ const paramsWithType = {
24921
+ ...params,
24922
+ sensorType: "TT_C_ANTENNA" /* TT_C_ANTENNA */
24923
+ };
24924
+ super(paramsWithType);
24925
+ this.antennaDiameter = params.antennaDiameter;
24926
+ this.antennaGain = params.antennaGain;
24927
+ this.hasRanging = params.hasRanging ?? true;
24928
+ this.hasUplink = params.hasUplink ?? true;
24454
24929
  }
24455
24930
  /**
24456
- * Parses NASA Horizons vector table format.
24457
- *
24458
- * Expects output from Horizons with settings:
24459
- * - VECTORS (type 2 or 3)
24460
- * - Output in km and km/s
24931
+ * Creates an angle observation (Ra/Dec) of a cooperative target.
24932
+ * Angles come from antenna autotrack while locked to the target's downlink.
24933
+ * @param target - The space object to observe
24934
+ * @param date - Time of observation (defaults to now)
24935
+ * @returns ObservationOptical or null if target not in FOV
24936
+ */
24937
+ observe(target, date = /* @__PURE__ */ new Date()) {
24938
+ if (!this.canObserve(target, date)) {
24939
+ return null;
24940
+ }
24941
+ const sensorJ2000 = this.getJ2000(date);
24942
+ const targetJ2000 = target.toJ2000(date);
24943
+ const radec = RadecTopocentric.fromStateVector(targetJ2000, sensorJ2000);
24944
+ return new ObservationOptical(sensorJ2000, radec);
24945
+ }
24946
+ serializeSpecific() {
24947
+ return {
24948
+ antennaDiameter: this.antennaDiameter,
24949
+ antennaGain: this.antennaGain,
24950
+ hasRanging: this.hasRanging,
24951
+ hasUplink: this.hasUplink
24952
+ };
24953
+ }
24954
+ /**
24955
+ * Creates a deep copy of this TT&C antenna.
24956
+ * The cloned sensor will not have a parent assigned.
24957
+ * @returns A new TtcAntenna instance with the same properties
24958
+ */
24959
+ clone() {
24960
+ return new _TtcAntenna({
24961
+ id: this.id,
24962
+ name: this.name,
24963
+ sensorType: this.sensorType,
24964
+ fieldOfView: this.fieldOfView.serialize(),
24965
+ antennaDiameter: this.antennaDiameter,
24966
+ antennaGain: this.antennaGain,
24967
+ hasRanging: this.hasRanging,
24968
+ hasUplink: this.hasUplink,
24969
+ shortName: this.shortName,
24970
+ system: this.system,
24971
+ country: this.country,
24972
+ operator: this.operator,
24973
+ dwellTime: this.dwellTime,
24974
+ freqBand: this.freqBand,
24975
+ isVolumetric: this.isVolumetric,
24976
+ url: this.url,
24977
+ metadata: this.metadata ? { ...this.metadata } : void 0
24978
+ });
24979
+ }
24980
+ };
24981
+
24982
+ // src/parsers/HorizonsParser.ts
24983
+ function julianDateToDate(jd) {
24984
+ const unixMs = (jd - 24405875e-1) * 864e5;
24985
+ return new Date(unixMs);
24986
+ }
24987
+ function tdbDateToEpochUtc(tdb) {
24988
+ const j2000Posix = 946728e3;
24989
+ return EpochUTC.fromJ2000TTSeconds(tdb.getTime() / 1e3 - j2000Posix);
24990
+ }
24991
+ var HorizonsParser = class {
24992
+ constructor() {
24993
+ }
24994
+ /**
24995
+ * Parses NASA Horizons vector table format.
24996
+ *
24997
+ * Expects output from Horizons with settings:
24998
+ * - VECTORS (type 2 or 3)
24999
+ * - Output in km and km/s
24461
25000
  *
24462
25001
  * @param data - Raw Horizons output text
24463
25002
  * @returns Parsed ephemeris data
@@ -24480,22 +25019,34 @@ var HorizonsParser = class {
24480
25019
  let referenceFrame = "";
24481
25020
  for (let i = 0; i < lines.length; i++) {
24482
25021
  const line = lines[i].trim();
24483
- if (line.startsWith("Target body name:")) {
24484
- targetName = this.extractTargetName_(line, "Target body name:");
24485
- metadata.targetName = targetName;
24486
- } else if (line.startsWith("Center body name:")) {
24487
- centerBody = this.extractValue_(line, "Center body name:");
24488
- metadata.centerBody = centerBody;
24489
- } else if (line.startsWith("Center-site name:") && !centerBody) {
24490
- centerBody = this.extractValue_(line, "Center-site name:");
24491
- metadata.centerBody = centerBody;
24492
- } else if (line.startsWith("Output type:")) {
24493
- metadata.outputType = this.extractValue_(line, "Output type:");
24494
- } else if (line.startsWith("Reference frame:")) {
24495
- referenceFrame = this.extractValue_(line, "Reference frame:");
24496
- metadata.referenceFrame = referenceFrame;
24497
- } else if (line.startsWith("Output units:")) {
24498
- metadata.outputUnits = this.extractValue_(line, "Output units:");
25022
+ const meta = this.matchMetadataLine_(line);
25023
+ if (meta) {
25024
+ switch (meta.label) {
25025
+ case "Target body name":
25026
+ targetName = this.stripNameSuffixes_(meta.value);
25027
+ metadata.targetName = targetName;
25028
+ break;
25029
+ case "Center body name":
25030
+ centerBody = meta.value;
25031
+ metadata.centerBody = centerBody;
25032
+ break;
25033
+ case "Center-site name":
25034
+ if (!centerBody) {
25035
+ centerBody = meta.value;
25036
+ metadata.centerBody = centerBody;
25037
+ }
25038
+ break;
25039
+ case "Output type":
25040
+ metadata.outputType = meta.value;
25041
+ break;
25042
+ case "Reference frame":
25043
+ referenceFrame = meta.value;
25044
+ metadata.referenceFrame = referenceFrame;
25045
+ break;
25046
+ case "Output units":
25047
+ metadata.outputUnits = meta.value;
25048
+ break;
25049
+ }
24499
25050
  }
24500
25051
  if (line === "$$SOE") {
24501
25052
  inData = true;
@@ -24540,10 +25091,12 @@ var HorizonsParser = class {
24540
25091
  let observerLocation = "";
24541
25092
  for (let i = 0; i < lines.length; i++) {
24542
25093
  const line = lines[i].trim();
24543
- if (line.startsWith("Target body name:")) {
24544
- targetName = this.extractTargetName_(line, "Target body name:");
24545
- } else if (line.startsWith("Center-site name:")) {
24546
- observerLocation = this.extractValue_(line, "Center-site name:");
25094
+ const targetRaw = this.extractLabeledValue_(line, "Target body name");
25095
+ const siteRaw = this.extractLabeledValue_(line, "Center-site name");
25096
+ if (targetRaw !== null) {
25097
+ targetName = this.stripNameSuffixes_(targetRaw);
25098
+ } else if (siteRaw !== null) {
25099
+ observerLocation = siteRaw;
24547
25100
  }
24548
25101
  if (line === "$$SOE") {
24549
25102
  inData = true;
@@ -24567,23 +25120,51 @@ var HorizonsParser = class {
24567
25120
  };
24568
25121
  }
24569
25122
  // ==================== Private Helper Methods ====================
25123
+ /** Metadata labels recognized in Horizons output headers. */
25124
+ static metadataLabels_ = [
25125
+ "Target body name",
25126
+ "Center body name",
25127
+ "Center-site name",
25128
+ "Output type",
25129
+ "Reference frame",
25130
+ "Output units"
25131
+ ];
24570
25132
  /**
24571
- * Extracts a value from a key: value line.
25133
+ * Extracts a metadata value from a `Label : value` line, tolerating the
25134
+ * column-aligned padding Horizons inserts before the colon (e.g.
25135
+ * "Output units : KM-S"). Returns null if the line does not carry the
25136
+ * given label.
24572
25137
  */
24573
- static extractValue_(line, key) {
24574
- const value = line.substring(key.length).trim();
24575
- return value.split(/\s{2,}/u)[0].trim();
25138
+ static extractLabeledValue_(line, label) {
25139
+ if (!line.startsWith(label)) {
25140
+ return null;
25141
+ }
25142
+ const rest = line.substring(label.length);
25143
+ const colonIdx = rest.indexOf(":");
25144
+ if (colonIdx === -1 || rest.substring(0, colonIdx).trim().length > 0) {
25145
+ return null;
25146
+ }
25147
+ return rest.substring(colonIdx + 1).trim().split(/\s{2,}/u)[0].trim();
25148
+ }
25149
+ /** Returns the first recognized metadata label/value on the line, if any. */
25150
+ static matchMetadataLine_(line) {
25151
+ for (const label of this.metadataLabels_) {
25152
+ const value = this.extractLabeledValue_(line, label);
25153
+ if (value !== null) {
25154
+ return { label, value };
25155
+ }
25156
+ }
25157
+ return null;
24576
25158
  }
24577
25159
  /**
24578
- * Extracts the target body name, stripping parenthetical descriptors and IDs.
25160
+ * Strips parenthetical descriptors and IDs from a target body name.
24579
25161
  *
24580
25162
  * Horizons format examples:
24581
- * - "1 Ceres {source: astDys}" → "1 Ceres"
24582
- * - "Artemis II (spacecraft) (-1024) {source: ...}" → "Artemis II"
24583
- * - "Mars (499)" → "Mars"
25163
+ * - "1 Ceres" → "1 Ceres"
25164
+ * - "Artemis II (spacecraft) (-1024)" → "Artemis II"
25165
+ * - "Mars (499)" → "Mars"
24584
25166
  */
24585
- static extractTargetName_(line, key) {
24586
- const raw = this.extractValue_(line, key);
25167
+ static stripNameSuffixes_(raw) {
24587
25168
  const parenIdx = raw.search(/\s\(/u);
24588
25169
  if (parenIdx > 0) {
24589
25170
  return raw.substring(0, parenIdx).trim();
@@ -24604,7 +25185,7 @@ var HorizonsParser = class {
24604
25185
  if (isNaN(jd)) {
24605
25186
  return null;
24606
25187
  }
24607
- const epoch2 = EpochUTC.fromDateTime(julianDateToDate(jd));
25188
+ const epoch2 = tdbDateToEpochUtc(julianDateToDate(jd));
24608
25189
  const x2 = parseFloat(parts[1]);
24609
25190
  const y2 = parseFloat(parts[2]);
24610
25191
  const z2 = parseFloat(parts[3]);
@@ -24652,7 +25233,8 @@ var HorizonsParser = class {
24652
25233
  };
24653
25234
  const month = monthMap[monthStr] ?? 0;
24654
25235
  const date = new Date(Date.UTC(year, month, day, hour, minute, Math.floor(second), second % 1 * 1e3));
24655
- const epoch = EpochUTC.fromDateTime(date);
25236
+ const isUt = /\bUTC?\b/u.test(line.slice(dateMatch[0].length));
25237
+ const epoch = isUt ? EpochUTC.fromDateTime(date) : tdbDateToEpochUtc(date);
24656
25238
  let nextIndex = currentIndex + 1;
24657
25239
  if (nextIndex >= allLines.length) {
24658
25240
  return null;
@@ -26570,7 +27152,7 @@ var Transmitter = class _Transmitter extends CommunicationDevice {
26570
27152
  const distance = this.getDistanceTo(receiver, date);
26571
27153
  const fspl = calculateFspl(distance, this.frequency);
26572
27154
  const receivedPower = this.eirp - fspl + receiver.antenna.gain - receiver.lineLoss;
26573
- const noiseFloorDbw = -204 + 10 * Math.log10(this.bandwidth) + receiver.noiseFigure;
27155
+ const noiseFloorDbw = receiver.noiseFloor;
26574
27156
  const snr = receivedPower - noiseFloorDbw;
26575
27157
  return {
26576
27158
  eirp: this.eirp,
@@ -27310,6 +27892,11 @@ var ChebyshevCoefficients = class _ChebyshevCoefficients {
27310
27892
  this.cyd_ = _ChebyshevCoefficients._derivative(a, b, cy_);
27311
27893
  this.czd_ = _ChebyshevCoefficients._derivative(a, b, cz_);
27312
27894
  }
27895
+ a;
27896
+ b;
27897
+ cx_;
27898
+ cy_;
27899
+ cz_;
27313
27900
  cxd_;
27314
27901
  cyd_;
27315
27902
  czd_;
@@ -27641,6 +28228,43 @@ var PlaneChangeBurn = class _PlaneChangeBurn {
27641
28228
  }
27642
28229
  };
27643
28230
 
28231
+ // src/maneuver/PropellantBudget.ts
28232
+ var PropellantBudget = class _PropellantBudget {
28233
+ /** Standard gravity used by the specific-impulse definition (m/s^2). */
28234
+ static g0 = 9.80665;
28235
+ /**
28236
+ * Mass remaining after an impulsive burn: `m * exp(-dv / (isp * g0))`.
28237
+ * @param massKg Total mass before the burn. (kg)
28238
+ * @param deltaVMs Delta-V of the burn. (m/s)
28239
+ * @param ispS Specific impulse of the propulsion system. (s)
28240
+ * @returns Total mass after the burn. (kg)
28241
+ */
28242
+ static massAfterBurn(massKg, deltaVMs, ispS) {
28243
+ return massKg * Math.exp(-deltaVMs / (ispS * _PropellantBudget.g0));
28244
+ }
28245
+ /**
28246
+ * Propellant consumed by an impulsive burn.
28247
+ * @param massKg Total mass before the burn. (kg)
28248
+ * @param deltaVMs Delta-V of the burn. (m/s)
28249
+ * @param ispS Specific impulse of the propulsion system. (s)
28250
+ * @returns Propellant mass consumed. (kg)
28251
+ */
28252
+ static propellantUsed(massKg, deltaVMs, ispS) {
28253
+ return massKg - _PropellantBudget.massAfterBurn(massKg, deltaVMs, ispS);
28254
+ }
28255
+ /**
28256
+ * Delta-V available before the propellant is exhausted:
28257
+ * `isp * g0 * ln(m / dry)`.
28258
+ * @param massKg Current total mass. (kg)
28259
+ * @param dryMassKg Dry mass (no propellant). (kg)
28260
+ * @param ispS Specific impulse of the propulsion system. (s)
28261
+ * @returns Remaining delta-V. (m/s)
28262
+ */
28263
+ static deltaVRemaining(massKg, dryMassKg, ispS) {
28264
+ return ispS * _PropellantBudget.g0 * Math.log(massKg / dryMassKg);
28265
+ }
28266
+ };
28267
+
27644
28268
  // src/maneuver/Waypoint.ts
27645
28269
  var Waypoint = class _Waypoint {
27646
28270
  epoch;
@@ -27829,6 +28453,11 @@ var TwoBurnOrbitTransfer = class _TwoBurnOrbitTransfer {
27829
28453
  this.vTransB = vTransB;
27830
28454
  this.tTrans = tTrans;
27831
28455
  }
28456
+ vInit;
28457
+ vFinal;
28458
+ vTransA;
28459
+ vTransB;
28460
+ tTrans;
27832
28461
  /**
27833
28462
  * Calculates the parameters for a Hohmann transfer orbit between two circular orbits.
27834
28463
  * @param rInit The initial radius of the orbit. (km)
@@ -27843,6 +28472,55 @@ var TwoBurnOrbitTransfer = class _TwoBurnOrbitTransfer {
27843
28472
  const tTrans = Math.PI * Math.sqrt((rInit + rFinal) ** 3 / (8 * Earth.mu));
27844
28473
  return new _TwoBurnOrbitTransfer(vInit, vFinal, vTransA, vTransB, tTrans);
27845
28474
  }
28475
+ /**
28476
+ * Calculates a Hohmann transfer between two circular orbits with an optional
28477
+ * inclination change folded into one of the two burns as a single combined
28478
+ * impulse.
28479
+ *
28480
+ * The plane change is cheapest where the orbital velocity is lowest. For a
28481
+ * raising transfer that is the second burn (at rFinal); for a lowering
28482
+ * transfer it is the first burn (at rInit). The plane change is auto-assigned
28483
+ * to the cheaper burn and the assignment is reported in the result. A
28484
+ * combined burn costs `sqrt(v1^2 + v2^2 - 2 * v1 * v2 * cos(deltaInc))`
28485
+ * (law of cosines between the pre- and post-burn velocity vectors).
28486
+ * @param rInit The initial orbit radius. (km)
28487
+ * @param rFinal The final orbit radius. (km)
28488
+ * @param deltaIncRad The inclination change. (rad)
28489
+ * @returns Per-burn delta-V magnitudes, total, transfer time, and the plane-change assignment.
28490
+ */
28491
+ static hohmannTransferWithPlaneChange(rInit, rFinal, deltaIncRad) {
28492
+ const vInit = Math.sqrt(Earth.mu / rInit);
28493
+ const vFinal = Math.sqrt(Earth.mu / rFinal);
28494
+ const vTransInit = Math.sqrt(Earth.mu * (2 / rInit - 2 / (rInit + rFinal)));
28495
+ const vTransFinal = Math.sqrt(Earth.mu * (2 / rFinal - 2 / (rInit + rFinal)));
28496
+ const tTrans = Math.PI * Math.sqrt((rInit + rFinal) ** 3 / (8 * Earth.mu));
28497
+ const combined = (v1, v2) => Math.sqrt(v1 ** 2 + v2 ** 2 - 2 * v1 * v2 * Math.cos(deltaIncRad));
28498
+ let planeChangeBurn;
28499
+ let deltaV1;
28500
+ let deltaV2;
28501
+ if (deltaIncRad === 0) {
28502
+ planeChangeBurn = "none";
28503
+ deltaV1 = Math.abs(vTransInit - vInit);
28504
+ deltaV2 = Math.abs(vFinal - vTransFinal);
28505
+ } else if (rFinal >= rInit) {
28506
+ planeChangeBurn = "burn2";
28507
+ deltaV1 = Math.abs(vTransInit - vInit);
28508
+ deltaV2 = combined(vTransFinal, vFinal);
28509
+ } else {
28510
+ planeChangeBurn = "burn1";
28511
+ deltaV1 = combined(vInit, vTransInit);
28512
+ deltaV2 = Math.abs(vFinal - vTransFinal);
28513
+ }
28514
+ return {
28515
+ vInit,
28516
+ vFinal,
28517
+ deltaV1,
28518
+ deltaV2,
28519
+ deltaVTotal: deltaV1 + deltaV2,
28520
+ tTrans,
28521
+ planeChangeBurn
28522
+ };
28523
+ }
27846
28524
  // / Return the total delta-velocity magnitude for both maneuvers _(km/s)_.
27847
28525
  get deltaV() {
27848
28526
  return Math.abs(this.vTransA) + Math.abs(this.vTransB);
@@ -27881,6 +28559,7 @@ var ChebyshevCompressor = class _ChebyshevCompressor {
27881
28559
  constructor(interpolator_) {
27882
28560
  this.interpolator_ = interpolator_;
27883
28561
  }
28562
+ interpolator_;
27884
28563
  /** Returns the cosine of π times x. */
27885
28564
  static cosPi_(x) {
27886
28565
  return Math.cos(Math.PI * x);
@@ -28121,6 +28800,8 @@ var SimpleLinearRegression = class _SimpleLinearRegression {
28121
28800
  this.ys = ys;
28122
28801
  this.update();
28123
28802
  }
28803
+ xs;
28804
+ ys;
28124
28805
  /** Line slope (m in y = mx + b). */
28125
28806
  slope_ = 0;
28126
28807
  /** Y-axis intercept (b in y = mx + b). */
@@ -29319,8 +30000,7 @@ var LaunchTrajectoryGenerator = class _LaunchTrajectoryGenerator {
29319
30000
  azimuthRad,
29320
30001
  dr
29321
30002
  );
29322
- const gmst = epoch.gmstAngle();
29323
- const pos = _LaunchTrajectoryGenerator.geodeticToEci_(lat, lon, alt, gmst);
30003
+ const pos = _LaunchTrajectoryGenerator.geodeticToJ2000_(lat, lon, alt, epoch);
29324
30004
  positions.push({ epoch, pos });
29325
30005
  }
29326
30006
  for (let i = 0; i < positions.length; i++) {
@@ -29418,20 +30098,17 @@ var LaunchTrajectoryGenerator = class _LaunchTrajectoryGenerator {
29418
30098
  return { lat, lon };
29419
30099
  }
29420
30100
  /**
29421
- * Convert geodetic coordinates to ECI position.
29422
- * Uses spherical Earth approximation (consistent with lla2eci in transforms.ts).
30101
+ * Convert geodetic coordinates to a J2000 position.
30102
+ *
30103
+ * The point is placed on the WGS84 ellipsoid in ITRF and rotated into J2000 with
30104
+ * precession, nutation and sidereal time, so the trajectory starts on the same launch
30105
+ * site a `GroundObject` at those coordinates reports. (A spherical Earth rotated by GMST
30106
+ * alone, the earlier model, is a TEME position on the wrong surface: the pad sat ~17 km
30107
+ * off on the sphere plus up to ~40 km of precession and nutation since J2000.)
29423
30108
  */
29424
- static geodeticToEci_(latRad, lonRad, altKm, gmst) {
29425
- const cosLat = Math.cos(latRad);
29426
- const sinLat = Math.sin(latRad);
29427
- const cosLon = Math.cos(lonRad + gmst);
29428
- const sinLon = Math.sin(lonRad + gmst);
29429
- const r = Earth.radiusMean + altKm;
29430
- return new Vector3D(
29431
- r * cosLat * cosLon,
29432
- r * cosLat * sinLon,
29433
- r * sinLat
29434
- );
30109
+ static geodeticToJ2000_(latRad, lonRad, altKm, epoch) {
30110
+ const lonWrapped = Math.atan2(Math.sin(lonRad), Math.cos(lonRad));
30111
+ return new Geodetic(latRad, lonWrapped, altKm).toITRF(epoch).toJ2000().position;
29435
30112
  }
29436
30113
  /**
29437
30114
  * Compute the orbital velocity vector at the insertion point.
@@ -29552,390 +30229,175 @@ var LaunchTrajectoryGenerator = class _LaunchTrajectoryGenerator {
29552
30229
  }
29553
30230
  };
29554
30231
 
29555
- // src/orbit-design/OrbitFinder.ts
29556
- var OrbitFinder = class _OrbitFinder {
29557
- static MAX_LAT_ERROR = 0.025;
29558
- static MAX_LON_ERROR = 0.025;
29559
- static MAX_ALT_ERROR = 10;
29560
- static MAX_ITERATIONS = 5e3;
29561
- static COARSE_STEP = 1;
29562
- // degrees
29563
- static FINE_STEP = 5e-3;
29564
- // degrees
29565
- sat_;
29566
- goalParams_;
29567
- now_;
29568
- goalDirection_;
29569
- debugCallback_;
29570
- currentParams_;
29571
- lastLatitude_ = null;
29572
- currentDirection_ = null;
29573
- constructor(sat, goalLat, goalLon, goalDirection, now, goalAlt, raanOffset = 0, options) {
29574
- this.sat_ = sat;
29575
- this.now_ = now;
29576
- this.goalDirection_ = goalDirection;
29577
- this.debugCallback_ = options?.debugCallback;
29578
- this.goalParams_ = {
29579
- meanAnomaly: 0,
29580
- argOfPerigee: sat.argOfPerigee,
29581
- raan: sat.rightAscension + raanOffset,
29582
- altitude: goalAlt || 0,
29583
- latitude: goalLat,
29584
- longitude: goalLon
30232
+ // src/orbit-design/LaunchWindow.ts
30233
+ var LaunchWindowFinder = class _LaunchWindowFinder {
30234
+ options_;
30235
+ constructor(options) {
30236
+ this.options_ = {
30237
+ searchHours: 24,
30238
+ coarseStepSec: 60,
30239
+ fineStepSec: 1,
30240
+ ...options
29585
30241
  };
29586
- this.currentParams_ = this.getCurrentOrbitParams_();
29587
30242
  }
29588
30243
  /**
29589
- * Rotates the orbit to pass over the target latitude/longitude at the specified time.
29590
- * @returns A tuple of [TleLine1, TleLine2] on success, or ['Error', message] on failure.
30244
+ * Whether the site can inject directly into the desired inclination: the
30245
+ * orbit plane must reach the site's latitude, i.e. |sin(lat)| <= sin(inc).
29591
30246
  */
29592
- rotateOrbitToLatLon() {
29593
- try {
29594
- if (this.goalParams_.altitude > 0) {
29595
- const perigeeParams = this.findPerigeePosition_();
29596
- this.debug_(`Original perigee position: ${perigeeParams.latitude} - ${perigeeParams.longitude} - ${perigeeParams.altitude}`);
29597
- this.updateOrbitWithoutDirectionCheck_({
29598
- meanAnomaly: 0
29599
- });
29600
- this.debug_(`Positioned at initial perigee: ${this.currentParams_.latitude} - ${this.currentParams_.longitude} - ${this.currentParams_.altitude}`);
29601
- let bestArgPerigee = this.currentParams_.argOfPerigee;
29602
- let bestLatError = Infinity;
29603
- for (let argPer = 0; argPer < 360; argPer += 0.25) {
29604
- this.updateOrbitWithoutDirectionCheck_({
29605
- meanAnomaly: 0,
29606
- argOfPerigee: argPer
29607
- });
29608
- const latError = Math.abs(this.currentParams_.latitude - perigeeParams.latitude);
29609
- if (latError < bestLatError) {
29610
- bestLatError = latError;
29611
- bestArgPerigee = argPer;
29612
- this.debug_(`New best arg perigee: ${argPer} with lat error ${latError}`);
29613
- }
29614
- if (latError <= _OrbitFinder.MAX_LAT_ERROR) {
29615
- break;
29616
- }
29617
- }
29618
- this.updateOrbitWithoutDirectionCheck_({
29619
- meanAnomaly: 0,
29620
- argOfPerigee: bestArgPerigee
29621
- });
29622
- this.debug_(`After arg perigee adjustment: ${this.currentParams_.latitude} - ${this.currentParams_.longitude} - ${this.currentParams_.altitude}`);
29623
- this.lastLatitude_ = null;
29624
- this.currentDirection_ = null;
29625
- const finalMeanAResult = this.meanACalcLoop_(this.goalDirection_);
29626
- if (finalMeanAResult !== 1 /* Success */) {
29627
- return ["Error", "Failed to find final target position"];
29628
- }
29629
- const successfulDirection = this.currentDirection_;
29630
- const newRaan = this.linearSearchRaan_();
29631
- this.currentDirection_ = successfulDirection;
29632
- const finalSuccess = this.updateOrbit_({
29633
- meanAnomaly: this.currentParams_.meanAnomaly,
29634
- raan: newRaan,
29635
- argOfPerigee: bestArgPerigee
29636
- });
29637
- if (!finalSuccess) {
29638
- return ["Error", "Final position adjustment failed"];
29639
- }
29640
- this.debug_(`Final position: ${this.currentParams_.latitude}, ${this.currentParams_.longitude}, ${this.currentParams_.altitude}`);
29641
- this.debug_(`Target altitude: ${this.goalParams_.altitude}, Current altitude: ${this.currentParams_.altitude}`);
29642
- } else {
29643
- const result = this.meanACalcLoop_(this.goalDirection_);
29644
- if (result !== 1 /* Success */) {
29645
- return ["Error", `Failed to find solution with ${this.goalDirection_} bound direction`];
29646
- }
29647
- const meanAnomalySuccess = this.updateOrbit_({
29648
- meanAnomaly: this.currentParams_.meanAnomaly
29649
- });
29650
- if (!meanAnomalySuccess) {
29651
- return ["Error", "Mean anomaly adjustment resulted in incorrect direction"];
29652
- }
29653
- const successfulDirection = this.currentDirection_;
29654
- const newRaan = this.linearSearchRaan_();
29655
- this.currentDirection_ = successfulDirection;
29656
- const combinedSuccess = this.updateOrbit_({
29657
- meanAnomaly: this.currentParams_.meanAnomaly,
29658
- raan: newRaan
29659
- });
29660
- if (!combinedSuccess) {
29661
- return ["Error", "Combined mean anomaly and RAAN adjustment failed"];
29662
- }
29663
- }
29664
- return [
29665
- this.generateTle1_(),
29666
- this.generateTle2_(this.currentParams_)
29667
- ];
29668
- } catch (error) {
29669
- this.debug_(`Error in rotateOrbitToLatLon: ${error.message}`);
29670
- return ["Error", error.message];
29671
- }
29672
- }
29673
- debug_(message) {
29674
- if (this.debugCallback_) {
29675
- this.debugCallback_(message);
30247
+ isGeometryPossible() {
30248
+ const sinInc = Math.sin(this.options_.inclination * DEG2RAD);
30249
+ if (sinInc === 0) {
30250
+ return this.options_.siteLat === 0;
29676
30251
  }
30252
+ return Math.abs(Math.sin(this.options_.siteLat * DEG2RAD) / sinInc) <= 1;
29677
30253
  }
29678
- calculateTimeVariables_(date, satrec) {
29679
- const j = jday(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()) + date.getUTCMilliseconds() * MILLISECONDS_TO_DAYS;
29680
- const gmst = Sgp4.gstime(j);
29681
- const m = satrec ? (j - satrec.jdsatepoch) * MINUTES_PER_DAY : null;
29682
- return { gmst, m, j };
29683
- }
29684
- getCurrentOrbitParams_() {
29685
- if (!this.sat_?.satrec) {
29686
- throw new Error("Satellite data is not available");
29687
- }
29688
- const { m, gmst } = this.calculateTimeVariables_(this.now_, this.sat_.satrec);
29689
- if (m === null) {
29690
- throw new Error("Invalid time variables");
29691
- }
29692
- const positionEci = Sgp4.propagate(this.sat_.satrec, m).position;
29693
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29694
- return {
29695
- meanAnomaly: this.sat_.meanAnomaly,
29696
- argOfPerigee: this.sat_.argOfPerigee,
29697
- raan: this.sat_.rightAscension,
29698
- altitude: alt,
29699
- latitude: lat,
29700
- longitude: lon
29701
- };
30254
+ /**
30255
+ * RAAN of the (only) plane with the configured inclination that contains the
30256
+ * launch site at the given time on the configured leg. (deg, 0-360)
30257
+ */
30258
+ achievableRaan(time) {
30259
+ const inc = this.options_.inclination * DEG2RAD;
30260
+ const lat = this.options_.siteLat * DEG2RAD;
30261
+ const sinU = Math.sin(lat) / Math.sin(inc);
30262
+ const u = this.options_.direction === "N" ? Math.asin(sinU) : Math.PI - Math.asin(sinU);
30263
+ const nodeToSite = Math.atan2(Math.sin(u) * Math.cos(inc), Math.cos(u));
30264
+ const gmst = Sgp4.gstime(_LaunchWindowFinder.julianDate_(time));
30265
+ const lst = gmst + this.options_.siteLon * DEG2RAD;
30266
+ return _LaunchWindowFinder.normalizeDeg_((lst - nodeToSite) * RAD2DEG);
29702
30267
  }
29703
- determineDirection_(newLat) {
29704
- if (this.lastLatitude_ === null) {
29705
- this.debug_(`Initial latitude: ${newLat}`);
29706
- this.lastLatitude_ = newLat;
30268
+ /**
30269
+ * Scans the search window for the launch time minimizing |ΔRAAN| to the
30270
+ * target plane, then refines around the best coarse sample. Returns null
30271
+ * when the geometry is impossible (inclination below site latitude).
30272
+ */
30273
+ findBestLaunchTime() {
30274
+ if (!this.isGeometryPossible()) {
29707
30275
  return null;
29708
30276
  }
29709
- if (this.currentDirection_ && Math.abs(newLat - this.lastLatitude_) < 0.01) {
29710
- return this.currentDirection_;
29711
- }
29712
- const direction = newLat > this.lastLatitude_ ? "N" : "S";
29713
- this.debug_(`Current latitude: ${this.lastLatitude_} - New latitude: ${newLat} - Direction: ${direction}`);
29714
- this.lastLatitude_ = newLat;
29715
- this.debug_(`New direction: ${direction}`);
29716
- return direction;
29717
- }
29718
- isCorrectDirection_() {
29719
- this.debug_(`Current direction: ${this.currentDirection_} - Goal direction: ${this.goalDirection_}`);
29720
- return this.currentDirection_ === this.goalDirection_;
29721
- }
29722
- updateOrbit_(newParams) {
29723
- const tle1 = this.generateTle1_();
29724
- const tle2 = this.generateTle2_(newParams);
29725
- const satrec = Sgp4.createSatrec(tle1, tle2);
29726
- if (!satrec) {
29727
- throw new Error("Invalid orbit parameters");
29728
- }
29729
- const { m, gmst } = this.calculateTimeVariables_(this.now_, satrec);
29730
- if (m === null) {
29731
- throw new Error("Invalid time variables");
29732
- }
29733
- const positionEci = Sgp4.propagate(satrec, m).position;
29734
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29735
- const newDirection = this.determineDirection_(lat);
29736
- if (newDirection !== null) {
29737
- this.currentDirection_ = newDirection;
29738
- }
29739
- this.currentParams_ = {
29740
- ...this.currentParams_,
29741
- ...newParams,
29742
- latitude: lat,
29743
- longitude: lon,
29744
- altitude: alt
29745
- };
29746
- return this.isCorrectDirection_();
29747
- }
29748
- meanACalcLoop_(direction) {
29749
- const startVal = direction === "N" ? 0 : 180;
29750
- const endVal = direction === "N" ? 360 : 540;
29751
- for (let posVal = startVal * 10; posVal < endVal * 10; posVal += 0.25) {
29752
- const normalizedVal = posVal % (360 * 10);
29753
- const result = this.meanACalc_(normalizedVal, this.now_);
29754
- if (result === 1 /* Success */) {
29755
- if (this.currentDirection_ !== direction) {
29756
- posVal += 20;
29757
- } else {
29758
- return 1 /* Success */;
29759
- }
29760
- }
29761
- if (result === 3 /* Far */) {
29762
- posVal += 100;
29763
- }
29764
- if (result === 2 /* Error */) {
29765
- return 2 /* Error */;
29766
- }
29767
- }
29768
- return 0 /* Near */;
29769
- }
29770
- meanACalc_(meana, now) {
29771
- meana /= 10;
29772
- meana %= 360;
29773
- const tle1 = this.generateTle1_();
29774
- const tle2 = this.generateTle2_({ meanAnomaly: meana });
29775
- const satrec = Sgp4.createSatrec(tle1, tle2);
29776
- if (!satrec) {
29777
- return 2 /* Error */;
29778
- }
29779
- const { m, gmst } = this.calculateTimeVariables_(now, satrec);
29780
- if (m === null) {
29781
- return 2 /* Error */;
29782
- }
29783
- const positionEci = Sgp4.propagate(satrec, m).position;
29784
- const { lat } = eci2lla(positionEci, gmst);
29785
- if (this.lastLatitude_ !== null) {
29786
- if (Math.abs(lat - this.lastLatitude_) > 1e-3) {
29787
- this.currentDirection_ = lat > this.lastLatitude_ ? "N" : "S";
29788
- }
29789
- }
29790
- this.lastLatitude_ = lat;
29791
- if (Math.abs(lat - this.goalParams_.latitude) <= _OrbitFinder.MAX_LAT_ERROR) {
29792
- if (this.currentDirection_ === this.goalDirection_) {
29793
- this.currentParams_.meanAnomaly = meana;
29794
- return 1 /* Success */;
29795
- }
29796
- }
29797
- if (Math.abs(lat - this.goalParams_.latitude) > 11) {
29798
- return 3 /* Far */;
29799
- }
29800
- return 0 /* Near */;
29801
- }
29802
- linearSearchRaan_() {
29803
- let bestValue = this.currentParams_.raan;
30277
+ const startMs = this.options_.startTime.getTime();
30278
+ const endMs = startMs + this.options_.searchHours * 3600 * 1e3;
30279
+ const coarseMs = this.options_.coarseStepSec * 1e3;
30280
+ let bestMs = startMs;
29804
30281
  let bestError = Infinity;
29805
- for (let raan = 0; raan < 360; raan += _OrbitFinder.COARSE_STEP) {
29806
- this.updateOrbit_({ raan });
29807
- const error = Math.abs(this.calculateError_("raan"));
29808
- if (error < Math.abs(bestError)) {
30282
+ for (let ms = startMs; ms <= endMs; ms += coarseMs) {
30283
+ const error = Math.abs(this.raanErrorAt_(new Date(ms)));
30284
+ if (error < bestError) {
29809
30285
  bestError = error;
29810
- bestValue = raan;
29811
- }
29812
- if (error < _OrbitFinder.MAX_LON_ERROR) {
29813
- for (let fineRaan = bestValue - 5; fineRaan <= bestValue + 5; fineRaan += _OrbitFinder.FINE_STEP) {
29814
- const normalizedRaan = (fineRaan % 360 + 360) % 360;
29815
- this.updateOrbit_({ raan: normalizedRaan });
29816
- const fineError = Math.abs(this.calculateError_("raan"));
29817
- if (fineError < error) {
29818
- bestValue = normalizedRaan;
29819
- bestError = fineError;
29820
- }
29821
- }
29822
- return bestValue;
30286
+ bestMs = ms;
29823
30287
  }
29824
30288
  }
29825
- return bestValue;
29826
- }
29827
- normalizeAngleDifference_(angle1, angle2) {
29828
- const diff = (angle1 - angle2) % 360;
29829
- if (diff > 180) {
29830
- return diff - 360;
29831
- } else if (diff < -180) {
29832
- return diff + 360;
29833
- }
29834
- return diff;
29835
- }
29836
- calculateError_(param) {
29837
- switch (param) {
29838
- case "meanAnomaly":
29839
- return this.currentParams_.latitude - this.goalParams_.latitude;
29840
- case "raan":
29841
- return this.normalizeAngleDifference_(
29842
- this.currentParams_.longitude,
29843
- this.goalParams_.longitude
29844
- );
29845
- case "argOfPerigee":
29846
- this.debug_(`Current altitude: ${this.currentParams_.altitude} - Goal altitude: ${this.goalParams_.altitude}`);
29847
- return this.currentParams_.altitude - this.goalParams_.altitude;
29848
- default:
29849
- return 0;
29850
- }
29851
- }
29852
- updateOrbitWithoutDirectionCheck_(newParams) {
29853
- const tle1 = this.generateTle1_();
29854
- const tle2 = this.generateTle2_(newParams);
29855
- const satrec = Sgp4.createSatrec(tle1, tle2);
29856
- if (!satrec) {
29857
- throw new Error("Invalid orbit parameters");
29858
- }
29859
- const { m, gmst } = this.calculateTimeVariables_(this.now_, satrec);
29860
- if (m === null) {
29861
- throw new Error("Invalid time variables");
29862
- }
29863
- const positionEci = Sgp4.propagate(satrec, m).position;
29864
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29865
- this.currentParams_ = {
29866
- ...this.currentParams_,
29867
- ...newParams,
29868
- latitude: lat,
29869
- longitude: lon,
29870
- altitude: alt
29871
- };
29872
- }
29873
- findPerigeePosition_() {
29874
- let lowestAltitude = Infinity;
29875
- let perigeeParams = null;
29876
- for (let meanA = 0; meanA < 360; meanA += 0.05) {
29877
- const tle1 = this.generateTle1_();
29878
- const tle2 = this.generateTle2_({ meanAnomaly: meanA });
29879
- const satrec = Sgp4.createSatrec(tle1, tle2);
29880
- if (!satrec) {
29881
- continue;
29882
- }
29883
- const { m, gmst } = this.calculateTimeVariables_(this.now_, satrec);
29884
- if (m === null) {
29885
- throw new Error("Invalid time variables");
29886
- }
29887
- const positionEci = Sgp4.propagate(satrec, m).position;
29888
- const { lat, lon, alt } = eci2lla(positionEci, gmst);
29889
- if (alt < lowestAltitude) {
29890
- lowestAltitude = alt;
29891
- perigeeParams = {
29892
- meanAnomaly: meanA,
29893
- argOfPerigee: this.currentParams_.argOfPerigee,
29894
- raan: this.currentParams_.raan,
29895
- altitude: alt,
29896
- latitude: lat,
29897
- longitude: lon
29898
- };
30289
+ const fineMs = this.options_.fineStepSec * 1e3;
30290
+ const refineStart = Math.max(startMs, bestMs - coarseMs);
30291
+ const refineEnd = Math.min(endMs, bestMs + coarseMs);
30292
+ for (let ms = refineStart; ms <= refineEnd; ms += fineMs) {
30293
+ const error = Math.abs(this.raanErrorAt_(new Date(ms)));
30294
+ if (error < bestError) {
30295
+ bestError = error;
30296
+ bestMs = ms;
29899
30297
  }
29900
30298
  }
29901
- if (!perigeeParams) {
29902
- throw new Error("Failed to find perigee position");
29903
- }
29904
- this.debug_(`Found perigee at altitude ${perigeeParams.altitude} km`);
29905
- return perigeeParams;
30299
+ const time = new Date(bestMs);
30300
+ const achievableRaan = this.achievableRaan(time);
30301
+ const targetRaan = this.targetRaanAt_(time);
30302
+ return {
30303
+ time,
30304
+ achievableRaan,
30305
+ targetRaan,
30306
+ raanError: _LaunchWindowFinder.normalize180_(achievableRaan - targetRaan)
30307
+ };
29906
30308
  }
29907
30309
  /**
29908
- * Returns the 5-char satnum to embed in the synthesized TLEs.
29909
- *
29910
- * Why not {@link Satellite.sccNum}? For extended (7+ digit) IDs sccNum is
29911
- * the full 9-digit canonical value, which would push the TLE past 69 chars
29912
- * and break Sgp4.createSatrec. The input tle1 always has a valid 5-char
29913
- * satnum at columns 2-6, and SGP4 itself doesn't care what value lives
29914
- * there as long as line 1 and line 2 agree — so use that.
30310
+ * Mean RAAN of a satellite at a given time: the TLE epoch value advanced by
30311
+ * the secular nodal drift (J2). Suitable as the `targetRaan` function so the
30312
+ * target plane's regression over the search window is accounted for.
29915
30313
  */
29916
- tleSatNum_() {
29917
- return this.sat_.tle1.substring(2, 7);
30314
+ static meanRaanAt(satrec, time) {
30315
+ const minutesSinceEpoch = (_LaunchWindowFinder.julianDate_(time) - satrec.jdsatepoch) * MINUTES_PER_DAY;
30316
+ return _LaunchWindowFinder.normalizeDeg_((satrec.nodeo + satrec.nodedot * minutesSinceEpoch) * RAD2DEG);
29918
30317
  }
29919
- generateTle1_() {
29920
- return `1 ${this.tleSatNum_()}U ${this.sat_.tle1.substring(9, 17)} ${this.sat_.tle1.substring(18, 32)}${this.sat_.tle1.substring(32, 71)}`;
30318
+ targetRaanAt_(time) {
30319
+ const target = this.options_.targetRaan;
30320
+ return typeof target === "function" ? target(time) : target;
29921
30321
  }
29922
- generateTle2_(newParams) {
29923
- const mergedParams = {
29924
- ...this.currentParams_,
29925
- // Use current parameters as base
29926
- ...newParams
29927
- // Override with any provided parameters
29928
- };
29929
- const inc = this.sat_.inclination.toFixed(4).padStart(8, "0");
29930
- const raan = mergedParams.raan.toFixed(4).padStart(8, "0");
29931
- const ecc = this.sat_.eccentricity.toFixed(7).substring(2, 9);
29932
- const argPer = mergedParams.argOfPerigee.toFixed(4).padStart(8, "0");
29933
- const meanA = mergedParams.meanAnomaly.toFixed(4).padStart(8, "0");
29934
- const meanMo = this.sat_.tle2.substring(52, 63);
29935
- return `2 ${this.tleSatNum_()} ${inc} ${raan} ${ecc} ${argPer} ${meanA} ${meanMo} 10`;
30322
+ raanErrorAt_(time) {
30323
+ return _LaunchWindowFinder.normalize180_(this.achievableRaan(time) - this.targetRaanAt_(time));
30324
+ }
30325
+ static julianDate_(time) {
30326
+ return jday(time.getUTCFullYear(), time.getUTCMonth() + 1, time.getUTCDate(), time.getUTCHours(), time.getUTCMinutes(), time.getUTCSeconds()) + time.getUTCMilliseconds() * MILLISECONDS_TO_DAYS;
30327
+ }
30328
+ static normalizeDeg_(deg) {
30329
+ return (deg % 360 + 360) % 360;
30330
+ }
30331
+ static normalize180_(deg) {
30332
+ const normalized = (deg % 360 + 360) % 360;
30333
+ return normalized >= 180 ? normalized - 360 : normalized;
29936
30334
  }
29937
30335
  };
29938
30336
 
30337
+ // src/orbit-design/GroundTrackStateVector.ts
30338
+ var WGS84_A = 6378.137;
30339
+ var WGS84_F = 1 / 298.257223563;
30340
+ var WGS84_E2 = 2 * WGS84_F - WGS84_F * WGS84_F;
30341
+ var MIN_ECCENTRICITY = 1e-3;
30342
+ var groundTrackStateVector = (opts) => {
30343
+ const { semimajorAxisKm: a, inclinationRad: inc, latRad, lonRad, gmstRad, direction } = opts;
30344
+ const e = Math.max(opts.eccentricity, MIN_ECCENTRICITY);
30345
+ const rPerigee = a * (1 - e);
30346
+ const vPerigee = Math.sqrt(Earth.mu / a * ((1 + e) / (1 - e)));
30347
+ const sinLat = Math.sin(latRad);
30348
+ const cosLat = Math.cos(latRad);
30349
+ const nPrime = WGS84_A / Math.sqrt(1 - WGS84_E2 * sinLat * sinLat);
30350
+ const mPrime = nPrime * (1 - WGS84_E2);
30351
+ const c = cosLat * cosLat;
30352
+ const s = sinLat * sinLat;
30353
+ const bCoef = c * nPrime + s * mPrime;
30354
+ const cCoef = c * nPrime * nPrime + s * mPrime * mPrime - rPerigee * rPerigee;
30355
+ const disc = bCoef * bCoef - cCoef;
30356
+ if (disc < 0) {
30357
+ return null;
30358
+ }
30359
+ const height = -bCoef + Math.sqrt(disc);
30360
+ const cosLonE = Math.cos(lonRad);
30361
+ const sinLonE = Math.sin(lonRad);
30362
+ const xEcef = (nPrime + height) * cosLat * cosLonE;
30363
+ const yEcef = (nPrime + height) * cosLat * sinLonE;
30364
+ const zEcef = (mPrime + height) * sinLat;
30365
+ const cosG = Math.cos(gmstRad);
30366
+ const sinG = Math.sin(gmstRad);
30367
+ const position = {
30368
+ x: xEcef * cosG - yEcef * sinG,
30369
+ y: xEcef * sinG + yEcef * cosG,
30370
+ z: zEcef
30371
+ };
30372
+ const rMag = Math.hypot(position.x, position.y, position.z);
30373
+ const rHat = { x: position.x / rMag, y: position.y / rMag, z: position.z / rMag };
30374
+ const geocentricLat = Math.asin(rHat.z);
30375
+ const cosGeoLat = Math.cos(geocentricLat);
30376
+ const sinAz = Math.cos(inc) / cosGeoLat;
30377
+ if (sinAz < -1 || sinAz > 1) {
30378
+ return null;
30379
+ }
30380
+ const cosAz = (direction === "N" ? 1 : -1) * Math.sqrt(Math.max(0, 1 - sinAz * sinAz));
30381
+ const eastMag = Math.hypot(rHat.x, rHat.y);
30382
+ const eastHat = eastMag > 1e-9 ? { x: -rHat.y / eastMag, y: rHat.x / eastMag, z: 0 } : { x: 1, y: 0, z: 0 };
30383
+ const northHat = {
30384
+ x: rHat.y * eastHat.z - rHat.z * eastHat.y,
30385
+ y: rHat.z * eastHat.x - rHat.x * eastHat.z,
30386
+ z: rHat.x * eastHat.y - rHat.y * eastHat.x
30387
+ };
30388
+ const velocity = {
30389
+ x: vPerigee * (sinAz * eastHat.x + cosAz * northHat.x),
30390
+ y: vPerigee * (sinAz * eastHat.y + cosAz * northHat.y),
30391
+ z: vPerigee * (sinAz * eastHat.z + cosAz * northHat.z)
30392
+ };
30393
+ return { position, velocity };
30394
+ };
30395
+ var semimajorAxisFromMeanMotion = (meanMotionRevPerDay) => {
30396
+ const n = meanMotionRevPerDay * 2 * Math.PI / 86400;
30397
+ return (Earth.mu / (n * n)) ** (1 / 3);
30398
+ };
30399
+ var perigeeSpeed = (semimajorAxisKm, eccentricity) => Math.sqrt(Earth.mu / semimajorAxisKm * ((1 + eccentricity) / (1 - eccentricity)));
30400
+
29939
30401
  // src/orbit-design/RepeatGroundTrack.ts
29940
30402
  var SUN_SYNC_PRECESSION_RAD_PER_SEC = 0.9856 * DEG2RAD / secondsPerDay;
29941
30403
  var RepeatGroundTrack = class _RepeatGroundTrack {
@@ -30222,6 +30684,688 @@ var RepeatGroundTrack = class _RepeatGroundTrack {
30222
30684
  return a;
30223
30685
  }
30224
30686
  };
30687
+
30688
+ // src/external/Sgp4WasmBase.ts
30689
+ var DS50_UTC_JD_EPOCH = 24332815e-1;
30690
+ var ERR_SAT_TEXT_BYTES = 512;
30691
+ var Sgp4WasmBase = class _Sgp4WasmBase {
30692
+ module_ = null;
30693
+ fs_ = null;
30694
+ bindings_ = null;
30695
+ loadPromise_ = null;
30696
+ isDynArrInitialized_ = false;
30697
+ scratchKeyPtr_ = 0;
30698
+ scratchResultPtr_ = 0;
30699
+ /**
30700
+ * Converts a UTC epoch to the Astro Standards ds50UTC time system
30701
+ * (days since 1949 December 31, 00:00 UTC).
30702
+ * @param epoch The epoch to convert.
30703
+ * @returns Days since 1950 UTC.
30704
+ */
30705
+ static toDs50Utc(epoch) {
30706
+ return epoch.toJulianDate() - DS50_UTC_JD_EPOCH;
30707
+ }
30708
+ /**
30709
+ * Whether the wasm runtime has been loaded and initialized.
30710
+ */
30711
+ get isLoaded() {
30712
+ return this.module_ !== null;
30713
+ }
30714
+ /**
30715
+ * Raw Emscripten Module escape hatch (`_malloc`, `ccall`, heap views, ...).
30716
+ * @throws {Sgp4WasmError} If the module is not loaded yet.
30717
+ */
30718
+ get module() {
30719
+ if (!this.module_) {
30720
+ throw new Sgp4WasmError(`${this.constructor.name} is not loaded. Call load() first.`);
30721
+ }
30722
+ return this.module_;
30723
+ }
30724
+ /**
30725
+ * Raw Emscripten virtual filesystem escape hatch.
30726
+ * @throws {Sgp4WasmError} If the module is not loaded yet.
30727
+ */
30728
+ get fs() {
30729
+ if (!this.fs_) {
30730
+ throw new Sgp4WasmError(`${this.constructor.name} is not loaded. Call load() first.`);
30731
+ }
30732
+ return this.fs_;
30733
+ }
30734
+ /**
30735
+ * Loads and initializes the wasm runtime. Idempotent — repeat calls return
30736
+ * the same in-flight or completed load.
30737
+ * @param options Artifact sources and stdout/stderr hooks. In Node both
30738
+ * artifacts default to files next to this module; in the browser both must
30739
+ * be provided (typically URLs to statically served copies).
30740
+ * @returns This instance, once the runtime is initialized.
30741
+ */
30742
+ load(options = {}) {
30743
+ this.loadPromise_ ??= this.loadInternal_(options).catch((err) => {
30744
+ this.loadPromise_ = null;
30745
+ throw err;
30746
+ });
30747
+ return this.loadPromise_;
30748
+ }
30749
+ /**
30750
+ * Frees the dynamic array (if initialized) and drops all references to the
30751
+ * wasm runtime so it can be garbage collected. Note Emscripten linear
30752
+ * memory cannot be truly freed; a subsequent load() evaluates a fresh,
30753
+ * isolated runtime.
30754
+ */
30755
+ dispose() {
30756
+ if (this.isDynArrInitialized_ && this.bindings_) {
30757
+ this.bindings_.freeDynArr();
30758
+ }
30759
+ if (this.module_ && this.scratchKeyPtr_) {
30760
+ this.module_._free(this.scratchKeyPtr_);
30761
+ this.module_._free(this.scratchResultPtr_);
30762
+ }
30763
+ this.scratchKeyPtr_ = 0;
30764
+ this.scratchResultPtr_ = 0;
30765
+ this.isDynArrInitialized_ = false;
30766
+ this.module_ = null;
30767
+ this.fs_ = null;
30768
+ this.bindings_ = null;
30769
+ this.loadPromise_ = null;
30770
+ }
30771
+ // TLE tree management
30772
+ /**
30773
+ * Adds a single TLE to the TLE tree (`TleAddSatFrLines_wasm`).
30774
+ * @param line1 Line 1 of the TLE.
30775
+ * @param line2 Line 2 of the TLE.
30776
+ * @returns The satKey of the added TLE.
30777
+ */
30778
+ addSat(line1, line2) {
30779
+ const satKey = this.fn_().tleAddSatFrLines(line1, line2);
30780
+ if (satKey <= 0n) {
30781
+ throw new Sgp4WasmError(`TleAddSatFrLines_wasm failed for line1 "${line1}" (code ${satKey})`, Number(satKey));
30782
+ }
30783
+ return satKey;
30784
+ }
30785
+ /**
30786
+ * Adds any number of TLEs from newline-separated text
30787
+ * (`TleAddSatsFrLines_wasm`).
30788
+ *
30789
+ * The library adds TLEs individually: entries it fails to parse come back
30790
+ * as `0n` in the returned array (positions match the input order), while
30791
+ * the rest are added normally.
30792
+ * @param tleText Newline-separated TLE lines.
30793
+ * @returns One satKey per input TLE; `0n` marks a failed entry.
30794
+ */
30795
+ addSats(tleText) {
30796
+ const m = this.module;
30797
+ const numSats = _Sgp4WasmBase.countTles_(tleText);
30798
+ if (numSats === 0) {
30799
+ return [];
30800
+ }
30801
+ const textBytes = new TextEncoder().encode(tleText);
30802
+ return this.withMalloc_(textBytes.length + 1, (textPtr) => this.withMalloc_(numSats * 8, (keysPtr) => this.withMalloc_(ERR_SAT_TEXT_BYTES, (errPtr) => {
30803
+ m.HEAPU8.set(textBytes, textPtr);
30804
+ m.HEAPU8[textPtr + textBytes.length] = 0;
30805
+ m.HEAPU8.fill(0, errPtr, errPtr + ERR_SAT_TEXT_BYTES);
30806
+ const ret = this.fn_().tleAddSatsFrLines(textPtr, keysPtr, errPtr);
30807
+ if (ret !== 0) {
30808
+ const errLine = m.UTF8ToString(errPtr, ERR_SAT_TEXT_BYTES);
30809
+ throw new Sgp4WasmError(`TleAddSatsFrLines_wasm failed (code ${ret}); offending TLE line 1: "${errLine}"`, ret);
30810
+ }
30811
+ return this.readI64Array_(keysPtr, numSats);
30812
+ })));
30813
+ }
30814
+ /**
30815
+ * Adds any number of TLEs through the Emscripten virtual filesystem
30816
+ * (`TleLoadFileVFS_wasm`). The text is written to the VFS `inFile`, and
30817
+ * the satKeys the library writes to `outFile` are read back.
30818
+ * @param tleText Newline-separated TLE lines.
30819
+ * @returns The satKeys of all added TLEs.
30820
+ */
30821
+ loadTlesVfs(tleText) {
30822
+ this.fn_();
30823
+ const vfs = this.fs;
30824
+ try {
30825
+ vfs.writeFile("inFile", tleText);
30826
+ const ret = this.fn_().tleLoadFileVfs("inFile");
30827
+ if (ret <= 0) {
30828
+ throw new Sgp4WasmError(`TleLoadFileVFS_wasm failed (code ${ret})`, ret);
30829
+ }
30830
+ const outText = vfs.readFile("outFile", { encoding: "utf8" });
30831
+ return outText.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => BigInt(line));
30832
+ } finally {
30833
+ for (const file of ["inFile", "outFile"]) {
30834
+ if (vfs.analyzePath(file).exists) {
30835
+ vfs.unlink(file);
30836
+ }
30837
+ }
30838
+ }
30839
+ }
30840
+ /**
30841
+ * Removes TLEs from the TLE tree (`TleRemoveSats_wasm`).
30842
+ * @param satKeys The satKeys to remove.
30843
+ */
30844
+ removeSats(satKeys) {
30845
+ this.runKeysWithErrSat_((keysPtr, numKeys, errPtr) => this.fn_().tleRemoveSats(keysPtr, numKeys, errPtr), satKeys, "TleRemoveSats_wasm");
30846
+ }
30847
+ // SGP4 object lifecycle
30848
+ /**
30849
+ * Initializes SGP4 objects for previously added TLEs
30850
+ * (`Sgp4InitSats_wasm`). Must be called after adding TLEs and before
30851
+ * propagating them.
30852
+ * @param satKeys The satKeys to initialize.
30853
+ */
30854
+ initSats(satKeys) {
30855
+ this.runKeysWithErrSat_((keysPtr, numKeys, errPtr) => this.fn_().sgp4InitSats(keysPtr, numKeys, errPtr), satKeys, "Sgp4InitSats_wasm");
30856
+ }
30857
+ /**
30858
+ * Removes initialized SGP4 objects (`Sgp4RemoveSats_wasm`).
30859
+ * @param satKeys The satKeys to remove.
30860
+ */
30861
+ uninitSats(satKeys) {
30862
+ this.runKeysWithErrSat_((keysPtr, numKeys, errPtr) => this.fn_().sgp4RemoveSats(keysPtr, numKeys, errPtr), satKeys, "Sgp4RemoveSats_wasm");
30863
+ }
30864
+ // Propagation by satKey
30865
+ /**
30866
+ * Propagates satKeys with geodetic output (`Sgp4Prop_wasm`).
30867
+ * @param satKeys The satKeys to propagate (must be initialized).
30868
+ * @param startTimeMse Start time in minutes since each TLE's epoch.
30869
+ * @param propsPerSat Number of propagations per satellite.
30870
+ * @param stepSizeMin Step size in minutes between propagations.
30871
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30872
+ */
30873
+ propagate(satKeys, startTimeMse, propsPerSat, stepSizeMin) {
30874
+ const buf = this.propagateKeys_(
30875
+ (...args) => this.fn_().sgp4Prop(...args),
30876
+ satKeys,
30877
+ startTimeMse,
30878
+ propsPerSat,
30879
+ stepSizeMin,
30880
+ 11,
30881
+ "Sgp4Prop_wasm"
30882
+ );
30883
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 11);
30884
+ }
30885
+ /**
30886
+ * Propagates satKeys, position/velocity only (`Sgp4PropPosVel_wasm`).
30887
+ * @param satKeys The satKeys to propagate (must be initialized).
30888
+ * @param startTimeMse Start time in minutes since each TLE's epoch.
30889
+ * @param propsPerSat Number of propagations per satellite.
30890
+ * @param stepSizeMin Step size in minutes between propagations.
30891
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30892
+ */
30893
+ propagatePosVel(satKeys, startTimeMse, propsPerSat, stepSizeMin) {
30894
+ const buf = this.propagateKeys_(
30895
+ (...args) => this.fn_().sgp4PropPosVel(...args),
30896
+ satKeys,
30897
+ startTimeMse,
30898
+ propsPerSat,
30899
+ stepSizeMin,
30900
+ 8,
30901
+ "Sgp4PropPosVel_wasm"
30902
+ );
30903
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 8);
30904
+ }
30905
+ /**
30906
+ * Propagates satKeys from an absolute time with geodetic output
30907
+ * (`Sgp4PropDs50Utc_wasm`).
30908
+ * @param satKeys The satKeys to propagate (must be initialized).
30909
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
30910
+ * @param propsPerSat Number of propagations per satellite.
30911
+ * @param stepSizeMin Step size in minutes between propagations.
30912
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30913
+ */
30914
+ propagateDs50Utc(satKeys, startTimeDs50Utc, propsPerSat, stepSizeMin) {
30915
+ const buf = this.propagateKeys_(
30916
+ (...args) => this.fn_().sgp4PropDs50Utc(...args),
30917
+ satKeys,
30918
+ startTimeDs50Utc,
30919
+ propsPerSat,
30920
+ stepSizeMin,
30921
+ 11,
30922
+ "Sgp4PropDs50Utc_wasm"
30923
+ );
30924
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 11);
30925
+ }
30926
+ /**
30927
+ * Propagates satKeys from an absolute time, position/velocity only
30928
+ * (`Sgp4PropDs50UtcPosVel_wasm`).
30929
+ * @param satKeys The satKeys to propagate (must be initialized).
30930
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
30931
+ * @param propsPerSat Number of propagations per satellite.
30932
+ * @param stepSizeMin Step size in minutes between propagations.
30933
+ * @returns Records indexed as `[satIndex][stepIndex]`.
30934
+ */
30935
+ propagateDs50UtcPosVel(satKeys, startTimeDs50Utc, propsPerSat, stepSizeMin) {
30936
+ const buf = this.propagateKeys_(
30937
+ (...args) => this.fn_().sgp4PropDs50UtcPosVel(...args),
30938
+ satKeys,
30939
+ startTimeDs50Utc,
30940
+ propsPerSat,
30941
+ stepSizeMin,
30942
+ 8,
30943
+ "Sgp4PropDs50UtcPosVel_wasm"
30944
+ );
30945
+ return this.parseRecords_(buf, satKeys.length, propsPerSat, 8);
30946
+ }
30947
+ /**
30948
+ * Convenience single-satellite, single-step propagation.
30949
+ * @param satKey The satKey to propagate (must be initialized).
30950
+ * @param minutesSinceEpoch Minutes since the TLE epoch.
30951
+ * @returns The propagated state.
30952
+ */
30953
+ propagateOne(satKey, minutesSinceEpoch) {
30954
+ return this.propagate([satKey], minutesSinceEpoch, 1, 0)[0][0];
30955
+ }
30956
+ /**
30957
+ * Convenience single-satellite propagation to an absolute UTC epoch.
30958
+ * @param satKey The satKey to propagate (must be initialized).
30959
+ * @param epoch The UTC epoch to propagate to.
30960
+ * @returns The propagated state.
30961
+ */
30962
+ propagateEpoch(satKey, epoch) {
30963
+ return this.propagateDs50Utc([satKey], _Sgp4WasmBase.toDs50Utc(epoch), 1, 0)[0][0];
30964
+ }
30965
+ /**
30966
+ * Hot-path single-satellite propagation (`Sgp4PropPosVel_wasm`) that reuses
30967
+ * persistent scratch buffers instead of allocating per call, and returns
30968
+ * plain numbers instead of Vector3D instances. Intended for per-frame use
30969
+ * (e.g. as a drop-in backend for `Sgp4.propagate`).
30970
+ * @param satKey The satKey to propagate (must be initialized).
30971
+ * @param minutesSinceEpoch Minutes since the TLE epoch.
30972
+ * @returns Plain TEME position/velocity plus the per-record error code; a
30973
+ * nonzero function-level failure is reported as `err` too.
30974
+ */
30975
+ propagateOnePosVelFast(satKey, minutesSinceEpoch) {
30976
+ this.fn_();
30977
+ const m = this.module;
30978
+ if (!this.scratchKeyPtr_) {
30979
+ this.scratchKeyPtr_ = m._malloc(8);
30980
+ this.scratchResultPtr_ = m._malloc(8 * 8);
30981
+ }
30982
+ m.HEAP64[this.scratchKeyPtr_ >> 3] = satKey;
30983
+ const ret = m._Sgp4PropPosVel_wasm(this.scratchKeyPtr_, 1, minutesSinceEpoch, 1, 0, this.scratchResultPtr_);
30984
+ if (ret !== 0) {
30985
+ return { err: ret, position: { x: 0, y: 0, z: 0 }, velocity: { x: 0, y: 0, z: 0 } };
30986
+ }
30987
+ const heap = m.HEAPF64;
30988
+ const off = this.scratchResultPtr_ / 8;
30989
+ return {
30990
+ err: heap[off],
30991
+ position: { x: heap[off + 2], y: heap[off + 3], z: heap[off + 4] },
30992
+ velocity: { x: heap[off + 5], y: heap[off + 6], z: heap[off + 7] }
30993
+ };
30994
+ }
30995
+ // Dynamic array batch path
30996
+ /**
30997
+ * Initializes the module-global dynamic array of SGP4 objects
30998
+ * (`InitDynArr_wasm`).
30999
+ * @param initialSize Initial capacity of the array.
31000
+ */
31001
+ initDynArr(initialSize) {
31002
+ const ret = this.fn_().initDynArr(initialSize);
31003
+ if (ret !== 0) {
31004
+ throw new Sgp4WasmError(`InitDynArr_wasm failed (code ${ret}): ${SGP4_WASM_DYN_ARR_ERRORS[ret] ?? "unknown error"}`, ret);
31005
+ }
31006
+ this.isDynArrInitialized_ = true;
31007
+ }
31008
+ /**
31009
+ * Re-allocates the dynamic array to a new capacity
31010
+ * (`ReallocateDynArr_wasm`).
31011
+ * @param newSize New capacity of the array.
31012
+ */
31013
+ reallocDynArr(newSize) {
31014
+ const ret = this.fn_().reallocDynArr(newSize);
31015
+ if (ret !== 0) {
31016
+ throw new Sgp4WasmError(`ReallocateDynArr_wasm failed (code ${ret}): ${SGP4_WASM_DYN_ARR_ERRORS[ret] ?? "unknown error"}`, ret);
31017
+ }
31018
+ }
31019
+ /**
31020
+ * Frees the dynamic array (`FreeDynArr_wasm`).
31021
+ */
31022
+ freeDynArr() {
31023
+ this.fn_().freeDynArr();
31024
+ this.isDynArrInitialized_ = false;
31025
+ }
31026
+ /**
31027
+ * Current size of the dynamic array (`GetDynArrSize_wasm`).
31028
+ */
31029
+ get dynArrSize() {
31030
+ return this.fn_().getDynArrSize();
31031
+ }
31032
+ /**
31033
+ * Adds a TLE to the dynamic array (`AddSatToDynArr_wasm`).
31034
+ * @param line1 Line 1 of the TLE.
31035
+ * @param line2 Line 2 of the TLE.
31036
+ * @returns The dynamic array index of the added satellite.
31037
+ */
31038
+ addSatToDynArr(line1, line2) {
31039
+ const index = this.fn_().addSatToDynArr(line1, line2);
31040
+ if (index < 0) {
31041
+ throw new Sgp4WasmError(`AddSatToDynArr_wasm failed (code ${index}): ${SGP4_WASM_ADD_SAT_ERRORS[index] ?? "unknown error"}`, index);
31042
+ }
31043
+ return index;
31044
+ }
31045
+ /**
31046
+ * Propagates dynamic array entries, position/velocity only
31047
+ * (`Sgp4PropDynArrPosVel_wasm`).
31048
+ * @param indexes Dynamic array indices to propagate.
31049
+ * @param startTimeMse Start time in minutes since each TLE's epoch.
31050
+ * @param propsPerSat Number of propagations per satellite.
31051
+ * @param stepSizeMin Step size in minutes between propagations.
31052
+ * @returns Records indexed as `[satIndex][stepIndex]`.
31053
+ */
31054
+ propagateDynArrPosVel(indexes, startTimeMse, propsPerSat, stepSizeMin) {
31055
+ const buf = this.propagateIndexes_(
31056
+ (...args) => this.fn_().sgp4PropDynArrPosVel(...args),
31057
+ indexes,
31058
+ startTimeMse,
31059
+ propsPerSat,
31060
+ stepSizeMin,
31061
+ 8
31062
+ );
31063
+ return this.parseRecords_(buf, indexes.length, propsPerSat, 8);
31064
+ }
31065
+ /**
31066
+ * Propagates dynamic array entries from an absolute time with geodetic
31067
+ * output (`Sgp4PropDs50UtcDynArr_wasm`).
31068
+ * @param indexes Dynamic array indices to propagate.
31069
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
31070
+ * @param propsPerSat Number of propagations per satellite.
31071
+ * @param stepSizeMin Step size in minutes between propagations.
31072
+ * @returns Records indexed as `[satIndex][stepIndex]`.
31073
+ */
31074
+ propagateDs50UtcDynArr(indexes, startTimeDs50Utc, propsPerSat, stepSizeMin) {
31075
+ const buf = this.propagateIndexes_(
31076
+ (...args) => this.fn_().sgp4PropDs50UtcDynArr(...args),
31077
+ indexes,
31078
+ startTimeDs50Utc,
31079
+ propsPerSat,
31080
+ stepSizeMin,
31081
+ 11
31082
+ );
31083
+ return this.parseRecords_(buf, indexes.length, propsPerSat, 11);
31084
+ }
31085
+ /**
31086
+ * Propagates dynamic array entries from an absolute time,
31087
+ * position/velocity only (`Sgp4PropDs50UtcDynArrPosVel_wasm`).
31088
+ * @param indexes Dynamic array indices to propagate.
31089
+ * @param startTimeDs50Utc Start time in days since 1950 UTC (see {@link toDs50Utc}).
31090
+ * @param propsPerSat Number of propagations per satellite.
31091
+ * @param stepSizeMin Step size in minutes between propagations.
31092
+ * @returns Records indexed as `[satIndex][stepIndex]`.
31093
+ */
31094
+ propagateDs50UtcDynArrPosVel(indexes, startTimeDs50Utc, propsPerSat, stepSizeMin) {
31095
+ const buf = this.propagateIndexes_(
31096
+ (...args) => this.fn_().sgp4PropDs50UtcDynArrPosVel(...args),
31097
+ indexes,
31098
+ startTimeDs50Utc,
31099
+ propsPerSat,
31100
+ stepSizeMin,
31101
+ 8
31102
+ );
31103
+ return this.parseRecords_(buf, indexes.length, propsPerSat, 8);
31104
+ }
31105
+ // Logging
31106
+ /**
31107
+ * Sets the wasm library's logging level (`SetLogLevel_wasm`).
31108
+ * @param level The new log level.
31109
+ */
31110
+ setLogLevel(level) {
31111
+ const ret = this.fn_().setLogLevel(level);
31112
+ if (ret !== 0) {
31113
+ throw new Sgp4WasmError(`SetLogLevel_wasm failed (code ${ret})`, ret);
31114
+ }
31115
+ }
31116
+ // Internals
31117
+ /**
31118
+ * Counts the TLEs in newline-separated text by counting line-1 rows.
31119
+ * @param tleText Newline-separated TLE lines.
31120
+ * @returns Number of TLEs.
31121
+ */
31122
+ static countTles_(tleText) {
31123
+ return tleText.split(/\r?\n/u).filter((line) => line.trim().startsWith("1 ")).length;
31124
+ }
31125
+ static isHttpUrl_(source) {
31126
+ const href = source instanceof URL ? source.href : source;
31127
+ return /^https?:\/\//u.test(href);
31128
+ }
31129
+ fn_() {
31130
+ if (!this.bindings_) {
31131
+ throw new Sgp4WasmError(`${this.constructor.name} is not loaded. Call load() first.`);
31132
+ }
31133
+ return this.bindings_;
31134
+ }
31135
+ async loadInternal_(options) {
31136
+ const isNode = typeof process !== "undefined" && Boolean(process.versions?.node);
31137
+ const glueText = await this.resolveGlueText_(options.glue, isNode);
31138
+ const wasmBinary = await this.resolveWasmBinary_(options.wasm, isNode);
31139
+ let requireFn;
31140
+ let dirname;
31141
+ if (isNode) {
31142
+ const nodeModule = await import(
31143
+ /* webpackIgnore: true */
31144
+ 'module'
31145
+ );
31146
+ requireFn = nodeModule.createRequire(importMetaUrl);
31147
+ dirname = ".";
31148
+ }
31149
+ let markReady = () => void 0;
31150
+ let markFailed = () => void 0;
31151
+ const runtimeReady = new Promise((resolve, reject) => {
31152
+ markReady = resolve;
31153
+ markFailed = reject;
31154
+ });
31155
+ const seed = {
31156
+ wasmBinary,
31157
+ onRuntimeInitialized: () => markReady(),
31158
+ onAbort: (reason) => markFailed(new Sgp4WasmError(`${this.constructor.name} wasm runtime aborted: ${reason}`))
31159
+ };
31160
+ if (options.print) {
31161
+ seed.print = options.print;
31162
+ }
31163
+ if (options.printErr) {
31164
+ seed.printErr = options.printErr;
31165
+ }
31166
+ const evaluateGlue = new Function("Module", "require", "__dirname", `${glueText};
31167
+ return { Module: Module, FS: FS };`);
31168
+ const { Module, FS } = evaluateGlue(seed, requireFn, dirname);
31169
+ await runtimeReady;
31170
+ this.module_ = Module;
31171
+ this.fs_ = FS;
31172
+ this.bindings_ = _Sgp4WasmBase.buildBindings_(Module);
31173
+ return this;
31174
+ }
31175
+ static buildBindings_(m) {
31176
+ const num = "number";
31177
+ return {
31178
+ tleAddSatFrLines: m.cwrap("TleAddSatFrLines_wasm", num, ["string", "string"]),
31179
+ tleAddSatsFrLines: m.cwrap("TleAddSatsFrLines_wasm", num, [num, num, num]),
31180
+ tleLoadFileVfs: m.cwrap("TleLoadFileVFS_wasm", num, ["string"]),
31181
+ tleRemoveSats: m.cwrap("TleRemoveSats_wasm", num, [num, num, num]),
31182
+ sgp4InitSats: m.cwrap("Sgp4InitSats_wasm", num, [num, num, num]),
31183
+ sgp4RemoveSats: m.cwrap("Sgp4RemoveSats_wasm", num, [num, num, num]),
31184
+ sgp4Prop: m.cwrap("Sgp4Prop_wasm", num, [num, num, num, num, num, num]),
31185
+ sgp4PropPosVel: m.cwrap("Sgp4PropPosVel_wasm", num, [num, num, num, num, num, num]),
31186
+ sgp4PropDs50Utc: m.cwrap("Sgp4PropDs50Utc_wasm", num, [num, num, num, num, num, num]),
31187
+ sgp4PropDs50UtcPosVel: m.cwrap("Sgp4PropDs50UtcPosVel_wasm", num, [num, num, num, num, num, num]),
31188
+ initDynArr: m.cwrap("InitDynArr_wasm", num, [num]),
31189
+ reallocDynArr: m.cwrap("ReallocateDynArr_wasm", num, [num]),
31190
+ freeDynArr: m.cwrap("FreeDynArr_wasm", null, []),
31191
+ getDynArrSize: m.cwrap("GetDynArrSize_wasm", num, []),
31192
+ addSatToDynArr: m.cwrap("AddSatToDynArr_wasm", num, ["string", "string"]),
31193
+ sgp4PropDynArrPosVel: m.cwrap("Sgp4PropDynArrPosVel_wasm", null, [num, num, num, num, num, num]),
31194
+ sgp4PropDs50UtcDynArr: m.cwrap("Sgp4PropDs50UtcDynArr_wasm", null, [num, num, num, num, num, num]),
31195
+ sgp4PropDs50UtcDynArrPosVel: m.cwrap("Sgp4PropDs50UtcDynArrPosVel_wasm", null, [num, num, num, num, num, num]),
31196
+ setLogLevel: m.cwrap("SetLogLevel_wasm", num, [num])
31197
+ };
31198
+ }
31199
+ async resolveGlueText_(source, isNode) {
31200
+ if (source && typeof source === "object" && "text" in source) {
31201
+ return source.text;
31202
+ }
31203
+ const resolved = this.resolveDefaultSource_(source, this.defaultGlueFile_, isNode);
31204
+ if (isNode && !_Sgp4WasmBase.isHttpUrl_(resolved)) {
31205
+ return await this.readNodeFile_(resolved, "utf8");
31206
+ }
31207
+ const response = await fetch(resolved);
31208
+ if (!response.ok) {
31209
+ throw this.missingArtifactError_(resolved.toString(), `HTTP ${response.status}`);
31210
+ }
31211
+ return response.text();
31212
+ }
31213
+ async resolveWasmBinary_(source, isNode) {
31214
+ if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
31215
+ return source;
31216
+ }
31217
+ const resolved = this.resolveDefaultSource_(source, this.defaultWasmFile_, isNode);
31218
+ if (isNode && !_Sgp4WasmBase.isHttpUrl_(resolved)) {
31219
+ return await this.readNodeFile_(resolved, null);
31220
+ }
31221
+ const response = await fetch(resolved);
31222
+ if (!response.ok) {
31223
+ throw this.missingArtifactError_(resolved.toString(), `HTTP ${response.status}`);
31224
+ }
31225
+ return response.arrayBuffer();
31226
+ }
31227
+ resolveDefaultSource_(source, defaultFile, isNode) {
31228
+ if (source) {
31229
+ return source;
31230
+ }
31231
+ if (!isNode || typeof importMetaUrl !== "string") {
31232
+ throw new Sgp4WasmError(
31233
+ `${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.`
31234
+ );
31235
+ }
31236
+ return new URL(defaultFile, importMetaUrl);
31237
+ }
31238
+ async readNodeFile_(source, encoding) {
31239
+ const fsPromises = await import(
31240
+ /* webpackIgnore: true */
31241
+ 'fs/promises'
31242
+ );
31243
+ try {
31244
+ return encoding ? await fsPromises.readFile(source, encoding) : await fsPromises.readFile(source);
31245
+ } catch (err) {
31246
+ throw this.missingArtifactError_(source.toString(), err.message);
31247
+ }
31248
+ }
31249
+ missingArtifactError_(location, cause) {
31250
+ return new Sgp4WasmError(
31251
+ `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.`
31252
+ );
31253
+ }
31254
+ // Memory marshalling
31255
+ withMalloc_(bytes, fn) {
31256
+ const m = this.module;
31257
+ const ptr = m._malloc(Math.max(bytes, 1));
31258
+ try {
31259
+ return fn(ptr);
31260
+ } finally {
31261
+ m._free(ptr);
31262
+ }
31263
+ }
31264
+ withI64Array_(values, fn) {
31265
+ return this.withMalloc_(values.length * 8, (ptr) => {
31266
+ const m = this.module;
31267
+ values.forEach((value, i) => m.setValue(ptr + i * 8, BigInt(value), "i64"));
31268
+ return fn(ptr);
31269
+ });
31270
+ }
31271
+ withI32Array_(values, fn) {
31272
+ return this.withMalloc_(values.length * 4, (ptr) => {
31273
+ const m = this.module;
31274
+ values.forEach((value, i) => m.setValue(ptr + i * 4, value, "i32"));
31275
+ return fn(ptr);
31276
+ });
31277
+ }
31278
+ readI64Array_(ptr, count) {
31279
+ const m = this.module;
31280
+ const out = [];
31281
+ for (let i = 0; i < count; i++) {
31282
+ out.push(m.getValue(ptr + i * 8, "i64"));
31283
+ }
31284
+ return out;
31285
+ }
31286
+ /**
31287
+ * Allocates a result buffer of `count` doubles, runs `fn`, and copies the
31288
+ * buffer out. The HEAPF64 view is re-read from the module after the call
31289
+ * because wasm memory growth invalidates cached views.
31290
+ */
31291
+ withF64Result_(count, fn) {
31292
+ return this.withMalloc_(count * 8, (ptr) => {
31293
+ fn(ptr);
31294
+ return this.module.HEAPF64.slice(ptr / 8, ptr / 8 + count);
31295
+ });
31296
+ }
31297
+ runKeysWithErrSat_(op, satKeys, opName) {
31298
+ this.withI64Array_(satKeys, (keysPtr) => this.withMalloc_(8, (errPtr) => {
31299
+ const m = this.module;
31300
+ m.setValue(errPtr, 0n, "i64");
31301
+ const ret = op(keysPtr, satKeys.length, errPtr);
31302
+ if (ret !== 0) {
31303
+ const errKey = m.getValue(errPtr, "i64");
31304
+ throw new Sgp4WasmError(`${opName} failed (code ${ret}); offending satKey: ${errKey}`, ret);
31305
+ }
31306
+ }));
31307
+ }
31308
+ propagateKeys_(op, satKeys, start, propsPerSat, stepMin, width, opName) {
31309
+ return this.withI64Array_(satKeys, (keysPtr) => this.withF64Result_(satKeys.length * propsPerSat * width, (resultPtr) => {
31310
+ const ret = op(keysPtr, satKeys.length, start, propsPerSat, stepMin, resultPtr);
31311
+ if (ret !== 0) {
31312
+ throw new Sgp4WasmError(`${opName} failed (code ${ret})`, ret);
31313
+ }
31314
+ }));
31315
+ }
31316
+ propagateIndexes_(op, indexes, start, propsPerSat, stepMin, width) {
31317
+ return this.withI32Array_(indexes, (idxPtr) => this.withF64Result_(indexes.length * propsPerSat * width, (resultPtr) => {
31318
+ op(idxPtr, indexes.length, start, propsPerSat, stepMin, resultPtr);
31319
+ }));
31320
+ }
31321
+ /**
31322
+ * Parses a copied-out result buffer into per-satellite, per-step records.
31323
+ * Records are 8 doubles wide (err, time, pos, vel) or 11 doubles wide
31324
+ * (plus lat, lon, height) — the distribution docs' "7 per record"
31325
+ * precondition is stale.
31326
+ */
31327
+ parseRecords_(buf, numKeys, propsPerSat, width) {
31328
+ const results = [];
31329
+ for (let satIndex = 0; satIndex < numKeys; satIndex++) {
31330
+ const satRecords = [];
31331
+ for (let stepIndex = 0; stepIndex < propsPerSat; stepIndex++) {
31332
+ const off = (satIndex * propsPerSat + stepIndex) * width;
31333
+ const record = {
31334
+ err: buf[off],
31335
+ time: buf[off + 1],
31336
+ position: new Vector3D(buf[off + 2], buf[off + 3], buf[off + 4]),
31337
+ velocity: new Vector3D(
31338
+ buf[off + 5],
31339
+ buf[off + 6],
31340
+ buf[off + 7]
31341
+ )
31342
+ };
31343
+ if (width === 11) {
31344
+ record.llh = {
31345
+ lat: buf[off + 8],
31346
+ lon: buf[off + 9],
31347
+ height: buf[off + 10]
31348
+ };
31349
+ }
31350
+ satRecords.push(record);
31351
+ }
31352
+ results.push(satRecords);
31353
+ }
31354
+ return results;
31355
+ }
31356
+ };
31357
+
31358
+ // src/external/Sgp4Wasm.ts
31359
+ var Sgp4Wasm = class extends Sgp4WasmBase {
31360
+ defaultGlueFile_ = "Sgp4Prop.js";
31361
+ defaultWasmFile_ = "Sgp4Prop.wasm";
31362
+ };
31363
+
31364
+ // src/external/Sgp4XpWasm.ts
31365
+ var Sgp4XpWasm = class extends Sgp4WasmBase {
31366
+ defaultGlueFile_ = "Sgp4Prop.xp.js";
31367
+ defaultWasmFile_ = "Sgp4Prop.xp.wasm";
31368
+ };
30225
31369
  /**
30226
31370
  * @author Theodore Kruczek
30227
31371
  * @license AGPL-3.0-or-later
@@ -30395,6 +31539,22 @@ var RepeatGroundTrack = class _RepeatGroundTrack {
30395
31539
  * You should have received a copy of the GNU Affero General Public License along with
30396
31540
  * Orbital Object ToolKit. If not, see <http://www.gnu.org/licenses/>.
30397
31541
  */
31542
+ /**
31543
+ * @author Theodore Kruczek
31544
+ * @license AGPL-3.0-or-later
31545
+ * @copyright (c) 2026 Kruczek Labs LLC
31546
+ *
31547
+ * Orbital Object ToolKit is free software: you can redistribute it and/or modify it under the
31548
+ * terms of the GNU Affero General Public License as published by the Free Software
31549
+ * Foundation, either version 3 of the License, or (at your option) any later version.
31550
+ *
31551
+ * Orbital Object ToolKit is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
31552
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
31553
+ * See the GNU Affero General Public License for more details.
31554
+ *
31555
+ * You should have received a copy of the GNU Affero General Public License along with
31556
+ * Orbital Object ToolKit. If not, see <http://www.gnu.org/licenses/>.
31557
+ */
30398
31558
  /*! Bundled license information:
30399
31559
 
30400
31560
  astronomy-engine/esm/astronomy.js:
@@ -30518,6 +31678,7 @@ exports.LambertIOD = LambertIOD;
30518
31678
  exports.LandObject = LandObject;
30519
31679
  exports.LaserRangingSensor = LaserRangingSensor;
30520
31680
  exports.LaunchTrajectoryGenerator = LaunchTrajectoryGenerator;
31681
+ exports.LaunchWindowFinder = LaunchWindowFinder;
30521
31682
  exports.LevenbergMarquardtOD = LevenbergMarquardtOD;
30522
31683
  exports.LevenbergMarquardtResult = LevenbergMarquardtResult;
30523
31684
  exports.MILLISECONDS_PER_DAY = MILLISECONDS_PER_DAY;
@@ -30544,7 +31705,6 @@ exports.OmmParser = OmmParser;
30544
31705
  exports.OotkError = OotkError;
30545
31706
  exports.OpticalSensor = OpticalSensor;
30546
31707
  exports.OrbitDeterminationError = OrbitDeterminationError;
30547
- exports.OrbitFinder = OrbitFinder;
30548
31708
  exports.OrbitRegime = OrbitRegime;
30549
31709
  exports.PI = PI;
30550
31710
  exports.ParseError = ParseError;
@@ -30560,6 +31720,7 @@ exports.ProbabilityOfCollision = ProbabilityOfCollision;
30560
31720
  exports.PropagationError = PropagationError;
30561
31721
  exports.Propagator = Propagator;
30562
31722
  exports.PropagatorType = PropagatorType;
31723
+ exports.PropellantBudget = PropellantBudget;
30563
31724
  exports.Quaternion = Quaternion;
30564
31725
  exports.RAD2DEG = RAD2DEG;
30565
31726
  exports.RADIUS_OF_EARTH = RADIUS_OF_EARTH;
@@ -30580,6 +31741,8 @@ exports.RkResult = RkResult;
30580
31741
  exports.RungeKutta4Propagator = RungeKutta4Propagator;
30581
31742
  exports.RungeKutta89Propagator = RungeKutta89Propagator;
30582
31743
  exports.RungeKuttaAdaptive = RungeKuttaAdaptive;
31744
+ exports.SGP4_WASM_ADD_SAT_ERRORS = SGP4_WASM_ADD_SAT_ERRORS;
31745
+ exports.SGP4_WASM_DYN_ARR_ERRORS = SGP4_WASM_DYN_ARR_ERRORS;
30583
31746
  exports.SLR_WAVELENGTHS = SLR_WAVELENGTHS;
30584
31747
  exports.SPEED_OF_LIGHT_KM_S = SPEED_OF_LIGHT_KM_S;
30585
31748
  exports.Satellite = Satellite;
@@ -30594,6 +31757,12 @@ exports.Sgp4Error = Sgp4Error;
30594
31757
  exports.Sgp4ErrorCode = Sgp4ErrorCode;
30595
31758
  exports.Sgp4OpsMode = Sgp4OpsMode;
30596
31759
  exports.Sgp4Propagator = Sgp4Propagator;
31760
+ exports.Sgp4Wasm = Sgp4Wasm;
31761
+ exports.Sgp4WasmBase = Sgp4WasmBase;
31762
+ exports.Sgp4WasmError = Sgp4WasmError;
31763
+ exports.Sgp4WasmLogLevel = Sgp4WasmLogLevel;
31764
+ exports.Sgp4WasmPropagator = Sgp4WasmPropagator;
31765
+ exports.Sgp4XpWasm = Sgp4XpWasm;
30597
31766
  exports.SimpleLinearRegression = SimpleLinearRegression;
30598
31767
  exports.SolarRadiationPressure = SolarRadiationPressure;
30599
31768
  exports.SolarSystem = SolarSystem;
@@ -30614,6 +31783,7 @@ exports.TimeStamped = TimeStamped;
30614
31783
  exports.Tle = Tle;
30615
31784
  exports.Transmitter = Transmitter;
30616
31785
  exports.Transponder = Transponder;
31786
+ exports.TtcAntenna = TtcAntenna;
30617
31787
  exports.TwoBurnOrbitTransfer = TwoBurnOrbitTransfer;
30618
31788
  exports.Uranus = Uranus;
30619
31789
  exports.ValidationError = ValidationError;
@@ -30677,6 +31847,7 @@ exports.getDegLat = getDegLat;
30677
31847
  exports.getDegLon = getDegLon;
30678
31848
  exports.getRadLat = getRadLat;
30679
31849
  exports.getRadLon = getRadLon;
31850
+ exports.groundTrackStateVector = groundTrackStateVector;
30680
31851
  exports.halfPi = halfPi;
30681
31852
  exports.isLeapYear = isLeapYear;
30682
31853
  exports.jacobian = jacobian;
@@ -30699,6 +31870,7 @@ exports.normalizeAngle = normalizeAngle;
30699
31870
  exports.observationDerivative = observationDerivative;
30700
31871
  exports.observationNoiseFromSigmas = observationNoiseFromSigmas;
30701
31872
  exports.parseCenterBody = parseCenterBody;
31873
+ exports.perigeeSpeed = perigeeSpeed;
30702
31874
  exports.rad2deg = rad2deg;
30703
31875
  exports.radecToPosition = radecToPosition;
30704
31876
  exports.radecToVelocity = radecToVelocity;
@@ -30718,6 +31890,7 @@ exports.sech = sech;
30718
31890
  exports.secondsPerDay = secondsPerDay;
30719
31891
  exports.secondsPerSiderealDay = secondsPerSiderealDay;
30720
31892
  exports.secondsPerWeek = secondsPerWeek;
31893
+ exports.semimajorAxisFromMeanMotion = semimajorAxisFromMeanMotion;
30721
31894
  exports.sez2rae = sez2rae;
30722
31895
  exports.sign = sign;
30723
31896
  exports.spaceObjType2Str = spaceObjType2Str;