@openwaters/almanac 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Almanac
2
2
 
3
- Offline sky engine for the Salish Sea and everywhere else: Sun, 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.
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, fixed-star apparent places, altitude, and azimuth from catalog positions, and Greenwich sidereal time for sight reduction, computed from pure geometry with zero network and zero runtime data files.
4
4
 
5
5
  Three implementations, one behavior:
6
6
 
@@ -111,7 +111,8 @@ npm install @openwaters/almanac
111
111
  ```ts
112
112
  import {
113
113
  nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, nextGlobalSolarEclipse, sunEvents, starAltAz,
114
- seasons, moonApsides, equationOfTime
114
+ starPosition, greenwichSiderealTime,
115
+ seasons, moonApsides, earthApsides, equationOfTime
115
116
  } from '@openwaters/almanac';
116
117
 
117
118
  const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
@@ -136,11 +137,16 @@ for (const { kind, time } of sunEvents(today, tomorrow, observer)) {
136
137
  // Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
137
138
  const { azDeg, altDeg } = starAltAz(88.792939, 7.407064, today, observer);
138
139
 
140
+ // Its apparent place and Greenwich hour angle for sight reduction, in degrees.
141
+ const place = starPosition(88.792939, 7.407064, today);
142
+ const ghaDeg = (greenwichSiderealTime(today) - place.raDeg + 360) % 360;
143
+
139
144
  // The year's equinoxes and solstices, the Moon's perigees and apogees with
140
145
  // distances, and the equation of time in minutes.
141
146
  const year = new Date(today.getUTCFullYear() + 1, 0, 1);
142
147
  for (const { kind, time } of seasons(today, year)) console.log(kind, time.toISOString());
143
148
  for (const { kind, time, distanceKm } of moonApsides(today, year)) console.log(kind, time.toISOString(), distanceKm);
149
+ for (const { kind, time, distanceKm } of earthApsides(today, year)) console.log(kind, time.toISOString(), distanceKm);
144
150
  console.log(equationOfTime(today)); // sundial minus clock, minutes
145
151
  ```
146
152
 
@@ -160,7 +166,7 @@ const crossings = sunEvents(today, tomorrow, viewer, eyeHeightM);
160
166
  Requires a Swift 6 or newer toolchain.
161
167
 
162
168
  ```swift
163
- .package(url: "https://github.com/openwatersio/almanac.git", exact: "0.10.0")
169
+ .package(url: "https://github.com/openwatersio/almanac.git", exact: "0.12.0")
164
170
  ```
165
171
 
166
172
  ```swift
@@ -192,17 +198,22 @@ for event in try sunEvents(from: today, to: tomorrow, observer: observer) {
192
198
  // Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
193
199
  let star = try starAltAz(raDeg: 88.792939, decDeg: 7.407064, at: today, observer: observer)
194
200
 
201
+ // Its apparent place and Greenwich hour angle for sight reduction, in degrees.
202
+ let place = try starPosition(raDeg: 88.792939, decDeg: 7.407064, at: today)
203
+ let ghaDeg = (try greenwichSiderealTime(today) - place.raDeg + 360).truncatingRemainder(dividingBy: 360)
204
+
195
205
  // The year's equinoxes and solstices, the Moon's perigees and apogees with
196
206
  // distances, and the equation of time in minutes.
197
207
  let year = today.addingTimeInterval(366 * 24 * 60 * 60)
198
208
  for event in try seasons(from: today, to: year) { print(event.kind, event.time) }
199
209
  for apsis in try moonApsides(from: today, to: year) { print(apsis.kind, apsis.time, apsis.distanceKm) }
210
+ for apsis in try earthApsides(from: today, to: year) { print(apsis.kind, apsis.time, apsis.distanceKm) }
200
211
  print(try equationOfTime(today)) // sundial minus clock, minutes
201
212
  ```
202
213
 
203
214
  ### Kotlin/JVM and Android
204
215
 
205
- The Kotlin library provides the same 38 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).
216
+ The Kotlin library provides the same 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).
206
217
 
