124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
// src/components/MapDisplay.tsx
|
|
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';
|
|
import 'leaflet.heat';
|
|
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[];
|
|
radiusMultiplier: number;
|
|
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()),
|
|
load: () => onBoundsChange(map.getBounds()), // Handle initial load
|
|
});
|
|
|
|
return null;
|
|
};
|
|
|
|
|
|
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier, viewMode }) => {
|
|
const germanyCenter: [number, number] = [51.1657, 10.4515];
|
|
|
|
const [visibleData, setVisibleData] = useState<HeatmapPoint[]>(heatmapData);
|
|
const dynamicMaxCount = Math.max(...visibleData.map(p => p.count), 1);
|
|
|
|
const calculateRadius = (count: number) => {
|
|
return 3 + Math.log(count + 1) * 5 * radiusMultiplier;
|
|
};
|
|
|
|
const getColor = (count: number) => {
|
|
const ratio = count / dynamicMaxCount;
|
|
if (ratio > 0.8) return '#d73027'; // Red
|
|
if (ratio > 0.5) return '#fdae61'; // Orange
|
|
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]);
|
|
|
|
|
|
const renderPoints = () => (
|
|
<MarkerClusterGroup>
|
|
{heatmapData.map((point, idx) => (
|
|
<CircleMarker
|
|
key={idx}
|
|
center={[point.lat, point.lon]}
|
|
radius={calculateRadius(point.count)}
|
|
pathOptions={{
|
|
color: getColor(point.count),
|
|
fillColor: getColor(point.count),
|
|
fillOpacity: 0.7
|
|
}}
|
|
>
|
|
<Tooltip>
|
|
PLZ: {point.plz} <br />
|
|
Count: {point.count}
|
|
{point.attributes_summary && Object.entries(point.attributes_summary).map(([attr, values]) => (
|
|
<div key={attr}>
|
|
<strong>{attr}:</strong> {values.join(', ')}
|
|
</div>
|
|
))}
|
|
</Tooltip>
|
|
</CircleMarker>
|
|
))}
|
|
</MarkerClusterGroup>
|
|
);
|
|
|
|
const renderHeatmap = () => (
|
|
<HeatmapLayer
|
|
points={heatmapData}
|
|
longitudeExtractor={(p: HeatmapPoint) => p.lon}
|
|
latitudeExtractor={(p: HeatmapPoint) => p.lat}
|
|
intensityExtractor={(p: HeatmapPoint) => p.count}
|
|
radius={25}
|
|
blur={20}
|
|
max={dynamicMaxCount * 0.1}
|
|
/>
|
|
);
|
|
|
|
if (heatmapData.length === 0) {
|
|
return (
|
|
<div style={{ textAlign: 'center', paddingTop: '50px' }}>
|
|
<p>No data to display on the map.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<MapContainer key={viewMode} center={germanyCenter} zoom={6} style={{ height: '100%', width: '100%' }}>
|
|
<TileLayer
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
/>
|
|
<MapBoundsManager data={heatmapData} />
|
|
<DynamicLegendHandler onBoundsChange={updateVisibleData} />
|
|
{viewMode === 'points' ? renderPoints() : renderHeatmap()}
|
|
{viewMode === 'points' && <Legend getColor={getColor} maxCount={dynamicMaxCount} />}
|
|
</MapContainer>
|
|
);
|
|
};
|
|
|
|
export default MapDisplay; |