Skip to content
Documentation menu
docs / live-layers · raw .md

Live layers: events, weather, wind and aurora (@mapmap/maps/live)

@mapmap/maps 0.23.0 adds a subpath, @mapmap/maps/live, with four layers for a MapMap map: earthquakes, volcano alerts and fire detections (EarthEventsLayer), forecast colour fields and pressure lines (WeatherLayer), wind particles (WindLayer) and the aurora forecast (AuroraLayer). They draw on the globe and on the flat map, to the poles, and each one reads the keyed gateway by default.

ts
import { createMap } from "@mapmap/maps";
import { AuroraLayer, EarthEventsLayer, WeatherLayer, WindLayer } from "@mapmap/maps/live";

const map = createMap({ container: "map", apiKey: "snk_browser_...", projection: "globe" });
const weather = new WeatherLayer(map, { field: "rain" });
const wind = new WindLayer(map);
const aurora = new AuroraLayer(map);
const events = new EarthEventsLayer(map, { onReport: (r) => console.log(r.quake.label) });
for (const layer of [weather, wind, aurora, events]) layer.start();

The subpath is never re-exported from the root, so an app that never draws a live layer does not grow. It imports neither three nor pmtiles. It is not in React Native or Points yet.

What they read

LayerGateway endpointPage
EarthEventsLayerGET /v1/events/docs/api/v1/events
WeatherLayer, WindLayerGET /v1/weather/frames and its frame files/docs/api/v1/weather/frames
AuroraLayerGET /v1/space-weather and its grid/docs/api/v1/space-weather

Each request is a Standard call, one unit (each frame file counts as one request), and a browser key may read them. The key goes in an Authorization: Bearer header on every call to the map's baseUrl (never in the URL), and is never sent to any other origin: liveFetch(url, { apiKey, baseUrl, signal }) is the one fetch the layers use, and it rejects on a non-2xx answer with the problem+json code in the message. DEFAULT_LIVE_LABEL is the label a layer reports before anything vouches for its feed ("Feed status unknown"). LiveFetchOptions is its options type.