207
218
  ```kotlin
208
219
  import io.openwaters.almanac.*
@@ -0,0 +1,14 @@
1
+ /** Closest approach or farthest remove of the Earth from the Sun's centre. */
2
+ export type EarthApsisKind = 'perihelion' | 'aphelion';
3
+ /** A terrestrial perihelion or aphelion, with the Sun–Earth centre distance there. */
4
+ export interface EarthApsis {
5
+ time: Date;
6
+ kind: EarthApsisKind;
7
+ distanceKm: number;
8
+ }
9
+ /**
10
+ * The Earth's perihelia and aphelia within the half-open window
11
+ * `[startUtc, endUtc)`, sorted ascending, each with the heliocentric distance
12
+ * at that instant.
13
+ */
14
+ export declare function earthApsides(startUtc: Date, endUtc: Date): EarthApsis[];
@@ -0,0 +1,76 @@
1
+ // SearchPlanetApsis (Earth), pinned Astronomy Engine 865d3da7d8112bbc7911238052c6af4aaf877181.
2
+ // Central difference in UT, orbital-period/6 brackets; fixed J2000 grid gives window-independent roots.
3
+ import { assertSupported, assertSupportedWindowEnd } from './types.js';
4
+ import { utDays, ttDaysFromUt, dateFromUt } from './time.js';
5
+ import { KM_PER_AU } from './nutation.js';
6
+ import { earthHelioDistance } from './sun.js';
7
+ import { search, quantizedUt, assertReached } from './events.js';
8
+ /** UPSTREAM: the central-difference half-width `dt` inside `SearchPlanetApsis`. */
9
+ const SLOPE_DT_DAYS = 0.001;
10
+ /** UPSTREAM: `increment` — the scan step, here also the grid cell in days since J2000 UT. */
11
+ const APSIS_CELL_DAYS = 365.256 / 6;
12
+ /** UPSTREAM: `positive_slope` — dr/dt in AU per day, by central difference. */
13
+ function distanceSlope(ut) {
14
+ const r1 = earthHelioDistance(ttDaysFromUt(ut - SLOPE_DT_DAYS / 2));
15
+ const r2 = earthHelioDistance(ttDaysFromUt(ut + SLOPE_DT_DAYS / 2));
16
+ return (r2 - r1) / SLOPE_DT_DAYS;
17
+ }
18
+ function negativeDistanceSlope(ut) {
19
+ return -distanceSlope(ut);
20
+ }
21
+ /**
22
+ * UPSTREAM: the body of `SearchPlanetApsis`'s scan loop for one cell `[t1, t2]`
23
+ * with the slopes `m1`, `m2` at its ends. A slope rising through zero is a
24
+ * perihelion and falling through zero an aphelion. A zero exactly at a grid point
25
+ * belongs to the cell it ends, so no apsis is reported twice.
26
+ */
27
+ function searchApsisInCell(t1, t2, m1, m2) {
28
+ if (m1 < 0 && m2 >= 0) {
29
+ const ut = search(distanceSlope, t1, t2, 1, undefined, 'perihelion search');
30
+ assertReached(ut !== null, 'perihelion search');
31
+ return { ut: ut, kind: 'perihelion' };
32
+ }
33
+ if (m1 > 0 && m2 <= 0) {
34
+ const ut = search(negativeDistanceSlope, t1, t2, 1, undefined, 'aphelion search');
35
+ assertReached(ut !== null, 'aphelion search');
36
+ return { ut: ut, kind: 'aphelion' };
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * The Earth's perihelia and aphelia within the half-open window
42
+ * `[startUtc, endUtc)`, sorted ascending, each with the heliocentric distance
43
+ * at that instant.
44
+ */
45
+ export function earthApsides(startUtc, endUtc) {
46
+ assertSupported(startUtc);
47
+ assertSupportedWindowEnd(endUtc);
48
+ if (startUtc.getTime() >= endUtc.getTime())
49
+ return [];
50
+ const startUt = utDays(startUtc);
51
+ const endUt = utDays(endUtc);
52
+ const events = [];
53
+ let cell = Math.floor(startUt / APSIS_CELL_DAYS);
54
+ let t1 = cell * APSIS_CELL_DAYS;
55
+ let m1 = distanceSlope(t1);
56
+ while (t1 < endUt) {
57
+ const t2 = (cell + 1) * APSIS_CELL_DAYS;
58
+ const m2 = distanceSlope(t2);
59
+ const found = searchApsisInCell(t1, t2, m1, m2);
60
+ if (found !== null) {
61
+ const q = quantizedUt(found.ut); // judge the half-open bounds on the reported instant, not the raw root
62
+ if (q >= endUt)
63
+ break;
64
+ if (q >= startUt) {
65
+ events.push({
66
+ time: dateFromUt(found.ut), kind: found.kind,
67
+ distanceKm: earthHelioDistance(ttDaysFromUt(found.ut)) * KM_PER_AU
68
+ });
69
+ }
70
+ }
71
+ cell++;
72
+ t1 = t2;
73
+ m1 = m2;
74
+ }
75
+ return events;
76
+ }
package/dist/index.d.ts CHANGED
@@ -4,14 +4,14 @@ export type { HeliocentricPosition, PlanetPosition, PlanetIllumination } from '.
4
4
  export { planetHeliocentricPosition, planetPosition, planetAltAz, planetIllumination } from './planets.js';
5
5
  export { horizonDip } from './horizon.js';
6
6
  export { AlmanacOutOfRangeError } from './types.js';
7
- export type { SunPosition, MoonPosition } from './positions.js';
8
- export { sunPosition, moonPosition, equationOfTime } from './positions.js';
7
+ export type { SunPosition, MoonPosition, StarPosition } from './positions.js';
8
+ export { sunPosition, moonPosition, starPosition, equationOfTime } from './positions.js';
9
9
  export type { SeasonKind, SeasonEvent } from './seasons.js';
10
10
  export { seasons } from './seasons.js';
11
11
  export type { MoonApsisKind, MoonApsis } from './apsides.js';
12
12
  export { moonApsides } from './apsides.js';
13
13
  export type { AltAz } from './transforms.js';
14
- export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
14
+ export { sunAltAz, moonAltAz, starAltAz, greenwichSiderealTime } from './transforms.js';
15
15
  export type { MoonIllumination, MoonOrientation } from './illumination.js';
16
16
  export { moonIllumination, moonOrientation } from './illumination.js';
17
17
  export type { SunEvent, SunEventKind, MoonEvent, MoonEventKind, MoonPhaseEvent, MoonPhaseName, PlanetEvent, PlanetEventKind, AltitudeEvent, AltitudeEventKind, TransitEvent, TransitEventKind } from './events.js';
@@ -22,3 +22,5 @@ export type { SolarEclipse, SolarEclipseKind, SolarEclipseSunAltitudes } from '.
22
22
  export { nextSolarEclipse, previousSolarEclipse, solarEclipses, solarObscuration } from './solar.js';
23
23
  export type { GlobalSolarEclipse, SolarEclipseAxisPoint } from './globalSolar.js';
24
24
  export { nextGlobalSolarEclipse, previousGlobalSolarEclipse, globalSolarEclipses, solarEclipseAxisPoint, solarEclipseCentralLine } from './globalSolar.js';
25
+ export type { EarthApsisKind, EarthApsis } from './earthApsides.js';
26
+ export { earthApsides } from './earthApsides.js';
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@ export { degreesToRadians, radiansToDegrees } from './angles.js';
3
3
  export { planetHeliocentricPosition, planetPosition, planetAltAz, planetIllumination } from './planets.js';
4
4
  export { horizonDip } from './horizon.js';
5
5
  export { AlmanacOutOfRangeError } from './types.js';
6
- export { sunPosition, moonPosition, equationOfTime } from './positions.js';
6
+ export { sunPosition, moonPosition, starPosition, equationOfTime } from './positions.js';
7
7
  export { seasons } from './seasons.js';
8
8
  export { moonApsides } from './apsides.js';
9
- export { sunAltAz, moonAltAz, starAltAz } from './transforms.js';
9
+ export { sunAltAz, moonAltAz, starAltAz, greenwichSiderealTime } from './transforms.js';
10
10
  export { moonIllumination, moonOrientation } from './illumination.js';
11
11
  export { sunIsUp, sunEvents, moonEvents, searchMoonPhases, planetEvents, sunAltitudeEvents, sunTransits, moonTransits } from './events.js';
12
12
  export { nextLunarEclipse, previousLunarEclipse, lunarEclipses, lunarEclipseVisibility } from './eclipse.js';
13
13
  export { nextSolarEclipse, previousSolarEclipse, solarEclipses, solarObscuration } from './solar.js';
14
14
  export { nextGlobalSolarEclipse, previousGlobalSolarEclipse, globalSolarEclipses, solarEclipseAxisPoint, solarEclipseCentralLine } from './globalSolar.js';
15
+ export { earthApsides } from './earthApsides.js';
@@ -34,6 +34,12 @@ export declare enum PrecessDirection {
34
34
  }
35
35
  /** UPSTREAM: `Frac`, astronomy.ts line 235. */
36
36
  export declare function frac(x: number): number;
37
+ /**
38
+ * Not upstream: an angle wrapped into [0, 360). Upstream adds 360 to a
39
+ * negative remainder, and a remainder within an ulp below zero then rounds
40
+ * to 360 itself.
41
+ */
42
+ export declare function wrapDeg360(deg: number): number;
37
43
  /** UPSTREAM: `mean_obliq`, astronomy.ts lines 1437-1447. Degrees. */
38
44
  export declare function meanObliquityDeg(tt: number): number;
39
45
  /** UPSTREAM: `EarthTiltInfo`, astronomy.ts lines 1449-1457. dpsi/deps arcsec, mobl/tobl degrees. */
package/dist/nutation.js CHANGED
@@ -37,6 +37,19 @@ export var PrecessDirection;
37
37
  export function frac(x) {
38
38
  return x - Math.floor(x);
39
39
  }
40
+ /**
41
+ * Not upstream: an angle wrapped into [0, 360). Upstream adds 360 to a
42
+ * negative remainder, and a remainder within an ulp below zero then rounds
43
+ * to 360 itself.
44
+ */
45
+ export function wrapDeg360(deg) {
46
+ let r = deg % 360;
47
+ if (r < 0)
48
+ r += 360;
49
+ if (r >= 360)
50
+ r -= 360;
51
+ return r;
52
+ }
40
53
  /**
41
54
  * UPSTREAM: `iau2000b`, astronomy.ts lines 1392-1435. The IAU 2000B nutation
42
55
  * series truncated to its five largest terms; returns arcseconds.
@@ -247,9 +260,7 @@ export function equatorialFromVector(pos) {
247
260
  throw new Error('Indeterminate sky coordinates');
248
261
  return { raDeg: 0, decDeg: (pos.z < 0) ? -90 : +90, distanceAu: dist };
249
262
  }
250
- let raDeg = RAD2DEG * Math.atan2(pos.y, pos.x);
251
- if (raDeg < 0)
252
- raDeg += 360;
263
+ const raDeg = wrapDeg360(RAD2DEG * Math.atan2(pos.y, pos.x));
253
264
  const decDeg = RAD2DEG * Math.atan2(pos.z, Math.sqrt(xyproj));
254
265
  return { raDeg, decDeg, distanceAu: dist };
255
266
  }
@@ -10,6 +10,11 @@ export interface MoonPosition {
10
10
  decDeg: number;
11
11
  distanceKm: number;
12
12
  }
13
+ /** Apparent geocentric position of a fixed star: equator of date, RA in [0, 360). */
14
+ export interface StarPosition {
15
+ raDeg: number;
16
+ decDeg: number;
17
+ }
13
18
  /**
14
19
  * INTERNAL: apparent Sun for a TT instant, expressed as days since the J2000
15
20
  * epoch in Terrestrial Time. This is the entry point the TT-labeled position
@@ -32,6 +37,17 @@ export declare function sunPosition(time: Date): SunPosition;
32
37
  * the Earth-Moon centre distance in kilometres.
33
38
  */
34
39
  export declare function moonPosition(time: Date): MoonPosition;
40
+ /**
41
+ * Apparent geocentric position of a fixed star from its J2000 (ICRS) catalog
42
+ * right ascension and declination, both in degrees — right ascension and
43
+ * declination on the true equator and equinox of date, with annual
44
+ * aberration, precession and nutation applied. Proper motion and stellar
45
+ * parallax are not applied.
46
+ *
47
+ * UPSTREAM: `Equator(star, date, observer, ofdate=true, aberration=true)`
48
+ * (astronomy.ts ~2803) with the geocentric observer term dropped.
49
+ */
50
+ export declare function starPosition(raDeg: number, decDeg: number, time: Date): StarPosition;
35
51
  /**
36
52
  * INTERNAL: equation of time for a TT instant, in minutes. Not from upstream:
37
53
  * Meeus, *Astronomical Algorithms*, formula 28.1, `E = L0 − 0.0057183° − α +
