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.