moon-sighting/src/types.ts
Aric Camarata df4dbfe53e ci: fix eslint parser devDeps, add files patterns, format src
- Add @typescript-eslint/parser and @typescript-eslint/eslint-plugin as
  explicit devDependencies so pnpm hoists them for eslint.config.mjs
- Add files: ['**/*.ts'] to eslint config entries so ESLint 10 processes
  TS sources instead of ignoring them
- Add parserOptions.project for typed-lint rules
- Run prettier --write to fix pre-existing format issues in 12 src files
2026-05-31 08:48:31 -04:00

488 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─── Primitive geometry ──────────────────────────────────────────────────────
/** 3-element position or velocity vector in km or km/s */
export type Vec3 = [number, number, number];
/** Position + velocity state vector from the ephemeris */
export interface StateVector {
position: Vec3; // km, in the frame determined by context
velocity: Vec3; // km/s
}
/** Azimuth + altitude in degrees */
export interface AzAlt {
/** Degrees from North, measured clockwise (0 = N, 90 = E, 180 = S, 270 = W) */
azimuth: number;
/** Degrees above the horizon (negative = below) */
altitude: number;
}
// ─── Kernel-free moon results ─────────────────────────────────────────────────
/**
* Topocentric moon position from getMoonPosition().
* Computed via Meeus Ch. 47 (no kernel required).
* Accuracy: azimuth/altitude ~0.3°, distance ~300 km.
*/
export interface MoonPosition {
/** Azimuth in degrees from North, measured clockwise (0 = N, 90 = E, 180 = S, 270 = W) */
azimuth: number;
/** Apparent altitude in degrees above the horizon (atmospheric refraction applied) */
altitude: number;
/** Distance from Earth center to Moon center, km */
distance: number;
/**
* Parallactic angle in radians.
* The angle between the great circle through the Moon and zenith, and the great circle
* through the Moon and the north celestial pole. Positive east of the meridian.
*/
parallacticAngle: number;
}
/**
* Moon illumination from getMoonIllumination().
* Computed via Meeus Ch. 47/48 (no kernel required).
* Accuracy: fraction ~0.5%, phase fraction ~0.003.
*/
export interface MoonIlluminationResult {
/** Illuminated fraction of the Moon disk, 0 (new moon) to 1 (full moon) */
fraction: number;
/**
* Phase cycle fraction in [0, 1):
* 0 = new moon, 0.25 = first quarter, 0.5 = full moon, 0.75 = last quarter
*/
phase: number;
/**
* Position angle of the midpoint of the bright limb, measured eastward from
* the north celestial pole, in radians. Matches the suncalc convention.
*/
angle: number;
/** True while elongation is increasing (new moon toward full moon) */
isWaxing: boolean;
}
// ─── Time ────────────────────────────────────────────────────────────────────
/** All relevant time scale values for a single moment */
export interface TimeScales {
utc: Date;
/** Julian Date in UTC */
jdUTC: number;
/** Julian Date in Terrestrial Time (TT = TAI + 32.184s) */
jdTT: number;
/** Julian Date in Barycentric Dynamical Time (used by JPL ephemerides) */
jdTDB: number;
/** Julian Date in UT1 (Earth rotation time) */
jdUT1: number;
/** TT - UT1 in seconds (delta-T) */
deltaT: number;
/** TAI - UTC in seconds (leap seconds count) */
deltaAT: number;
}
// ─── Observer ────────────────────────────────────────────────────────────────
/** Observer location and environmental parameters */
export interface Observer {
/** Geodetic latitude in degrees (north positive) */
lat: number;
/** Longitude in degrees (east positive) */
lon: number;
/** Height above WGS84 ellipsoid in meters */
elevation: number;
/** Optional label for the location */
name?: string;
/**
* Override TT - UT1 in seconds.
* When provided, used directly. Otherwise the built-in polynomial is used.
* For maximum accuracy, supply the current IERS value (typically within ±0.9s).
*/
deltaT?: number;
/**
* Override UT1 - UTC in seconds (from IERS Bulletin A).
* Takes precedence over deltaT when both are provided.
*/
ut1utc?: number;
/** Atmospheric pressure in millibars (default 1013.25) */
pressure?: number;
/** Ambient temperature in Celsius (default 15) */
temperature?: number;
}
// ─── Crescent geometry ───────────────────────────────────────────────────────
/**
* The five geometric quantities used by all major crescent visibility criteria.
* All values computed at best time (T_b) unless noted.
*/
export interface CrescentGeometry {
/** Arc of light: topocentric Sun-Moon angular separation (elongation), degrees */
ARCL: number;
/**
* Arc of vision: Moon airless altitude minus Sun airless altitude, degrees.
* Used as the primary visibility discriminant in both Yallop and Odeh.
*/
ARCV: number;
/**
* Relative azimuth: Sun azimuth minus Moon azimuth, normalized to [-180, 180], degrees.
* Positive = Moon north of Sun.
*/
DAZ: number;
/**
* Topocentric crescent width in arc minutes.
* Used directly in Odeh's polynomial V expression.
*/
W: number;
/** Moonset minus sunset in minutes. Negative = Moon sets before Sun (no sighting possible). */
lag: number;
}
// ─── Yallop q-test ───────────────────────────────────────────────────────────
/** Yallop q-test visibility category (NAO Technical Note 69) */
/** Yallop visibility category (A = easily visible, F = below Danjon limit). */
export type YallopCategory = "A" | "B" | "C" | "D" | "E" | "F";
/**
* Published q thresholds (Yallop 1997, NAO TN 69):
* A: q > +0.216 — Easily visible to the naked eye
* B: q > -0.014 — Visible under perfect conditions
* C: q > -0.160 — May need optical aid to find; visible to naked eye
* D: q > -0.232 — Optical aid needed; will not be visible to naked eye
* E: q > -0.293 — Not visible even with telescope
* F: q <= -0.293 — Below Danjon limit (Moon too close to Sun)
*/
export const YALLOP_THRESHOLDS = {
A: 0.216,
B: -0.014,
C: -0.16,
D: -0.232,
E: -0.293,
} as const;
/**
* Human-readable descriptions for each Yallop visibility category (AF).
* Sourced from Yallop (NAO TN 69, 1997).
*/
export const YALLOP_DESCRIPTIONS: Record<YallopCategory, string> = {
A: "Easily visible to the naked eye",
B: "Visible under perfect conditions",
C: "May need optical aid to find; naked eye possible",
D: "Optical aid needed; naked eye not possible",
E: "Not visible even with telescope under good conditions",
F: "Below Danjon limit — crescent cannot form",
};
export interface YallopResult {
/** The continuous q parameter (higher = more visible) */
q: number;
/** Visibility category A through F */
category: YallopCategory;
/** Human-readable interpretation */
description: string;
/** True for categories A and B */
isVisibleNakedEye: boolean;
/** True for categories C and D */
requiresOpticalAid: boolean;
/** True for category F */
isBelowDanjonLimit: boolean;
/** Topocentric crescent width W' used in the q formula, arc minutes */
Wprime: number;
}
// ─── Odeh criterion ──────────────────────────────────────────────────────────
/** Odeh visibility zone (Experimental Astronomy 2006) */
/** Odeh visibility zone (A = naked eye visible, D = not visible with any aid). */
export type OdehZone = "A" | "B" | "C" | "D";
/**
* Published V thresholds (Odeh 2006):
* A: V >= 5.65 — Visible with naked eye
* B: V >= 2.00 — Visible with optical aid; may be seen with naked eye
* C: V >= -0.96 — Visible with optical aid only
* D: V < -0.96 — Not visible even with optical aid
*/
export const ODEH_THRESHOLDS = {
A: 5.65,
B: 2.0,
C: -0.96,
} as const;
/**
* Human-readable descriptions for each Odeh visibility zone (AD).
* Sourced from Odeh (Experimental Astronomy, 2006).
*/
export const ODEH_DESCRIPTIONS: Record<OdehZone, string> = {
A: "Visible with naked eye",
B: "Visible with optical aid; may be seen with naked eye under excellent conditions",
C: "Visible with optical aid only",
D: "Not visible even with optical aid",
};
export interface OdehResult {
/**
* Continuous visibility parameter V = ARCV - (arcv_minimum(W)).
* Positive = crescent exceeds minimum visibility threshold.
*/
V: number;
/** Visibility zone A through D */
zone: OdehZone;
/** Human-readable interpretation */
description: string;
/** True for zone A */
isVisibleNakedEye: boolean;
/** True for zones A and B */
isVisibleWithOpticalAid: boolean;
}
// ─── Kernel-free visibility estimate ─────────────────────────────────────────
/**
* Kernel-free Odeh-based crescent visibility estimate from getMoonVisibilityEstimate().
* Computed via Meeus Ch. 47 approximation at the given observation time.
* For DE442S-quality results, use getMoonSightingReport().
*/
export interface MoonVisibilityEstimate {
/**
* Odeh V parameter: V = ARCV f(W).
* Positive = crescent exceeds minimum visibility threshold.
*/
V: number;
/** Visibility zone A through D */
zone: OdehZone;
/** Human-readable zone description */
description: string;
/** True for zone A */
isVisibleNakedEye: boolean;
/** True for zones A and B */
isVisibleWithOpticalAid: boolean;
/** Arc of light (Sun-Moon elongation) in degrees */
ARCL: number;
/** Arc of vision (Moon airless altitude minus Sun airless altitude) in degrees */
ARCV: number;
/** Topocentric crescent width in arc minutes */
W: number;
/** True when Moon is above the horizon at the given time */
moonAboveHorizon: boolean;
/** Always true: computed via Meeus approximation, not DE442S */
isApproximate: true;
}
/**
* Combined kernel-free moon snapshot from getMoon().
* Bundles phase, position, illumination, and a quick visibility estimate
* into a single call.
*/
export interface MoonSnapshot {
/** Phase name, illumination, age, and next events */
phase: MoonPhaseResult;
/** Topocentric az/alt, distance, parallactic angle */
position: MoonPosition;
/** Illumination fraction, phase cycle, bright limb angle, waxing/waning */
illumination: MoonIlluminationResult;
/** Quick Odeh-based crescent visibility estimate */
visibility: MoonVisibilityEstimate;
}
// ─── Moon phase ──────────────────────────────────────────────────────────────
export type MoonPhaseName =
| "new-moon"
| "waxing-crescent"
| "first-quarter"
| "waxing-gibbous"
| "full-moon"
| "waning-gibbous"
| "last-quarter"
| "waning-crescent";
export interface MoonPhaseResult {
/** Named phase based on illumination and waxing/waning state */
phase: MoonPhaseName;
/** Human-readable phase name, e.g. "Waxing Crescent" */
phaseName: string;
/** Moon phase emoji symbol, e.g. "🌒" */
phaseSymbol: string;
/** Illuminated fraction 0-100 (percent) */
illumination: number;
/** Hours since last new moon */
age: number;
/** Ecliptic longitude of the Moon minus the Sun, degrees [0, 360) */
elongationDeg: number;
/** True when Moon is moving away from the Sun (illumination increasing) */
isWaxing: boolean;
/** UTC date of the next new moon */
nextNewMoon: Date;
/** UTC date of the next full moon */
nextFullMoon: Date;
/** UTC date of the previous new moon */
prevNewMoon: Date;
}
// ─── Event times ─────────────────────────────────────────────────────────────
export interface SunMoonEvents {
/** UTC time of sunset for the given date at the observer's location */
sunsetUTC: Date | null;
/** UTC time of moonset for the given date at the observer's location */
moonsetUTC: Date | null;
/** UTC time of sunrise */
sunriseUTC: Date | null;
/** UTC time of moonrise */
moonriseUTC: Date | null;
/** UTC time when civil twilight ends (Sun at -6°) */
civilTwilightEndUTC: Date | null;
/** UTC time when nautical twilight ends (Sun at -12°) */
nauticalTwilightEndUTC: Date | null;
/** UTC time when astronomical twilight ends (Sun at -18°) */
astronomicalTwilightEndUTC: Date | null;
}
// ─── Full moon sighting report ────────────────────────────────────────────────
export interface MoonSightingReport {
/** Date for which the sighting report was computed */
date: Date;
/** Observer location used */
observer: Observer;
// Event times
sunsetUTC: Date | null;
moonsetUTC: Date | null;
/** Moonset minus sunset, in minutes. Null if either event is null. */
lagMinutes: number | null;
/** Best observation time (Odeh/Yallop: T_s + 4/9 * Lag) */
bestTimeUTC: Date | null;
/** Conservative observation window [bestTime - 20min, bestTime + 20min] */
bestTimeWindowUTC: [Date, Date] | null;
// At best time
/** Topocentric Moon position at best time */
moonPosition: AzAlt | null;
/** Topocentric Sun position at best time */
sunPosition: AzAlt | null;
/** Moon illumination percent at best time */
illumination: number | null;
/** Hours since conjunction (new moon) */
moonAge: number | null;
// Crescent geometry at best time
geometry: CrescentGeometry | null;
// Visibility criteria results
yallop: YallopResult | null;
odeh: OdehResult | null;
// Sighting guidance
/**
* Plain-language direction for observers.
* Includes where to look (azimuth, altitude), when (best time), and what to expect.
*/
guidance: string;
// Metadata
/** Source ephemeris used for this calculation */
ephemerisSource: "DE442S" | "approximate";
/** Whether the Moon is even above the horizon at best time */
moonAboveHorizon: boolean | null;
/** Whether sighting is geometrically possible (lag > 0, Moon above horizon at best time) */
sightingPossible: boolean;
}
// ─── Kernel configuration ─────────────────────────────────────────────────────
/**
* How to source a binary kernel file.
* Used for both the planetary SPK (de442s.bsp) and leap-second kernel (naif0012.tls).
*/
export type KernelSource =
| { type: "file"; path: string }
| { type: "buffer"; data: ArrayBuffer; name: string }
| { type: "url"; url: string }
| { type: "auto" }; // auto-download from NAIF, cache in ~/.cache/moon-sighting
export interface KernelConfig {
/** Planetary SPK kernel — defaults to de442s.bsp via auto-download */
planetary?: KernelSource;
/** Leap-second kernel — defaults to naif0012.tls via auto-download */
leapSeconds?: KernelSource;
/**
* Directory for the download cache.
* Defaults to ~/.cache/moon-sighting on POSIX, %LOCALAPPDATA%\moon-sighting on Windows.
*/
cacheDir?: string;
/**
* SHA-256 checksum of de442s.bsp for download verification.
* Bundled default matches the NAIF distribution as of 2024.
*/
checksumOverride?: string;
}
// ─── Top-level options ────────────────────────────────────────────────────────
export interface SightingOptions {
/** Kernel acquisition configuration. Defaults to auto-download. */
kernels?: KernelConfig;
/**
* Best-time computation method.
* 'heuristic' — T_b = T_sunset + 4/9 * Lag (Odeh/Yallop approximation, fast)
* 'optimized' — scan sunset-to-moonset interval, maximize Odeh V parameter
* Default: 'heuristic'
*/
bestTimeMethod?: "heuristic" | "optimized";
}
// ─── WGS84 constants ─────────────────────────────────────────────────────────
/** WGS84 reference ellipsoid parameters */
export const WGS84 = {
/** Semi-major axis in meters */
a: 6378137.0,
/** Inverse flattening */
invF: 298.257223563,
/** Flattening */
f: 1 / 298.257223563,
/** Semi-minor axis in meters */
get b() {
return this.a * (1 - this.f);
},
/** First eccentricity squared */
get e2() {
return 2 * this.f - this.f * this.f;
},
} as const;
// ─── Internal ephemeris types ─────────────────────────────────────────────────
/** A segment in a JPL SPK (DAF) kernel file */
export interface SpkSegment {
/** NAIF body ID of the target body */
target: number;
/** NAIF body ID of the center body */
center: number;
/** Reference frame code */
frame: number;
/** SPK data type (2 = Chebyshev position only, 3 = Chebyshev position + velocity) */
dataType: 2 | 3;
/** Segment start time in ET seconds past J2000 */
startET: number;
/** Segment end time in ET seconds past J2000 */
endET: number;
/** Byte offset of the data array in the file */
dataOffset: number;
/** Number of double-precision values in the data array */
dataSize: number;
}
/** A decoded Chebyshev record from a Type 2 or Type 3 SPK segment */
export interface ChebRecord {
/** Midpoint of the record interval in ET seconds past J2000 */
mid: number;
/** Half-width of the record interval in seconds */
radius: number;
/** Chebyshev coefficients for X, Y, Z [3][degree+1] */
coeffs: Float64Array[];
/** Degree of the polynomial */
degree: number;
}