The Problem
Displaying massive LiDAR point clouds in a web map requires on‑the‑fly loading, CRS conversion, and interactive styling. Existing MapLibre plugins handle raster tiles only, so developers must write custom loaders, manage streaming, and build UI controls for color schemes, filters, and point picking.
What This Does
The repo delivers a MapLibre GL JS plugin (src/index.ts) that wraps a Deck.gl PointCloudLayer. Core logic lives in src/lib/core/LidarControl.ts (control lifecycle, state handling) and the streaming loaders src/lib/loaders/CopcStreamingLoader.ts and src/lib/loaders/EptStreamingLoader.ts. GUI components such as the classification legend and range sliders are in src/lib/gui/.
Example entry points – examples/basic/main.ts, examples/ept/main.ts, and the React demo examples/react/main.tsx – show how to instantiate LidarControl (vanilla) or LidarControlReact (React hook) and call loadPointCloud() with a COPC/EPT URL. The plugin ships CSS (src/lib/styles/lidar-control.css) and WebAssembly binaries (public/*.wasm) for LAZ decoding.
How It Is Wired
Execution begins in an example file, e.g. examples/react/main.tsx → App component (line 14). The component calls useLidarState() (from src/lib/hooks/useLidarState.ts) which internally creates a LidarControlReact instance (src/lib/core/LidarControlReact.tsx).
When loadPointCloud(url) is invoked, the call path is:
LidarControlReact.loadPointCloud→LidarControl.loadPointCloud(src/lib/core/LidarControl.ts).LidarControldecides the loader based on URL and creates eitherCopcStreamingLoaderorEptStreamingLoader(src/lib/loaders/*).- The loader calls
createBufferGetter→ WebAssemblylaz-perf(public/laz-perf.wasm) to decode LAS/LAZ chunks. - Decoded buffers flow to
PointCloudManager(src/lib/layers/PointCloudManager.ts) which builds a Deck.glPointCloudLayerand registers it on the MapLibre map. - UI updates (point size, color scheme) travel through
PanelBuilder(src/lib/gui/PanelBuilder.ts) and the React hookuseLidarState, which ultimately callsetState(found 23 callers) and emit events via_emit(24 callers).
The blast‑radius modules are:
src/lib/core/LidarControl.ts– 64 internal functions, called from 4 files, emits events, holds the main state machine.src/lib/loaders/CopcStreamingLoader.ts– 32 functions, creates buffers and attribute arrays, called from 3 files.src/lib/core/types.ts– a pure type hub with 15 dependents; changes here affect most of the code base.
No circular imports were detected, but the high instability scores (e.g., LidarControl 0.83) indicate many downstream dependencies.
How To Use It
# Clone the repo
git clone https://github.com/moses-y/maplibre-gl-lidar
cd maplibre-gl-lidar
# Install dependencies
npm ci
# Run a demo (basic example)
npm run dev -- --config vite.examples.config.ts # uses Vite to serve examples/basic
The package.json defines the dev script via Vite; the vite.examples.config.ts configures the example server.
For containerised builds:
docker build -t maplibre-gl-lidar .
docker run -p 5173:5173 maplibre-gl-lidar # serves the Vite dev server inside the container
The plugin CSS must be imported (import "maplibre-gl-lidar/style.css"). No additional environment variables are required—point‑cloud URLs are passed directly to loadPointCloud().
Real‑World Use
A municipal GIS portal can embed the plugin in a React dashboard:
import { LidarControlReact, useLidarState } from "maplibre-gl-lidar/react";
function CityLidar() {
const { setColorScheme, setPointSize } = useLidarState();
return (
<LidarControlReact
url="https://s3.amazonaws.com/city/streets.copc.laz"
onLoad={() => setColorScheme("classification")}
pointSize={3}
/>
);
}
The control streams only the viewport‑visible nodes, keeping bandwidth low while allowing analysts to toggle classification layers and adjust Z‑offset for alignment with orthophotos.
Code Health & Issues
- High – Pin GitHub Action versions to commit SHAs (
.github/workflows/*). - Medium – Declare least‑privilege
GITHUB_TOKENpermissions (publish.yml). - Medium – Pin Docker base images by digest (
Dockerfile). - Medium – Add a dependency‑vulnerability scan to CI.
- Medium – Set
persist-credentials: falseon checkout (deploy-pages.yml). - Medium – Run container as non‑root (
Dockerfile). - Medium – Test coverage is low (2 test files vs 41 source files).
- Low – Define job timeouts in workflows (
deploy-pages.yml).
Additional observations: the three oversized modules (LidarControl.ts, PointCloudLoader.ts, CopcStreamingLoader.ts) each exceed 1 400 lines, making single‑file changes high‑risk. Repeated 6‑line blocks appear across examples and the library entry point, suggesting a DRY refactor. The hub module src/lib/core/types.ts has the highest fan‑in; keeping it stable is critical.
The Bottom Line
maplibre-gl-lidar provides a functional, TypeScript‑first plugin for streaming LiDAR into MapLibre, with both vanilla and React APIs. It works out‑of‑the‑box for demos, but the core codebase is large, tightly coupled, and under‑tested, which raises maintenance overhead. It suits teams that need quick visualisation of COPC/EPT clouds and are prepared to invest in refactoring and test expansion for production use.