Skip to content
← Back to the map

docs

darkmap — a planning surface for instrumentation, sensor, astronomy, and low-light field work. Pick the lens that matches your question; every tool stays reachable from every lens.

  • Sky

    Astro instrumentation & astrophotography

    “When and where is tonight dark, clear, and steady enough for my target?”

    VIIRS + Falchi radiance → Bortle / SQM · ephemeris & time helix · SkyCompass · DEM horizon · T(λ) extinction

    Open the Sky lens
  • Air

    Weather, pollen & smog analyst

    “Is the air getting better or worse here, do the sources agree, and when is the exposure window?”

    Atmosphere overlays · PM2.5 field · multi-pollutant AQI · OpenAQ ↔ CAMS cross-validation · history · the /aq dashboard

    Open the Air lens
  • Links

    Laser / RF instrumentation technician

    “Given my hardware and this geometry, does the link close tonight — with margin?”

    Directable boresight (az/el) · T(λ) + live Mie + HITRAN bands · path-integrated AOD · beam footprint · link budget

    Open the Links lens
  • Orbit

    LEO instrument setup & ground-station Tx

    “When does a satellite pass over my site, is it feasible over my real horizon, and what is the az/el track?”

    TLE / SGP4 pass prediction · slant geometry & airmass · DEM horizon occlusion · T(λ) vs elevation · sub-satellite footprint

    Open the Orbit lens

What this tool is for

darkmap is one map with four lenses, one per audience. Pick the lens that matches your question; every tool stays reachable from every lens — the lens only re-weights what leads, it never hides anything:

  • ◐ Sky — astronomers & astrophotographers: VIIRS + Falchi radiance → Bortle / SQM, the time helix + sky compass, and the real DEM horizon for a dark-sky site.
  • ☁ Air — weather, pollen & smog analysts: GIBS cloud / aerosol / water-vapor overlays plus a modeled PM2.5 field with NowCast-vs-24h AQI and OpenAQ ↔ CAMS cross-validation.
  • 📡 Links — laser / RF link technicians: a directable boresight, path-integrated AOD → spectral transmission T(λ), and a term-by-term link budget with a go / no-go margin.
  • 🛰 Orbit — LEO ground-station operators: SGP4 pass prediction gated by the real terrain horizon, the az / el track, Doppler, and the sub-satellite ground footprint.

The original problem statement was inspired by lightpollutionmap.info: a faster, calmer dark-sky planning surface without ad-tech or tracking. darkmap keeps that calm, ad-free spirit and widens it from dark-sky planning to the four jobs above.

Feature surface

  • VIIRS DNB radiance — NOAA annual composites 2012-2019, selected with a single-select year picker in the layer rail
  • Atmospheric overlays — NASA GIBS clouds (MODIS Terra, VIIRS NOAA-20), MODIS aerosol optical depth, and column water vapor, plus an OpenAQ ground-station PM2.5 layer with a kernel-diffusion estimate
  • Spectral transmission widget — T(λ) for the picked point with a plain-language "clearest window / worst band" takeaway; the local PM2.5 estimate can drive the aerosol input
  • Falchi 2016 World Atlas — the styled overlay, plus the raw mcd/m² radiance surfaced in the point-query readout (with Bortle / class mapping)
  • Per-view ephemeris — sun + moon position, the now-centered time helix (astro / nautical / civil twilight), sky compass with sun trajectory arc + moon position + atmospheric airmass
  • Real-terrain horizon — 36-ray, 10-distance raycast over AWS Mapzen Terrarium tiles; sun / moon altitude is reported relative to the local horizon polygon at its azimuth, and event times can be refined to true-horizon crossings
  • Per-viewport range pill — 4×4 grid sampling across the visible viewport so e.g. civil dusk "Δ 26 min" surfaces the spread when you're looking at a state-sized region
  • Geocoder — search for a place (typo-tolerant), or paste coords as decimal / DMS / DMM
  • Shareable URLs — the hash captures map view + active layers (with opacity) + basemap + ephemeris cursor

Sources, attribution, inspiration

darkmap was originally inspired by Jurij Stare's lightpollutionmap.info — a long-running public map surface for VIIRS and Falchi light-pollution data. It is an inspiration and comparison point, not an affiliation or primary data attribution. Scientific and software attributions are listed below:

