import { useRef, useEffect, useState, useCallback } from 'react' import { MapContainer, TileLayer, Marker, Popup, Rectangle, useMap, useMapEvents } from 'react-leaflet' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import 'leaflet-polylinedecorator' import { useTranslation } from 'react-i18next' import DrawControl from './DrawControl' import MapPoint from './MapPoint' import MiniMap from './MiniMap' import { currentPositionIcon } from './mapIcons' import { getTileUrl } from './types' import type { FlightPoint, CalculatedPointInfo, MapRectangle, ActionMode, MovingPointInfo } from './types' interface MapEventsProps { points: FlightPoint[] handlePolylineClick: (e: L.LeafletMouseEvent) => void containerRef: React.RefObject onMapMove: (center: L.LatLng) => void } function MapEvents({ points, handlePolylineClick, containerRef, onMapMove }: MapEventsProps) { const map = useMap() const polylineRef = useRef(null) const arrowRef = useRef(null) useEffect(() => { const handler = () => onMapMove(map.getCenter()) map.on('moveend', handler) return () => { map.off('moveend', handler) } }, [map, onMapMove]) useEffect(() => { if (polylineRef.current) map.removeLayer(polylineRef.current) if (arrowRef.current) map.removeLayer(arrowRef.current) if (points.length > 1) { const positions: L.LatLngTuple[] = points.map(p => [p.position.lat, p.position.lng]) polylineRef.current = L.polyline(positions, { color: '#36D6C5', weight: 6, opacity: 0.7, lineJoin: 'round' }).addTo(map) arrowRef.current = L.polylineDecorator(polylineRef.current, { patterns: [{ offset: '10%', repeat: '40%', symbol: L.Symbol.arrowHead({ pixelSize: 12, pathOptions: { fillOpacity: 1, weight: 0, color: '#36D6C5' } }) }], }).addTo(map) polylineRef.current.on('click', handlePolylineClick) } const observer = new ResizeObserver(() => map.invalidateSize()) if (containerRef.current) observer.observe(containerRef.current) return () => { if (polylineRef.current) { map.removeLayer(polylineRef.current); polylineRef.current = null } if (arrowRef.current) { map.removeLayer(arrowRef.current); arrowRef.current = null } observer.disconnect() } }, [map, points, handlePolylineClick, containerRef]) return null } function SetView({ center }: { center: L.LatLngExpression }) { const map = useMap() useEffect(() => { map.setView(center) }, [center, map]) return null } function MapRefCapture({ onReady }: { onReady: (m: L.Map) => void }) { const m = useMap() useEffect(() => { onReady(m) }, [m, onReady]) return null } interface Props { points: FlightPoint[] calculatedPointInfo: CalculatedPointInfo[] currentPosition: { lat: number; lng: number } rectangles: MapRectangle[] setRectangles: React.Dispatch> rectangleColor: string actionMode: ActionMode onAddPoint: (lat: number, lng: number) => void onUpdatePoint: (index: number, position: { lat: number; lng: number }) => void onRemovePoint: (id: string) => void onAltitudeChange: (index: number, altitude: number) => void onMetaChange: (index: number, meta: string[]) => void onPolylineClick: (e: L.LeafletMouseEvent) => void onPositionChange: (pos: { lat: number; lng: number }) => void onMapMove: (center: L.LatLng) => void // v2 HUD optional props — safe defaults keep existing call sites intact liveGps?: { lat: number; lon: number; satellites: number; status: string } | null flightLabel?: string } export default function FlightMap({ points, currentPosition, rectangles, setRectangles, rectangleColor, actionMode, onAddPoint, onUpdatePoint, onRemovePoint, onAltitudeChange, onMetaChange, onPolylineClick, onPositionChange, onMapMove, liveGps = null, flightLabel = '—', }: Props) { const { t } = useTranslation() const containerRef = useRef(null) const [movingPoint, setMovingPoint] = useState(null) const [draggablePoints, setDraggablePoints] = useState(points) const polylineClickRef = useRef(false) const [mapInstance, setMapInstance] = useState(null) useEffect(() => { setDraggablePoints(points) }, [points]) const handleMapReady = useCallback((m: L.Map) => { setMapInstance(m) }, []) function ClickHandler() { useMapEvents({ click(e) { if (actionMode === 'points') { if (!polylineClickRef.current) onAddPoint(e.latlng.lat, e.latlng.lng) polylineClickRef.current = false } }, }) return null } const handlePolylineClick = (e: L.LeafletMouseEvent) => { if (actionMode === 'points') { polylineClickRef.current = true onPolylineClick(e) } } const handleDrag = (index: number, pos: { lat: number; lng: number }) => { const updated = [...draggablePoints] updated[index] = { ...updated[index], position: pos } setDraggablePoints(updated) } const displayLat = liveGps?.lat ?? currentPosition.lat const displayLon = liveGps?.lon ?? currentPosition.lng const satelliteCount = liveGps?.satellites ?? 12 return (
{movingPoint && } {draggablePoints.map((point, index) => ( onUpdatePoint(i, pos)} onAltitudeChange={onAltitudeChange} onMetaChange={onMetaChange} onRemove={onRemovePoint} onMoving={setMovingPoint} /> ))} {currentPosition && ( onPositionChange((e.target as L.Marker).getLatLng()) }}> {t('flights.planner.currentLocation')} )} {rectangles.map(rect => ( ))} {/* v2 drawing-hint HUD — restyled to v2 tokens */} {(actionMode === 'workArea' || actionMode === 'prohibitedArea') && (
{t(actionMode === 'workArea' ? 'flights.v2.drawHintWork' : 'flights.v2.drawHintNoGo')}
)} {/* ======================================================= */} {/* Compass rosette — top-left */} {/* ======================================================= */}
N
{/* ======================================================= */} {/* Telemetry HUD — top-right */} {/* ======================================================= */}
{t('flights.v2.hud.liveConnected')} {flightLabel}
{t('flights.v2.hud.sat')} {satelliteCount} / 14
{t('flights.v2.hud.lat')} {displayLat.toFixed(5)}° N
{t('flights.v2.hud.lon')} {displayLon.toFixed(5)}° E
{t('flights.v2.hud.alt')} 320 M / AGL
{t('flights.v2.hud.hdg')} 047° NE
{t('flights.v2.hud.spd')} 11.4 M/S
{t('flights.v2.hud.link')} RSSI -52 DBM
{/* ======================================================= */} {/* Legend — bottom-left */} {/* ======================================================= */}
// {t('flights.v2.hud.mapLegend')}
{t('flights.v2.hud.plannedOriginal')}
{t('flights.v2.hud.correctedLive')}
{t('flights.v2.hud.originStart')}
{t('flights.v2.hud.waypoint')}
{t('flights.v2.hud.targetFinish')}
{/* ======================================================= */} {/* Map toolbar — right edge */} {/* ======================================================= */}
{/* ======================================================= */} {/* Bottom status strip */} {/* ======================================================= */}
{t('flights.v2.strip.telemetryLive')} SSE {t('flights.v2.strip.frame')} 12,847 / 18,400 · {displayLat.toFixed(5)} N · {displayLon.toFixed(5)} E {t('flights.v2.strip.lastPing')} +0.42S
) }