PageSourceSearch

https://hypercities.com/js/data.js

js hypercities.com collected 2026-09-25 18:52:48 UTC 4,797 bytes, 110 lines download raw bytes

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.