clone

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

main
atlas / analysis / test / districts.ts
districts.ts 6.1K
1// A third system, written to be thrown away.2//3// Issue #5 §5.4: "a third system (H3, MGRS, a game's district grid) is a4// registry entry plus a green conformance run." This is the game's district5// grid, and it exists for one reason: to be a system nobody designing the6// contract had in mind, and to pass anyway.7//8// It is deliberately unlike both shipped systems. Its ids are dash-joined9// segments, so `parent` is not `slice(0, -1)` and the label's two cuts are not10// the last character and everything before it. It divides nine ways rather11// than thirty-two or four. It refuses a draft that ends mid-separator, so its12// whole-or-nothing parsing has something to refuse. Nothing about it is13// registered with the app — it lives in the test tree, which is the point:14// the contract is what admits it, not a place in the shipped registry.1516import type {17 CellID,18 CellLabel,19 CellSystem,20 GroundedCellSystem,21 LocatedCell,22 Pole,23 Ring,24} from "../cellsystems/contract.ts";25import type { Coordinate, Extent, Ground } from "../cellsystems/ground.ts";26import { surfaceExtent } from "../cellsystems/ground.ts";27import { palette } from "../cellsystems/tokens.ts";2829/** Three by three, read left to right and top to bottom. */30const across = 3;31const perLevel = across * across;3233/** Three tiers: region, district, block. */34const depth = 3;3536const separator = "-";3738/** The segments of an address, or null if it is not one. */39function segments(id: CellID): number[] | null {40 if (id === "") return [];41 const parts = id.split(separator);42 const values: number[] = [];43 for (const part of parts) {44 if (!/^[0-8]$/.test(part)) return null;45 values.push(Number(part));46 }47 return values.length <= depth ? values : null;48}4950/** The extent of an address: nine-way subdivision, one tier per segment. */51function bbox(ground: Ground, id: CellID): Extent {52 const [minimumX, minimumY, maximumX, maximumY] = surfaceExtent(ground);53 const box: [number, number, number, number] = [minimumX, minimumY, maximumX, maximumY];54 for (const value of segments(id) ?? []) {55 const column = value % across;56 const row = Math.floor(value / across);57 const width = (box[2] - box[0]) / across;58 const height = (box[3] - box[1]) / across;59 box[0] += column * width;60 box[2] = box[0] + width;61 // Row 0 is the top of the picture, and y is negative-down.62 box[3] -= row * height;63 box[1] = box[3] - height;64 }65 return box;66}6768function grounded(ground: Ground): GroundedCellSystem {69 const cellAt = (at: Coordinate, tiers: number): CellID => {70 const [minimumX, minimumY, maximumX, maximumY] = surfaceExtent(ground);71 const [x, y] = at;72 if (x < minimumX || x > maximumX || y < minimumY || y > maximumY) return "";73 const box: [number, number, number, number] = [minimumX, minimumY, maximumX, maximumY];74 const parts: string[] = [];75 for (let tier = 0; tier < tiers; tier++) {76 const width = (box[2] - box[0]) / across;77 const height = (box[3] - box[1]) / across;78 const column = Math.min(across - 1, Math.floor((x - box[0]) / width));79 const row = Math.min(across - 1, Math.floor((box[3] - y) / height));80 box[0] += column * width;81 box[2] = box[0] + width;82 box[3] -= row * height;83 box[1] = box[3] - height;84 parts.push(String(row * across + column));85 }86 return parts.join(separator);87 };8889 return {90 level(id) {91 return (segments(id) ?? []).length;92 },93 parent(id) {94 return id.split(separator).slice(0, -1).join(separator);95 },96 children(id) {97 return Array.from({ length: perLevel }, (_, at) =>98 id === "" ? String(at) : `${id}${separator}${at}`);99 },100 childIndex(id) {101 const parts = segments(id);102 const last = parts?.[parts.length - 1];103 return last ?? 0;104 },105 contains(id, at) {106 const [minimumX, minimumY, maximumX, maximumY] = bbox(ground, id);107 const [x, y] = at;108 return x >= minimumX && x <= maximumX && y >= minimumY && y <= maximumY;109 },110 descendTarget(id, at) {111 const level = (segments(id) ?? []).length;112 if (level >= depth) return "";113 return cellAt(at, level + 1);114 },115 bbox(id) {116 return bbox(ground, id);117 },118 center(id) {119 const [minimumX, minimumY, maximumX, maximumY] = bbox(ground, id);120 return [(minimumX + maximumX) / 2, (minimumY + maximumY) / 2];121 },122 ring(id): Ring {123 const [minimumX, minimumY, maximumX, maximumY] = bbox(ground, id);124 return [125 [minimumX, minimumY],126 [minimumX, maximumY],127 [maximumX, maximumY],128 [maximumX, minimumY],129 [minimumX, minimumY],130 ];131 },132 poleContained(): Pole | null {133 return null;134 },135 label(id): CellLabel {136 const parts = id === "" ? [] : id.split(separator);137 return {138 context: parts.slice(0, -1).join(separator),139 principal: parts[parts.length - 1] ?? "",140 };141 },142 colorKey(id) {143 const parts = segments(id);144 const last = parts?.[parts.length - 1];145 return (last ?? 0) % palette.length;146 },147 normalizeInput(text) {148 const kept = [...text.toLocaleLowerCase()]149 .filter((character) => "012345678-".includes(character))150 .join("");151 return kept152 .split(separator)153 .filter((part) => part !== "")154 .slice(0, depth)155 .join(separator);156 },157 // A draft that is not a whole address is not a place.158 parseInput(text) {159 const parts = segments(text);160 return parts === null ? null : text;161 },162 locate(at): LocatedCell | null {163 const value = cellAt(at, depth);164 return value ? { label: "District", value } : null;165 },166 };167}168169export const districtSystem: CellSystem = {170 slug: "districts",171 name: "Districts",172 short: "D",173174 // A district grid is a curation choice about one game's map, so it divides175 // only a ground whose world says it has districts.176 appliesTo(ground: Ground): boolean {177 return ground.world.attrs["atlas.district.grid"] === "3x3";178 },179180 maxLevel(): number {181 return depth;182 },183184 // Three tiers and two separators.185 inputLength(): number {186 return depth * 2 - 1;187 },188189 on: grounded,190};