@openwaters/almanac 0.8.0 → 0.8.1
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 +70 -7
- package/dist/angles.d.ts +4 -0
- package/dist/angles.js +16 -0
- package/dist/apsides.d.ts +14 -0
- package/dist/apsides.js +88 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.js +4 -4
- package/dist/illumination.d.ts +6 -0
- package/dist/illumination.js +8 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/positions.d.ts +15 -0
- package/dist/positions.js +28 -1
- package/dist/seasons.d.ts +17 -0
- package/dist/seasons.js +73 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Almanac
|
|
2
2
|
|
|
3
|
-
Offline sky engine for the Salish Sea and everywhere else: Sun, Moon, and planet positions, apparent horizon dip, rise, set, twilight, any Sun altitude, upper and lower transits, Moon phase and orientation, 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.
|
|
3
|
+
Offline sky engine for the Salish Sea and everywhere else: Sun, Moon, and planet positions, apparent horizon dip, rise, set, twilight, any Sun altitude, upper and lower transits, Moon phase and orientation, equinoxes and solstices, lunar perigee and apogee, the equation of time, 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
|
+
Three implementations, one behavior:
|
|
6
6
|
|
|
7
7
|
- `swift/` — SwiftPM package `Almanac`
|
|
8
8
|
- `typescript/` — npm `@openwaters/almanac`
|
|
9
|
-
- `
|
|
9
|
+
- `kotlin/` — Kotlin/JVM `io.openwaters.almanac`, source-consumed with Gradle
|
|
10
|
+
- `fixtures/` — the shared test corpus (JPL Horizons, USNO, Espenak) all three suites
|
|
10
11
|
must pass; the contract that keeps the ports identical
|
|
11
12
|
|
|
12
13
|
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.
|
|
@@ -21,7 +22,7 @@ Algorithms translated from [Astronomy Engine](https://github.com/cosinekitty/ast
|
|
|
21
22
|
|
|
22
23
|
## Performance
|
|
23
24
|
|
|
24
|
-
Almanac includes a shared performance harness for
|
|
25
|
+
Almanac includes a shared performance harness for TypeScript and Swift: **34 workloads**
|
|
25
26
|
cover positions, a 228-hour sky track, short/year/polar event windows, full-range
|
|
26
27
|
moon phases, next/previous/range lunar, solar, and global solar eclipse searches,
|
|
27
28
|
including empty windows, solar obscuration over a 228-hour track, and the central
|
|
@@ -66,9 +67,38 @@ background task; use shorter windows for interactive queries.
|
|
|
66
67
|
|
|
67
68
|
## Usage
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
Migration guides for [SunCalc](docs/migrating-from-suncalc.md), [Astronomy Engine](docs/migrating-from-astronomy-engine.md), and [Solar](docs/migrating-from-solar.md) include tested examples. The [library comparison](docs/comparison.md) explains the features and limits of each library.
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
Angles use degrees. All three ports provide `degreesToRadians(angle)` and `radiansToDegrees(angle)` for callers that use radians. These functions preserve sign and full turns without wrapping or changing the angle's origin; non-finite inputs and conversion overflow raise validation errors. Swift calls use `try`.
|
|
73
|
+
|
|
74
|
+
### JavaScript module loading
|
|
75
|
+
|
|
76
|
+
The npm package is ESM and has no separate CommonJS build. Use Node 22.12 or later for the Node examples; CI and release checks load the packed package on Node 22.12 and Node 24. Browser applications can import it through their ESM bundler.
|
|
77
|
+
|
|
78
|
+
In an ES module (`.mjs` or a package with `"type": "module"`):
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import { sunAltAz } from '@openwaters/almanac';
|
|
82
|
+
const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
|
|
83
|
+
console.log(sunAltAz(new Date(), observer));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A CommonJS application can keep synchronous initialization. [Node's `require(esm)` support](https://nodejs.org/docs/latest-v22.x/api/modules.html#loading-ecmascript-modules-using-require) is enabled without a flag from Node 22.12:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const { sunAltAz } = require('@openwaters/almanac');
|
|
90
|
+
const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
|
|
91
|
+
console.log(sunAltAz(new Date(), observer));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
CommonJS can also load ESM asynchronously if its initialization flow already supports it:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
async function initializeSky() {
|
|
98
|
+
const { sunAltAz } = await import('@openwaters/almanac');
|
|
99
|
+
return sunAltAz(new Date(), { latitudeDeg: 48.5, longitudeDeg: -123.0 });
|
|
100
|
+
}
|
|
101
|
+
```
|
|
72
102
|
|
|
73
103
|
### TypeScript
|
|
74
104
|
|
|
@@ -78,7 +108,8 @@ npm install @openwaters/almanac
|
|
|
78
108
|
|
|
79
109
|
```ts
|
|
80
110
|
import {
|
|
81
|
-
nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, nextGlobalSolarEclipse, sunEvents, starAltAz
|
|
111
|
+
nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, nextGlobalSolarEclipse, sunEvents, starAltAz,
|
|
112
|
+
seasons, moonApsides, equationOfTime
|
|
82
113
|
} from '@openwaters/almanac';
|
|
83
114
|
|
|
84
115
|
const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
|
|
@@ -102,6 +133,13 @@ for (const { kind, time } of sunEvents(today, tomorrow, observer)) {
|
|
|
102
133
|
|
|
103
134
|
// Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
|
|
104
135
|
const { azDeg, altDeg } = starAltAz(88.792939, 7.407064, today, observer);
|
|
136
|
+
|
|
137
|
+
// The year's equinoxes and solstices, the Moon's perigees and apogees with
|
|
138
|
+
// distances, and the equation of time in minutes.
|
|
139
|
+
const year = new Date(today.getUTCFullYear() + 1, 0, 1);
|
|
140
|
+
for (const { kind, time } of seasons(today, year)) console.log(kind, time.toISOString());
|
|
141
|
+
for (const { kind, time, distanceKm } of moonApsides(today, year)) console.log(kind, time.toISOString(), distanceKm);
|
|
142
|
+
console.log(equationOfTime(today)); // sundial minus clock, minutes
|
|
105
143
|
```
|
|
106
144
|
|
|
107
145
|
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:
|
|
@@ -149,6 +187,31 @@ for event in try sunEvents(from: today, to: tomorrow, observer: observer) {
|
|
|
149
187
|
|
|
150
188
|
// Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
|
|
151
189
|
let star = try starAltAz(raDeg: 88.792939, decDeg: 7.407064, at: today, observer: observer)
|
|
190
|
+
|
|
191
|
+
// The year's equinoxes and solstices, the Moon's perigees and apogees with
|
|
192
|
+
// distances, and the equation of time in minutes.
|
|
193
|
+
let year = today.addingTimeInterval(366 * 24 * 60 * 60)
|
|
194
|
+
for event in try seasons(from: today, to: year) { print(event.kind, event.time) }
|
|
195
|
+
for apsis in try moonApsides(from: today, to: year) { print(apsis.kind, apsis.time, apsis.distanceKm) }
|
|
196
|
+
print(try equationOfTime(today)) // sundial minus clock, minutes
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Kotlin/JVM and Android
|
|
200
|
+
|
|
201
|
+
The Kotlin library provides the same 37 functions and result fields, using `java.time.Instant` and immutable data classes. It targets JVM 8 and Android minSdk 26, builds with JDK 17, and has no runtime dependencies beyond the Kotlin standard library and platform APIs. Maven publication and a Kotlin benchmark runner are deferred; consume the source at a reviewed Git SHA as described in [`kotlin/README.md`](kotlin/README.md).
|
|
202
|
+
|
|
203
|
+
```kotlin
|
|
204
|
+
import io.openwaters.almanac.*
|
|
205
|
+
import java.time.Instant
|
|
206
|
+
|
|
207
|
+
val observer = Observer(48.5, -123.0)
|
|
208
|
+
val today = Instant.now()
|
|
209
|
+
val tomorrow = today.plusSeconds(86400)
|
|
210
|
+
val events = sunEvents(today, tomorrow, observer)
|
|
211
|
+
val moon = moonIllumination(today)
|
|
212
|
+
val next = nextGlobalSolarEclipse(today)
|
|
213
|
+
val line = solarEclipseCentralLine(next.peak)
|
|
214
|
+
val minutes = equationOfTime(today)
|
|
152
215
|
```
|
|
153
216
|
|
|
154
217
|
### Golden hour, transits, and the Moon's tilt
|
package/dist/angles.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Converts degrees to radians without wrapping the angle. */
|
|
2
|
+
export declare function degreesToRadians(degrees: number): number;
|
|
3
|
+
/** Converts radians to degrees without wrapping; rejects conversion overflow. */
|
|
4
|
+
export declare function radiansToDegrees(radians: number): number;
|
package/dist/angles.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DEG2RAD, RAD2DEG } from './nutation.js';
|
|
2
|
+
/** Converts degrees to radians without wrapping the angle. */
|
|
3
|
+
export function degreesToRadians(degrees) {
|
|
4
|
+
if (!Number.isFinite(degrees))
|
|
5
|
+
throw new RangeError('degrees must be finite');
|
|
6
|
+
return degrees * DEG2RAD;
|
|
7
|
+
}
|
|
8
|
+
/** Converts radians to degrees without wrapping; rejects conversion overflow. */
|
|
9
|
+
export function radiansToDegrees(radians) {
|
|
10
|
+
if (!Number.isFinite(radians))
|
|
11
|
+
throw new RangeError('radians must be finite');
|
|
12
|
+
const degrees = radians * RAD2DEG;
|
|
13
|
+
if (!Number.isFinite(degrees))
|
|
14
|
+
throw new RangeError('angle conversion overflow');
|
|
15
|
+
return degrees;
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Closest approach or farthest remove of the Moon from the Earth's centre. */
|
|
2
|
+
export type MoonApsisKind = 'perigee' | 'apogee';
|
|
3
|
+
/** A lunar perigee or apogee, with the Earth–Moon centre distance there. */
|
|
4
|
+
export interface MoonApsis {
|
|
5
|
+
time: Date;
|
|
6
|
+
kind: MoonApsisKind;
|
|
7
|
+
distanceKm: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The Moon's perigees and apogees within the half-open window
|
|
11
|
+
* `[startUtc, endUtc)`, sorted ascending, each with the geocentric distance
|
|
12
|
+
* at that instant.
|
|
13
|
+
*/
|
|
14
|
+
export declare function moonApsides(startUtc: Date, endUtc: Date): MoonApsis[];
|
package/dist/apsides.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// L3 lunar apsides: perigee and apogee, the zeros of the Moon's radial
|
|
2
|
+
// velocity, from the MOON2 distance the position layer already evaluates.
|
|
3
|
+
//
|
|
4
|
+
// Translated from the cosinekitty/astronomy upstream, pinned sha
|
|
5
|
+
// 865d3da7d8112bbc7911238052c6af4aaf877181, source/js/astronomy.ts:
|
|
6
|
+
// `SearchLunarApsis` (~6534) — its central-difference `distance_slope`, its
|
|
7
|
+
// five-day scan, its sign-change classification, and the `Search` it hands
|
|
8
|
+
// each bracket to. Upstream scans forward from the caller's instant and
|
|
9
|
+
// `NextLunarApsis` (~6621) chains from the last result; here the five-day scan
|
|
10
|
+
// points are a fixed grid anchored at J2000, so every window that contains an
|
|
11
|
+
// apsis finds it from the same bracket and reports the same millisecond — the
|
|
12
|
+
// fixed-seed rule the eclipse searches follow. Apsides are at least 11.8 days
|
|
13
|
+
// apart, so a cell holds at most one.
|
|
14
|
+
//
|
|
15
|
+
// INTERNAL except `moonApsides`: not part of the curated public API (see index.ts).
|
|
16
|
+
import { assertSupported, assertSupportedWindowEnd } from './types.js';
|
|
17
|
+
import { utDays, ttDaysFromUt, dateFromUt } from './time.js';
|
|
18
|
+
import { KM_PER_AU } from './nutation.js';
|
|
19
|
+
import { calcMoon } from './moon.js';
|
|
20
|
+
import { search, quantizedUt, assertReached } from './events.js';
|
|
21
|
+
/** UPSTREAM: the central-difference half-width `dt` inside `SearchLunarApsis`. */
|
|
22
|
+
const SLOPE_DT_DAYS = 0.001;
|
|
23
|
+
/** UPSTREAM: `increment` — the scan step, here also the grid cell in days since J2000 UT. */
|
|
24
|
+
const APSIS_CELL_DAYS = 5;
|
|
25
|
+
/** UPSTREAM: `distance_slope` — dr/dt in AU per day, by central difference. */
|
|
26
|
+
function distanceSlope(ut) {
|
|
27
|
+
const tt = ttDaysFromUt(ut);
|
|
28
|
+
const r1 = calcMoon(tt - SLOPE_DT_DAYS / 2).distanceAu;
|
|
29
|
+
const r2 = calcMoon(tt + SLOPE_DT_DAYS / 2).distanceAu;
|
|
30
|
+
return (r2 - r1) / SLOPE_DT_DAYS;
|
|
31
|
+
}
|
|
32
|
+
function negativeDistanceSlope(ut) {
|
|
33
|
+
return -distanceSlope(ut);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* UPSTREAM: the body of `SearchLunarApsis`'s scan loop for one cell `[t1, t2]`
|
|
37
|
+
* with the slopes `m1`, `m2` at its ends. A slope rising through zero is a
|
|
38
|
+
* perigee and falling through zero an apogee. A zero exactly at a grid point
|
|
39
|
+
* belongs to the cell it ends, so no apsis is reported twice.
|
|
40
|
+
*/
|
|
41
|
+
function searchApsisInCell(t1, t2, m1, m2) {
|
|
42
|
+
if (m1 < 0 && m2 >= 0) {
|
|
43
|
+
const ut = search(distanceSlope, t1, t2, 1, undefined, 'perigee search');
|
|
44
|
+
assertReached(ut !== null, 'perigee search');
|
|
45
|
+
return { ut: ut, kind: 'perigee' };
|
|
46
|
+
}
|
|
47
|
+
if (m1 > 0 && m2 <= 0) {
|
|
48
|
+
const ut = search(negativeDistanceSlope, t1, t2, 1, undefined, 'apogee search');
|
|
49
|
+
assertReached(ut !== null, 'apogee search');
|
|
50
|
+
return { ut: ut, kind: 'apogee' };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The Moon's perigees and apogees within the half-open window
|
|
56
|
+
* `[startUtc, endUtc)`, sorted ascending, each with the geocentric distance
|
|
57
|
+
* at that instant.
|
|
58
|
+
*/
|
|
59
|
+
export function moonApsides(startUtc, endUtc) {
|
|
60
|
+
assertSupported(startUtc);
|
|
61
|
+
assertSupportedWindowEnd(endUtc);
|
|
62
|
+
if (startUtc.getTime() >= endUtc.getTime())
|
|
63
|
+
return [];
|
|
64
|
+
const startUt = utDays(startUtc);
|
|
65
|
+
const endUt = utDays(endUtc);
|
|
66
|
+
const events = [];
|
|
67
|
+
let t1 = Math.floor(startUt / APSIS_CELL_DAYS) * APSIS_CELL_DAYS;
|
|
68
|
+
let m1 = distanceSlope(t1);
|
|
69
|
+
while (t1 < endUt) {
|
|
70
|
+
const t2 = t1 + APSIS_CELL_DAYS;
|
|
71
|
+
const m2 = distanceSlope(t2);
|
|
72
|
+
const found = searchApsisInCell(t1, t2, m1, m2);
|
|
73
|
+
if (found !== null) {
|
|
74
|
+
const q = quantizedUt(found.ut); // judge the half-open bounds on the reported instant, not the raw root
|
|
75
|
+
if (q >= endUt)
|
|
76
|
+
break;
|
|
77
|
+
if (q >= startUt) {
|
|
78
|
+
events.push({
|
|
79
|
+
time: dateFromUt(found.ut), kind: found.kind,
|
|
80
|
+
distanceKm: calcMoon(ttDaysFromUt(found.ut)).distanceAu * KM_PER_AU
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
t1 = t2;
|
|
85
|
+
m1 = m2;
|
|
86
|
+
}
|
|
87
|
+
return events;
|
|
88
|
+
}
|
package/dist/events.d.ts
CHANGED
|
@@ -43,6 +43,16 @@ export interface MoonPhaseEvent {
|
|
|
43
43
|
export declare const MOON_MEAN_RADIUS_KM = 1737.4;
|
|
44
44
|
/** UPSTREAM: `SUN_RADIUS_KM`, astronomy.ts line 134. INTERNAL, see above. */
|
|
45
45
|
export declare const SUN_RADIUS_KM = 695700;
|
|
46
|
+
/**
|
|
47
|
+
* Clamps a probe into the supported interval. The altitude-search chain
|
|
48
|
+
* (searchAltitudeEvents) applies this to every probe it samples, so it never
|
|
49
|
+
* evaluates the model outside [MIN_UT, MAX_UT]. The phase-search walk
|
|
50
|
+
* (searchMoonPhases) only clamps its first probe — later quarter-to-quarter
|
|
51
|
+
* probes can land past MAX_UT and evaluate the model there harmlessly; it is
|
|
52
|
+
* the half-open window filter on emitted results, not this clamp, that keeps
|
|
53
|
+
* reported answers inside the interval.
|
|
54
|
+
*/
|
|
55
|
+
export declare function clampUt(ut: number): number;
|
|
46
56
|
/**
|
|
47
57
|
* Truncates a ut-days root to the same integer-ms precision `dateFromUt`
|
|
48
58
|
* reports it at (TimeClip). A root finder's raw `ut` carries sub-ms
|
|
@@ -55,6 +65,10 @@ export declare const SUN_RADIUS_KM = 695700;
|
|
|
55
65
|
* no other module consumes it.
|
|
56
66
|
*/
|
|
57
67
|
export declare function quantizedUt(ut: number): number;
|
|
68
|
+
/** Hitting an iteration cap is a bug, not a runtime condition — see the spec. */
|
|
69
|
+
export declare function assertReached(condition: boolean, what: string): void;
|
|
70
|
+
/** Wrap a hour-angle difference into (−180, 180]. */
|
|
71
|
+
export declare function angleOffset(diff: number): number;
|
|
58
72
|
/**
|
|
59
73
|
* Sun rise, set, the three twilights and upper transit within the half-open
|
|
60
74
|
* window `[startUtc, endUtc)`, sorted ascending.
|
|
@@ -97,6 +111,13 @@ export declare function sunTransits(startUtc: Date, endUtc: Date, observer: Obse
|
|
|
97
111
|
* parallax is included, whether or not the Moon is above the horizon.
|
|
98
112
|
*/
|
|
99
113
|
export declare function moonTransits(startUtc: Date, endUtc: Date, observer: Observer): TransitEvent[];
|
|
114
|
+
/**
|
|
115
|
+
* UPSTREAM: `MEAN_SYNODIC_MONTH`, astronomy.ts line 129. The elongation it
|
|
116
|
+
* searches is {@link moonPhaseDeg}, apparent on both sides per the spec's
|
|
117
|
+
* moon-phase definition — upstream's `PairLongitude` uses geometric longitudes,
|
|
118
|
+
* which sit a flat ~40 s off the USNO catalogue.
|
|
119
|
+
*/
|
|
120
|
+
export declare const MEAN_SYNODIC_MONTH = 29.530588;
|
|
100
121
|
/**
|
|
101
122
|
* UPSTREAM: `Search`, astronomy.ts ~4634 — bisection with quadratic
|
|
102
123
|
* acceleration for the ascending zero crossing of `f` in `[t1, t2]`. Times are
|
package/dist/events.js
CHANGED
|
@@ -94,7 +94,7 @@ const MAX_UT = utDays(new Date(SUPPORTED_MAX));
|
|
|
94
94
|
* the half-open window filter on emitted results, not this clamp, that keeps
|
|
95
95
|
* reported answers inside the interval.
|
|
96
96
|
*/
|
|
97
|
-
function clampUt(ut) {
|
|
97
|
+
export function clampUt(ut) {
|
|
98
98
|
return ut < MIN_UT ? MIN_UT : (ut > MAX_UT ? MAX_UT : ut);
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
@@ -112,7 +112,7 @@ export function quantizedUt(ut) {
|
|
|
112
112
|
return utDays(dateFromUt(ut));
|
|
113
113
|
}
|
|
114
114
|
/** Hitting an iteration cap is a bug, not a runtime condition — see the spec. */
|
|
115
|
-
function assertReached(condition, what) {
|
|
115
|
+
export function assertReached(condition, what) {
|
|
116
116
|
if (!condition)
|
|
117
117
|
throw new Error(`almanac internal: ${what} did not converge`);
|
|
118
118
|
}
|
|
@@ -150,7 +150,7 @@ function crossingSign(s, level) {
|
|
|
150
150
|
}
|
|
151
151
|
// ----------------------------------------------------------- root findings
|
|
152
152
|
/** Wrap a hour-angle difference into (−180, 180]. */
|
|
153
|
-
function angleOffset(diff) {
|
|
153
|
+
export function angleOffset(diff) {
|
|
154
154
|
let offset = diff % 360;
|
|
155
155
|
if (offset <= -180)
|
|
156
156
|
offset += 360;
|
|
@@ -574,7 +574,7 @@ export function moonTransits(startUtc, endUtc, observer) {
|
|
|
574
574
|
* moon-phase definition — upstream's `PairLongitude` uses geometric longitudes,
|
|
575
575
|
* which sit a flat ~40 s off the USNO catalogue.
|
|
576
576
|
*/
|
|
577
|
-
const MEAN_SYNODIC_MONTH = 29.530588;
|
|
577
|
+
export const MEAN_SYNODIC_MONTH = 29.530588;
|
|
578
578
|
const PHASE_NAMES = ['new', 'firstQuarter', 'full', 'lastQuarter'];
|
|
579
579
|
/** UPSTREAM: `QuadInterp`, astronomy.ts ~4528. */
|
|
580
580
|
function quadInterp(tm, dt, fa, fm, fb) {
|
package/dist/illumination.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface MoonIllumination {
|
|
|
9
9
|
}
|
|
10
10
|
/** UPSTREAM: `AngleBetween`, astronomy.ts lines 256-273 — angle between two vectors, [0, 180]. */
|
|
11
11
|
export declare function angleBetweenDeg(a: Vec3, b: Vec3): number;
|
|
12
|
+
/**
|
|
13
|
+
* INTERNAL: the Sun's apparent geocentric ecliptic longitude of date, [0, 360)
|
|
14
|
+
* — upstream `SunPosition(t).elon` (astronomy.ts ~2738), which `seasons.ts`
|
|
15
|
+
* roots against 0/90/180/270. Aberration is in the vector, nutation in the frame.
|
|
16
|
+
*/
|
|
17
|
+
export declare function sunEclipticLonDeg(tt: number): number;
|
|
12
18
|
/**
|
|
13
19
|
* INTERNAL: the Moon's apparent ecliptic longitude ahead of the Sun's, [0, 360)
|
|
14
20
|
* — 0 at new moon, 180 at full. The single definition of lunar phase in this
|
package/dist/illumination.js
CHANGED
|
@@ -44,6 +44,14 @@ function eclipticLonOfDateDeg(eqj, tt) {
|
|
|
44
44
|
elon += 360;
|
|
45
45
|
return elon;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* INTERNAL: the Sun's apparent geocentric ecliptic longitude of date, [0, 360)
|
|
49
|
+
* — upstream `SunPosition(t).elon` (astronomy.ts ~2738), which `seasons.ts`
|
|
50
|
+
* roots against 0/90/180/270. Aberration is in the vector, nutation in the frame.
|
|
51
|
+
*/
|
|
52
|
+
export function sunEclipticLonDeg(tt) {
|
|
53
|
+
return eclipticLonOfDateDeg(sunGeoVectorEqj(tt), tt);
|
|
54
|
+
}
|
|
47
55
|
/** UPSTREAM: `NormalizeLongitude`, astronomy.ts lines 4735-4739. */
|
|
48
56
|
function normalizeLongitude(lon) {
|
|
49
57
|
return ((lon % 360) + 360) % 360;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
export { degreesToRadians, radiansToDegrees } from './angles.js';
|
|
1
2
|
export type { Observer, Planet } from './types.js';
|
|
2
3
|
export type { HeliocentricPosition, PlanetPosition, PlanetIllumination } from './planets.js';
|
|
3
4
|
export { planetHeliocentricPosition, planetPosition, planetAltAz, planetIllumination } from './planets.js';
|
|
4
5
|
export { horizonDip } from './horizon.js';
|
|
5
6
|
export { AlmanacOutOfRangeError } from './types.js';
|
|
6
7
|
export type { SunPosition, MoonPosition } from './positions.js';
|
|
7
|
-
export { sunPosition, moonPosition } from './positions.js';
|
|
8
|
+
export { sunPosition, moonPosition, equationOfTime } from './positions.js';
|
|
9
|
+
export type { SeasonKind, SeasonEvent } from './seasons.js';
|
|
10
|
+
export { seasons } from './seasons.js';
|
|
11
|
+
export type { MoonApsisKind, MoonApsis } from './apsides.js';
|
|
12
|
+
export { moonApsides } from './apsides.js';
|
|
8
13
|
export type { AltAz } from './transforms.js';
|
|
9
14
|
export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
|
|
10
15
|
export type { MoonIllumination, MoonOrientation } from './illumination.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
// The public contract's API table is the complete export allow-list.
|
|
2
|
+
export { degreesToRadians, radiansToDegrees } from './angles.js';
|
|
1
3
|
export { planetHeliocentricPosition, planetPosition, planetAltAz, planetIllumination } from './planets.js';
|
|
2
4
|
export { horizonDip } from './horizon.js';
|
|
3
5
|
export { AlmanacOutOfRangeError } from './types.js';
|
|
4
|
-
export { sunPosition, moonPosition } from './positions.js';
|
|
6
|
+
export { sunPosition, moonPosition, equationOfTime } from './positions.js';
|
|
7
|
+
export { seasons } from './seasons.js';
|
|
8
|
+
export { moonApsides } from './apsides.js';
|
|
5
9
|
export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
|
|
6
10
|
export { moonIllumination, moonOrientation } from './illumination.js';
|
|
7
11
|
export { sunEvents, moonEvents, searchMoonPhases, planetEvents, sunAltitudeEvents, sunTransits, moonTransits } from './events.js';
|
package/dist/positions.d.ts
CHANGED
|
@@ -32,3 +32,18 @@ export declare function sunPosition(time: Date): SunPosition;
|
|
|
32
32
|
* the Earth-Moon centre distance in kilometres.
|
|
33
33
|
*/
|
|
34
34
|
export declare function moonPosition(time: Date): MoonPosition;
|
|
35
|
+
/**
|
|
36
|
+
* INTERNAL: equation of time for a TT instant, in minutes. Not from upstream:
|
|
37
|
+
* Meeus, *Astronomical Algorithms*, formula 28.1, `E = L0 − 0.0057183° − α +
|
|
38
|
+
* Δψ cos ε`, with `L0` the Sun's mean longitude from formula 28.2 and `α` the
|
|
39
|
+
* apparent right ascension `sunApparentAtTT` already computes. The constant
|
|
40
|
+
* is aberration plus the FK5 correction; `Δψ cos ε` moves `L0` from the mean
|
|
41
|
+
* equinox to the true one `α` is measured from.
|
|
42
|
+
*/
|
|
43
|
+
export declare function equationOfTimeAtTT(tt: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* Equation of time: apparent solar time minus mean solar time, in minutes.
|
|
46
|
+
* Positive when a sundial runs ahead of the clock; about +16.4 at the start
|
|
47
|
+
* of November and −14.2 in mid February.
|
|
48
|
+
*/
|
|
49
|
+
export declare function equationOfTime(time: Date): number;
|
package/dist/positions.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// parallax subtraction) is zero and drops out.
|
|
14
14
|
import { assertSupported } from './types.js';
|
|
15
15
|
import { ttDays } from './time.js';
|
|
16
|
-
import { KM_PER_AU, equatorialFromVector } from './nutation.js';
|
|
16
|
+
import { DEG2RAD, KM_PER_AU, earthTilt, equatorialFromVector } from './nutation.js';
|
|
17
17
|
import { moonGeoVector } from './moon.js';
|
|
18
18
|
import { sunGeoVector } from './sun.js';
|
|
19
19
|
/**
|
|
@@ -50,3 +50,30 @@ export function moonPosition(time) {
|
|
|
50
50
|
assertSupported(time);
|
|
51
51
|
return moonApparentAtTT(ttDays(time));
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* INTERNAL: equation of time for a TT instant, in minutes. Not from upstream:
|
|
55
|
+
* Meeus, *Astronomical Algorithms*, formula 28.1, `E = L0 − 0.0057183° − α +
|
|
56
|
+
* Δψ cos ε`, with `L0` the Sun's mean longitude from formula 28.2 and `α` the
|
|
57
|
+
* apparent right ascension `sunApparentAtTT` already computes. The constant
|
|
58
|
+
* is aberration plus the FK5 correction; `Δψ cos ε` moves `L0` from the mean
|
|
59
|
+
* equinox to the true one `α` is measured from.
|
|
60
|
+
*/
|
|
61
|
+
export function equationOfTimeAtTT(tt) {
|
|
62
|
+
const tau = tt / 365250;
|
|
63
|
+
const l0 = 280.4664567 + tau * (360007.6982779 + tau * (0.03032028
|
|
64
|
+
+ tau * (1 / 49931 + tau * (-1 / 15300 + tau * (-1 / 2000000)))));
|
|
65
|
+
const tilt = earthTilt(tt);
|
|
66
|
+
const nutationRaDeg = (tilt.dpsi / 3600) * Math.cos(tilt.tobl * DEG2RAD);
|
|
67
|
+
let e = l0 - 0.0057183 - sunApparentAtTT(tt).raDeg + nutationRaDeg;
|
|
68
|
+
e = ((e % 360) + 540) % 360 - 180; // (−180, 180]
|
|
69
|
+
return e * 4; // 360° of right ascension is 1440 minutes
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Equation of time: apparent solar time minus mean solar time, in minutes.
|
|
73
|
+
* Positive when a sundial runs ahead of the clock; about +16.4 at the start
|
|
74
|
+
* of November and −14.2 in mid February.
|
|
75
|
+
*/
|
|
76
|
+
export function equationOfTime(time) {
|
|
77
|
+
assertSupported(time);
|
|
78
|
+
return equationOfTimeAtTT(ttDays(time));
|
|
79
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The season the Sun's longitude starts: 0° March equinox, 90° June solstice, 180° September equinox, 270° December solstice. */
|
|
2
|
+
export type SeasonKind = 'marchEquinox' | 'juneSolstice' | 'septemberEquinox' | 'decemberSolstice';
|
|
3
|
+
/** An equinox or solstice. */
|
|
4
|
+
export interface SeasonEvent {
|
|
5
|
+
time: Date;
|
|
6
|
+
kind: SeasonKind;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The equinoxes and solstices within the half-open window `[startUtc, endUtc)`,
|
|
10
|
+
* sorted ascending.
|
|
11
|
+
*
|
|
12
|
+
* A season starts the instant the Sun's apparent geocentric ecliptic longitude
|
|
13
|
+
* reaches 0° (March equinox), 90° (June solstice), 180° (September equinox) or
|
|
14
|
+
* 270° (December solstice). The names are months, not seasons, because the
|
|
15
|
+
* same instant starts opposite seasons in the two hemispheres.
|
|
16
|
+
*/
|
|
17
|
+
export declare function seasons(startUtc: Date, endUtc: Date): SeasonEvent[];
|
package/dist/seasons.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// L3 season search: the four instants a year when the Sun's apparent
|
|
2
|
+
// geocentric ecliptic longitude reaches 0°, 90°, 180° and 270°.
|
|
3
|
+
//
|
|
4
|
+
// Translated from the cosinekitty/astronomy upstream, pinned sha
|
|
5
|
+
// 865d3da7d8112bbc7911238052c6af4aaf877181, source/js/astronomy.ts:
|
|
6
|
+
// `SearchSunLongitude` (~4787), whose `sun_offset` and 0.01 s `Search` are
|
|
7
|
+
// kept. Upstream's `Seasons(year)` (~6130) brackets each search from a fixed
|
|
8
|
+
// calendar date. Here the bracket is a cell of a fixed 80-day grid anchored at
|
|
9
|
+
// J2000, so every window that contains a season finds it from the same bracket
|
|
10
|
+
// and reports the same millisecond — the fixed-seed rule the eclipse searches
|
|
11
|
+
// follow. A cell spans under 90° of solar longitude, so it holds at most one
|
|
12
|
+
// quarter crossing.
|
|
13
|
+
//
|
|
14
|
+
// INTERNAL except `seasons`: not part of the curated public API (see index.ts).
|
|
15
|
+
import { assertSupported, assertSupportedWindowEnd } from './types.js';
|
|
16
|
+
import { utDays, ttDaysFromUt, dateFromUt } from './time.js';
|
|
17
|
+
import { sunEclipticLonDeg } from './illumination.js';
|
|
18
|
+
import { search, angleOffset, quantizedUt, assertReached } from './events.js';
|
|
19
|
+
const SEASON_KINDS = ['marchEquinox', 'juneSolstice', 'septemberEquinox', 'decemberSolstice'];
|
|
20
|
+
/** Grid cell, in days since J2000 UT: under the 89-day shortest season, so a cell holds at most one crossing. */
|
|
21
|
+
const SEASON_CELL_DAYS = 80;
|
|
22
|
+
function sunLonDeg(ut) {
|
|
23
|
+
return sunEclipticLonDeg(ttDaysFromUt(ut));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* UPSTREAM: `SearchSunLongitude` on one grid cell `[t1, t2]`, with
|
|
27
|
+
* `lon1 = sunLonDeg(t1)` already in hand. Returns the crossing of the next
|
|
28
|
+
* multiple of 90° after `lon1`, or `null` when the cell ends before it.
|
|
29
|
+
*/
|
|
30
|
+
function searchSeasonInCell(t1, t2, lon1) {
|
|
31
|
+
const quarter = (Math.floor(lon1 / 90) + 1) % 4;
|
|
32
|
+
const targetLonDeg = 90 * quarter;
|
|
33
|
+
const sunOffset = (ut) => angleOffset(sunLonDeg(ut) - targetLonDeg);
|
|
34
|
+
if (sunOffset(t2) < 0)
|
|
35
|
+
return null;
|
|
36
|
+
const ut = search(sunOffset, t1, t2, 0.01, undefined, 'season search');
|
|
37
|
+
assertReached(ut !== null, 'season search');
|
|
38
|
+
return { ut: ut, quarter };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The equinoxes and solstices within the half-open window `[startUtc, endUtc)`,
|
|
42
|
+
* sorted ascending.
|
|
43
|
+
*
|
|
44
|
+
* A season starts the instant the Sun's apparent geocentric ecliptic longitude
|
|
45
|
+
* reaches 0° (March equinox), 90° (June solstice), 180° (September equinox) or
|
|
46
|
+
* 270° (December solstice). The names are months, not seasons, because the
|
|
47
|
+
* same instant starts opposite seasons in the two hemispheres.
|
|
48
|
+
*/
|
|
49
|
+
export function seasons(startUtc, endUtc) {
|
|
50
|
+
assertSupported(startUtc);
|
|
51
|
+
assertSupportedWindowEnd(endUtc);
|
|
52
|
+
if (startUtc.getTime() >= endUtc.getTime())
|
|
53
|
+
return [];
|
|
54
|
+
const startUt = utDays(startUtc);
|
|
55
|
+
const endUt = utDays(endUtc);
|
|
56
|
+
const events = [];
|
|
57
|
+
let t1 = Math.floor(startUt / SEASON_CELL_DAYS) * SEASON_CELL_DAYS;
|
|
58
|
+
let lon1 = sunLonDeg(t1);
|
|
59
|
+
while (t1 < endUt) {
|
|
60
|
+
const t2 = t1 + SEASON_CELL_DAYS;
|
|
61
|
+
const found = searchSeasonInCell(t1, t2, lon1);
|
|
62
|
+
if (found !== null) {
|
|
63
|
+
const q = quantizedUt(found.ut); // judge the half-open bounds on the reported instant, not the raw root
|
|
64
|
+
if (q >= endUt)
|
|
65
|
+
break;
|
|
66
|
+
if (q >= startUt)
|
|
67
|
+
events.push({ time: dateFromUt(found.ut), kind: SEASON_KINDS[found.quarter] });
|
|
68
|
+
}
|
|
69
|
+
t1 = t2;
|
|
70
|
+
lon1 = sunLonDeg(t1);
|
|
71
|
+
}
|
|
72
|
+
return events;
|
|
73
|
+
}
|