@openwaters/almanac 0.7.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 +101 -11
- 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 +53 -0
- package/dist/events.js +62 -13
- package/dist/illumination.d.ts +25 -0
- package/dist/illumination.js +40 -2
- package/dist/index.d.ts +10 -5
- package/dist/index.js +7 -3
- package/dist/planetModels.js +795 -1
- package/dist/planets.js +7 -0
- 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/dist/transforms.d.ts +6 -3
- package/dist/transforms.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +3 -2
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, 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.
|
|
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,11 +22,13 @@ 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
|
|
28
|
-
line of the 2024-04-08 eclipse, five-planet sky tracks,
|
|
29
|
+
line of the 2024-04-08 eclipse, five-planet sky tracks, planetary rise/set, a
|
|
30
|
+
year of golden-hour crossings and Sun and Moon transits, and a 228-hour Moon
|
|
31
|
+
orientation track.
|
|
29
32
|
|
|
30
33
|
Event searches find altitude extrema with Brent's method and altitude crossings
|
|
31
34
|
with a cosine-seeded secant solver, each root proven inside a half-second
|
|
@@ -49,10 +52,10 @@ Reproduce the comparison locally from the repository root with mise 2026.9.1 or
|
|
|
49
52
|
```bash
|
|
50
53
|
mise install
|
|
51
54
|
mise exec -- npm ci --prefix typescript
|
|
52
|
-
mise exec -- node benchmarks/run.mjs --base v0.4.1 --skip '^(global|planets)/'
|
|
55
|
+
mise exec -- node benchmarks/run.mjs --base v0.4.1 --skip '^(global|planets|suncalc)/'
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
The `--skip` pattern leaves out global eclipse and
|
|
58
|
+
The `--skip` pattern leaves out global eclipse, planetary, and SunCalc-parity workloads absent from that base. Use `--skip '^(solar|global|planets|suncalc)/'` when the base predates 0.4.0, which is when the solar eclipse workloads arrived. For a base that lacks only the SunCalc-parity APIs, use `--skip '^suncalc/'`.
|
|
56
59
|
|
|
57
60
|
CI runs the harness on code changes and fails on **median regressions over 20%**.
|
|
58
61
|
Results include timing tables, raw samples, checksums, and revision/toolchain
|
|
@@ -64,6 +67,39 @@ background task; use shorter windows for interactive queries.
|
|
|
64
67
|
|
|
65
68
|
## Usage
|
|
66
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.
|
|
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
|
+
```
|
|
102
|
+
|
|
67
103
|
### TypeScript
|
|
68
104
|
|
|
69
105
|
```bash
|
|
@@ -72,7 +108,8 @@ npm install @openwaters/almanac
|
|
|
72
108
|
|
|
73
109
|
```ts
|
|
74
110
|
import {
|
|
75
|
-
nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, nextGlobalSolarEclipse, sunEvents, starAltAz
|
|
111
|
+
nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, nextGlobalSolarEclipse, sunEvents, starAltAz,
|
|
112
|
+
seasons, moonApsides, equationOfTime
|
|
76
113
|
} from '@openwaters/almanac';
|
|
77
114
|
|
|
78
115
|
const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
|
|
@@ -96,6 +133,13 @@ for (const { kind, time } of sunEvents(today, tomorrow, observer)) {
|
|
|
96
133
|
|
|
97
134
|
// Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
|
|
98
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
|
|
99
143
|
```
|
|
100
144
|
|
|
101
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:
|
|
@@ -112,7 +156,7 @@ const crossings = sunEvents(today, tomorrow, viewer, eyeHeightM);
|
|
|
112
156
|
### Swift
|
|
113
157
|
|
|
114
158
|
```swift
|
|
115
|
-
.package(url: "https://github.com/openwatersio/almanac.git", exact: "0.
|
|
159
|
+
.package(url: "https://github.com/openwatersio/almanac.git", exact: "0.8.0")
|
|
116
160
|
```
|
|
117
161
|
|
|
118
162
|
```swift
|
|
@@ -143,18 +187,64 @@ for event in try sunEvents(from: today, to: tomorrow, observer: observer) {
|
|
|
143
187
|
|
|
144
188
|
// Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
|
|
145
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)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Golden hour, transits, and the Moon's tilt
|
|
218
|
+
|
|
219
|
+
`sunAltitudeEvents` finds the Sun's center crossing any altitude, with the twilight convention, so `6` gives golden hour and `-6` civil twilight. `sunTransits` and `moonTransits` give each body's highest (`upper`) and lowest (`lower`) meridian crossings. `moonOrientation` turns the Moon's disc: subtracting the parallactic angle from the bright-limb angle gives the bright limb's direction from the zenith.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { sunAltitudeEvents, sunTransits, moonTransits, moonOrientation } from '@openwaters/almanac';
|
|
223
|
+
const golden = sunAltitudeEvents(today, tomorrow, observer, 6); // 'rising' ends morning golden hour, 'falling' starts the evening one
|
|
224
|
+
const solarMidnight = sunTransits(today, tomorrow, observer).find(e => e.kind === 'lower');
|
|
225
|
+
const moonHighest = moonTransits(today, tomorrow, observer).find(e => e.kind === 'upper');
|
|
226
|
+
const { brightLimbAngleDeg, parallacticAngleDeg } = moonOrientation(today, observer);
|
|
227
|
+
const brightLimbFromZenithDeg = brightLimbAngleDeg - parallacticAngleDeg;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```swift
|
|
231
|
+
let golden = try sunAltitudeEvents(from: today, to: tomorrow, observer: observer, altitudeDeg: 6)
|
|
232
|
+
let solarMidnight = try sunTransits(from: today, to: tomorrow, observer: observer).first { $0.kind == .lower }
|
|
233
|
+
let moonHighest = try moonTransits(from: today, to: tomorrow, observer: observer).first { $0.kind == .upper }
|
|
234
|
+
let orientation = try moonOrientation(at: today, observer: observer)
|
|
235
|
+
let brightLimbFromZenithDeg = orientation.brightLimbAngleDeg - orientation.parallacticAngleDeg
|
|
146
236
|
```
|
|
147
237
|
|
|
148
238
|
### Planetary views
|
|
149
239
|
|
|
150
|
-
Mercury, Venus, Earth, Mars, Jupiter, and
|
|
240
|
+
Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune 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
241
|
|
|
152
242
|
```ts
|
|
153
243
|
import { planetHeliocentricPosition, planetAltAz, planetIllumination, planetEvents,
|
|
154
244
|
horizonDip, sunAltAz } from '@openwaters/almanac';
|
|
155
245
|
import type { Planet } from '@openwaters/almanac';
|
|
156
246
|
|
|
157
|
-
const planets: Planet[] = ['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn'];
|
|
247
|
+
const planets: Planet[] = ['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'];
|
|
158
248
|
const now = new Date();
|
|
159
249
|
const positions = planets.map(planet => ({ planet, ...planetHeliocentricPosition(planet, now) }));
|
|
160
250
|
|
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
|
@@ -17,6 +17,18 @@ export interface PlanetEvent {
|
|
|
17
17
|
time: Date;
|
|
18
18
|
kind: PlanetEventKind;
|
|
19
19
|
}
|
|
20
|
+
/** The Sun's center crossing a caller-chosen altitude, upward or downward. */
|
|
21
|
+
export type AltitudeEventKind = 'rising' | 'falling';
|
|
22
|
+
export interface AltitudeEvent {
|
|
23
|
+
time: Date;
|
|
24
|
+
kind: AltitudeEventKind;
|
|
25
|
+
}
|
|
26
|
+
/** A body crossing the local meridian: highest at `upper`, lowest at `lower`. */
|
|
27
|
+
export type TransitEventKind = 'upper' | 'lower';
|
|
28
|
+
export interface TransitEvent {
|
|
29
|
+
time: Date;
|
|
30
|
+
kind: TransitEventKind;
|
|
31
|
+
}
|
|
20
32
|
/** A quarter lunar phase. */
|
|
21
33
|
export type MoonPhaseName = 'new' | 'firstQuarter' | 'full' | 'lastQuarter';
|
|
22
34
|
export interface MoonPhaseEvent {
|
|
@@ -31,6 +43,16 @@ export interface MoonPhaseEvent {
|
|
|
31
43
|
export declare const MOON_MEAN_RADIUS_KM = 1737.4;
|
|
32
44
|
/** UPSTREAM: `SUN_RADIUS_KM`, astronomy.ts line 134. INTERNAL, see above. */
|
|
33
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;
|
|
34
56
|
/**
|
|
35
57
|
* Truncates a ut-days root to the same integer-ms precision `dateFromUt`
|
|
36
58
|
* reports it at (TimeClip). A root finder's raw `ut` carries sub-ms
|
|
@@ -43,6 +65,10 @@ export declare const SUN_RADIUS_KM = 695700;
|
|
|
43
65
|
* no other module consumes it.
|
|
44
66
|
*/
|
|
45
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;
|
|
46
72
|
/**
|
|
47
73
|
* Sun rise, set, the three twilights and upper transit within the half-open
|
|
48
74
|
* window `[startUtc, endUtc)`, sorted ascending.
|
|
@@ -65,6 +91,33 @@ export declare function sunEvents(startUtc: Date, endUtc: Date, observer: Observ
|
|
|
65
91
|
export declare function moonEvents(startUtc: Date, endUtc: Date, observer: Observer, heightAboveGroundM?: number): MoonEvent[];
|
|
66
92
|
/** Rise/set of a point center at geometric altitude horizonDip minus 34 arcminutes. */
|
|
67
93
|
export declare function planetEvents(planet: Planet, startUtc: Date, endUtc: Date, observer: Observer, heightAboveGroundM?: number): PlanetEvent[];
|
|
94
|
+
/**
|
|
95
|
+
* The Sun's unrefracted geometric center crossing `altitudeDeg` within the
|
|
96
|
+
* half-open window `[startUtc, endUtc)`, sorted ascending: `rising` upward,
|
|
97
|
+
* `falling` downward. The twilight convention, so `-6` returns the civil
|
|
98
|
+
* twilight instants. A window with no crossing, as in polar day or night,
|
|
99
|
+
* returns an empty list.
|
|
100
|
+
*/
|
|
101
|
+
export declare function sunAltitudeEvents(startUtc: Date, endUtc: Date, observer: Observer, altitudeDeg: number): AltitudeEvent[];
|
|
102
|
+
/**
|
|
103
|
+
* The Sun's upper (local hour angle 0) and lower (180°) transits within the
|
|
104
|
+
* half-open window `[startUtc, endUtc)`, sorted ascending, whether or not the
|
|
105
|
+
* Sun is above the horizon. The upper transit is `sunEvents`' `transit`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function sunTransits(startUtc: Date, endUtc: Date, observer: Observer): TransitEvent[];
|
|
108
|
+
/**
|
|
109
|
+
* The Moon's upper and lower transits within the half-open window
|
|
110
|
+
* `[startUtc, endUtc)`, sorted ascending, from the topocentric hour angle, so
|
|
111
|
+
* parallax is included, whether or not the Moon is above the horizon.
|
|
112
|
+
*/
|
|
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;
|
|
68
121
|
/**
|
|
69
122
|
* UPSTREAM: `Search`, astronomy.ts ~4634 — bisection with quadratic
|
|
70
123
|
* acceleration for the ascending zero crossing of `f` in `[t1, t2]`. Times are
|
package/dist/events.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
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 { assertPlanet, assertSupported, assertSupportedWindowEnd, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
|
|
24
|
+
import { assertObserver, 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';
|
|
@@ -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;
|
|
@@ -412,9 +412,12 @@ function secantCrossing(f, a, b, s0, s1, targetDeg) {
|
|
|
412
412
|
* solving every monotonic segment between consecutive extrema for every target
|
|
413
413
|
* level. The chain starts a full cycle before `startUt` so the segment
|
|
414
414
|
* containing the window's first instant is covered, and runs one extremum past
|
|
415
|
-
* `endUt` so an event just inside the window's end is too.
|
|
415
|
+
* `endUt` so an event just inside the window's end is too. `transitKind` is
|
|
416
|
+
* emitted at each hour angle 0 and `lowerTransitKind` at each hour angle 180.
|
|
417
|
+
* With no levels the extrema are never refined: the hour-angle chain alone
|
|
418
|
+
* places the transits.
|
|
416
419
|
*/
|
|
417
|
-
function searchAltitudeEvents(sample, levels, transitKind, startUt, endUt, haRateDegPerDay, cycleDays, flatCycle) {
|
|
420
|
+
function searchAltitudeEvents(sample, levels, transitKind, lowerTransitKind, startUt, endUt, haRateDegPerDay, cycleDays, flatCycle) {
|
|
418
421
|
const extremumHalfWidthDays = flatCycle ? cycleDays / 2 : EXTREMUM_HALF_WIDTH_DAYS;
|
|
419
422
|
const found = [];
|
|
420
423
|
const emit = (ut, kind) => {
|
|
@@ -438,7 +441,8 @@ function searchAltitudeEvents(sample, levels, transitKind, startUt, endUt, haRat
|
|
|
438
441
|
// extremum we are not allowed to probe past. The segment up to it is
|
|
439
442
|
// still monotonic, so any crossing inside the window is still found.
|
|
440
443
|
const s1 = atRangeEnd ? sample(MAX_UT)
|
|
441
|
-
:
|
|
444
|
+
: levels.length === 0 ? sample(nextEst)
|
|
445
|
+
: refineExtremum(sample, nextEst, nextIsMax, extremumHalfWidthDays, s0.ut, flatCycle);
|
|
442
446
|
assertReached(s1.ut > s0.ut, 'extremum chain ordering');
|
|
443
447
|
// Earlier extrema still establish the chain, but their crossings cannot be emitted.
|
|
444
448
|
if (quantizedUt(s1.ut) >= startUt) {
|
|
@@ -450,8 +454,9 @@ function searchAltitudeEvents(sample, levels, transitKind, startUt, endUt, haRat
|
|
|
450
454
|
emit(solveCrossing(sample, spec.level, s0, s1), g1 > 0 ? spec.rising : spec.falling);
|
|
451
455
|
}
|
|
452
456
|
}
|
|
453
|
-
|
|
454
|
-
|
|
457
|
+
const transit = nextIsMax ? transitKind : lowerTransitKind;
|
|
458
|
+
if (transit !== null && !atRangeEnd)
|
|
459
|
+
emit(nextEst, transit);
|
|
455
460
|
s0 = s1;
|
|
456
461
|
if (atRangeEnd)
|
|
457
462
|
break;
|
|
@@ -491,7 +496,7 @@ export function sunEvents(startUtc, endUtc, observer, heightAboveGroundM = 0) {
|
|
|
491
496
|
const dip = horizonDip(observer, heightAboveGroundM);
|
|
492
497
|
if (startUtc.getTime() >= endUtc.getTime())
|
|
493
498
|
return [];
|
|
494
|
-
return searchAltitudeEvents(sunSampler(observer, dip), SUN_LEVELS, 'transit', utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
|
|
499
|
+
return searchAltitudeEvents(sunSampler(observer, dip), SUN_LEVELS, 'transit', null, utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
|
|
495
500
|
}
|
|
496
501
|
/**
|
|
497
502
|
* Moonrise and moonset within the half-open window `[startUtc, endUtc)`,
|
|
@@ -505,7 +510,7 @@ export function moonEvents(startUtc, endUtc, observer, heightAboveGroundM = 0) {
|
|
|
505
510
|
const dip = horizonDip(observer, heightAboveGroundM);
|
|
506
511
|
if (startUtc.getTime() >= endUtc.getTime())
|
|
507
512
|
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));
|
|
513
|
+
return searchAltitudeEvents(moonSampler(observer, dip), MOON_LEVELS, null, null, utDays(startUtc), utDays(endUtc), MOON_HA_RATE_DEG_PER_DAY, MOON_CYCLE_DAYS, flatCycle(observer));
|
|
509
514
|
}
|
|
510
515
|
/** Rise/set of a point center at geometric altitude horizonDip minus 34 arcminutes. */
|
|
511
516
|
export function planetEvents(planet, startUtc, endUtc, observer, heightAboveGroundM = 0) {
|
|
@@ -516,7 +521,51 @@ export function planetEvents(planet, startUtc, endUtc, observer, heightAboveGrou
|
|
|
516
521
|
if (startUtc.getTime() >= endUtc.getTime())
|
|
517
522
|
return [];
|
|
518
523
|
const cycleDays = 0.9972695717592592;
|
|
519
|
-
return searchAltitudeEvents(planetSampler(planet, observer, dip), MOON_LEVELS, null, utDays(startUtc), utDays(endUtc), 360 / cycleDays, cycleDays, flatCycle(observer));
|
|
524
|
+
return searchAltitudeEvents(planetSampler(planet, observer, dip), MOON_LEVELS, null, null, utDays(startUtc), utDays(endUtc), 360 / cycleDays, cycleDays, flatCycle(observer));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The Sun's unrefracted geometric center crossing `altitudeDeg` within the
|
|
528
|
+
* half-open window `[startUtc, endUtc)`, sorted ascending: `rising` upward,
|
|
529
|
+
* `falling` downward. The twilight convention, so `-6` returns the civil
|
|
530
|
+
* twilight instants. A window with no crossing, as in polar day or night,
|
|
531
|
+
* returns an empty list.
|
|
532
|
+
*/
|
|
533
|
+
export function sunAltitudeEvents(startUtc, endUtc, observer, altitudeDeg) {
|
|
534
|
+
if (!(altitudeDeg >= -90 && altitudeDeg <= 90))
|
|
535
|
+
throw new RangeError(`altitudeDeg out of range: ${altitudeDeg}`);
|
|
536
|
+
assertSupported(startUtc);
|
|
537
|
+
assertSupportedWindowEnd(endUtc);
|
|
538
|
+
assertObserver(observer);
|
|
539
|
+
if (startUtc.getTime() >= endUtc.getTime())
|
|
540
|
+
return [];
|
|
541
|
+
const levels = [{ level: altitudeDeg, rising: 'rising', falling: 'falling' }];
|
|
542
|
+
return searchAltitudeEvents(sunSampler(observer, 0), levels, null, null, utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* The Sun's upper (local hour angle 0) and lower (180°) transits within the
|
|
546
|
+
* half-open window `[startUtc, endUtc)`, sorted ascending, whether or not the
|
|
547
|
+
* Sun is above the horizon. The upper transit is `sunEvents`' `transit`.
|
|
548
|
+
*/
|
|
549
|
+
export function sunTransits(startUtc, endUtc, observer) {
|
|
550
|
+
assertSupported(startUtc);
|
|
551
|
+
assertSupportedWindowEnd(endUtc);
|
|
552
|
+
assertObserver(observer);
|
|
553
|
+
if (startUtc.getTime() >= endUtc.getTime())
|
|
554
|
+
return [];
|
|
555
|
+
return searchAltitudeEvents(sunSampler(observer, 0), [], 'upper', 'lower', utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The Moon's upper and lower transits within the half-open window
|
|
559
|
+
* `[startUtc, endUtc)`, sorted ascending, from the topocentric hour angle, so
|
|
560
|
+
* parallax is included, whether or not the Moon is above the horizon.
|
|
561
|
+
*/
|
|
562
|
+
export function moonTransits(startUtc, endUtc, observer) {
|
|
563
|
+
assertSupported(startUtc);
|
|
564
|
+
assertSupportedWindowEnd(endUtc);
|
|
565
|
+
assertObserver(observer);
|
|
566
|
+
if (startUtc.getTime() >= endUtc.getTime())
|
|
567
|
+
return [];
|
|
568
|
+
return searchAltitudeEvents(moonSampler(observer, 0), [], 'upper', 'lower', utDays(startUtc), utDays(endUtc), MOON_HA_RATE_DEG_PER_DAY, MOON_CYCLE_DAYS, flatCycle(observer));
|
|
520
569
|
}
|
|
521
570
|
// -------------------------------------------------------------- moon phases
|
|
522
571
|
/**
|
|
@@ -525,7 +574,7 @@ export function planetEvents(planet, startUtc, endUtc, observer, heightAboveGrou
|
|
|
525
574
|
* moon-phase definition — upstream's `PairLongitude` uses geometric longitudes,
|
|
526
575
|
* which sit a flat ~40 s off the USNO catalogue.
|
|
527
576
|
*/
|
|
528
|
-
const MEAN_SYNODIC_MONTH = 29.530588;
|
|
577
|
+
export const MEAN_SYNODIC_MONTH = 29.530588;
|
|
529
578
|
const PHASE_NAMES = ['new', 'firstQuarter', 'full', 'lastQuarter'];
|
|
530
579
|
/** UPSTREAM: `QuadInterp`, astronomy.ts ~4528. */
|
|
531
580
|
function quadInterp(tm, dt, fa, fm, fb) {
|
package/dist/illumination.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Observer } from './types.js';
|
|
1
2
|
import { Vec3 } from './nutation.js';
|
|
2
3
|
/** Moon illumination at an instant. `phase` is 0 at new moon, 0.5 at full. */
|
|
3
4
|
export interface MoonIllumination {
|
|
@@ -8,6 +9,12 @@ export interface MoonIllumination {
|
|
|
8
9
|
}
|
|
9
10
|
/** UPSTREAM: `AngleBetween`, astronomy.ts lines 256-273 — angle between two vectors, [0, 180]. */
|
|
10
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;
|
|
11
18
|
/**
|
|
12
19
|
* INTERNAL: the Moon's apparent ecliptic longitude ahead of the Sun's, [0, 360)
|
|
13
20
|
* — 0 at new moon, 180 at full. The single definition of lunar phase in this
|
|
@@ -33,3 +40,21 @@ export declare function moonIlluminationAtTT(tt: number): MoonIllumination;
|
|
|
33
40
|
* phase (0 = new, 0.5 = full, `waxing` = phase < 0.5).
|
|
34
41
|
*/
|
|
35
42
|
export declare function moonIllumination(time: Date): MoonIllumination;
|
|
43
|
+
/**
|
|
44
|
+
* How the Moon's disc is turned on the observer's sky. `brightLimbAngleDeg` is
|
|
45
|
+
* the position angle of the bright limb's midpoint from celestial north
|
|
46
|
+
* through east, in [0, 360); `parallacticAngleDeg` is the angle at the Moon
|
|
47
|
+
* from celestial north to the observer's zenith, in (-180, 180]. Their
|
|
48
|
+
* difference is the bright limb's angle from the zenith.
|
|
49
|
+
*/
|
|
50
|
+
export interface MoonOrientation {
|
|
51
|
+
brightLimbAngleDeg: number;
|
|
52
|
+
parallacticAngleDeg: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The Moon's orientation at an instant: the bright limb from the apparent
|
|
56
|
+
* topocentric Moon and the apparent geocentric Sun (Meeus, Astronomical
|
|
57
|
+
* Algorithms, 48.5), and the parallactic angle from the Moon's topocentric
|
|
58
|
+
* hour angle and declination at the observer's geodetic latitude (Meeus 14.1).
|
|
59
|
+
*/
|
|
60
|
+
export declare function moonOrientation(time: Date, observer: Observer): MoonOrientation;
|
package/dist/illumination.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// 865d3da7d8112bbc7911238052c6af4aaf877181, source/js/astronomy.ts.
|
|
9
9
|
//
|
|
10
10
|
// INTERNAL: not part of the curated public API (see index.ts).
|
|
11
|
-
import { assertSupported } from './types.js';
|
|
12
|
-
import { ttDays } from './time.js';
|
|
11
|
+
import { assertObserver, assertSupported } from './types.js';
|
|
12
|
+
import { ttDays, ttDaysFromUt, utDays } from './time.js';
|
|
13
|
+
import { topoAltAzUnrefracted } from './transforms.js';
|
|
14
|
+
import { sunApparentAtTT } from './positions.js';
|
|
13
15
|
import { DEG2RAD, PrecessDirection, RAD2DEG, earthTilt, eclipticToEquatorial, gyration } from './nutation.js';
|
|
14
16
|
import { moonGeoVectorEqj } from './moon.js';
|
|
15
17
|
import { earthHelioVector, sunGeoVectorEqj } from './sun.js';
|
|
@@ -42,6 +44,14 @@ function eclipticLonOfDateDeg(eqj, tt) {
|
|
|
42
44
|
elon += 360;
|
|
43
45
|
return elon;
|
|
44
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
|
+
}
|
|
45
55
|
/** UPSTREAM: `NormalizeLongitude`, astronomy.ts lines 4735-4739. */
|
|
46
56
|
function normalizeLongitude(lon) {
|
|
47
57
|
return ((lon % 360) + 360) % 360;
|
|
@@ -89,3 +99,31 @@ export function moonIllumination(time) {
|
|
|
89
99
|
assertSupported(time);
|
|
90
100
|
return moonIlluminationAtTT(ttDays(time));
|
|
91
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The Moon's orientation at an instant: the bright limb from the apparent
|
|
104
|
+
* topocentric Moon and the apparent geocentric Sun (Meeus, Astronomical
|
|
105
|
+
* Algorithms, 48.5), and the parallactic angle from the Moon's topocentric
|
|
106
|
+
* hour angle and declination at the observer's geodetic latitude (Meeus 14.1).
|
|
107
|
+
*/
|
|
108
|
+
export function moonOrientation(time, observer) {
|
|
109
|
+
assertSupported(time);
|
|
110
|
+
assertObserver(observer);
|
|
111
|
+
const ut = utDays(time);
|
|
112
|
+
const tt = ttDaysFromUt(ut);
|
|
113
|
+
const moon = topoAltAzUnrefracted(moonGeoVectorEqj(tt), ut, observer);
|
|
114
|
+
const sun = sunApparentAtTT(tt);
|
|
115
|
+
const dec = moon.decDeg * DEG2RAD;
|
|
116
|
+
const sunDec = sun.decDeg * DEG2RAD;
|
|
117
|
+
const dRa = (sun.raDeg - moon.raDeg) * DEG2RAD;
|
|
118
|
+
let brightLimbAngleDeg = RAD2DEG * Math.atan2(Math.cos(sunDec) * Math.sin(dRa), Math.sin(sunDec) * Math.cos(dec) - Math.cos(sunDec) * Math.sin(dec) * Math.cos(dRa));
|
|
119
|
+
if (brightLimbAngleDeg < 0)
|
|
120
|
+
brightLimbAngleDeg += 360;
|
|
121
|
+
if (brightLimbAngleDeg >= 360)
|
|
122
|
+
brightLimbAngleDeg -= 360;
|
|
123
|
+
const ha = moon.hourAngleDeg * DEG2RAD;
|
|
124
|
+
const lat = observer.latitudeDeg * DEG2RAD;
|
|
125
|
+
let parallacticAngleDeg = RAD2DEG * Math.atan2(Math.sin(ha), Math.tan(lat) * Math.cos(dec) - Math.sin(dec) * Math.cos(ha));
|
|
126
|
+
if (parallacticAngleDeg <= -180)
|
|
127
|
+
parallacticAngleDeg += 360;
|
|
128
|
+
return { brightLimbAngleDeg, parallacticAngleDeg };
|
|
129
|
+
}
|