Proof of concept for the other one
at main 148 lines 4.0 kB view raw
1import type { CalendarEvent } from "./parser"; 2 3export interface GeocodedLocation { 4 lat: number; 5 lng: number; 6} 7 8const NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"; 9const RATE_LIMIT_MS = 1100; // Nominatim requires max 1 req/sec 10 11/** 12 * Build a geocoding query string from an event's venue and location. 13 * Returns null if there's nothing to geocode. 14 */ 15function buildQuery(event: CalendarEvent): string | null { 16 const parts: string[] = []; 17 if (event.venue) parts.push(event.venue); 18 if (event.location) parts.push(event.location); 19 if (parts.length === 0) return null; 20 return parts.join(", "); 21} 22 23/** 24 * Normalize a query string for deduplication. 25 */ 26function normalizeQuery(query: string): string { 27 return query.toLowerCase().trim(); 28} 29 30/** 31 * Fetch a single geocode result from Nominatim. 32 */ 33async function fetchGeocode(query: string): Promise<GeocodedLocation | null> { 34 try { 35 console.debug(`[CalendarViewer] Geocoding: "${query}"`); 36 const params = new URLSearchParams({ 37 q: query, 38 format: "json", 39 limit: "1", 40 }); 41 const response = await fetch(`${NOMINATIM_URL}?${params}`, { 42 headers: { 43 "User-Agent": "ObsidianCalendarViewer/1.0", 44 }, 45 }); 46 if (!response.ok) { 47 console.warn(`[CalendarViewer] Geocoding failed for "${query}": HTTP ${response.status}`); 48 return null; 49 } 50 const results = await response.json(); 51 if (results.length > 0) { 52 const loc = { 53 lat: parseFloat(results[0].lat), 54 lng: parseFloat(results[0].lon), 55 }; 56 console.debug(`[CalendarViewer] Geocoded "${query}" → ${loc.lat}, ${loc.lng}`); 57 return loc; 58 } 59 console.warn(`[CalendarViewer] Geocoding returned no results for "${query}"`); 60 return null; 61 } catch (e) { 62 console.warn(`[CalendarViewer] Geocoding error for "${query}":`, e); 63 return null; 64 } 65} 66 67/** 68 * Sleep for a given number of milliseconds. 69 */ 70function sleep(ms: number): Promise<void> { 71 return new Promise((resolve) => setTimeout(resolve, ms)); 72} 73 74/** 75 * Geocode a list of events. Events that already have lat/lng (e.g. 76 * from a "geo:" line in the document) are skipped. Uncached venues 77 * are geocoded sequentially with rate limiting. 78 * 79 * The onProgress callback is called after each newly geocoded event, 80 * so the map can progressively populate. 81 * 82 * Returns the list of events that were newly geocoded (so the caller 83 * can write "geo:" lines back into the document). 84 */ 85export async function geocodeEvents( 86 events: CalendarEvent[], 87 onProgress: () => void, 88): Promise<CalendarEvent[]> { 89 const needsGeocoding: Array<{ event: CalendarEvent; query: string; key: string }> = []; 90 91 for (const event of events) { 92 // Already has coordinates (from "geo:" line in doc) 93 if (event.lat !== undefined && event.lng !== undefined) { 94 console.debug(`[CalendarViewer] Skipping "${event.title}" — already has coords`); 95 continue; 96 } 97 98 const query = buildQuery(event); 99 if (!query) { 100 console.debug(`[CalendarViewer] Skipping "${event.title}" — no venue/location`); 101 continue; 102 } 103 104 needsGeocoding.push({ event, query, key: normalizeQuery(query) }); 105 } 106 107 // Deduplicate by normalized query (multiple events at same venue) 108 const seen = new Set<string>(); 109 const unique: typeof needsGeocoding = []; 110 for (const item of needsGeocoding) { 111 if (!seen.has(item.key)) { 112 seen.add(item.key); 113 unique.push(item); 114 } 115 } 116 117 if (unique.length > 0) { 118 console.debug(`[CalendarViewer] Geocoding ${unique.length} unique location(s)...`); 119 } 120 121 const newlyGeocoded: CalendarEvent[] = []; 122 123 // Geocode sequentially with rate limiting 124 for (let i = 0; i < unique.length; i++) { 125 const { query, key } = unique[i]; 126 127 if (i > 0) { 128 await sleep(RATE_LIMIT_MS); 129 } 130 131 const result = await fetchGeocode(query); 132 133 // Apply result to ALL events with the same normalized query 134 if (result) { 135 for (const item of needsGeocoding) { 136 if (item.key === key) { 137 item.event.lat = result.lat; 138 item.event.lng = result.lng; 139 newlyGeocoded.push(item.event); 140 } 141 } 142 } 143 144 onProgress(); 145 } 146 147 return newlyGeocoded; 148}