npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bhumi-sdk2.0

v2.3.0

Published

Bhumi 3D map engine for three.js - terrain, your own DEM and street maps, drawing, and in-browser terrain analysis (slope, aspect, hillshade, contour, line of sight, dome)

Readme

bhumi-sdk2.0

npm version types

3D maps and terrain analysis for three.js. Stream any street map and any elevation (DEM) tiles onto a 3D terrain, draw points, lines and polygons on it, and run slope, aspect, hillshade, contour, line-of-sight, dome and viewshed analysis, all in the browser, with no backend.

  • Your own data: any XYZ / TMS street map or satellite imagery, any DEM in Mapbox Terrain-RGB, Terrarium or Bhumi encoding
  • 3D terrain with tile level of detail, vertical exaggeration and overzoom past the deepest level your server has
  • Drawing: points, lines and polygons that follow the terrain, with live length / area, editing and GeoJSON in / out
  • Coordinates bar: latitude, longitude and altitude under the cursor, camera heading, pitch and a scale bar
  • Terrain analysis: slope, aspect, hillshade, elevation and contours as map layers, limited to a polygon if you like, with polygon statistics
  • Visibility: line of sight, dome (sky coverage) and ground viewshed, with Earth curvature and refraction
  • TypeScript types included; ES module and UMD / CommonJS builds

Contents

Install

npm install bhumi-sdk2.0 three

three 0.156 or newer is a peer dependency.

Quick start

import { Scene, PerspectiveCamera, WebGLRenderer, AmbientLight } from 'three';
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
import { createMapView, TileProvider, DEMProvider, MapView, CoordinatesBar, UnitsUtils } from 'bhumi-sdk2.0';

const scene = new Scene();
scene.add(new AmbientLight(0xffffff, 1));
const camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1e12);
const renderer = new WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new MapControls(camera, renderer.domElement);
controls.zoomToCursor = true; // scroll zooms towards the point under the cursor

