aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/tiles.js
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/js/tiles.js')
-rw-r--r--viewer/js/tiles.js162
1 files changed, 162 insertions, 0 deletions
diff --git a/viewer/js/tiles.js b/viewer/js/tiles.js
new file mode 100644
index 0000000..78bac6d
--- /dev/null
+++ b/viewer/js/tiles.js
@@ -0,0 +1,162 @@
+// tiles.js — deep-zoom tile maths (pure): the scheme shared with map/tiles.py, target zoom, visible tiles, patch meshes
+import { R_KM, toVec, project } from './geo.js';
+
+export const TILE = 256;
+export const MIN_TILE_Z = 5; // below this the base texture is as sharp as the tiles
+export const MAX_TILE_Z = 16;
+export const MESH_SEGS = 128; // 3D patches: one vertex per server height (map/tiles.py MESH_N = 129)
+export const tileSpanDeg = z => 180 / 2 ** z;
+export const pxKm = z => (2 * Math.PI * R_KM) / (2 ** (z + 1) * TILE);
+export const tileKey = t => `${t.z}/${t.x}/${t.y}`;
+export const DETAIL_TILES = 16; // the 3D fine grain repeats every 16 tiles (columns per zoom are a multiple of 16)
+export const detailOrigin = t => [(t.x % DETAIL_TILES) * TILE, (t.y % DETAIL_TILES) * TILE]; // texels, small: float32-exact
+export const parentTile = t => ({ z: t.z - 1, x: t.x >> 1, y: t.y >> 1 });
+
+export function tileOf(lat, lon, z) {
+ const s = tileSpanDeg(z), nx = 2 ** (z + 1), ny = 2 ** z;
+ const x = ((Math.floor((lon + 180) / s) % nx) + nx) % nx;
+ const y = Math.min(ny - 1, Math.max(0, Math.floor((90 - lat) / s)));
+ return { z, x, y };
+}
+
+export function tileBounds({ z, x, y }) {
+ const s = tileSpanDeg(z);
+ return { west: -180 + x * s, east: -180 + (x + 1) * s, north: 90 - y * s, south: 90 - (y + 1) * s };
+}
+
+export function targetZoom(kmPerPx) {
+ if (!(kmPerPx > 0)) return 0;
+ const z = Math.ceil(Math.log2((2 * Math.PI * R_KM) / (kmPerPx * 2 * TILE)) - 1e-12);
+ return Math.max(0, Math.min(MAX_TILE_Z, z));
+}
+
+function ringTiles(t, ring) { // the tile first, then its neighbours (rows clipped at the poles, columns wrapped)
+ const nx = 2 ** (t.z + 1), ny = 2 ** t.z, out = [t];
+ for (let dy = -ring; dy <= ring; dy++) {
+ const y = t.y + dy;
+ if (y < 0 || y >= ny) continue;
+ for (let dx = -ring; dx <= ring; dx++) if (dx || dy) out.push({ z: t.z, x: (((t.x + dx) % nx) + nx) % nx, y });
+ }
+ return out;
+}
+
+export function tilesForPoints(points, z, ring = 1) {
+ const out = new Map();
+ for (const p of points) for (const u of ringTiles(tileOf(p.lat, p.lon, z), ring)) if (!out.has(tileKey(u))) out.set(tileKey(u), u);
+ return [...out.values()];
+}
+
+const children = t => [0, 1, 2, 3].map(i => ({ z: t.z + 1, x: 2 * t.x + (i & 1), y: 2 * t.y + (i >> 1) }));
+
+// A quadtree cut for per-point zooms: every point wants its tile (and ring) at its own zoom. Any tile holding a
+// wanted finer tile is split into all four children, so nothing overlaps; a split tile that was itself wanted
+// (on screen) has all its children drawn, so nothing it covered goes missing. Order: first wanted first.
+export function lodTiles(points, zs, ring = 1) {
+ const want = new Map(), split = new Set();
+ points.forEach((p, i) => {
+ if (!(zs[i] >= MIN_TILE_Z)) return;
+ for (const u of ringTiles(tileOf(p.lat, p.lon, zs[i]), ring)) {
+ const k = tileKey(u);
+ if (!want.has(k)) want.set(k, { t: u, n: want.size });
+ for (let a = parentTile(u); a.z >= MIN_TILE_Z; a = parentTile(a)) split.add(tileKey(a));
+ }
+ });
+ const out = [], roots = new Map();
+ const visit = (t, cov, n) => {
+ const k = tileKey(t), w = want.get(k), c = cov || !!w, m = w ? w.n : n;
+ if (split.has(k)) for (const ch of children(t)) visit(ch, c, m);
+ else if (c) out.push({ t, n: w ? w.n : n + 0.5 });
+ };
+ for (const { t } of want.values()) {
+ let r = t;
+ while (r.z > MIN_TILE_Z) r = parentTile(r);
+ roots.set(tileKey(r), r);
+ }
+ for (const r of roots.values()) visit(r, false, Infinity);
+ return out.sort((a, b) => a.n - b.n).map(o => o.t);
+}
+
+function grid(t, segs) { // rows north → south, columns west → east (same winding as geo.sphereMesh)
+ const b = tileBounds(t), n = segs + 1;
+ const lat = new Float64Array(n * n), lon = new Float64Array(n * n), uvs = new Float32Array(n * n * 2);
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ const k = i * n + j;
+ lat[k] = b.north - ((b.north - b.south) * i) / segs;
+ lon[k] = b.west + ((b.east - b.west) * j) / segs;
+ uvs[2 * k] = j / segs;
+ uvs[2 * k + 1] = 1 - i / segs;
+ }
+ }
+ const indices = new Uint32Array(segs * segs * 6);
+ let q = 0;
+ for (let i = 0; i < segs; i++) {
+ for (let j = 0; j < segs; j++) {
+ const a = i * n + j, c = a + n;
+ indices.set([a, c, c + 1, a, c + 1, a + 1], q);
+ q += 6;
+ }
+ }
+ return { b, lat, lon, uvs, indices };
+}
+
+const R_M = R_KM * 1000;
+
+function edgeRing(segs) { // grid indices around the border, each once, in order (N, E, S, W)
+ const n = segs + 1, out = [];
+ for (let j = 0; j < segs; j++) out.push(j);
+ for (let i = 0; i < segs; i++) out.push(i * n + segs);
+ for (let j = segs; j > 0; j--) out.push(segs * n + j);
+ for (let i = segs; i > 0; i--) out.push(i * n);
+ return out;
+}
+
+export function globePatch(t, segs = 16, heights = null, exag = 1) { // positions relative to the patch centre
+ const g = grid(t, segs), N = g.lat.length;
+ const center = toVec((g.b.north + g.b.south) / 2, (g.b.west + g.b.east) / 2);
+ const edge = heights ? edgeRing(segs) : [], M = N + edge.length;
+ const spacingM = (((g.b.north - g.b.south) * Math.PI) / 180 / segs) * R_M;
+ const skirt = heights ? 2 * spacingM * Math.max(1, exag) : 0; // m: hides cracks against neighbours up to 2 zooms coarser
+ const positions = new Float32Array(M * 3), uvs = new Float32Array(M * 2);
+ const skirtFlags = heights ? new Float32Array(M).fill(1, N) : undefined; // 1: skirt (the cliff shading skips it)
+ const put = (k, lat, lon, m) => {
+ const v = toVec(lat, lon), r = 1 + m / R_M;
+ for (let i = 0; i < 3; i++) positions[3 * k + i] = v[i] * r - center[i];
+ };
+ for (let k = 0; k < N; k++) put(k, g.lat[k], g.lon[k], heights ? heights[k] * exag : 0);
+ uvs.set(g.uvs);
+ edge.forEach((k, i) => {
+ put(N + i, g.lat[k], g.lon[k], heights[k] * exag - skirt);
+ uvs[2 * (N + i)] = g.uvs[2 * k];
+ uvs[2 * (N + i) + 1] = g.uvs[2 * k + 1];
+ });
+ const indices = new Uint32Array(g.indices.length + edge.length * 6);
+ indices.set(g.indices);
+ for (let i = 0, q = g.indices.length; i < edge.length; i++, q += 6) {
+ const a = edge[i], b = edge[(i + 1) % edge.length], a2 = N + i, b2 = N + ((i + 1) % edge.length);
+ indices.set([a, a2, b2, a, b2, b], q);
+ }
+ return { center, positions, uvs, indices, skirt: skirtFlags };
+}
+
+export function gridHeight(t, heights, lat, lon, segs = MESH_SEGS) { // bilinear height inside a tile's mesh
+ const b = tileBounds(t), n = segs + 1, s = b.east - b.west;
+ const fx = ((((lon - b.west) % 360) + 360) % 360 / s) * segs, fy = ((b.north - lat) / (b.north - b.south)) * segs;
+ const j = Math.min(segs - 1, Math.max(0, Math.floor(fx))), i = Math.min(segs - 1, Math.max(0, Math.floor(fy)));
+ const u = Math.min(1, Math.max(0, fx - j)), v = Math.min(1, Math.max(0, fy - i)), h = (a, c) => heights[a * n + c];
+ return (1 - v) * ((1 - u) * h(i, j) + u * h(i, j + 1)) + v * ((1 - u) * h(i + 1, j) + u * h(i + 1, j + 1));
+}
+
+export function flatPatch(t, kind, segs = 16, zOff = 0) {
+ const g = grid(t, segs), N = g.lat.length, D = Math.PI / 180;
+ const c = project(kind, (g.b.north + g.b.south) / 2, (g.b.west + g.b.east) / 2);
+ const positions = new Float32Array(N * 3), ll = new Float32Array(N * 2);
+ for (let k = 0; k < N; k++) {
+ const p = project(kind, g.lat[k], g.lon[k]);
+ positions[3 * k] = p.x - c.x;
+ positions[3 * k + 1] = p.y - c.y;
+ ll[2 * k] = g.lat[k] * D;
+ ll[2 * k + 1] = g.lon[k] * D;
+ }
+ return { center: [c.x, c.y, zOff], positions, uvs: g.uvs, indices: g.indices, ll };
+}