/** * Event Map Feature * * Renders an event location map with H3 hexagons. * Used on the event view page. */ import type { Feature, Polygon } from 'geojson' import type { GeoLocation } from '../../types' import { loadMapLibraries, createMap, h3ToGeoJsonFeature, addHexagonLayers, updateHexagonSource, calculateBounds, toMapCenter, } from './map-utils' const H3_RESOLUTION = 9 export async function initEventMap(): Promise { const container = document.getElementById('event-map') if (!container) return // Skip if already initialized or currently initializing if (container.dataset.mapInitialized === 'true' || container.dataset.mapInitializing === 'true') return // Mark as initializing immediately to prevent race conditions container.dataset.mapInitializing = 'true' try { // Parse geo locations from data attribute const geoLocationsData = container.dataset.geoLocations if (!geoLocationsData) { container.dataset.mapInitializing = 'false' return } let geoLocations: GeoLocation[] try { geoLocations = JSON.parse(geoLocationsData) } catch (e) { console.error('Failed to parse geo locations:', e) container.dataset.mapInitializing = 'false' return } // Parse and filter locations - coordinates may be strings from backend const parsedLocations = geoLocations .filter((loc) => loc != null) .map((loc) => ({ latitude: typeof loc.latitude === 'string' ? parseFloat(loc.latitude) : loc.latitude, longitude: typeof loc.longitude === 'string' ? parseFloat(loc.longitude) : loc.longitude, name: loc.name, })) .filter((loc) => Number.isFinite(loc.latitude) && Number.isFinite(loc.longitude)) if (parsedLocations.length === 0) { container.dataset.mapInitializing = 'false' return } // Lazy load MapLibre and H3 const { maplibregl, h3 } = await loadMapLibraries() // Double-check we haven't been initialized while loading if (container.dataset.mapInitialized === 'true') { container.dataset.mapInitializing = 'false' return } // Calculate center of all locations for initial map view const avgLat = parsedLocations.reduce((sum, loc) => sum + loc.latitude, 0) / parsedLocations.length const avgLng = parsedLocations.reduce((sum, loc) => sum + loc.longitude, 0) / parsedLocations.length // Final validation before creating map if (!Number.isFinite(avgLat) || !Number.isFinite(avgLng)) { container.dataset.mapInitializing = 'false' return } // Initialize map const map = createMap(maplibregl, { container: 'event-map', center: toMapCenter(avgLat, avgLng), zoom: 16, scrollZoom: false, }) map.on('load', () => { // Add hexagon layers addHexagonLayers(map) // Build GeoJSON features for each location const features: Feature[] = parsedLocations.map((loc) => { const h3Index = h3.latLngToCell(loc.latitude, loc.longitude, H3_RESOLUTION) return h3ToGeoJsonFeature(h3, h3Index, { fillColor: '#3273dc', fillOpacity: 0.2, strokeColor: '#3273dc', strokeWidth: 2, }) }) // Update the source with features updateHexagonSource(map, features) // Fit map to show all hexagons const bounds = calculateBounds(features) if (bounds) { map.fitBounds(bounds, { padding: 20 }) } }) // Mark as successfully initialized container.dataset.mapInitialized = 'true' } catch (e) { console.error('Failed to initialize event map:', e) } finally { container.dataset.mapInitializing = 'false' } }