package/dist/positions.js CHANGED
@@ -1,5 +1,5 @@
1
- // L1 public positions: apparent geocentric Sun and Moon on the true equator and
2
- // equinox of date.
1
+ // L1 public positions: apparent geocentric Sun, Moon and fixed stars on the true
2
+ // equator and equinox of date.
3
3
  //
4
4
  // The composition chain mirrors the cosinekitty/astronomy upstream (pinned sha
5
5
  // 865d3da7d8112bbc7911238052c6af4aaf877181):
@@ -13,9 +13,10 @@
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 { DEG2RAD, KM_PER_AU, earthTilt, equatorialFromVector } from './nutation.js';
16
+ import { DEG2RAD, KM_PER_AU, PrecessDirection, earthTilt, equatorialFromVector, gyration } from './nutation.js';
17
17
  import { moonGeoVector } from './moon.js';
18
18
  import { sunGeoVector } from './sun.js';
19
+ import { assertStarCatalog, starGeoVectorEqj } from './transforms.js';
19
20
  /**
20
21
  * INTERNAL: apparent Sun for a TT instant, expressed as days since the J2000
21
22
  * epoch in Terrestrial Time. This is the entry point the TT-labeled position
@@ -50,6 +51,23 @@ export function moonPosition(time) {
50
51
  assertSupported(time);
51
52
  return moonApparentAtTT(ttDays(time));
52
53
  }
54
+ /**
55
+ * Apparent geocentric position of a fixed star from its J2000 (ICRS) catalog
56
+ * right ascension and declination, both in degrees — right ascension and
57
+ * declination on the true equator and equinox of date, with annual
58
+ * aberration, precession and nutation applied. Proper motion and stellar
59
+ * parallax are not applied.
60
+ *
61
+ * UPSTREAM: `Equator(star, date, observer, ofdate=true, aberration=true)`
62
+ * (astronomy.ts ~2803) with the geocentric observer term dropped.
63
+ */
64
+ export function starPosition(raDeg, decDeg, time) {
65
+ assertSupported(time);
66
+ assertStarCatalog(raDeg, decDeg);
67
+ const tt = ttDays(time);
68
+ const { raDeg: ra, decDeg: dec } = equatorialFromVector(gyration(starGeoVectorEqj(raDeg, decDeg, tt), tt, PrecessDirection.From2000));
69
+ return { raDeg: ra, decDeg: dec };
70
+ }
53
71
  /**
54
72
  * INTERNAL: equation of time for a TT instant, in minutes. Not from upstream:
55
73
  * Meeus, *Astronomical Algorithms*, formula 28.1, `E = L0 − 0.0057183° − α +
package/dist/sun.d.ts CHANGED
@@ -11,11 +11,24 @@ export declare const vsopEarth: VsopModel;
11
11
  export declare function vsopRotate(eclip: Vec3): Vec3;
12
12
  /** UPSTREAM: `VsopSphereToRect`, astronomy.ts lines 3243-3253. */
