From cdba28f2ef09c1ec100d82d23a8833b14356735c Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 4 Feb 2026 14:02:14 +0000 Subject: [PATCH] fix([2fd88f42]): resolve infinite loop in dynamic legend handler --- .../frontend/src/components/MapDisplay.tsx | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/heatmap-tool/frontend/src/components/MapDisplay.tsx b/heatmap-tool/frontend/src/components/MapDisplay.tsx index b7bda4f9..b8e3ce65 100644 --- a/heatmap-tool/frontend/src/components/MapDisplay.tsx +++ b/heatmap-tool/frontend/src/components/MapDisplay.tsx @@ -1,5 +1,5 @@ // src/components/MapDisplay.tsx -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { MapContainer, TileLayer, CircleMarker, Tooltip, useMapEvents } from 'react-leaflet'; import { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3'; import 'leaflet/dist/leaflet.css'; @@ -8,6 +8,8 @@ import type { HeatmapPoint, MapMode } from '../App'; import MarkerClusterGroup from 'react-leaflet-cluster'; import Legend from './Legend'; import MapBoundsManager from './MapBoundsManager'; +import { LatLngBounds } from 'leaflet'; + interface MapDisplayProps { heatmapData: HeatmapPoint[]; @@ -15,20 +17,32 @@ interface MapDisplayProps { viewMode: MapMode; } +// This component listens to map events and calls a callback when the view changes +const DynamicLegendHandler = ({ onBoundsChange }: { onBoundsChange: (bounds: LatLngBounds) => void }) => { + const map = useMapEvents({ + zoomend: () => onBoundsChange(map.getBounds()), + moveend: () => onBoundsChange(map.getBounds()), + }); + + // Initial load + useEffect(() => { + onBoundsChange(map.getBounds()); + }, []); // Run only once on initial mount + + return null; +}; + + const MapDisplay: React.FC = ({ heatmapData, radiusMultiplier, viewMode }) => { const germanyCenter: [number, number] = [51.1657, 10.4515]; - // State for the data currently visible in the map viewport const [visibleData, setVisibleData] = useState(heatmapData); - - // The dynamic max count based only on visible data const dynamicMaxCount = Math.max(...visibleData.map(p => p.count), 1); const calculateRadius = (count: number) => { return 3 + Math.log(count + 1) * 5 * radiusMultiplier; }; - // getColor now uses the dynamicMaxCount const getColor = (count: number) => { const ratio = count / dynamicMaxCount; if (ratio > 0.8) return '#d73027'; // Red @@ -36,30 +50,19 @@ const MapDisplay: React.FC = ({ heatmapData, radiusMultiplier, if (ratio > 0.2) return '#fee08b'; // Yellow return '#66bd63'; // Green }; + + const updateVisibleData = useCallback((bounds: LatLngBounds) => { + const visible = heatmapData.filter(p => + bounds.contains([p.lat, p.lon]) + ); + setVisibleData(visible.length > 0 ? visible : heatmapData); // Fallback to all data if none are visible + }, [heatmapData]); + + // Reset visible data when heatmapData changes + useEffect(() => { + setVisibleData(heatmapData); + }, [heatmapData]); - // This component listens to map events and updates the visible data - const DynamicLegendHandler = () => { - const map = useMapEvents({ - zoomend: () => updateVisibleData(), - moveend: () => updateVisibleData(), - }); - - const updateVisibleData = () => { - const bounds = map.getBounds(); - const visible = heatmapData.filter(p => - bounds.contains([p.lat, p.lon]) - ); - setVisibleData(visible.length > 0 ? visible : heatmapData); // Fallback to all data if none are visible - }; - - // Initial load - useEffect(() => { - updateVisibleData(); - }, [heatmapData]); - - - return null; - }; const renderPoints = () => ( @@ -115,7 +118,7 @@ const MapDisplay: React.FC = ({ heatmapData, radiusMultiplier, attribution='© OpenStreetMap contributors' /> - + {viewMode === 'points' ? renderPoints() : renderHeatmap()} {viewMode === 'points' && }