clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

archive/golden-reference
atlas / frontend / test / cellsystems.test.mjs
cellsystems.test.mjs 9.5K
1// Golden tests for the cell systems: pure math, no DOM, no OpenLayers.2// The geohash values are hand-derived from the halving rules -- not3// computed by the code under test -- so a regression in the extraction4// shows up as a number, not a tautology.5import assert from "node:assert/strict";6import test from "node:test";78import { state } from "../src/state.js";9import { clipRingX, equivalentCell, surfaceExtent } from "../src/cellsystems/index.js";10import { geohashSystem, geohashCellAt } from "../src/cellsystems/geohash.js";11import { s2System } from "../src/cellsystems/s2.js";1213function onSquareSurface() {14 state.volume = { tileGrid: { size: 1024 } };15 state.lens = { surface: { x: 0, y: 0, width: 1024, height: 1024 } };16}1718test("surfaceExtent prefers the surface and falls back to bounds", () => {19 onSquareSurface();20 assert.deepEqual(surfaceExtent(), [0, -1024, 1024, -0]);21 state.lens = { bounds: { x: 128, y: 256, width: 512, height: 256 } };22 assert.deepEqual(surfaceExtent(), [128, -512, 640, -256]);23 state.lens = null;24 assert.deepEqual(surfaceExtent(), [0, -1024, 1024, -0]);25});2627test("bbox halves the way geohash always has", () => {28 onSquareSurface();29 assert.deepEqual(geohashSystem.bbox(""), [0, -1024, 1024, -0]);30 // m = alphabet index 19 = 10011: x right, y bottom, x left, y up, x right.31 assert.deepEqual(geohashSystem.bbox("m"), [640, -768, 768, -512]);32 // 6 = 00110, continuing on the alternate axis.33 assert.deepEqual(geohashSystem.bbox("m6"), [672, -704, 704, -672]);34});3536test("cellAt is bbox run backward", () => {37 onSquareSurface();38 const center = [688, -688];39 assert.equal(geohashSystem.descendTarget("", center), "m");40 assert.equal(geohashSystem.descendTarget("m", center), "m6");41 assert.equal(geohashCellAt(center, 3), "m6s");42 assert.equal(geohashCellAt([-1, -1], 3), "", "off the surface names nothing");43});4445test("contains is boundary-inclusive", () => {46 onSquareSurface();47 assert.equal(geohashSystem.contains("m6", [704, -672]), true);48 assert.equal(geohashSystem.contains("m6", [672, -704]), true);49 assert.equal(geohashSystem.contains("m6", [704.0001, -672]), false);50});5152test("normalizeInput keeps what the alphabet keeps", () => {53 onSquareSurface();54 assert.equal(geohashSystem.normalizeInput("M6X!"), "m6x");55 assert.equal(geohashSystem.normalizeInput("aM6iLo"), "m6");56 assert.equal(geohashSystem.normalizeInput("m6w9"), "m6w");57 assert.equal(geohashSystem.parseInput(""), "", "the root is a place");58});5960test("clipRingX cuts a wrapped ring against the surface edges", () => {61 // The clip may start its ring at any vertex; the shape is what matters.62 const canonical = (ring) => {63 const open = ring.slice(0, -1).map(([x, y]) => `${x},${y}`);64 const start = open.indexOf([...open].sort()[0]);65 return [...open.slice(start), ...open.slice(0, start)].join(" ");66 };67 // A square straddling the right edge at x=100: the piece as it lies.68 const straddling = [[80, -10], [80, -30], [120, -30], [120, -10], [80, -10]];69 assert.equal(70 canonical(clipRingX(straddling, 0, 100)),71 canonical([[80, -10], [80, -30], [100, -30], [100, -10], [80, -10]]),72 );73 // Shifted a world (100) left, the other piece appears at the left edge.74 const shifted = straddling.map(([x, y]) => [x - 100, y]);75 assert.equal(76 canonical(clipRingX(shifted, 0, 100)),77 canonical([[0, -10], [0, -30], [20, -30], [20, -10], [0, -10]]),78 );79 // A ring entirely outside survives as nothing.80 assert.deepEqual(clipRingX(shifted.map(([x, y]) => [x - 200, y]), 0, 100), []);81 // A ring entirely inside comes back whole, closed, in its own order.82 const inside = [[10, -10], [10, -20], [20, -20], [20, -10], [10, -10]];83 assert.deepEqual(clipRingX(inside, 0, 100), inside);84});8586test("plan primitives stay stable", () => {87 onSquareSurface();88 const children = geohashSystem.children("m");89 assert.equal(children.length, 32);90 assert.equal(children[0], "m0");91 assert.equal(children[31], "mz");92 assert.equal(geohashSystem.childIndex("m6"), 6);93 assert.equal(geohashSystem.parent("m6"), "m");94 assert.equal(geohashSystem.level(""), 0);95 assert.deepEqual(geohashSystem.label("m6s"), { context: "m6", principal: "s" });96 const ring = geohashSystem.ring("m6");97 assert.equal(ring.length, 5);98 assert.deepEqual(ring[0], ring[4], "the ring closes");99 assert.deepEqual(geohashSystem.locate([688, -688]), { label: "Geohash", value: "m6s" });100 assert.equal(geohashSystem.locate([-1, -1]), null);101});102103// --- S2 over the declared sphere -------------------------------------104105// A Mars-shaped map: the top half of an 8192 world square declared as the106// whole equirectangular ground.107function onSphereMap() {108 state.volume = { tileGrid: { size: 8192 } };109 state.lens = { surface: { x: 0, y: 0, width: 8192, height: 4096 } };110 state.world = {111 attrs: {112 "atlas.geometry.surface": "sphere",113 "atlas.geometry.projection": "equirect",114 "atlas.geometry.equirect.px": "0,0,8192,4096",115 "atlas.geometry.equirect.deg": "-180,90,180,-90",116 },117 };118}119120test("s2 applies only where a sphere is declared", () => {121 onSphereMap();122 assert.equal(s2System.appliesTo(state.world), true);123 assert.equal(s2System.appliesTo({ attrs: {} }), false);124 assert.equal(geohashSystem.appliesTo(state.world), true, "geohash applies everywhere");125});126127test("s2 tokens carry the Go library's vectors", () => {128 onSphereMap();129 // (49.703498679, 11.770681595) is leaf 0x47a1cbd595522b39; its level-10130 // parent is token 47a1cb. The point in world pixels comes through the131 // declared mapping.132 const world = [((11.770681595 + 180) / 360) * 8192, -(((90 - 49.703498679) / 180) * 4096)];133 const row = s2System.locate(world, state.world);134 assert.deepEqual(row, { label: "S2", value: "47a1cb" });135 assert.equal(s2System.level("47a1cb"), 11, "port level = s2 level + 1");136 assert.equal(s2System.parent("1"), "", "a face's parent is the root");137});138139test("s2 children nest exactly and stay four wide", () => {140 onSphereMap();141 const faces = s2System.children("");142 assert.deepEqual(faces, ["1", "3", "5", "7", "9", "b"]);143 const children = s2System.children("47a1cb");144 assert.equal(children.length, 4);145 for (const child of children) {146 assert.equal(s2System.parent(child), "47a1cb", `${child} nests`);147 }148 assert.deepEqual(149 children.map((child) => s2System.childIndex(child)),150 [0, 1, 2, 3],151 "sibling ordinals are stable",152 );153});154155test("s2 containment agrees with descent", () => {156 onSphereMap();157 const world = [((11.770681595 + 180) / 360) * 8192, -(((90 - 49.703498679) / 180) * 4096)];158 let id = "";159 for (let hop = 0; hop < 11; hop++) {160 id = s2System.descendTarget(id, world);161 assert.ok(id, `descent step ${hop} names a cell`);162 assert.equal(s2System.contains(id, world), true, `${id} holds its own point`);163 }164 assert.equal(id, "47a1cb", "eleven hops land on the level-10 cell");165 assert.equal(s2System.descendTarget(id, world), "", "the telescope has a floor");166});167168test("s2 input parses whole or not at all", () => {169 onSphereMap();170 assert.equal(s2System.parseInput(""), "");171 assert.equal(s2System.parseInput("47a1cb"), "47a1cb");172 assert.equal(s2System.parseInput("47a1cbd595522b39".slice(0, 6)), "47a1cb");173 assert.equal(s2System.parseInput("xyz"), null, "not hex, not a place");174 assert.equal(s2System.normalizeInput("47A1 CB!"), "47a1cb");175});176177test("a place carries across systems at like precision", () => {178 onSphereMap();179 assert.equal(equivalentCell(geohashSystem, s2System, "", state.world), "", "root maps to root");180 // A level-2 geohash cell is 256x128 of the 8192x4096 world; the S2 cell181 // holding its center at the nearest area sits within a level either way.182 const token = s2System.parseInput(equivalentCell(geohashSystem, s2System, "m6", state.world));183 assert.ok(token, "the translation names a real cell");184 assert.equal(s2System.contains(token, geohashSystem.center("m6")), true,185 "the new cell holds the old center");186 assert.ok(Math.abs(s2System.level(token) - 4) <= 1,187 `precision carries: level ${s2System.level(token)} ~ 4`);188 // And back: the round trip stays on the same ground at the same depth.189 const home = equivalentCell(s2System, geohashSystem, token, state.world);190 assert.equal(geohashSystem.contains(home, s2System.center(token)), true);191 assert.ok(Math.abs(home.length - 2) <= 1, `round trip keeps depth: "${home}"`);192});193194test("s2 rings close, stay continuous, and know the poles", () => {195 onSphereMap();196 const ring = s2System.ring("47a1cb");197 assert.ok(ring.length > 8, "edges arrive tessellated");198 assert.deepEqual(ring[0], ring[ring.length - 1], "the ring closes");199 // Continuity holds through the closing segment, and for the pole faces200 // whose loops accumulate a whole world of longitude: the closure joins201 // in the frame the walk ended in, a world over from where it began.202 for (const id of ["47a1cb", "5", "b"]) {203 const loop = s2System.ring(id);204 for (let at = 1; at < loop.length; at++) {205 assert.ok(206 Math.abs(loop[at][0] - loop[at - 1][0]) < 4096,207 `${id}: no seam-sized jumps, the loop is continuous`,208 );209 }210 const drift = Math.abs(loop[0][0] - loop[loop.length - 1][0]) % 8192;211 assert.ok(drift < 1e-6 || Math.abs(drift - 8192) < 1e-6,212 `${id}: the closure lands on the opening point, some frame over`);213 assert.equal(loop[0][1], loop[loop.length - 1][1], `${id}: same latitude row`);214 }215 // Face 2 (token 5) holds the north pole; face 5 (token b) the south.216 assert.equal(s2System.poleContained("5"), "north");217 assert.equal(s2System.poleContained("b"), "south");218 assert.equal(s2System.poleContained("47a1cb"), null);219});