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