feat: implement heat map visualization with Leaflet.heat plugin

- Integrate Leaflet.heat plugin for heat map overlay
- Add heat map toggle control in MapFilters component
- Configure heat map intensity based on report quantity levels (minimal: 0.3, moderate: 0.5, significant: 0.7, severe: 1.0)
- Implement heat map layer management with proper cleanup
- Add custom gradient colors (blue to red) for visual intensity representation
- Configure heat map with radius: 25px, blur: 15, maxZoom: 17
- Add TypeScript type declarations for leaflet.heat
- Heat map respects existing filter state (date range, litter type, quantity)

Implements task 5: Create heat map visualization
Requirements: 3.1, 3.5
This commit is contained in:
2025-11-16 16:05:31 +00:00
parent ba324b370e
commit 8f297e1ec4
5 changed files with 114 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import 'leaflet.markercluster';
import 'leaflet.heat';
import type { Report, CleanupEvent, FilterState } from '@/types';
// Fix for default marker icons in Leaflet with Vite
@@ -37,6 +38,7 @@ export function MapView({
onMarkerClick,
onMapClick,
showClusters = true,
showHeatmap = false,
center = [53.3498, -6.2603], // Dublin, Ireland default
zoom = 7,
}: MapViewProps) {
@@ -44,6 +46,7 @@ export function MapView({
const mapInstanceRef = useRef<L.Map | null>(null);
const markersLayerRef = useRef<L.LayerGroup | null>(null);
const clusterGroupRef = useRef<L.MarkerClusterGroup | null>(null);
const heatLayerRef = useRef<L.HeatLayer | null>(null);
// Initialize map
useEffect(() => {
@@ -99,6 +102,7 @@ export function MapView({
mapInstanceRef.current = null;
markersLayerRef.current = null;
clusterGroupRef.current = null;
heatLayerRef.current = null;
};
}, []);
@@ -221,6 +225,57 @@ export function MapView({
markersLayer.addTo(map);
}, [reports, events, filters, showClusters, onMarkerClick]);
// Update heat map when reports, filters, or showHeatmap changes
useEffect(() => {
if (!mapInstanceRef.current) return;
const map = mapInstanceRef.current;
// Remove existing heat layer if it exists
if (heatLayerRef.current && map.hasLayer(heatLayerRef.current)) {
map.removeLayer(heatLayerRef.current);
heatLayerRef.current = null;
}
// Only create heat layer if showHeatmap is true
if (showHeatmap) {
const filteredReports = getFilteredReports();
// Convert reports to heat map data points
// Format: [latitude, longitude, intensity]
const heatData: [number, number, number][] = filteredReports.map((report) => {
// Calculate intensity based on quantity level
const intensityMap = {
minimal: 0.3,
moderate: 0.5,
significant: 0.7,
severe: 1.0,
};
const intensity = intensityMap[report.quantity] || 0.5;
return [report.latitude, report.longitude, intensity];
});
// Create heat layer with configuration
if (heatData.length > 0) {
heatLayerRef.current = L.heatLayer(heatData, {
radius: 25, // Radius of each heat point in pixels
blur: 15, // Amount of blur
maxZoom: 17, // Maximum zoom level for heat layer
max: 1.0, // Maximum intensity value
gradient: {
0.0: '#0000ff', // Blue for low intensity
0.3: '#00ffff', // Cyan
0.5: '#00ff00', // Green
0.7: '#ffff00', // Yellow
0.9: '#ff8800', // Orange
1.0: '#ff0000', // Red for high intensity
},
}).addTo(map);
}
}
}, [reports, filters, showHeatmap]);
// Handle geolocation
const handleGeolocation = () => {
if (!navigator.geolocation) {