forked from
smokesignal.events/smokesignal
fork
Configure Feed
Select the types of activity you want to include in your feed.
The smokesignal.events web application
fork
Configure Feed
Select the types of activity you want to include in your feed.
1/**
2 * Location Heatmap Feature
3 *
4 * Renders the location heatmap on the location page showing event distribution.
5 * Uses MapLibre GL for map rendering and H3 for hexagon visualization.
6 */
7
8import type { Feature, Polygon } from 'geojson'
9import type { H3Bucket } from '../../types'
10import {
11 loadMapLibraries,
12 createMap,
13 h3ToGeoJsonFeature,
14 addHexagonLayers,
15 updateHexagonSource,
16 calculateBounds,
17 heatmapGradientColor,
18 toMapCenter,
19} from './map-utils'
20
21export async function initLocationHeatmap(): Promise<void> {
22 const mapContainer = document.getElementById('location-heatmap')
23 if (!mapContainer) return
24
25 // Skip if already initialized or currently initializing
26 if (
27 mapContainer.dataset.mapInitialized === 'true' ||
28 mapContainer.dataset.mapInitializing === 'true'
29 )
30 return
31 mapContainer.dataset.mapInitializing = 'true'
32
33 try {
34 // Parse data from data attributes
35 const centerLat = parseFloat(mapContainer.dataset.centerLat ?? '0')
36 const centerLon = parseFloat(mapContainer.dataset.centerLon ?? '0')
37 const centerCell = mapContainer.dataset.centerCell ?? ''
38 const geoBucketsData = mapContainer.dataset.geoBuckets
39
40 if (!geoBucketsData) return
41
42 let geoBuckets: H3Bucket[]
43 try {
44 geoBuckets = JSON.parse(geoBucketsData)
45 } catch (e) {
46 console.error('Failed to parse geo buckets:', e)
47 return
48 }
49
50 // Lazy load MapLibre and H3
51 const { maplibregl, h3 } = await loadMapLibraries()
52
53 // Create non-interactive map
54 const map = createMap(maplibregl, {
55 container: 'location-heatmap',
56 center: toMapCenter(centerLat, centerLon),
57 zoom: 9,
58 interactive: false,
59 })
60
61 map.on('load', () => {
62 // Add hexagon layers
63 addHexagonLayers(map)
64
65 // Only draw hexes with events
66 if (geoBuckets && geoBuckets.length > 0) {
67 const counts = geoBuckets.map((b) => b.doc_count ?? 0)
68 const minCount = Math.min(...counts)
69 const maxCount = Math.max(...counts)
70
71 const features: Feature<Polygon>[] = []
72
73 geoBuckets.forEach((bucket) => {
74 try {
75 const cellIndex = bucket.key
76 const count = bucket.doc_count ?? 0
77 const isCenter = cellIndex === centerCell
78 const color = heatmapGradientColor(count, minCount, maxCount)
79
80 features.push(
81 h3ToGeoJsonFeature(h3, cellIndex, {
82 fillColor: color,
83 fillOpacity: 0.5,
84 strokeColor: isCenter ? '#1a1a1a' : color,
85 strokeWidth: isCenter ? 3 : 2,
86 })
87 )
88 } catch (e) {
89 console.warn('Failed to draw hex:', bucket.key, e)
90 }
91 })
92
93 // Update the source with features
94 updateHexagonSource(map, features)
95
96 // Fit bounds to show all hexes
97 const bounds = calculateBounds(features)
98 if (bounds) {
99 map.fitBounds(bounds, { padding: 10 })
100 }
101 }
102 })
103
104 mapContainer.dataset.mapInitialized = 'true'
105 } catch (err) {
106 console.error('Failed to initialize location heatmap:', err)
107 } finally {
108 mapContainer.dataset.mapInitializing = 'false'
109 }
110}