Protocol Buffers-based raster elevation tiles.
3-tier zoom LOD · GEBCO + ALOS dual sources · Int16 delta + deflateRaw · Worker zero-copy
altpbf is a binary raster elevation tile format built on Protocol Buffers. Where GeoPBF handles vector geometry (coastlines, roads, polygons), altpbf handles raster elevation grids — the digital elevation model (DEM) and bathymetry data needed for terrain rendering, shadow calculation, and 3D visualization.
Each tile is a rectangular grid of Int16 elevation values (meters),
delta-encoded and compressed with deflateRaw.
Three tile sizes cover the globe at different resolutions,
and the appropriate tier is selected automatically based on zoom level.
Earth's elevation range is roughly −11,000 m (Mariana Trench) to +8,849 m (Everest). A signed 16-bit integer covers −32,768 to +32,767 m — exactly enough, with no wasted bits. Compared to Float32, Int16 halves the data size before any compression is applied.
| Source | Coverage | Resolution | Used for |
|---|---|---|---|
| GEBCO General Bathymetric Chart of the Oceans |
Global (land + sea) | ~450 m (15 arc-sec) | HGT90 · HGT10 |
| ALOS AW3D30 JAXA ALOS World 3D |
Land only | ~30 m (1 arc-sec) | HGT01 |
GEBCO provides global coverage including ocean bathymetry, making it the baseline for all zoom levels. ALOS AW3D30 is a high-precision land-only DEM published by JAXA, used for close-up terrain rendering where its 30 m resolution delivers visually meaningful detail. Since ALOS tiles only exist for land areas, altpbf maintains an index of available tiles and falls back gracefully to GEBCO HGT10 for ocean queries.
altpbf defines three tile sizes, each covering a different angular extent of the globe.
createGetHeight switches between tiers based on the current zoom level,
defaulting to level1 = 7 and level2 = 12.
Each tier is a strict fallback chain: HGT01 → HGT10 → HGT90. If a high-resolution tile is unavailable (ocean, no ALOS data) or returns no elevation, the next coarser tier answers the query transparently. The caller always receives a valid elevation value.
Within each tile, calcHeight(x, y, v) performs bilinear interpolation
across the four nearest grid cells — no staircase artifacts.
Normalized coordinates (0–1) within the tile are computed from the raw lat/lng
and passed directly to the interpolation function.
Each .altpbf tile is a deflateRaw-compressed Protocol Buffers message.
deflateRaw / inflateRawTerrain elevation changes gradually — neighboring cells in a DEM rarely differ by more than a few tens of metres. Delta encoding exploits this locality: storing differences rather than absolute values keeps numbers small, and small numbers encode to fewer Varint bytes.
After delta encoding, most values in a smooth terrain tile fit in a single Varint byte. Combined with deflateRaw on top, altpbf tiles are typically 5–15× smaller than an equivalent raw Int16 array.
altpbf2png renders a tile as a PNG image using altitude/depth step colors:
Each tile is uniquely identified by a compact name string encoding its range, latitude, and longitude:
encodeName(lng, lat, range)
// range > 0 → "R{02d(range)}{lat_prefix}{03d(|lat|)}{lng_prefix}{03d(|lng|)}"
// range = 0 → "{lat_prefix}{03d(|lat|)}{lng_prefix}{03d(|lng|)}" (ALOS 1° tiles)
// Examples:
encodeName(0, -90, 90) // → "R90S090E000" (HGT90 south-west quadrant)
encodeName(130, 30, 10) // → "R10N030E130" (HGT10 Japan region)
encodeName(135, 35, 0) // → "N035E135" (HGT01 ALOS, Kinki region)
ALOS AW3D30 tiles exist only over land. Before requesting an HGT01 tile,
altpbf checks an in-memory index loaded from the JAXA tile list
(index_alos()). Ocean queries immediately fall back to HGT10
without a network round-trip.
// worker.js — zero-copy Int16Array transfer to main thread
import { load } from "./altpbf.js";
onmessage = async e => {
const { name, apiUrl } = e.data;
if (apiUrl) setApiUrl(apiUrl);
const obj = await load(name);
obj ? postMessage(obj, [obj.data.buffer]) // ← Transferable: zero-copy
: postMessage(null);
};
The worker inflates and decodes the tile off-main-thread,
then transfers the Int16Array buffer back via the Transferable interface —
no copying, no serialization.
A single worker handles requests one at a time; if a load is already in flight,
subsequent callers wait for the same Promise.
Once decoded, tiles are persisted to IndexedDB for instant re-use across sessions.
The raw GEBCO source is a global GeoTIFF at 21,600 × 21,600 pixels per 90° quadrant
(total 43,200 × 21,600 for the globe).
The GEBCO() function downloads and tiles it into altpbf format:
// Step 1 — HGT90: 8× average-pool downsampling
// 21600 × 21600 → 2700 × 2700 (one 90° tile per quadrant)
// Each output pixel = mean of 8×8 source pixels
// Step 2 — HGT10: extract 9×9 = 81 sub-tiles per quadrant
// Apply polar width shrink so tiles remain square on-screen
// shrink(lat): fraction of full-width actually used at that latitude
Meridians converge toward the poles, so a 10° longitude span represents far fewer kilometres at high latitudes than at the equator. GEBCO HGT10 tile generation compensates by reducing the pixel width of polar tiles:
Tile pixel width relative to equatorial baseline. Polar tiles are narrower, matching the reduced real-world extent per degree of longitude.
// ── createGetHeight — main entry point ─────────────────
const getHeight = await createGetHeight({
apiUrl: 'https://your-api.example.com', // nativeBucket base URL
level1: 7, // zoom threshold HGT90 → HGT10
level2: 12, // zoom threshold HGT10 → HGT01
onstart: name => console.log(`loading ${name}...`),
onend: name => console.log(`done ${name}`),
});
// Query elevation at any (lng, lat, zoom)
const h = await getHeight(139.77, 35.68, 14); // → height in metres
// Zoom omitted → always use highest resolution (HGT01)
const h = await getHeight(139.77, 35.68);
// ── altpbf2png — render tile as PNG ────────────────────
const blob = await altpbf2png(pbfBlob, {
size: 256, // output PNG size (square)
colorMap: h => [r, g, b] // custom color function (optional)
});
// ── Low-level encode / decode ───────────────────────────
const buf = await encode({ name, source, lng, lat, range, width, height, data });
const obj = await decode(blob); // → { name, source, lng, lat, range, width, height, data: Int16Array }
// ── GEBCO bulk tile generation ──────────────────────────
await GEBCO({ year: 2026, noIce: false, log: console });
// Downloads ~12 GB source, generates HGT90 + HGT10 altpbf tiles, uploads to bucket
// ── ALOS tile index ─────────────────────────────────────
const index = await index_alos();
// → { "N035E135": "v2404", "N036E135": "v2404", ... }