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:

  1. LidarControlReact.loadPointCloud → LidarControl.loadPointCloud (src/lib/core/LidarControl.ts).
  2. LidarControl decides the loader based on URL and creates either CopcStreamingLoader or EptStreamingLoader (src/lib/loaders/*).
  3. The loader calls createBufferGetter → WebAssembly laz-perf (public/laz-perf.wasm) to decode LAS/LAZ chunks.
  4. Decoded buffers flow to PointCloudManager (src/lib/layers/PointCloudManager.ts) which builds a Deck.gl PointCloudLayer and registers it on the MapLibre map.
  5. UI updates (point size, color scheme) travel through PanelBuilder (src/lib/gui/PanelBuilder.ts) and the React hook useLidarState, which ultimately call setState (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_TOKEN permissions (publish.yml).
  • Medium – Pin Docker base images by digest (Dockerfile).
  • Medium – Add a dependency‑vulnerability scan to CI.
  • Medium – Set persist-credentials: false on 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.