npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openmeteo/weather-map-layer

v0.3.0

Published

Weather Map Layer for MapLibre/Mapbox GL JS powered by Open-Meteo OMfiles

Downloads

4,663

Readme

Open-Meteo Weather Map Layer

codecov Linting & Tests GitHub license npm version

⚠️ 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'
	});

	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'
		});

		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 serve

This 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 the interpolation URL 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, OpenLayers and CesiumJS. Each adapter provides addProtocol / removeProtocol plus factory methods for creating map-library-native source or layer objects. See examples/leaflet, examples/openlayers, examples/mapbox and examples/cesium.

  • Leaflet – addLeafletProtocolSupport(L) gives createTileLayer and createVectorTileLayer, both plain L.GridLayers. They default to tileSize: 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. Pass tileSize: 256, zoomOffset: 0 for 256 px tiles.
  • OpenLayers – addOpenLayersProtocolSupport(ol) gives createRasterSource (a DataTile source for WebGLTile layers) and createVectorTileSource (an MVT VectorTile source).
  • Mapbox GL JS – addMapboxProtocolSupport() gives createRasterSource, a type: 'custom' raster source for map.addSource, and addVectorSource(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 the layer property, e.g. ['==', ['get', 'layer'], 'wind-arrows'], instead of source-layer.
  • CesiumJS – addCesiumProtocolSupport(Cesium) gives createImageryProvider and createVectorImageryProvider, both resolving to an ImageryProvider once the TileJSON is known; add them with viewer.imageryLayers.add(Cesium.ImageryLayer.fromProviderAsync(provider)). Cesium has no vector tile renderer, so the vector provider draws the arrows, barbs or contours onto raster tiles with a style function, like the Leaflet vector tile layer. Do not limit the data window with updateCurrentBounds in Cesium: it requests the root tiles and the ancestors of what it shows, which cover the whole domain anyway, plus tiles beyond the view while the camera moves, and keeps them all. A window would save nothing and leave those tiles partly empty until their level changes.

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 the dark=true URL 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.

Derivation rules

Some variables are not stored in the .om files but computed while reading them: wind speed and direction come from the u/v components, wave height is paired with the wave direction, and vapour_pressure_deficit, wet_bulb_temperature_2m and apparent_temperature are computed from the 2 m temperature and humidity (the Magnus formula for the dew point, Stull's fit for the wet-bulb temperature and Steadman's apparent temperature, as the Open-Meteo backend computes them). Each of these is a derivation rule, matching the requested variable name and listing the variables it is read from.

What a model stores differs: snowfall is stored by some models and derived from snowfall_water_equivalent on others, wind comes as u/v components or as speed and direction, and ECMWF stores the dew point where other models store the relative humidity. A rule therefore sees the variables of the file it reads from and decides whether it applies. The stored field wins over a derived one, and a derived field needs all of its sources in the file (the first file of a run has no accumulations yet). Where not stored, the defaults derive snowfall, rain, dew_point_2m, relative_humidity_2m, shortwave_radiation, direct_radiation, diffuse_radiation and wind_speed_*/wind_direction_* from the components, and the humidity-derived fields take the dew point, the wind speed or the direct and diffuse radiation where a model stores those instead. The precipitation family (precipitation, rain, showers, snowfall, snowfall_water_equivalent and their ensemble _mean and _spread), which the files store as sums over each time step, is always shown per hour (mm/h, cm/h for snowfall), stored or derived, so a series of maps reads the same across a model's hourly, 3-hourly and 6-hourly steps. That needs the length of the file's time step, which the om protocol takes from the delta_time_hours parameter of the om URL: the host application sets it from the run's valid times (the gap to the previous valid time; the first step takes the gap to the next one), and a Capture API URL (latest.json, in-progress.json) gets it set while resolving, from the valid times it fetches anyway.

The defaults are exported as defaultDerivationRules; own rules are passed through fileReaderConfig:

// Fresh snow from the water equivalent with a ratio that follows the
// temperature, instead of the fixed 0.7 cm per mm of the default rule.
// Listed first, this rule takes `variable=snowfall` over.
const snowfallRule = {
	pattern: /^snowfall$/,
	provides: { directions: false, barbs: false },
	scaleFactor: 'primary',
	// A model storing snowfall itself keeps it; otherwise both sources are needed
	getSourceVars: ({ variable, stored }) =>
		!stored.has(variable) && stored.has('snowfall_water_equivalent') && stored.has('temperature_2m')
			? ['snowfall_water_equivalent', 'temperature_2m']
			: null,
	process: ([waterEquivalent, temperature]) => ({
		// 1 cm per mm near freezing, up to 2 cm per mm of cold, dry snow
		values: waterEquivalent.map((mm, i) => mm * Math.min(2, Math.max(1, 1 - temperature[i] / 10))),
		directions: undefined
	})
};

const omProtocolOptions = OMWeatherMapLayer.defaultOmProtocolSettings;
omProtocolOptions.fileReaderConfig = {
	...omProtocolOptions.fileReaderConfig,
	// The first rule claiming a name wins, so own rules go in front of the defaults.
	derivationRules: [snowfallRule, ...OMWeatherMapLayer.defaultDerivationRules]
};

A string pattern matches anywhere in the name, which is why the rule above anchors a RegExp: a plain 'snowfall' would also claim snowfall_water_equivalent. getSourceVars receives the requested name and the set of variables stored in the file; returning null passes the name on to the next matching rule, and when no rule claims it the variable is read as stored. A rule lists as many source variables as it needs: a single one to convert units, two for the u/v pairs, more to combine fields. They all have to live in the same file and share its dimensions, and process receives their data in the order getSourceVars returns, along with the same file context. A rule producing a member of the precipitation family returns the per-step sum; the reader divides it by the step after the rule. A rule needing the step for anything else sets usesTimeStep and gets it in hours as context.timeStepHours. variableUsesTimeStep(variable) tells a host which variables need delta_time_hours, and timeStepHoursAt(validTime, validTimes) computes it from a run's valid times; a direct readVariable call passes it as its last argument, and a read of such a variable without it is rejected.

provides is declared up front because it is needed before any data is read: directions: true makes a variable eligible for arrows, and barbs: true additionally marks its values as a wind speed, since barbs encode knots. Both are also queried through variableHasDirections(variable, rules) and variableSupportsBarbs(variable, rules), for a UI that offers arrow styles. They are answered from the first rule matching the name, before any file is open, so rules matching the same name have to agree on them.

Since the rules replace the defaults rather than extending them, leaving out defaultDerivationRules is how a default rule is dropped. The list is read once, when the protocol creates its shared file reader, so it has to be set before the first tile request.

  • examples/custom-derivation-rules.html – snowfall from the water equivalent with a temperature-dependent ratio, in front of the default rule.

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 .om tile 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_spatial is the raw AWS S3 endpoint, used throughout this README and the examples. The official maps app uses https://data-spatial.open-meteo.com/data_spatial, which only accepts requests with a localhost or *.open-meteo.com referer.

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.