@openmeteo/weather-map-layer
v0.2.1
Published
Weather Map Layer for MapLibre/Mapbox GL JS powered by Open-Meteo OMfiles
Readme
Open-Meteo Weather Map Layer
⚠️ Notice This package is still under construction and is not yet fully production-ready. API changes may occur and some features might be incomplete.
Overview
This repository serves as a demonstration of the Open-Meteo File Protocol (om://) with Mapbox / MapLibre GL JS. The om:// scheme is a custom MapLibre protocol registered via addProtocol. The .om files are hosted on an S3 bucket and can be accessed directly through the protocol handler.
The core weather data generation and API is hosted in the open-meteo/open-meteo repository.
An interactive demo is available at maps.open-meteo.com.
Installation
Node
npm install @openmeteo/weather-map-layer// ...
import { omProtocol } from '@openmeteo/weather-map-layer';
// Standard MapLibre GL JS setup
// ...
maplibregl.addProtocol('om', omProtocol);
const omUrl = `https://openmeteo.s3.amazonaws.com/data_spatial/dwd_icon/latest.json?variable=temperature_2m`;
map.on('load', () => {
map.addSource('omFileSource', {
url: 'om://' + omUrl,
type: 'raster',
maxzoom: 12 // tiles look pretty much the same below zoom-level 12, even on the high res models
});
map.addLayer({
id: 'omFileLayer',
type: 'raster',
source: 'omFileSource',
paint: {
'raster-opacity': 0.75
}
});
});Two files ship next to the module and are referenced with new URL(..., import.meta.url): the tile render worker (dist/worker.js) and the file reader's WebAssembly binary (dist/om_file_format.web.wasm). Bundlers that understand that pattern (Vite, webpack 5, Rollup, Parcel) copy them into their output as assets without configuration. The worker starts with the protocol; the binary is fetched on the first data read, not at import time.
HTML / UNPKG
The package ships as an ES module only, so load it from a <script type="module">. The render worker and the .wasm binary are fetched from the same directory; the worker is started through a same-origin blob when the module comes from another origin, as a worker script itself must be same-origin. For a standalone example, see examples/temperature.html.
<script type="module">
import * as maplibregl from 'https://unpkg.com/[email protected]/dist/maplibre-gl.mjs';
import * as OMWeatherMapLayer from 'https://unpkg.com/@openmeteo/[email protected]/dist/index.mjs'; // x-release-please-version
// Standard MapLibre GL JS setup
// ...
maplibregl.addProtocol('om', OMWeatherMapLayer.omProtocol);
const omUrl = `https://openmeteo.s3.amazonaws.com/data_spatial/dwd_icon/latest.json?variable=temperature_2m`;
map.on('load', () => {
map.addSource('omFileSource', {
url: 'om://' + omUrl,
type: 'raster',
maxzoom: 12 // tiles look pretty much the same below zoom-level 12, even on the high res models
});
map.addLayer({
id: 'omFileLayer',
type: 'raster',
source: 'omFileSource',
paint: {
'raster-opacity': 0.75
}
});
});
</script>Development
The examples can be served locally, providing direct access to the bundled assets and data files. To launch the development server, execute:
npm run serveThis command initiates a lightweight static server, enabling the interactive demos to be viewed in a browser while reflecting any code changes in real time.
Examples
Raster sources
The repository contains an examples directory with ready-to-run demos:
examples/temperature.html– shows temperature data from an OM file.examples/precipitation.html– displays precipitation using a similar setup.examples/wind.html– displays wind values, for arrows overlay see Vector sources.examples/combined-variables.html– shows multiple data sources on the same map.examples/partial-requests.html– demonstrates partial / incremental data requests.examples/interpolation.html– switch the interpolation method live via theinterpolationURL parameter.
Run the examples by opening the corresponding .html file in a browser.
Vector sources
For directional arrows / contouring / gridpoints, an additional source must be added, since these features use vector tiles instead of raster tiles.
...
map.on('load', () => {
map.addSource('omFileVectorSource', {
url: 'om://' + omUrl,
type: 'vector'
});
map.addLayer({
id: 'omFileVectorLayer',
type: 'line',
source: 'omFileVectorSource',
'source-layer': 'contours',
paint: {
'line-color': 'black',
'line-width': 4
}
});
});For the vector source examples there is the examples/vector sub-directory with ready-to-run demos:
examples/vector/grid-points.html– displays all grid points for a model, with value data on each point.examples/vector/temperature-anomaly.html– shows a seasonal forecast map with temperature anomalies.examples/vector/temperature-labels.html– displays all grid points for a model, using value data to show temperature labels.examples/vector/wind-arrows.html– displays wind map with directional arrows.
Framework Adapters
The core omProtocol handler is designed for MapLibre GL JS, but this package also ships adapters for Mapbox GL JS, Leaflet and OpenLayers. Each adapter provides addProtocol / removeProtocol plus factory methods for creating map-library-native source or layer objects. See examples/leaflet, examples/openlayers and examples/mapbox.
- Leaflet –
addLeafletProtocolSupport(L)givescreateTileLayerandcreateVectorTileLayer, both plainL.GridLayers. They default totileSize: 512, zoomOffset: -1: the protocol renders 512 px tiles and sizes its arrow/barb lattice for them, so this reproduces the MapLibre look 1:1. PasstileSize: 256, zoomOffset: 0for 256 px tiles. - OpenLayers –
addOpenLayersProtocolSupport(ol)givescreateRasterSource(aDataTilesource forWebGLTilelayers) andcreateVectorTileSource(an MVTVectorTilesource). - Mapbox GL JS –
addMapboxProtocolSupport()givescreateRasterSource, atype: 'custom'raster source formap.addSource, andaddVectorSource(map, sourceId, url), which keeps a GeoJSON source in sync with the vector tiles of the viewport (Mapbox custom sources carry raster data only). Style layers select their features with a filter on thelayerproperty, e.g.['==', ['get', 'layer'], 'wind-arrows'], instead ofsource-layer.
Cross-origin isolation
The protocol shares the decoded weather data with its render workers through SharedArrayBuffer, which browsers only enable on cross-origin isolated pages. Serve your page with
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp(require-corp is what the Open-Meteo maps app uses; every cross-origin resource then needs CORS or a Cross-Origin-Resource-Policy header. credentialless is a more lenient alternative where supported.) Without these headers the protocol still works, but every tile request has to copy the whole variable into a worker, which blocks the main thread noticeably while panning and zooming. npm run serve sends them (scripts/serve-examples.js); crossOriginIsolated in the console tells whether a page has them.
Maptiler SDK
The Maptiler SDK natively supports addProtocol, but unfortunately it mangles the param url, by removing the second :, this small snippet will fix that:
// MapTiler SDK mangles URLs like `om://https://...` into `om://https//...`
maptilersdk.addProtocol('om', (params, abortController) => {
params.url = params.url.replace('https//', 'https://');
return omProtocol(params, abortController, omProtocolSettings);
});Contouring
examples/vector/contouring/contouring-pressure.html– shows how to use contouring with a pressure map.examples/vector/contouring/contouring-on-colorscale.html– shows how to use contouring to follow the breakpoints in the colorscale.examples/vector/contouring/custom-contouring-intervals.html– shows how to use contouring with a custom contouring interval.
Colors
If you’re rendering tiles on a dark base‑map or simply want to experiment with alternative color schemes, the documentation includes several example pages that illustrate all the available color‑scale options:
examples/colorscales/darkmode.html– demonstrates thedark=trueURL parameter, which automatically switches to palettes fine‑tuned for dark backgrounds.examples/colorscales/custom-rgba.html– shows how to build a linear gradient from a user‑defined array of RGBA values.examples/colorscales/custom-breakpoints.html– demonstrates how to insert your own breakpoints into the scale definitions.
Callbacks
In scenarios where post‑loading transformations of weather data is required, the protocol provides a post‑read callback. This callback is invoked immediately after the data has been parsed by omFileReader, allowing transformations before the data is forwarded to the rendering pipeline. A typical usage pattern is illustrated below:
const omProtocolOptions = OMWeatherMapLayer.defaultOmProtocolSettings;
omProtocolOptions.postReadCallback = (omFileReader, data, state) => {
if (data.values) {
data.values = data.values?.map((value) => value / 100);
}
};
maplibregl.addProtocol('om', (params, abortController) =>
OMWeatherMapLayer.omProtocol(params, abortController, omProtocolOptions)
);An example implementation with a useful case is available in the examples/callbacks sub-directory.
Clipping
To restrict weather data to a geometric boundary, the clipping parameters can be supplied during the instantiation of the omProtocol.
const omProtocolOptions = OMWeatherMapLayer.defaultOmProtocolSettings;
omProtocolOptions.clippingOptions = {
geojson: geojson, // optionally clip weather data to geojson
bounds: clipBbox // optionally limit tile generation to bbox bounds, automatically generated from geojson when left blank
};
...The clipping examples require npm run serve, as they load GeoJSON files over HTTP.
examples/clipping/raster/clip-switzerland.html– Demonstrates temperature raster data clipped to the geographical contour of Switzerland.examples/clipping/arrows/clip-italy.html– Shows wind velocity raster and vector arrow fields clipped to the contour of Italy.examples/clipping/contours/clip-france.html– Illustrates temperature and isocontour overlays confined to the French boundary.examples/clipping/bounds/clip-germany-bounds.html– Restricts tile generation to a bounding box around Germany.examples/clipping/oceans/clip-oceans.html– Depicts the exclusion of oceanic regions from a global model, thereby hiding weather data on ocean surfaces.
Capture API
⚠️ Using the Capture API will add 0.5-1s delay for each request, because it must first fetch a metadata JSON file to resolve the latest model run before requesting the actual
.omtile data.
Because the use of OM files on the S3 storage is often quite ambiguous, a Capture API is added, that will automatically produce the correct file paths for you.
Note on endpoints:
https://openmeteo.s3.amazonaws.com/data_spatialis the raw AWS S3 endpoint, used throughout this README and the examples. The official maps app useshttps://data-spatial.open-meteo.com/data_spatial, which only accepts requests with alocalhostor*.open-meteo.comreferer.
For each Weather Model, there will be a latest.json and in-progress.json metadata file, containing data like valid time steps, valid variables and reference times.
An example can be found here, for DWD Icon Global:
https://openmeteo.s3.amazonaws.com/data_spatial/dwd_icon/latest.json{
"completed": true,
"last_modified_time": "2025-11-11T09:42:17Z",
"reference_time": "2025-11-11T06:00:00Z",
"valid_times": ["2025-11-11T06:00Z", "2025-11-11T07:00Z", "...+91"],
"variables": ["cape", "cloud_cover", "cloud_cover_high", "...+120"]
}Using the Capture API
If you don't want to select a particular model run, but instead always want to use the latest available run. Instead of using the model run in the URL you replace that part with latest.json
For example, with the link below replace the highlighted part:
With latest.json:
If you want to show the closest current time, or a pick a different valid time than the first one, you could use:
or the 5th index of the valid_times array
Time Step Modifiers
The modifier suffix on current_time_ controls the rounding granularity when snapping to the nearest available time step. For example, current_time_1H rounds to the nearest hour, while current_time_30M rounds to the nearest 30 minutes.
| modifier | Alteration | | -------- | ---------- | | M | Minutes | | H | Hours | | d | Days | | m | Months |
Seamless Composite Domains
Several domains support a seamless mode: a composite of a base model and one or more finer regional ones. At every point the finest model that is active at the current zoom level and has data there is shown, so the map switches to a finer model over its area and falls back to the coarser one elsewhere.
The protocol handler resolves the seamless domain on the client side: the latest.json metadata is fetched from the base model, every layer is requested at the base model's run and time step, and tiles are composited per pixel from the active sub-domains. Regional models with a shorter forecast horizon drop out past it (maxForecastHours), so a composite stays valid for the full range of its base model.
Available seamless domains
| Domain value | Constituent models (finest → base) |
| ---------------------- | ---------------------------------------------------------------------------------------------------------- |
| dwd_icon_seamless | dwd_icon_d2 (zoom 3+) → dwd_icon_eu (zoom 2+) → dwd_icon |
| ncep_gfs_seamless | ncep_hrrr_conus (zoom 2+) → ncep_gfs025 |
| meteofrance_seamless | meteofrance_arome_france0025 (zoom 3+) → meteofrance_arpege_europe (zoom 2+) → meteofrance_arpege_world025 |
| cmc_gem_seamless | cmc_gem_hrdps_west (zoom 4+) → cmc_gem_hrdps (zoom 3+) → cmc_gem_rdps_10km (zoom 2+) → cmc_gem_gdps_15km |
| jma_seamless | jma_msm (zoom 3+) → jma_gsm |
| ukmo_seamless | ukmo_uk_deterministic_2km (zoom 3+) → ukmo_global_deterministic_10km |
| knmi_seamless | knmi_harmonie_arome_netherlands (zoom 3+) → knmi_harmonie_arome_europe |
| chmi_seamless | chmi_aladin_cz_1km (zoom 3+) → chmi_aladin_central_europe_2km |
A ready-to-run example is available at examples/seamless.html.
License
This project is licensed under the GNU General Public License v2.0.
