1import { state } from "./state.js";2import { legendSections } from "./legend.js";34// A map arrives in two pieces: its layers and collections as JSON, and its5// point features packed as parallel arrays. Nothing here is fetched until the6// map is opened, so the catalog can grow without the wait growing with it.7export async function loadWorld(entry) {8 const [detailResponse, packedResponse] = await Promise.all([9 fetch(`${state.volume.base}/worlds/${entry.slug}.json`),10 fetch(`${state.volume.base}/worlds/${entry.slug}.bin`),11 ]);12 // A refusal here is almost always a stamp that has gone stale: the bundle13 // was replaced between the catalog fetch and this one. The caller refetches14 // the catalog and arrives at the new build's URLs.15 if (!detailResponse.ok || !packedResponse.ok) {16 throw new Error(`world ${entry.slug} is not served under this catalog any more`);17 }18 const [detail, packed] = await Promise.all([19 detailResponse.json(),20 packedResponse.arrayBuffer(),21 ]);22 const collections = detail.collections || [];23 unpackLocations(packed, collections);24 const sections = legendSections(collections);25 return {26 ...entry,27 grid: detail.grid,28 lenses: detail.lenses,29 collections,30 // One index by id, because everything downstream -- the hide set, the31 // label ladder, a zone asking after its own collection -- speaks in ids.32 collectionsById: new Map(collections.map((collection) => [collection.id, collection])),33 attrs: detail.attrs || {},34 merged: detail.merged || [],35 sections,36 };37}3839// The reader of packLocations. Each field is a view straight onto the buffer,40// laid out so no copying or realignment is needed to get at it. The owner41// column indexes the collections array; only a point collection may own a42// packed location, and a payload saying otherwise is refused rather than43// guessed at.44export function unpackLocations(buffer, collections) {45 const view = new DataView(buffer);46 const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 8));47 if (magic !== "ATLASLOC") throw new Error("location payload is not in the expected form");48 const version = view.getUint16(8, true);49 if (version !== 3) throw new Error(`location payload is version ${version}, and this reads 3`);50 const count = view.getUint32(10, true);5152 let at = 16;53 const ids = new Int32Array(buffer, at, count);54 const latitudes = new Float32Array(buffer, (at += count * 4), count);55 const longitudes = new Float32Array(buffer, (at += count * 4), count);56 const members = new Int32Array(buffer, (at += count * 4), count);57 const shards = new Int32Array(buffer, (at += count * 4), count);58 const offsets = new Uint32Array(buffer, (at += count * 4), count + 1);59 const owners = new Uint16Array(buffer, (at += (count + 1) * 4), count);60 const titles = new Uint8Array(buffer, at + count * 2);6162 const decoder = new TextDecoder();63 for (const collection of collections) {64 if (collection.kind === "point") collection.locations = [];65 }66 for (let index = 0; index < count; index++) {67 const owner = collections[owners[index]];68 if (!owner || owner.kind !== "point") {69 throw new Error(`location ${ids[index]} names collection ${owners[index]}, which is no point collection`);70 }71 owner.locations.push({72 id: ids[index],73 title: decoder.decode(titles.subarray(offsets[index], offsets[index + 1])),74 lat: latitudes[index],75 lng: longitudes[index],76 memberId: members[index] || undefined,77 shard: shards[index] || undefined,78 });79 }80}8182// Descriptions and cross-references are half the catalog by weight and are read83// one feature at a time, so a map's are fetched the first time one of its84// features is opened, and not at all if none ever is. The cache is keyed by the85// volume's stamped base as well as the map, so an updated bundle is never86// answered with the words of the build it replaced.87export async function worldText() {88 const key = `${state.volume.base}/${state.world.slug}`;89 if (!state.textByWorld.has(key)) {90 state.textByWorld.set(91 key,92 fetch(`${state.volume.base}/worlds/${state.world.slug}.text`).then((r) => r.json()).catch(() => ({})),93 );94 }95 return state.textByWorld.get(key);96}