13
13
  export declare function vsopSphereToRect(lon: number, lat: number, radius: number): Vec3;
14
+ export declare function earthHelioDistance(tt: number): number;
14
15
  /**
15
16
  * UPSTREAM: `CalcVsop`, astronomy.ts lines 3255-3262, specialised to
16
17
  * `vsop.Earth` — i.e. `HelioVector(Body.Earth, time)`. J2000 mean equator (EQJ).
17
18
  */
18
19
  export declare function earthHelioVector(tt: number): Vec3;
20
+ /** Heliocentric position in AU and velocity in AU per day, J2000 mean equator (EQJ). */
21
+ export interface HelioState {
22
+ pos: Vec3;
23
+ vel: Vec3;
24
+ }
25
+ /**
26
+ * UPSTREAM: `CalcVsopPosVel`, astronomy.ts lines 3264-3314, specialised to
27
+ * `vsop.Earth` — i.e. `HelioState(Body.Earth, time)`. Not upstream: a
28
+ * one-entry cache on the exact instant, because a star catalog drawn for one
29
+ * moment asks for the same Earth state once per star.
30
+ */
31
+ export declare function earthHelioState(tt: number): HelioState;
19
32
  /**
20
33
  * UPSTREAM: `CorrectLightTravel` (astronomy.ts 4170-4191) driving
21
34
  * `BodyPosition.Position` (4193-4241) with observerBody = Earth,
package/dist/sun.js CHANGED
@@ -223,6 +223,8 @@ export function vsopSphereToRect(lon, lat, radius) {
223
223
  z: radius * Math.sin(lat)
224
224
  };
225
225
  }
226
+ // UPSTREAM: HelioDistance(Body.Earth), the scalar VSOP radius without rotations.
227
+ export function earthHelioDistance(tt) { return earthRad(tt / 365250); }
226
228
  /**
227
229
  * UPSTREAM: `CalcVsop`, astronomy.ts lines 3255-3262, specialised to
228
230
  * `vsop.Earth` — i.e. `HelioVector(Body.Earth, time)`. J2000 mean equator (EQJ).
@@ -235,6 +237,75 @@ export function earthHelioVector(tt) {
235
237
  const eclip = vsopSphereToRect(lon, lat, rad);
236
238
  return vsopRotate(eclip);
237
239
  }
240
+ /**
241
+ * UPSTREAM: `VsopDeriv`, astronomy.ts lines 3207-3227 — the time derivative
242
+ * of a VSOP series, per millennium. It loops over the table rather than
243
+ * unrolling like `earthLon`: only the star path needs it, once per call.
244
+ */
245
+ function vsopDeriv(formula, t) {
246
+ let tpower = 1; // t^s
247
+ let dpower = 0; // t^(s-1)
248
+ let deriv = 0;
249
+ let s = 0;
250
+ for (const series of formula) {
251
+ let sin_sum = 0;
252
+ let cos_sum = 0;
253
+ for (const [ampl, phas, freq] of series) {
254
+ const angle = phas + (t * freq);
255
+ sin_sum += ampl * freq * Math.sin(angle);
256
+ if (s > 0)
257
+ cos_sum += ampl * Math.cos(angle);
258
+ }
259
+ deriv += (s * dpower * cos_sum) - (tpower * sin_sum);
260
+ dpower = tpower;
261
+ tpower *= t;
262
+ ++s;
263
+ }
264
+ return deriv;
265
+ }
266
+ let cacheEarthState;
267
+ let cacheEarthStateTt = NaN;
268
+ /**
269
+ * UPSTREAM: `CalcVsopPosVel`, astronomy.ts lines 3264-3314, specialised to
270
+ * `vsop.Earth` — i.e. `HelioState(Body.Earth, time)`. Not upstream: a
271
+ * one-entry cache on the exact instant, because a star catalog drawn for one
272
+ * moment asks for the same Earth state once per star.
273
+ */
274
+ export function earthHelioState(tt) {
275
+ if (!cacheEarthState || cacheEarthStateTt !== tt) {
276
+ cacheEarthState = computeEarthHelioState(tt);
277
+ cacheEarthStateTt = tt;
278
+ }
279
+ return cacheEarthState;
280
+ }
281
+ function computeEarthHelioState(tt) {
282
+ const t = tt / DAYS_PER_MILLENNIUM;
283
+ const lon = earthLon(t);
284
+ const lat = earthLat(t);
285
+ const rad = earthRad(t);
286
+ const dlon_dt = vsopDeriv(vsopEarth[0], t);
287
+ const dlat_dt = vsopDeriv(vsopEarth[1], t);
288
+ const drad_dt = vsopDeriv(vsopEarth[2], t);
289
+ const coslon = Math.cos(lon);
290
+ const sinlon = Math.sin(lon);
291
+ const coslat = Math.cos(lat);
292
+ const sinlat = Math.sin(lat);
293
+ const vx = (+(drad_dt * coslat * coslon)
294
+ - (rad * sinlat * coslon * dlat_dt)
295
+ - (rad * coslat * sinlon * dlon_dt));
296
+ const vy = (+(drad_dt * coslat * sinlon)
297
+ - (rad * sinlat * sinlon * dlat_dt)
298
+ + (rad * coslat * coslon * dlon_dt));
299
+ const vz = (+(drad_dt * sinlat)
300
+ + (rad * coslat * dlat_dt));
301
+ // AU per millennium to AU per day.
302
+ const eclipVel = {
303
+ x: vx / DAYS_PER_MILLENNIUM,
304
+ y: vy / DAYS_PER_MILLENNIUM,
305
+ z: vz / DAYS_PER_MILLENNIUM
306
+ };
307
+ return { pos: vsopRotate(vsopSphereToRect(lon, lat, rad)), vel: vsopRotate(eclipVel) };
308
+ }
238
309
  /**
239
310
  * UPSTREAM: `CorrectLightTravel` (astronomy.ts 4170-4191) driving
240
311
  * `BodyPosition.Position` (4193-4241) with observerBody = Earth,
@@ -72,15 +72,35 @@ export declare function sunAltAz(time: Date, observer: Observer): AltAz;
72
72
  * refraction (upstream `'normal'` model) both applied.
73
73
  */
