1export const mapLibraryCsvUrl = "https://raw.githubusercontent.com/yohman/maplibrary/main/data/maps.csv"; 2 3export function parseCsv(text) { 4 const rows = []; 5 let row = []; 6 let value = ""; 7 let quoted = false; 8 for (let index = 0; index < text.length; index += 1) { 9 const char = text[index]; 10 if (quoted) { 11 if (char === '"' && text[index + 1] === '"') { value += '"'; index += 1; } 12 else if (char === '"') quoted = false; 13 else value += char; 14 } else if (char === '"' && value === "") quoted = true; 15 else if (char === ",") { row.push(value); value = ""; } 16 else if (char === "\n" || char === "\r") { 17 if (char === "\r" && text[index + 1] === "\n") index += 1; 18 row.push(value); value = ""; 19 if (row.some((cell) => cell !== "")) rows.push(row); 20 row = []; 21 } else value += char; 22 } 23 if (value || row.length) { row.push(value); if (row.some((cell) => cell !== "")) rows.push(row); } 24 if (!rows.length) return []; 25 const [headers, ...records] = rows; 26 return records.map((cells) => Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? ""]))); 27} 28 29export async function loadMapLibraryRows() { 30 const url = new URL(mapLibraryCsvUrl); 31 url.searchParams.set("v", Date.now().toString()); 32 const response = await fetch(url, { cache: "no-store" }); 33 if (!response.ok) throw new Error(`Map Library CSV unavailable (${response.status})`); 34 const records = parseCsv(await response.text()); 35 if (!records.length || !records[0].id) throw new Error("Map Library CSV has no map records."); 36 return records; 37} 38 39function yearFrom(value, fallback = 0) { 40 const match = String(value ?? "").match(/(?:^|\D)(\d{3,4})(?:\D|$)/); 41 return match ? Number(match[1]) : fallback; 42} 43 44export function nodeFromMapRecord(record, existing = {}) { 45 const sourceRecordId = String(record.id || existing.source?.sourceRecordId || ""); 46 const south = Number(record["mapping.swLat"]); 47 const west = Number(record["mapping.swLon"]); 48 const north = Number(record["mapping.neLat"]); 49 const east = Number(record["mapping.neLon"]); 50 const validBounds = [west, south, east, north].every(Number.isFinite) && west < east && south < north; 51 const startDate = record["mapping.dateFrom.date"] || ""; 52 const endDate = record["mapping.dateTo.date"] || startDate; 53 const startYear = yearFrom(startDate, yearFrom(record.publicationDate, existing.temporal?.startYear || 0)); 54 const endYear = yearFrom(endDate, startYear); 55 return { 56 ...existing, 57 id: existing.id || `map:${sourceRecordId}`, 58 kind: "historical-map", 59 title: record.titleEn || record.title || existing.title || "Untitled historical map", 60 alternativeTitle: record.title || existing.alternativeTitle || null, 61 source: { ...(existing.source || {}), dataset: "map-library", sourceRecordId }, 62 place: { ...(existing.place || {}), label: record.city || existing.place?.label || "Unplaced" }, 63 temporal: { ...(existing.temporal || {}), start: startDate, end: endDate, startYear, endYear }, 64 geometry: validBounds ? { ...(existing.geometry || {}), bbox: [west, south, east, north] } : existing.geometry, 65 rendering: { 66 ...(existing.rendering || {}), tileType: record.tileType || null, tileUrl: record.tileUrl || null, 67 thumbnailUrl: record.thumbnailUrl || null, minZoom: Number(record.minZoom || 0), maxZoom: Number(record.maxZoom || 22), 68 projection: record.projection || null 69 }, 70 sourceRecord: record, 71 eligibleForCoring: validBounds && startYear > 0 72 }; 73}
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.