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:
Oleksandr Hutsul
2026-04-17 00:31:24 +03:00
parent 567092188d
commit 274800e508
21 changed files with 1489 additions and 131 deletions
+87
View File
@@ -0,0 +1,87 @@
import { useRef } from 'react'
import { Marker, Popup } from 'react-leaflet'
import { useTranslation } from 'react-i18next'
import { pointIconGreen, pointIconBlue, pointIconRed } from './mapIcons'
import { PURPOSES } from './types'
import type { FlightPoint, MovingPointInfo } from './types'
import type L from 'leaflet'
interface Props {
point: FlightPoint
points: FlightPoint[]
index: number
mapElement: HTMLElement | null
onDrag: (index: number, position: { lat: number; lng: number }) => void
onDragEnd: (index: number, position: { lat: number; lng: number }) => void
onAltitudeChange: (index: number, altitude: number) => void
onMetaChange: (index: number, meta: string[]) => void
onRemove: (id: string) => void
onMoving: (info: MovingPointInfo | null) => void
}
export default function MapPoint({
point, points, index, mapElement,
onDrag, onDragEnd, onAltitudeChange, onMetaChange, onRemove, onMoving,
}: Props) {
const { t } = useTranslation()
const markerRef = useRef<L.Marker>(null)
const icon = index === 0 ? pointIconGreen : index === points.length - 1 ? pointIconRed : pointIconBlue
const handleMove = (e: L.LeafletEvent) => {
const marker = markerRef.current
if (!marker || !mapElement) return
const markerEl = (marker as unknown as { _icon: HTMLElement })._icon
if (!markerEl) return
const mapRect = mapElement.getBoundingClientRect()
const mRect = markerEl.getBoundingClientRect()
const dx = mRect.left - mapRect.left + mRect.width > mapRect.width / 2 ? -150 : 200
const dy = mRect.top + mRect.height > mapRect.height / 2 ? -150 : 150
onMoving({ x: mRect.left - mapRect.left + dx, y: mRect.top - mapRect.top + dy, latlng: (e.target as L.Marker).getLatLng() })
}
const toggleMeta = (value: string) => {
const newMeta = point.meta.includes(value) ? point.meta.filter(m => m !== value) : [...point.meta, value]
onMetaChange(index, newMeta)
}
return (
<Marker
position={point.position}
icon={icon}
draggable
ref={markerRef}
eventHandlers={{
drag: (e) => onDrag(index, (e.target as L.Marker).getLatLng()),
dragend: (e) => { onDragEnd(index, (e.target as L.Marker).getLatLng()); onMoving(null) },
move: handleMove,
}}
>
<Popup>
<div className="text-xs space-y-1.5 min-w-[140px]">
<div className="font-semibold">{t('flights.planner.point')} {index + 1}</div>
<div>
<label className="text-az-muted text-[10px]">{t('flights.planner.altitude')}</label>
<input type="range" min={0} max={3000} value={point.altitude}
onChange={e => onAltitudeChange(index, Number(e.target.value))}
className="w-full accent-az-orange" />
<span className="text-[10px] text-az-muted">{point.altitude}m</span>
</div>
<div className="flex gap-2">
{PURPOSES.map(p => (
<label key={p.value} className="flex items-center gap-1 text-[10px] cursor-pointer">
<input type="checkbox" checked={point.meta.includes(p.value)}
onChange={() => toggleMeta(p.value)} className="accent-az-orange" />
{t(`flights.planner.${p.label}`)}
</label>
))}
</div>
<button onClick={() => onRemove(point.id)}
className="text-az-red text-[10px] hover:underline">
{t('flights.planner.removePoint')}
</button>
</div>
</Popup>
</Marker>
)
}