74
74
  export declare function moonAltAz(time: Date, observer: Observer): AltAz;
75
+ /** INTERNAL: shared argument checks for the J2000 catalog position the star functions take. */
76
+ export declare function assertStarCatalog(raDeg: number, decDeg: number): void;
77
+ /**
78
+ * UPSTREAM: `GeoVector` for a `DefineStar` body with aberration = true —
79
+ * `HelioVector`'s `VectorFromSphere(Spherical(dec, 15*ra, dist))`
80
+ * (astronomy.ts ~4097, 7015) through the user-defined-star branch of
81
+ * `BackdatePosition` (4270-4300). The star is not backdated; the vector from
82
+ * Earth to the star is shifted by Earth's heliocentric velocity over the
83
+ * light travel time, which is the annual aberration, at most 20.5″.
84
+ *
85
+ * @returns geocentric position in AU, J2000 mean equator (EQJ).
86
+ */
87
+ export declare function starGeoVectorEqj(raDeg: number, decDeg: number, tt: number): Vec3;
75
88
  /**
76
89
  * Topocentric azimuth/altitude of a fixed star from its J2000 (ICRS)
77
90
  * catalog right ascension and declination, both in degrees.
78
91
  *
79
- * Applied: precession and nutation to the equator of date, Greenwich
80
- * apparent sidereal time, and atmospheric refraction (upstream `'normal'`
81
- * model, the same as sunAltAz/moonAltAz). Not applied: annual aberration
82
- * (at most 20.5″ — see the roadmap), proper motion (pass catalog
83
- * coordinates; fast movers such as Arcturus drift ~1′ in 26 years), and
84
- * stellar parallax (negligible at the 1000 ly the star is placed at).
92
+ * Applied: annual aberration, precession and nutation to the equator of
93
+ * date, Greenwich apparent sidereal time, and atmospheric refraction
94
+ * (upstream `'normal'` model, the same as sunAltAz/moonAltAz). Not applied:
95
+ * proper motion (pass catalog coordinates; fast movers such as Arcturus
96
+ * drift ~1′ in 26 years) and stellar parallax (negligible at the 1000 ly the
97
+ * star is placed at).
85
98
  */
