Skip to content

About

Totality duration isolines and map for total solar eclipses

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chasing totality

A personal tool for picking a spot to watch a total solar eclipse. You explore a map with the path of totality on it, click a location, and get what you need to decide: how long totality lasts there, whether the sky is usually clear, how to fly there and whether it's safe.

Current status: totality duration for any lat/lon and isolines on a map, for 2 August 2027. Weather, flights and safety are planned.

Stack: TypeScript + Node 24, Fastify, React + Vite, MapLibre GL JS + OpenFreeMap, Vitest, d3-contour

Isolines over the Strait of Gibraltar

Quick Overview

graph LR
    Elements[Besselian elements]
    Engine[Local circumstances]
    Grid[Duration grid]
    Contours[Isolines]
    GeoJSON[(GeoJSON)]

    Elements --> Engine
    Engine --> Grid
    Grid --> Contours
    Contours --> GeoJSON
Loading

Key Features:

  • ✅ Totality duration for any lat/lon, from NASA's Besselian elements
  • ✅ Isolines every 30 s, from the path's edge up to the longest totality, as GeoJSON
  • ✅ Tested against NASA's path table and NASA's JSEX calculator

How it works, what was measured and what's left out: spike #4 plan and findings.

Quick Start

System requirements

  • NVM (Node Version Management)
  • Node.js >= 24
  • PNPM 10.18.2 (via corepack)

Development

nvm use
pnpm install

pnpm test        # run the tests
pnpm test:unit   # only the unit tests (tests/unit/)
pnpm test:integration  # only the integration tests (tests/integration/)
pnpm test:coverage  # run the tests with coverage, report in coverage/
pnpm typecheck   # type-check without emitting
pnpm dev         # start the API with reload on port 3000
pnpm build       # compile src/ to dist/
pnpm start       # run the compiled API
pnpm data:isolines 2027-08-02  # regenerate data/isolines/2027-08-02.geojson
pnpm data:airports  # refresh data/airports.geojson from OurAirports

The API settings and their defaults are in .env.example; copy it to .env to change them. Endpoints:

  • GET /health: {"status":"ok"}
  • GET /eclipses: dates with data, e.g. ["2027-08-02"]
  • GET /eclipses/:date/isolines: GeoJSON isolines; 404 if there's no data, 422 for an invalid date
  • GET /airports?bbox=west,south,east,north: GeoJSON airports with their source and retrieval date; the optional bbox keeps only those inside it, 422 if it's invalid

pnpm data:isolines <date> writes two files:

  • data/isolines/<date>.geojson: the isolines the API serves, over NASA's whole path; commit the result.
  • out/<date>-nasa-path.geojson: NASA's limits and central line, to compare against.

Drop both into geojson.io to see them on a map.

To add an eclipse, add its Besselian elements under src/engine/eclipse/elements/ and to the list there, and NASA's path table as data/nasa-paths/<date>.txt, with a # header like the existing tables' that gives the ΔT NASA used (ΔT = <seconds> s). The tests then check the elements against the table; run pnpm data:isolines <date> to generate its isolines. In Claude Code, /add-eclipse <date> does all of this, checks it, and stops for review before anything is committed.

The airports come from OurAirports (public domain): large and medium airports with scheduled service and an IATA code, worldwide. pnpm data:airports downloads them again and rewrites data/airports.geojson; commit the result.

Frontend

The React app lives in web/, a pnpm workspace package. It calls /api/* on its own origin and the Vite dev server forwards those requests to the API (prefix removed), so run both:

pnpm dev                         # API on port 3000
pnpm --filter web dev            # frontend on http://localhost:5173
pnpm --filter web test           # frontend tests
pnpm --filter web typecheck
pnpm --filter web build          # static files in web/dist

The page shows the API status from GET /api/health and a map: an OpenFreeMap base map drawn by MapLibre GL JS, with the picked eclipse's isolines from GET /api/eclipses/:date/isolines and the airports around the path from GET /api/airports on top. The airport icon is Maki's (CC0). The tiles come straight from OpenFreeMap: no key, no proxy. The proxy target is API_PROXY_TARGET (default http://localhost:3000).

pnpm fixtures:jsex regenerates tests/fixtures/jsex-2027.json from JSEX (a GitHub copy of NASA's calculator, pinned to one commit). Only needed if the test points or the elements change.

Docker

compose.yaml is the production stack: the compiled API and the built frontend served by nginx. compose.dev.yaml is an override that turns the same two services into development mode, with your source mounted and reload on.

Prod Dev
Command docker compose up --build docker compose -f compose.yaml -f compose.dev.yaml up --build
API http://localhost:8080/api/health (through nginx; the API port isn't published) http://localhost:3000/health (pnpm dev)
Frontend http://localhost:8080 (nginx) http://localhost:5173 (Vite)

In both, the frontend reaches the API through /api. In dev, after adding a dependency, add -V to up --build: node_modules lives in a volume that otherwise keeps the old packages.

# Tests and scripts run in the dev container; out/ shows up on the host
docker compose -f compose.yaml -f compose.dev.yaml run --rm api pnpm test
docker compose -f compose.yaml -f compose.dev.yaml run --rm api pnpm data:isolines 2027-08-02

The frontend's nginx forwards /api to a host named api, so it only starts where the api service exists (as in compose).

Project Structure

src/
└── engine/                # Everything that computes; a frontend/backend would only consume it
    ├── eclipse/           # Totality duration at one place
    │   ├── elements/      # Besselian elements per eclipse, listed by date
    │   ├── besselian.ts   # Evaluates the elements at a given time
    │   ├── observer.ts    # Observer's position relative to the Earth's centre
    │   ├── localCircumstances.ts  # Eclipse type, contact times and duration
    │   └── nasaPath.ts    # Reads NASA's path tables
    └── isolines/
        ├── area.ts        # The grid's area, around NASA's path
        ├── grid.ts        # Engine values on a lat/lon grid
        └── contours.ts    # Grid to GeoJSON isolines
└── server/                # Fastify API
    ├── app.ts             # Builds the app: logger, data dir, /health
    ├── routes/eclipses.ts # /eclipses and /eclipses/:date/isolines
    ├── routes/airports.ts # /airports
    └── main.ts            # Reads the env, listens, shuts down cleanly

web/                   # React + Vite frontend (own package, tests and Dockerfile)
├── src/               # App, the API status and the map with its isolines layers
└── tests/             # Vitest tests: unit/, integration/, e2e/ (empty for now), helpers/

data/isolines/         # Pre-generated isolines the API serves, one file per eclipse
data/nasa-paths/       # NASA's path table per eclipse, to generate and test against
data/airports.geojson  # Airports snapshot the API serves
scripts/               # data:isolines, data:airports, fixtures:jsex
tests/                 # Vitest tests: unit/, integration/, e2e/ (empty for now), helpers/, fixtures/
docs/plans/            # Plans, deviations and findings

The repo is a pnpm workspace: the root package (engine and API) and web/ each have their own package.json and dependencies, and share one lockfile. Run a script in web/ from the root with pnpm --filter web <script>.

About

Totality duration isolines and map for total solar eclipses

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages