mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 11:16:34 +00:00
feat(flights): integrate mission-planner into Flights page
- 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
This commit is contained in:
@@ -1,35 +1,183 @@
|
||||
import { MapContainer, TileLayer, Marker, Polyline, Popup } from 'react-leaflet'
|
||||
import type { Waypoint } from '../../types'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
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'
|
||||
|
||||
const icon = L.divIcon({ className: 'bg-az-orange rounded-full w-3 h-3 border border-white', iconSize: [12, 12] })
|
||||
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 {
|
||||
waypoints: Waypoint[]
|
||||
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({ waypoints }: Props) {
|
||||
const center: [number, number] = waypoints.length > 0
|
||||
? [waypoints[0].latitude, waypoints[0].longitude]
|
||||
: [50.45, 30.52]
|
||||
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)
|
||||
|
||||
const positions = waypoints
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(w => [w.latitude, w.longitude] as [number, number])
|
||||
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 (
|
||||
<MapContainer center={center} zoom={13} className="h-full w-full" key={center.join(',')}>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OSM</a>'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{waypoints.map(wp => (
|
||||
<Marker key={wp.id} position={[wp.latitude, wp.longitude]} icon={icon}>
|
||||
<Popup>{wp.name}</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
{positions.length > 1 && <Polyline positions={positions} color="#fd7e14" weight={2} />}
|
||||
</MapContainer>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user