Add an offline terrain shadow overlay to the Overlays panel
Shades the visible map by whether each point is currently blocked from the sun by surrounding terrain (shademap.app-style), computed fully offline via the same Terrarium DEM tiles the Terrain shading layer already renders, plus this session's sun-position math and Radar's checkbox+expandable UI shape. dem.js pulls elevation-at-a-point out on its own since TODO.adoc's slope readout and solar calculator will need the same lookup.
This commit is contained in:
1 parent
a8d58abaca
commit
c9561a5489
7 files changed
+357
-3
No files matched your search
@@ -0,0 +1,72 @@
|
||||
// Elevation lookups against the same Terrarium-encoded terrain tiles
|
||||
// overlays.js's HillshadeControl already renders — pulled into its own file
|
||||
// since TODO.adoc lists two more near-future features (slope readout, solar
|
||||
// calculator) that will need the same "elevation at a point" primitive,
|
||||
// shadow.js's raymarch being the first real consumer.
|
||||
|
||||
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
|
||||
let offlinePmtiles = null;
|
||||
|
||||
function lngLatToTile(lng, lat, zoom) {
|
||||
const n = 2 ** zoom;
|
||||
const x = ((lng + 180) / 360) * n;
|
||||
const latRad = (lat * Math.PI) / 180;
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
||||
async function decodeTerrarium(pngBytes) {
|
||||
const bitmap = await createImageBitmap(new Blob([pngBytes], { type: "image/png" }));
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
const { data } = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
||||
const elevations = new Int16Array(bitmap.width * bitmap.height);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function fetchTile(z, x, y, offlineAvailable) {
|
||||
const key = `${z}/${x}/${y}`;
|
||||
let cached = tileCache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const promise = fetchTileBytes(z, x, y, offlineAvailable)
|
||||
.then((bytes) => (bytes ? decodeTerrarium(bytes) : null))
|
||||
.catch(() => null);
|
||||
tileCache.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// -> 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;
|
||||
}
|
||||
Reference in new issue
Block a user