clone

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

feat/atlas-next-horizon
atlas / analysis / test / ring.test.ts
ring.test.ts 4.0K
1// The ring utilities: what a renderer would otherwise have to work out.23import assert from "node:assert/strict";4import test from "node:test";56import type { Coordinate } from "../cellsystems/ground.ts";7import { cellRings, clipRingX, ringClosesOn, ringIsClosed } from "../cellsystems/ring.ts";8import { cellPlan } from "../cellsystems/plan.ts";9import { s2System } from "../cellsystems/s2.ts";10import { geohashSystem } from "../cellsystems/geohash.ts";11import { sphere, square } from "./grounds.ts";1213/** A clip may start its ring at any vertex; the shape is what matters. */14const canonical = (ring: readonly Coordinate[]) => {15 const open = ring.slice(0, -1).map(([x, y]) => `${x},${y}`);16 const start = open.indexOf([...open].sort()[0] ?? "");17 return [...open.slice(start), ...open.slice(0, start)].join(" ");18};1920test("clipRingX cuts a wrapped ring against the surface edges", () => {21 const straddling: Coordinate[] = [[80, -10], [80, -30], [120, -30], [120, -10], [80, -10]];22 assert.equal(23 canonical(clipRingX(straddling, 0, 100)),24 canonical([[80, -10], [80, -30], [100, -30], [100, -10], [80, -10]]),25 "the piece as it lies",26 );27 const shifted = straddling.map(([x, y]): Coordinate => [x - 100, y]);28 assert.equal(29 canonical(clipRingX(shifted, 0, 100)),30 canonical([[0, -10], [0, -30], [20, -30], [20, -10], [0, -10]]),31 "and the other piece, a world over",32 );33 assert.deepEqual(clipRingX(shifted.map(([x, y]): Coordinate => [x - 200, y]), 0, 100), [],34 "a ring entirely outside survives as nothing");35 const inside: Coordinate[] = [[10, -10], [10, -20], [20, -20], [20, -10], [10, -10]];36 assert.deepEqual(clipRingX(inside, 0, 100), inside,37 "a ring entirely inside comes back whole, closed, in its own order");38});3940test("clipRingX answers a closed ring, or none at all", () => {41 const straddling: Coordinate[] = [[80, -10], [80, -30], [120, -30], [120, -10], [80, -10]];42 assert.equal(ringIsClosed(clipRingX(straddling, 0, 100)), true);43});4445test("closure on a ground allows the pole cell's whole world of drift", () => {46 const on = s2System.on(sphere);47 assert.equal(ringIsClosed(on.ring("47a1cb")), true, "an ordinary cell closes exactly");48 assert.equal(ringIsClosed(on.ring("5")), false, "a pole face does not, and must not");49 assert.equal(ringClosesOn(sphere, on.ring("5")), true, "but it closes on the ground");50 assert.equal(ringClosesOn(sphere, on.ring("47a1cb")), true);51 assert.equal(ringClosesOn(sphere, [[0, 0], [1, 0], [1, -1], [0, -5]]), false,52 "an open loop is open");53});5455test("a plain cell draws as one piece, untouched", () => {56 const [cell] = cellPlan(square, geohashSystem, "m6");57 assert.ok(cell);58 const pieces = cellRings(square, cell);59 assert.equal(pieces.length, 1);60 assert.deepEqual(pieces[0], [...cell.ring], "every geohash ring is already a polygon");61});6263test("a pole cell closes along the picture's own edge", () => {64 const plan = cellPlan(sphere, s2System, "5");65 const face = plan.find((cell) => cell.hash === "5");66 assert.ok(face);67 const pieces = cellRings(sphere, face);68 assert.ok(pieces.length >= 2, "and it left the surface, so it arrives cut");69 for (const piece of pieces) {70 assert.ok(piece.every(([x]) => x >= 0 && x <= 8192), "every piece is on the picture");71 assert.equal(ringIsClosed(piece), true);72 assert.ok(piece.some(([, y]) => y === 0),73 "the loop is closed along the top edge, where the north pole is drawn");74 }75});7677test("a cell that only crosses the seam arrives as its two pieces", () => {78 const plan = cellPlan(sphere, s2System, "");79 const crossing = plan.filter((cell) => {80 const xs = cell.ring.map(([x]) => x);81 return Math.min(...xs) < 0 || Math.max(...xs) > 8192;82 });83 assert.ok(crossing.length > 0, "the six faces of a cube do not all fit inside one rectangle");84 for (const cell of crossing) {85 const pieces = cellRings(sphere, cell);86 assert.ok(pieces.length >= 2, `${cell.hash} should arrive as more than one piece`);87 for (const piece of pieces) {88 assert.ok(piece.every(([x]) => x >= 0 && x <= 8192));89 }90 }91});