raw · 7130 bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 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'); } } }); |