1// The grid the chart draws, judged against the documented halving.2//3// Geohash divides a ground by alternating halvings, five bits to a4// character, most significant bit first, x first — and the axis does NOT5// reset between characters, so an odd character splits x three times and y6// twice, and an even character does the opposite. That is the whole of the7// arithmetic, so every extent below is computed from it by hand and inlined:8// nothing here reads a recording, and a change to the halving moves these9// numbers before it moves anything a reader sees.10//11// THE HAND DERIVATION, used throughout and spot-checked against literals:12// for an odd-position character of ordinal v (bits b16 b8 b4 b2 b1), the13// splits land x, y, x, y, x — so column = 4·b16 + 2·b4 + b1 of eight columns14// and row = 2·b8 + b2 of four rows, rows counted from the extent's bottom15// (a set bit keeps the upper half, and "up" in OL world coordinates is16// toward 0). An even-position character starts on y: column = 2·b8 + b2 of17// four, row = 4·b16 + 2·b4 + b1 of eight.18//19// The ground is real: mars, whose lens declares an 8192 × 4096 surface, so20// its level-1 cells are 1024 × 1024 — and bend-or, a plane city anchored to21// real earth by its Mercator window, whose grid is therefore REAL geohash:22// base-depth-4 WGS84 cells put on the picture through the declared23// flattening (its own derivation, at the bend-or section below). The pin24// counts are the corpus's own locations, recounted here by an independent25// sweep against the hand-derived boxes.2627import test from "node:test";28import { strict as assert } from "node:assert";29import VectorSource from "ol/source/Vector.js";30import type { FeatureLike } from "ol/Feature.js";31import { cellSystems, gridCellVisual } from "@atlas/analysis";32import type { Extent, PlanCell } from "@atlas/analysis";33import { cellMoved, drawGrid, planCells } from "../chart/grid.ts";34import { labelFitsCell } from "../chart/styles.ts";35import type { DrawnCell } from "../chart/grid.ts";36import { WorldModel, worldGrid } from "../world/model.ts";37import type { PointRecord } from "../world/model.ts";38import { Visibility } from "../world/visibility.ts";39import { EMPTY_SCENE } from "../scene/read.ts";40import { LocationTable } from "../data/atlasloc.ts";41import { locations, payloads, tileGrid } from "./fixtures.ts";42import type { LocationsFixture } from "./fixtures.ts";4344/** The alphabet, spelled out by hand: the child order and the plan order. */45const ALPHABET = "0123456789bcdefghjkmnpqrstuvwxyz";4647/**48 * A page with a ruler on it.49 *50 * The fit gate measures an address in the pitch it will be drawn in, so it51 * needs a canvas; a monospace stub is enough, and it is the honest shape of52 * the thing — every hash at a level is the same length, so a level keeps or53 * drops its labels as one.54 */55(globalThis as unknown as Record<string, unknown>).document = {56 createElement: () => ({57 getContext: () => ({58 font: "",59 measureText(text: string) {60 const size = Number(/(\d+)px/.exec(String(this.font))?.[1] ?? 10);61 return { width: text.length * size * 0.6 };62 },63 }),64 }),65};6667/** Pack a fixture's records so the model can be built from them. */68function table(fixture: LocationsFixture): LocationTable {69 const encoder = new TextEncoder();70 const n = fixture.locations.length;71 const titles = fixture.locations.map((record) => encoder.encode(record.title));72 const total = titles.reduce((held, run) => held + run.length, 0);73 const buffer = new ArrayBuffer(20 + 26 * n + total);74 const bytes = new Uint8Array(buffer);75 bytes.set(encoder.encode("ATLASLOC"), 0);76 const header = new DataView(buffer);77 header.setUint16(8, 3, true);78 header.setUint32(10, n, true);79 const id = new Int32Array(buffer, 16, n);80 const lat = new Float32Array(buffer, 16 + 4 * n, n);81 const lng = new Float32Array(buffer, 16 + 8 * n, n);82 const member = new Int32Array(buffer, 16 + 12 * n, n);83 const shard = new Int32Array(buffer, 16 + 16 * n, n);84 const offsets = new Uint32Array(buffer, 16 + 20 * n, n + 1);85 const owner = new Uint16Array(buffer, 20 + 24 * n, n);86 let at = 0;87 fixture.locations.forEach((record, i) => {88 id[i] = record.id; lat[i] = record.lat; lng[i] = record.lng;89 member[i] = record.member; shard[i] = record.shard; owner[i] = record.owner;90 offsets[i] = at;91 bytes.set(titles[i] ?? new Uint8Array(), 20 + 26 * n + at);92 at += (titles[i] ?? new Uint8Array()).length;93 });94 offsets[n] = at;95 return LocationTable.over(buffer);96}9798function world(slug: string, name: string): WorldModel {99 const payload = payloads(slug).get(name);100 assert.ok(payload, `${slug}/${name} has a payload`);101 // The world's own window overrides the manifest's, which is the whole102 // difference between a coordinate on this world and one 850,000 pixels away.103 return new WorldModel(name, payload, worldGrid(tileGrid(slug), payload),104 table(locations(slug, name)));105}106107/**108 * As a diagnostics snapshot would carry it.109 *110 * A snapshot is JSON, and JSON has one zero: the analysis lane's surface111 * extent legitimately produces `-0` for the top edge of a lens anchored at112 * y = 0 (docs/analysis.md), which serializes as `0`. Comparing through the113 * serializer is comparing what a reader of the snapshot would actually see.114 */115function asRecorded<T>(value: T): T {116 return JSON.parse(JSON.stringify(value)) as T;117}118119/** The hand derivation from the header, one character at a time. */120function handCell(surface: Extent, hash: string): Extent {121 let [minX, minY, maxX, maxY] = surface;122 for (let depth = 0; depth < hash.length; depth++) {123 const v = ALPHABET.indexOf(hash[depth] ?? "");124 const odd = depth % 2 === 0;125 const column = odd ? ((v >> 4) & 1) * 4 + ((v >> 2) & 1) * 2 + (v & 1) : ((v >> 3) & 1) * 2 + ((v >> 1) & 1);126 const row = odd ? ((v >> 3) & 1) * 2 + ((v >> 1) & 1) : ((v >> 4) & 1) * 4 + ((v >> 2) & 1) * 2 + (v & 1);127 const columns = odd ? 8 : 4;128 const rows = odd ? 4 : 8;129 const width = (maxX - minX) / columns;130 const height = (maxY - minY) / rows;131 minX += width * column;132 maxX = minX + width;133 minY += height * row;134 maxY = minY + height;135 }136 return [minX, minY, maxX, maxY];137}138139test("the hand derivation lands on the numbers worked out on paper", () => {140 const mars: Extent = [0, -4096, 8192, 0];141 // "0" is every bit clear: the bottom-left corner. "m" is ordinal 19,142 // binary 10011 — column 4+0+1 = 5, row 0+1 = 1. "z" is 31: the top-right.143 assert.deepEqual(handCell(mars, "0"), [0, -4096, 1024, -3072]);144 assert.deepEqual(handCell(mars, "m"), [5120, -3072, 6144, -2048]);145 assert.deepEqual(handCell(mars, "z"), [7168, -1024, 8192, 0]);146 // "6" in second position starts on y: ordinal 6, binary 00110 — column147 // 2·0 + 1 = 1 of four 256-wide columns, row 0 + 2·1 + 0 = 2 of eight148 // 128-tall rows. A depth-2 cell is 256 × 128, not 128 × 256.149 assert.deepEqual(handCell(mars, "m6"), [5376, -2816, 5632, -2688]);150 // And the third character is back on x: 32 × 32.151 assert.deepEqual(handCell(mars, "m60"), [5376, -2816, 5408, -2784]);152 assert.deepEqual(handCell(mars, "m6s"), [5504, -2752, 5536, -2720]);153});154155/** The independent recount: standing pins inside a hand-derived box. */156function sweep(points: readonly PointRecord[], extent: Extent): number {157 // Closed intervals on every edge, exactly as the containment rule reads: a158 // pin on a boundary belongs to both cells it touches.159 return points.filter(({ coordinate: [x, y] }) =>160 x >= extent[0] && x <= extent[2] && y >= extent[1] && y <= extent[3]).length;161}162163/** Draw one grid the way the chart does, and answer what it held. */164function draw(model: WorldModel, cell: string): {165 cells: DrawnCell[]; extent: readonly number[] | null; standing: PointRecord[];166} {167 const lens = model.payload.lenses[0] ?? null;168 const ground = model.ground(lens);169 const system = cellSystems.require("geohash");170 const standing = [...new Visibility(model, EMPTY_SCENE, lens?.shard ?? 0, null).standing()];171 const chosen = new VectorSource({ wrapX: false });172 const context = new VectorSource({ wrapX: false });173 const drawn = drawGrid(ground, system, cell, standing, chosen, context);174 // `drawn.cells` is the plan's own order; the sources hand features back in175 // whatever order their index likes, so the two are checked against each176 // other as sets and the ordering claims are made of the draw's account.177 const held = [...chosen.getFeatures(), ...context.getFeatures()]178 .map((feature) => (feature.get("gridCell") as DrawnCell).hash).sort();179 assert.deepEqual(held, drawn.cells.map((cell) => cell.hash).sort(),180 "every planned cell became a feature on one of the two sources");181 return { cells: drawn.cells, extent: drawn.extent, standing };182}183184test("the root grid over Mars is the thirty-two children of the lens's surface", () => {185 const surface: Extent = [0, -4096, 8192, 0];186 const { cells, extent, standing } = draw(world("mars", "global"), "");187 assert.deepEqual(asRecorded(extent), [...surface], "the ground the system divides");188 assert.equal(standing.length, 2048, "every pin the corpus world holds is standing");189 assert.deepEqual(190 cells.map((cell) => [cell.hash, cell.role, cell.contextDistance]),191 [...ALPHABET].map((character) => [character, "child", 0]),192 "one child per character, in the alphabet's own order, and nothing else");193 for (const cell of cells) {194 assert.deepEqual(asRecorded([...cell.extent]), asRecorded([...handCell(surface, cell.hash)]),195 `cell ${cell.hash}: the hand-derived box`);196 assert.equal(cell.count, sweep(standing, handCell(surface, cell.hash)),197 `cell ${cell.hash}: the pins it holds, recounted independently`);198 }199 // Two anchors read straight off the corpus, so the sweep itself is held to200 // something: of the 2,048 Trek locations, 26 stand in "0" and 95 in "m".201 assert.equal(cells.find((cell) => cell.hash === "0")?.count, 26);202 assert.equal(cells.find((cell) => cell.hash === "m")?.count, 95);203});204205test("descending into a cell holds its neighbours as context", () => {206 const surface: Extent = [0, -4096, 8192, 0];207 const { cells, extent, standing } = draw(world("mars", "global"), "m");208 assert.deepEqual(asRecorded(extent), [5120, -3072, 6144, -2048],209 "the held cell is the ground now");210 const roles: Record<string, number> = {};211 for (const cell of cells) roles[cell.role] = (roles[cell.role] ?? 0) + 1;212 assert.deepEqual(roles, { child: 32, scope: 1, neighbor: 31 },213 "the subdivision, the held boundary, and every sibling dimmed around it");214 for (const cell of cells) {215 assert.equal(cell.contextDistance, cell.role === "neighbor" ? 1 : 0,216 `cell ${cell.hash}: one level of ancestors means one distance`);217 assert.deepEqual(asRecorded([...cell.extent]), asRecorded([...handCell(surface, cell.hash)]),218 `cell ${cell.hash}: the hand-derived box`);219 assert.equal(cell.count, sweep(standing, handCell(surface, cell.hash)),220 `cell ${cell.hash}: the pins it holds, recounted independently`);221 }222 assert.deepEqual(223 cells.filter((cell) => cell.role === "child").map((cell) => cell.hash),224 [...ALPHABET].map((character) => `m${character}`),225 "the children refine the held address by one character");226});227228// ---- the earth-anchored city: REAL geohash --------------------------------229//230// bend-or declares a plane, `atlas.geometry.body: earth` and a Mercator231// window — px 0,0,8192,8192 picturing -121.48204667177453 … -121.1529533282255232// east-west and 44.17572950664809 … 43.93922950850393 north-south — so its233// grid is genuine WGS84 geohash. THE HAND DERIVATION: cell spans halve from234// 360°/180° per the 5-bit alternation, so depth 4 is 0.3515625° × 0.17578125°;235// depth 3 (1.40625° both ways) fits neither of the window's spans (0.32909°236// × 0.23650°) and depth 4's latitude does, so the base depth is 4. The window237// crosses columns 166–167 and rows 761–763 of the depth-4 grid counted from238// (-180°, -90°) — 58.51795/0.3515625 = 166.45 to 58.84705/0.3515625 = 167.39239// in longitude, 133.93923/0.17578125 = 761.98 to 134.17573/0.17578125 =240// 763.31 in latitude — six cells, whose ids read north to south then west to241// east: 9rce 9rcg / 9rcd 9rcf / 9rc9 9rcc (encoded with the textbook242// algorithm from each cell's centre, cross-checked in python).243244/** bend-or's declared window, transcribed from the corpus payload. */245const BEND_WINDOW = {246 west: -121.48204667177453, north: 44.17572950664809,247 east: -121.1529533282255, south: 43.93922950850393,248} as const;249250/**251 * A depth-4 cell's box from its grid indices, through the window's own252 * formulas: x = (lng − west) / (east − west) · 8192, y = (asinh(tan latN) −253 * asinh(tan lat)) / (asinh(tan latN) − asinh(tan latS)) · 8192, then the254 * sign flip — independent of the analysis lane, which must land on the same255 * doubles.256 */257function realCell(column: number, row: number): Extent {258 const { west, north, east, south } = BEND_WINDOW;259 const project = (lat: number) => Math.asinh(Math.tan((lat * Math.PI) / 180));260 const top = project(north);261 const bottom = project(south);262 const x = (lng: number) => ((lng - west) / (east - west)) * 8192;263 const y = (lat: number) => ((top - project(lat)) / (top - bottom)) * 8192;264 const cellWest = -180 + column * 0.3515625;265 const cellSouth = -90 + row * 0.17578125;266 return [267 x(cellWest), -y(cellSouth),268 x(cellWest + 0.3515625), -y(cellSouth + 0.17578125),269 ];270}271272test("an earth-anchored city's root grid is the window's own real cells", () => {273 const { cells, extent, standing } = draw(world("bend-or", "2026-08-02"), "");274 // The camera's root extent is still the lens's surface: the grid may275 // overhang the picture, but the view opens on the picture.276 assert.deepEqual(asRecorded(extent), [0, -8192, 8192, 0]);277 assert.equal(standing.length, 65, "the city corpus stands 65 pins");278 // Six cells — not thirty-two — in reading order, every one a root child.279 const placed: [string, number, number][] = [280 ["9rce", 166, 763], ["9rcg", 167, 763],281 ["9rcd", 166, 762], ["9rcf", 167, 762],282 ["9rc9", 166, 761], ["9rcc", 167, 761],283 ];284 assert.deepEqual(285 cells.map((cell) => [cell.hash, cell.role, cell.contextDistance]),286 placed.map(([hash]) => [hash, "child", 0]),287 "the base-depth cells the window intersects, north to south, west to east");288 for (const [hash, column, row] of placed) {289 const box = realCell(column, row);290 const drawn = cells.find((cell) => cell.hash === hash);291 assert.ok(drawn);292 assert.deepEqual(asRecorded([...drawn.extent]), asRecorded([...box]),293 `cell ${hash}: the hand-derived Mercator box`);294 assert.equal(drawn.count, sweep(standing, box),295 `cell ${hash}: the pins it holds, recounted independently`);296 }297 // The cells overhang the picture, honestly: the window's west edge sits298 // inside column 166, so the west column starts west of the raster.299 const west = realCell(166, 762);300 assert.ok(west[0] < 0, `column 166 starts at x = ${west[0]}, west of the raster`);301 // Two anchors read straight off the corpus, so the sweep itself is held to302 // something: of the 65 city pins, 63 stand in 9rcd and 2 in 9rcf.303 assert.equal(cells.find((cell) => cell.hash === "9rcd")?.count, 63);304 assert.equal(cells.find((cell) => cell.hash === "9rcf")?.count, 2);305});306307test("descending into a real cell holds the window's own context", () => {308 const { cells } = draw(world("bend-or", "2026-08-02"), "9rcd");309 const roles: Record<string, number> = {};310 for (const cell of cells) roles[cell.role] = (roles[cell.role] ?? 0) + 1;311 // Five window neighbours dimmed around the held cell — not thirty-one —312 // and twenty children of the alphabet's thirty-two: 9rcd overhangs the313 // window's west edge (-121.48204667° sits inside it), and a depth-5 child314 // column is 0.3515625/8 = 0.0439453125° wide, so the three westmost315 // columns end at -121.59668°, -121.55273° and -121.50879° — all west of316 // the window — and their twelve cells picture none of it.317 assert.deepEqual(roles, { neighbor: 5, scope: 1, child: 20 });318 assert.deepEqual(319 cells.filter((cell) => cell.role === "neighbor").map((cell) => cell.hash),320 ["9rce", "9rcg", "9rcf", "9rc9", "9rcc"],321 "the root's other five, in the plan's reading order");322 assert.deepEqual(asRecorded([...(cells.find((cell) => cell.role === "scope")?.extent ?? [])]),323 asRecorded([...realCell(166, 762)]), "the held cell is drawn at its real box");324});325326test("the chosen path draws under the pins and the context over them", () => {327 const model = world("mars", "global");328 const lens = model.payload.lenses[0] ?? null;329 const ground = model.ground(lens);330 const system = cellSystems.require("geohash");331 const chosen = new VectorSource({ wrapX: false });332 const context = new VectorSource({ wrapX: false });333 drawGrid(ground, system, "m", [], chosen, context);334 const roleOf = (source: VectorSource) => new Set(335 source.getFeatures().map((feature) => (feature.get("gridCell") as DrawnCell).role));336 assert.deepEqual([...roleOf(chosen)].sort(), ["child", "scope"]);337 assert.deepEqual([...roleOf(context)], ["neighbor"]);338});339340// ---- the subgrid at the depth cap --------------------------------------341//342// THE DEFECT, as the reader met it: hold a geohash cell two levels down with343// the subdivision on, and the chart draws no children at all — though the cap344// is three, the plan holds all thirty-two of them, and typing a depth-three345// address works.346//347// The plan was never the problem: held at "m6", it carries thirty-two348// children, in the frozen emission order the first test below spells out by349// hand. What was wrong is *when* the question "can this cell carry its350// address?" was asked. A child too small for its label draws **nothing**351// (analysis/cellsystems/visual.ts), so that one boolean decides whether the352// subdivision exists — and it was answered once, while the grid was being353// built, against the resolution the camera was *leaving*. Descending flies the354// camera in afterwards; nothing asked again. The deeper the held cell, the355// smaller its children are against the zoom they were judged at, and at two356// levels down every one of them failed.357//358// So the fit gate belongs to the style function, where OpenLayers hands the359// resolution being drawn at. These tests are that, in numbers derived from360// the halving and from the ruler stub above.361362/** Every feature one draw built, chosen path first, in the sources' own order. */363function features(model: WorldModel, cell: string): FeatureLike[] {364 const lens = model.payload.lenses[0] ?? null;365 const chosen = new VectorSource({ wrapX: false });366 const context = new VectorSource({ wrapX: false });367 drawGrid(model.ground(lens), cellSystems.require("geohash"), cell, [], chosen, context);368 return [...chosen.getFeatures(), ...context.getFeatures()];369}370371/** Every cell one draw planned, with the plan cell the style will read. */372function planned(model: WorldModel, cell: string): PlanCell[] {373 return features(model, cell).map((feature) => feature.get("gridPlan") as PlanCell);374}375376/** What one cell paints at one resolution: null is "nothing at all". */377function paints(cell: PlanCell, resolution: number, subgridVisible: boolean): boolean {378 const model = world("mars", "global");379 const ground = model.ground(model.payload.lenses[0] ?? null);380 return gridCellVisual(ground, cellSystems.require("geohash"), cell, {381 subgridVisible,382 labelled: labelFitsCell(cell.hash, cell.role, cell.extent, resolution),383 }) !== null;384}385386test("a cell held two levels down plans every one of its children, in the frozen order", () => {387 // The emission order, spelled out by hand from the contract: each388 // ancestor's children root-first, skipping the one on the path — thirty-one389 // at distance two, thirty-one at distance one — then the held cell as the390 // scope, then its thirty-two children.391 const expected: [string, string, number][] = [392 ...[...ALPHABET].filter((c) => c !== "m").map((c): [string, string, number] => [c, "neighbor", 2]),393 ...[...ALPHABET].filter((c) => c !== "6").map((c): [string, string, number] => [`m${c}`, "neighbor", 1]),394 ["m6", "scope", 0],395 ...[...ALPHABET].map((c): [string, string, number] => [`m6${c}`, "child", 0]),396 ];397 const model = world("mars", "global");398 const plan = planCells(399 model.ground(model.payload.lenses[0] ?? null), cellSystems.require("geohash"), "m6");400 assert.deepEqual(401 plan.map((cell) => [cell.hash, cell.role, cell.contextDistance]), expected,402 "ninety-five cells, position for position");403 assert.equal(plan.filter((cell) => cell.role === "child").length, 32,404 "thirty-two children, one level above the cap");405 assert.deepEqual(406 planned(model, "m6").map((cell) => cell.hash).sort(),407 plan.map((cell) => cell.hash).sort(),408 "and the drawn features carry that same plan, cell for cell");409 // AND NOTHING ABOUT HOW THEY LOOK IS DECIDED HERE. A visual baked onto the410 // feature is a visual decided at the wrong moment: the camera moves between411 // a grid being built and a reader seeing it, and the answer never changed412 // afterwards.413 for (const feature of features(world("mars", "global"), "m6")) {414 assert.equal(feature.get("gridVisual"), undefined,415 "the tokens are the style function's answer, at the resolution it is handed");416 }417});418419test("and draws them, once the camera has landed on the cell it was flown to", () => {420 const cells = planned(world("mars", "global"), "m6");421 const children = cells.filter((cell) => cell.role === "child");422 // The gate's own arithmetic, with the ruler stub above: a three-character423 // chip at 11 px is 3 · 11 · 0.6 + 10 of padding = 29.8 screen pixels, and a424 // child of "m6" is 32 world pixels across — so it carries its address only425 // below 32 / 29.8 ≈ 1.07 world pixels per screen pixel.426 const leaving = 2; // reading "m" from far enough out that a child is 16 px427 assert.equal(children.some((cell) => paints(cell, leaving, true)), false,428 "no child fits its address at the zoom the camera is leaving — which is what was drawn");429 // Where the camera actually lands: a rung of the pyramid's own ladder well430 // under the gate, where the same child is 128 screen pixels across.431 const landed = 0.25;432 for (const child of children) {433 assert.equal(paints(child, landed, true), true,434 `child ${child.hash} draws once the camera is over it`);435 }436});437438test("the subdivision still answers to the reader's own switch", () => {439 const cells = planned(world("mars", "global"), "m6");440 const child = cells.find((cell) => cell.role === "child");441 const scope = cells.find((cell) => cell.role === "scope");442 assert.ok(child && scope);443 assert.equal(paints(child, 0.25, false), false, "the subgrid put away draws no children");444 assert.equal(paints(scope, 0.25, false), true, "and the held cell keeps its own boundary");445});446447test("a cell at the cap is a leaf with nothing under it", () => {448 const cells = planned(world("mars", "global"), "m6s");449 assert.equal(cells.filter((cell) => cell.role === "leaf").length, 1);450 assert.equal(cells.filter((cell) => cell.role === "child").length, 0,451 "the floor of the telescope divides no further");452 // A leaf is drawn whether or not its address fits: it is the ground the453 // reader asked for, not a subdivision offered to them.454 const leaf = cells.find((cell) => cell.role === "leaf");455 assert.ok(leaf);456 assert.deepEqual(asRecorded([...leaf.extent]), [5504, -2752, 5536, -2720],457 "a 32-pixel square, three characters down");458 assert.equal(paints(leaf, 64, true), true, "even from far enough out to lose its chip");459});460461test("a chip is measured as it will be drawn, not against a round number", () => {462 // Two hashes of different lengths in the same cell: a flat pixel threshold463 // keeps or drops both, and one of them is twice as wide as the other.464 const extent: [number, number, number, number] = [0, 0, 32, 32];465 assert.equal(labelFitsCell("m", "child", extent, 1.2), true);466 assert.equal(labelFitsCell("m6sz1", "child", extent, 1.2), false,467 "a label wider than its cell names the neighbours instead");468});469470// ---- cycling the system is not a descent -------------------------------471//472// THE DEFECT: press the cell-system key with a cell held and both panes moved473// the camera. The chart fitted its view and the sphere turned the planet — to474// ground the reader was already looking at, because cycling carries the held475// cell across to the cell of the next system covering the same place. "m" in476// geohash becomes "24" in S2, and read as a string that is a descent.477478test("a cell that only changed its spelling is not a cell that moved", () => {479 assert.equal(cellMoved({ system: "geohash", cell: "m" }, { system: "s2", cell: "24" }), false,480 "the same ground under a new name: the camera is already there");481 assert.equal(cellMoved({ system: "s2", cell: "24" }, { system: "geohash", cell: "m" }), false,482 "and back again");483});484485test("a descent within one system still moves the camera", () => {486 assert.equal(cellMoved({ system: "geohash", cell: "m" }, { system: "geohash", cell: "m6" }), true);487 assert.equal(cellMoved({ system: "geohash", cell: "m6" }, { system: "geohash", cell: "m" }), true,488 "ascending is a move too: the reader asked for the ground one level out");489 assert.equal(cellMoved({ system: "s2", cell: "24" }, { system: "s2", cell: "241" }), true,490 "including the descent made straight after a cycle");491});492493test("redrawing the same grid moves nothing", () => {494 assert.equal(cellMoved({ system: "geohash", cell: "m" }, { system: "geohash", cell: "m" }), false,495 "a filter moving must not drag the reader back to the cell they are in");496 assert.equal(cellMoved({ system: "", cell: "" }, { system: "geohash", cell: "" }), false,497 "and opening a grid over the ground already on screen is not a request to go anywhere");498});