// Street map and elevation tiles (AWS Terrain Tiles are public, Terrarium encoded)
const streets = new TileProvider({ url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: [], maxNativeZoom: 19 });
const dem = new DEMProvider({ url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png', subdomains: [], encoding: 'terrarium', maxNativeZoom: 15 });

const map = createMapView({ mode: MapView.HEIGHT, imagery: streets, dem });
scene.add(map);

// Look at Kedarnath from 40 km away
const p = UnitsUtils.datumsToSpherical(30.7346, 79.0669);
controls.target.set(p.x, 0, -p.y);
camera.position.set(p.x, 40000, -p.y + 32000);

new CoordinatesBar({ view: map, camera, domElement: renderer.domElement, controls });

renderer.setAnimationLoop(() => {
  controls.update();
  renderer.render(scene, camera);
});

World units are Web Mercator metres: UnitsUtils.datumsToSpherical(lat, lon) gives x (east) and y (north); place things at (x, height, -y).

Street maps and DEMs

TileProvider (street map / imagery)

new TileProvider({
  url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png',
  subdomains: ['a', 'b', 'c', 'd'],
  maxZoom: 20,
});

// Mapbox styles (your own access token)
new TileProvider({ url: `https://api.mapbox.com/styles/v1/mapbox/streets-v12/tiles/256/{z}/{x}/{y}?access_token=${token}`, subdomains: [], maxNativeZoom: 22 });

| Option | Default | Description | | --- | --- | --- | | url | required | Template with {z}, {x}, {y}; {-y} for the TMS row; {s} for a subdomain | | subdomains | ['a', 'b', 'c'] | Values rotated into {s} (an array or a string such as 'abc'); only used when the URL has {s} | | tms | false | Rows counted from the south; or (zoom) => boolean for servers that mix TMS and XYZ | | minZoom / maxZoom | 0 / 20 | Levels the map subdivides between | | maxNativeZoom | maxZoom | Deepest level the server has; deeper tiles are cut from it and enlarged | | crossOrigin | 'anonymous' | null leaves it unset |

DEMProvider (elevation)

new DEMProvider({
  url: 'https://dem.example.com/{z}/{x}/{y}.png',
  encoding: 'terrarium',
  maxNativeZoom: 14,
  exaggeration: 1.5,
});

Takes the same url, subdomains, tms, minZoom and crossOrigin options, plus:

| Option | Default | Description | | --- | --- | --- | | encoding | 'mapbox' | 'mapbox' (Terrain-RGB), 'terrarium', 'bhumi', or your own (r, g, b) => metres | | maxNativeZoom | 15 | Deepest DEM level; deeper tiles are interpolated (bilinear) | | exaggeration | 1 | Vertical scale of the 3D terrain (analyses always use true heights) | | noData | none | Source value meaning "no data"; read as 0 m | | mode | MapView.HEIGHT | Map mode it feeds; createMapView sets it for you |

A DEM tile that fails to load (outside coverage, 404) is drawn flat at 0 m, so the map keeps refining. dem.sampleElevations([{ latitude, longitude }]) reads true heights in metres at any position.

createMapView

createMapView({ mode, imagery, dem, lod, mapboxToken }): MapView

Builds the map and matches the DEM's tile format to the mode. Without mapboxToken the engine's Mapbox fallback for failed imagery tiles is switched off.

| Mode | Use | | --- | --- | | MapView.PLANAR | Flat map (default without a DEM) | | MapView.HEIGHT | 3D terrain (default with a DEM, recommended) | | MapView.SPHERICAL | Globe | | MapView.HEIGHT_SHADER, MapView.MARTINI | Not usable with three ≥ 0.152 (see limitations) |

Drawing points, lines and polygons

import { DrawTool } from 'bhumi-sdk2.0';

const draw = new DrawTool({ view: map, camera, domElement: renderer.domElement, scene, controls });

draw.start('polygon'); // 'point' | 'line' | 'polygon'; stays active until draw.stop()
draw.on('create', (feature) => {
  // GeoJSON Feature with [longitude, latitude, height above sea level]
  console.log(feature.properties.area_m2, feature.properties.perimeter_m);
});

| Action | How | | --- | --- | | Add a vertex | Click (a drag pans the map as usual) | | Finish | Double-click or Enter; a polygon also closes when you click its first corner | | Undo last vertex | Backspace or Ctrl+Z | | Cancel / leave the tool | Esc / Esc again | | Edit | When not drawing: click a shape, drag its vertices, Delete removes it |

Lines keep their pixel width at every zoom, vertices are round antialiased markers, and shapes follow the terrain between vertices. While drawing, a dashed guide follows the cursor and a label shows the length or area.

| Member | Description | | --- | --- | | start(mode), stop(), finish(), undo() | Drawing control | | select(id), getSelected(), remove(id), clear() | Shapes | | toGeoJSON(), addGeoJSON(data) | Point, LineString, Polygon and Multi* in and out; lines carry length_m, polygons area_m2 and perimeter_m | | setStyle({ color, selectedColor, width, fillOpacity, vertexSize, pointSize }) | Look | | on(event, fn) | drawstart, drawend, create, update, delete, select | | setView(map), dispose() | Lifecycle |

Options: style, measure (live label, default true), editable (default true), drape (follow the terrain, default true). Helpers distance, pathLength, ringArea, formatLength and formatArea are exported.

Coordinates bar and picking

import { CoordinatesBar, MapPicker } from 'bhumi-sdk2.0';

new CoordinatesBar({ view: map, camera, domElement: renderer.domElement, controls });

const picker = new MapPicker(map, camera, renderer.domElement);
picker.pickGeo(event.clientX, event.clientY); // { latitude, longitude, altitude } or null

The bar shows latitude, longitude (click to switch decimal degrees / DMS), altitude above sea level under the cursor, camera altitude, heading, pitch, a scale bar and a copy button; C over the map copies lat, lon, altitude. Options: container, dms, copyKey (null turns it off), logo (image URL).

MapPicker also has pick (world position), toGeo, toWorld and heightAt. Altitudes are above sea level with the terrain exaggeration taken out.

Terrain analysis

Computed in the browser from a DEMProvider and drawn over your street map.

import { TerrainAnalysisProvider, createMapView } from 'bhumi-sdk2.0';

const analysis = new TerrainAnalysisProvider({ dem, base: streets, analysis: 'slope', opacity: 0.6 });
const map = createMapView({ imagery: analysis, dem });

analysis.legend();                            // [{ color, label }] for a legend
analysis.setAnalysis('hillshade'); map.clear(); // switch later and redraw

| analysis | Method | Shown as | | --- | --- | --- | | slope | Horn's 3 × 3 gradient | Classes 0–5, 5–15, 15–25, 25–35, 35–45, 45–60, 60–90° | | aspect | Direction the slope faces (ESRI convention) | 8 compass colours; flat (< 2°) grey | | hillshade | ESRI formula, sun azimuth 315°, altitude 45° | Shading multiplied over the imagery | | elevation | Height above sea level | Colour ramp 0 → 5,000 m | | contour | Marching squares | Lines; interval follows zoom and relief (5, 10, 50, 100, 500 or 1,000 m), every 5th thicker |

| Option | Default | Description | | --- | --- | --- | | dem | required | Elevation source | | base | none | Street map drawn underneath | | analysis | 'hillshade' | One of the above | | opacity | 0.6 | Colour opacity / hillshade strength | | contours | false | Contours on top of any other analysis | | contourInterval | auto | Fixed interval in metres | | azimuth, altitude | 315, 45 | Hillshade sun position | | zFactor | 1 | Vertical exaggeration for slope / hillshade maths | | smooth | 2 for Bhumi DEMs, else 0 | 3 × 3 smoothing passes, to hide the height steps of coarse DEMs | | clip | null | Only analyse inside these polygons |

Inside a polygon

import { polygonTerrainStats } from 'bhumi-sdk2.0';

draw.on('create', async (feature) => {
  if (feature.geometry.type !== 'Polygon') return;

  // Show the analysis only inside the drawn polygons
  const rings = draw.toGeoJSON().features.filter((f) => f.geometry.type === 'Polygon').map((f) => f.geometry.coordinates[0]);
  analysis.setClip(rings); // [] = nowhere, null = whole map
  map.clear();

  // Terrain statistics of this polygon
  const stats = await polygonTerrainStats(dem, feature.geometry.coordinates[0]);
});

| polygonTerrainStats field | Meaning | | --- | --- | | area, surfaceArea | Map area and area along the terrain, m² | | elevation.min, .max, .mean | Metres above sea level | | slope.mean, .max | Degrees | | slope.classes, aspect.classes | { label, color, percent } per class, same classes as the legend | | samples, zoom | DEM samples used and the zoom they were read at |

Line of sight

import { LineOfSight } from 'bhumi-sdk2.0';

const los = new LineOfSight({ view: map, camera, domElement: renderer.domElement, scene, dem, observerHeight: 1.7, targetHeight: 0 });
los.start(); // click the observer, then the target
los.on('result', (r) => console.log(r.visible, r.clearance, r.obstruction));

// or without clicks
await los.analyze({ latitude: 30.73, longitude: 79.06 }, { latitude: 30.75, longitude: 79.10 });

The line is drawn green up to the first obstruction and red after it. The result has visible, distance, observer, target, obstruction (position and distance), clearance (smallest gap to the terrain, negative when blocked) and the full profile for a chart. Options: observerHeight (1.7 m), targetHeight (0 m), curvature (true), refraction (0.13), visibleColor, blockedColor.

Dome and viewshed

import { DomeAnalysis, computeViewshed } from 'bhumi-sdk2.0';

const dome = new DomeAnalysis({ view: map, camera, domElement: renderer.domElement, scene, dem, radius: 3000, observerHeight: 10 });
dome.start(); // click a point
dome.on('result', (r) => console.log(r.visiblePercent, r.viewshed?.visiblePercent));

// Ground viewshed only, as data
const v = await computeViewshed(dem, { latitude: 30.73, longitude: 79.06 }, { radius: 3000, observerHeight: 10 });
  • Dome: every direction of a hemisphere (5° azimuth steps, 18 elevation bands) is tested out to radius; open directions green, directions the terrain blocks red. visiblePercent is by solid angle; cells holds each direction.
  • Viewshed: every ground cell inside the radius is tested from the observer and draped on the terrain, green seen and red hidden; viewshed.visiblePercent is the share of ground that can be seen. computeViewshed returns the grid (visibility, elevations, cellToLatLon).

Options: radius (2000 m), observerHeight (10 m), targetHeight (0 m), minElevation (0°, negative looks down), azimuthSteps, elevationSteps, curvature, refraction, viewshed (true), visibleColor, blockedColor, opacity, viewshedOpacity.

Accuracy

Each analysis was checked against an independent calculation or a case with a known answer:

| Check | Reference | Result | | --- | --- | --- | | Slope on a real Himalaya DEM tile | ESRI Horn formula in numpy, WGS84 geodesic pixel size | Max difference 0.025° | | Aspect, hillshade on the same tile | ESRI formulas in numpy | Identical; max 0.11 of 255 | | Contours | Height at every drawn point | Max error 0.0000 m over 18,659 segments | | Line of sight, 10 m to 10 m over sea level | Horizon formula √(2Rh₁) + √(2Rh₂) | 24.23 km vs 24.20 km | | Dome on flat ground | Ground-hit angle, solid angle | No direction wrong; 70.5 % vs 70.0 % | | Viewshed on flat ground / hilly terrain | Horizon distance / brute-force line of sight to every cell | 12.17 vs 12.10 km; 98.3 % of cells agree | | Polygon stats on a 20° plane | Geometry | Mean slope 20.000°, surface / map area = 1 / cos 20° | | Lengths and areas | WGS84 geodesic (pyproj) | +0.04 % and +0.11 % (spherical Earth model) |

Results can only be as good as the DEM: a ~30 m pixel averages summits, so peaks read a little low.

Using it without a bundler

dist/bhumi-sdk.umd.cjs works with require() and as a <script>: load three's global build first (it defines THREE); the SDK is then available as the global BhumiSDK.

Requirements and limitations

  • A browser with WebGL. Tile servers must send CORS headers, because tiles become WebGL textures.
  • three ≥ 0.156.
  • MapView.HEIGHT_SHADER and MapView.MARTINI render black with three ≥ 0.152 (their shader reads vUv, which three.js removed); use MapView.HEIGHT.
  • Keep the default LODRaycast on flat (PLANAR) maps; LODFrustum does not refine them.
  • Shapes and analysis overlays are drawn on top of the terrain, so they stay visible behind hills.
  • Lengths and areas use a spherical Earth (within about 0.1 % of WGS84).

License

MIT © 2026 HeliBhumi. See LICENSE.

The map engine is a modified version of geo-three (MIT); the package also bundles code from martini (ISC), three.js and global-mercator (MIT). Their notices are in THIRD_PARTY_NOTICES.md.