Build terrain.pmtiles from Mapterhorn instead of fetching tiles ourselves
Mapterhorn (mapterhorn.com, Protomaps-affiliated) publishes pre-built Terrarium-encoded terrain as PMTiles: a global planet.pmtiles (z0-12) plus ~450 regional chunks (z13+). Same pmtiles extract --bbox=... mechanism offline.pmtiles already uses, just against their files — confirmed live against their real manifest and the documented Interlaken example. Replaces fetching every individual tile ourselves (even after this session's earlier concurrency fix, still tens of minutes for a real region) with 1-3 extract calls plus a merge when the bbox spans more than one chunk: seconds, not minutes. Removes CONCURRENT_TILE_FETCHES, write_terrarium_mbtiles, tile_range, lng/lat_to_tile_x/y, fetch_terrarium_tile, and the sqlite3 gem entirely — none of it is needed once the tiles arrive pre-packaged. dem.js's point-elevation lookup (shadow.js's terrain-shadow overlay) hardcoded a 256px PNG tile assumption that Mapterhorn's 512px WebP tiles would have silently broken — now reads the actually-decoded tile's own size and mime type instead of assuming either.
This commit is contained in:
1 parent
d041c21270
commit
f89dea5805
8 files changed
+224
-234
No files matched your search
+19
-17
@@ -7,11 +7,14 @@
|
||||
import { ONLINE_TERRAIN_TILES } from "/js/overlays.js";
|
||||
|
||||
const DEM_ZOOM = 10; // fixed lookup zoom, independent of the map's own display zoom
|
||||
const TILE_SIZE = 256;
|
||||
|
||||
const tileCache = new Map(); // "z/x/y" -> Promise<Int16Array|null>, so concurrent lookups on the same not-yet-fetched tile share one fetch
|
||||
const tileCache = new Map(); // "z/x/y" -> Promise<{elevations,size}|null>, so concurrent lookups on the same not-yet-fetched tile share one fetch
|
||||
let offlinePmtiles = null;
|
||||
|
||||
// fracX/fracY (not pixel coordinates): the tile hasn't been fetched yet at
|
||||
// this point, so its actual pixel size isn't known — online tiles are
|
||||
// 256px PNG, offline ones (Mapterhorn, see lib/maptool/pmtiles_builder.rb)
|
||||
// are 512px WebP. elevationAt scales these once the real size is known.
|
||||
function lngLatToTile(lng, lat, zoom) {
|
||||
const n = 2 ** zoom;
|
||||
const x = ((lng + 180) / 360) * n;
|
||||
@@ -19,16 +22,11 @@ function lngLatToTile(lng, lat, zoom) {
|
||||
const y = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n;
|
||||
const tileX = Math.floor(x);
|
||||
const tileY = Math.floor(y);
|
||||
return {
|
||||
tileX,
|
||||
tileY,
|
||||
px: Math.min(TILE_SIZE - 1, Math.floor((x - tileX) * TILE_SIZE)),
|
||||
py: Math.min(TILE_SIZE - 1, Math.floor((y - tileY) * TILE_SIZE)),
|
||||
};
|
||||
return { tileX, tileY, fracX: x - tileX, fracY: y - tileY };
|
||||
}
|
||||
|
||||
async function decodeTerrarium(pngBytes) {
|
||||
const bitmap = await createImageBitmap(new Blob([pngBytes], { type: "image/png" }));
|
||||
async function decodeTerrarium(bytes, mime) {
|
||||
const bitmap = await createImageBitmap(new Blob([bytes], { type: mime }));
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
@@ -37,18 +35,18 @@ async function decodeTerrarium(pngBytes) {
|
||||
for (let i = 0; i < elevations.length; i++) {
|
||||
elevations[i] = data[i * 4] * 256 + data[i * 4 + 1] + data[i * 4 + 2] / 256 - 32768;
|
||||
}
|
||||
return elevations;
|
||||
return { elevations, size: bitmap.width };
|
||||
}
|
||||
|
||||
async function fetchTileBytes(z, x, y, offlineAvailable) {
|
||||
if (offlineAvailable) {
|
||||
if (!offlinePmtiles) offlinePmtiles = new window.pmtiles.PMTiles(`${window.location.origin}/tiles/terrain.pmtiles`);
|
||||
const result = await offlinePmtiles.getZxy(z, x, y);
|
||||
return result ? new Uint8Array(result.data) : null;
|
||||
return result ? { bytes: new Uint8Array(result.data), mime: "image/webp" } : null;
|
||||
}
|
||||
const url = ONLINE_TERRAIN_TILES[0].replace("{z}", z).replace("{x}", x).replace("{y}", y);
|
||||
const response = await fetch(url);
|
||||
return response.ok ? new Uint8Array(await response.arrayBuffer()) : null;
|
||||
return response.ok ? { bytes: new Uint8Array(await response.arrayBuffer()), mime: "image/png" } : null;
|
||||
}
|
||||
|
||||
function fetchTile(z, x, y, offlineAvailable) {
|
||||
@@ -57,7 +55,7 @@ function fetchTile(z, x, y, offlineAvailable) {
|
||||
if (cached) return cached;
|
||||
|
||||
const promise = fetchTileBytes(z, x, y, offlineAvailable)
|
||||
.then((bytes) => (bytes ? decodeTerrarium(bytes) : null))
|
||||
.then((result) => (result ? decodeTerrarium(result.bytes, result.mime) : null))
|
||||
.catch(() => null);
|
||||
tileCache.set(key, promise);
|
||||
return promise;
|
||||
@@ -66,7 +64,11 @@ function fetchTile(z, x, y, offlineAvailable) {
|
||||
// -> Promise<number|null> — null on a fetch/decode failure, or a point
|
||||
// falling outside a partial offline extract's zoom range.
|
||||
export async function elevationAt(lng, lat, offlineAvailable) {
|
||||
const { tileX, tileY, px, py } = lngLatToTile(lng, lat, DEM_ZOOM);
|
||||
const elevations = await fetchTile(DEM_ZOOM, tileX, tileY, offlineAvailable);
|
||||
return elevations ? elevations[py * TILE_SIZE + px] : null;
|
||||
const { tileX, tileY, fracX, fracY } = lngLatToTile(lng, lat, DEM_ZOOM);
|
||||
const tile = await fetchTile(DEM_ZOOM, tileX, tileY, offlineAvailable);
|
||||
if (!tile) return null;
|
||||
|
||||
const px = Math.min(tile.size - 1, Math.floor(fracX * tile.size));
|
||||
const py = Math.min(tile.size - 1, Math.floor(fracY * tile.size));
|
||||
return tile.elevations[py * tile.size + px];
|
||||
}
|
||||
+11
-1
@@ -80,8 +80,18 @@ class HillshadeControl {
|
||||
_addLayer() {
|
||||
if (this._map.getSource(HILLSHADE_ID)) return;
|
||||
|
||||
// terrain.pmtiles is built from Mapterhorn (mapterhorn.com — see
|
||||
// lib/maptool/pmtiles_builder.rb) whether generated in-app or by hand
|
||||
// (docs/development.adoc), so this credits them regardless of how it
|
||||
// got there — their own site lists the full per-region source
|
||||
// provenance (CC-BY-4.0 and similar), not practical to reproduce here.
|
||||
const source = this._offlineAvailable
|
||||
? { type: "raster-dem", url: `pmtiles://${window.location.origin}/tiles/terrain.pmtiles`, encoding: "terrarium" }
|
||||
? {
|
||||
type: "raster-dem",
|
||||
url: `pmtiles://${window.location.origin}/tiles/terrain.pmtiles`,
|
||||
encoding: "terrarium",
|
||||
attribution: "Terrain: Mapterhorn",
|
||||
}
|
||||
: {
|
||||
type: "raster-dem",
|
||||
tiles: ONLINE_TERRAIN_TILES,
|
||||
|
||||
Reference in new issue
Block a user