@mytec: 2nd iteration implemented for tests
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet.heat';
|
||||
@@ -28,9 +28,39 @@ function rsrpToIntensity(rsrp: number): number {
|
||||
return Math.max(0, Math.min(1, (rsrp - min) / (max - min)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate adaptive heatmap radius and blur based on zoom level.
|
||||
* Lower zoom (zoomed out) = larger radius for smoother appearance.
|
||||
* Higher zoom (zoomed in) = smaller radius to avoid blocky squares.
|
||||
*
|
||||
* Zoom 6 (country): radius=35, blur=18
|
||||
* Zoom 10 (region): radius=25, blur=13
|
||||
* Zoom 14 (city): radius=15, blur=8
|
||||
* Zoom 18 (street): radius=8, blur=6
|
||||
*/
|
||||
function getHeatmapParams(zoom: number) {
|
||||
const radius = Math.max(8, Math.min(40, 50 - zoom * 2.5));
|
||||
const blur = Math.max(6, Math.min(20, 30 - zoom * 1.5));
|
||||
return { radius: Math.round(radius), blur: Math.round(blur) };
|
||||
}
|
||||
|
||||
export default function Heatmap({ points, visible }: HeatmapProps) {
|
||||
const map = useMap();
|
||||
const [mapZoom, setMapZoom] = useState(map.getZoom());
|
||||
|
||||
// Track zoom changes
|
||||
useEffect(() => {
|
||||
const handleZoomEnd = () => {
|
||||
setMapZoom(map.getZoom());
|
||||
};
|
||||
|
||||
map.on('zoomend', handleZoomEnd);
|
||||
return () => {
|
||||
map.off('zoomend', handleZoomEnd);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
// Recreate heatmap layer when points, visibility, or zoom changes
|
||||
useEffect(() => {
|
||||
if (!visible || points.length === 0) return;
|
||||
|
||||
@@ -40,9 +70,11 @@ export default function Heatmap({ points, visible }: HeatmapProps) {
|
||||
rsrpToIntensity(p.rsrp),
|
||||
]);
|
||||
|
||||
const { radius, blur } = getHeatmapParams(mapZoom);
|
||||
|
||||
const heatLayer = L.heatLayer(heatData, {
|
||||
radius: 15,
|
||||
blur: 20,
|
||||
radius,
|
||||
blur,
|
||||
maxZoom: 17,
|
||||
max: 1.0,
|
||||
minOpacity: 0.3,
|
||||
@@ -61,7 +93,7 @@ export default function Heatmap({ points, visible }: HeatmapProps) {
|
||||
return () => {
|
||||
map.removeLayer(heatLayer);
|
||||
};
|
||||
}, [map, points, visible]);
|
||||
}, [map, points, visible, mapZoom]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'leaflet/dist/leaflet.css';
|
||||
import type { Map as LeafletMap } from 'leaflet';
|
||||
import type { Site } from '@/types/index.ts';
|
||||
import { useSitesStore } from '@/store/sites.ts';
|
||||
import { useSettingsStore } from '@/store/settings.ts';
|
||||
import SiteMarker from './SiteMarker.tsx';
|
||||
|
||||
interface MapViewProps {
|
||||
@@ -42,6 +43,8 @@ function MapRefSetter({ mapRef }: { mapRef: React.MutableRefObject<LeafletMap |
|
||||
export default function MapView({ onMapClick, onEditSite, children }: MapViewProps) {
|
||||
const sites = useSitesStore((s) => s.sites);
|
||||
const isPlacingMode = useSitesStore((s) => s.isPlacingMode);
|
||||
const showTerrain = useSettingsStore((s) => s.showTerrain);
|
||||
const setShowTerrain = useSettingsStore((s) => s.setShowTerrain);
|
||||
const mapRef = useRef<LeafletMap | null>(null);
|
||||
|
||||
const handleFitToSites = useCallback(() => {
|
||||
@@ -62,10 +65,19 @@ export default function MapView({ onMapClick, onEditSite, children }: MapViewPro
|
||||
className={`w-full h-full ${isPlacingMode ? 'cursor-crosshair' : ''}`}
|
||||
>
|
||||
<MapRefSetter mapRef={mapRef} />
|
||||
{/* Base OSM layer */}
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{/* Terrain overlay (OpenTopoMap, semi-transparent when enabled) */}
|
||||
{showTerrain && (
|
||||
<TileLayer
|
||||
attribution='Map data: © OpenStreetMap, SRTM | Style: © <a href="https://opentopomap.org">OpenTopoMap</a>'
|
||||
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
|
||||
opacity={0.6}
|
||||
/>
|
||||
)}
|
||||
<MapClickHandler onMapClick={onMapClick} />
|
||||
{sites
|
||||
.filter((s) => s.visible)
|
||||
@@ -97,6 +109,16 @@ export default function MapView({ onMapClick, onEditSite, children }: MapViewPro
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowTerrain(!showTerrain)}
|
||||
className={`bg-white dark:bg-dark-surface shadow-lg rounded px-3 py-2 text-sm
|
||||
hover:bg-gray-50 dark:hover:bg-dark-border transition-colors
|
||||
text-gray-700 dark:text-dark-text min-h-[36px]
|
||||
${showTerrain ? 'ring-2 ring-blue-500' : ''}`}
|
||||
title={showTerrain ? 'Hide terrain' : 'Show terrain elevation'}
|
||||
>
|
||||
Topo
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user