mirror of
https://github.com/acamarata/moon-sighting.git
synced 2026-06-30 19:04:24 +00:00
chore: stage P1 audit changes (docs/wiki/ci)
This commit is contained in:
parent
b19d9114ec
commit
1b85f060cd
6 changed files with 208 additions and 400 deletions
15
.github/wiki/_Sidebar.md
vendored
15
.github/wiki/_Sidebar.md
vendored
|
|
@ -7,6 +7,19 @@
|
|||
|
||||
**Examples**
|
||||
- [Basic Usage](examples/basic-usage)
|
||||
- [Ramadan Crescent Prediction](examples/ramadan-crescent)
|
||||
|
||||
**API Reference**
|
||||
- [getMoonSightingReport](api/getMoonSightingReport)
|
||||
- [getMoonPhase](api/getMoonPhase)
|
||||
- [getMoonPosition](api/getMoonPosition)
|
||||
- [getMoonIllumination](api/getMoonIllumination)
|
||||
- [getMoonVisibilityEstimate](api/getMoonVisibilityEstimate)
|
||||
- [getMoon](api/getMoon)
|
||||
- [getSunMoonEvents](api/getSunMoonEvents)
|
||||
- [initKernels](api/initKernels)
|
||||
- [downloadKernels](api/downloadKernels)
|
||||
- [verifyKernels](api/verifyKernels)
|
||||
|
||||
**Domain Reference**
|
||||
- [Crescent Visibility](Crescent-Visibility)
|
||||
|
|
@ -17,7 +30,7 @@
|
|||
- [Validation](Validation)
|
||||
|
||||
**Package Reference**
|
||||
- [API Reference](API-Reference)
|
||||
- [Full API Reference](API-Reference)
|
||||
- [Architecture](Architecture)
|
||||
- [Benchmarks](benchmarks/index)
|
||||
|
||||
|
|
|
|||
119
.github/wiki/examples/basic-usage.md
vendored
119
.github/wiki/examples/basic-usage.md
vendored
|
|
@ -1,96 +1,83 @@
|
|||
# Basic Usage Examples
|
||||
|
||||
## Basic crescent visibility check
|
||||
## Crescent visibility report with JPL kernel
|
||||
|
||||
```typescript
|
||||
import { visibility } from 'moon-sighting';
|
||||
import { initKernels, getMoonSightingReport } from 'moon-sighting'
|
||||
|
||||
const result = visibility({
|
||||
date: new Date(2023, 2, 22), // March 22, 2023 (month is 0-indexed)
|
||||
lat: 21.39,
|
||||
lng: 39.86,
|
||||
elevation: 277,
|
||||
});
|
||||
// One-time kernel setup (downloads ~31 MB from NASA NAIF on first run)
|
||||
await initKernels()
|
||||
|
||||
console.log(result.visible); // true or false
|
||||
console.log(result.q?.toFixed(3)); // Yallop q-value, e.g. '0.216'
|
||||
// Full sighting report for London, 29 March 2025
|
||||
const report = await getMoonSightingReport(new Date('2025-03-29'), {
|
||||
lat: 51.5074,
|
||||
lon: -0.1278,
|
||||
elevation: 10
|
||||
})
|
||||
|
||||
console.log(report.yallop.category) // 'A' — easily visible to the naked eye
|
||||
console.log(report.odeh.zone) // 'A' — visible with naked eye
|
||||
console.log(report.bestTimeUTC) // Date: optimal observation window
|
||||
console.log(report.guidance)
|
||||
// "Best time to look: 2025-03-29 20:14 UTC (73 min after sunset).
|
||||
// Look West at 8° above the horizon. Yallop: A. Odeh: A."
|
||||
```
|
||||
|
||||
## Use Odeh criterion
|
||||
## Quick kernel-free visibility estimate
|
||||
|
||||
No kernel download required. Uses the Meeus approximation (~0.3° accuracy).
|
||||
|
||||
```typescript
|
||||
import { visibility } from 'moon-sighting';
|
||||
import { getMoonVisibilityEstimate } from 'moon-sighting'
|
||||
|
||||
const result = visibility({
|
||||
date: new Date(2023, 2, 22),
|
||||
lat: 40.0,
|
||||
lng: -75.0,
|
||||
elevation: 100,
|
||||
criterion: 'odeh',
|
||||
});
|
||||
// Estimate at approximately sunset + 40 min in Mecca
|
||||
const obs = new Date('2025-03-02T15:30:00Z')
|
||||
const est = getMoonVisibilityEstimate(obs, 21.42, 39.83, 277)
|
||||
|
||||
console.log(result.visible); // true or false
|
||||
console.log(result.category); // Odeh category string
|
||||
console.log(est.zone) // 'A' through 'D'
|
||||
console.log(est.V) // Odeh V parameter
|
||||
console.log(est.isVisibleNakedEye) // true/false
|
||||
console.log(est.ARCL, est.ARCV, est.W)
|
||||
```
|
||||
|
||||
## Check visibility from multiple cities
|
||||
## Check from multiple cities
|
||||
|
||||
```typescript
|
||||
import { visibility } from 'moon-sighting';
|
||||
import { initKernels, getMoonSightingReport } from 'moon-sighting'
|
||||
|
||||
const newMoonDate = new Date(2023, 2, 22);
|
||||
await initKernels()
|
||||
|
||||
const cities = [
|
||||
{ name: 'Mecca', lat: 21.39, lng: 39.86, elevation: 277 },
|
||||
{ name: 'London', lat: 51.51, lng: -0.13, elevation: 11 },
|
||||
{ name: 'New York', lat: 40.71, lng: -74.00, elevation: 10 },
|
||||
{ name: 'Karachi', lat: 24.86, lng: 67.01, elevation: 13 },
|
||||
];
|
||||
{ name: 'Mecca', lat: 21.42, lon: 39.83, elevation: 277 },
|
||||
{ name: 'London', lat: 51.51, lon: -0.13, elevation: 11 },
|
||||
{ name: 'New York', lat: 40.71, lon: -74.00, elevation: 10 },
|
||||
{ name: 'Karachi', lat: 24.86, lon: 67.01, elevation: 13 },
|
||||
]
|
||||
|
||||
const date = new Date('2025-03-29')
|
||||
for (const city of cities) {
|
||||
const r = visibility({ ...city, date: newMoonDate });
|
||||
console.log(`${city.name}: ${r.visible ? 'visible' : 'not visible'} (q=${r.q?.toFixed(3)})`);
|
||||
const r = await getMoonSightingReport(date, city)
|
||||
const cat = r.yallop?.category ?? 'N/A'
|
||||
console.log(`${city.name}: Yallop=${cat} sighting=${r.sightingPossible}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Scan multiple nights for first visibility
|
||||
## Moon phase and position (no kernel)
|
||||
|
||||
```typescript
|
||||
import { visibility } from 'moon-sighting';
|
||||
import { getMoonPhase, getMoonPosition, getMoon } from 'moon-sighting'
|
||||
|
||||
function findFirstVisible(observer: { lat: number; lng: number; elevation: number }, startDate: Date, maxDays = 5) {
|
||||
for (let i = 0; i < maxDays; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(startDate.getDate() + i);
|
||||
const r = visibility({ ...observer, date });
|
||||
if (r.visible) {
|
||||
return { date, result: r };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const date = new Date('2025-03-04')
|
||||
|
||||
const observer = { lat: 21.39, lng: 39.86, elevation: 277 };
|
||||
const startDate = new Date(2023, 2, 21); // March 21, 2023
|
||||
const phase = getMoonPhase(date)
|
||||
console.log(phase.phaseName) // 'Waxing Crescent'
|
||||
console.log(phase.phaseSymbol) // '🌒'
|
||||
console.log(phase.illumination) // percent
|
||||
|
||||
const firstVisible = findFirstVisible(observer, startDate);
|
||||
if (firstVisible) {
|
||||
console.log('First visible:', firstVisible.date.toDateString());
|
||||
console.log('q-value:', firstVisible.result.q?.toFixed(3));
|
||||
}
|
||||
```
|
||||
|
||||
## CJS usage
|
||||
|
||||
```javascript
|
||||
const { visibility } = require('moon-sighting');
|
||||
|
||||
const result = visibility({
|
||||
date: new Date(2023, 2, 22),
|
||||
lat: 21.39,
|
||||
lng: 39.86,
|
||||
elevation: 277,
|
||||
});
|
||||
|
||||
console.log(result.visible);
|
||||
const pos = getMoonPosition(date, 51.5, -0.1, 10)
|
||||
console.log(pos.azimuth, pos.altitude) // e.g. 247.3, 22.8
|
||||
|
||||
// Or everything in one call
|
||||
const moon = getMoon(date, 51.5, -0.1)
|
||||
console.log(moon.phase.phaseName, moon.visibility.zone)
|
||||
```
|
||||
|
|
|
|||
94
.github/wiki/examples/ramadan-crescent.md
vendored
Normal file
94
.github/wiki/examples/ramadan-crescent.md
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Ramadan Crescent Prediction
|
||||
|
||||
Predicting the first visible crescent of Ramadan is the main use case for this library. The crescent that marks the beginning of Ramadan is the first sighting of the new crescent after the new moon that precedes the month.
|
||||
|
||||
## Find the first visible night for Ramadan 1447
|
||||
|
||||
Ramadan 1447 AH begins with the first crescent sighting after the new moon on approximately 28 February 2026.
|
||||
|
||||
```typescript
|
||||
import { initKernels, getMoonSightingReport } from 'moon-sighting'
|
||||
|
||||
await initKernels()
|
||||
|
||||
// Observer: Mecca (the reference location for many Islamic calendars)
|
||||
const mecca = { lat: 21.42, lon: 39.83, elevation: 277 }
|
||||
|
||||
// Scan three nights starting from the calculated new moon
|
||||
const startDate = new Date('2026-02-28')
|
||||
const results = []
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const date = new Date(startDate)
|
||||
date.setDate(startDate.getDate() + i)
|
||||
const r = await getMoonSightingReport(date, mecca)
|
||||
results.push({ date, report: r })
|
||||
}
|
||||
|
||||
// Print the night when sighting becomes possible
|
||||
for (const { date, report } of results) {
|
||||
const cat = report.yallop?.category ?? '-'
|
||||
const zone = report.odeh?.zone ?? '-'
|
||||
const guidance = report.sightingPossible
|
||||
? `Yallop: ${cat}, Odeh: ${zone}`
|
||||
: 'Sighting not possible'
|
||||
console.log(`${date.toDateString()}: ${guidance}`)
|
||||
}
|
||||
```
|
||||
|
||||
Expected output (approximate):
|
||||
|
||||
```
|
||||
Sat Feb 28 2026: Sighting not possible
|
||||
Sun Mar 01 2026: Sighting not possible
|
||||
Mon Mar 02 2026: Yallop: B, Odeh: A
|
||||
Tue Mar 03 2026: Yallop: A, Odeh: A
|
||||
```
|
||||
|
||||
The first night when `sightingPossible` is true and Yallop returns A or B is the candidate for Ramadan's start.
|
||||
|
||||
## Global visibility scan
|
||||
|
||||
Different locations see the crescent on different nights. Use this pattern to check multiple cities and identify the global first sighting.
|
||||
|
||||
```typescript
|
||||
import { initKernels, getMoonSightingReport } from 'moon-sighting'
|
||||
|
||||
await initKernels()
|
||||
|
||||
const cities = [
|
||||
{ name: 'Mecca', lat: 21.42, lon: 39.83, elevation: 277 },
|
||||
{ name: 'Istanbul', lat: 41.01, lon: 28.97, elevation: 100 },
|
||||
{ name: 'Jakarta', lat: -6.20, lon: 106.82, elevation: 8 },
|
||||
{ name: 'London', lat: 51.51, lon: -0.13, elevation: 11 },
|
||||
{ name: 'New York', lat: 40.71, lon: -74.00, elevation: 10 },
|
||||
{ name: 'Los Angeles', lat: 34.05, lon:-118.24, elevation: 89 },
|
||||
]
|
||||
|
||||
const date = new Date('2026-03-02')
|
||||
|
||||
for (const city of cities) {
|
||||
const r = await getMoonSightingReport(date, city)
|
||||
if (!r.sightingPossible) {
|
||||
console.log(`${city.name}: not possible`)
|
||||
continue
|
||||
}
|
||||
const cat = r.yallop?.category ?? '?'
|
||||
const lag = r.lagMinutes?.toFixed(0) ?? '?'
|
||||
console.log(`${city.name}: Yallop ${cat}, lag ${lag} min, ${r.guidance?.slice(0, 60)}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Interpret the results
|
||||
|
||||
The Yallop categories for the purpose of Ramadan determination:
|
||||
|
||||
| Category | Meaning for crescent sighting |
|
||||
| -------- | ----------------------------- |
|
||||
| A | Easily visible to the naked eye. Most scholars accept this. |
|
||||
| B | Visible under perfect conditions. Accepted with naked-eye report. |
|
||||
| C | May need optical aid to locate. Binoculars may find it. |
|
||||
| D | Optical aid only; naked-eye sighting effectively impossible. |
|
||||
| E, F | Not visible by any means. |
|
||||
|
||||
For a detailed explanation of the criteria and their Islamic jurisprudence context, see [Crescent Visibility](../Crescent-Visibility).
|
||||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
|
@ -81,3 +81,25 @@ jobs:
|
|||
! grep -q "test.mjs" pack-output.txt || (echo "test.mjs should not be in pack" && exit 1)
|
||||
! grep -q "node_modules" pack-output.txt || (echo "node_modules should not be in pack" && exit 1)
|
||||
echo "Pack contents verified."
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Coverage
|
||||
run: pnpm run coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
|
|
|||
348
README.md
348
README.md
|
|
@ -4,9 +4,7 @@
|
|||
[](https://github.com/acamarata/moon-sighting/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
|
||||
High-accuracy lunar crescent visibility and moon sighting library for Node.js and browsers. Uses JPL DE442S ephemerides with full IERS Earth orientation for sub-arcsecond topocentric Moon and Sun positions.
|
||||
|
||||
Implements the Yallop (NAO TN 69) and Odeh (Experimental Astronomy 2006) crescent visibility criteria, the two most widely used models in Islamic crescent sighting workflows.
|
||||
High-accuracy lunar crescent visibility and moon sighting calculations for Node.js and browsers. Uses the JPL DE442S ephemeris with full IERS Earth orientation for sub-arcsecond topocentric Moon and Sun positions. Implements the Yallop (NAO TN 69) and Odeh (Experimental Astronomy 2006) criteria.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -14,370 +12,54 @@ Implements the Yallop (NAO TN 69) and Odeh (Experimental Astronomy 2006) crescen
|
|||
npm install moon-sighting
|
||||
```
|
||||
|
||||
After installing, download the JPL ephemeris kernel (31 MB, one-time setup):
|
||||
Then download the JPL kernel (31 MB, one-time):
|
||||
|
||||
```bash
|
||||
npx moon-sighting download-kernels
|
||||
```
|
||||
|
||||
This fetches `de442s.bsp` and `naif0012.tls` from NASA's NAIF server and caches them locally. The download is verified by SHA-256 checksum.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { initKernels, getMoonSightingReport, getMoonPhase } from 'moon-sighting'
|
||||
import { initKernels, getMoonSightingReport } from 'moon-sighting'
|
||||
|
||||
// One-time setup: load the ephemeris kernel
|
||||
await initKernels()
|
||||
|
||||
// Full sighting report for a date and location
|
||||
const report = await getMoonSightingReport(new Date('2025-03-29'), {
|
||||
lat: 51.5074,
|
||||
lon: -0.1278,
|
||||
elevation: 10,
|
||||
name: 'London, UK'
|
||||
lat: 51.5074, lon: -0.1278, elevation: 10
|
||||
})
|
||||
|
||||
console.log(report.yallop.category) // 'A' (easily visible to the naked eye)
|
||||
console.log(report.odeh.zone) // 'A' (visible with naked eye)
|
||||
console.log(report.yallop.category) // 'A' — easily visible to the naked eye
|
||||
console.log(report.odeh.zone) // 'A' — visible with naked eye
|
||||
console.log(report.guidance)
|
||||
// "Best time to look: 2025-03-29 20:14 UTC (73 min after sunset).
|
||||
// Look West at 8° above the horizon. The crescent should be visible
|
||||
// to the naked eye. Yallop: A (Easily visible to the naked eye).
|
||||
// Odeh: A (Visible with naked eye)."
|
||||
```
|
||||
|
||||
Five functions work without any kernel (Meeus approximation, ~0.3° accuracy):
|
||||
|
||||
```ts
|
||||
import { getMoonPhase, getMoonPosition, getMoonIllumination,
|
||||
getMoonVisibilityEstimate, getMoon } from 'moon-sighting'
|
||||
|
||||
// Moon phase works without a kernel
|
||||
const phase = getMoonPhase()
|
||||
console.log(phase.phase) // 'waxing-crescent'
|
||||
console.log(phase.illumination) // 14.3
|
||||
console.log(phase.nextFullMoon) // Date
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `initKernels(config?)`
|
||||
|
||||
Load the ephemeris kernel. Required before `getMoonSightingReport()`.
|
||||
|
||||
```ts
|
||||
// Auto-download and cache (default)
|
||||
await initKernels()
|
||||
|
||||
// User-supplied file path (Node.js)
|
||||
await initKernels({ planetary: { type: 'file', path: '/data/de442s.bsp' } })
|
||||
|
||||
// ArrayBuffer (browser)
|
||||
await initKernels({ planetary: { type: 'buffer', data: buf, name: 'de442s.bsp' } })
|
||||
```
|
||||
|
||||
### `getMoonSightingReport(date, observer, options?)`
|
||||
|
||||
Returns a complete moon sighting report.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `date` | `Date` | Civil date to check (UTC) |
|
||||
| `observer.lat` | `number` | Geodetic latitude, degrees (north positive) |
|
||||
| `observer.lon` | `number` | Longitude, degrees (east positive) |
|
||||
| `observer.elevation` | `number` | Height above WGS84 ellipsoid, meters |
|
||||
| `observer.deltaT` | `number?` | Override TT - UT1 in seconds (IERS value) |
|
||||
| `observer.pressure` | `number?` | Atmospheric pressure, mbar (default 1013.25) |
|
||||
| `observer.temperature` | `number?` | Temperature, Celsius (default 15) |
|
||||
|
||||
**Returns** `MoonSightingReport`:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `sunsetUTC` | `Date` | Sunset time |
|
||||
| `moonsetUTC` | `Date` | Moonset time |
|
||||
| `lagMinutes` | `number` | Moonset - sunset, minutes |
|
||||
| `bestTimeUTC` | `Date` | Optimal observation time |
|
||||
| `geometry.ARCL` | `number` | Arc of light (elongation), degrees |
|
||||
| `geometry.ARCV` | `number` | Arc of vision (airless), degrees |
|
||||
| `geometry.DAZ` | `number` | Relative azimuth, degrees |
|
||||
| `geometry.W` | `number` | Crescent width, arc minutes |
|
||||
| `yallop.category` | `'A'`–`'F'` | Yallop visibility class |
|
||||
| `yallop.q` | `number` | Continuous q parameter |
|
||||
| `odeh.zone` | `'A'`–`'D'` | Odeh visibility zone |
|
||||
| `odeh.V` | `number` | Continuous V parameter |
|
||||
| `moonPosition` | `AzAlt` | Moon azimuth/altitude at best time |
|
||||
| `guidance` | `string` | Plain-language sighting instructions |
|
||||
|
||||
### `getMoonPosition(date?, lat, lon, elevation?)`
|
||||
|
||||
Compute the Moon's topocentric position. Works without a kernel. Uses Meeus Chapter 47 approximate positions (~0.3° accuracy).
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `date` | `Date?` | Date to evaluate (default: now) |
|
||||
| `lat` | `number` | Geodetic latitude, degrees (north positive) |
|
||||
| `lon` | `number` | Longitude, degrees (east positive) |
|
||||
| `elevation` | `number?` | Height above ellipsoid, meters (default: 0) |
|
||||
|
||||
**Returns** `MoonPosition`:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `azimuth` | `number` | Degrees from North, clockwise (0–360) |
|
||||
| `altitude` | `number` | Apparent altitude in degrees (refraction applied) |
|
||||
| `distance` | `number` | Distance from Earth center to Moon center, km |
|
||||
| `parallacticAngle` | `number` | Angle between zenith and north pole as seen from the Moon, radians |
|
||||
|
||||
### `getMoonIllumination(date?)`
|
||||
|
||||
Compute the Moon's illumination. Works without a kernel. Uses Meeus Chapter 47/48 (~0.5% illumination accuracy).
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `date` | `Date?` | Date to evaluate (default: now) |
|
||||
|
||||
**Returns** `MoonIlluminationResult`:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `fraction` | `number` | Illuminated fraction, 0 (new moon) to 1 (full moon) |
|
||||
| `phase` | `number` | Position in the 0–1 lunar cycle: 0=new, 0.25=first quarter, 0.5=full, 0.75=last quarter |
|
||||
| `angle` | `number` | Position angle of the bright limb midpoint, eastward from north, radians |
|
||||
| `isWaxing` | `boolean` | True when elongation is increasing (new moon toward full moon) |
|
||||
|
||||
### `getMoonPhase(date?)`
|
||||
|
||||
Compute the Moon's current phase. Works without a kernel.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `phase` | `string` | Phase key (e.g., `'waxing-crescent'`) |
|
||||
| `phaseName` | `string` | Display name (e.g., `'Waxing Crescent'`) |
|
||||
| `phaseSymbol` | `string` | Moon emoji (e.g., `'🌒'`) |
|
||||
| `illumination` | `number` | Illuminated fraction, 0–100 |
|
||||
| `age` | `number` | Hours since last new moon |
|
||||
| `isWaxing` | `boolean` | True when illumination is increasing |
|
||||
| `prevNewMoon` | `Date` | Time of previous new moon |
|
||||
| `nextNewMoon` | `Date` | Time of next new moon |
|
||||
| `nextFullMoon` | `Date` | Time of next full moon |
|
||||
|
||||
### `getMoonVisibilityEstimate(date?, lat, lon, elevation?)`
|
||||
|
||||
Quick kernel-free Odeh crescent visibility estimate. Pass an estimated post-sunset observation time. Returns V parameter, zone A–D, ARCL, ARCV, W, and `moonAboveHorizon`. For precise crescent work use `getMoonSightingReport()`.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `zone` | `'A'`–`'D'` | Odeh visibility zone |
|
||||
| `V` | `number` | Odeh V parameter (positive = crescent exceeds threshold) |
|
||||
| `isVisibleNakedEye` | `boolean` | True for zone A |
|
||||
| `isVisibleWithOpticalAid` | `boolean` | True for zones A and B |
|
||||
| `ARCL` | `number` | Elongation, degrees |
|
||||
| `ARCV` | `number` | Moon alt − Sun alt (airless), degrees |
|
||||
| `W` | `number` | Crescent width, arc minutes |
|
||||
| `moonAboveHorizon` | `boolean` | Moon above horizon at given time |
|
||||
| `isApproximate` | `true` | Always true: Meeus approximation |
|
||||
|
||||
### `getMoon(date?, lat, lon, elevation?)`
|
||||
|
||||
Convenience wrapper returning phase, position, illumination, and visibility estimate in one call. Works without a kernel.
|
||||
|
||||
```ts
|
||||
const moon = getMoon(new Date(), 51.5074, -0.1278, 10)
|
||||
moon.phase.phaseName // 'Waxing Crescent'
|
||||
moon.phase.phaseSymbol // '🌒'
|
||||
moon.position.altitude // degrees above horizon
|
||||
moon.illumination.fraction // 0.0 to 1.0
|
||||
moon.visibility.zone // 'A' through 'D'
|
||||
```
|
||||
|
||||
### `getSunMoonEvents(date, observer)`
|
||||
|
||||
Get rise, set, and twilight times. Requires kernel.
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| `sunsetUTC` | Sunset |
|
||||
| `moonsetUTC` | Moonset |
|
||||
| `sunriseUTC` | Sunrise |
|
||||
| `moonriseUTC` | Moonrise |
|
||||
| `civilTwilightEndUTC` | Civil twilight (Sun at -6°) |
|
||||
| `nauticalTwilightEndUTC` | Nautical twilight (Sun at -12°) |
|
||||
| `astronomicalTwilightEndUTC` | Astronomical twilight (Sun at -18°) |
|
||||
|
||||
### `downloadKernels(config?)`
|
||||
|
||||
Download DE442S and naif0012.tls to the local cache (Node.js).
|
||||
|
||||
### `verifyKernels(config?)`
|
||||
|
||||
Verify cached kernels by SHA-256 checksum.
|
||||
|
||||
## Visibility criteria
|
||||
|
||||
### Yallop (A–F)
|
||||
|
||||
| Category | q range | Interpretation |
|
||||
| -------- | ------- | -------------- |
|
||||
| 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 |
|
||||
| D | q > -0.232 | Needs optical aid; not visible naked eye |
|
||||
| E | q > -0.293 | Not visible even with telescope |
|
||||
| F | q ≤ -0.293 | Below Danjon limit |
|
||||
|
||||
### Odeh (A–D)
|
||||
|
||||
| Zone | V range | Interpretation |
|
||||
| ---- | ------- | -------------- |
|
||||
| A | V ≥ 5.65 | Visible with naked eye |
|
||||
| B | V ≥ 2.00 | Visible with optical aid; may be naked eye |
|
||||
| C | V ≥ -0.96 | Visible with optical aid only |
|
||||
| D | V < -0.96 | Not visible even with optical aid |
|
||||
|
||||
## Kernel-free utilities
|
||||
|
||||
Five functions work without loading any kernel. They use Meeus Chapters 47 and 48. Use them for display, widgets, and any context where JPL-grade accuracy is not required.
|
||||
|
||||
```ts
|
||||
import {
|
||||
getMoonPhase, getMoonPosition, getMoonIllumination,
|
||||
getMoonVisibilityEstimate, getMoon,
|
||||
} from 'moon-sighting'
|
||||
|
||||
// Current phase with display name and emoji
|
||||
const phase = getMoonPhase()
|
||||
console.log(phase.phase) // 'waxing-crescent'
|
||||
console.log(phase.phaseName) // 'Waxing Crescent'
|
||||
console.log(phase.phaseSymbol) // '🌒'
|
||||
console.log(phase.illumination) // 14.3 (percent)
|
||||
console.log(phase.nextFullMoon) // Date
|
||||
|
||||
// Topocentric position (azimuth, altitude, distance)
|
||||
const pos = getMoonPosition(new Date(), 51.5074, -0.1278, 10)
|
||||
console.log(pos.azimuth) // 214.7 (degrees from North)
|
||||
console.log(pos.altitude) // 38.2 (degrees above horizon, refraction applied)
|
||||
console.log(pos.distance) // 384400 (km)
|
||||
|
||||
// Illumination fraction and phase angle
|
||||
const illum = getMoonIllumination()
|
||||
console.log(illum.fraction) // 0.143 (0=new, 1=full)
|
||||
console.log(illum.phase) // 0.09 (0–1 cycle position)
|
||||
console.log(illum.isWaxing) // true
|
||||
|
||||
// Quick Odeh crescent visibility estimate (pass a post-sunset time)
|
||||
const vis = getMoonVisibilityEstimate(new Date('2025-03-02T18:30:00Z'), 51.5074, -0.1278)
|
||||
console.log(vis.zone) // 'A' through 'D'
|
||||
console.log(vis.V) // Odeh V parameter
|
||||
console.log(vis.isVisibleNakedEye) // true/false
|
||||
|
||||
// All four in a single call
|
||||
const moon = getMoon(new Date(), 51.5074, -0.1278, 10)
|
||||
console.log(moon.phase.phaseName) // 'Waxing Crescent'
|
||||
console.log(moon.position.altitude) // degrees
|
||||
console.log(moon.illumination.fraction) // 0.0–1.0
|
||||
console.log(moon.visibility.zone) // 'A'–'D'
|
||||
```
|
||||
|
||||
These are a direct replacement for the equivalent `suncalc` moon functions, with no external dependencies.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
src/
|
||||
math/ Vector/matrix, Chebyshev evaluation, root-finding
|
||||
time/ UTC → TAI → TT → TDB conversions, leap seconds, Julian Day
|
||||
spk/ JPL DAF/SPK kernel parser, Chebyshev segment evaluator
|
||||
frames/ IERS Q·R·W chain: precession + nutation + ERA + polar motion
|
||||
observer/ WGS84 geodetic → ECEF, topocentric ENU, Bennett refraction
|
||||
bodies/ Moon/Sun state computation, illumination, crescent width
|
||||
events/ Rise/set solver, twilight, best-time computation
|
||||
visibility/ Yallop q-test, Odeh zones, crescent geometry
|
||||
api/ User-facing functions, kernel management
|
||||
cli/ Command-line interface
|
||||
```
|
||||
|
||||
See the [Architecture wiki page](https://github.com/acamarata/moon-sighting/wiki/Architecture) for a full technical description.
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
# Setup (one-time)
|
||||
npx moon-sighting download-kernels
|
||||
|
||||
# Sighting report
|
||||
npx moon-sighting sighting 51.5 -0.1 2025-03-29
|
||||
npx moon-sighting sighting 21.4 39.8 # Mecca
|
||||
|
||||
# Moon phase
|
||||
npx moon-sighting phase 2025-03-01
|
||||
|
||||
# Verify downloaded kernels
|
||||
npx moon-sighting verify-kernels
|
||||
|
||||
# Benchmark
|
||||
npx moon-sighting benchmark
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Environment | Support |
|
||||
| ----------- | ------- |
|
||||
| Node.js 20+ | Full (all features) |
|
||||
| Node.js 22, 24 | Full |
|
||||
| Browser | Partial (no auto-download; supply kernel buffer) |
|
||||
| ESM | `import` from `moon-sighting` |
|
||||
| CommonJS | `require('moon-sighting')` |
|
||||
| TypeScript | Full type definitions included |
|
||||
|
||||
## TypeScript
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Observer,
|
||||
MoonSightingReport,
|
||||
MoonPhaseResult,
|
||||
MoonPosition,
|
||||
MoonIlluminationResult,
|
||||
MoonVisibilityEstimate,
|
||||
MoonSnapshot,
|
||||
YallopCategory,
|
||||
OdehZone,
|
||||
KernelConfig,
|
||||
} from 'moon-sighting'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is on the [GitHub Wiki](https://github.com/acamarata/moon-sighting/wiki):
|
||||
Full documentation on the [GitHub Wiki](https://github.com/acamarata/moon-sighting/wiki):
|
||||
|
||||
- [Getting Started](https://github.com/acamarata/moon-sighting/wiki/Getting-Started)
|
||||
- [API Reference](https://github.com/acamarata/moon-sighting/wiki/API-Reference)
|
||||
- [Architecture](https://github.com/acamarata/moon-sighting/wiki/Architecture)
|
||||
- [Crescent Visibility Criteria](https://github.com/acamarata/moon-sighting/wiki/Crescent-Visibility)
|
||||
- [Ephemeris and Kernel Setup](https://github.com/acamarata/moon-sighting/wiki/Ephemeris)
|
||||
- [Time Scales](https://github.com/acamarata/moon-sighting/wiki/Time-Scales)
|
||||
- [Reference Frames](https://github.com/acamarata/moon-sighting/wiki/Reference-Frames)
|
||||
- [Observer Model and Refraction](https://github.com/acamarata/moon-sighting/wiki/Observer-Model)
|
||||
- [Validation](https://github.com/acamarata/moon-sighting/wiki/Validation)
|
||||
|
||||
## Related
|
||||
|
||||
- [nrel-spa](https://github.com/acamarata/nrel-spa): Pure JS solar position algorithm (zero deps)
|
||||
- [nrel-spa](https://github.com/acamarata/nrel-spa): Pure JS solar position algorithm
|
||||
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer times with dynamic angle algorithm
|
||||
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): Hijri/Gregorian calendar conversion
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Crescent visibility criteria implemented from:
|
||||
|
||||
- B.D. Yallop, "A Method for Predicting the First Sighting of the New Crescent Moon," NAO Technical Note No. 69, Royal Greenwich Observatory, 1997.
|
||||
- M.Sh. Odeh, "New Criterion for Lunar Crescent Visibility," Experimental Astronomy 18(1), 39–64, 2006.
|
||||
|
||||
Planetary ephemeris data from:
|
||||
|
||||
- JPL DE442S. Jet Propulsion Laboratory, NASA. Ryan S. Park et al. (2021). "The JPL Planetary and Lunar Ephemerides DE440 and DE441." Astronomical Journal 161(3), 105. [doi:10.3847/1538-3881/abd414](https://doi.org/10.3847/1538-3881/abd414)
|
||||
|
||||
NAIF SPICE toolkit concepts:
|
||||
|
||||
- Navigation and Ancillary Information Facility (NAIF), Jet Propulsion Laboratory.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
|
||||
The DE442S kernel data is provided by NASA/JPL and is not redistributed with this package. It is downloaded separately from the NAIF public server.
|
||||
|
|
|
|||
10
src/types.ts
10
src/types.ts
|
|
@ -140,6 +140,7 @@ export interface CrescentGeometry {
|
|||
// ─── 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'
|
||||
|
||||
/**
|
||||
|
|
@ -159,6 +160,10 @@ export const YALLOP_THRESHOLDS = {
|
|||
E: -0.293,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Human-readable descriptions for each Yallop visibility category (A–F).
|
||||
* 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',
|
||||
|
|
@ -188,6 +193,7 @@ export interface YallopResult {
|
|||
// ─── 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'
|
||||
|
||||
/**
|
||||
|
|
@ -203,6 +209,10 @@ export const ODEH_THRESHOLDS = {
|
|||
C: -0.96,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Human-readable descriptions for each Odeh visibility zone (A–D).
|
||||
* 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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue