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