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 / render / data / report.ts
report.ts 5.0K
1// What leaves this lane: a settled camera, a pick, and a cell.2//3// They are the only three, and they leave the same way -- as a DOM event4// rather than as a request. The application renders a form for each and the5// fields it wants filled; this fills them and says so; the page's own6// hypermedia runtime posts it and applies the answer.7//8// THAT SPLIT IS DELIBERATE and it is the reason this module makes no network9// call at all. A report the seam posted for itself would throw the answer10// away, and the answer is the state island carrying the camera that was just11// written -- the server's half of the joint diagnostics (issue #5 §6), which12// would then be a render behind the reader forever. Data flows one way13// (docs/render-seam.md §1.1); this is the page carrying a value the seam14// happens to be the only one who can compute, which is exactly what the15// labels hint and the corner locator already are.16//17// A page that renders no such form -- a host that mounted the seam over some18// other chrome -- simply loses the report, which costs a reader their place19// on the next launch, or a click that selects nothing, and nothing else.2021import { logger } from "../log.ts";2223const log = logger("data");2425/** A camera as the session stores it, keyed by the world it belongs to. */26export interface CameraPost {27 readonly volume: string;28 readonly world: string;29 readonly x: number;30 readonly y: number;31 readonly zoom: number;32 readonly rotation: number;33}3435/** Report a settled camera. Never throws: the page is not the report's. */36export function reportCamera(camera: CameraPost): void {37 const form = document.querySelector("#atlas-camera");38 if (!form) return;39 const fill = (id: string, value: string) => {40 const field = document.querySelector<HTMLInputElement>(id);41 if (field) field.value = value;42 };43 fill("#atlas-camera-world", camera.world);44 fill("#atlas-camera-x", String(camera.x));45 fill("#atlas-camera-y", String(camera.y));46 fill("#atlas-camera-zoom", String(camera.zoom));47 fill("#atlas-camera-rotation", String(camera.rotation));48 window.dispatchEvent(new CustomEvent("atlas:camera", { bubbles: false }));49 log.debug("the camera settled", {50 op: "session", volume: camera.volume, world: camera.world, zoom: camera.zoom,51 });52}5354/**55 * A hit, as the pane that resolved it describes it.56 *57 * `kind` rides in the event and posts no field. The session stores an identity58 * and looks the rest up itself, so a form field for the kind would be the seam59 * telling the application something it already knows -- but a consumer of the60 * event that wants to tell a pin from an area (a status line, a cursor) should61 * not have to ask, so it travels.62 */63export interface PickPost {64 readonly feature: string;65 readonly kind: string;66}6768/** Report a pick off the canvas. Never throws: the page is not the report's. */69export function reportPick(pick: PickPost): void {70 // A MISS DOES NOT POST. Clicking open water is not a request to close what71 // the reader is reading: the card is put away by Escape and by its own72 // button, and by nothing else. An empty feature posted here would clear the73 // selection on every stray click, which is a thing the reference never did.74 if (!pick.feature) return;75 const form = document.querySelector("#atlas-pick");76 if (!form) return;77 const field = document.querySelector<HTMLInputElement>("#atlas-pick-feature");78 if (field) field.value = pick.feature;79 window.dispatchEvent(new CustomEvent("atlas:pick", {80 bubbles: false, detail: { feature: pick.feature, kind: pick.kind },81 }));82 log.debug("a pick off the canvas", {83 op: "session", feature: pick.feature, kind: pick.kind,84 });85}8687/**88 * Report a cell chosen off a surface -- the chart's canvas, or the sphere.89 *90 * The second form, and the same arrangement as the first: the panes resolve a91 * pixel into an address, which is a question only they can answer, and the92 * address is the session's to keep. What comes back is the whole scene the new93 * cell implies -- the navigator's field, the dock's narrowed list, the state94 * island the panes redraw from -- so this cannot be a request the seam makes95 * for itself any more than a pick can.96 *97 * AN EMPTY ADDRESS IS NOT THE ROOT HERE. It is a place -- the field posts it98 * on the way out, and Escape telescopes to it -- but neither of this99 * function's callers can mean it: the canvas only ever names a cell it drew,100 * and the sphere's descent answers `""` for "there is nothing below this",101 * which is the floor of the telescope rather than the top of it. So an empty102 * cell is a miss, and a miss says nothing, exactly as a pick's does.103 */104export function reportGridPick(cell: string): void {105 if (!cell) return;106 const form = document.querySelector("#atlas-grid-pick");107 if (!form) return;108 const field = document.querySelector<HTMLInputElement>("#atlas-grid-pick-cell");109 if (field) field.value = cell;110 window.dispatchEvent(new CustomEvent("atlas:grid-pick", {111 bubbles: false, detail: { cell },112 }));113 log.debug("a cell chosen off a surface", { op: "session", cell });114}