Skip to content

geozelot/Treelet

Repository files navigation

npm CI license gzip

Treelet

A lightweight 3D terrain & overlay map library for the web - inspired by Leaflet and powered by Three.js.

Treelet renders tiled elevation data as 3D terrain with drape overlays, LOD scheduling, and shader-based visualization - in the spirit of Leaflet's simple high-level API, solid low-level access, and zero bloat.

Features

  • Tile sources — XYZ, OGC WMS, OGC WMTS (RESTful + KVP)
  • Elevation decoding — Terrain-RGB, Terrarium, Mapbox, custom decoders
  • Visualization modes — Elevation, slope, aspect, contours, wireframe
  • Drape layers — Overlay satellite, OSM, or any raster imagery onto terrain
  • LOD scheduling — Nadir-based quadtree with cross-LOD seam resolution
  • Web Workers — Off-thread tile fetch, decode, and mesh generation
  • Fluent API — Chainable setters, Leaflet-style factory, EventEmitter

This had been a project related to my PHD and that I needed to keep private until now - thoroughly overhauled and elevated to current tech standards. 3D terrain rendering has become more commonly supported, but this library still shines with simplicity, ease-of-use and performance. It's especially useful for self-hosted elevation and overlay data.

Quick Start

Installation

npm install @geozelot/treelet three

CDN / Script Tag

For direct browser use without a bundler, use the standalone build which bundles Three.js:

<script src="https://unpkg.com/@geozelot/treelet/dist/treelet.cdn.standalone.js"></script>
<script>
  const map = Treelet.map('map', {
    center: { lng: 11.39, lat: 47.27 },
    zoom: 10,
  });
  // ...
  map.start();
</script>

Minimal Example

import { Treelet } from '@geozelot/treelet';

const map = Treelet.map('map', {
  center: { lng: 11.39, lat: 47.27 },
  zoom: 10,
});

map.addBaseLayer({
  id: 'terrain',
  name: 'AWS Terrain',
  source: {
    type: 'xyz',
    url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
    tileSize: 512,
    maxZoom: 15,
  },
  decoder: 'terrarium',
});

map.addDrapeLayer({
  id: 'osm',
  name: 'OpenStreetMap',
  source: {
    type: 'xyz',
    url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
    tileSize: 512,
    maxZoom: 19,
  },
});

map.start();

API Reference

Factory

Treelet.map(container, options)

Create a new Treelet map instance (static factory).

Param Type Description
container string | HTMLElement DOM element or element ID
options TreeletOptions Map configuration

Returns Treelet. Equivalent to new Treelet(container, options).

Treelet.version

Library version string (static).


TreeletOptions

Option Type Default Description
center LngLat required Initial map center { lng, lat }
zoom number required Initial zoom level
minZoom number 0 Minimum allowed zoom
maxZoom number 20 Maximum allowed zoom
tileSegments number 128 Mesh subdivisions per tile
worldScale number 40075.02 CRS meters → scene units scale
exaggeration number 1.0 Vertical exaggeration factor
elevationRange [number, number] [0, 4000] Min/max elevation for color mapping
contourInterval number 100 Contour line spacing in meters
antialias boolean true WebGL antialiasing
gui boolean true Show compass + layer panel
minPitch number 25 Minimum pitch (most tilted) in degrees
maxPitch number 90 Maximum pitch (top-down) in degrees
compassPosition UICorner 'top-right' Compass widget corner
layerPosition UICorner 'bottom-right' Layer panel corner
workerCount number navigator.hardwareConcurrency Web Worker pool size

Treelet

The main map class. Extends EventEmitter.

View

Method Returns Description
setView(center, zoom) this Set map center and zoom
getCenter() LngLat Get current map center
getZoom() number Get current zoom level
getBounds() { sw, ne } | null Get visible bounding box
getTileCount() number Get number of cached tiles

Base Layers

Only one base layer is active at a time. The active base layer provides elevation data for the terrain mesh.

Method Returns Description
addBaseLayer(options) this Add an elevation data layer
removeBaseLayer(id) this Remove a base layer by ID
setActiveBaseLayer(id) this Switch active base layer
getBaseLayers() BaseLayer[] Get all registered base layers
BaseLayerOptions
Option Type Default Description
id string required Unique layer identifier
name string required Display name
source LayerSourceOptions required Tile source configuration
decoder DecoderType 'terrain-rgb' Elevation decoder
decoderFn CustomDecoderFn Custom per-pixel decoder function
exaggeration number 1.0 Per-layer exaggeration multiplier
active boolean true Set as active on add

Drape Layers

