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 / render / data / payload.ts
payload.ts 5.5K
1// What `/data` says, as types.2//3// These are `docs/format.md` §4 and §6 written as TypeScript, and nothing4// more: no defaults are applied here, no key is renamed, nothing is dropped.5// A reader is lenient (format.md §2.3) — every optional field really is6// optional, unknown keys ride along untouched, and the only thing this seam7// refuses is a `formatVersion` it does not know, which the catalog never8// carries because the server already refused it.9//10// The seam reads these shapes and nothing else off the wire. Everything the11// application decided — what is hidden, what is highlighted, which lens is12// open — arrives through the scene description instead (`scene/`), because13// data flows one way and the payload is not where a session lives.1415import {16 KEY_LABEL_POLICY,17 KEY_RENDER_AS,18 KEY_STROKE_WIDTH_PX,19} from "@atlas/analysis/semconv/keys";2021/** `atlas.*` keys to their values, flat, as every entity carries them. */22export type Attrs = Readonly<Record<string, string>>;2324/** A rectangle in y-down world pixels, the space a lens declares itself in. */25export interface PixelRect {26 readonly x: number;27 readonly y: number;28 readonly width: number;29 readonly height: number;30}3132/** Sparse-level tile presence: format.md §6.3.1. */33export interface CoverageLevel {34 readonly x: number;35 readonly y: number;36 readonly w: number;37 readonly h: number;38 readonly bits: string;39}4041/** One raster pyramid picturing a world. */42export interface Lens {43 readonly name: string;44 readonly tiles: string;45 readonly minZoom: number;46 readonly maxZoom: number;47 readonly fullZoom: number;48 readonly sourceZoom: number;49 readonly formats: readonly string[];50 readonly bounds?: PixelRect;51 readonly surface?: PixelRect;52 readonly interpolate: boolean;53 readonly background?: string;54 readonly shard?: number;55 readonly coverage?: Readonly<Record<string, CoverageLevel>>;56}5758/** A point in the volume's own world space, as payloads spell it. */59export interface LatLng {60 readonly lat: number;61 readonly lng: number;62}6364/** GeoJSON-shaped geometry in the volume's own world space. */65export interface Geometry {66 readonly type: string;67 readonly coordinates: unknown;68}6970/** An inline path or area feature. Point collections carry none. */71export interface ShapeFeature {72 readonly id: number;73 readonly title: string;74 readonly subtitle?: string;75 readonly hasText?: boolean;76 readonly parent?: number;77 readonly center?: LatLng;78 readonly shard?: number;79 readonly geometry: readonly Geometry[];80 readonly attrs?: Attrs;81}8283/** The kinds a collection declares. Every feature is exactly one. */84export type Kind = "point" | "path" | "area";8586/** One ordered group of features. The array's order is load-bearing. */87export interface Collection {88 readonly id: number;89 readonly title: string;90 readonly group?: string;91 readonly kind: Kind;92 readonly icon?: string;93 readonly iconAsset?: string;94 readonly iconPicture?: boolean;95 readonly color?: string;96 readonly iconColor?: string;97 readonly visible: boolean;98 readonly attrs?: Attrs;99 readonly features?: readonly ShapeFeature[];100}101102/** `worlds/<slug>.json`. */103export interface WorldPayload {104 readonly grid?: { readonly sourceZoom: number; readonly firstTile: number };105 readonly lenses: readonly Lens[];106 readonly collections: readonly Collection[];107 readonly attrs?: Attrs;108 readonly merged?: readonly unknown[];109}110111/** One entry of `worlds/<slug>.text`, fetched when a card opens. */112export interface TextEntry {113 readonly d?: string;114 readonly l?: readonly unknown[];115 readonly a?: Attrs;116}117118/** `worlds/<slug>.text`: feature id as a string, to its prose. */119export type TextPayload = Readonly<Record<string, TextEntry>>;120121/** The volume's world square and the window its worlds were cut from. */122export interface TileGrid {123 readonly sourceZoom: number;124 readonly firstTile: number;125 readonly tileSize: number;126 readonly size: number;127}128129/** One world as the manifest lists it. */130export interface WorldEntry {131 readonly slug: string;132 readonly title: string;133 readonly parent?: string;134 readonly iconOutset?: string;135 readonly center: LatLng;136 readonly points: number;137 readonly paths: number;138 readonly areas: number;139 readonly updatedAt: string;140}141142/** One volume as `/data/catalog.json` lists it. */143export interface CatalogVolume {144 readonly slug: string;145 readonly title: string;146 readonly stamp: string;147 readonly base: string;148 readonly tileGrid: TileGrid;149 readonly worlds: readonly WorldEntry[];150}151152/** `/data/catalog.json`. Composed when asked for, never cached. */153export interface Catalog {154 readonly volumes: readonly CatalogVolume[];155 readonly bundlesDir: string;156}157158/** The extension of every tile at zoom `z`: `formats[z − minZoom]`. */159export function tileFormat(lens: Lens, z: number): string | null {160 return lens.formats[z - lens.minZoom] ?? null;161}162163/** The label policy an area collection curates, `always` when it says nothing. */164export function labelPolicy(collection: Collection): "always" | "quiet" {165 return collection.attrs?.[KEY_LABEL_POLICY] === "quiet" ? "quiet" : "always";166}167168/** How a point collection draws: markers, or floating text. Absent means pin. */169export function renderAs(collection: Collection): "pin" | "text" {170 return collection.attrs?.[KEY_RENDER_AS] === "text" ? "text" : "pin";171}172173/** The ground width of a path collection's features, in world pixels. */174export function strokeWidth(collection: Collection): number {175 const declared = Number(collection.attrs?.[KEY_STROKE_WIDTH_PX]);176 return Number.isFinite(declared) && declared > 0 ? declared : 0;177}