A host that serves its own copies passes source: explicit URLs that win over the gateway (a baseUrl and apiKey option also override the map's). The pressure lines come from keyed tiles, so their tile URL carries the key as api_key (a map tile request cannot carry a header).

Shared behaviour

Every layer has start() (safe before the style has loaded; re-installs after setStyle), stop() (stops polling; what is drawn stays), remove() (removes every layer, source, image and listener; safe to call twice), ids, report() and onReport. Options id (a prefix), slot ("bottom", "middle" by default, "top") and beforeId (a layer id, or a function giving one, which wins over the slot) say where it sits.

Each layer adds its credit to the map's one credit line while something is drawn, and removes it with the layer. Nothing says or implies endorsement by or affiliation with USGS, NOAA or NASA. Reduced motion is honoured: no pulses, no particles (static arrows), no breathe.

EarthEventsLayer

Earthquakes (a ring sized by magnitude, with one slow 6 second ring for a quake first seen this session and under 2 hours old), volcano alerts (a muted amber triangle) and fire detections (a heat glow at globe zoom, then coral dots from zoom 4). Calm by rule: no red, no flashing, no sound.

ts
const events = new EarthEventsLayer(map, {
  types: ["quake", "volcano", "fire"], // EVENT_TYPES, the default
  minMagnitude: 2.5,
  base: "dark",                        // EventsBase: "light" (default) or "dark"
  popup: (detail) => `<strong>${detail.kind}</strong>`, // EventDetail
  onSelect: (detail) => console.log(detail),
});
events.start();
events.setTypes(["quake"]);

Options are EarthEventsLayerOptions; EventType is "quake" | "volcano" | "fire". EarthEventsSource is either { url } (a GET /v1/events URL) or { files, status } (the three box files and status.json). onReport receives an EarthEventsReport with one EarthEventsFeedReport per type: status (ok, stale, unknown, error), count, fetchedAt, ageS and a plain label ("48 today", "6 alerts", "23,499 detections", "Unavailable", "Loading"). A stale feed is hidden and reads "Unavailable", so an old quake is never drawn as today's.

Honest labels, always on the data:

  • Fire cells are derived, never raw detections: FIRES_HONEST reads "Satellite-detected heat, not confirmed fires", and a fire EventDetail carries it as label.
  • VOLCANOES_HONEST reads "US-monitored volcanoes only": an absent alert says nothing about volcanoes elsewhere. A volcano EventDetail carries it as coverage.
  • Reported events for display. Not a warning service and not safety-critical.

EVENTS_SHORT_CREDITS holds the credits the sources carry ("Earthquakes: USGS", "Volcanoes: USGS, NOAA NCEI", "Fires: NASA FIRMS"). quakeRadius(magnitude) is the ring's radius in pixels and timeAgo(then, now) reads "38 min ago".

WeatherLayer

NOAA GFS forecast fields (WEATHER_FIELDS: rain, temperature, cloud, gusts) and pressure lines, over the 17 frames (48 hours, every 3) of the newest run. WeatherField names a field.

ts
const weather = new WeatherLayer(map, { isobars: true });
weather.start();            // shows the frame nearest now; never plays
weather.setField("rain");
weather.setTime(Date.parse("2026-10-18T09:00:00Z")); // the nearest frame; null is now
weather.play(true);         // 750 ms a frame, 1.5 s hold on the last, loops

Nothing autoplays: the viewer presses play. Half-resolution frames are drawn at globe zoom and on touch screens, full ones from zoom 4 on a pointer device, with a crossfade across the swap (preferHalf(zoom, touch) is the rule). A frame that cannot be loaded stops playback and appears in report().failed; a field with no file for the hour appears in report().unavailable with plain words. The PMTiles fallback is not part of the layer.

WeatherLayerOptions, WeatherSource ({ manifestUrl, base, tiles? }), WeatherFrame ({ run, hour, validAt, key }), WeatherManifest and the report type WeatherReport (status, manifestStatus, field, isobars, frames, index, playing, resolution, failed, unavailable, legend, label). FieldEncoding is how a field's PNG decodes (decodeValue(r, g, b, encoding) gives the value), FieldStop is one stop of a colour ramp and fieldStops(field, base) returns the design ramp. nearestFrameIndex(frames, atMs) is the one rule every layer uses for time.

Honest label, on every report: "Forecast, not an observation". The data is NOAA GFS, processed by MapMap (regridded, quantised and thinned); WEATHER_SHORT_CREDIT is "Weather: NOAA GFS forecast, processed by MapMap", shown as "Weather: NOAA GFS".

WindLayer

Wind particles from the same GFS frames, drawn on a GPU state texture (65,536 particles on a desktop, WIND_PARTICLES_DESKTOP; 16,384 on touch, WIND_PARTICLES_TOUCH). A new frame crossfades the velocity in 280 ms with no particle reset.

ts
const wind = new WindLayer(map, { base: "dark" });
wind.start();
wind.setTime(null);        // the frame nearest now
wind.setDim(windDimUnderField(map.map.getZoom())); // dim the trails under a colour field
const here = wind.sample([0.12, 51.5]); // { u, v, speedMs, headingDeg } or null

Under reduced motion, or if a shader will not compile, it draws static arrows from the same field (fallback: "arrows", the default; "none" draws nothing) and report().reason says why (reduced-motion or gpu). WindLayerOptions, WindSource ({ manifestUrl, base }), WindReport (status, mode, reason, frame, resolution, label), WindFrameRef and WindMeta (the sidecar's ranges). pickWindFrame(manifest, atMs, base, { half }) chooses a frame, windSpeed(u, v) and windHeadingDeg(u, v) read a sample, and windDimUnderField(zoom) is the alpha the page gives wind under a field. Same honest label and credit as the weather layer.

AuroraLayer

The NOAA SWPC OVATION aurora forecast, about 30 to 90 minutes ahead, drawn on the night side from a 360 by 181 grid: teal at 10 percent to pale green at 60 percent, at most 55 percent alpha, gated by where the sun is below the horizon (nightOnly, default on), with a 12 second breathe.

ts
const aurora = new AuroraLayer(map, { onReport: (r) => console.log(r.label) });
aurora.start();
aurora.setTime(Date.now() + 2 * 3_600_000); // drawn within 3 hours of now only

AuroraLayerOptions, AuroraSource ({ jsonUrl, pngUrl? }), AuroraReport and AuroraFile, which parseAuroraFile(body) reads from either the box's latest.json or the gateway's GET /v1/space-weather answer. auroraLabel(kp) gives "Quiet" under Kp 3, else "Kp 4". A forecast older than AURORA_STALE_MS (an hour) reads "Unavailable" and is not drawn; a scene time more than 3 hours from now reads "Now only" and is not drawn. subsolarPoint(atMs) and solarElevationDeg([lng, lat], atMs) are the sun almanac behind the night gate (about 0.5 degrees). AURORA_SHORT_CREDIT is "Aurora: NOAA SWPC forecast", shown as "Aurora: NOAA SWPC". A forecast, not an observation; no endorsement by or affiliation with NOAA is implied.

Licences and credits

  • NOAA GFS: data disseminated through the NOAA Open Data Dissemination programme is open to the public. "NOAA requests attribution for the use or dissemination of unaltered NOAA data. However, it is not permissible to state or imply endorsement by or affiliation with NOAA. If you modify NOAA data, you may not state or imply that it is original, unaltered NOAA data." MapMap regrids, quantises and thins it, so it is credited as processed by MapMap.
  • NASA FIRMS: "We acknowledge the use of data and/or imagery from NASA's Fire Information for Resource Management System (FIRMS) (https://earthdata.nasa.gov/firms), part of NASA's Land, Atmosphere Near real-time Capability for Earth observations (LANCE) (https://earthdata.nasa.gov/lance) and NASA's Earth Science Data and Information System (ESDIS)." Disclaimer: "The information presented through LANCE, GIBS, Worldview, and FIRMS are provided 'as is' and users bear all responsibility and liability for their use of data."
  • NOAA NCEI volcano locations: "Please cite this data/database as doi:10.7289/V5JW8BSH."
  • USGS earthquakes and volcano alerts, and NOAA SWPC aurora and Kp: US government works in the public domain; credit USGS and NOAA SWPC as the sources.
  • The wind particle engine is ported from mapbox/webgl-wind (ISC licence, Copyright (c) 2016, Mapbox). The notice ships in the npm package as THIRD-PARTY-NOTICES.md, and dist/live.js carries the copyright line.

Not in React Native or Points this wave.

See also Maps SDK for the rest of the web SDK.