# 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

| Layer | Gateway endpoint | Page |
|---|---|---|
| `EarthEventsLayer` | `GET /v1/events` | [/docs/api/v1/events](/docs/api/v1/events) |
| `WeatherLayer`, `WindLayer` | `GET /v1/weather/frames` and its frame files | [/docs/api/v1/weather/frames](/docs/api/v1/weather/frames) |
| `AuroraLayer` | `GET /v1/space-weather` and its grid | [/docs/api/v1/space-weather](/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](/docs/maps) for the rest of the web SDK.
