mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 06:46:34 +00:00
274800e508
- Port mission-planner flight planning to main app (Tailwind, react-leaflet v5, react-i18next) - Add FlightMap with click-to-add waypoints, draggable markers, polyline with arrows - Add FlightParamsPanel with action modes, waypoint list (drag-reorder), altitude chart, wind, JSON import/export - Add FlightListSidebar with create/delete and telemetry date - Add collapsible left panel with quick action mode shortcuts - Add work area / no-go zone drawing via manual mouse events (L.rectangle) - Add AltitudeDialog and JsonEditorDialog (Tailwind modals) - Add battery/time/distance calculations per waypoint segment - Add satellite/classic map toggle and mini-map on point drag - Add Camera FOV and Communication Addr fields - Add current position display under location search - Merge mission-planner translations under flights.planner.* - Gitignore .superpowers session data
184 lines
7.3 KiB
TypeScript
184 lines
7.3 KiB
TypeScript
import { useRef, useEffect, useState } from 'react'
|
|
import { MapContainer, TileLayer, Marker, Popup, Polyline, 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 { defaultIcon } from './mapIcons'
|
|
import { TILE_URLS } from './types'
|
|
import type { FlightPoint, CalculatedPointInfo, MapRectangle, ActionMode, MovingPointInfo } from './types'
|
|
|
|
interface MapEventsProps {
|
|
points: FlightPoint[]
|
|
handlePolylineClick: (e: L.LeafletMouseEvent) => void
|
|
containerRef: React.RefObject<HTMLDivElement | null>
|
|
onMapMove: (center: L.LatLng) => void
|
|
}
|
|
|
|
function MapEvents({ points, handlePolylineClick, containerRef, onMapMove }: MapEventsProps) {
|
|
const map = useMap()
|
|
const polylineRef = useRef<L.Polyline | null>(null)
|
|
const arrowRef = useRef<L.FeatureGroup | null>(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: '#228be6', 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: '#228be6' } }) }],
|
|
}).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
|
|
}
|
|
|
|
interface Props {
|
|
points: FlightPoint[]
|
|
calculatedPointInfo: CalculatedPointInfo[]
|
|
currentPosition: { lat: number; lng: number }
|
|
rectangles: MapRectangle[]
|
|
setRectangles: React.Dispatch<React.SetStateAction<MapRectangle[]>>
|
|
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
|
|
}
|
|
|
|
export default function FlightMap({
|
|
points, currentPosition, rectangles, setRectangles,
|
|
rectangleColor, actionMode, onAddPoint, onUpdatePoint, onRemovePoint,
|
|
onAltitudeChange, onMetaChange, onPolylineClick, onPositionChange, onMapMove,
|
|
}: Props) {
|
|
const { t } = useTranslation()
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const [mapType, setMapType] = useState<'classic' | 'satellite'>('satellite')
|
|
const [movingPoint, setMovingPoint] = useState<MovingPointInfo | null>(null)
|
|
const [draggablePoints, setDraggablePoints] = useState(points)
|
|
const polylineClickRef = useRef(false)
|
|
|
|
useEffect(() => { setDraggablePoints(points) }, [points])
|
|
|
|
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)
|
|
}
|
|
|
|
return (
|
|
<div className="flex-1 relative" ref={containerRef}>
|
|
<MapContainer center={currentPosition} zoom={15} className="h-full w-full">
|
|
<ClickHandler />
|
|
<TileLayer
|
|
url={mapType === 'classic' ? TILE_URLS.classic : TILE_URLS.satellite}
|
|
attribution={mapType === 'classic' ? '© <a href="https://www.openstreetmap.org/copyright">OSM</a>' : 'Satellite'}
|
|
/>
|
|
<MapEvents points={draggablePoints} handlePolylineClick={handlePolylineClick} containerRef={containerRef} onMapMove={onMapMove} />
|
|
<SetView center={currentPosition} />
|
|
|
|
{movingPoint && <MiniMap pointPosition={movingPoint} mapType={mapType} />}
|
|
|
|
{draggablePoints.map((point, index) => (
|
|
<MapPoint key={point.id}
|
|
point={point} points={draggablePoints} index={index}
|
|
mapElement={containerRef.current}
|
|
onDrag={handleDrag}
|
|
onDragEnd={(i, pos) => onUpdatePoint(i, pos)}
|
|
onAltitudeChange={onAltitudeChange}
|
|
onMetaChange={onMetaChange}
|
|
onRemove={onRemovePoint}
|
|
onMoving={setMovingPoint}
|
|
/>
|
|
))}
|
|
|
|
{draggablePoints.length > 1 && (
|
|
<Polyline
|
|
positions={[[draggablePoints[draggablePoints.length - 1].position.lat, draggablePoints[draggablePoints.length - 1].position.lng],
|
|
[draggablePoints[0].position.lat, draggablePoints[0].position.lng]]}
|
|
color="#228be6" dashArray="5,10"
|
|
/>
|
|
)}
|
|
|
|
{currentPosition && (
|
|
<Marker position={currentPosition} icon={defaultIcon} draggable
|
|
eventHandlers={{ dragend: (e) => onPositionChange((e.target as L.Marker).getLatLng()) }}>
|
|
<Popup>{t('flights.planner.currentLocation')}</Popup>
|
|
</Marker>
|
|
)}
|
|
|
|
{rectangles.map(rect => (
|
|
<Rectangle key={rect.id} bounds={rect.bounds} pathOptions={{ color: rect.color }} />
|
|
))}
|
|
|
|
<DrawControl color={rectangleColor} actionMode={actionMode} rectangles={rectangles} setRectangles={setRectangles} />
|
|
</MapContainer>
|
|
|
|
{(actionMode === 'workArea' || actionMode === 'prohibitedArea') && (
|
|
<div className="absolute top-2 left-1/2 -translate-x-1/2 z-[400] bg-az-panel/90 border border-az-border rounded px-3 py-1 text-[11px] text-az-text pointer-events-none">
|
|
Click and drag on the map to draw a {actionMode === 'workArea' ? 'work area' : 'no-go zone'}
|
|
</div>
|
|
)}
|
|
|
|
<button onClick={() => setMapType(m => m === 'classic' ? 'satellite' : 'classic')}
|
|
className={`absolute top-2 right-2 z-[400] px-2 py-1 text-xs rounded border ${
|
|
mapType === 'satellite' ? 'bg-az-panel border-az-orange text-white' : 'bg-az-panel border-az-border text-az-text'
|
|
}`}>
|
|
{t('flights.planner.satellite')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|