1import { loadMapLibraryRows, nodeFromMapRecord } from "./map-library.js?v=live-map-csv-1"; 2 3const sourcePaths = { 4 book: "./data/hyperbook/hyperbook.json", 5 entities: "./data/hyperbook/entities.json", 6 edges: "./data/hyperbook/edges.json", 7 encounters: "./data/hyperbook/encounters.json", 8 affordances: "./data/hyperbook/experience-affordances.json", 9 bookPages: "./data/book-page-locations.json", 10 mapGraph: "./data/hyperbook-graph.json" 11}; 12 13async function loadJson(path) { 14 const response = await fetch(path); 15 if (!response.ok) throw new Error(`Could not load ${path} (${response.status})`); 16 return response.json(); 17} 18 19function normaliseMap(node) { 20 const bbox = node.geometry?.bbox; 21 if (!Array.isArray(bbox) || bbox.length !== 4 || !node.temporal?.startYear) return null; 22 const [west, south, east, north] = bbox.map(Number); 23 if (![west, south, east, north].every(Number.isFinite) || west >= east || south >= north) return null; 24 return { 25 id: node.id, 26 title: node.title || node.alternativeTitle || "Untitled historical map", 27 alternativeTitle: node.alternativeTitle || null, 28 city: node.place?.label || node.sourceRecord?.city || "Unplaced", 29 year: Number(node.temporal.startYear), 30 endYear: Number(node.temporal.endYear || node.temporal.startYear), 31 bbox: [west, south, east, north], 32 tileBase: node.rendering?.tileUrl || null, 33 tileType: node.rendering?.tileType || null, 34 minZoom: Number(node.rendering?.minZoom ?? 0), 35 maxZoom: Number(node.rendering?.maxZoom ?? 22), 36 sourceId: node.source?.sourceRecordId || node.id, 37 original: node 38 }; 39} 40 41export function containsCoordinate(map, lngLat) { 42 const [west, south, east, north] = map.bbox; 43 return lngLat.lng >= west && lngLat.lng <= east && lngLat.lat >= south && lngLat.lat <= north; 44} 45 46export function asPolygon(map, elevation = 0) { 47 const [west, south, east, north] = map.bbox; 48 return [ 49 [west, south, elevation], [east, south, elevation], [east, north, elevation], 50 [west, north, elevation], [west, south, elevation] 51 ]; 52} 53 54export function tileTemplate(map) { 55 if (!map.tileBase) return null; 56 const base = map.tileBase.endsWith("/") ? map.tileBase : `${map.tileBase}/`; 57 return `${base.replace(/^http:/, "https:")}{z}/{x}/{y}.png`; 58} 59 60export function tileDiagnostic(map) { 61 if (!map.tileBase) return { state: "missing", message: "No raster endpoint is recorded for this map." }; 62 const originalProtocol = new URL(map.tileBase).protocol; 63 const secure = tileTemplate(map); 64 if (originalProtocol === "http:") { 65 return { 66 state: "upgraded", 67 message: "Stored as HTTP; HyperCities upgrades the same host to HTTPS before loading tiles.", 68 originalUrl: map.tileBase, 69 url: secure 70 }; 71 } 72 return { state: "recorded", message: "Secure raster template recorded.", url: secure }; 73} 74 75export async function loadData() { 76 const [book, entityDoc, edgeDoc, encounterDoc, affordanceDoc, bookPageDoc, mapGraph, liveMapRows] = await Promise.all([ 77 loadJson(sourcePaths.book), loadJson(sourcePaths.entities), loadJson(sourcePaths.edges), 78 loadJson(sourcePaths.encounters), loadJson(sourcePaths.affordances), loadJson(sourcePaths.bookPages), loadJson(sourcePaths.mapGraph), 79 loadMapLibraryRows().catch(() => null) 80 ]); 81 const liveById = new Map((liveMapRows || []).map((record) => [String(record.id), record])); 82 const currentMapNodes = mapGraph.nodes.map((node) => { 83 if (node.kind !== "historical-map") return node; 84 const row = liveById.get(String(node.source?.sourceRecordId || "")); 85 return row ? nodeFromMapRecord(row, node) : node; 86 }); 87 const maps = currentMapNodes 88 .filter((node) => node.kind === "historical-map" && node.eligibleForCoring !== false) 89 .map(normaliseMap).filter(Boolean) 90 .sort((a, b) => a.year - b.year || a.title.localeCompare(b.title)); 91 const affordancesByEncounter = new Map(encounterDoc.encounters.map((encounter) => [encounter.id, []])); 92 for (const affordance of affordanceDoc.affordances) { 93 if (!affordancesByEncounter.has(affordance.encounterId)) continue; 94 affordancesByEncounter.get(affordance.encounterId).push(affordance); 95 } 96 return { 97 book, 98 entities: new Map(entityDoc.entities.map((entity) => [entity.id, entity])), 99 objects: new Map(book.objects.map((object) => [object.id, object])), 100 edges: edgeDoc.edges, 101 encounters: new Map(encounterDoc.encounters.map((encounter) => [encounter.id, encounter])), 102 encounterList: encounterDoc.encounters, 103 affordances: affordanceDoc.affordances, 104 affordancesByEncounter, 105 bookPageLocations: bookPageDoc.highlights || {}, 106 maps 107 }; 108} 109 110// The Take a Map iframe should remain a lightweight, independently usable 111// example. It needs one map record, not the full Hyperbook and encounter set. 112export async function loadMapById(id) { 113 const graph = await loadJson(sourcePaths.mapGraph); 114 const node = graph.nodes.find((candidate) =>
114 candidate.id === id && candidate.kind === "historical-map"); 115 if (!node) return null; 116 const sourceRecordId = String(node.source?.sourceRecordId || ""); 117 const rows = await loadMapLibraryRows().catch(() => null); 118 const row = rows?.find((record) => String(record.id) === sourceRecordId); 119 return normaliseMap(row ? nodeFromMapRecord(row, node) : node); 120} 121 122export { loadMapLibraryRows }; 123 124export function titleFor(data, id) { return data.entities.get(id)?.preferredLabel || data.objects.get(id)?.title || id; } 125export function objectKind(data, id) { return data.entities.get(id)?.type || data.objects.get(id)?.kind || "object"; }
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.