86
99
  export declare function starAltAz(raDeg: number, decDeg: number, time: Date, observer: Observer): AltAz;
100
+ /**
101
+ * Greenwich apparent sidereal time in degrees, [0, 360): the hour angle of
102
+ * the true equinox of date at Greenwich, including the equation of the
103
+ * equinoxes. A body's Greenwich hour angle is this minus its right ascension
104
+ * of date.
105
+ */
106
+ export declare function greenwichSiderealTime(time: Date): number;
@@ -13,20 +13,17 @@
13
13
  // refinement since this layer only needs az/alt; Refraction 'normal' (~7176).
14
14
  //
15
15
  // INTERNAL: not part of the curated public API (see index.ts) except
16
- // sunAltAz/moonAltAz.
16
+ // sunAltAz, moonAltAz, starAltAz and greenwichSiderealTime.
17
17
  import { assertObserver, assertSupported } from './types.js';
18
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';
19
+ import { C_AUDAY, DEG2RAD, RAD2DEG, EARTH_EQUATORIAL_RADIUS_KM, EARTH_FLATTENING, EARTH_FLATTENING_SQUARED, KM_PER_AU, PrecessDirection, earthTilt, equatorialFromVector, gyration, wrapDeg360 } from './nutation.js';
20
+ import { earthHelioState, sunGeoVectorEqj } from './sun.js';
21
21
  import { moonGeoVectorEqj } from './moon.js';
