Architecture & Core Concepts
Dependency graph
@openweather/maps-react
├── @openweather/maps-maplibre (optional peer, ./maplibre subpath only)
│ ├── @openweather/maps-core
│ └── @openweather/maps-gl
└── @openweather/maps-ui-core
@openweather/maps-ui
├── @openweather/maps-core
└── @openweather/maps-ui-core
@openweather/maps-ui-core is the shared, framework-agnostic view-model layer (legend, timeline, layer
grouping, overlays, point-query, query-flow, pins, layout, units, visuals) behind the SDK's simple
UI kit — the same modules back both @openweather/maps-ui (DOM) and @openweather/maps-react's ./ui (React), so
the two plain-styled renderers can't drift from each other.
@openweather/maps-corehas no MapLibre / React / WebGL dependency.@openweather/maps-ui-corehas no DOM / MapLibre / React / WebGL dependency — pure view-models, checked in CI the same way@openweather/maps-coreis (pnpm verify:core-isolation).@openweather/maps-gland@openweather/maps-maplibrepeer-depend onmaplibre-gl≥ 5.@openweather/maps-reactpeer-depends onreact/react-dom≥ 18 and, only for its./maplibresubpath, on@openweather/maps-maplibre+maplibre-gl— importing.or./uinever pulls MapLibre into your bundle.
Rendering pipeline
App (vanilla / React)
createWeatherMap() or <WeatherMap> + hooks / compounds
│
▼
@openweather/maps-maplibre
WeatherMapHandle · ScalarFlowRenderer · RasterLayerManager
ContourLayerManager · CycloneOverlay · LightningOverlay · CropMask
MultiTileScalarManager (internal) · PointQuerySampler (internal)
│ │
▼ ▼
@openweather/maps-core @openweather/maps-gl
WeatherEngine ScalarFlow plugin
LayerRegistry + catalog createScalarLayer
ProviderRegistry createParticlesLayer
Timeline / Search / Query createWavesLayer
SettingsStore (external) contour (CPU / worker / GPU)
shaders (WebGL2 + WebGL1)
│
▼
maps.openweathermap.org (Maps 3.0 tiles, radar, GeoJSON)
Design principles
- Core owns business state. Layer selection, date alignment, resolve/prefetch, and
entitlements all live in
@openweather/maps-core. UI and renderers subscribe; they never duplicate domain logic in component state. - Metadata-driven layers. Each layer is a
LayerMetadatarecord — path, format, color ramp, cadence, models, contour config, crop mask, entitlement key. The catalog is hardcoded in@openweather/maps-core(DEFAULT_LAYER_CATALOG); passavailableLayersto override it. There is no remote metadata endpoint. - Providers resolve assets.
tile|raster|geojsonproviders each return aResolvedLayerAsset(URL, date, lock status, scalar config). The MapLibre adapter paints whatever was resolved — it never talks to a data source directly. - Stores are
getSnapshot()+subscribe(). Every stateful piece (engine, settings, layers registry, timeline) follows this shape, which isReact.useSyncExternalStore-compatible by construction — see Events & State.
Data pipeline
Timeline / setDate / setActiveLayer
│
▼
WeatherEngine.resolveActiveLayer()
│
├── ProviderRegistry (tile | raster | geojson)
│ │
│ ├── buildLayerUrl / buildRasterTileUrl
│ ├── alignDateForLayer
│ └── probeLayerAsset (HEAD walk) → URL, or LayerLockedError (403) / LayerAuthError (401)
│
▼
ResolvedLayerAsset { url, date, locked?, tileGrid?, scalarConfig? }
│
▼
MapLibre adapter renderAsset()
├── ScalarFlowRenderer / MultiTileScalarManager
├── ContourLayerManager
├── RasterLayerManager
└── particle / waves / cyclone / lightning / crop mask sync
Auth & entitlements
- The API key (
appid) lives inSettingsStoreand is appended to tile requests (unless a layer opts out withappendAppId: false— some satellite URLs already carry their own key). - 403 on a tile/probe request →
LayerLockedError→ the engine emitslayer:lockedwith an optionalentitlementKey; the render path does not throw, so one locked layer never breaks the rest of the map. - 401 →
LayerAuthError(tile/probe) — the whole key is invalid, not just one layer, so it routes to the engine's genericerrorevent rather than the per-layer locked-layer UI. See Events & State. - Layer catalog: hardcoded
DEFAULT_LAYER_CATALOGin@openweather/maps-core. Override withavailableLayersoncreateCoreMapControllerif you need a subset or custom entries.
Time alignment & prefetch
alignDateForLayer snaps the scrubber date to the layer's real data cadence (floor10m /
floor30m / floorHour / hour, or an arbitrary updateCadenceMinutes). After each resolve, the
engine prefetches adjacent frames so playback/scrub hits already-resolved assets; an in-flight
prefetch is aborted the moment a new tick supersedes it.
Point query
PointQueryController (in @openweather/maps-core) is renderer-agnostic — it has no pixel access of its
own. The MapLibre adapter injects a sampler that:
- Maps lng/lat to a texel in the active scalar texture (or assembled multi-tile canvas).
- Reads the normalized channel.
- Runs
decodeScalarValuewith the layer's value scale + range. - Applies
displayScale/displayUnits/displayDigitsfor the UI (e.g. Pa → hPa).
Low-level technology
MapLibre GL JS v5, WebGL1 + WebGL2
MapLibre v5 can hand a custom layer either a WebGLRenderingContext or a WebGL2RenderingContext.
Every shader in @openweather/maps-gl therefore ships in two variants:
| Variant | Files | Notes |
|---|---|---|
WebGL2 (#version 300 es) |
*.vertex.glsl / *.fragment.glsl |
Primary path; VAOs, transform feedback |
| WebGL1 (GLSL ES 1.00) | *.gl1.*.glsl |
attribute/varying, texture2D, gl_FragColor |
detectCapabilities(gl) picks the program set once per onAdd. On WebGL1: attributes are
re-bound each frame (no VAO isolation), scalar interpolation caps at Bicubic (no Lanczos/RBF),
and the particle layer needs vertex-texture-fetch support — if MAX_VERTEX_TEXTURE_IMAGE_UNITS is
0, particles emit renderer:degraded and draw nothing rather than garbage.
Data-encoded tiles (Maps 3.0)
Unlike classic colored PNG weather tiles, Maps 3.0 serves data-encoded images. Pixel channels
store normalized physical values; the client loads the image, uploads it as a WebGL texture,
decodes + colorizes in the fragment shader against the layer's color ramp and value scale, and
optionally runs particle/wave/contour passes from the same field. Default scalar base URL:
https://maps.openweathermap.org/maps/3.0/gfs025 (radar and other products use alternate bases).
Wind particles
Two update engines, chosen once via capabilities.hasTransformFeedback:
| Engine | Context | Position storage | Notes |
|---|---|---|---|
| Texture | WebGL1 (or WebGL2 fallback) | RGBA8 ping-pong textures (~16-bit/axis) | Fragment update via FBO; draw VS samples a position texture |
| Transform feedback | WebGL2 | float32 VBOs | Update VS + RASTERIZER_DISCARD; no FBO; no 16-bit jitter |
A shared screen-space trail-fade pass is identical for both engines. Particle budget scales with
zoom and getDeviceTier() (high | medium | low).
Contours / isolines
- Decode the scalar field from the displayed image (
prepareContourField). - Extract isolines — CPU marching squares, optionally off the main thread in a Web Worker, or via WebGPU where available.
- Stitch segments into polylines, with optional smoothing and progressive assembly.
- Render as a MapLibre GeoJSON line + label layer, with optional H/L extrema markers.
Multi-tile assembly & radar pyramids
The global overview is typically tile 0/0/0. On zoom-in, MultiTileScalarManager fetches a grid
(or pyramid), stitches it into one canvas, and feeds it to ScalarFlow as a single texture. Missing
frames are found via a HEAD probe with a per-model policy (offset, step, max steps) — radar walks
back in 10-minute steps, for example. Fetches are cancellation-aware and cache-shared: a superseded
selection aborts its own in-flight tile fetches without breaking a still-live caller sharing the
same URL, and ImageBitmap results are explicitly disposed on LRU eviction and cache clear.
Repository layout
This SDK lives in one pnpm-workspace monorepo rather than per-package repos + submodules — atomic
changes across core/adapter/React, one CI pipeline, and Changesets-aligned semver across all six
packages. See the repo's docs/ARCHITECTURE.md for the full rationale, including why the WebGL
renderer (@openweather/maps-gl) was internalized from a formerly-standalone sibling library.