@mytec: iteration1 implemented

This commit is contained in:
2026-01-30 08:23:29 +02:00
parent f59e63f181
commit 6bc4357a3c
17 changed files with 566 additions and 187 deletions

View File

@@ -47,12 +47,12 @@ export default function Heatmap({ points, visible }: HeatmapProps) {
max: 1.0,
minOpacity: 0.3,
gradient: {
0.0: '#ef4444', // red (weak)
0.2: '#f97316', // orange (poor)
0.4: '#eab308', // yellow (fair)
0.6: '#84cc16', // lime (good)
0.8: '#22c55e', // green (excellent)
1.0: '#16a34a', // dark green (very strong)
0.0: '#0d47a1', // Dark Blue (very weak, -120 dBm)
0.2: '#00bcd4', // Cyan (weak, -110 dBm)
0.4: '#4caf50', // Green (fair, -100 dBm)
0.6: '#ffeb3b', // Yellow (good, -85 dBm)
0.8: '#ff9800', // Orange (strong, -70 dBm)
1.0: '#f44336', // Red (excellent, > -70 dBm)
},
});

View File

@@ -1,22 +1,28 @@
import { RSRP_LEGEND } from '@/constants/rsrp-thresholds.ts';
import { useCoverageStore } from '@/store/coverage.ts';
import { useSitesStore } from '@/store/sites.ts';
export default function Legend() {
const result = useCoverageStore((s) => s.result);
const heatmapVisible = useCoverageStore((s) => s.heatmapVisible);
const toggleHeatmap = useCoverageStore((s) => s.toggleHeatmap);
const settings = useCoverageStore((s) => s.settings);
const sites = useSitesStore((s) => s.sites);
if (!result) return null;
// Estimate coverage area: total points * resolution^2
const areaKm2 = (result.totalPoints * settings.resolution * settings.resolution) / 1e6;
return (
<div className="absolute bottom-6 right-2 z-[1000] bg-white rounded-lg shadow-lg border border-gray-200 p-3 min-w-[160px]">
<div className="absolute bottom-6 right-2 z-[1000] bg-white dark:bg-dark-surface rounded-lg shadow-lg border border-gray-200 dark:border-dark-border p-3 min-w-[170px]">
{/* Header with toggle */}
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-gray-700">Signal (RSRP)</h3>
<h3 className="text-xs font-semibold text-gray-700 dark:text-dark-text">Signal (RSRP)</h3>
<button
onClick={toggleHeatmap}
className={`w-8 h-4 rounded-full transition-colors relative
${heatmapVisible ? 'bg-blue-500' : 'bg-gray-300'}`}
${heatmapVisible ? 'bg-blue-500' : 'bg-gray-300 dark:bg-dark-border'}`}
>
<span
className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow transition-transform
@@ -33,7 +39,7 @@ export default function Legend() {
className="w-3 h-3 rounded-sm flex-shrink-0"
style={{ backgroundColor: item.color }}
/>
<span className="text-[10px] text-gray-600">
<span className="text-[10px] text-gray-600 dark:text-gray-400">
{item.label}: {item.range}
</span>
</div>
@@ -41,11 +47,11 @@ export default function Legend() {
</div>
{/* Stats */}
<div className="mt-2 pt-2 border-t border-gray-100 text-[10px] text-gray-400 space-y-0.5">
<div className="mt-2 pt-2 border-t border-gray-100 dark:border-dark-border text-[10px] text-gray-400 dark:text-dark-muted space-y-0.5">
<div>Points: {result.totalPoints.toLocaleString()}</div>
<div>
Time: {(result.calculationTime / 1000).toFixed(2)}s
</div>
<div>Time: {(result.calculationTime / 1000).toFixed(2)}s</div>
<div>Area: ~{areaKm2.toFixed(1)} km²</div>
<div>Sites: {sites.length}</div>
</div>
</div>
);

View File

@@ -1,5 +1,7 @@
import { MapContainer, TileLayer, useMapEvents } from 'react-leaflet';
import { useRef, useCallback } from 'react';
import { MapContainer, TileLayer, useMapEvents, useMap } from 'react-leaflet';
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 SiteMarker from './SiteMarker.tsx';
@@ -28,27 +30,74 @@ function MapClickHandler({
return null;
}
/**
* Inner component that exposes the map instance via ref callback
*/
function MapRefSetter({ mapRef }: { mapRef: React.MutableRefObject<LeafletMap | null> }) {
const map = useMap();
mapRef.current = map;
return null;
}
export default function MapView({ onMapClick, onEditSite, children }: MapViewProps) {
const sites = useSitesStore((s) => s.sites);
const isPlacingMode = useSitesStore((s) => s.isPlacingMode);
const mapRef = useRef<LeafletMap | null>(null);
const handleFitToSites = useCallback(() => {
if (sites.length === 0 || !mapRef.current) return;
const bounds = sites.map((site) => [site.lat, site.lon] as [number, number]);
mapRef.current.fitBounds(bounds, { padding: [50, 50] });
}, [sites]);
const handleResetView = useCallback(() => {
mapRef.current?.setView([48.4, 35.0], 7);
}, []);
return (
<MapContainer
center={[48.4, 35.0]}
zoom={7}
className={`w-full h-full ${isPlacingMode ? 'cursor-crosshair' : ''}`}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapClickHandler onMapClick={onMapClick} />
{sites
.filter((s) => s.visible)
.map((site) => (
<SiteMarker key={site.id} site={site} onEdit={onEditSite} />
))}
{children}
</MapContainer>
<>
<MapContainer
center={[48.4, 35.0]}
zoom={7}
className={`w-full h-full ${isPlacingMode ? 'cursor-crosshair' : ''}`}
>
<MapRefSetter mapRef={mapRef} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapClickHandler onMapClick={onMapClick} />
{sites
.filter((s) => s.visible)
.map((site) => (
<SiteMarker key={site.id} site={site} onEdit={onEditSite} />
))}
{children}
</MapContainer>
{/* Map control buttons */}
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-2">
<button
onClick={handleFitToSites}
disabled={sites.length === 0}
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
disabled:opacity-40 disabled:cursor-not-allowed
text-gray-700 dark:text-dark-text min-h-[36px]"
title="Fit view to all sites"
>
Fit
</button>
<button
onClick={handleResetView}
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]"
title="Reset to Ukraine view"
>
Reset
</button>
</div>
</>
);
}