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 / cellsystems / registry.ts
registry.ts 3.9K
1// The registry: which systems exist, in which order, and how a place crosses2// between them.3//4// A registry is an immutable list. That is deliberate — a mutable module-level5// registry is the same shared client state issue #5 §5.4 just removed from the6// systems themselves, and it would make "which systems exist" depend on import7// order. `cellSystems` is the default list; `.with(system)` answers a new8// registry, so a consumer that wants a third system composes one and passes it9// where it is needed.1011import type { CellSystem } from "./contract.ts";12import type { Ground } from "./ground.ts";13import { extentArea } from "./ground.ts";14import { geohashSystem } from "./geohash.ts";15import { s2System } from "./s2.ts";1617/** An immutable, ordered set of cell systems. Order is the navigator's order. */18export class CellSystemRegistry {19 readonly systems: readonly CellSystem[];2021 constructor(systems: readonly CellSystem[]) {22 const seen = new Set<string>();23 for (const system of systems) {24 if (seen.has(system.slug)) {25 throw new Error(`cellsystems: two systems both call themselves "${system.slug}"`);26 }27 seen.add(system.slug);28 }29 this.systems = Object.freeze([...systems]);30 }3132 /** A new registry with these systems appended, in the order given. */33 with(...more: readonly CellSystem[]): CellSystemRegistry {34 return new CellSystemRegistry([...this.systems, ...more]);35 }3637 /** The system with this slug, or undefined. */38 get(slug: string): CellSystem | undefined {39 return this.systems.find((system) => system.slug === slug);40 }4142 /** The system with this slug, or a named failure. */43 require(slug: string): CellSystem {44 const system = this.get(slug);45 if (!system) {46 const known = this.systems.map((candidate) => candidate.slug).join(", ");47 throw new Error(`cellsystems: no system named "${slug}" (registered: ${known || "none"})`);48 }49 return system;50 }5152 /** Those willing to divide this ground, in registry order. */53 applicableTo(ground: Ground): readonly CellSystem[] {54 return this.systems.filter((system) => system.appliesTo(ground));55 }56}5758/**59 * The systems Atlas ships, in the order the navigator offers them. Geohash60 * first because it divides anything; S2 second because it asks the ground a61 * question first.62 */63export const cellSystems = new CellSystemRegistry([geohashSystem, s2System]);6465/** Those of a registry willing to divide this ground, in registry order. */66export function applicableSystems(67 ground: Ground,68 registry: CellSystemRegistry = cellSystems,69): readonly CellSystem[] {70 return registry.applicableTo(ground);71}7273/**74 * Carry a place across systems: the cell in the new hierarchy holding the old75 * cell's centre, taken to the depth whose cells cover closest to the same76 * ground.77 *78 * The two hierarchies share no boundaries, so what survives the translation is79 * the point and the precision. Area is compared on a log scale, because levels80 * grow geometrically and "closest" should mean closest in kind, not in pixels.81 * The root carries to the root, which is the only place the two hierarchies82 * agree exactly.83 */84export function equivalentCell(85 ground: Ground,86 from: CellSystem,87 to: CellSystem,88 id: string,89): string {90 if (!id) return "";91 const source = from.on(ground);92 const target = to.on(ground);93 const centre = source.center(id);94 const area = extentArea(source.bbox(id));95 const floor = to.maxLevel(ground);96 let held = "";97 let best = "";98 let bestFit = Infinity;99 while (target.level(held) < floor) {100 const next = target.descendTarget(held, centre);101 if (!next) break;102 const nextArea = extentArea(target.bbox(next));103 const fit = Math.abs(Math.log(nextArea / area));104 if (fit < bestFit) {105 best = next;106 bestFit = fit;107 }108 // Areas only shrink on the way down: past the target, deeper is worse.109 if (nextArea <= area) break;110 held = next;111 }112 return best;113}