@openwaters/almanac 0.0.0-bootstrap
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/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +75 -0
- package/dist/eclipse.d.ts +66 -0
- package/dist/eclipse.js +295 -0
- package/dist/events.d.ts +81 -0
- package/dist/events.js +492 -0
- package/dist/illumination.d.ts +32 -0
- package/dist/illumination.js +88 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/moon.d.ts +38 -0
- package/dist/moon.js +310 -0
- package/dist/nutation.d.ts +65 -0
- package/dist/nutation.js +255 -0
- package/dist/positions.d.ts +34 -0
- package/dist/positions.js +52 -0
- package/dist/sun.d.ts +27 -0
- package/dist/sun.js +184 -0
- package/dist/time.d.ts +28 -0
- package/dist/time.js +144 -0
- package/dist/transforms.d.ts +61 -0
- package/dist/transforms.js +198 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +31 -0
- package/package.json +34 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// L2 transform layer: Greenwich sidereal time, topocentric parallax (the
|
|
2
|
+
// observer's geocentric position via WGS-84 flattening), and atmospheric
|
|
3
|
+
// refraction — turning L1's geocentric equatorial vectors into what an
|
|
4
|
+
// observer on Earth's surface sees.
|
|
5
|
+
//
|
|
6
|
+
// Translated from the cosinekitty/astronomy upstream (pinned sha
|
|
7
|
+
// 865d3da7d8112bbc7911238052c6af4aaf877181, source/js/astronomy.ts):
|
|
8
|
+
// SiderealTime (era ~2014 + sidereal_time ~2031); the observer's geocentric
|
|
9
|
+
// position (terra ~2147, geo_pos ~2236), which the topocentric path inside
|
|
10
|
+
// Equator(body, date, observer, ofdate=true, aberration=true) (~2803: body
|
|
11
|
+
// EQJ vector minus observer EQJ vector, then gyrate the difference to date)
|
|
12
|
+
// consumes; Horizon (~2565), stopping short of its post-refraction ra/dec
|
|
13
|
+
// refinement since this layer only needs az/alt; Refraction 'normal' (~7176).
|
|
14
|
+
//
|
|
15
|
+
// INTERNAL: not part of the curated public API (see index.ts) except
|
|
16
|
+
// sunAltAz/moonAltAz.
|
|
17
|
+
import { assertObserver, assertSupported } from './types.js';
|
|
18
|
+
import { ttDaysFromUt, utDays } from './time.js';
|
|
19
|
+
import { DEG2RAD, RAD2DEG, EARTH_EQUATORIAL_RADIUS_KM, EARTH_FLATTENING, EARTH_FLATTENING_SQUARED, KM_PER_AU, PrecessDirection, earthTilt, equatorialFromVector, gyration } from './nutation.js';
|
|
20
|
+
import { sunGeoVectorEqj } from './sun.js';
|
|
21
|
+
import { moonGeoVectorEqj } from './moon.js';
|
|
22
|
+
/** UPSTREAM: `era` (Earth Rotation Angle), astronomy.ts ~2014. */
|
|
23
|
+
function earthRotationAngleDeg(ut) {
|
|
24
|
+
const thet1 = 0.7790572732640 + 0.00273781191135448 * ut;
|
|
25
|
+
const thet3 = ut % 1;
|
|
26
|
+
let theta = 360 * ((thet1 + thet3) % 1);
|
|
27
|
+
if (theta < 0)
|
|
28
|
+
theta += 360;
|
|
29
|
+
return theta;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* UPSTREAM: `sidereal_time`, astronomy.ts ~2031 — Greenwich Apparent Sidereal
|
|
33
|
+
* Time (GAST). Returned in **degrees** [0, 360) rather than upstream's hours:
|
|
34
|
+
* every caller in this port (terra/geo_pos, Horizon's spin angle) only ever
|
|
35
|
+
* uses `15 * gastHours`, so upstream's final `/15` and each caller's `*15`
|
|
36
|
+
* are folded into one multiply here — same value, one division dropped.
|
|
37
|
+
*/
|
|
38
|
+
export function siderealDeg(ut) {
|
|
39
|
+
const tt = ttDaysFromUt(ut);
|
|
40
|
+
const t = tt / 36525;
|
|
41
|
+
const eqeq = 15 * earthTilt(tt).ee; // equation of the equinoxes
|
|
42
|
+
const theta = earthRotationAngleDeg(ut);
|
|
43
|
+
const st = eqeq + 0.014506 +
|
|
44
|
+
((((-0.0000000368 * t
|
|
45
|
+
- 0.000029956) * t
|
|
46
|
+
- 0.00000044) * t
|
|
47
|
+
+ 1.3915817) * t
|
|
48
|
+
+ 4612.156534) * t;
|
|
49
|
+
let gastDeg = (st / 3600 + theta) % 360;
|
|
50
|
+
if (gastDeg < 0)
|
|
51
|
+
gastDeg += 360;
|
|
52
|
+
return gastDeg;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* UPSTREAM: `terra` (astronomy.ts ~2147 — position only; its velocity output
|
|
56
|
+
* is for observer diurnal aberration, which this parallax-only path doesn't
|
|
57
|
+
* need). Upstream's `geo_pos` (~2236) then gyrates this into EQJ so that
|
|
58
|
+
* `Equator` can subtract it from the body's EQJ vector and gyrate the
|
|
59
|
+
* difference back to date. Gyration is a rotation, so
|
|
60
|
+
* `R(body − observer) === R·body − R·observer`, and `R` applied to the
|
|
61
|
+
* observer's own inverse-gyration is the vector below: one precession and one
|
|
62
|
+
* nutation per evaluation drop out of the inner loop of every event search.
|
|
63
|
+
*
|
|
64
|
+
* @returns geocentric position of the observer, in AU, true equator of date.
|
|
65
|
+
*/
|
|
66
|
+
function observerGeoVectorOfDate(gastDeg, observer) {
|
|
67
|
+
const phi = observer.latitudeDeg * DEG2RAD;
|
|
68
|
+
const sinphi = Math.sin(phi);
|
|
69
|
+
const cosphi = Math.cos(phi);
|
|
70
|
+
const c = 1 / Math.hypot(cosphi, EARTH_FLATTENING * sinphi);
|
|
71
|
+
const s = EARTH_FLATTENING_SQUARED * c;
|
|
72
|
+
const htKm = (observer.elevationM ?? 0) / 1000;
|
|
73
|
+
const ach = EARTH_EQUATORIAL_RADIUS_KM * c + htKm;
|
|
74
|
+
const ash = EARTH_EQUATORIAL_RADIUS_KM * s + htKm;
|
|
75
|
+
const stlocl = (gastDeg + observer.longitudeDeg) * DEG2RAD;
|
|
76
|
+
const sinst = Math.sin(stlocl);
|
|
77
|
+
const cosst = Math.cos(stlocl);
|
|
78
|
+
return {
|
|
79
|
+
x: ach * cosphi * cosst / KM_PER_AU,
|
|
80
|
+
y: ach * cosphi * sinst / KM_PER_AU,
|
|
81
|
+
z: ash * sinphi / KM_PER_AU
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** UPSTREAM: `spin`, astronomy.ts ~2518 — rotate a horizontal-frame unit vector by sidereal angle. */
|
|
85
|
+
function spin(angleDeg, pos) {
|
|
86
|
+
const angr = angleDeg * DEG2RAD;
|
|
87
|
+
const c = Math.cos(angr);
|
|
88
|
+
const s = Math.sin(angr);
|
|
89
|
+
return { x: c * pos.x + s * pos.y, y: c * pos.y - s * pos.x, z: pos.z };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* UPSTREAM: the topocentric path composed of `Equator(body, date, observer,
|
|
93
|
+
* ofdate=true, aberration=true)` (astronomy.ts ~2803) followed by `Horizon`
|
|
94
|
+
* (astronomy.ts ~2565), through its az/zd computation only — Horizon's
|
|
95
|
+
* post-refraction ra/dec refinement branch produces topocentric ra/dec, which
|
|
96
|
+
* this layer's callers (sunAltAz/moonAltAz) don't need.
|
|
97
|
+
*
|
|
98
|
+
* @param bodyEqj geocentric position of the body in AU, J2000 mean equator
|
|
99
|
+
* (EQJ) — i.e. `GeoVector`'s return, matching {@link sunGeoVectorEqj} /
|
|
100
|
+
* {@link moonGeoVectorEqj}, NOT the already-of-date vector
|
|
101
|
+
* {@link sunApparentAtTT}/{@link moonApparentAtTT} consume.
|
|
102
|
+
* @param ut days since J2000 in UT — the event layer iterates in days, and
|
|
103
|
+
* `Date` round-trips are pure overhead on a search's inner loop.
|
|
104
|
+
* @returns unrefracted topocentric azimuth/altitude, plus the local hour angle
|
|
105
|
+
* and topocentric distance the same evaluation already produced.
|
|
106
|
+
*/
|
|
107
|
+
export function topoAltAzUnrefracted(bodyEqj, ut, observer) {
|
|
108
|
+
const tt = ttDaysFromUt(ut);
|
|
109
|
+
const gast = siderealDeg(ut);
|
|
110
|
+
const gcObserver = observerGeoVectorOfDate(gast, observer);
|
|
111
|
+
const bodyOfDate = gyration(bodyEqj, tt, PrecessDirection.From2000);
|
|
112
|
+
const datevect = {
|
|
113
|
+
x: bodyOfDate.x - gcObserver.x,
|
|
114
|
+
y: bodyOfDate.y - gcObserver.y,
|
|
115
|
+
z: bodyOfDate.z - gcObserver.z
|
|
116
|
+
};
|
|
117
|
+
const { raDeg, decDeg, distanceAu } = equatorialFromVector(datevect);
|
|
118
|
+
const sinlat = Math.sin(observer.latitudeDeg * DEG2RAD);
|
|
119
|
+
const coslat = Math.cos(observer.latitudeDeg * DEG2RAD);
|
|
120
|
+
const sinlon = Math.sin(observer.longitudeDeg * DEG2RAD);
|
|
121
|
+
const coslon = Math.cos(observer.longitudeDeg * DEG2RAD);
|
|
122
|
+
const sindc = Math.sin(decDeg * DEG2RAD);
|
|
123
|
+
const cosdc = Math.cos(decDeg * DEG2RAD);
|
|
124
|
+
// upstream's ra*HOUR2RAD === raDeg*DEG2RAD under this port's degrees-only RA convention.
|
|
125
|
+
const sinra = Math.sin(raDeg * DEG2RAD);
|
|
126
|
+
const cosra = Math.cos(raDeg * DEG2RAD);
|
|
127
|
+
const uze = { x: coslat * coslon, y: coslat * sinlon, z: sinlat };
|
|
128
|
+
const une = { x: -sinlat * coslon, y: -sinlat * sinlon, z: coslat };
|
|
129
|
+
const uwe = { x: sinlon, y: -coslon, z: 0 };
|
|
130
|
+
const spinAngle = -gast;
|
|
131
|
+
const uz = spin(spinAngle, uze);
|
|
132
|
+
const un = spin(spinAngle, une);
|
|
133
|
+
const uw = spin(spinAngle, uwe);
|
|
134
|
+
const p = { x: cosdc * cosra, y: cosdc * sinra, z: sindc };
|
|
135
|
+
const pz = p.x * uz.x + p.y * uz.y + p.z * uz.z;
|
|
136
|
+
const pn = p.x * un.x + p.y * un.y + p.z * un.z;
|
|
137
|
+
const pw = p.x * uw.x + p.y * uw.y + p.z * uw.z;
|
|
138
|
+
const proj = Math.hypot(pn, pw);
|
|
139
|
+
let az;
|
|
140
|
+
if (proj > 0) {
|
|
141
|
+
az = -RAD2DEG * Math.atan2(pw, pn);
|
|
142
|
+
if (az < 0)
|
|
143
|
+
az += 360;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
az = 0;
|
|
147
|
+
}
|
|
148
|
+
const zd = RAD2DEG * Math.atan2(proj, pz);
|
|
149
|
+
let hourAngleDeg = (gast + observer.longitudeDeg - raDeg) % 360;
|
|
150
|
+
if (hourAngleDeg < 0)
|
|
151
|
+
hourAngleDeg += 360;
|
|
152
|
+
return { azDeg: az, altDeg: 90 - zd, hourAngleDeg, distanceAu };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* UPSTREAM: `Refraction('normal', altitude)`, astronomy.ts ~7176 — the
|
|
156
|
+
* Saemundsson/Meeus atmospheric-lift formula (JPL Horizons' algorithm per
|
|
157
|
+
* upstream's comment), gradually reduced toward the nadir so a refracted
|
|
158
|
+
* altitude never drops below -90.
|
|
159
|
+
*/
|
|
160
|
+
export function refractionDeg(altitudeDeg) {
|
|
161
|
+
if (altitudeDeg < -90.0 || altitudeDeg > 90.0)
|
|
162
|
+
return 0.0;
|
|
163
|
+
let hd = altitudeDeg;
|
|
164
|
+
if (hd < -1.0)
|
|
165
|
+
hd = -1.0;
|
|
166
|
+
let refr = (1.02 / Math.tan((hd + 10.3 / (hd + 5.11)) * DEG2RAD)) / 60.0;
|
|
167
|
+
if (altitudeDeg < -1.0) {
|
|
168
|
+
// Gradually reduce refraction toward the nadir so we never get an
|
|
169
|
+
// altitude angle less than -90 degrees.
|
|
170
|
+
refr *= (altitudeDeg + 90.0) / 89.0;
|
|
171
|
+
}
|
|
172
|
+
return refr;
|
|
173
|
+
}
|
|
174
|
+
function refract(unrefracted) {
|
|
175
|
+
return { azDeg: unrefracted.azDeg, altDeg: unrefracted.altDeg + refractionDeg(unrefracted.altDeg) };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Topocentric azimuth/altitude of the Sun — parallax (via the observer's
|
|
179
|
+
* geocentric position, WGS-84 flattening, elevation) and atmospheric
|
|
180
|
+
* refraction (upstream `'normal'` model) both applied.
|
|
181
|
+
*/
|
|
182
|
+
export function sunAltAz(time, observer) {
|
|
183
|
+
assertSupported(time);
|
|
184
|
+
assertObserver(observer);
|
|
185
|
+
const ut = utDays(time);
|
|
186
|
+
return refract(topoAltAzUnrefracted(sunGeoVectorEqj(ttDaysFromUt(ut)), ut, observer));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Topocentric azimuth/altitude of the Moon — parallax (via the observer's
|
|
190
|
+
* geocentric position, WGS-84 flattening, elevation) and atmospheric
|
|
191
|
+
* refraction (upstream `'normal'` model) both applied.
|
|
192
|
+
*/
|
|
193
|
+
export function moonAltAz(time, observer) {
|
|
194
|
+
assertSupported(time);
|
|
195
|
+
assertObserver(observer);
|
|
196
|
+
const ut = utDays(time);
|
|
197
|
+
return refract(topoAltAzUnrefracted(moonGeoVectorEqj(ttDaysFromUt(ut)), ut, observer));
|
|
198
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Observer {
|
|
2
|
+
latitudeDeg: number;
|
|
3
|
+
longitudeDeg: number;
|
|
4
|
+
elevationM?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class AlmanacOutOfRangeError extends RangeError {
|
|
7
|
+
constructor(msg?: string);
|
|
8
|
+
}
|
|
9
|
+
export declare const SUPPORTED_MIN: number;
|
|
10
|
+
export declare const SUPPORTED_MAX: number;
|
|
11
|
+
/** Point in time: [min, max). */
|
|
12
|
+
export declare function assertSupported(d: Date): void;
|
|
13
|
+
/** Window END: [min, max] — the exact full-range window [min, max) is legal. */
|
|
14
|
+
export declare function assertSupportedWindowEnd(d: Date): void;
|
|
15
|
+
/** Cross-port rule: all instants are integer epoch ms (JS Dates already are). */
|
|
16
|
+
export declare function assertObserver(o: Observer): void;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class AlmanacOutOfRangeError extends RangeError {
|
|
2
|
+
constructor(msg = 'time outside supported interval 1950-01-01T00:00Z ≤ t < 2101-01-01T00:00Z') { super(msg); this.name = 'AlmanacOutOfRangeError'; }
|
|
3
|
+
}
|
|
4
|
+
export const SUPPORTED_MIN = Date.UTC(1950, 0, 1);
|
|
5
|
+
export const SUPPORTED_MAX = Date.UTC(2101, 0, 1);
|
|
6
|
+
/** Point in time: [min, max). */
|
|
7
|
+
export function assertSupported(d) {
|
|
8
|
+
const t = d.getTime();
|
|
9
|
+
if (!Number.isFinite(t))
|
|
10
|
+
throw new RangeError('invalid Date'); // validation, not out-of-range
|
|
11
|
+
if (t < SUPPORTED_MIN || t >= SUPPORTED_MAX)
|
|
12
|
+
throw new AlmanacOutOfRangeError();
|
|
13
|
+
}
|
|
14
|
+
/** Window END: [min, max] — the exact full-range window [min, max) is legal. */
|
|
15
|
+
export function assertSupportedWindowEnd(d) {
|
|
16
|
+
const t = d.getTime();
|
|
17
|
+
if (!Number.isFinite(t))
|
|
18
|
+
throw new RangeError('invalid Date');
|
|
19
|
+
if (t < SUPPORTED_MIN || t > SUPPORTED_MAX)
|
|
20
|
+
throw new AlmanacOutOfRangeError();
|
|
21
|
+
}
|
|
22
|
+
/** Cross-port rule: all instants are integer epoch ms (JS Dates already are). */
|
|
23
|
+
export function assertObserver(o) {
|
|
24
|
+
const { latitudeDeg: lat, longitudeDeg: lon, elevationM: elev = 0 } = o;
|
|
25
|
+
if (!(lat >= -90 && lat <= 90))
|
|
26
|
+
throw new RangeError(`latitudeDeg out of range: ${lat}`);
|
|
27
|
+
if (!(lon >= -180 && lon <= 180))
|
|
28
|
+
throw new RangeError(`longitudeDeg out of range: ${lon}`);
|
|
29
|
+
if (!(elev >= -500 && elev <= 10000))
|
|
30
|
+
throw new RangeError(`elevationM out of range: ${elev}`);
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openwaters/almanac",
|
|
3
|
+
"version": "0.0.0-bootstrap",
|
|
4
|
+
"description": "Offline sun & moon engine: positions, rise/set, moon phase, lunar eclipses.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/openwatersio/almanac.git",
|
|
9
|
+
"directory": "typescript"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"NOTICE"
|
|
23
|
+
],
|
|
24
|
+
"//": "index.ts is a curated allow-list: ONLY the spec's public API table is exported — no guards, no L0/L1 internals",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"prepack": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.5.0",
|
|
32
|
+
"vitest": "^2.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|