Drape layers overlay raster imagery on the terrain mesh. Only one drape (external or BaseDrape) is active at a time.

Method Returns Description
addDrapeLayer(options) this Add an imagery overlay layer
removeDrapeLayer(id) this Remove a drape layer by ID
activateDrapeLayer(id) this Activate an external drape
activateBaseDrape(mode?) this Switch to elevation-derived visualization
setBaseDrapeMode(mode) this Change BaseDrape mode
getBaseDrapeMode() BaseDrapeMode Get current BaseDrape mode
getActiveDrapeId() string | null Active drape ID ('__base_drape__' for BaseDrape)
isBaseDrapeActive() boolean Check if BaseDrape is active
setDrapeOpacity(id, opacity) this Set drape opacity (0–1)
getDrapeOpacity(id) number Get drape opacity
getDrapeLayers() DrapeLayer[] Get all registered drape layers
DrapeLayerOptions
Option Type Default Description
id string required Unique layer identifier
name string required Display name
source LayerSourceOptions required Tile source configuration
opacity number 1.0 Layer opacity
blendMode BlendMode 'normal' Blend mode: 'normal', 'multiply', 'screen'
active boolean true Set as active on add
BaseDrapeMode

Elevation-derived visualization modes:

Mode Description
'wireframe' Mesh wireframe (normals-colored or flat white)
'elevation' Elevation-colored with lighting
'slope' Slope angle visualization
'aspect' Cardinal direction coloring
'contours' Elevation coloring with contour line overlay

Visualization

Method Returns Description
setColorRamp(ramp) this Set color ramp for current BaseDrape mode
getColorRamp() ColorRamp Get current color ramp
setExaggeration(value) this Set global vertical exaggeration
getExaggeration() number Get exaggeration factor
setContourInterval(meters) this Set contour line spacing
getContourInterval() number Get contour interval
setContourThickness(value) this Set contour line thickness
getContourThickness() number Get contour thickness
setContourColor(r, g, b) this Set contour color (RGB, 0–1 each)
getContourColor() [r, g, b] Get contour color
setWireframeWhite(white) this Toggle wireframe flat-white mode
getWireframeWhite() boolean Get wireframe white state
ColorRamp
Ramp Description
'hypsometric' Green → tan → brown → white (elevation default)
'viridis' Purple → teal → yellow (perceptually uniform)
'inferno' Black → purple → orange → white (high contrast)
'grayscale' Black → white

Camera

Method Returns Description
setMinPitch(degrees) this Set minimum pitch (most tilted), 0–90
getMinPitch() number Get minimum pitch
setMaxPitch(degrees) this Set maximum pitch (top-down), 0–90
getMaxPitch() number Get maximum pitch
getCameraController() CameraController Direct camera access

Lifecycle

Method Returns Description
start() this Begin rendering and tile loading
stop() this Pause rendering
destroy() void Clean up all resources

Events

map.on('ready', () => { /* initial tiles loaded */ });
map.on('move', ({ center }) => { /* camera moving */ });
Event Data Description
'ready' {} Initial tiles loaded
'zoom' { zoom } Zoom level changed
'move' { center: LngLat } Camera moving
'moveend' { center, zoom } Camera movement complete
'tileload' { coord: TileCoord } Tile loaded
'tileunload' { coord: TileCoord } Tile unloaded
'layeradd' { id } Layer added
'layerremove' { id } Layer removed
'layerchange' {} Layer configuration changed

Tile Sources

All layers accept a source object conforming to LayerSourceOptions.

LayerSourceOptions

Option Type Default Used by Description
type 'xyz' | 'wms' | 'wmts' required all Source type
url string required all Endpoint URL or template
subdomains string[] [] XYZ, WMTS Subdomain rotation via {s}
tileSize number 256 all Tile pixel size
maxZoom number 22 all Maximum zoom level
attribution string '' all Attribution text
layers string '' WMS, WMTS Layer name(s)
format string 'image/png' WMS, WMTS Image format
crs string 'EPSG:3857' WMS Coordinate reference system
style string 'default' WMTS WMTS style
tilematrixSet string 'EPSG:3857' WMTS WMTS TileMatrixSet

XYZ Source

Standard slippy-map tiles with {z}/{x}/{y} URL placeholders.

source: {
  type: 'xyz',
  url: 'https://{s}.tile.example.com/{z}/{x}/{y}.png',
  subdomains: ['a', 'b', 'c'],
  tileSize: 256,
  maxZoom: 19,
}

WMS Source

OGC WMS GetMap requests. Computes BBOX from tile coordinates with CRS reprojection.