22
22
  /** UPSTREAM: `era` (Earth Rotation Angle), astronomy.ts ~2014. */
23
23
  function earthRotationAngleDeg(ut) {
24
24
  const thet1 = 0.7790572732640 + 0.00273781191135448 * ut;
25
25
  const thet3 = ut % 1;
26
- let theta = 360 * ((thet1 + thet3) % 1);
27
- if (theta < 0)
28
- theta += 360;
29
- return theta;
26
+ return wrapDeg360(360 * ((thet1 + thet3) % 1));
30
27
  }
31
28
  /**
32
29
  * UPSTREAM: `sidereal_time`, astronomy.ts ~2031 — Greenwich Apparent Sidereal
@@ -46,10 +43,7 @@ export function siderealDeg(ut) {
46
43
  - 0.00000044) * t
47
44
  + 1.3915817) * t
48
45
  + 4612.156534) * t;
49
- let gastDeg = (st / 3600 + theta) % 360;
50
- if (gastDeg < 0)
51
- gastDeg += 360;
52
- return gastDeg;
46
+ return wrapDeg360(st / 3600 + theta);
53
47
  }
54
48
  /**
55
49
  * UPSTREAM: `terra` (astronomy.ts ~2147 — position only; its velocity output
@@ -149,17 +143,13 @@ export function topoAltAzUnrefracted(bodyEqj, ut, observer) {
149
143
  const proj = Math.hypot(pn, pw);
150
144
  let az;
151
145
  if (proj > 0) {
152
- az = -RAD2DEG * Math.atan2(pw, pn);
153
- if (az < 0)
154
- az += 360;
146
+ az = wrapDeg360(-RAD2DEG * Math.atan2(pw, pn));
155
147
  }
156
148
  else {
157
149
  az = 0;
158
150
  }
159
151
  const zd = RAD2DEG * Math.atan2(proj, pz);
160
- let hourAngleDeg = (gast + observer.longitudeDeg - raDeg) % 360;
161
- if (hourAngleDeg < 0)
162
- hourAngleDeg += 360;
152
+ const hourAngleDeg = wrapDeg360(gast + observer.longitudeDeg - raDeg);
163
153
  return { azDeg: az, altDeg: 90 - zd, hourAngleDeg, distanceAu, raDeg, decDeg };
164
154
  }
165
155
  /**
@@ -208,40 +198,64 @@ export function moonAltAz(time, observer) {
208
198
  return refract(topoAltAzUnrefracted(moonGeoVectorEqj(ttDaysFromUt(ut)), ut, observer));
209
199
  }
210
200
  /**
211
- * UPSTREAM: the user-defined star path — `HelioVector` for a `DefineStar`
212
- * body (astronomy.ts ~4097: `VectorFromSphere(Spherical(dec, 15*ra, dist))`)
213
- * into `Equator(body, date, observer, ofdate=true, aberration=false)` (~2803)
214
- * and `Horizon` (~2565): the same topocentric path sunAltAz/moonAltAz take.
215
201
  * Upstream floors a star's distance at 1 light-year and suggests 1000 when
216
202
  * unknown; the star sits at 1000 ly here (`AU_PER_LY`, astronomy.ts line 52).
217
- * Upstream's geocentric step (star vector minus Earth's heliocentric
218
- * position) is skipped: at 1000 ly it moves the star 0.003″.
219
203
  */
220
204
  const STAR_DISTANCE_AU = 1000 * 63241.07708807546;
