@openwaters/almanac 0.6.0 → 0.7.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Almanac
2
2
 
3
- Offline sky engine for the Salish Sea and everywhere else: Sun and Moon positions, rise, set, twilight, Moon phase, lunar eclipses, solar eclipses for an observer and anywhere on Earth, and fixed-star altitude and azimuth from catalog positions, computed from pure geometry with zero network and zero runtime data files.
3
+ Offline sky engine for the Salish Sea and everywhere else: Sun, Moon, and planet positions, apparent horizon dip, rise, set, twilight, Moon phase, planetary brightness, lunar and solar eclipses, and fixed-star altitude and azimuth from catalog positions, computed from pure geometry with zero network and zero runtime data files.
4
4
 
5
5
  Twin implementations, one behavior:
6
6
 
@@ -12,7 +12,7 @@ Twin implementations, one behavior:
12
12
  Supported interval: 1950-01-01T00:00Z ≤ t < 2101-01-01T00:00Z; results outside it raise a typed error. All instants are UT1-accurate, while unknown future DUT1 is outside the civil-UTC accuracy promise. See the [public contract](docs/CONTRACT.md#time-and-supported-interval) for the time model.
13
13
 
14
14
  - Contract: [`docs/CONTRACT.md`](docs/CONTRACT.md), including coordinates, public behavior, accuracy, and fixture evidence.
15
- - Scope: [`docs/ROADMAP.md`](docs/ROADMAP.md), including supported behavior and deliberate boundaries.
15
+ - Scope: [`docs/SCOPE.md`](docs/SCOPE.md), including supported behavior and deliberate boundaries.
16
16
  - Development and releases: [`CONTRIBUTING.md`](CONTRIBUTING.md), including pinned tools and required checks.
17
17
  - Landing page: [openwaters.io/sky](https://openwaters.io/sky), with the library running live in a browser.
18
18
 
@@ -21,11 +21,11 @@ Algorithms translated from [Astronomy Engine](https://github.com/cosinekitty/ast
21
21
 
22
22
  ## Performance
23
23
 
24
- Almanac includes a shared performance harness for both ports: **27 workloads**
24
+ Almanac includes a shared performance harness for both ports: **30 workloads**
25
25
  cover positions, a 228-hour sky track, short/year/polar event windows, full-range
26
26
  moon phases, next/previous/range lunar, solar, and global solar eclipse searches,
27
27
  including empty windows, solar obscuration over a 228-hour track, and the central
28
- line of the 2024-04-08 eclipse.
28
+ line of the 2024-04-08 eclipse, five-planet sky tracks, and planetary rise/set.
29
29
 
30
30
  Event searches find altitude extrema with Brent's method and altitude crossings
31
31
  with a cosine-seeded secant solver, each root proven inside a half-second
@@ -49,10 +49,10 @@ Reproduce the comparison locally from the repository root with mise 2026.9.1 or
49
49
  ```bash
50
50
  mise install
51
51
  mise exec -- npm ci --prefix typescript
52
- mise exec -- node benchmarks/run.mjs --base v0.4.1 --skip '^global/'
52
+ mise exec -- node benchmarks/run.mjs --base v0.4.1 --skip '^(global|planets)/'
53
53
  ```
54
54
 
55
- The `--skip` pattern leaves out the global solar eclipse workloads, which need a base of 0.5.0 or later. Use `--skip '^(solar|global)/'` when the base predates 0.4.0, which is when the solar eclipse workloads arrived.
55
+ The `--skip` pattern leaves out global eclipse and planetary workloads absent from that base. Use `--skip '^(solar|global|planets)/'` when the base predates 0.4.0, which is when the solar eclipse workloads arrived. For a newer base that lacks only planets, use `--skip '^planets/'`.
56
56
 
57
57
  CI runs the harness on code changes and fails on **median regressions over 20%**.
58
58
  Results include timing tables, raw samples, checksums, and revision/toolchain
@@ -98,10 +98,21 @@ for (const { kind, time } of sunEvents(today, tomorrow, observer)) {
98
98
  const { azDeg, altDeg } = starAltAz(88.792939, 7.407064, today, observer);
99
99
  ```
100
100
 
101
+ For an unobstructed sea horizon, supply eye height above the water separately from elevation above sea level. Body altitudes remain measured from the horizontal plane:
102
+
103
+ ```ts
104
+ import { horizonDip, sunAltAz } from '@openwaters/almanac';
105
+ const eyeHeightM = 2;
106
+ const viewer = { latitudeDeg: 48.5, longitudeDeg: -123.0, elevationM: 2 };
107
+ const horizonAltDeg = horizonDip(viewer, eyeHeightM);
108
+ const sunAltitudeAboveHorizonDeg = sunAltAz(new Date(), viewer).altDeg - horizonAltDeg;
109
+ const crossings = sunEvents(today, tomorrow, viewer, eyeHeightM);
110
+ ```
111
+
101
112
  ### Swift
102
113
 
103
114
  ```swift
104
- .package(url: "https://github.com/openwatersio/almanac.git", exact: "0.6.0")
115
+ .package(url: "https://github.com/openwatersio/almanac.git", exact: "0.7.0")
105
116
  ```
106
117
 
107
118
  ```swift
@@ -134,6 +145,44 @@ for event in try sunEvents(from: today, to: tomorrow, observer: observer) {
134
145
  let star = try starAltAz(raDeg: 88.792939, decDeg: 7.407064, at: today, observer: observer)
135
146
  ```
136
147
 
148
+ ### Planetary views
149
+
150
+ Mercury, Venus, Earth, Mars, Jupiter, and Saturn have geometric Sun-centered positions in AU, in fixed J2000 equatorial coordinates. Place the Sun at `(0, 0, 0)` and orient the scene's camera for the desired view:
151
+
152
+ ```ts
153
+ import { planetHeliocentricPosition, planetAltAz, planetIllumination, planetEvents,
154
+ horizonDip, sunAltAz } from '@openwaters/almanac';
155
+ import type { Planet } from '@openwaters/almanac';
156
+
157
+ const planets: Planet[] = ['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn'];
158
+ const now = new Date();
159
+ const positions = planets.map(planet => ({ planet, ...planetHeliocentricPosition(planet, now) }));
160
+
161
+ const viewer = { latitudeDeg: 48.5, longitudeDeg: -123.0, elevationM: 2 };
162
+ const horizonAltDeg = horizonDip(viewer, 2);
163
+ const venus = planetAltAz('venus', now, viewer);
164
+ const brightness = planetIllumination('venus', now);
165
+ console.log(venus.azDeg, venus.altDeg - horizonAltDeg, sunAltAz(now, viewer).altDeg,
166
+ brightness.magnitude, brightness.elongationDeg);
167
+ const crossings = planetEvents('venus', now, new Date(now.getTime() + 86400000), viewer, 2);
168
+ ```
169
+
170
+ ```swift
171
+ let now = Date()
172
+ let positions = try Planet.allCases.map { try planetHeliocentricPosition($0, at: now) }
173
+ let viewer = try Observer(latitudeDeg: 48.5, longitudeDeg: -123.0, elevationM: 2)
174
+ let horizon = try horizonDip(observer: viewer, heightAboveGroundM: 2)
175
+ let venus = try planetAltAz(.venus, at: now, observer: viewer)
176
+ let brightness = try planetIllumination(.venus, at: now)
177
+ print(venus.azDeg, venus.altDeg - horizon, brightness.magnitude, brightness.elongationDeg)
178
+ let crossings = try planetEvents(.venus, from: now, to: now.addingTimeInterval(86400),
179
+ observer: viewer, heightAboveGroundM: 2)
180
+ ```
181
+
182
+ Earth is included for the solar-system view. Earth-based position, altitude/azimuth, illumination, and rise/set reject Earth. Sky coordinates include light-time and apparent corrections; heliocentric scene coordinates do not.
183
+
184
+ A viewing-window policy can combine planet altitude above the horizon, Sun altitude, elongation, and brightness. Actual visibility also depends on weather and observing conditions. Magnitude is approximate: the pinned model and Horizons differ by up to 0.75 magnitudes for extreme Venus crescents in the fixture interval.
185
+
137
186
  ### Eclipse searches
138
187
 
139
188
  Search for a previous eclipse or all eclipses in a time window:
package/dist/eclipse.d.ts CHANGED
@@ -127,4 +127,4 @@ export declare function lunarEclipses(startUtc: Date, endUtc: Date): LunarEclips
127
127
  *
128
128
  * @throws {RangeError} if `eclipse` is structurally invalid or `observer` is out of range.
129
129
  */
130
- export declare function lunarEclipseVisibility(eclipse: LunarEclipse, observer: Observer): LunarEclipseVisibility;
130
+ export declare function lunarEclipseVisibility(eclipse: LunarEclipse, observer: Observer, heightAboveGroundM?: number): LunarEclipseVisibility;
package/dist/eclipse.js CHANGED
@@ -17,12 +17,13 @@
17
17
  // result.
18
18
  // - upstream reports semi-durations in minutes; the spec's public shape is
19
19
  // the contact instants themselves, and `obscuration` is not in it.
20
- import { assertObserver, assertSupported, assertSupportedWindowEnd, AlmanacOutOfRangeError, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
20
+ import { assertSupported, assertSupportedWindowEnd, AlmanacOutOfRangeError, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
21
21
  import { dateFromUt, ttDaysFromUt, utDays } from './time.js';
22
22
  import { KM_PER_AU, RAD2DEG } from './nutation.js';
23
23
  import { calcMoon, moonGeoVectorEqj } from './moon.js';
24
24
  import { sunGeoVectorEqj } from './sun.js';
25
25
  import { topoAltAzUnrefracted } from './transforms.js';
26
+ import { horizonDip } from './horizon.js';
26
27
  import { MOON_MEAN_RADIUS_KM, SUN_RADIUS_KM, search, searchMoonPhase } from './events.js';
27
28
  /** UPSTREAM: `EARTH_MEAN_RADIUS_KM` + `EARTH_ATMOSPHERE_KM`, astronomy.ts 142-144.
28
29
  * The 88 km of atmosphere is what makes the umbra match observed eclipse
@@ -306,19 +307,19 @@ function assertLunarEclipse(e) {
306
307
  *
307
308
  * @throws {RangeError} if `eclipse` is structurally invalid or `observer` is out of range.
308
309
  */
309
- export function lunarEclipseVisibility(eclipse, observer) {
310
+ export function lunarEclipseVisibility(eclipse, observer, heightAboveGroundM = 0) {
310
311
  assertLunarEclipse(eclipse);
311
- assertObserver(observer);
312
+ const dip = horizonDip(observer, heightAboveGroundM);
312
313
  const altAt = (d) => {
313
314
  const ut = utDays(d);
314
315
  return topoAltAzUnrefracted(moonGeoVectorEqj(ttDaysFromUt(ut)), ut, observer).altDeg;
315
316
  };
316
317
  // `== null` deliberately: the validator counts `undefined` as absent too,
317
318
  // so the reader must, or an `undefined` contact would reach `utDays` raw.
318
- const up = (d) => (d == null ? null : altAt(d) > 0);
319
+ const up = (d) => (d == null ? null : altAt(d) > dip);
319
320
  const peakAltDeg = altAt(eclipse.peak);
320
321
  return {
321
- visibleAtPeak: peakAltDeg > 0,
322
+ visibleAtPeak: peakAltDeg > dip,
322
323
  moonGeometricAltAtPeakDeg: peakAltDeg,
323
324
  contactsVisible: {
324
325
  p1: up(eclipse.p1),
package/dist/events.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Observer } from './types.js';
1
+ import { Observer, type Planet } from './types.js';
2
2
  /** A Sun event: a horizon or twilight crossing, or upper transit. */
3
3
  export type SunEventKind = 'rise' | 'set' | 'civilDawn' | 'civilDusk' | 'nauticalDawn' | 'nauticalDusk' | 'astroDawn' | 'astroDusk' | 'transit';
4
4
  export interface SunEvent {
@@ -11,6 +11,12 @@ export interface MoonEvent {
11
11
  time: Date;
12
12
  kind: MoonEventKind;
13
13
  }
14
+ /** A planet's point center crossing the apparent horizon. */
15
+ export type PlanetEventKind = 'rise' | 'set';
16
+ export interface PlanetEvent {
17
+ time: Date;
18
+ kind: PlanetEventKind;
19
+ }
14
20
  /** A quarter lunar phase. */
15
21
  export type MoonPhaseName = 'new' | 'firstQuarter' | 'full' | 'lastQuarter';
16
22
  export interface MoonPhaseEvent {
@@ -41,7 +47,7 @@ export declare function quantizedUt(ut: number): number;
41
47
  * Sun rise, set, the three twilights and upper transit within the half-open
42
48
  * window `[startUtc, endUtc)`, sorted ascending.
43
49
  *
44
- * Rise and set are the unrefracted geometric centre altitude at −(34′ + the
50
+ * Rise and set use the supplied horizon dip minus (34′ + the
45
51
  * Sun's true semidiameter at distance) — the upper-limb convention with the
46
52
  * actual disc, the same rule the Moon gets; the twilights are centre altitude
47
53
  * −6°, −12° and −18° with no refraction term; transit is local hour angle
@@ -49,14 +55,16 @@ export declare function quantizedUt(ut: number): number;
49
55
  * the crossings — while transit is reported regardless of whether the Sun is
50
56
  * above the horizon when it happens.
51
57
  */
52
- export declare function sunEvents(startUtc: Date, endUtc: Date, observer: Observer): SunEvent[];
58
+ export declare function sunEvents(startUtc: Date, endUtc: Date, observer: Observer, heightAboveGroundM?: number): SunEvent[];
53
59
  /**
54
60
  * Moonrise and moonset within the half-open window `[startUtc, endUtc)`,
55
61
  * sorted ascending — the apparent topocentric upper limb crossing the horizon,
56
62
  * so refraction (34′), topocentric parallax and the true semidiameter at the
57
63
  * Moon's distance are all included — the same upper-limb rule as the Sun's. An empty list is a valid answer.
58
64
  */
59
- export declare function moonEvents(startUtc: Date, endUtc: Date, observer: Observer): MoonEvent[];
65
+ export declare function moonEvents(startUtc: Date, endUtc: Date, observer: Observer, heightAboveGroundM?: number): MoonEvent[];
66
+ /** Rise/set of a point center at geometric altitude horizonDip minus 34 arcminutes. */
67
+ export declare function planetEvents(planet: Planet, startUtc: Date, endUtc: Date, observer: Observer, heightAboveGroundM?: number): PlanetEvent[];
60
68
  /**
61
69
  * UPSTREAM: `Search`, astronomy.ts ~4634 — bisection with quadratic
62
70
  * acceleration for the ascending zero crossing of `f` in `[t1, t2]`. Times are
package/dist/events.js CHANGED
@@ -21,13 +21,15 @@
21
21
  // `SearchMoonPhase` (~5260) and the `Search` (~4634) / `QuadInterp` (~4528)
22
22
  // root finder it calls, plus the quarter walk of `SearchMoonQuarter` (~5339)
23
23
  // and `NextMoonQuarter` (~5362).
24
- import { assertObserver, assertSupported, assertSupportedWindowEnd, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
24
+ import { assertPlanet, assertSupported, assertSupportedWindowEnd, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
25
25
  import { dateFromUt, ttDaysFromUt, utDays } from './time.js';
26
26
  import { DEG2RAD, KM_PER_AU, RAD2DEG } from './nutation.js';
27
27
  import { topoAltAzUnrefracted } from './transforms.js';
28
28
  import { sunGeoVectorEqj } from './sun.js';
29
29
  import { moonGeoVectorEqj } from './moon.js';
30
30
  import { moonPhaseDeg } from './illumination.js';
31
+ import { horizonDip } from './horizon.js';
32
+ import { planetGeoVectorEqj } from './planetModels.js';
31
33
  // ---------------------------------------------------------------- constants
32
34
  const CIVIL_ALT_DEG = -6;
33
35
  const NAUTICAL_ALT_DEG = -12;
@@ -114,24 +116,30 @@ function assertReached(condition, what) {
114
116
  if (!condition)
115
117
  throw new Error(`almanac internal: ${what} did not converge`);
116
118
  }
117
- function sunSampler(observer) {
119
+ function sunSampler(observer, dip) {
118
120
  return (ut) => {
119
121
  const p = topoAltAzUnrefracted(sunGeoVectorEqj(ttDaysFromUt(ut)), ut, observer);
120
122
  return {
121
123
  ut, altDeg: p.altDeg, hourAngleDeg: p.hourAngleDeg,
122
- riseSetAltDeg: upperLimbTargetDeg(SUN_RADIUS_KM, p.distanceAu)
124
+ riseSetAltDeg: dip + upperLimbTargetDeg(SUN_RADIUS_KM, p.distanceAu)
123
125
  };
124
126
  };
125
127
  }
126
- function moonSampler(observer) {
128
+ function moonSampler(observer, dip) {
127
129
  return (ut) => {
128
130
  const p = topoAltAzUnrefracted(moonGeoVectorEqj(ttDaysFromUt(ut)), ut, observer);
129
131
  return {
130
132
  ut, altDeg: p.altDeg, hourAngleDeg: p.hourAngleDeg,
131
- riseSetAltDeg: upperLimbTargetDeg(MOON_MEAN_RADIUS_KM, p.distanceAu)
133
+ riseSetAltDeg: dip + upperLimbTargetDeg(MOON_MEAN_RADIUS_KM, p.distanceAu)
132
134
  };
133
135
  };
134
136
  }
137
+ function planetSampler(planet, observer, dip) {
138
+ return (ut) => {
139
+ const p = topoAltAzUnrefracted(planetGeoVectorEqj(planet, ttDaysFromUt(ut)), ut, observer);
140
+ return { ut, altDeg: p.altDeg, hourAngleDeg: p.hourAngleDeg, riseSetAltDeg: dip - HORIZON_REFRACTION_DEG };
141
+ };
142
+ }
135
143
  function offsetDeg(s, level) {
136
144
  return s.altDeg - (level === null ? s.riseSetAltDeg : level);
137
145
  }
@@ -469,7 +477,7 @@ function flatCycle(observer) {
469
477
  * Sun rise, set, the three twilights and upper transit within the half-open
470
478
  * window `[startUtc, endUtc)`, sorted ascending.
471
479
  *
472
- * Rise and set are the unrefracted geometric centre altitude at −(34′ + the
480
+ * Rise and set use the supplied horizon dip minus (34′ + the
473
481
  * Sun's true semidiameter at distance) — the upper-limb convention with the
474
482
  * actual disc, the same rule the Moon gets; the twilights are centre altitude
475
483
  * −6°, −12° and −18° with no refraction term; transit is local hour angle
@@ -477,13 +485,13 @@ function flatCycle(observer) {
477
485
  * the crossings — while transit is reported regardless of whether the Sun is
478
486
  * above the horizon when it happens.
479
487
  */
480
- export function sunEvents(startUtc, endUtc, observer) {
488
+ export function sunEvents(startUtc, endUtc, observer, heightAboveGroundM = 0) {
481
489
  assertSupported(startUtc);
482
490
  assertSupportedWindowEnd(endUtc);
483
- assertObserver(observer);
491
+ const dip = horizonDip(observer, heightAboveGroundM);
484
492
  if (startUtc.getTime() >= endUtc.getTime())
485
493
  return [];
486
- return searchAltitudeEvents(sunSampler(observer), SUN_LEVELS, 'transit', utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
494
+ return searchAltitudeEvents(sunSampler(observer, dip), SUN_LEVELS, 'transit', utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
487
495
  }
488
496
  /**
489
497
  * Moonrise and moonset within the half-open window `[startUtc, endUtc)`,
@@ -491,13 +499,24 @@ export function sunEvents(startUtc, endUtc, observer) {
491
499
  * so refraction (34′), topocentric parallax and the true semidiameter at the
492
500
  * Moon's distance are all included — the same upper-limb rule as the Sun's. An empty list is a valid answer.
493
501
  */
494
- export function moonEvents(startUtc, endUtc, observer) {
502
+ export function moonEvents(startUtc, endUtc, observer, heightAboveGroundM = 0) {
503
+ assertSupported(startUtc);
504
+ assertSupportedWindowEnd(endUtc);
505
+ const dip = horizonDip(observer, heightAboveGroundM);
506
+ if (startUtc.getTime() >= endUtc.getTime())
507
+ return [];
508
+ return searchAltitudeEvents(moonSampler(observer, dip), MOON_LEVELS, null, utDays(startUtc), utDays(endUtc), MOON_HA_RATE_DEG_PER_DAY, MOON_CYCLE_DAYS, flatCycle(observer));
509
+ }
510
+ /** Rise/set of a point center at geometric altitude horizonDip minus 34 arcminutes. */
511
+ export function planetEvents(planet, startUtc, endUtc, observer, heightAboveGroundM = 0) {
512
+ assertPlanet(planet);
495
513
  assertSupported(startUtc);
496
514
  assertSupportedWindowEnd(endUtc);
497
- assertObserver(observer);
515
+ const dip = horizonDip(observer, heightAboveGroundM);
498
516
  if (startUtc.getTime() >= endUtc.getTime())
499
517
  return [];
500
- return searchAltitudeEvents(moonSampler(observer), MOON_LEVELS, null, utDays(startUtc), utDays(endUtc), MOON_HA_RATE_DEG_PER_DAY, MOON_CYCLE_DAYS, flatCycle(observer));
518
+ const cycleDays = 0.9972695717592592;
519
+ return searchAltitudeEvents(planetSampler(planet, observer, dip), MOON_LEVELS, null, utDays(startUtc), utDays(endUtc), 360 / cycleDays, cycleDays, flatCycle(observer));
501
520
  }
502
521
  // -------------------------------------------------------------- moon phases
503
522
  /**
@@ -0,0 +1,3 @@
1
+ import { Observer } from './types.js';
2
+ /** Apparent horizon altitude below the horizontal plane; eye height is measured above the unobstructed surface. */
3
+ export declare function horizonDip(observer: Observer, heightAboveGroundM: number): number;
@@ -0,0 +1,24 @@
1
+ import { assertObserver } from './types.js';
2
+ import { DEG2RAD, RAD2DEG, EARTH_FLATTENING, EARTH_EQUATORIAL_RADIUS_KM } from './nutation.js';
3
+ /** Apparent horizon altitude below the horizontal plane; eye height is measured above the unobstructed surface. */
4
+ export function horizonDip(observer, heightAboveGroundM) {
5
+ assertObserver(observer);
6
+ const elevation = observer.elevationM ?? 0;
7
+ const ground = elevation - heightAboveGroundM;
8
+ if (!(heightAboveGroundM >= 0 && heightAboveGroundM <= 10000) || !(ground >= -500 && ground <= 10000))
9
+ throw new RangeError('eye height or ground elevation out of range');
10
+ if (heightAboveGroundM === 0)
11
+ return 0;
12
+ // HorizonDipAngle: https://github.com/cosinekitty/astronomy/blob/865d3da7d8112bbc7911238052c6af4aaf877181/source/js/astronomy.ts#L5437.
13
+ const phi = observer.latitudeDeg * DEG2RAD;
14
+ const sinphi = Math.sin(phi);
15
+ const cosphi = Math.cos(phi);
16
+ const c = 1 / Math.hypot(cosphi, sinphi * EARTH_FLATTENING);
17
+ const s = c * (EARTH_FLATTENING * EARTH_FLATTENING);
18
+ const htKm = ground / 1000;
19
+ const ach = EARTH_EQUATORIAL_RADIUS_KM * c + htKm;
20
+ const ash = EARTH_EQUATORIAL_RADIUS_KM * s + htKm;
21
+ const radiusM = 1000 * Math.hypot(ach * cosphi, ash * sinphi);
22
+ const k = 0.175 * Math.pow(1 - (6.5e-3 / 283.15) * (elevation - (2 / 3) * heightAboveGroundM), 3.256);
23
+ return RAD2DEG * -(Math.sqrt(2 * (1 - k) * heightAboveGroundM / radiusM) / (1 - k));
24
+ }
@@ -1,3 +1,4 @@
1
+ import { Vec3 } from './nutation.js';
1
2
  /** Moon illumination at an instant. `phase` is 0 at new moon, 0.5 at full. */
2
3
  export interface MoonIllumination {
3
4
  fraction: number;
@@ -5,6 +6,8 @@ export interface MoonIllumination {
5
6
  phase: number;
6
7
  waxing: boolean;
7
8
  }
9
+ /** UPSTREAM: `AngleBetween`, astronomy.ts lines 256-273 — angle between two vectors, [0, 180]. */
10
+ export declare function angleBetweenDeg(a: Vec3, b: Vec3): number;
8
11
  /**
9
12
  * INTERNAL: the Moon's apparent ecliptic longitude ahead of the Sun's, [0, 360)
10
13
  * — 0 at new moon, 180 at full. The single definition of lunar phase in this
@@ -14,7 +14,7 @@ import { DEG2RAD, PrecessDirection, RAD2DEG, earthTilt, eclipticToEquatorial, gy
14
14
  import { moonGeoVectorEqj } from './moon.js';
15
15
  import { earthHelioVector, sunGeoVectorEqj } from './sun.js';
16
16
  /** UPSTREAM: `AngleBetween`, astronomy.ts lines 256-273 — angle between two vectors, [0, 180]. */
17
- function angleBetweenDeg(a, b) {
17
+ export function angleBetweenDeg(a, b) {
18
18
  const aa = a.x * a.x + a.y * a.y + a.z * a.z;
19
19
  const bb = b.x * b.x + b.y * b.y + b.z * b.z;
20
20
  const dot = (a.x * b.x + a.y * b.y + a.z * b.z) / Math.sqrt(aa * bb);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- export type { Observer } from './types.js';
1
+ export type { Observer, Planet } from './types.js';
2
+ export type { HeliocentricPosition, PlanetPosition, PlanetIllumination } from './planets.js';
3
+ export { planetHeliocentricPosition, planetPosition, planetAltAz, planetIllumination } from './planets.js';
4
+ export { horizonDip } from './horizon.js';
2
5
  export { AlmanacOutOfRangeError } from './types.js';
3
6
  export type { SunPosition, MoonPosition } from './positions.js';
4
7
  export { sunPosition, moonPosition } from './positions.js';
@@ -6,8 +9,8 @@ export type { AltAz } from './transforms.js';
6
9
  export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
7
10
  export type { MoonIllumination } from './illumination.js';
8
11
  export { moonIllumination } from './illumination.js';
9
- export type { SunEvent, SunEventKind, MoonEvent, MoonEventKind, MoonPhaseEvent, MoonPhaseName } from './events.js';
10
- export { sunEvents, moonEvents, searchMoonPhases } from './events.js';
12
+ export type { SunEvent, SunEventKind, MoonEvent, MoonEventKind, MoonPhaseEvent, MoonPhaseName, PlanetEvent, PlanetEventKind } from './events.js';
13
+ export { sunEvents, moonEvents, searchMoonPhases, planetEvents } from './events.js';
11
14
  export type { LunarEclipse, LunarEclipseVisibility } from './eclipse.js';
12
15
  export { nextLunarEclipse, previousLunarEclipse, lunarEclipses, lunarEclipseVisibility } from './eclipse.js';
13
16
  export type { SolarEclipse, SolarEclipseKind, SolarEclipseSunAltitudes } from './solar.js';
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
+ export { planetHeliocentricPosition, planetPosition, planetAltAz, planetIllumination } from './planets.js';
2
+ export { horizonDip } from './horizon.js';
1
3
  export { AlmanacOutOfRangeError } from './types.js';
2
4
  export { sunPosition, moonPosition } from './positions.js';
3
5
  export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
4
6
  export { moonIllumination } from './illumination.js';
5
- export { sunEvents, moonEvents, searchMoonPhases } from './events.js';
7
+ export { sunEvents, moonEvents, searchMoonPhases, planetEvents } from './events.js';
6
8
  export { nextLunarEclipse, previousLunarEclipse, lunarEclipses, lunarEclipseVisibility } from './eclipse.js';
7
9
  export { nextSolarEclipse, previousSolarEclipse, solarEclipses, solarObscuration } from './solar.js';
8
10
  export { nextGlobalSolarEclipse, previousGlobalSolarEclipse, globalSolarEclipses, solarEclipseAxisPoint, solarEclipseCentralLine } from './globalSolar.js';
@@ -0,0 +1,5 @@
1
+ import { type Planet } from './types.js';
2
+ import { type Vec3 } from './nutation.js';
3
+ /** INTERNAL: geometric Sun-centered vector in AU, J2000 mean equator. */
4
+ export declare function planetHelioVector(planet: Planet, tt: number): Vec3;
5
+ export declare function planetGeoVectorEqj(planet: Planet, tt: number, aberration?: boolean): Vec3;
@@ -0,0 +1,467 @@
1
+ import { earthHelioVector, vsopRotate, vsopSphereToRect } from './sun.js';
2
+ import { C_AUDAY, PI2 } from './nutation.js';
3
+ // VsopFormula / CalcVsop: pinned astronomy.ts#L3191 / #L3255.
4
+ function vsopFormula(formula, t, clampAngle) {
5
+ let tpower = 1, coord = 0;
6
+ for (const series of formula) {
7
+ let sum = 0;
8
+ for (const [ampl, phas, freq] of series)
9
+ sum += ampl * Math.cos(phas + (t * freq));
10
+ let incr = tpower * sum;
11
+ if (clampAngle)
12
+ incr %= PI2;
13
+ coord += incr;
14
+ tpower *= t;
15
+ }
16
+ return coord;
17
+ }
18
+ /** INTERNAL: geometric Sun-centered vector in AU, J2000 mean equator. */
19
+ export function planetHelioVector(planet, tt) {
20
+ if (planet === 'earth')
21
+ return earthHelioVector(tt);
22
+ const model = models[planet], t = tt / 365250;
23
+ const lon = vsopFormula(model[0], t, true);
24
+ const lat = vsopFormula(model[1], t, false);
25
+ const rad = vsopFormula(model[2], t, false);
26
+ return vsopRotate(vsopSphereToRect(lon, lat, rad));
27
+ }
28
+ // BackdatePosition / BodyPosition / CorrectLightTravel: pinned astronomy.ts#L4170–L4313.
29
+ export function planetGeoVectorEqj(planet, tt, aberration = true) {
30
+ let ltime = tt, dt = 0;
31
+ const fixedEarth = aberration ? null : earthHelioVector(tt);
32
+ for (let iter = 0; iter < 10; ++iter) {
33
+ // Backdating Earth along with the planet supplies upstream's aberration correction.
34
+ const earth = fixedEarth ?? earthHelioVector(ltime), target = planetHelioVector(planet, ltime);
35
+ const pos = { x: target.x - earth.x, y: target.y - earth.y, z: target.z - earth.z };
36
+ const lt = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) / C_AUDAY;
37
+ if (lt > 1)
38
+ throw new Error('Object is too distant for light-travel solver.');
39
+ const next = tt - lt;
40
+ dt = Math.abs(next - ltime);
41
+ if (dt < 1e-9)
42
+ return pos;
43
+ ltime = next;
44
+ }
45
+ throw new Error(`Light-travel time solver did not converge: dt = ${dt}`);
46
+ }
47
+ const models = {
48
+ mercury: [
49
+ [
50
+ [
51
+ [4.40250710144, 0.00000000000, 0.00000000000],
52
+ [0.40989414977, 1.48302034195, 26087.90314157420],
53
+ [0.05046294200, 4.47785489551, 52175.80628314840],
54
+ [0.00855346844, 1.16520322459, 78263.70942472259],
55
+ [0.00165590362, 4.11969163423, 104351.61256629678],
56
+ [0.00034561897, 0.77930768443, 130439.51570787099],
57
+ [0.00007583476, 3.71348404924, 156527.41884944518]
58
+ ],
59
+ [
60
+ [26087.90313685529, 0.00000000000, 0.00000000000],
61
+ [0.01131199811, 6.21874197797, 26087.90314157420],
62
+ [0.00292242298, 3.04449355541, 52175.80628314840],
63
+ [0.00075775081, 6.08568821653, 78263.70942472259],
64
+ [0.00019676525, 2.80965111777, 104351.61256629678]
65
+ ]
66
+ ],
67
+ [
68
+ [
69
+ [0.11737528961, 1.98357498767, 26087.90314157420],
70
+ [0.02388076996, 5.03738959686, 52175.80628314840],
71
+ [0.01222839532, 3.14159265359, 0.00000000000],
72
+ [0.00543251810, 1.79644363964, 78263.70942472259],
73
+ [0.00129778770, 4.83232503958, 104351.61256629678],
74
+ [0.00031866927, 1.58088495658, 130439.51570787099],
75
+ [0.00007963301, 4.60972126127, 156527.41884944518]
76
+ ],
77
+ [
78
+ [0.00274646065, 3.95008450011, 26087.90314157420],
79
+ [0.00099737713, 3.14159265359, 0.00000000000]
80
+ ]
81
+ ],
82
+ [
83
+ [
84
+ [0.39528271651, 0.00000000000, 0.00000000000],
85
+ [0.07834131818, 6.19233722598, 26087.90314157420],
86
+ [0.00795525558, 2.95989690104, 52175.80628314840],
87
+ [0.00121281764, 6.01064153797, 78263.70942472259],
88
+ [0.00021921969, 2.77820093972, 104351.61256629678],
89
+ [0.00004354065, 5.82894543774, 130439.51570787099]
90
+ ],
91
+ [
92
+ [0.00217347740, 4.65617158665, 26087.90314157420],
93
+ [0.00044141826, 1.42385544001, 52175.80628314840]
94
+ ]
95
+ ]
96
+ ],
97
+ venus: [
98
+ [
99
+ [
100
+ [3.17614666774, 0.00000000000, 0.00000000000],
101
+ [0.01353968419, 5.59313319619, 10213.28554621100],
102
+ [0.00089891645, 5.30650047764, 20426.57109242200],
103
+ [0.00005477194, 4.41630661466, 7860.41939243920],
104
+ [0.00003455741, 2.69964447820, 11790.62908865880],
105
+ [0.00002372061, 2.99377542079, 3930.20969621960],
106
+ [0.00001317168, 5.18668228402, 26.29831979980],
107
+ [0.00001664146, 4.25018630147, 1577.34354244780],
108
+ [0.00001438387, 4.15745084182, 9683.59458111640],
109
+ [0.00001200521, 6.15357116043, 30639.85663863300]
110
+ ],
111
+ [
112
+ [10213.28554621638, 0.00000000000, 0.00000000000],
113
+ [0.00095617813, 2.46406511110, 10213.28554621100],
114
+ [0.00007787201, 0.62478482220, 20426.57109242200]
115
+ ]
116
+ ],
117
+ [
118
+ [
119
+ [0.05923638472, 0.26702775812, 10213.28554621100],
120
+ [0.00040107978, 1.14737178112, 20426.57109242200],
121
+ [0.00032814918, 3.14159265359, 0.00000000000]
122
+ ],
123
+ [
124
+ [0.00287821243, 1.88964962838, 10213.28554621100]
125
+ ]
126
+ ],
127
+ [
128
+ [
129
+ [0.72334820891, 0.00000000000, 0.00000000000],
130
+ [0.00489824182, 4.02151831717, 10213.28554621100],
131
+ [0.00001658058, 4.90206728031, 20426.57109242200],
132
+ [0.00001378043, 1.12846591367, 11790.62908865880],
133
+ [0.00001632096, 2.84548795207, 7860.41939243920],
134
+ [0.00000498395, 2.58682193892, 9683.59458111640],
135
+ [0.00000221985, 2.01346696541, 19367.18916223280],
136
+ [0.00000237454, 2.55136053886, 15720.83878487840]
137
+ ],
138
+ [
139
+ [0.00034551041, 0.89198706276, 10213.28554621100]
140
+ ]
141
+ ]
142
+ ],
143
+ mars: [
144
+ [
145
+ [
146
+ [6.20347711581, 0.00000000000, 0.00000000000],
147
+ [0.18656368093, 5.05037100270, 3340.61242669980],
148
+ [0.01108216816, 5.40099836344, 6681.22485339960],
149
+ [0.00091798406, 5.75478744667, 10021.83728009940],
150
+ [0.00027744987, 5.97049513147, 3.52311834900],
151
+ [0.00010610235, 2.93958560338, 2281.23049651060],
152
+ [0.00012315897, 0.84956094002, 2810.92146160520],
153
+ [0.00008926784, 4.15697846427, 0.01725365220],
154
+ [0.00008715691, 6.11005153139, 13362.44970679920],
155
+ [0.00006797556, 0.36462229657, 398.14900340820],
156
+ [0.00007774872, 3.33968761376, 5621.84292321040],
157
+ [0.00003575078, 1.66186505710, 2544.31441988340],
158
+ [0.00004161108, 0.22814971327, 2942.46342329160],
159
+ [0.00003075252, 0.85696614132, 191.44826611160],
160
+ [0.00002628117, 0.64806124465, 3337.08930835080],
161
+ [0.00002937546, 6.07893711402, 0.06731030280],
162
+ [0.00002389414, 5.03896442664, 796.29800681640],
163
+ [0.00002579844, 0.02996736156, 3344.13554504880],
164
+ [0.00001528141, 1.14979301996, 6151.53388830500],
165
+ [0.00001798806, 0.65634057445, 529.69096509460],
166
+ [0.00001264357, 3.62275122593, 5092.15195811580],
167
+ [0.00001286228, 3.06796065034, 2146.16541647520],
168
+ [0.00001546404, 2.91579701718, 1751.53953141600],
169
+ [0.00001024902, 3.69334099279, 8962.45534991020],
170
+ [0.00000891566, 0.18293837498, 16703.06213349900],
171
+ [0.00000858759, 2.40093811940, 2914.01423582380],
172
+ [0.00000832715, 2.46418619474, 3340.59517304760],
173
+ [0.00000832720, 4.49495782139, 3340.62968035200],
174
+ [0.00000712902, 3.66335473479, 1059.38193018920],
175
+ [0.00000748723, 3.82248614017, 155.42039943420],
176
+ [0.00000723861, 0.67497311481, 3738.76143010800],
177
+ [0.00000635548, 2.92182225127, 8432.76438481560],
178
+ [0.00000655162, 0.48864064125, 3127.31333126180],
179
+ [0.00000550474, 3.81001042328, 0.98032106820],
180
+ [0.00000552750, 4.47479317037, 1748.01641306700],
181
+ [0.00000425966, 0.55364317304, 6283.07584999140],
182
+ [0.00000415131, 0.49662285038, 213.29909543800],
183
+ [0.00000472167, 3.62547124025, 1194.44701022460],
184
+ [0.00000306551, 0.38052848348, 6684.74797174860],
185
+ [0.00000312141, 0.99853944405, 6677.70173505060],
186
+ [0.00000293198, 4.22131299634, 20.77539549240],
187
+ [0.00000302375, 4.48618007156, 3532.06069281140],
188
+ [0.00000274027, 0.54222167059, 3340.54511639700],
189
+ [0.00000281079, 5.88163521788, 1349.86740965880],
190
+ [0.00000231183, 1.28242156993, 3870.30339179440],
191
+ [0.00000283602, 5.76885434940, 3149.16416058820],
192
+ [0.00000236117, 5.75503217933, 3333.49887969900],
193
+ [0.00000274033, 0.13372524985, 3340.67973700260],
194
+ [0.00000299395, 2.78323740866, 6254.62666252360]
195
+ ],
196
+ [
197
+ [3340.61242700512, 0.00000000000, 0.00000000000],
198
+ [0.01457554523, 3.60433733236, 3340.61242669980],
199
+ [0.00168414711, 3.92318567804, 6681.22485339960],
200
+ [0.00020622975, 4.26108844583, 10021.83728009940],
201
+ [0.00003452392, 4.73210393190, 3.52311834900],
202
+ [0.00002586332, 4.60670058555, 13362.44970679920],
203
+ [0.00000841535, 4.45864030426, 2281.23049651060]
204
+ ],
205
+ [
206
+ [0.00058152577, 2.04961712429, 3340.61242669980],
207
+ [0.00013459579, 2.45738706163, 6681.22485339960]
208
+ ]
209
+ ],
210
+ [
211
+ [
212
+ [0.03197134986, 3.76832042431, 3340.61242669980],
213
+ [0.00298033234, 4.10616996305, 6681.22485339960],
214
+ [0.00289104742, 0.00000000000, 0.00000000000],
215
+ [0.00031365539, 4.44651053090, 10021.83728009940],
216
+ [0.00003484100, 4.78812549260, 13362.44970679920]
217
+ ],
218
+ [
219
+ [0.00217310991, 6.04472194776, 3340.61242669980],
220
+ [0.00020976948, 3.14159265359, 0.00000000000],
221
+ [0.00012834709, 1.60810667915, 6681.22485339960]
222
+ ]
223
+ ],
224
+ [
225
+ [
226
+ [1.53033488271, 0.00000000000, 0.00000000000],
227
+ [0.14184953160, 3.47971283528, 3340.61242669980],
228
+ [0.00660776362, 3.81783443019, 6681.22485339960],
229
+ [0.00046179117, 4.15595316782, 10021.83728009940],
230
+ [0.00008109733, 5.55958416318, 2810.92146160520],
231
+ [0.00007485318, 1.77239078402, 5621.84292321040],
232
+ [0.00005523191, 1.36436303770, 2281.23049651060],
233
+ [0.00003825160, 4.49407183687, 13362.44970679920],
234
+ [0.00002306537, 0.09081579001, 2544.31441988340],
235
+ [0.00001999396, 5.36059617709, 3337.08930835080],
236
+ [0.00002484394, 4.92545639920, 2942.46342329160],
237
+ [0.00001960195, 4.74249437639, 3344.13554504880],
238
+ [0.00001167119, 2.11260868341, 5092.15195811580],
239
+ [0.00001102816, 5.00908403998, 398.14900340820],
240
+ [0.00000899066, 4.40791133207, 529.69096509460],
241
+ [0.00000992252, 5.83861961952, 6151.53388830500],
242
+ [0.00000807354, 2.10217065501, 1059.38193018920],
243
+ [0.00000797915, 3.44839203899, 796.29800681640],
244
+ [0.00000740975, 1.49906336885, 2146.16541647520]
245
+ ],
246
+ [
247
+ [0.01107433345, 2.03250524857, 3340.61242669980],
248
+ [0.00103175887, 2.37071847807, 6681.22485339960],
249
+ [0.00012877200, 0.00000000000, 0.00000000000],
250
+ [0.00010815880, 2.70888095665, 10021.83728009940]
251
+ ],
252
+ [
253
+ [0.00044242249, 0.47930604954, 3340.61242669980],
254
+ [0.00008138042, 0.86998389204, 6681.22485339960]
255
+ ]
256
+ ]
257
+ ],
258
+ jupiter: [
259
+ [
260
+ [
261
+ [0.59954691494, 0.00000000000, 0.00000000000],
262
+ [0.09695898719, 5.06191793158, 529.69096509460],
263
+ [0.00573610142, 1.44406205629, 7.11354700080],
264
+ [0.00306389205, 5.41734730184, 1059.38193018920],
265
+ [0.00097178296, 4.14264726552, 632.78373931320],
266
+ [0.00072903078, 3.64042916389, 522.57741809380],
267
+ [0.00064263975, 3.41145165351, 103.09277421860],
268
+ [0.00039806064, 2.29376740788, 419.48464387520],
269
+ [0.00038857767, 1.27231755835, 316.39186965660],
270
+ [0.00027964629, 1.78454591820, 536.80451209540],
271
+ [0.00013589730, 5.77481040790, 1589.07289528380],
272
+ [0.00008246349, 3.58227925840, 206.18554843720],
273
+ [0.00008768704, 3.63000308199, 949.17560896980],
274
+ [0.00007368042, 5.08101194270, 735.87651353180],
275
+ [0.00006263150, 0.02497628807, 213.29909543800],
276
+ [0.00006114062, 4.51319998626, 1162.47470440780],
277
+ [0.00004905396, 1.32084470588, 110.20632121940],
278
+ [0.00005305285, 1.30671216791, 14.22709400160],
279
+ [0.00005305441, 4.18625634012, 1052.26838318840],
280
+ [0.00004647248, 4.69958103684, 3.93215326310],
281
+ [0.00003045023, 4.31676431084, 426.59819087600],
282
+ [0.00002609999, 1.56667394063, 846.08283475120],
283
+ [0.00002028191, 1.06376530715, 3.18139373770],
284
+ [0.00001764763, 2.14148655117, 1066.49547719000],
285
+ [0.00001722972, 3.88036268267, 1265.56747862640],
286
+ [0.00001920945, 0.97168196472, 639.89728631400],
287
+ [0.00001633223, 3.58201833555, 515.46387109300],
288
+ [0.00001431999, 4.29685556046, 625.67019231240],
289
+ [0.00000973272, 4.09764549134, 95.97922721780]
290
+ ],
291
+ [
292
+ [529.69096508814, 0.00000000000, 0.00000000000],
293
+ [0.00489503243, 4.22082939470, 529.69096509460],
294
+ [0.00228917222, 6.02646855621, 7.11354700080],
295
+ [0.00030099479, 4.54540782858, 1059.38193018920],
296
+ [0.00020720920, 5.45943156902, 522.57741809380],
297
+ [0.00012103653, 0.16994816098, 536.80451209540],
298
+ [0.00006067987, 4.42422292017, 103.09277421860],
299
+ [0.00005433968, 3.98480737746, 419.48464387520],
300
+ [0.00004237744, 5.89008707199, 14.22709400160]
301
+ ],
302
+ [
303
+ [0.00047233601, 4.32148536482, 7.11354700080],
304
+ [0.00030649436, 2.92977788700, 529.69096509460],
305
+ [0.00014837605, 3.14159265359, 0.00000000000]
306
+ ]
307
+ ],
308
+ [
309
+ [
310
+ [0.02268615702, 3.55852606721, 529.69096509460],
311
+ [0.00109971634, 3.90809347197, 1059.38193018920],
312
+ [0.00110090358, 0.00000000000, 0.00000000000],
313
+ [0.00008101428, 3.60509572885, 522.57741809380],
314
+ [0.00006043996, 4.25883108339, 1589.07289528380],
315
+ [0.00006437782, 0.30627119215, 536.80451209540]
316
+ ],
317
+ [
318
+ [0.00078203446, 1.52377859742, 529.69096509460]
319
+ ]
320
+ ],
321
+ [
322
+ [
323
+ [5.20887429326, 0.00000000000, 0.00000000000],
324
+ [0.25209327119, 3.49108639871, 529.69096509460],
325
+ [0.00610599976, 3.84115365948, 1059.38193018920],
326
+ [0.00282029458, 2.57419881293, 632.78373931320],
327
+ [0.00187647346, 2.07590383214, 522.57741809380],
328
+ [0.00086792905, 0.71001145545, 419.48464387520],
329
+ [0.00072062974, 0.21465724607, 536.80451209540],
330
+ [0.00065517248, 5.97995884790, 316.39186965660],
331
+ [0.00029134542, 1.67759379655, 103.09277421860],
332
+ [0.00030135335, 2.16132003734, 949.17560896980],
333
+ [0.00023453271, 3.54023522184, 735.87651353180],
334
+ [0.00022283743, 4.19362594399, 1589.07289528380],
335
+ [0.00023947298, 0.27458037480, 7.11354700080],
336
+ [0.00013032614, 2.96042965363, 1162.47470440780],
337
+ [0.00009703360, 1.90669633585, 206.18554843720],
338
+ [0.00012749023, 2.71550286592, 1052.26838318840],
339
+ [0.00007057931, 2.18184839926, 1265.56747862640],
340
+ [0.00006137703, 6.26418240033, 846.08283475120],
341
+ [0.00002616976, 2.00994012876, 1581.95934828300]
342
+ ],
343
+ [
344
+ [0.01271801520, 2.64937512894, 529.69096509460],
345
+ [0.00061661816, 3.00076460387, 1059.38193018920],
346
+ [0.00053443713, 3.89717383175, 522.57741809380],
347
+ [0.00031185171, 4.88276958012, 536.80451209540],
348
+ [0.00041390269, 0.00000000000, 0.00000000000]
349
+ ]
350
+ ]
351
+ ],
352
+ saturn: [
353
+ [
354
+ [
355
+ [0.87401354025, 0.00000000000, 0.00000000000],
356
+ [0.11107659762, 3.96205090159, 213.29909543800],
357
+ [0.01414150957, 4.58581516874, 7.11354700080],
358
+ [0.00398379389, 0.52112032699, 206.18554843720],
359
+ [0.00350769243, 3.30329907896, 426.59819087600],
360
+ [0.00206816305, 0.24658372002, 103.09277421860],
361
+ [0.00079271300, 3.84007056878, 220.41264243880],
362
+ [0.00023990355, 4.66976924553, 110.20632121940],
363
+ [0.00016573588, 0.43719228296, 419.48464387520],
364
+ [0.00014906995, 5.76903183869, 316.39186965660],
365
+ [0.00015820290, 0.93809155235, 632.78373931320],
366
+ [0.00014609559, 1.56518472000, 3.93215326310],
367
+ [0.00013160301, 4.44891291899, 14.22709400160],
368
+ [0.00015053543, 2.71669915667, 639.89728631400],
369
+ [0.00013005299, 5.98119023644, 11.04570026390],
370
+ [0.00010725067, 3.12939523827, 202.25339517410],
371
+ [0.00005863206, 0.23656938524, 529.69096509460],
372
+ [0.00005227757, 4.20783365759, 3.18139373770],
373
+ [0.00006126317, 1.76328667907, 277.03499374140],
374
+ [0.00005019687, 3.17787728405, 433.71173787680],
375
+ [0.00004592550, 0.61977744975, 199.07200143640],
376
+ [0.00004005867, 2.24479718502, 63.73589830340],
377
+ [0.00002953796, 0.98280366998, 95.97922721780],
378
+ [0.00003873670, 3.22283226966, 138.51749687070],
379
+ [0.00002461186, 2.03163875071, 735.87651353180],
380
+ [0.00003269484, 0.77492638211, 949.17560896980],
381
+ [0.00001758145, 3.26580109940, 522.57741809380],
382
+ [0.00001640172, 5.50504453050, 846.08283475120],
383
+ [0.00001391327, 4.02333150505, 323.50541665740],
384
+ [0.00001580648, 4.37265307169, 309.27832265580],
385
+ [0.00001123498, 2.83726798446, 415.55249061210],
386
+ [0.00001017275, 3.71700135395, 227.52618943960],
387
+ [0.00000848642, 3.19150170830, 209.36694217490]
388
+ ],
389
+ [
390
+ [213.29909521690, 0.00000000000, 0.00000000000],
391
+ [0.01297370862, 1.82834923978, 213.29909543800],
392
+ [0.00564345393, 2.88499717272, 7.11354700080],
393
+ [0.00093734369, 1.06311793502, 426.59819087600],
394
+ [0.00107674962, 2.27769131009, 206.18554843720],
395
+ [0.00040244455, 2.04108104671, 220.41264243880],
396
+ [0.00019941774, 1.27954390470, 103.09277421860],
397
+ [0.00010511678, 2.74880342130, 14.22709400160],
398
+ [0.00006416106, 0.38238295041, 639.89728631400],
399
+ [0.00004848994, 2.43037610229, 419.48464387520],
400
+ [0.00004056892, 2.92133209468, 110.20632121940],
401
+ [0.00003768635, 3.64965330780, 3.93215326310]
402
+ ],
403
+ [
404
+ [0.00116441330, 1.17988132879, 7.11354700080],
405
+ [0.00091841837, 0.07325195840, 213.29909543800],
406
+ [0.00036661728, 0.00000000000, 0.00000000000],
407
+ [0.00015274496, 4.06493179167, 206.18554843720]
408
+ ]
409
+ ],
410
+ [
411
+ [
412
+ [0.04330678039, 3.60284428399, 213.29909543800],
413
+ [0.00240348302, 2.85238489373, 426.59819087600],
414
+ [0.00084745939, 0.00000000000, 0.00000000000],
415
+ [0.00030863357, 3.48441504555, 220.41264243880],
416
+ [0.00034116062, 0.57297307557, 206.18554843720],
417
+ [0.00014734070, 2.11846596715, 639.89728631400],
418
+ [0.00009916667, 5.79003188904, 419.48464387520],
419
+ [0.00006993564, 4.73604689720, 7.11354700080],
420
+ [0.00004807588, 5.43305312061, 316.39186965660]
421
+ ],
422
+ [
423
+ [0.00198927992, 4.93901017903, 213.29909543800],
424
+ [0.00036947916, 3.14159265359, 0.00000000000],
425
+ [0.00017966989, 0.51979431110, 426.59819087600]
426
+ ]
427
+ ],
428
+ [
429
+ [
430
+ [9.55758135486, 0.00000000000, 0.00000000000],
431
+ [0.52921382865, 2.39226219573, 213.29909543800],
432
+ [0.01873679867, 5.23549604660, 206.18554843720],
433
+ [0.01464663929, 1.64763042902, 426.59819087600],
434
+ [0.00821891141, 5.93520042303, 316.39186965660],
435
+ [0.00547506923, 5.01532618980, 103.09277421860],
436
+ [0.00371684650, 2.27114821115, 220.41264243880],
437
+ [0.00361778765, 3.13904301847, 7.11354700080],
438
+ [0.00140617506, 5.70406606781, 632.78373931320],
439
+ [0.00108974848, 3.29313390175, 110.20632121940],
440
+ [0.00069006962, 5.94099540992, 419.48464387520],
441
+ [0.00061053367, 0.94037691801, 639.89728631400],
442
+ [0.00048913294, 1.55733638681, 202.25339517410],
443
+ [0.00034143772, 0.19519102597, 277.03499374140],
444
+ [0.00032401773, 5.47084567016, 949.17560896980],
445
+ [0.00020936596, 0.46349251129, 735.87651353180],
446
+ [0.00009796004, 5.20477537945, 1265.56747862640],
447
+ [0.00011993338, 5.98050967385, 846.08283475120],
448
+ [0.00020839300, 1.52102476129, 433.71173787680],
449
+ [0.00015298404, 3.05943814940, 529.69096509460],
450
+ [0.00006465823, 0.17732249942, 1052.26838318840],
451
+ [0.00011380257, 1.73105427040, 522.57741809380],
452
+ [0.00003419618, 4.94550542171, 1581.95934828300]
453
+ ],
454
+ [
455
+ [0.06182981340, 0.25843511480, 213.29909543800],
456
+ [0.00506577242, 0.71114625261, 206.18554843720],
457
+ [0.00341394029, 5.79635741658, 426.59819087600],
458
+ [0.00188491195, 0.47215589652, 220.41264243880],
459
+ [0.00186261486, 3.14159265359, 0.00000000000],
460
+ [0.00143891146, 1.40744822888, 7.11354700080]
461
+ ],
462
+ [
463
+ [0.00436902572, 4.78671677509, 213.29909543800]
464
+ ]
465
+ ]
466
+ ]
467
+ };
@@ -0,0 +1,32 @@
1
+ import { type Planet, type Observer } from './types.js';
2
+ import { type AltAz } from './transforms.js';
3
+ /** Geometric Sun-centered AU vector in the fixed J2000 mean equatorial frame. */
4
+ export interface HeliocentricPosition {
5
+ xAu: number;
6
+ yAu: number;
7
+ zAu: number;
8
+ }
9
+ /** Apparent geocentric position on the true equator and equinox of date. */
10
+ export interface PlanetPosition {
11
+ raDeg: number;
12
+ decDeg: number;
13
+ distanceAu: number;
14
+ }
15
+ /** Geometric phase and approximate airless visual magnitude, plus apparent solar separation. */
16
+ export interface PlanetIllumination {
17
+ fraction: number;
18
+ phaseAngleDeg: number;
19
+ magnitude: number;
20
+ elongationDeg: number;
21
+ }
22
+ export declare function planetIlluminationAtTT(planet: Planet, tt: number): PlanetIllumination;
23
+ /** Approximate airless magnitude includes Saturn's rings; elongation is apparent separation from the Sun. */
24
+ export declare function planetIllumination(planet: Planet, time: Date): PlanetIllumination;
25
+ /** INTERNAL: TT-labeled evidence bypasses the civil-time Delta-T conversion. */
26
+ export declare function planetApparentAtTT(planet: Planet, tt: number): PlanetPosition;
27
+ /** Geometric position at the requested instant; accepts Earth. */
28
+ export declare function planetHeliocentricPosition(planet: Planet, time: Date): HeliocentricPosition;
29
+ /** Apparent geocentric position with light-time, aberration, precession, and nutation. */
30
+ export declare function planetPosition(planet: Planet, time: Date): PlanetPosition;
31
+ /** Refracted topocentric position; altitude remains relative to the horizontal plane. */
32
+ export declare function planetAltAz(planet: Planet, time: Date, observer: Observer): AltAz;
@@ -0,0 +1,104 @@
1
+ import { assertPlanet, assertSupported, assertObserver } from './types.js';
2
+ import { ttDays, ttDaysFromUt, utDays } from './time.js';
3
+ import { gyration, PrecessDirection, equatorialFromVector, DEG2RAD, RAD2DEG, earthTilt, eclipticToEquatorial } from './nutation.js';
4
+ import { planetHelioVector, planetGeoVectorEqj } from './planetModels.js';
5
+ import { topoAltAzUnrefracted, refract } from './transforms.js';
6
+ import { angleBetweenDeg } from './illumination.js';
7
+ import { earthHelioVector, sunGeoVectorEqj } from './sun.js';
8
+ // VisualMagnitude: https://github.com/cosinekitty/astronomy/blob/865d3da7d8112bbc7911238052c6af4aaf877181/source/js/astronomy.ts#L4901.
9
+ function visualMagnitude(planet, phase, helioDist, geoDist) {
10
+ let c0, c1 = 0, c2 = 0, c3 = 0;
11
+ switch (planet) {
12
+ case 'mercury':
13
+ c0 = -0.60;
14
+ c1 = 4.98;
15
+ c2 = -4.88;
16
+ c3 = 3.02;
17
+ break;
18
+ case 'venus':
19
+ if (phase < 163.6) {
20
+ c0 = -4.47;
21
+ c1 = 1.03;
22
+ c2 = 0.57;
23
+ c3 = 0.13;
24
+ }
25
+ else {
26
+ c0 = 0.98;
27
+ c1 = -1.02;
28
+ }
29
+ break;
30
+ case 'mars':
31
+ c0 = -1.52;
32
+ c1 = 1.60;
33
+ break;
34
+ case 'jupiter':
35
+ c0 = -9.40;
36
+ c1 = 0.50;
37
+ break;
38
+ default: throw new Error('unsupported planet in visualMagnitude');
39
+ }
40
+ const x = phase / 100;
41
+ let mag = c0 + x * (c1 + x * (c2 + x * c3));
42
+ mag += 5 * Math.log10(helioDist * geoDist);
43
+ return mag;
44
+ }
45
+ // SaturnMagnitude: pinned astronomy.ts#L4927; Ecliptic: #L3013.
46
+ function saturnMagnitude(phase, helioDist, geoDist, gc, tt) {
47
+ const ecl = eclipticToEquatorial(-earthTilt(tt).tobl, gyration(gc, tt, PrecessDirection.From2000));
48
+ const lat = RAD2DEG * Math.atan2(ecl.z, Math.hypot(ecl.x, ecl.y));
49
+ let lon = RAD2DEG * Math.atan2(ecl.y, ecl.x);
50
+ if (lon < 0)
51
+ lon += 360;
52
+ const ir = DEG2RAD * 28.06, nr = DEG2RAD * (169.51 + (3.82e-5 * tt));
53
+ const tilt = Math.asin(Math.sin(DEG2RAD * lat) * Math.cos(ir) - Math.cos(DEG2RAD * lat) * Math.sin(ir) * Math.sin(DEG2RAD * lon - nr));
54
+ const sinTilt = Math.sin(Math.abs(tilt));
55
+ let mag = -9 + 0.044 * phase;
56
+ mag += sinTilt * (-2.6 + 1.2 * sinTilt);
57
+ mag += 5 * Math.log10(helioDist * geoDist);
58
+ return mag;
59
+ }
60
+ // Illumination: pinned astronomy.ts#L5066; AngleFromSun: #L4869.
61
+ export function planetIlluminationAtTT(planet, tt) {
62
+ const earth = earthHelioVector(tt), hc = planetHelioVector(planet, tt);
63
+ const gc = { x: hc.x - earth.x, y: hc.y - earth.y, z: hc.z - earth.z };
64
+ const phaseAngleDeg = angleBetweenDeg(gc, hc);
65
+ const helioDist = Math.sqrt(hc.x * hc.x + hc.y * hc.y + hc.z * hc.z);
66
+ const geoDist = Math.sqrt(gc.x * gc.x + gc.y * gc.y + gc.z * gc.z);
67
+ const magnitude = planet === 'saturn' ? saturnMagnitude(phaseAngleDeg, helioDist, geoDist, gc, tt) : visualMagnitude(planet, phaseAngleDeg, helioDist, geoDist);
68
+ return { fraction: (1 + Math.cos(DEG2RAD * phaseAngleDeg)) / 2, phaseAngleDeg, magnitude,
69
+ elongationDeg: angleBetweenDeg(sunGeoVectorEqj(tt), planetGeoVectorEqj(planet, tt)) };
70
+ }
71
+ /** Approximate airless magnitude includes Saturn's rings; elongation is apparent separation from the Sun. */
72
+ export function planetIllumination(planet, time) {
73
+ assertPlanet(planet);
74
+ assertSupported(time);
75
+ return planetIlluminationAtTT(planet, ttDays(time));
76
+ }
77
+ /** INTERNAL: TT-labeled evidence bypasses the civil-time Delta-T conversion. */
78
+ export function planetApparentAtTT(planet, tt) {
79
+ const direction = equatorialFromVector(gyration(planetGeoVectorEqj(planet, tt), tt, PrecessDirection.From2000));
80
+ // Aberration changes direction, not physical range; keep Earth at reception for distance.
81
+ const range = planetGeoVectorEqj(planet, tt, false);
82
+ return { raDeg: direction.raDeg, decDeg: direction.decDeg, distanceAu: Math.hypot(range.x, range.y, range.z) };
83
+ }
84
+ /** Geometric position at the requested instant; accepts Earth. */
85
+ export function planetHeliocentricPosition(planet, time) {
86
+ assertPlanet(planet, true);
87
+ assertSupported(time);
88
+ const p = planetHelioVector(planet, ttDays(time));
89
+ return { xAu: p.x, yAu: p.y, zAu: p.z };
90
+ }
91
+ /** Apparent geocentric position with light-time, aberration, precession, and nutation. */
92
+ export function planetPosition(planet, time) {
93
+ assertPlanet(planet);
94
+ assertSupported(time);
95
+ return planetApparentAtTT(planet, ttDays(time));
96
+ }
97
+ /** Refracted topocentric position; altitude remains relative to the horizontal plane. */
98
+ export function planetAltAz(planet, time, observer) {
99
+ assertPlanet(planet);
100
+ assertSupported(time);
101
+ assertObserver(observer);
102
+ const ut = utDays(time);
103
+ return refract(topoAltAzUnrefracted(planetGeoVectorEqj(planet, ttDaysFromUt(ut)), ut, observer));
104
+ }
package/dist/solar.d.ts CHANGED
@@ -67,7 +67,7 @@ export declare function eclipseKindFromUmbra(k: number): 'total' | 'annular';
67
67
  * interval, or if no visible eclipse remains before the end of it.
68
68
  * @throws {RangeError} if `after` is invalid or `observer` is out of range.
69
69
  */
70
- export declare function nextSolarEclipse(after: Date, observer: Observer): SolarEclipse;
70
+ export declare function nextSolarEclipse(after: Date, observer: Observer, heightAboveGroundM?: number): SolarEclipse;
71
71
  /**
72
72
  * The last solar eclipse `observer` can see whose peak falls strictly before
73
73
  * `before`, skipping peaks within 100 ms of it, just as `nextSolarEclipse`
@@ -77,6 +77,6 @@ export declare function nextSolarEclipse(after: Date, observer: Observer): Solar
77
77
  * interval, or if no visible eclipse remains after the start of it.
78
78
  * @throws {RangeError} if `before` is invalid or `observer` is out of range.
79
79
  */
80
- export declare function previousSolarEclipse(before: Date, observer: Observer): SolarEclipse;
80
+ export declare function previousSolarEclipse(before: Date, observer: Observer, heightAboveGroundM?: number): SolarEclipse;
81
81
  /** Solar eclipses `observer` can see with peaks in `[startUtc, endUtc)`, sorted ascending. Contacts may fall outside the window. */
82
- export declare function solarEclipses(startUtc: Date, endUtc: Date, observer: Observer): SolarEclipse[];
82
+ export declare function solarEclipses(startUtc: Date, endUtc: Date, observer: Observer, heightAboveGroundM?: number): SolarEclipse[];
package/dist/solar.js CHANGED
@@ -34,6 +34,7 @@ import { moonGeoVectorEqj } from './moon.js';
34
34
  import { sunGeoVectorEqj } from './sun.js';
35
35
  import { observerGeoVectorEqj, refractionDeg, topoAltAzUnrefracted } from './transforms.js';
36
36
  import { MOON_MEAN_RADIUS_KM, SUN_RADIUS_KM, search, searchMoonPhase } from './events.js';
37
+ import { horizonDip } from './horizon.js';
37
38
  import { calcShadow, moonEclipticLatitudeDeg, PRUNE_LATITUDE_DEG, SAME_ECLIPSE_MS, SHADOW_TOL_SECONDS, SHADOW_ITER_CAP } from './eclipse.js';
38
39
  /** UPSTREAM: `SUN_RADIUS_AU` and `MOON_POLAR_RADIUS_AU`, astronomy.ts 135 and 149-150.
39
40
  * INTERNAL: `MOON_POLAR_RADIUS_KM` is shared with globalSolar.ts. */
@@ -66,7 +67,7 @@ function localMoonShadow(ut, observer) {
66
67
  return calcShadow(MOON_MEAN_RADIUS_KM, ut, o, hm);
67
68
  }
68
69
  /** UPSTREAM: `AngleBetween`, astronomy.ts ~256 — degrees. */
69
- function angleBetweenDeg(a, b) {
70
+ function solarAngleBetweenDeg(a, b) {
70
71
  const aa = (a.x * a.x + a.y * a.y + a.z * a.z);
71
72
  if (Math.abs(aa) < 1.0e-8)
72
73
  throw new Error('almanac internal: AngleBetween first vector is too short');
@@ -131,7 +132,7 @@ function discObscuration(hm, lo) {
131
132
  // Calculate the apparent angular radius of the Moon for the observer.
132
133
  const moonRadius = Math.asin(MOON_POLAR_RADIUS_AU / Math.hypot(lo.x, lo.y, lo.z));
133
134
  // Calculate the apparent angular separation between the Sun's center and the Moon's center.
134
- const sunMoonSeparation = angleBetweenDeg(lo, ho);
135
+ const sunMoonSeparation = solarAngleBetweenDeg(lo, ho);
135
136
  // Find the fraction of the Sun's apparent disc area that is covered by the Moon.
136
137
  return discOverlap(sunRadius, moonRadius, sunMoonSeparation * DEG2RAD);
137
138
  }
@@ -254,8 +255,8 @@ function buildSolarEclipse(shadow, observer) {
254
255
  * ponytail: three samples, not a sunrise search — a day that starts after C1
255
256
  * and ends before the peak still drops. Upgrade path: sunEvents over [c1, c4].
256
257
  */
257
- function seesAnyOfIt(e) {
258
- return e.sunAltDeg.c1 > 0.0 || e.sunAltDeg.peak > 0.0 || e.sunAltDeg.c4 > 0.0;
258
+ function seesAnyOfIt(e, dip) {
259
+ return e.sunAltDeg.c1 > dip || e.sunAltDeg.peak > dip || e.sunAltDeg.c4 > dip;
259
260
  }
260
261
  /**
261
262
  * The first solar eclipse `observer` can see whose peak falls strictly after
@@ -270,8 +271,8 @@ function seesAnyOfIt(e) {
270
271
  * interval, or if no visible eclipse remains before the end of it.
271
272
  * @throws {RangeError} if `after` is invalid or `observer` is out of range.
272
273
  */
273
- export function nextSolarEclipse(after, observer) {
274
- return nearestSolarEclipse(after, 1, observer);
274
+ export function nextSolarEclipse(after, observer, heightAboveGroundM = 0) {
275
+ return nearestSolarEclipse(after, 1, observer, heightAboveGroundM);
275
276
  }
276
277
  /**
277
278
  * The last solar eclipse `observer` can see whose peak falls strictly before
@@ -282,30 +283,30 @@ export function nextSolarEclipse(after, observer) {
282
283
  * interval, or if no visible eclipse remains after the start of it.
283
284
  * @throws {RangeError} if `before` is invalid or `observer` is out of range.
284
285
  */
285
- export function previousSolarEclipse(before, observer) {
286
- return nearestSolarEclipse(before, -1, observer);
286
+ export function previousSolarEclipse(before, observer, heightAboveGroundM = 0) {
287
+ return nearestSolarEclipse(before, -1, observer, heightAboveGroundM);
287
288
  }
288
289
  /** Solar eclipses `observer` can see with peaks in `[startUtc, endUtc)`, sorted ascending. Contacts may fall outside the window. */
289
- export function solarEclipses(startUtc, endUtc, observer) {
290
+ export function solarEclipses(startUtc, endUtc, observer, heightAboveGroundM = 0) {
290
291
  assertSupported(startUtc);
291
292
  assertSupportedWindowEnd(endUtc);
292
- assertObserver(observer);
293
- return scanSolarEclipses(startUtc.getTime(), endUtc.getTime(), 1, false, observer);
293
+ const dip = horizonDip(observer, heightAboveGroundM);
294
+ return scanSolarEclipses(startUtc.getTime(), endUtc.getTime(), 1, false, observer, dip);
294
295
  }
295
- function nearestSolarEclipse(anchor, direction, observer) {
296
+ function nearestSolarEclipse(anchor, direction, observer, heightAboveGroundM) {
296
297
  assertSupported(anchor);
297
- assertObserver(observer);
298
+ const dip = horizonDip(observer, heightAboveGroundM);
298
299
  const ms = anchor.getTime();
299
300
  // No scan limit: walk to the supported boundary. A place can go years
300
301
  // without a visible solar eclipse, and a pruned new moon is cheap.
301
302
  const startMs = direction > 0 ? ms + SAME_ECLIPSE_MS + 1 : SUPPORTED_MIN;
302
303
  const endMs = direction > 0 ? SUPPORTED_MAX : ms - SAME_ECLIPSE_MS;
303
- const found = scanSolarEclipses(startMs, endMs, direction, true, observer);
304
+ const found = scanSolarEclipses(startMs, endMs, direction, true, observer, dip);
304
305
  if (found.length)
305
306
  return found[0];
306
307
  throw new AlmanacOutOfRangeError();
307
308
  }
308
- function scanSolarEclipses(startMs, endMs, direction, firstOnly, observer) {
309
+ function scanSolarEclipses(startMs, endMs, direction, firstOnly, observer, dip) {
309
310
  const found = [];
310
311
  if (startMs >= endMs)
311
312
  return found;
@@ -337,7 +338,7 @@ function scanSolarEclipses(startMs, endMs, direction, firstOnly, observer) {
337
338
  // This is at least a partial solar eclipse for the observer.
338
339
  const eclipse = buildSolarEclipse(shadow, observer);
339
340
  // Ignore any eclipse that happens completely at night.
340
- if (!seesAnyOfIt(eclipse))
341
+ if (!seesAnyOfIt(eclipse, dip))
341
342
  continue;
342
343
  found.push(eclipse);
343
344
  if (firstOnly)
package/dist/sun.d.ts CHANGED
@@ -7,6 +7,10 @@ type VsopModel = [VsopFormula, VsopFormula, VsopFormula];
7
7
  * below are checked against; exported for that test only.
8
8
  */
9
9
  export declare const vsopEarth: VsopModel;
10
+ /** UPSTREAM: `VsopRotate`, astronomy.ts lines 3234-3241 — VSOP ecliptic to J2000 equatorial. */
11
+ export declare function vsopRotate(eclip: Vec3): Vec3;
12
+ /** UPSTREAM: `VsopSphereToRect`, astronomy.ts lines 3243-3253. */
13
+ export declare function vsopSphereToRect(lon: number, lat: number, radius: number): Vec3;
10
14
  /**
11
15
  * UPSTREAM: `CalcVsop`, astronomy.ts lines 3255-3262, specialised to
12
16
  * `vsop.Earth` — i.e. `HelioVector(Body.Earth, time)`. J2000 mean equator (EQJ).
package/dist/sun.js CHANGED
@@ -204,7 +204,7 @@ function earthRad(t) {
204
204
  return coord;
205
205
  }
206
206
  /** UPSTREAM: `VsopRotate`, astronomy.ts lines 3234-3241 — VSOP ecliptic to J2000 equatorial. */
207
- function vsopRotate(eclip) {
207
+ export function vsopRotate(eclip) {
208
208
  return {
209
209
  x: eclip.x + 0.000000440360 * eclip.y - 0.000000190919 * eclip.z,
210
210
  y: -0.000000479966 * eclip.x + 0.917482137087 * eclip.y - 0.397776982902 * eclip.z,
@@ -212,7 +212,7 @@ function vsopRotate(eclip) {
212
212
  };
213
213
  }
214
214
  /** UPSTREAM: `VsopSphereToRect`, astronomy.ts lines 3243-3253. */
215
- function vsopSphereToRect(lon, lat, radius) {
215
+ export function vsopSphereToRect(lon, lat, radius) {
216
216
  // Convert spherical coordinates to ecliptic cartesian coordinates.
217
217
  const r_coslat = radius * Math.cos(lat);
218
218
  const coslon = Math.cos(lon);
@@ -56,6 +56,7 @@ export declare function topoAltAzUnrefracted(bodyEqj: Vec3, ut: number, observer
56
56
  * altitude never drops below -90.
57
57
  */
58
58
  export declare function refractionDeg(altitudeDeg: number): number;
59
+ export declare function refract(unrefracted: AltAz): AltAz;
59
60
  /**
60
61
  * Topocentric azimuth/altitude of the Sun — parallax (via the observer's
61
62
  * geocentric position, WGS-84 flattening, elevation) and atmospheric
@@ -182,7 +182,7 @@ export function refractionDeg(altitudeDeg) {
182
182
  }
183
183
  return refr;
184
184
  }
185
- function refract(unrefracted) {
185
+ export function refract(unrefracted) {
186
186
  return { azDeg: unrefracted.azDeg, altDeg: unrefracted.altDeg + refractionDeg(unrefracted.altDeg) };
187
187
  }
188
188
  /**
package/dist/types.d.ts CHANGED
@@ -3,6 +3,9 @@ export interface Observer {
3
3
  longitudeDeg: number;
4
4
  elevationM?: number;
5
5
  }
6
+ export type Planet = 'mercury' | 'venus' | 'earth' | 'mars' | 'jupiter' | 'saturn';
7
+ /** INTERNAL: Earth has no planetary direction from an Earth-based observer. */
8
+ export declare function assertPlanet(planet: Planet, earthAllowed?: boolean): void;
6
9
  export declare class AlmanacOutOfRangeError extends RangeError {
7
10
  constructor(msg?: string);
8
11
  }
package/dist/types.js CHANGED
@@ -1,3 +1,8 @@
1
+ /** INTERNAL: Earth has no planetary direction from an Earth-based observer. */
2
+ export function assertPlanet(planet, earthAllowed = false) {
3
+ if (!['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn'].includes(planet) || (!earthAllowed && planet === 'earth'))
4
+ throw new RangeError(`invalid planet: ${planet}`);
5
+ }
1
6
  export class AlmanacOutOfRangeError extends RangeError {
2
7
  constructor(msg = 'time outside supported interval 1950-01-01T00:00Z ≤ t < 2101-01-01T00:00Z') { super(msg); this.name = 'AlmanacOutOfRangeError'; }
3
8
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openwaters/almanac",
3
- "version": "0.6.0",
4
- "description": "Offline sun & moon engine: positions, rise/set, moon phase, lunar and solar eclipses.",
3
+ "version": "0.7.0",
4
+ "description": "Offline astronomy: Sun, Moon and planet positions, apparent horizon, rise/set, phases and eclipses.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",