source: {
  type: 'wms',
  url: 'https://example.com/geoserver/wms',
  layers: 'elevation',
  format: 'image/png',
  crs: 'EPSG:4326',
  tileSize: 256,
}

Supported CRS values: EPSG:3857, EPSG:900913, EPSG:4326, CRS:84.

WMTS Source

OGC WMTS GetTile requests. Two modes, auto-detected:

RESTful — URL contains {z} placeholder (treated like XYZ):

source: {
  type: 'wmts',
  url: 'https://example.com/wmts/rest/dem/default/EPSG3857/{z}/{y}/{x}.png',
}

KVP — URL without placeholders (constructs GetTile query):

source: {
  type: 'wmts',
  url: 'https://example.com/wmts',
  layers: 'dem',
  tilematrixSet: 'EPSG:3857',
  style: 'default',
  format: 'image/png',
}

Elevation Decoders

Built-in decoders for common DEM tile formats:

Decoder Formula Precision
'terrain-rgb' -10000 + (R×65536 + G×256 + B) × 0.1 0.1 m
'mapbox' Alias for terrain-rgb 0.1 m
'terrarium' R×256 + G + B/256 - 32768 ~0.004 m

Custom Decoder

import { createCustomDecoder, registerDecoder } from '@geozelot/treelet';

// Inline: pass directly as decoderFn
map.addBaseLayer({
  id: 'dem',
  name: 'Custom DEM',
  source: { type: 'xyz', url: '...' },
  decoder: 'custom',
  decoderFn: (r, g, b, a) => r * 100 + g,
});

// Or register globally by name
registerDecoder('myformat', createCustomDecoder((r, g, b, a) => r * 100 + g));
map.addBaseLayer({
  id: 'dem',
  name: 'Custom DEM',
  source: { type: 'xyz', url: '...' },
  decoder: 'myformat' as any,
});

Types

interface LngLat { lng: number; lat: number }
interface WorldPoint { x: number; y: number }
interface TileCoord { z: number; x: number; y: number }
interface TileBounds { west: number; south: number; east: number; north: number }

type BaseDrapeMode = 'wireframe' | 'elevation' | 'slope' | 'aspect' | 'contours';
type ColorRamp = 'hypsometric' | 'viridis' | 'inferno' | 'grayscale';
type BlendMode = 'normal' | 'multiply' | 'screen';
type DecoderType = 'terrain-rgb' | 'mapbox' | 'terrarium' | 'custom';
type UICorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

Low-Level API

For advanced use, treelet re-exports its internal building blocks:

Export Description
Treelet Main class with static Treelet.map() factory
LayerSource Abstract source base class
createSource() Factory: create a source from LayerSourceOptions
XYZSource, WMSSource, WMTSSource Concrete tile sources
BaseLayer, DrapeLayer Layer wrappers
LayerRegistry Layer management registry
DrapeCompositor Multi-resolution drape compositing
SeamResolver Cross-LOD edge seam resolution
CameraController Camera state + controls
WebMercator EPSG:3857 projection utilities
TileGrid Tile math (world ↔ tile coordinate conversion)
EventEmitter Base event system
createTerrainMaterial() Unified shader material factory
terrainRGBDecoder, terrariumDecoder Decoder instances
createCustomDecoder(), registerDecoder() Decoder registration

Controls

Input Action
Left-click drag Pan
Scroll wheel Zoom
Right-click drag Orbit (rotate + tilt)
Compass click Reset orientation

Bundles

Treelet ships two minified bundles:

File Format Three.js Use case
treelet.es.js ESM External Bundlers (Vite, Webpack, Rollup)
treelet.cdn.standalone.js IIFE Included <script> tag, CDN, no dependencies

Slim (treelet.es.js) — ~140 KB minified. Three.js is a peer dependency; install it alongside treelet via npm. Best for projects that already use Three.js or want control over the Three.js version.

Standalone (treelet.cdn.standalone.js) — ~580 KB minified. Bundles only the Three.js modules treelet actually uses (tree-shaken). Drop-in ready for CDN or direct <script> tag usage — zero external dependencies.

Both bundles require the tileWorker asset file (dist/assets/tileWorker-*.js) to be served from the same origin.

Browser Support

Modern browsers with WebGL2 and Web Workers. Requires ES2020+.

What's next

  • compositional overlay blend modes
  • additional elevation-derived visualization modes
  • support for more sources and formats
  • responsive UI

License

MIT

About

A slim JavaScript library for interactive 3D Terrain & Overlay maps - inspired by the simplicity of Leaflet

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors