https://github.com/opengeos/maplibre-gl-lidar
A MapLibre plugin for visualizing LiDAR Point Cloud
https://github.com/opengeos/maplibre-gl-lidar
deck-gl geospatial lidar maplibre maplibre-gl-js mapping point-cloud
Last synced: about 1 month ago
JSON representation
A MapLibre plugin for visualizing LiDAR Point Cloud
- Host: GitHub
- URL: https://github.com/opengeos/maplibre-gl-lidar
- Owner: opengeos
- License: mit
- Created: 2026-01-10T20:45:33.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-01-30T17:05:16.000Z (4 months ago)
- Last Synced: 2026-01-31T10:12:03.719Z (4 months ago)
- Topics: deck-gl, geospatial, lidar, maplibre, maplibre-gl-js, mapping, point-cloud
- Language: TypeScript
- Homepage: https://opengeos.org/maplibre-gl-lidar
- Size: 785 KB
- Stars: 129
- Watchers: 3
- Forks: 19
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-maplibre - maplibre-gl-lidar - A MapLibre GL JS plugin for rendering LIDAR point cloud data. (Layer Types Plugins / JavaScript)
README
# maplibre-gl-lidar
A MapLibre GL JS plugin for visualizing LiDAR point clouds using deck.gl.
[](https://www.npmjs.com/package/maplibre-gl-lidar)
[](https://opensource.org/licenses/MIT)
[](https://codesandbox.io/p/github/opengeos/maplibre-gl-lidar)
[](https://stackblitz.com/github/opengeos/maplibre-gl-lidar)
## Features
- 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
- Vue integration with [@geoql/v-maplibre](https://github.com/geoql/v-maplibre)
- deck.gl PointCloudLayer with optimized chunking for large datasets
- TypeScript support
## Demo
Try the [live demo](https://opengeos.org/maplibre-gl-lidar).

## Online Viewer
Use the [Online Viewer](https://opengeos.org/maplibre-gl-lidar/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.
## Installation
```bash
npm install maplibre-gl-lidar
```
## Quick Start
### Basic Usage (Vanilla JS/TypeScript)
```typescript
import 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"
);
});
```
### React Usage
```tsx
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(null);
const [map, setMap] = useState(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 (
{map && (
console.log("Loaded:", pc)}
defaultUrl="https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz"
/>
)}
);
}
```
### Vue Usage
The [`@geoql/v-maplibre`](https://github.com/geoql/v-maplibre) package provides a `VControlLidar` component that wraps maplibre-gl-lidar for Vue/Nuxt applications.
```bash
npm install @geoql/v-maplibre maplibre-gl-lidar
```
```vue
import { VMap, VControlLidar } from "@geoql/v-maplibre";
import "maplibre-gl-lidar/style.css";
import "maplibre-gl/dist/maplibre-gl.css";
const mapOptions = {
style: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
center: [-123.075, 44.05],
zoom: 14,
pitch: 60,
maxPitch: 85,
};
const lidarOptions = {
collapsed: false,
pointSize: 2,
colorScheme: "elevation",
pickable: true,
autoZoom: true,
};
const copcUrl =
"https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz";
console.log('Loaded:', info)"
/>
```
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `LidarControlOptions` | See defaults | Configuration options for the LiDAR control |
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'top-right'` | Position of the control on the map |
| `defaultUrl` | `string` | — | URL of a point cloud file to auto-load on mount |
#### Events
All [LidarControl events](#events) are available as Vue events: `@load`, `@loadstart`, `@loaderror`, `@unload`, `@statechange`, `@stylechange`, `@collapse`, `@expand`, `@streamingstart`, `@streamingstop`, `@streamingprogress`, `@budgetreached`.
#### Exposed Methods
Access methods via template ref:
```vue
import { ref } from "vue";
import { VControlLidar } from "@geoql/v-maplibre";
const lidarRef = ref<InstanceType<typeof VControlLidar> | null>(null);
// Load a point cloud programmatically
const loadFile = async (url: string) => {
await lidarRef.value?.loadPointCloud(url);
};
// Classification control
const showOnlyBuildings = () => {
lidarRef.value?.hideAllClassifications();
lidarRef.value?.setClassificationVisibility(6, true);
};
```
Available methods: `loadPointCloud`, `unloadPointCloud`, `flyToPointCloud`, `setPointSize`, `setColorScheme`, `setOpacity`, `setPickable`, `setUsePercentile`, `setElevationRange`, `clearElevationRange`, `setZOffset`, `setZOffsetEnabled`, `toggle`, `expand`, `collapse`, `getState`, `getPointClouds`, `stopStreaming`, `isStreaming`.
**Vue examples:** [EPT Streaming](https://mapcn-vue.geoql.in/examples/lidar-ept) · [Classification Filter](https://mapcn-vue.geoql.in/examples/lidar-classification)
**Full docs:** [@geoql/v-maplibre LidarControl](https://v-maplibre.geoql.in/controls/lidar)
## API Reference
### LidarControl
The main control class implementing MapLibre's `IControl` interface.
#### Constructor Options
```typescript
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)
}
```
#### Methods
```typescript
// Loading
loadPointCloud(source: string | File | ArrayBuffer, options?: { loadingMode?: 'full' | 'dynamic' }): Promise
loadPointCloudStreaming(source: string | File | ArrayBuffer, options?: StreamingLoaderOptions): Promise
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 | undefined
```
#### Events
- `load` - Point cloud loaded successfully
- `loadstart` - Loading started
- `loaderror` - Loading failed
- `unload` - Point cloud unloaded
- `statechange` - Control state changed
- `stylechange` - Styling changed
- `collapse` - Panel collapsed
- `expand` - Panel expanded
- `streamingstart` - Dynamic streaming started
- `streamingstop` - Dynamic streaming stopped
- `streamingprogress` - Streaming progress update
- `budgetreached` - Point budget limit reached
### Color Schemes
- `'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)
### Percentile-Based Coloring
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.
```typescript
// 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 false
```
The 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.
### Point Picking
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:
```typescript
// 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",
]);
```
### Z Offset
Shift point clouds vertically for alignment with terrain or other data:
```typescript
// 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()); // 50
```
The Z offset can also be adjusted interactively via the "Z Offset" checkbox and slider in the GUI panel.
### Classification Legend
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
```typescript
// 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 |
### Dynamic COPC Streaming
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:**
1. When loading a COPC file (from URL or local file), dynamic mode is used by default
2. As you pan/zoom the map, new nodes are streamed based on viewport
3. Deeper octree levels (more detail) load as you zoom in
4. Parent nodes provide coverage where child nodes don't exist
```typescript
// 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 streaming
```
**Loading 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.
### EPT (Entwine Point Tile) Support
maplibre-gl-lidar supports [Entwine Point Tile (EPT)](https://entwine.io/en/latest/entwine-point-tile.html) 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:**
```typescript
// 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.
### React Hooks
#### useLidarState
```typescript
const {
state,
setState,
setPointSize,
setOpacity,
setColorScheme,
setUsePercentile,
setElevationRange,
setZOffsetEnabled,
setZOffset,
toggle,
reset,
} = useLidarState(initialState?);
```
#### usePointCloud
```typescript
const { data, loading, error, progress, load, reset } = usePointCloud();
// Load a file
await load(file);
console.log(`Loaded ${data.pointCount} points`);
```
## Supported Formats
- 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.
## Framework Integration
### Nuxt
maplibre-gl-lidar works with Nuxt via [`@geoql/v-maplibre`](https://github.com/geoql/v-maplibre). The `VControlLidar` component handles dynamic imports internally, so no additional bundler configuration is needed. Wrap the map in `` for SSR compatibility:
```vue
```
### Next.js
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`:
```javascript
/** @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;
```
## Coordinate Systems
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)
## Docker
The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
### Pull and Run
```bash
# 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:latest
```
Then open http://localhost:8080/maplibre-gl-lidar/ in your browser to view the examples.
### Build Locally
```bash
# Build the image
docker build -t maplibre-gl-lidar .
# Run the container
docker run -p 8080:80 maplibre-gl-lidar
```
### Available Tags
| Tag | Description |
| -------- | -------------------------------- |
| `latest` | Latest release |
| `x.y.z` | Specific version (e.g., `1.0.0`) |
| `x.y` | Minor version (e.g., `1.0`) |
## Dependencies
- [deck.gl](https://deck.gl/) - WebGL visualization layers
- [copc.js](https://github.com/connormanning/copc.js) - COPC/LAS/LAZ parsing (LAS 1.2/1.4)
- [@loaders.gl/las](https://loaders.gl/modules/las/docs) - LAS parsing fallback (LAS 1.0/1.1/1.3)
- [laz-perf](https://github.com/hobu/laz-perf) - LAZ decompression
- [proj4](http://proj4js.org/) - Coordinate transformation
- [maplibre-gl](https://maplibre.org/) - Map rendering
## License
MIT
## Credits
- Microsoft Planetary Computer: [USGS 3DEP Lidar Point Cloud Dataset](https://planetarycomputer.microsoft.com/dataset/usgs-3dep-lidar)
- AWS Open Data: [USGS 3DEP LiDAR Point Clouds](https://registry.opendata.aws/usgs-lidar)
- [Hobu, Inc.](https://hobu.co)
- [COPC.io](https://copc.io)
- [Entwine](https://entwine.io)