NASA VIIRS DNB
Suomi-NPP Visible Infrared Imaging Radiometer Suite Day/Night Band. Composites by NOAA NCEI + the Earth Observation Group.
Falchi 2016 World Atlas
Falchi et al., Sci. Adv. 2 (2016) — "The new world atlas of artificial night sky brightness". mcd/m² radiance grid + classification.
Current WMS transport
Selected public light-pollution rasters currently load through a public GeoServer WMS transport. We proxy through /api/raster to normalize caching and keep ad-tech headers out of darkmap responses.
AWS Mapzen Terrarium
Free, global, RGB-encoded elevation tiles at s3://elevation-tiles-prod/terrarium/ — the input to the horizon-polygon raycaster.
Photon (komoot)
Typo-tolerant OSM-backed geocoder; data © OpenStreetMap contributors, ODbL. Proxied through /api/geocode.
astronomy-engine
cosinekitty's high-precision VSOP87 + lunar ephemeris, validated against USNO MICA to ~1 arcsec. The sun / moon / twilight math is theirs.
Carto Dark Matter / ESRI / OpenStreetMap
The three basemap options.

Scientific notes

VIIRS units

VIIRS DNB radiance is reported in nW · cm⁻² · sr⁻¹ on the upstream rasters. The styled composites apply a NOAA color ramp; the raw GRAY_INDEX values are what feed our Bortle-class mapping in the point-readout panel.

Falchi class boundaries

Falchi's six-class scheme — Pristine / Wilderness / Rural / Suburban / Urban / Inner City — maps onto mcd/m² thresholds. We surface the class label in the readout next to the raw radiance.

Airmass

The X chip in the sky compass uses Kasten & Young (1989) — better than plane-parallel sec(z) below ~30° altitude where atmospheric curvature dominates.

Horizon raycaster

Default 36 rays × 10 distance samples (250 m → 25 km). Each sample uses a refraction-adjusted earth-curvature drop (R_eff = 7/6 · R) when computing angular elevation. Polygon results are cached per (lat, lon) rounded to ~0.001° + options key; revisit is instant.

Orbit pass prediction

The Orbit lens propagates Celestrak two-line elements with SGP4 (satellite.js) and reports a pass only where the satellite clears the real DEM horizon at your site — the same raycaster above, not the flat 0° math horizon — so AOS / LOS, the az / el track, and the keyhole + Doppler readouts reflect the terrain. Predicted (SGP4), with the TLE epoch age surfaced so stale elements are never presented as fresh.

Atmospheric overlays + transmission

The Atmosphere section of the Layer rail surfaces four NASA GIBS raster overlays plus an OpenAQ ground-station PM2.5 station-density overlay. Drop a pin to see point-source RH / cloud cover / visibility via Open-Meteo, and tap the i chevron on any atmospheric row to open the transmission widget, which renders the spectral transmission curve T(λ) for the active inputs.

Data sources (V1)

  • NASA GIBS WMTS — clouds (MODIS Terra AM, VIIRS NOAA-20 PM), aerosol (MODIS Combined AOD @ 550 nm), water vapor (MODIS Terra infrared, 5 km). Public domain; "Imagery courtesy NASA EOSDIS GIBS" surfaces in the MapLibre attribution control when any GIBS layer is on.
  • GIBS dates are explicit. Atmospheric raster tiles request the current ephemeris UTC day via time=YYYY-MM-DD. Scrubbing within a day keeps the mounted tile source stable; crossing a UTC day remounts the active GIBS layers so the imagery, health pill, and cache key refer to the same product day. The server uses per-product publication-lag windows and falls back only to a bounded prior day when GIBS has not published yet.
  • Open-Meteo /v1/forecast — point RH, layered cloud cover, visibility. CC-BY 4.0, no key required; proxied through /api/atmospheric/point. PWV is treated as unavailable until a supported point source is wired.
  • OpenAQ v3 — PM2.5 ground stations in the viewport bbox, CC-BY 4.0. The map overlay shows station-observation density (heatmap + markers), with null readings treated as unknown rather than clean air. A separate kernel-diffusion model estimates point PM2.5 with a confidence signal — surfaced in the point readout and used to drive the transmission widget's aerosol input. Requires an OPENAQ_API_KEY env on the server; absent that, the proxy returns an empty FeatureCollection so the overlay renders nothing instead of throwing.
  • Celestrak GP / TLE — two-line element sets for the Orbit lens, proxied through /api/orbit/tle (no auth; the ~2-hour update cadence is respected via a server-side cache). Pass prediction runs satellite.js SGP4 in the browser, gated by the DEM horizon; the TLE-epoch age is surfaced on every pass so stale elements are never presented as fresh.

Transmission methodology

The transmission widget interpolates a pre-baked LUT shipped at /spectral-lut.json. The LUT axes are PWV (mm), AOD550, Ångström exponent, total ozone column (DU), and zenith angle; the wavelength grid spans ~0.3 µm → 30 µm.

