maplibre-gl-raster
v0.5.0
Published
A MapLibre GL JS plugin for visualizing local and remote raster datasets (GeoTIFF/COG)
Maintainers
Readme
maplibre-gl-raster
A MapLibre GL JS plugin for visualizing local and remote raster datasets (GeoTIFF / Cloud Optimized GeoTIFF) directly in the browser. No tile server required: COGs are read with HTTP range requests and rendered on the GPU through a deck.gl pipeline.
Features
- Local and remote rasters - Load Cloud Optimized GeoTIFFs from any CORS-enabled URL, or drag-and-drop local GeoTIFF files
- Multiple layers - Layer list with visibility toggles, reordering, zoom-to, and per-layer settings
- GPU rendering pipeline - Band compositing, per-band rescale, 90+ colormaps, nodata filtering, linear/sqrt/log stretch, and gamma correction as deck.gl shader modules; parameter changes re-render without re-fetching tiles
- Auto statistics - Per-band min/max and histograms sampled from COG overviews (or GDAL metadata), with draggable histogram handles for the rescale range
- Pixel inspector - Toggle inspect mode and click the map to read the raw source values of every band of the selected layer at that location, shown in a popup (works for COGs in any CRS)
- Colorbar legend - A standalone
Colorbarcontrol: gradient + tick labels for a named colormap (or custom colors), with configurable min/max, title, units, orientation, and position - Collapsible control - A compact 29x29 map button that expands into a floating panel
- TypeScript + React - Full type definitions, a React wrapper component, and hooks
- GeoLibre bundle output - Builds a zip with root
plugin.json, bundled ESM, and CSS for GeoLibre Desktop
Installation
npm install maplibre-gl-rasterThe plugin declares maplibre-gl, @deck.gl/*, and @luma.gl/* as peer dependencies (npm 7+ installs them automatically). This package is ESM-only.
Quick Start
Vanilla JavaScript/TypeScript
import maplibregl from "maplibre-gl";
import { RasterControl } from "maplibre-gl-raster";
import "maplibre-gl-raster/style.css";
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
center: [0, 0],
zoom: 2,
});
map.on("load", () => {
const control = new RasterControl({ collapsed: false });
map.addControl(control, "top-right");
// Optionally add a raster programmatically (users can also paste a URL
// or drop a local GeoTIFF file in the panel).
control.addRaster("https://example.com/data/cog.tif");
});React
import { useEffect, useRef, useState } from "react";
import maplibregl, { Map } from "maplibre-gl";
import { RasterControlReact, useRasterState } from "maplibre-gl-raster/react";
import "maplibre-gl-raster/style.css";
function App() {
const mapContainer = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<Map | null>(null);
const { state, toggle } = useRasterState();
useEffect(() => {
if (!mapContainer.current) return;
const mapInstance = new maplibregl.Map({
container: mapContainer.current,
style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
center: [0, 0],
zoom: 2,
});
mapInstance.on("load", () => setMap(mapInstance));
return () => mapInstance.remove();
}, []);
return (
<div style={{ width: "100%", height: "100vh" }}>
<div ref={mapContainer} style={{ width: "100%", height: "100%" }} />
{map && (
<RasterControlReact
map={map}
collapsed={state.collapsed}
onReady={(control) => control.addRaster("https://example.com/cog.tif")}
/>
)}
</div>
);
}API
RasterControl
The main control class implementing MapLibre's IControl interface.
Constructor Options
| Option | Type | Default | Description |
| ------------- | --------- | ------------- | ------------------------------------------------------------------------- |
| collapsed | boolean | true | Whether the panel starts collapsed (showing only the 29x29 toggle button) |
| position | string | 'top-right' | Control position on the map |
| title | string | 'Raster' | Title displayed in the header |
| panelWidth | number | 360 | Width of the dropdown panel in pixels |
| className | string | '' | Custom CSS class name |
| interleaved | boolean | true | Render the deck.gl overlay interleaved with the basemap layers |
| defaultUrl | string | '' | Prefills the Add data URL input (not loaded until the user clicks Load) |
| autoLoad | boolean | false | Load defaultUrl automatically when the control is added to the map |
| engine | RenderEngine | 'maplibre-gl-raster' | Initial rendering backend; switchable at runtime from the panel |
Raster Methods
addRaster(source, options?)- Add a raster from a COG URL (string) or a local GeoTIFFFile; resolves with the layer idremoveRaster(id)- Remove a raster layergetRaster(id)/getRasters()- Get layer snapshots (RasterLayerInfo)setRasterState(id, patch)- Update visualization state (mode, bands, rescale, colormap, reversed, nodata, opacity, gamma, stretch, visible)setVisible(id, visible)- Show / hide a layerselectRaster(id | null)- Choose which layer the panel's settings editzoomToRaster(id)- Fit the map to a layer's boundsreorderRaster(id, toIndex)- Move a layer in the draw order (0 = bottom)getEngine()/setEngine(engine)- Read / switch the rendering backend (see Rendering engines)
addRaster options (AddRasterOptions): id, name, state (initial Partial<RasterLayerState> overrides), zoomTo (default true), and beforeId (insert the raster beneath an existing style layer, e.g. a label layer; also available as an input in the panel's Add data section).
Panel Methods
toggle()/expand()/collapse()- Control the panelgetState()/setState(state)- Control-level state (collapsed, panelWidth)on(event, handler)/off(event, handler)- Event handlersgetMap()/getContainer()- Access the map / container
Events
collapse/expand/statechange- Panel state eventsrasteradd/rasterremove/rasterchange/rasterselect- Layer lifecycle events (payload includeslayerId)error- Loading or rendering errors (payload includeserror)
Rendering engines
The panel has a Rendering engine selector (and a matching engine option /
getEngine() / setEngine() API) that switches the backend used for every
layer:
maplibre-gl-raster(default) - the GPU pipeline described above: a deck.glCOGLayeron a sharedMapboxOverlay. Parameter changes re-render without re-fetching tiles.cog-tiler-wasm- a serverless CPU/WASM XYZ tiler (cog-tiler-wasm) wired to a MapLibre custom protocol. Tiles are rendered on the CPU and served as native MapLibre raster layers. The panel's settings (bands, rescale, colormap, curve, gamma, nodata, opacity) map directly onto its render parameters.
cog-tiler-wasm is an optional peer dependency, loaded lazily the first
time the engine is selected, so it never enters the default bundle. To use it,
install it alongside its own peers:
npm install cog-tiler-wasm whitebox-wasm proj4 "geotiff@^2.1" geotiff-geokeys-to-proj4Pin
geotiffto the2.xline.cog-tiler-wasmreads a GeoTIFF's embedded color table fromfileDirectory.ColorMap, whichgeotiff@3resolves lazily (so paletted rasters like NLCD would otherwise render through a continuous colormap instead of their categorical colors).
// Start on the WASM engine, or switch at runtime:
const control = new RasterControl({ engine: "cog-tiler-wasm" });
// ...
control.setEngine("maplibre-gl-raster");If the package is not installed, selecting the engine surfaces a load error via
the error event; the default engine keeps working.
RasterLayerState
Per-layer visualization state, editable via the panel or setRasterState:
interface RasterLayerState {
mode: "rgb" | "single"; // RGB composite or single band + colormap
bands: number[]; // 1-indexed band selection
rescale: [number, number][] | null; // per-channel min/max; null = auto (2-98%)
colormap: string; // colormap name; "palette" = embedded color table
reversed: boolean; // sample the named colormap back-to-front
nodata: number | "off" | "auto"; // nodata handling
opacity: number; // 0..1
gamma: number; // power-law correction (1 = off)
stretch: "linear" | "log" | "sqrt"; // curve applied after rescale
visible: boolean;
colorbar?: {
// optional on-map legend for this single-band layer
visible: boolean;
title?: string;
units?: string;
orientation?: "horizontal" | "vertical";
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
};
}When a raster loads, the mode and bands are picked automatically (3+ bands → RGB [1, 2, 3]; otherwise single-band), and the rescale range defaults to the 2-98% percentile of sampled statistics. Single-band rasters use the image's embedded color table when it carries one (colormap: "palette") and grayscale otherwise. The first four bands are fetched as GPU textures, so band combinations among them re-render instantly without re-downloading tiles.
RasterControlReact
React wrapper component for RasterControl.
Props
All RasterControl options plus:
| Prop | Type | Description |
| --------------- | ---------- | ------------------------------------------------------ |
| map | Map | MapLibre GL map instance (required) |
| onStateChange | function | Callback fired when the control state changes |
| onReady | function | Receives the RasterControl instance after map attach |
useRasterState
Custom React hook for managing control state.
const {
state, // Current state
setState, // Update entire state
setCollapsed, // Set collapsed state
setPanelWidth, // Set panel width
setData, // Set custom data
reset, // Reset to initial state
toggle, // Toggle collapsed state
} = useRasterState(initialState);Colorbar
The settings panel has a "Show colorbar" toggle for single-band layers
(with title, units, orientation, and position controls). Enabling it shows a
legend on the map driven by that layer's colormap, reversed flag, and
effective value range, and it follows rescale / colormap changes live. This is
persisted per layer in RasterLayerState.colorbar.
You can also use the legend directly as a standalone control. Add it like any
MapLibre control; it docks into a map corner and renders a gradient with tick
labels. The ramp is sampled from the same colormap sprite the renderer uses, so
a named colormap (and the reversed flag) matches the map exactly — or supply
your own colors.
import { Colorbar } from "maplibre-gl-raster";
const colorbar = new Colorbar({
colormap: "viridis", // or colors: ["#000", "#f00", "#ff0"]
min: 0,
max: 3000,
title: "Elevation",
units: "m",
orientation: "horizontal", // or "vertical"
position: "bottom-right",
ticks: 5,
});
map.addControl(colorbar);Keep it in sync with a single-band raster by updating it from the control's
rasterchange event:
control.on("rasterchange", ({ layerId }) => {
const info = layerId ? control.getRaster(layerId) : undefined;
const range = info?.state.rescale?.[0]; // [min, max] when set explicitly
// 'palette' uses the image's embedded table, not a named colormap.
if (info && range && info.state.colormap !== "palette") {
colorbar.update({
colormap: info.state.colormap,
reversed: info.state.reversed,
min: range[0],
max: range[1],
});
}
});ColorbarOptions: colormap? (default "viridis"), colors? (custom ramp,
overrides colormap), reversed?, min? / max? (default 0 / 1),
title?, titleAlign? ("left" | "center" | "right"), units?,
stretch? ("linear" | "log" | "sqrt" — spaces tick values to match the
layer's stretch), orientation? ("horizontal" | "vertical", default
"horizontal"), position? (map corner, default "bottom-right"), ticks?
(count, default 5), tickValues? (explicit ticks), decimals? (fixed
decimal places; omit for a compact auto format), barLength? /
barThickness? (px), className?. Reconfigure live with
colorbar.update(partial).
Utilities
The package also exports lower-level building blocks for advanced use:
loadGeoTIFF(url)- Open a (CORS-safe) GeoTIFF from a URL or blob URLcomputeAutoStats(tiff, signal, onProgress?)- Per-band min/max + histogramssummarizeGeoTIFF(tiff)- Image / CRS / band / GDAL metadata summaryreadBandNames(tiff)/percentileFromHistogram(stats, p)COLORMAP_NAMES/COLORMAP_OPTIONS/colormapsPngUrlsampleColormapStops(name, steps, reversed?)/loadColormapSprite()/isKnownColormap(name)- sample a colormap's colors in plain JSautoRangeFor(stats)/statsForBand(autoStats, band)- resolve a band's effective rescale rangeclamp,formatNumericValue,generateId,debounce,throttle,classNames
CORS requirements for remote COGs
Remote COGs must be served with CORS enabled (Access-Control-Allow-Origin). The loader includes a workaround for buckets that do not expose Content-Range via Access-Control-Expose-Headers, so most public S3/R2 buckets work out of the box.
Build a GeoLibre plugin zip
GeoLibre Desktop loads external plugins from an app data plugins/ directory. The zip must contain plugin.json at the root, plus a bundled ESM entry and optional CSS file.
npm install
npm run package:geolibreThis creates:
geolibre-plugin/maplibre-gl-raster-0.1.0.zipThe generated zip contains:
plugin.json
dist/index.js
dist/style.cssCopy the zip into GeoLibre Desktop's app data plugins/ directory and restart GeoLibre. On Linux with the default app identifier, that directory is usually:
~/.local/share/org.geolibre.desktop/plugins/For the GeoLibre web app, serve the unpacked plugin with CORS enabled:
npm run package:geolibre
npm run serve:geolibre -- 8000Then add this manifest URL in GeoLibre Settings > Plugins:
http://localhost:8000/plugin.jsonUsing python -m http.server for this cross-origin web app case is not enough
because it does not send Access-Control-Allow-Origin.
Development
Setup
# Clone the repository
git clone https://github.com/opengeos/maplibre-gl-raster.git
cd maplibre-gl-raster
# Install dependencies
npm install
# Start development server
npm run devScripts
| Script | Description |
| -------------------------- | ---------------------------------------- |
| npm run dev | Start development server |
| npm run build | Build the library and GeoLibre bundle |
| npm run build:lib | Build the standalone MapLibre library |
| npm run build:geolibre | Build the GeoLibre ESM and CSS bundle |
| npm run package:geolibre | Build and zip the GeoLibre plugin bundle |
| npm run build:examples | Build examples for deployment |
| npm run test | Run tests |
| npm run test:ui | Run tests with UI |
| npm run test:coverage | Run tests with coverage |
| npm run lint | Lint the code |
| npm run format | Format the code |
Project Structure
maplibre-gl-raster/
├── geolibre-plugin/
│ └── plugin.json # GeoLibre external plugin manifest
├── scripts/
│ └── package-geolibre-plugin.mjs
├── src/
│ ├── index.ts # Main entry point
│ ├── geolibre.ts # GeoLibre plugin wrapper entry point
│ ├── react.ts # React entry point
│ ├── index.css # Root styles
│ └── lib/
│ ├── core/ # RasterControl, React wrapper, types
│ ├── raster/ # GeoTIFF loading, stats, GPU render pipeline
│ ├── state/ # RasterLayer model + LayerManager
│ ├── ui/ # Vanilla DOM panel components
│ ├── hooks/ # React hooks
│ ├── utils/ # Utility functions
│ └── styles/ # Component styles
├── tests/ # Test files
├── examples/ # Example applications
│ ├── basic/ # Vanilla JS example
│ └── react/ # React example
└── .github/workflows/ # CI/CD workflowsDocker
The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
Pull and Run
# Pull the latest image
docker pull ghcr.io/opengeos/maplibre-gl-raster:latest
# Run the container
docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-raster:latestThen open http://localhost:8080/maplibre-gl-raster/ in your browser to view the examples.
Build Locally
# Build the image
docker build -t maplibre-gl-raster .
# Run the container
docker run -p 8080:80 maplibre-gl-rasterAvailable Tags
| Tag | Description |
| -------- | -------------------------------- |
| latest | Latest release |
| x.y.z | Specific version (e.g., 1.0.0) |
| x.y | Minor version (e.g., 1.0) |
Publish to npm
npm login
npm whoami
npm publish --access publicSet up Trusted Publisher on npmjs.com
Credits
The rendering pipeline, GeoTIFF loading strategy, statistics sampling, and much of the visualization UX are ported from Source Cooperative's cog-viewer, built on their excellent @developmentseed/deck.gl-geotiff and @developmentseed/deck.gl-raster libraries.
License
MIT License - see LICENSE for details.
