PageSourceSearch

https://hypercities.com/js/main.js?v=quiet-note-1

js hypercities.com collected 2026-09-25 18:52:46 UTC 27,360 bytes, 574 lines download raw bytes

1import { containsCoordinate, loadData, tileDiagnostic } from "./data.js";
2import { CoreView } from "./core-view.js?v=timewell-no-turtle-1";
3import { apertureFor, driftOffer, fieldFragment, isPlaceNode, mapContext, mapEncounter, nodeEncounter, strayOffer } from "./graph.js";
4import { MapView } from "./map-view.js?v=notes-navigation-1";
5import { AnnotationStore, annotationContext } from "./annotations.js?v=note-autoshow-1";
6import { annotationSimulationRequest, simulatedAnnotations } from "./annotations-simulation.js";
7import { downloadText, kmlFilenameFor, kmlFor } from "./take-map.js";
8import { Interface } from "./ui.js?v=quiet-note-1";
9
10const app = { data: null, field: null, core: null, ui: null, annotations: null, simulation: null, simulatedNotes: [], pendingAnnotations: [], mapAnnotations: [], annotationsVisible: true, timewellExpanded: false, activeAnnotationId: null, annotationMode: false, annotationDraft: null, annotationFloatOpen: false, annotationArrivalFocused: false, coreMaps: [], corePoint: null, selected: null, context: null, bookEncounter: null, activeEncounter: null, tile: null, stray: null, drift: null, takeMode: false, trail: [], history: [], seenEncounterIds: [], seenPassageIds: [], seenNodeIds: [], recentConceptIds: [] };
11
12function annotationsForMap(mapId) {
13  if (app.simulation?.mapId === mapId) return app.simulatedNotes;
14  const published = app.annotations?.forMap(mapId) || [];
15  const pending = app.pendingAnnotations.filter((note) => note.context.mapId === mapId && !published.some((live) => live.id === note.id));
16  return [...pending, ...published];
17}
18
19function pushTrail(label, id, why = null) {
20  const last = app.trail.at(-1);
21  if (last?.id === id) return;
22  app.trail.push({ label, id, why });
23  if (app.trail.length > 7) app.trail.shift();
24  app.ui.renderJourney(app.trail);
25}
26
27function fragmentTraceLabel(fragment) {
28  if (fragment.kind !== "quotation") return fragment.text;
29  const words = fragment.text.replace(/[“”]/g, "").split(/\s+/).filter(Boolean);
30  return `${words.slice(0, 5).join(" ")}…`;
31}
32
33function recordEncounter(fragment) {
34  if (!fragment || app.seenEncounterIds.at(-1) === fragment.id) return;
35  app.seenEncounterIds.push(fragment.id);
36  app.seenPassageIds.push(fragment.passageId);
37  app.recentConceptIds.push(...(fragment.conceptIds || []));
38  if (app.seenEncounterIds.length > 24) app.seenEncounterIds.shift();
39  if (app.seenPassageIds.length > 12) app.seenPassageIds.shift();
40  if (app.recentConceptIds.length > 20) app.recentConceptIds.splice(0, app.recentConceptIds.length - 20);
41  pushTrail(fragmentTraceLabel(fragment), fragment.id, fragment.arrival?.detail || "A sourced Hyperbook encounter.");
42}
43
44function encounterState(map, interaction = "select-map") {
45  const cues = ["select-map", "compare-representations", "raster-basemap-alignment"];
46  if (app.coreMaps.length > 3) cues.push("core-depth-many"); else cues.push("core-depth");
47  if (interaction === "move-earlier") cues.push("move-earlier", "move-earlier-or-later", "select-temporal-layer", "move-within-place");
48  if (interaction === "move-later") cues.push("move-later", "move-earlier-or-later", "select-temporal-layer", "move-within-place");
49  if (interaction === "zoom-change") cues.push("zoom-change");
50  if (interaction === "return") cues.push("return-to-place", "revisit-trace");
51  if (interaction === "lateral") cues.push("lateral-traverse", "conceptual-continuity");
52  if (interaction === "drift") cues.push("lateral-drift", "same-time-juxtaposition");
53  return { map, core: app.corePoint, cues, conceptIds: app.recentConceptIds };
54}
55
56function selectionOptions(map, interaction) {
57  return {
58    context: app.context,
59    state: encounterState(map, interaction),
60    seenEncounterIds: app.seenEncounterIds,
61    seenPassageIds: app.seenPassageIds,
62    seenNodeIds: app.seenNodeIds
63  };
64}
65
66function render() {
67  if (!app.selected) return;
68  syncTimewellSize();
69  app.ui.renderCore({ map: app.selected, coreMaps: app.coreMaps, encounter: app.activeEncounter || app.bookEncounter, tile: app.tile, lateral: app.stray, drift: app.drift, annotations: app.mapAnnotations, annotationsVisible: app.annotationsVisible, annotationMode: app.annotationMode, annotationCompact: app.annotationMode || Boolean(app.annotationDraft), annotationOpen: app.annotationFloatOpen, annotationSimulation: app.simulation?.mapId === app.selected.id });
70}
71
72function syncTimewellSize() {
73  const button = document.querySelector("#timewell-size");
74  const addingNote = app.annotationMode || Boolean(app.annotationDraft);
75  const canShrink = Boolean(app.selected && (addingNote || (app.annotationsVisible && app.mapAnnotations.length)));
76  const compact = canShrink && (addingNote || !app.timewellExpanded);
77  app.core?.setCompact(compact);
78  button.hidden = !canShrink;
79  button.classList.toggle("is-compact", compact);
80  button.innerHTML = compact
81    ? `<span>EXPAND ↗</span>`
82    : "SHRINK TIMEWELL ↘";
83  button.setAttribute("aria-label", compact ? "Expand TimeWell" : "Shrink TimeWell");
84}
85
86function showActiveAnnotation() {
87  if (!app.annotationFloatOpen || !app.selected || !app.field) return;
88  const index = app.mapAnnotations.findIndex((annotation) => annotation.id === app.activeAnnotationId);
89  if (index < 0) { app.ui.hideAnnotationFloat(); return; }
90  const annotation = app.mapAnnotations[index];
91  app.ui.showAnnotationNote({ annotation, index, count: app.mapAnnotations.length, point: app.field.projectPoint(annotation.context.point), map: a
91pp.selected });
92}
93
94function moveAnnotationFloatWithMap() {
95  const point = app.annotationDraft?.point || (app.annotationFloatOpen && app.mapAnnotations.find((annotation) => annotation.id === app.activeAnnotationId)?.context.point);
96  if (point) app.ui.positionAnnotationFloat(app.field.projectPoint(point));
97}
98
99async function refreshAnnotations({ force = false } = {}) {
100  if (!app.annotations || !app.selected) return;
101  const selectedId = app.selected.id;
102  try {
103    await app.annotations.refresh({ force });
104    app.pendingAnnotations = app.pendingAnnotations.filter((note) => !app.annotations.annotations.some((live) => live.id === note.id));
105  } catch (error) {
106    // A public note feed must never interrupt the map. The selected layer and
107    // its source record remain fully usable if Google Sheets is momentarily
108    // unavailable.
109    console.warn("Could not refresh annotations.", error);
110  }
111  if (!app.selected || app.selected.id !== selectedId) return;
112  app.mapAnnotations = annotationsForMap(selectedId);
113  if (!app.mapAnnotations.some((annotation) => annotation.id === app.activeAnnotationId)) {
114    const requested = new URLSearchParams(window.location.search).get("annotation");
115    app.activeAnnotationId = app.mapAnnotations.some((annotation) => annotation.id === requested)
116      ? requested
117      : app.mapAnnotations[0]?.id || null;
118  }
119  const requested = new URLSearchParams(window.location.search).get("annotation");
120  if (requested && !app.annotationArrivalFocused) {
121    const arrival = app.mapAnnotations.find((annotation) => annotation.id === requested);
122    if (arrival) {
123      app.annotationArrivalFocused = true;
124      app.field.focusMap(app.selected, { point: arrival.context.point });
125      app.annotationFloatOpen = true;
126    }
127  }
128  app.field.setAnnotations(app.mapAnnotations, { visible: app.annotationsVisible, activeId: app.annotationFloatOpen ? app.activeAnnotationId : null });
129  render();
130  showActiveAnnotation();
131}
132
133function selectMap(map, { keepEncounter = false, trailWhy = null, focus = true, interaction = "select-map" } = {}) {
134  if (!map) return;
135  if (app.takeMode) app.ui.closeTakeMap(false);
136  app.selected = map;
137  app.context = mapContext(app.data, map);
138  app.bookEncounter = mapEncounter(app.data, map, selectionOptions(map, interaction));
139  app.activeEncounter = keepEncounter ? app.activeEncounter : null;
140  app.tile = tileDiagnostic(map);
141  const visibleEncounter = app.activeEncounter || app.bookEncounter;
142  app.stray = strayOffer(app.data, { relations: visibleEncounter?.links || app.context.relations }, app.trail.map((item) => item.id));
143  // Temporal drift is an occasional consequence of a conceptual dérive, not
144  // a persistent competing prompt on every historical map layer.
145  app.drift = null;
146  app.annotationMode = false;
147  app.annotationDraft = null;
148  app.annotationFloatOpen = false;
149  app.ui.hideAnnotationFloat();
150  app.field.setPendingAnnotation(null);
151  app.annotationsVisible = true;
152  app.field.setAnnotationMode(false);
153  app.core.setPassThrough(false);
154  app.mapAnnotations = annotationsForMap(map.id);
155  if (!app.mapAnnotations.some((annotation) => annotation.id === app.activeAnnotationId)) app.activeAnnotationId = null;
156  app.core.select(map, { focus });
157  app.field.showRaster(map, { focus });
158  app.ui.showGroundForMap();
159  app.field.setAnnotations(app.mapAnnotations, { visible: app.annotationsVisible, activeId: null });
160  pushTrail(`${map.city} ${map.year}`, map.id, trailWhy || "Selected from the temporal stack; X/Y remains the map’s geographic footprint and Z is its historical position.");
161  recordEncounter(visibleEncounter?.fragment);
162  render();
163  refreshAnnotations();
164}
165
166function previewCore(maps, lngLat) {
167  app.core.preview([...maps].sort((a, b) => a.year - b.year || a.title.localeCompare(b.title)), lngLat);
168}
169
170function discoverField(maps, point, lngLat) {
171  const fragment = fieldFragment(app.data, maps, {
172    state: { core: lngLat },
173    seenEncounterIds: app.seenEncounterIds,
174    seenPassageIds: app.seenPassageIds
175  });
176  app.ui.showFieldEncounter(fragment, point);
177}
178
179function enterCore(maps, lngLat) {
180  if (!maps.length) { app.ui.field("No sampled historical layer holds this point. Keep moving; the map remains open."); return; }
181  app.timewellExpanded = false;
182  app.history.push({ type: "field" });
183  app.corePoint = lngLat;
184  app.coreMaps = [...maps].sort((a, b) => a.year - b.year || a.title.localeCompare(b.title));
185  app.field.enterCore(lngLat, app.coreMaps);
186  app.core.enter(app.coreMaps, lngLat);
187  app.ui.lockedCore(app.coreMaps);
188  // A core begins at its uppermost surviving layer. The visitor can descend
189  // immediately, without needing a second click in the timewell.
190  selectMap(app.coreMaps.at(-1), {
191    trailWhy: "The newest map opens the core; descend to move into its earlier layers.",
192    interaction: "core"
193  });
194}
195
196function moveTime(direction) {
197  const index = app.coreMaps.findIndex((map) => map.id === app.selected.id);
198  const next = direction === "older" ? app.coreMaps[index - 1] : app.coreMaps[index + 1];
199  if (next) selectMap(next, { interaction: direction === "older" ? "move-earlier" : "move-later" });
200}
201
202function followNode(nodeId, why = null) {
203  if (isPlaceNode(app.data, nodeId)) { moveToPlace(nodeId, why); return; }
204  const encounter = nodeEncounter(app.data, nodeId, {
205    map: app.selected,
206    state: encounterState(app.selected, "lateral"),
207    seenEncounterIds: app.seenEncounterIds,
208    seenPassageIds: app.seenPassageIds,
209    seenNodeIds: app.seenNodeIds
210  });
211  if (!encounter) return;
212  app.history.push({ type: "map", map: app.selected.id });
213  app.activeEncounter = encounter;
214  app.stray = strayOffer(app.data, { relations: encounter.links }, app.trail.map((item) => item.id));
215  // This is the one deliberately non-scholarly movement: the book's inferred
216  // dérive can open a contemporaneous map elsewhere, with the distinction
217  // preserved in its provenance trail.
218  app.drift = nodeId === "concept:derive" ? driftOffer(app.data, app.selected, { force: true }) : null;
219  pushTrail(encounter.title, nodeId, why || `Followed a Hyperbook relation with source evidence in ${encounter.provenance ? `book pp. ${encounter.provenance.printedPages.join("–")}` : "the Hyperbook"}.`);
220  app.seenNodeIds.push(nodeId);
221  if (app.seenNodeIds.length > 12) app.seenNodeIds.shift();
222  recordEncounter(encounter.fragment);
223  render();
224}
225
226function openAperture(nodeId) {
227  const aperture = apertureFor(app.data, nodeId);
228  if (!aperture) return;
229  pushTrail(aperture.title, aperture.id, `Opened a Hyperbook ${aperture.kind}; closing it returns to this same dérive.`);
230  app.ui.openAperture(aperture);
231}
232
233function moveToPlace(nodeId, why = null) {
234  const label = app.data.entities.get(nodeId)?.preferredLabel;
235  const maps = app.data.maps.filter((map) => map.city === label).sort((a, b) => a.year - b.year || a.title.localeCompare(b.title));
236  if (!maps.length) {
237    const encounter = nodeEncounter(app.data, nodeId, {
238      map: app.selected,
239      state: encounterState(app.selected, "lateral"),
240      seenEncounterIds: app.seenEncounterIds,
241      seenPassageIds: app.seenPassageIds,
242      seenNodeIds: app.seenNodeIds
243    });
244    if (!encounter) return;
245    app.activeEncounter = encounter;
246    pushTrail(encounter.title, nodeId, why || `Reached ${label} through the book; no historical map layer is available here.`);
247    recordEncounter(encounter.fragment);
248    render();
249    return;
250  }
251  const seed = maps.at(-1);
252  const core = { lng: (seed.bbox[0] + seed.bbox[2]) / 2, lat: (seed.bbox[1] + seed.bbox[3]) / 2 };
253  const layers = maps.filter((map) => map.bbox[0] <= core.lng && map.bbox[2] >= core.lng && map.bbox[1] <= core.lat && map.bbox[3] >= core.lat);
254  app.core.leave();
255  app.field.leaveCore();
256  app.coreMaps = layers.length ? layers : [seed];
257  app.timewellExpanded = false;
258  app.corePoint = core;
259  app.field.enterCore(core, app.coreMaps);
260  app.core.enter(app.coreMaps, core);
261  app.ui.lockedCore(app.coreMaps);
262  selectMap(app.coreMaps.at(-1), { trailWhy: why || `Followed a book path into ${label}.`, interaction: "lateral" });
263}
264
265function acceptStray(nodeId) { const offer = app.stray; followNode(nodeId, offer?.explanation || "A lateral Hyperbook movement."); }
266
267function acceptDrift(mapId) {
268  const offer = app.drift;
269  const map = app.data.maps.find((candidate) => candidate.id === mapId);
270  if (!map) return;
271  app.history.push({ type: "map", map: app.selected.id });
272  const core = { lng: (map.bbox[0] + map.bbox[2]) / 2, lat: (map.bbox[1] + map.bbox[3]) / 2 };
273  const layers = app.data.maps.filter((candidate) => candidate.bbox[0] <= core.lng && candidate.bbox[2] >= core.lng && candidate.bbox[1] <= core.lat && candidate.bbox[3] >= core.lat)
274    .sort((a, b) => a.year - b.year || a.title.localeCompare(b.title));
275  app.coreMaps = layers.length ? layers : [map];
276  app.timewellExpanded = false;
277  app.corePoint = core;
278  app.field.enterCore(core, app.coreMaps);
279  app.core.enter(app.coreMaps, core);
280  selectMap(map, { trailWhy: offer?.explanation || "Temporal juxtaposition; not a claimed scholarly relation.", interaction: "drift" });
281}
282
283function leaveCore() {
284  closeAnnotation();
285  app.ui.closeGround(true);
286  app.timewellExpanded = false;
287  app.core.leave(); app.field.leaveCore(); app.coreMaps = []; app.corePoint = null; app.selected = null; app.context = null; app.bookEncounter = null; app.activeEncounter = null; app.stray = null; app.drift = null; app.takeMode = false; app.annotationMode = false; app.mapAnnotations = []; app.activeAnnotationId = null; app.history = []; app.ui.clearTakeMap(); app.ui.field();
288  syncTimewellSize();
289}
290
291function beginTakeMap() {
292  app.takeMode = true;
293  app.ui.beginTakeMap(app.selected);
294  if (app.selected) render();
295}
296
297function openTakeMap() {
298  if (!app.selected) return;
299  app.ui.openTakeMap(app.selected);
300}
301
302function resumeTakeMap() { render(); }
303
304function exportMap(kind) {
305  if (!app.selected) return;
306  if (kind === "kml") downloadText(kmlFilenameFor(app.selected), kmlFor(app.selected), "application/vnd.google-earth.kml+xml");
307}
308
309function setBasemap(mode) { app.field.setBasemap(mode); }
310
311function setRasterOpacity(opacity) { app.field.setRasterOpacity(opacity); }
312
313function toggleAnnotations() {
314  if (!app.selected || !app.mapAnnotations.length) return;
315  app.annotationsVisible = !app.annotationsVisible;
316  if (app.annotationsVisible) app.timewellExpanded = false;
317  if (!app.annotationsVisible) { app.annotationFloatOpen = false; app.ui.hideAnnotationFloat(); }
318  app.field.setAnnotations(app.mapAnnotations, { visible: app.annotationsVisible, activeId: app.annotationFloatOpen ? app.activeAnnotationId : null });
319  render();
320}
321
322function openAnnotations() {
323  if (!app.selected || !app.mapAnnotations.length) return;
324  const wasHidden = !app.annotationsVisible;
325  app.activeAnnotationId = app.mapAnnotations.some((annotation) => annotation.id === app.activeAnnotationId)
326    ? app.activeAnnotationId : app.mapAnnotations[0].id;
327  app.annotationFloatOpen = true;
328  app.annotationsVisible = true;
329  if (wasHidden) app.timewellExpanded = false;
330  app.field.setAnnotations(app.mapAnnotations, { visible: true, activeId: app.activeAnnotationId });
331  app.field.focusAnnotation(app.mapAnnotations.find((annotation) => annotation.id === app.activeAnnotationId)?.context.point);
332  render();
333  showActiveAnnotation();
334}
335
336function toggleAnnotationMode() {
337  if (!app.selected || !app.annotations?.live) return;
338  app.annotationMode = !app.annotationMode;
339  if (app.annotationMode) app.timewellExpanded = false;
340  app.annotationDraft = null;
341  app.annotationFloatOpen = false;
342  app.ui.hideAnnotationFloat();
343  app.field.setPendingAnnotation(null);
344  app.field.setAnnotations(app.mapAnnotations, { visible: app.annotationsVisible, activeId: null });
345  app.field.setAnnotationMode(app.annotationMode);
346  app.core.setPassThrough(app.annotationMode);
347  render();
348}
349
350function stepAnnotation(direction) {
351  if (!app.mapAnnotations.length) return;
352  const current = app.mapAnnotations.findIndex((annotation) => annotation.id === app.activeAnnotationId);
353  const next = (Math.max(0, current) + direction + app.mapAnnotations.length) % app.mapAnnotations.length;
354  app.activeAnnotationId = app.mapAnnotations[next].id;
355  app.annotationsVisible = true;
356  app.annotationFloatOpen = true;
357  app.field.setAnnotations(app.mapAnnotations, { visible: true, activeId: app.activeAnnotationId });
358  app.field.focusAnnotation(app.mapAnnotations[next].context.point);
359  render();
360  showActiveAnnotation();
361}
362
363function selectAnnotation(annotation) {
364  if (!annotation || annotation.context.mapId !== app.selected?.id) return;
365  closeAnnotation();
366  app.annotationMode = false;
367  app.core.setPassThrough(false);
368  app.annotationsVisible = true;
369  app.activeAnnotationId = annotation.id;
370  app.annotationFloatOpen = true;
371  app.field.setAnnotationMode(false);
372  app.field.setAnnotations(app.mapAnnotations, { visible: true, activeId: annotation.id });
373  app.field.focusAnnotation(annotation.context.point);
374  render();
375  showActiveAnnotation();
376}
377
378function placeAnnotation(point) {
379  if (!app.selected || !app.annotations?.live) return;
380  if (!containsCoordinate(app.selected, point)) {
381    app.ui.annotationPlacementHint("Choose a point inside the selected historical map.");
382    return;
383  }
384  const submission = app.annotations.buildFormSubmission(annotationContext({
385    map: app.selected,
386    point,
387    core: app.corePoint,
388    camera: app.field.getAnnotationCamera(),
389    basemap: app.ui.groundState.mode,
390    opacity: app.ui.groundState.opacity
391  }));
392  if (!submission) return;
393  app.annotationMode = false;
394  app.core.setPassThrough(false);
395  app.annotationDraft = { point, submission };
396  app.field.setAnnotationMode(false);
397  app.field.setPendingAnnotation(point);
398  render();
399  app.ui.showAnnotationDraft({ point, screenPoint: app.field.projectPoint(point), map: app.selected, submission });
400}
401
402function closeAnnotation() {
403  app.annotationFloatOpen = false;
404  app.annotationDraft = null;
405  app.field?.setPendingAnnotation(null);
406  app.ui.hideAnnotationFloat();
407  if (app.selected) {
408    app.field.setAnnotations(app.mapAnnotations, { visible: app.annotationsVisible, activeId: null });
409    render();
410  }
411}
412
413function changeAnnotationPoint() {
414  closeAnnotation();
415  app.annotationMode = true;
416  app.core.setPassThrough(true);
417  app.field.setAnnotationMode(true);
418  render();
419}
420
421function confirmAnnotation({ text, name, tag }) {
422  const draft = app.annotationDraft;
423  if (!draft || !app.selected) return;
424  const id = new URLSearchParams(draft.submission.context).get("id");
425  if (!id) return;
426  const note = {
427    id, text: text.trim(), name: name.trim(), tag: tag.trim(),
428    tags: tag.split(",").map((part) => part.trim().replace(/^#+/, "")).filter(Boolean).slice(0, 8),
429    timestamp: new Date().toISOString(), timestampValue: Date.now(),
430    context: { mapId: app.selected.id, point: draft.point }, pending: true
431  };
432  app.pendingAnnotations.unshift(note);
433  app.annotationDraft = null;
434  app.annotationFloatOpen = true;
435  app.annotationsVisible = true;
436  app.activeAnnotationId = id;
437  app.field.setPendingAnnotation(null);
438  app.mapAnnotations = annotationsForMap(app.selected.id);
439  app.field.setAnnotations(app.mapAnnotations, { visible: true, activeId: id });
440  render();
441  showActiveAnnotation();
442  pollSubmittedNote(id);
443}
444
445function pollSubmittedNote(id, attempts = 0) {
446  window.setTimeout(async () => {
447    await refreshAnnotations({ force: true });
448    if (app.annotations?.annotations.some((note) => note.id === id)) return;
449    const pending = app.pendingAnnotations.find((note) => note.id === id);
450    if (!pending) return;
451    if (attempts < 17) { pollSubmittedNote(id, attempts + 1); return; }
452    pending.unconfirmed = true;
453    if (app.selected?.id === pending.context.mapId) {
454      app.mapAnnotations = annotationsForMap(app.selected.id);
455      render();
456      showActiveAnnotation();
457    }
458  }, attempts === 0 ? 2000 : 5000);
459}
460
461function stepBack() {
462  const previous = app.history.pop();
463  if (!previous || previous.type === "field") { leaveCore(); return; }
464  const map = app.data.maps.find((candidate) => candidate.id === previous.map);
465  if (map) { app.activeEncounter = null; selectMap(map, { interaction: "return" }); }
466}
467
468function respondToMapGesture(cue) {
469  if (!app.selected) return;
470  app.activeEncounter = null;
471  app.context = mapContext(app.data, app.selected);
472  app.bookEncounter = mapEncounter(app.data, app.selected, selectionOptions(app.selected, cue));
473  app.stray = strayOffer(app.data, { relations: app.bookEncounter.links }, app.trail.map((item) => item.id));
474  recordEncounter(app.bookEncounter.fragment);
475  render();
476}
477
478function keyboard(event) {
479  if (app.ui.bookEntryIsVisible()) return;
480  if (document.querySelector("#help-dialog").open || !document.querySelector("#site-index").hidden || document.querySelector("#hyperbook-window").open || document.querySelector("#take-map-window").open) return;
481  if ((app.annotationDraft || app.annotationFloatOpen) && event.key === "Escape") {
482    event.preventDefault();
483    closeAnnotation();
484    return;
485  }
486  if (event.target instanceof Element && event.target.closest("input, textarea, select, [contenteditable]")) return;
487  if (app.annotationMode && event.key === "Escape") {
488    event.preventDefault();
489    app.annotationMode = false;
490    app.core.setPassThrough(false);
491    app.field.setAnnotationMode(false);
492    render();
493    return;
494  }
495  if (app.annotationFloatOpen && (event.key === "ArrowLeft" || event.key === "ArrowRight")) {
496    event.preventDefault();
497    stepAnnotation(event.key === "ArrowRight" ? 1 : -1);
498    return;
499  }
500  if (event.key === "Escape" || event.key === "ArrowLeft") { event.preventDefault(); stepBack(); return; }
501  if (!app.selected) return;
502  if (event.key === "ArrowDown") { event.preventDefault(); moveTime("older"); }
503  if (event.key === "ArrowUp") { event.preventDefault(); moveTime("newer"); }
504  if ((event.key === "ArrowRight" || event.key === " ") && app.stray) { event.preventDefault(); acceptStray(app.stray.id); }
505}
506
507async function start() {
508  try {
509    app.ui = new Interface({ node: followNode, time: moveTime, stray: acceptStray, drift: acceptDrift, aperture: openAperture, read: () => app.ui.openRead(), surface: leaveCore, back: stepBack, take: beginTakeMap, takeSelected: openTakeMap, resumeTake: resumeTakeMap, exportMap, basemap: setBasemap, rasterOpacity: setRasterOpacity, annotations: toggleAnnotations, openAnnotations, annotate: toggleAnnotationMode, annotationStep: stepAnnotation, closeAnnotation, changeAnnotationPoint, confirmAnnotation });
510    document.querySelector("#timewell-size").addEventListener("click", () => {
511      app.timewellExpanded = !app.timewellExpanded;
512      syncTimewellSize();
513    });
514    const pageParams = new URLSearchParams(window.location.search);
515    if (pageParams.get("enter") === "map") app.ui.dismissBookEntry();
516    [app.data, app.annotations] = await Promise.all([loadData(), AnnotationStore.load()]);
517    app.simulation = annotationSimulationRequest(window.location.search);
518    if (app.simulation) {
519      const simulationMap = app.data.maps.find((map) => map.id === app.simulation.mapId);
520      if (simulationMap) app.simulatedNotes = simulatedAnnotations(simulationMap, app.simulation.count);
521    }
522    app.ui.setBookEntryQuotes(app.data);
523    app.core = new CoreView({
524      onSelect: (map) => selectMap(map),
525      onHover: (map) => app.field.highlightTimewellMap(map),
526      onMapGesture: (event) => app.field.forwardTimewellGesture(event)
527    });
528    app.field = new MapView({
529      maps: app.data.maps,
530      onCore: enterCore,
531      onDepth: (maps) => app.ui.showDepth(maps),
532      onPreview: previewCore,
533      onFieldEncounter: discoverField,
534      onInteraction: respondToMapGesture,
535      onTileStatus: (status) => { app.tile = status; render(); },
536      onBasemapChange: (state) => app.ui.setGroundState(state),
537      onRasterOpacityChange: (state) => app.ui.setGroundState(state),
538      onAnnotationPlace: placeAnnotation,
539      onAnnotationSelect: selectAnnotation,
540      onAnnotationViewMove: moveAnnotationFloatWithMap
541    });
542    await app.field.init();
543    app.ui.field();
544    const requestedMapId = pageParams.get("map");
545    const requestedMap = app.data.maps.find((map) => map.id === requestedMapId);
546    if (requestedMap) {
547      const core = { lng: (requestedMap.bbox[0] + requestedMap.bbox[2]) / 2, lat: (requestedMap.bbox[1] + requestedMap.bbox[3]) / 2 };
548      const layers = app.data.maps.filter((map) => map.bbox[0] <= core.lng && map.bbox[2] >= core.lng && map.bbox[1] <= core.lat && map.bbox[3] >= core.lat)
549        .sort((a, b) => a.year - b.year || a.title.localeCompare(b.title));
550      app.ui.dismissBookEntry();
551      app.corePoint = core;
552      app.coreMaps = layers.length ? layers : [requestedMap];
553      app.field.enterCore(core, app.coreMaps);
554      app.core.enter(app.coreMaps, core);
555      app.ui.lockedCore(app.coreMaps);
556      selectMap(requestedMap, { focus: true, interaction: "annotation-link", trailWhy: "Arrived through a map annotation." });
557    }
558    document.addEventListener("keydown", keyboard);
559  } catch (error) {
560    const directFile = window.location.protocol === "file:";
561    document.querySelector("#field-prompt").innerHTML = directFile
562      ? "<p>This map needs a static web preview. Open it through GitHub Pages or a local HTTP server so the book graph and map records can be read.</p>"
563      : `<p>Unable to open the map: ${error.message}</p>`;
564    document.querySelector("#field-prompt").hidden = false;
565    console.error(error);
566  }
567}
568
569// A module should evaluate once, but this guard keeps embedded-browser reload
570// quirks from registering a second set of map and interface listeners.
571if (!window.__hypercitiesStarted) {
572  window.__hypercitiesStarted = true;
573  start();
574}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.