aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/tests/tiles.test.mjs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/tests/tiles.test.mjs
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/tests/tiles.test.mjs')
-rw-r--r--viewer/tests/tiles.test.mjs126
1 files changed, 126 insertions, 0 deletions
diff --git a/viewer/tests/tiles.test.mjs b/viewer/tests/tiles.test.mjs
new file mode 100644
index 0000000..da3bf2a
--- /dev/null
+++ b/viewer/tests/tiles.test.mjs
@@ -0,0 +1,126 @@
+import './_planet.mjs';
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { tileOf, tileBounds, targetZoom, pxKm, tilesForPoints, parentTile, tileKey, globePatch, flatPatch, MAX_TILE_Z } from '../js/tiles.js';
+import { toLatLon, project } from '../js/geo.js';
+
+const near = (a, b, eps) => assert.ok(Math.abs(a - b) <= eps, `${a} vs ${b}`);
+
+test('tile of a point: wraps east–west, clamps at the poles', () => {
+ assert.deepEqual(tileOf(90, -180, 3), { z: 3, x: 0, y: 0 });
+ assert.deepEqual(tileOf(-90, 180, 3), { z: 3, x: 0, y: 7 });
+ assert.deepEqual(tileOf(0.1, 179.99, 3), { z: 3, x: 15, y: 3 });
+ const b = tileBounds(tileOf(12.3, 45.6, 9));
+ assert.ok(b.west <= 45.6 && 45.6 < b.east && b.south < 12.3 && 12.3 <= b.north);
+});
+
+test('target zoom: tile pixels just finer than screen pixels', () => {
+ for (const k of [50, 9.8, 1, 0.1, 0.004]) {
+ const z = targetZoom(k);
+ assert.ok(pxKm(z) <= k * (1 + 1e-9), `${k}: z ${z}`);
+ if (z > 0) assert.ok(pxKm(z - 1) > k);
+ }
+ assert.equal(targetZoom(1e-6), MAX_TILE_Z);
+ assert.equal(targetZoom(1e6), 0);
+ assert.equal(targetZoom(Infinity), 0);
+});
+
+test('tiles for points: neighbours, dedupe, antimeridian wrap, pole edge', () => {
+ const t = tilesForPoints([{ lat: 0.1, lon: 179.99 }], 3, 1).map(tileKey);
+ assert.equal(t.length, 9);
+ assert.ok(t.includes('3/0/3') && t.includes('3/14/2'));
+ assert.equal(tilesForPoints([{ lat: 0.1, lon: 179.99 }, { lat: 0.2, lon: 179.98 }], 3, 0).length, 1);
+ assert.equal(tilesForPoints([{ lat: 89.9, lon: 0 }], 3, 1).length, 6);
+});
+
+test('parent tiles', () => {
+ assert.deepEqual(parentTile({ z: 6, x: 45, y: 13 }), { z: 5, x: 22, y: 6 });
+});
+
+test('patches: globe vertices on the sphere with north-west first; flat vertices on the projection', () => {
+ const t = { z: 7, x: 130, y: 40 }, b = tileBounds(t), g = globePatch(t, 4);
+ for (let k = 0; k < g.positions.length / 3; k++) {
+ const v = [0, 1, 2].map(i => g.positions[3 * k + i] + g.center[i]);
+ near(Math.hypot(...v), 1, 1e-6);
+ }
+ const nw = toLatLon([0, 1, 2].map(i => g.positions[i] + g.center[i]));
+ near(nw.lat, b.north, 1e-4);
+ near(nw.lon, b.west, 1e-4);
+ assert.deepEqual([g.uvs[0], g.uvs[1]], [0, 1]);
+ assert.equal(g.indices.length, 4 * 4 * 6);
+ const f = flatPatch(t, 'equal_earth', 4), p = project('equal_earth', b.north, b.west);
+ near(f.positions[0] + f.center[0], p.x, 1e-6);
+ near(f.positions[1] + f.center[1], p.y, 1e-6);
+ near(f.ll[0], (b.north * Math.PI) / 180, 1e-6);
+});
+
+test('3D patch: vertices lifted by height × exaggeration, skirts hang below the border, flat patch unchanged', async () => {
+ const { globePatch, gridHeight, MESH_SEGS } = await import('../js/tiles.js');
+ const { R_KM } = await import('../js/geo.js');
+ const t = { z: 10, x: 600, y: 300 }, n = MESH_SEGS + 1, R_M = R_KM * 1000;
+ const h = Float32Array.from({ length: n * n }, (_, k) => 100 + (k % n)); // rises eastward
+ const flat = globePatch(t, 16), p = globePatch(t, MESH_SEGS, h, 3);
+ assert.equal(flat.positions.length, 17 * 17 * 3);
+ assert.equal(p.positions.length, (n * n + 4 * MESH_SEGS) * 3);
+ assert.equal(p.uvs.length, (n * n + 4 * MESH_SEGS) * 2);
+ const r = k => Math.hypot(...[0, 1, 2].map(i => p.positions[3 * k + i] + p.center[i]));
+ assert.ok(Math.abs(r(0) - (1 + (3 * 100) / R_M)) < 1e-9);
+ assert.ok(Math.abs(r(n - 1) - (1 + (3 * (100 + n - 1)) / R_M)) < 1e-9);
+ assert.ok(r(n * n) < r(0) - 1e-9, 'the first skirt vertex hangs below its border vertex');
+ const spacingM = (180 / 1024 / MESH_SEGS) * Math.PI / 180 * R_M;
+ assert.ok((r(0) - r(n * n)) * R_M >= 2 * spacingM * 3 - 0.05, 'skirts reach 2 vertex spacings × exaggeration (two zoom levels of mismatch)');
+ assert.ok(p.indices.every(i => i < n * n + 4 * MESH_SEGS));
+ assert.equal(p.indices.length, MESH_SEGS * MESH_SEGS * 6 + 4 * MESH_SEGS * 6);
+ const b = { west: -180 + 600 * 180 / 1024, north: 90 - 300 * 180 / 1024 }, s = 180 / 1024;
+ assert.equal(gridHeight(t, h, b.north, b.west), 100);
+ assert.ok(Math.abs(gridHeight(t, h, b.north - s / 2, b.west + s / 2) - (100 + MESH_SEGS / 2)) < 1e-9, 'centre: bilinear');
+ assert.equal(gridHeight(t, h, b.north - s, b.west + s), 100 + n - 1);
+});
+
+test('3D patch: skirt vertices are flagged (shaded as the ground above them, not as cliffs)', async () => {
+ const { globePatch, MESH_SEGS } = await import('../js/tiles.js');
+ const t = { z: 10, x: 600, y: 300 }, n = MESH_SEGS + 1;
+ const p = globePatch(t, MESH_SEGS, new Float32Array(n * n).fill(50), 1);
+ assert.equal(p.skirt.length, n * n + 4 * MESH_SEGS);
+ assert.ok(p.skirt.subarray(0, n * n).every(v => v === 0), 'the grid: no skirt');
+ assert.ok(p.skirt.subarray(n * n).every(v => v === 1), 'the skirt');
+ assert.equal(globePatch(t, 16).skirt, undefined, 'a flat patch has none');
+});
+
+test('lodTiles: uniform zoom = tilesForPoints; mixed zooms never overlap and cover every point', async () => {
+ const { lodTiles, tilesForPoints, tileOf, tileKey, parentTile } = await import('../js/tiles.js');
+ const pts = Array.from({ length: 30 }, (_, k) => ({ lat: 40 + (k % 6) * 0.02, lon: 10 + Math.floor(k / 6) * 0.03 }));
+ const u = lodTiles(pts, pts.map(() => 11));
+ assert.deepEqual(new Set(u.map(tileKey)), new Set(tilesForPoints(pts, 11, 1).map(tileKey)));
+ assert.equal(tileKey(u[0]), tileKey(tileOf(40, 10, 11)), 'first point first');
+ const zs = pts.map((_, k) => (k < 6 ? 14 : k < 18 ? 12 : 9)); // near → far
+ const m = lodTiles(pts, zs), keys = new Set(m.map(tileKey));
+ for (const t of m) for (let a = parentTile(t); a.z >= 5; a = parentTile(a)) assert.ok(!keys.has(tileKey(a)), `overlap ${tileKey(t)}`);
+ pts.forEach((p, k) => {
+ const cover = m.filter(t => { const o = tileOf(p.lat, p.lon, t.z); return o.x === t.x && o.y === t.y; });
+ assert.equal(cover.length, 1, `point ${k}`);
+ assert.ok(cover[0].z >= zs[k], `point ${k} never coarser than it asked`);
+ });
+ assert.ok(m.some(t => t.z === 14) && m.some(t => t.z < 12));
+ assert.deepEqual(lodTiles(pts, pts.map(() => 3)), [], 'below the tile zooms: nothing');
+});
+
+test('3D patches use the 129 × 129 server heights (128 segments)', async () => {
+ const { MESH_SEGS } = await import('../js/tiles.js');
+ assert.equal(MESH_SEGS, 128);
+});
+
+test('fine grain: its texel coordinates run on across tile edges (wrapping every DETAIL_TILES tiles and the antimeridian)', async () => {
+ const { detailOrigin, DETAIL_TILES, TILE } = await import('../js/tiles.js');
+ const P = DETAIL_TILES * TILE, mod = v => ((v % P) + P) % P;
+ for (const z of [5, 12, 16]) {
+ const nx = 2 ** (z + 1), ny = 2 ** z;
+ for (const [x, y] of [[0, 0], [7, 3], [DETAIL_TILES - 1, DETAIL_TILES - 1], [nx - 1, ny - 2], [54367, 37971 % ny]]) {
+ const o = detailOrigin({ z, x, y });
+ const e = detailOrigin({ z, x: (x + 1) % nx, y }), s = detailOrigin({ z, x, y: y + 1 });
+ assert.equal(mod(o[0] + TILE), mod(e[0]), `east of ${z}/${x}/${y}`);
+ assert.equal(mod(o[1] + TILE), mod(s[1]), `south of ${z}/${x}/${y}`);
+ assert.ok(o.every(v => v >= 0 && v < P), 'small numbers: float32 keeps sub-texel precision');
+ }
+ }
+});