mapbox-exif-layer
v1.3.4
Published
Wind particles, smooth weather raster display from JPEG/PNG, and GeoTIFF (scalar or RGB) visualization on Mapbox GL JS and MapLibre GL JS — no tile server
Maintainers
Readme
Mapbox EXIF Layer
Mapbox GL JS / MapLibre GL JS custom layers for wind particles, smooth weather rasters from JPEG/PNG, and client-side scalar or RGB GeoTIFF visualization — no tile server required.
Official site: https://www.mapbox-exif-layer.com/
At a glance
Three layer classes, four use cases:
| Use case | Class | Data Source | Visual |
| --- | --- | --- | --- |
| Wind | ParticleMotion | JPEG/PNG, scalar GeoTIFF | Flowing particle animation |
| Smooth weather display | SmoothRaster | JPEG/PNG only | Smooth gradients (linear texture filtering) |
| Scalar GeoTIFF preview | SmoothRaster | Scalar GeoTIFF | Native grid resolution; blocky when zoomed in |
| RGB / RGBA GeoTIFF | RgbGeoTiff | RGB GeoTIFF | True-color image layer |
Quick starts
Step-by-step guides by use case and map runtime:
| Use case | Mapbox GL JS | MapLibre GL JS | | --- | --- | --- | | Wind | Guide | Guide | | Smooth weather / scalar GeoTIFF | Guide | Guide | | RGB GeoTIFF | Guide | Guide |
Production note (scalar GeoTIFF preview & RGB GeoTIFF). Loading the entire GeoTIFF in the browser is intended for quick previews and small rasters — not production maps at many zoom levels or over large extents. For production, add a standard Mapbox/MapLibre raster tile source instead:
- Pre-generated MBTiles — build tiles with rio-mbtiles or QGIS Raster → Generate XYZ Tiles (MBTiles), then serve with mbtileserver. For scalar GeoTIFF, assign RGB(A) colors from the attribute first (e.g. rasterio + NumPy) before generating the tileset.
- COG + tile server — TiTiler (or GeoServer / similar) serves tiles directly from a Cloud Optimized GeoTIFF without pre-generating MBTiles.
- MapLibre serverless COG — host a COG on static storage and use maplibre-cog-protocol for range-request tiling in the client (no tile server).
GeoTIFF support (v1.3.1+).
ParticleMotionandSmoothRasteraccept scalar GeoTIFF sources (float32, EPSG:4326) in addition to JPEG/PNG. GeoTIFF rasters use physical cell values directly and do not require 0–255 normalization. See docs/geotiff-source.md. JPEG encoding is in docs/jpeg-source.md.
JPEG/PNG without EXIF (v1.3.2+). For normalized JPEG or PNG files that omit EXIF
ImageDescriptionmin/max metadata, passscalarValueRangeonSmoothRasterorvelocityRangeonParticleMotion. When EXIF is present, EXIF takes precedence. See docs/jpeg-source.md.
- Native custom layer integration (Mapbox GL JS or MapLibre GL JS) — not a canvas overlay
- No tile server — serve a static JPEG/PNG or GeoTIFF from a URL (e.g. S3); JPEG/PNG must be a properly encoded weather grid, not an arbitrary photo
- MapLibre globe projection (v1.2.0+) — set
mapRuntime: 'maplibre'on each layer; see maplibre-gl-demo - GPU-accelerated wind particles via transform feedback — no per-frame CPU loop over hundreds of thousands of points
Demos
- US wind & temperature demo (source)
- Weather map time slider demo (source)
- MapLibre GL JS globe projection demo (source)
- Demo video — Southern California wind particles
- Demo video — US continental wind particle animation (v1.1.0)
- Demo video — MapLibre GL JS globe projection (v1.3.1)
- Technique explanation (Medium)
Installation
This package does not include a map SDK. Install one of the following, depending on which runtime you use:
Mapbox GL JS (default mapRuntime: 'mapbox'):
npm install mapbox-glMapLibre GL JS (set mapRuntime: 'maplibre' on each layer):
npm install maplibre-glMap projection. Mapbox GL JS — set
projection: 'mercator'when initializing the map (defaultmapRuntime: 'mapbox'does not support globe). MapLibre GL JS — setmapRuntime: 'maplibre'on every layer; globe projection (v1.2.1+) viamap.setProjection({ type: 'globe' }). Seemaplibre-gl-demo.
Then install this package:
npm install mapbox-exif-layerIf you use GeoTIFF sources (.tif / .tiff), also install the optional peer dependency:
npm install geotiffJPEG-only setups do not need geotiff.
Then import the layer classes in your JavaScript code:
import { ParticleMotion, SmoothRaster, RgbGeoTiff } from 'mapbox-exif-layer';Quick start
Minimal wind layer example (assumes EXIF ImageDescription on the JPEG — see docs/jpeg-source.md; without EXIF, pass velocityRange per Method 1):
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/dark-v11',
zoom: 7,
center: [-119.699944, 34.432546],
projection: 'mercator' // mapbox-gl-js: use mercator (not globe)
});
const windLayer = new ParticleMotion({
id: 'wind-particle',
source: 'path/to/wind.jpeg', // or a .tif / .tiff GeoTIFF URL
color: [[0, [0, 195, 255]], [20, [249, 243, 1]], [42, [128, 0, 0]]], // [speed, [r,g,b]] — see react-demo for full palettes
unit: 'mph',
bounds: [-121, 36, -117, 32], // required for JPEG/PNG; optional for GeoTIFF (read from file)
readyForDisplay: true
});
map.on('load', () => map.addLayer(windLayer));More examples
- Per use case — Quick starts table above
- Multi-layer app —
react-demo/react-demo(wind + temperature + humidity + precipitation) - Time slider —
react-demo/real-time-example - MapLibre globe + GeoTIFF —
maplibre-gl-demo/maplibre-gl-demo(setmapRuntime: 'maplibre'on each layer) - RGB GeoTIFF —
RgbGeoTiffbelow anddocs/rgb-geotiff.md
For readyForDisplay, setSource, visibility toggling, and MapLibre globe setup, see the API reference below and Quick starts above.
TypeScript Usage
mapbox-exif-layer ships a typed constructor so your options are checked at compile time. However, the current type declarations do not formally implement CustomLayerInterface from either mapbox-gl or maplibre-gl — because the render method signature differs between the two runtimes at runtime and cannot be expressed as a single static type without being incorrect for one of them.
Workaround: cast the layer instance when passing it to map.addLayer:
// MapLibre GL JS
map.addLayer(particleLayer as unknown as maplibregl.CustomLayerInterface);
map.addLayer(weatherLayer as unknown as maplibregl.CustomLayerInterface);
// Mapbox GL JS
map.addLayer(particleLayer as unknown as mapboxgl.CustomLayerInterface);
map.addLayer(weatherLayer as unknown as mapboxgl.CustomLayerInterface);This cast is safe — both ParticleMotion and SmoothRaster implement the interface correctly at runtime.
Available Class Reference
ParticleMotion
A particle-based visualization layer that creates animated particles for wind direction and speed visualization. Supports JPEG/PNG image sources with u/v velocities encoded in the R and G bands (see docs/jpeg-source.md), and GeoTIFF sources with u- and v-component velocity stored in separate bands (see docs/geotiff-source.md).
Options
id(string): (required) Unique layer IDsource(string): (required) URL of JPEG/PNG image or GeoTIFF file (.tif/.tiff; GeoTIFF requires optional peer package dependencygeotiff)color(array): (required) Array of color stops[value, [r, g, b]]. Values do not have to be ordered since sorting is performed internally by the package.bounds(array): JPEG/PNG image only (required). Extent as[minX, maxY, maxX, minY](longitude −180…180, latitude −90…90). GeoTIFF source will read bounds from the file directly and ignore this parameter.readyForDisplay(bool): Preventing the layer from rendering when the layer is added to the map, if necessary (default: false)particleCount(number): Number of particles to render (default: 5000)velocityFactor(number): Speed multiplier for particle motion (default: 0.05)updateInterval(number): Minimum time between particle updates in ms (default: 50)pointSize(number): Size of particles in pixels (default: 5.0)fadeOpacity(number): Global opacity for particles (default: 0.9)trailLength(number): Number of trailing particles (default: 3)trailSizeDecay(number): How quickly point size decreases for trail particles (default: 0.8)ageThreshold(number): Age threshold before particle position reset probability increases. This prevents particles from degenerating to some circular/looped pattern (default: 500)maxAge(number): Maximum age before particle position is forced to reset. This prevents particles from degenerating to some circular/looped pattern (default: 1000)unit(string): (required) When the source is a GeoTIFF file, the unit of the u- and v-component velocities stored in the bands. When the source is EXIF JPEG, the unit of the min/max velocity and speed values in the EXIF information. Must be consistent with the wind-speed units in thecolorparameter. Can be one of:'mph'(default): Miles per hour'kph': Kilometers per hour'mps': Meters per second
velocityRange(array): Dataset-independent normalized JPEG/PNG only (required). Two-element[min, max]in the layerunitoption. Used to de-normalize u and v from the R and G bands when the JPEG/PNG source has no EXIF velocity metadata; applied to both u- and v- components. Ignored when valid EXIF metadata is present. Speed coloring without EXIF is inferred fromcolorstops, not from this range. See jpeg-source.md.cacheOption(string): Cache option to use when fetching the source image. It can be one of no-cache (default), no-store, reload, default, or force-cache.slot(string): Optional slot identifier for the layer (used by Mapbox GL JS for layer ordering); typical values may include "top", "middle" (recommended), "bottom".mapRuntime(string): (required for MapLibre)'mapbox'(default) or'maplibre'. This parameter must be explicitly set to'maplibre'if maplibre-gl-js SDK is used. Only'maplibre'with MapLibre GL JS supports globe projection.sourceType(string):'auto'(default),'jpeg', or'geotiff'. GeoTIFF requires optional peer package dependency geotiff; see geotiff-source.md.uBand(number): GeoTIFF only. GeoTIFF sample index for the u component (default:0, first band).vBand(number): GeoTIFF only. GeoTIFF sample index for the v component (default:1, second band).
Methods
setSource(source, percentParticleWhenSetSource = 0.5): Changes the source URL (JPEG or GeoTIFF), and optionally the proportion of particles whose positions must be reset when the source is updated (default half of the particles). The layer will repaint automatically.
SmoothRaster
A custom raster layer for scalar fields (temperature, humidity, precipitation, etc.).
With a JPEG/PNG image source, the grid is uploaded as an RGBA texture with linear filtering; the GPU bilinearly interpolates between adjacent texel values when sampling, producing a smooth, non-blocky gradient. See docs/jpeg-source.md.
With a GeoTIFF source, a small single-band file (or one band from a multi-band file) is read directly in the browser and colormapped via the shader — useful when you want a GIS-friendly pipeline without custom image encoding, though the display tends to look blockier at native grid resolution than the image path. See docs/geotiff-source.md.
Options
id(string): (required) Unique layer IDsource(string): (required) URL of an JPEG/PNG image or GeoTIFF file (.tif/.tiff; GeoTIFF requires optional peer package dependencygeotiff)color(array): (required) Array of color stops[value, [r, g, b]]. Values do not have to be ordered since sorting is performed internally by the package. An optional A-band (opacity) value can also be specified, but interpolation will not be applied to A-band. A-band is useful for rendering precipitation by setting all zero or near-zero precipitation cells completely transparent (see react-demo/real-time-example).bounds(array): JPEG/PNG image only (required). Extent as[minX, maxY, maxX, minY](longitude −180…180, latitude −90…90). GeoTIFF source will read bounds from the file directly and ignore this parameter.opacity(number): Layer global opacity (default: 1.0)readyForDisplay(bool): Preventing the layer from rendering when the layer is added to the map, if necessary (default: false)cacheOption(string): Cache option to use when fetching the source image. It can be one of no-cache (default in 1.0.3), no-store (default in 1.0.2), reload, default, or force-cache.slot(string): Optional slot identifier for the layer (used by Mapbox GL JS for layer ordering); typical values may include "top", "middle" (recommended), "bottom".mapRuntime(string): (required for MapLibre)'mapbox'(default) or'maplibre'. This parameter must be explicitly set to'maplibre'if maplibre-gl-js SDK is used. Only'maplibre'with MapLibre GL JS supports globe projection.sourceType(string):'auto'(default),'jpeg', or'geotiff'. GeoTIFF requires optional peer package dependency geotiff; seedocs/geotiff-source.md.scalarBand(number): GeoTIFF only. Optional GeoTIFF sample index (0-based band index) for scalar data (default:0, first band).scalarValueRange(array): Dataset-independent normalized JPEG/PNG only (required). Two-element[min, max]matching the physical range used when encoding the R band; maps encoded values to physical units for the colormap when EXIF scalar metadata is absent from JPEG/PNG image soruce (this parameter is ignored when EXIF is present).colorstop values should use the same physical units. See docs/jpeg-source.md.
Methods
setSource(source, color=null): Changes the source URL (JPEG or GeoTIFF), and optionally color array (default is to use the same color array as before). The layer will repaint automatically.
RgbGeoTiff
Displays an RGB or RGBA GeoTIFF (PhotometricInterpretation=2) as a native Mapbox/MapLibre image source and raster layer. Unlike ParticleMotion and SmoothRaster, this is not a custom WebGL layer — the GeoTIFF is decoded client-side into a PNG blob URL, then added to the map with the built-in raster layer type. Bounds are read from the file; no bounds option is required.
Requirements: EPSG:4326, uint8 or uint16 bands, peer package geotiff. See docs/rgb-geotiff.md.
import { RgbGeoTiff } from 'mapbox-exif-layer';
const rgbLayer = new RgbGeoTiff({
id: 'aerial-photo',
source: 'path/to/photo.tif',
opacity: 0.9
});
map.on('load', () => {
rgbLayer.addTo(map);
});
// later:
rgbLayer.remove();Options
id(string): (required) Layer ID. Also used as the base for the internal image source ID (${id}-rgb-source).source(string): (required) URL of an RGB or RGBA GeoTIFF file (.tif/.tiff; requires optional peer packagegeotiff).opacity(number): Raster layer opacity (default:1.0).cacheOption(string): Cache option to use when fetching the GeoTIFF. Can be one ofno-cache(default),no-store,reload,default, orforce-cache.slot(string): Optional slot identifier for the layer (Mapbox GL JS v3 layer ordering); typical values include"top","middle","bottom".beforeLayerId(string): Optional existing layer ID. When set, the raster layer is inserted below that layer in the stack (same as the second argument tomap.addLayer).
Methods
addTo(map): Fetches the GeoTIFF, decodes it, and adds theimagesource andrasterlayer to the map. Returnsthisfor chaining. Call after the map has loaded (e.g. insidemap.on('load', ...)).remove(): Removes the raster layer and image source from the map and revokes the internal blob URL to free memory.
Once added, the layer is a normal raster layer on the map. Use its id with the usual Mapbox/MapLibre APIs:
// Toggle visibility
map.setLayoutProperty('aerial-photo', 'visibility', 'none');
map.setLayoutProperty('aerial-photo', 'visibility', 'visible');
// Change opacity at runtime
map.setPaintProperty('aerial-photo', 'raster-opacity', 0.5);Acknowledgement
The shader utility code of this package is referencing from util.js of mapbox/webgl-wind. The idea of EXIF is credit to sakitam-fdd/wind-layer.
License
MIT
