@openwaters/almanac 0.5.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 +57 -8
- package/dist/eclipse.d.ts +1 -1
- package/dist/eclipse.js +6 -5
- package/dist/events.d.ts +12 -4
- package/dist/events.js +31 -12
- package/dist/globalSolar.d.ts +6 -0
- package/dist/globalSolar.js +55 -13
- package/dist/horizon.d.ts +3 -0
- package/dist/horizon.js +24 -0
- package/dist/illumination.d.ts +3 -0
- package/dist/illumination.js +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/planetModels.d.ts +5 -0
- package/dist/planetModels.js +467 -0
- package/dist/planets.d.ts +32 -0
- package/dist/planets.js +104 -0
- package/dist/solar.d.ts +3 -3
- package/dist/solar.js +17 -16
- package/dist/sun.d.ts +4 -0
- package/dist/sun.js +2 -2
- package/dist/transforms.d.ts +1 -0
- package/dist/transforms.js +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.js +5 -0
- package/package.json +2 -2
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
|
|
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/
|
|
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: **
|
|
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
|
|
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.
|
|
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:
|
|
@@ -155,7 +204,7 @@ anchor. Search results are global; apply `lunarEclipseVisibility` for an observe
|
|
|
155
204
|
|
|
156
205
|
Solar eclipses are searched for an observer, because their contacts only exist for a place: `nextSolarEclipse(after, observer)`, `previousSolarEclipse(before, observer)`, and `solarEclipses(startUtc, endUtc, observer)`. An eclipse whose Sun is below the horizon at C1, the peak, and C4 is not returned.
|
|
157
206
|
|
|
158
|
-
To ask about the whole Earth instead, `nextGlobalSolarEclipse(after)`, `previousGlobalSolarEclipse(before)`, and `globalSolarEclipses(startUtc, endUtc)` need no observer. Each eclipse reports its greatest eclipse, the shadow axis's distance from the Earth's center, and where the axis meets the ground, with the kind and obscuration seen there. When the axis misses the Earth the eclipse is partial and has no ground point.
|
|
207
|
+
To ask about the whole Earth instead, `nextGlobalSolarEclipse(after)`, `previousGlobalSolarEclipse(before)`, and `globalSolarEclipses(startUtc, endUtc)` need no observer. Each eclipse reports its greatest eclipse, the shadow axis's distance from the Earth's center, and where the axis meets the ground, with the kind and obscuration seen there. When the axis misses the Earth the eclipse is partial and has no ground point, and greatest eclipse falls on the Earth's limb nearest the axis, with the Sun on the horizon: `greatestLatitudeDeg`, `greatestLongitudeDeg`, and `greatestObscuration` give that place for every eclipse.
|
|
159
208
|
|
|
160
209
|
`solarEclipseCentralLine(peak, stepSeconds)` samples where the axis meets the ground from its first contact to its last, and `solarEclipseAxisPoint(time)` gives that point at any instant. How far the next totality passes from a place is a distance to each point of a line:
|
|
161
210
|
|
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 {
|
|
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
|
-
|
|
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) >
|
|
319
|
+
const up = (d) => (d == null ? null : altAt(d) > dip);
|
|
319
320
|
const peakAltDeg = altAt(eclipse.peak);
|
|
320
321
|
return {
|
|
321
|
-
visibleAtPeak: peakAltDeg >
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
515
|
+
const dip = horizonDip(observer, heightAboveGroundM);
|
|
498
516
|
if (startUtc.getTime() >= endUtc.getTime())
|
|
499
517
|
return [];
|
|
500
|
-
|
|
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
|
/**
|
package/dist/globalSolar.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface GlobalSolarEclipse {
|
|
|
13
13
|
longitudeDeg: number | null;
|
|
14
14
|
/** Fraction of the Sun's disc area covered at that point; exactly 1 for a total eclipse; `null` for a partial eclipse. */
|
|
15
15
|
obscuration: number | null;
|
|
16
|
+
/** Geodetic latitude of greatest eclipse, degrees: the ground point, or for a partial eclipse the point on the Earth's limb nearest the axis, with the Sun on the horizon. */
|
|
17
|
+
greatestLatitudeDeg: number;
|
|
18
|
+
/** Longitude of greatest eclipse, degrees east in (−180, 180]. */
|
|
19
|
+
greatestLongitudeDeg: number;
|
|
20
|
+
/** Fraction of the Sun's disc area covered at greatest eclipse. */
|
|
21
|
+
greatestObscuration: number;
|
|
16
22
|
}
|
|
17
23
|
/** A point on a solar eclipse's central line: where the Moon's shadow axis meets the ground at one instant. */
|
|
18
24
|
export interface SolarEclipseAxisPoint {
|
package/dist/globalSolar.js
CHANGED
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
// quadratic's discriminant, and an axis point rejects the crossing on the
|
|
31
31
|
// Sun's side of the Moon that upstream, asking only at greatest eclipse,
|
|
32
32
|
// never meets.
|
|
33
|
+
//
|
|
34
|
+
// Neither is greatest eclipse for an axis that misses the Earth: upstream
|
|
35
|
+
// reports no place at all, and this package reports the point on the limb
|
|
36
|
+
// nearest the axis, where the Sun is on the horizon.
|
|
33
37
|
import { assertSupported, assertSupportedWindowEnd, AlmanacOutOfRangeError, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
|
|
34
38
|
import { dateFromUt, ttDaysFromUt, utDays } from './time.js';
|
|
35
39
|
import { EARTH_EQUATORIAL_RADIUS_KM, EARTH_FLATTENING, EARTH_FLATTENING_SQUARED, KM_PER_AU, RAD2DEG, PrecessDirection, gyration } from './nutation.js';
|
|
@@ -133,6 +137,24 @@ function axisGround(shadow, axis, u) {
|
|
|
133
137
|
const px = u * v.x - e.x;
|
|
134
138
|
const py = u * v.y - e.y;
|
|
135
139
|
const pz = (u * v.z - e.z) * EARTH_FLATTENING;
|
|
140
|
+
const { latitudeDeg, longitudeDeg, o } = geodeticPlace(shadow, tt, px, py, pz);
|
|
141
|
+
// Recalculate the shadow using a vector from the Moon's center toward the observer.
|
|
142
|
+
const surface = calcShadow(MOON_POLAR_RADIUS_KM, shadow.ut, o, shadow.dir);
|
|
143
|
+
// If we did everything right, the shadow distance should be very close to zero.
|
|
144
|
+
// That's because we already determined the observer 'o' is on the shadow axis!
|
|
145
|
+
// Upstream's bound is 1e-9 km; exceeding it is an implementation failure.
|
|
146
|
+
if (surface.r > 1.0e-9 || surface.r < 0.0)
|
|
147
|
+
throw new Error(`almanac internal: unexpected shadow distance from geoid intersection = ${surface.r}`);
|
|
148
|
+
const kind = eclipseKindFromUmbra(surface.k);
|
|
149
|
+
const obscuration = (kind === 'total') ? 1.0 : solarEclipseObscuration(shadow.dir, o);
|
|
150
|
+
return { latitudeDeg, longitudeDeg, kind, obscuration };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* UPSTREAM: the middle of `GeoidIntersect`, astronomy.ts ~8842 — geodetic
|
|
154
|
+
* latitude and longitude of a geocentric equator-of-date point in km, and the
|
|
155
|
+
* lunacentric EQJ vector toward it in AU.
|
|
156
|
+
*/
|
|
157
|
+
function geodeticPlace(shadow, tt, px, py, pz) {
|
|
136
158
|
// Convert cartesian coordinates into geodetic latitude/longitude.
|
|
137
159
|
const proj = Math.hypot(px, py) * EARTH_FLATTENING_SQUARED;
|
|
138
160
|
let latitudeDeg;
|
|
@@ -153,16 +175,31 @@ function axisGround(shadow, axis, u) {
|
|
|
153
175
|
const og = gyration({ x: px / KM_PER_AU, y: py / KM_PER_AU, z: pz / KM_PER_AU }, tt, PrecessDirection.Into2000);
|
|
154
176
|
// Convert geocentric vector to lunacentric vector.
|
|
155
177
|
const o = { x: og.x + shadow.target.x, y: og.y + shadow.target.y, z: og.z + shadow.target.z };
|
|
156
|
-
|
|
178
|
+
return { latitudeDeg, longitudeDeg, o };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Greatest eclipse when the axis misses the geoid: the point on the Earth's
|
|
182
|
+
* limb nearest the axis. In the dilated coordinates the geoid is a sphere, and
|
|
183
|
+
* the axis's closest approach to its center, scaled onto it, stands for that point.
|
|
184
|
+
* The axis runs perpendicular to the radius there, so it lies in the sphere's
|
|
185
|
+
* tangent plane; dilation maps tangent planes to tangent planes, so on the
|
|
186
|
+
* ellipsoid the axis lies in the geodetic horizon and the Sun's center is at
|
|
187
|
+
* geometric altitude zero, as the catalog tabulates. The observer there sees
|
|
188
|
+
* what the local search would: a total or annular phase only inside the
|
|
189
|
+
* umbra, which a non-central eclipse can graze.
|
|
190
|
+
*/
|
|
191
|
+
function limbGround(shadow, axis) {
|
|
192
|
+
const { tt, v, e, A, B } = axis;
|
|
193
|
+
const u = -B / (2 * A);
|
|
194
|
+
const cx = u * v.x - e.x;
|
|
195
|
+
const cy = u * v.y - e.y;
|
|
196
|
+
const cz = u * v.z - e.z;
|
|
197
|
+
const scale = EARTH_EQUATORIAL_RADIUS_KM / Math.sqrt(cx * cx + cy * cy + cz * cz);
|
|
198
|
+
const { latitudeDeg, longitudeDeg, o } = geodeticPlace(shadow, tt, cx * scale, cy * scale, cz * scale * EARTH_FLATTENING);
|
|
157
199
|
const surface = calcShadow(MOON_POLAR_RADIUS_KM, shadow.ut, o, shadow.dir);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (surface.r > 1.0e-9 || surface.r < 0.0)
|
|
162
|
-
throw new Error(`almanac internal: unexpected shadow distance from geoid intersection = ${surface.r}`);
|
|
163
|
-
const kind = eclipseKindFromUmbra(surface.k);
|
|
164
|
-
const obscuration = (kind === 'total') ? 1.0 : solarEclipseObscuration(shadow.dir, o);
|
|
165
|
-
return { latitudeDeg, longitudeDeg, kind, obscuration };
|
|
200
|
+
const obscuration = (surface.r < Math.abs(surface.k) && eclipseKindFromUmbra(surface.k) === 'total')
|
|
201
|
+
? 1.0 : solarEclipseObscuration(shadow.dir, o);
|
|
202
|
+
return { latitudeDeg, longitudeDeg, obscuration };
|
|
166
203
|
}
|
|
167
204
|
/**
|
|
168
205
|
* UPSTREAM: `GeoidIntersect`'s choice of root — where the axis first meets
|
|
@@ -201,13 +238,18 @@ function grazingGround(shadow) {
|
|
|
201
238
|
function geoidIntersect(shadow, peak) {
|
|
202
239
|
const ground = nearAxisGround(shadow);
|
|
203
240
|
if (ground === null) {
|
|
204
|
-
// This is a partial solar eclipse
|
|
205
|
-
//
|
|
206
|
-
|
|
241
|
+
// This is a partial solar eclipse: no one stands on the axis, and
|
|
242
|
+
// greatest eclipse is on the limb nearest it.
|
|
243
|
+
const limb = limbGround(shadow, dilatedAxis(shadow));
|
|
244
|
+
return {
|
|
245
|
+
kind: 'partial', peak, axisDistanceKm: shadow.r, latitudeDeg: null, longitudeDeg: null, obscuration: null,
|
|
246
|
+
greatestLatitudeDeg: limb.latitudeDeg, greatestLongitudeDeg: limb.longitudeDeg, greatestObscuration: limb.obscuration
|
|
247
|
+
};
|
|
207
248
|
}
|
|
208
249
|
return {
|
|
209
250
|
kind: ground.kind, peak, axisDistanceKm: shadow.r,
|
|
210
|
-
latitudeDeg: ground.latitudeDeg, longitudeDeg: ground.longitudeDeg, obscuration: ground.obscuration
|
|
251
|
+
latitudeDeg: ground.latitudeDeg, longitudeDeg: ground.longitudeDeg, obscuration: ground.obscuration,
|
|
252
|
+
greatestLatitudeDeg: ground.latitudeDeg, greatestLongitudeDeg: ground.longitudeDeg, greatestObscuration: ground.obscuration
|
|
211
253
|
};
|
|
212
254
|
}
|
|
213
255
|
/**
|
package/dist/horizon.js
ADDED
|
@@ -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
|
+
}
|
package/dist/illumination.d.ts
CHANGED
|
@@ -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
|
package/dist/illumination.js
CHANGED
|
@@ -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;
|