Current LUT — smarts-analog-v1: improved engineering model in the spirit of SMARTS. Bodhaine 1999 Rayleigh with depolarization factor; Pierluissi-Maragoudakis 1986 water-vapor band model with τ ∝ ub·profile (b ≈ 0.78) capturing the column-scaling saturation a pure Gaussian misses; Bass-Paur Hartley + Huggins + Chappuis ozone; Kasten-Young airmass. Still an engineering estimate; expected error ~5-10 % in clean-sky cases.

V0 archive (PR-G): simpler analytical bake — pure λ−4.09 Rayleigh, Gaussian-only H₂O bands. The V0 model shipped first to land the LUT contract; V3a-2 swapped it for smarts-analog-v1 without touching the service or widget code.

V2: live aerosol recompute

The transmission widget surfaces an aerosol-type picker (Smoke / Dust / Urban / Pollen / Mixed). When a type is selected, the LUT-baked aerosol cell is bypassed and replaced with a live Bohren-Huffman 1983 Mie computation in the browser — `mie(x, n+ik)` integrated over the type's log-normal size distribution against published refractive-index data. The user-supplied AOD550 calibrates the magnitude; the spectral shape comes from the Mie integration. Sources: Reid 2005 (smoke), d'Almeida 1991 / OPAC (dust, mixed), Hess 1998 OPAC (urban), Griffiths 2012 (pollen). The pure-TS Mie core is validated against the geometric optics and Rayleigh limits.

Source-chip in the widget reads smarts-analog-v1+live-mie:<type> when the live aerosol path is active — making the model lineage explicit alongside the engineering-estimate disclaimer.

V3b: line-by-line for named bands

Beyond the coarse LUT, seven named astronomy bands have a high-resolution line-by-line bake at 0.01 nm sampling: the H₂O ρστ / Φ / ψ / Ω windows (940 / 1130 / 1380 / 1870 nm), the O₂ A-band (762 nm) and telluric O₂-X γ-band (628 nm), and the CO₂ ν₃ asymmetric-stretch fundamental at 4.3 µm. Clicking a band chip in the transmission widget opens an in-sheet detail panel showing the full Voigt profile across that window.

Line data is curated from HITRAN2020 (Gordon et al. 2022, JQSRT 277, 107949). The full catalog is gated behind free registration; we ship a representative subset in data/hitran/ with a documented regeneration workflow for users who want line-data refreshes. Voigt profile evaluation uses Thompson 1987 pseudo-Voigt against the Olivero-Longbothum 1977 combined HWHM — ~1 % accurate and fast enough for in-bake per-line per-wavelength evaluation.

Per-band JSONs ship at /spectral-lbl/[band-id].json and load lazily on first zoom. Total payload across all 7 bands is ~1.2 MB; only the bands the user actually clicks get fetched.

V3 next steps: the LUT JSON contract stays stable across model swaps. A future drop-in replaces spectral-lut.json with offline SMARTS (0.3 – 4 µm) + SBDART (4 – 30 µm) output against the US Standard Atmosphere. The LUT JSON contract and the shipped line-by-line bands above stay stable across model swaps; the widget code is unchanged across generations.

Caveats

  • Engineering estimate. V1 is good to within ±10 % at clean-sky standard atmospheres in the visible / NIR; the IR thermal bands beyond 5 µm get noticeably less accurate. For real instrument planning, cross-check against MODTRAN or libRadtran.
  • AOD defaults to 0.15 in the widget. When the PM2.5 overlay is on and a station is in range, a clicked point instead drives AOD from the modeled local PM2.5 estimate (shown with a confidence caption); pixel-sampling against the MODIS Combined AOD raster is still a follow-up. The displayed input chip shows the effective value.
  • Zenith defaults to 30°. Solar-zenith computation from the current ephemeris time + lat / lon is a follow-up.

Tech stack

  • SvelteKit (adapter-node), Svelte 5 runes, Skeleton 4.15.2, Tailwind v4
  • MapLibre GL JS 5 for the map surface
  • Effect.ts service layers — RasterClient, EphemerisClient, HorizonProvider, GeocoderClient
  • astronomy-engine (cosinekitty) for sun / moon math; satellite.js (SGP4) for LEO orbit propagation
  • Bazel 8 + Bzlmod via tinyland-inc/bazel-registry, with the MassageIthaca cache-attachment-contract pattern for RBE
  • OpenTofu and kustomize for the current deploy path. Runtime secrets stay outside the repository.

See the repo for source, issues, and contribution context.

Contact

darkmap is a personal project. The fastest paths to reach a human are GitHub for code and feature work, and the coordinated disclosure flow for anything security-shaped.

  • Bugs, requests, questions: open an issue on GitHub.
  • Security disclosures: file a private advisory — do not file public issues for vulnerabilities. See SECURITY.md for the response window and PGP key.
  • Tinyland context: this repo is one of several static spokes under tinyland.dev; cross-repo coordination happens on the tinyland-inc org.