A MapLibre GL JS plugin for visualizing LiDAR point clouds using deck.gl.
- Load and visualize LAS/LAZ/COPC point cloud files (LAS 1.0 - 1.4)
- Dynamic COPC streaming - viewport-based loading for large cloud-optimized point clouds
- EPT (Entwine Point Tile) support - stream large point cloud datasets from EPT servers
- Multiple color schemes: elevation, intensity, classification, RGB
- Classification legend with toggle - interactive legend to show/hide individual classification types
- Percentile-based coloring - use 2-98% percentile range for better color distribution (clips outliers)
- Interactive GUI control panel with scrollable content
- Point picking - hover over points to see all available attributes (coordinates, elevation, intensity, classification, RGB, GPS time, return number, etc.)
- Z offset adjustment - shift point clouds vertically for alignment
- Elevation filtering - filter points by elevation range
- Automatic coordinate transformation (projected CRS to WGS84)
- Programmatic API for loading and styling
- React integration with hooks
- deck.gl PointCloudLayer with optimized chunking for large datasets
- TypeScript support
Try the live demo.
Use the Online Viewer to load and visualize any COPC point cloud by entering a URL. You can also use URL parameters for direct linking:
https://opengeos.org/maplibre-gl-lidar/viewer/?url=https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz
This allows you to share links that automatically load specific point clouds.
npm install maplibre-gl-lidarimport maplibregl from "maplibre-gl";
import { LidarControl } from "maplibre-gl-lidar";
import "maplibre-gl-lidar/style.css";
import "maplibre-gl/dist/maplibre-gl.css";
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
center: [-122.4, 37.8],
zoom: 12,
pitch: 60,
maxPitch: 85, // Allow higher pitch for better 3D viewing
});
map.on("load", () => {
// Add the LiDAR control
const lidarControl = new LidarControl({
title: "LiDAR Viewer",
collapsed: true,
pointSize: 2,
colorScheme: "elevation",
pickable: true, // Enable point picking for hover tooltips
});
map.addControl(lidarControl, "top-right");
// Listen for events
lidarControl.on("load", (event) => {
console.log("Point cloud loaded:", event.pointCloud);
lidarControl.flyToPointCloud();
});
// Load a point cloud programmatically
lidarControl.loadPointCloud(
"https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz"
);
});import { useEffect, useRef, useState } from "react";
import maplibregl, { Map } from "maplibre-gl";
import { LidarControlReact, useLidarState } from "maplibre-gl-lidar/react";
import "maplibre-gl-lidar/style.css";
import "maplibre-gl/dist/maplibre-gl.css";
function App() {
const mapContainer = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<Map | null>(null);
const { state, setColorScheme, setPointSize } = useLidarState();
useEffect(() => {
if (!mapContainer.current) return;
const mapInstance = new maplibregl.Map({
container: mapContainer.current,
style: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
center: [-122.4, 37.8],
zoom: 12,
pitch: 60,
maxPitch: 85, // Allow higher pitch for better 3D viewing
});
mapInstance.on("load", () => setMap(mapInstance));
return () => mapInstance.remove();
}, []);
return (
<div style={{ width: "100%", height: "100vh" }}>
<div ref={mapContainer} style={{ width: "100%", height: "100%" }} />
{map && (
<LidarControlReact
map={map}
title="LiDAR Viewer"
pointSize={state.pointSize}
colorScheme={state.colorScheme}
onLoad={(pc) => console.log("Loaded:", pc)}
defaultUrl="https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz"
/>
)}
</div>
);
}The main control class implementing MapLibre's IControl interface.
interface LidarControlOptions {
// Panel settings
collapsed?: boolean; // Start collapsed (default: true)
position?: string; // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
title?: string; // Panel title (default: 'LiDAR Viewer')
panelWidth?: number; // Panel width in pixels (default: 365)
panelMaxHeight?: number; // Panel max height with scrollbar (default: 500)
className?: string; // Custom CSS class
// Point cloud styling
pointSize?: number; // Point size in pixels (default: 2)
opacity?: number; // Opacity 0-1 (default: 1.0)
colorScheme?: ColorScheme; // Color scheme (default: 'elevation')
usePercentile?: boolean; // Use 2-98% percentile for coloring (default: true)
pointBudget?: number; // Max points to display (default: 1000000)
// Filters and adjustments
elevationRange?: [number, number] | null; // Elevation filter
zOffsetEnabled?: boolean; // Enable Z offset adjustment (default: false)
zOffset?: number; // Z offset in meters (default: 0)
// Interaction
pickable?: boolean; // Enable point picking/hover tooltips (default: false)
pickInfoFields?: string[]; // Fields to show in tooltip (default: all)
// Behavior
autoZoom?: boolean; // Auto zoom to data after loading (default: true)
// COPC Streaming (dynamic loading)
copcLoadingMode?: "full" | "dynamic"; // Loading mode for COPC files (default: 'dynamic')
streamingPointBudget?: number; // Max points for streaming (default: 5000000)
streamingMaxConcurrentRequests?: number; // Concurrent node requests (default: 4)
streamingViewportDebounceMs?: number; // Viewport change debounce (default: 150)
}// Loading
loadPointCloud(source: string | File | ArrayBuffer, options?: { loadingMode?: 'full' | 'dynamic' }): Promise<PointCloudInfo>
loadPointCloudStreaming(source: string | File | ArrayBuffer, options?: StreamingLoaderOptions): Promise<PointCloudInfo>
stopStreaming(): void // Stop dynamic loading and clean up
unloadPointCloud(id?: string): void
getPointClouds(): PointCloudInfo[]
flyToPointCloud(id?: string): void
// Styling
setPointSize(size: number): void
setOpacity(opacity: number): void
setColorScheme(scheme: ColorScheme): void
setUsePercentile(usePercentile: boolean): void
getUsePercentile(): boolean
setElevationRange(min: number, max: number): void
clearElevationRange(): void
setPickable(pickable: boolean): void
// Z Offset
setZOffsetEnabled(enabled: boolean): void
setZOffset(offset: number): void
getZOffset(): number
// Pick info customization
setPickInfoFields(fields?: string[]): void
getPickInfoFields(): string[] | undefined
// Classification visibility (when using 'classification' color scheme)
setClassificationVisibility(code: number, visible: boolean): void
showAllClassifications(): void
hideAllClassifications(): void
getHiddenClassifications(): number[]
getAvailableClassifications(): number[]
// Panel control
toggle(): void
expand(): void
collapse(): void
// Events
on(event: LidarControlEvent, handler: LidarControlEventHandler): void
off(event: LidarControlEvent, handler: LidarControlEventHandler): void
// State
getState(): LidarState
getMap(): MapLibreMap | undefinedload- Point cloud loaded successfullyloadstart- Loading startedloaderror- Loading failedunload- Point cloud unloadedstatechange- Control state changedstylechange- Styling changedcollapse- Panel collapsedexpand- Panel expandedstreamingstart- Dynamic streaming startedstreamingstop- Dynamic streaming stoppedstreamingprogress- Streaming progress updatebudgetreached- Point budget limit reached
'elevation'- Viridis-like color ramp based on Z values'intensity'- Grayscale based on intensity attribute'classification'- ASPRS standard classification colors'rgb'- Use embedded RGB colors (if available)
By default, elevation and intensity coloring uses the 2nd-98th percentile range instead of the full min-max range. This clips outliers and provides better color distribution across the point cloud.
// Percentile coloring is enabled by default
const control = new LidarControl({
colorScheme: "elevation",
usePercentile: true, // default
});
// Disable to use full value range (min-max)
control.setUsePercentile(false);
// Check current setting
console.log(control.getUsePercentile()); // true or falseThe percentile toggle is also available in the GUI panel when using "Elevation" or "Intensity" color schemes. Uncheck "Use percentile range (2-98%)" to use the full value range.
When pickable is enabled, hovering over points displays a tooltip with all available attributes:
- X, Y, Z - Coordinates and elevation
- Intensity - Reflectance value
- Classification - ASPRS class name (Ground, Building, Vegetation, etc.)
- Red, Green, Blue - RGB color values (if available)
- GpsTime - GPS timestamp
- ReturnNumber / NumberOfReturns - Return information
- PointSourceId - Scanner source ID
- ScanAngle - Scan angle
- And more (EdgeOfFlightLine, ScanDirectionFlag, UserData, etc.)
Enable via constructor option or toggle in the GUI panel:
// Via constructor
const control = new LidarControl({ pickable: true });
// Or programmatically
control.setPickable(true);
// Optionally filter which fields to display
control.setPickInfoFields([
"Classification",
"Intensity",
"GpsTime",
"ReturnNumber",
]);Shift point clouds vertically for alignment with terrain or other data:
// Via constructor
const control = new LidarControl({
zOffsetEnabled: true,
zOffset: 50, // Shift up 50 meters
});
// Or programmatically
control.setZOffsetEnabled(true);
control.setZOffset(50);
// Get current offset
console.log(control.getZOffset()); // 50The Z offset can also be adjusted interactively via the "Z Offset" checkbox and slider in the GUI panel.
When using the "Classification" color scheme, an interactive legend appears showing all classification types found in the point cloud data. Each classification displays:
- A color swatch matching the ASPRS standard colors
- The classification name (Ground, Building, Vegetation, etc.)
- A checkbox to toggle visibility
Features:
- Show All / Hide All buttons - Quickly toggle all classifications at once
- Individual toggles - Show or hide specific classification types
- Auto-detection - Classifications are automatically detected from loaded data
- Streaming support - Classifications update as data streams in for COPC files
// Via GUI: Select "Classification" from the Color By dropdown
// The legend automatically appears with checkboxes for each class
// Programmatically control visibility
control.setColorScheme("classification");
// Hide specific classifications (e.g., hide noise points)
control.setClassificationVisibility(7, false); // Hide "Low Point (Noise)"
control.setClassificationVisibility(18, false); // Hide "High Noise"
// Show only ground and buildings
control.hideAllClassifications();
control.setClassificationVisibility(2, true); // Ground
control.setClassificationVisibility(6, true); // Building
// Get available classifications in the data
const available = control.getAvailableClassifications();
console.log("Classifications:", available); // [2, 3, 4, 5, 6, ...]
// Get currently hidden classifications
const hidden = control.getHiddenClassifications();
console.log("Hidden:", hidden); // [7, 18]ASPRS Classification Codes:
| Code | Name |
|---|---|
| 2 | Ground |
| 3 | Low Vegetation |
| 4 | Medium Vegetation |
| 5 | High Vegetation |
| 6 | Building |
| 7 | Low Point (Noise) |
| 9 | Water |
| 17 | Bridge Deck |
For large COPC (Cloud Optimized Point Cloud) files, dynamic streaming loads only the points visible in the current viewport, dramatically reducing initial load time and memory usage.
Key features:
- Viewport-based loading - Only loads octree nodes visible in the current map view
- Level-of-detail (LOD) - Automatically selects appropriate detail level based on zoom
- Center-first priority - Points near the viewport center load first
- Point budget - Limits total points in memory (default: 5 million)
How it works:
- When loading a COPC file (from URL or local file), dynamic mode is used by default
- As you pan/zoom the map, new nodes are streamed based on viewport
- Deeper octree levels (more detail) load as you zoom in
- Parent nodes provide coverage where child nodes don't exist
// Dynamic loading is the default for COPC files
const control = new LidarControl();
control.loadPointCloud("https://example.com/large-pointcloud.copc.laz");
// Explicitly set loading mode
const control = new LidarControl({
copcLoadingMode: "dynamic", // or 'full' for complete load
streamingPointBudget: 10_000_000, // 10 million points max
});
// Override per-load
control.loadPointCloud(file, { loadingMode: "full" }); // Force full load
control.loadPointCloud(url, { loadingMode: "dynamic" }); // Force streamingLoading modes:
'dynamic'(default for COPC) - Stream nodes based on viewport, ideal for large files'full'- Load entire point cloud upfront, better for small files
Note: Non-COPC files (regular LAS/LAZ) always use full loading mode since they don't have the octree structure required for streaming.
maplibre-gl-lidar supports Entwine Point Tile (EPT) datasets, a widely-used format for serving large point clouds over HTTP with viewport-based streaming.
Key features:
- Directory-based format - Metadata in ept.json, hierarchy in ept-hierarchy/, data in ept-data/
- Viewport-based streaming - Points load dynamically based on current map view
- LAZ compression - Efficient data transfer using LAZ compression
- Automatic CRS transformation - Coordinates transformed from source CRS to WGS84
Loading EPT data:
// Load EPT dataset by URL (automatically detected via ept.json)
lidarControl.loadPointCloud("https://na-c.entwine.io/dublin/ept.json");
// Or load programmatically
lidarControl.loadPointCloudEptStreaming(
"https://na-c.entwine.io/dublin/ept.json",
{
pointBudget: 5_000_000, // Max points in memory
}
);Sample EPT datasets:
- Dublin, Ireland:
https://na-c.entwine.io/dublin/ept.json - New York City (4.7B points):
https://na-c.entwine.io/nyc/ept.json - Red Rocks:
https://na-c.entwine.io/red-rocks/ept.json
Note: EPT datasets require CORS support from the server. The sample datasets from entwine.io are CORS-enabled.
const {
state,
setState,
setPointSize,
setOpacity,
setColorScheme,
setUsePercentile,
setElevationRange,
setZOffsetEnabled,
setZOffset,
toggle,
reset,
} = useLidarState(initialState?);const { data, loading, error, progress, load, reset } = usePointCloud();
// Load a file
await load(file);
console.log(`Loaded ${data.pointCount} points`);- LAS 1.0 - 1.4 (all versions supported via copc.js + loaders.gl fallback)
- LAZ (compressed LAS)
- COPC (Cloud Optimized Point Cloud) - with dynamic streaming support
- EPT (Entwine Point Tile) - viewport-based streaming from HTTP servers
Note: LAS 1.2 and 1.4 are loaded using copc.js for optimal performance. LAS 1.0, 1.1, and 1.3 files automatically fall back to @loaders.gl/las.
maplibre-gl-lidar works with Next.js out of the box, including Turbopack (the default bundler in Next.js 15+). The library bundles browser-safe shims for Node.js modules that are statically referenced but never executed in browsers.
Note for older Next.js versions or custom webpack configs: If you encounter "Can't resolve 'fs'" errors, add this to your next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
};
}
return config;
},
};
module.exports = nextConfig;Point clouds are automatically transformed to WGS84 (EPSG:4326) for display on the map. The loader reads the WKT coordinate reference system from the file and uses proj4 to transform coordinates. Supported features:
- Projected coordinate systems (State Plane, UTM, etc.)
- Compound coordinate systems (horizontal + vertical)
- Automatic unit conversion (feet to meters for elevation)
The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
# Pull the latest image
docker pull ghcr.io/opengeos/maplibre-gl-lidar:latest
# Run the container
docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-lidar:latestThen open http://localhost:8080/maplibre-gl-lidar/ in your browser to view the examples.
# Build the image
docker build -t maplibre-gl-lidar .
# Run the container
docker run -p 8080:80 maplibre-gl-lidar| Tag | Description |
|---|---|
latest |
Latest release |
x.y.z |
Specific version (e.g., 1.0.0) |
x.y |
Minor version (e.g., 1.0) |
- deck.gl - WebGL visualization layers
- copc.js - COPC/LAS/LAZ parsing (LAS 1.2/1.4)
- @loaders.gl/las - LAS parsing fallback (LAS 1.0/1.1/1.3)
- laz-perf - LAZ decompression
- proj4 - Coordinate transformation
- maplibre-gl - Map rendering
MIT
- Microsoft Planetary Computer: USGS 3DEP Lidar Point Cloud Dataset
- AWS Open Data: USGS 3DEP LiDAR Point Clouds
- Hobu, Inc.
- COPC.io
- Entwine
