worldmap-viewer

git clone https://git.godosa.eu/worldmap-viewer

master

raw · 7130 bytes

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');
    }
  }
});