221
- /**
222
- * Topocentric azimuth/altitude of a fixed star from its J2000 (ICRS)
223
- * catalog right ascension and declination, both in degrees.
224
- *
225
- * Applied: precession and nutation to the equator of date, Greenwich
226
- * apparent sidereal time, and atmospheric refraction (upstream `'normal'`
227
- * model, the same as sunAltAz/moonAltAz). Not applied: annual aberration
228
- * (at most 20.5″ — see the roadmap), proper motion (pass catalog
229
- * coordinates; fast movers such as Arcturus drift ~1′ in 26 years), and
230
- * stellar parallax (negligible at the 1000 ly the star is placed at).
231
- */
232
- export function starAltAz(raDeg, decDeg, time, observer) {
233
- assertSupported(time);
234
- assertObserver(observer);
205
+ /** INTERNAL: shared argument checks for the J2000 catalog position the star functions take. */
206
+ export function assertStarCatalog(raDeg, decDeg) {
235
207
  if (!(raDeg >= 0 && raDeg < 360))
236
208
  throw new RangeError(`raDeg out of range: ${raDeg}`);
237
209
  if (!(decDeg >= -90 && decDeg <= 90))
238
210
  throw new RangeError(`decDeg out of range: ${decDeg}`);
239
- const ut = utDays(time);
211
+ }
212
+ /**
213
+ * UPSTREAM: `GeoVector` for a `DefineStar` body with aberration = true —
214
+ * `HelioVector`'s `VectorFromSphere(Spherical(dec, 15*ra, dist))`
215
+ * (astronomy.ts ~4097, 7015) through the user-defined-star branch of
216
+ * `BackdatePosition` (4270-4300). The star is not backdated; the vector from
217
+ * Earth to the star is shifted by Earth's heliocentric velocity over the
218
+ * light travel time, which is the annual aberration, at most 20.5″.
219
+ *
220
+ * @returns geocentric position in AU, J2000 mean equator (EQJ).
221
+ */
222
+ export function starGeoVectorEqj(raDeg, decDeg, tt) {
240
223
  const rCosDec = STAR_DISTANCE_AU * Math.cos(decDeg * DEG2RAD);
241
- const eqj = {
224
+ const tvec = {
242
225
  x: rCosDec * Math.cos(raDeg * DEG2RAD),
243
226
  y: rCosDec * Math.sin(raDeg * DEG2RAD),
244
227
  z: STAR_DISTANCE_AU * Math.sin(decDeg * DEG2RAD),
245
228
  };
246
- return refract(topoAltAzUnrefracted(eqj, ut, observer));
229
+ const ostate = earthHelioState(tt);
230
+ const rvec = { x: tvec.x - ostate.pos.x, y: tvec.y - ostate.pos.y, z: tvec.z - ostate.pos.z };
231
+ const s = C_AUDAY / Math.sqrt(rvec.x * rvec.x + rvec.y * rvec.y + rvec.z * rvec.z);
232
+ return { x: rvec.x + ostate.vel.x / s, y: rvec.y + ostate.vel.y / s, z: rvec.z + ostate.vel.z / s };
233
+ }
234
+ /**
235
+ * Topocentric azimuth/altitude of a fixed star from its J2000 (ICRS)
236
+ * catalog right ascension and declination, both in degrees.
237
+ *
238
+ * Applied: annual aberration, precession and nutation to the equator of
239
+ * date, Greenwich apparent sidereal time, and atmospheric refraction
240
+ * (upstream `'normal'` model, the same as sunAltAz/moonAltAz). Not applied:
241
+ * proper motion (pass catalog coordinates; fast movers such as Arcturus
242
+ * drift ~1′ in 26 years) and stellar parallax (negligible at the 1000 ly the
243
+ * star is placed at).
244
+ */
245
+ export function starAltAz(raDeg, decDeg, time, observer) {
246
+ assertSupported(time);
247
+ assertObserver(observer);
248
+ assertStarCatalog(raDeg, decDeg);
249
+ const ut = utDays(time);
250
+ return refract(topoAltAzUnrefracted(starGeoVectorEqj(raDeg, decDeg, ttDaysFromUt(ut)), ut, observer));
251
+ }
252
+ /**
253
+ * Greenwich apparent sidereal time in degrees, [0, 360): the hour angle of
254
+ * the true equinox of date at Greenwich, including the equation of the
255
+ * equinoxes. A body's Greenwich hour angle is this minus its right ascension
256
+ * of date.
257
+ */
258
+ export function greenwichSiderealTime(time) {
259
+ assertSupported(time);
260
+ return siderealDeg(utDays(time));
247
261
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwaters/almanac",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Offline astronomy: Sun, Moon and planet positions, apparent horizon, rise/set, phases and eclipses.",
5
5
  "license": "MIT",
6
6
  "repository": {