mirror of
https://github.com/azaion/ui.git
synced 2026-06-23 04:01:11 +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,46 +1,82 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import L from 'leaflet'
|
||||
import { useFlight } from '../../components/FlightContext'
|
||||
import { api } from '../../api/client'
|
||||
import { createSSE } from '../../api/sse'
|
||||
import { useResizablePanel } from '../../hooks/useResizablePanel'
|
||||
import ConfirmDialog from '../../components/ConfirmDialog'
|
||||
import type { Flight, Waypoint, Aircraft } from '../../types'
|
||||
import FlightListSidebar from './FlightListSidebar'
|
||||
import FlightParamsPanel from './FlightParamsPanel'
|
||||
import FlightMap from './FlightMap'
|
||||
import AltitudeDialog from './AltitudeDialog'
|
||||
import JsonEditorDialog from './JsonEditorDialog'
|
||||
import { newGuid, calculateDistance, calculateAllPoints, parseCoordinates, getMockAircraftParams } from './flightPlanUtils'
|
||||
import { PURPOSES } from './types'
|
||||
import type { Aircraft, Waypoint } from '../../types'
|
||||
import type { FlightPoint, CalculatedPointInfo, MapRectangle, ActionMode, WindParams, AircraftParams } from './types'
|
||||
|
||||
export default function FlightsPage() {
|
||||
const { t } = useTranslation()
|
||||
const { flights, selectedFlight, selectFlight, refreshFlights } = useFlight()
|
||||
|
||||
const [mode, setMode] = useState<'params' | 'gps'>('params')
|
||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([])
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const [aircrafts, setAircrafts] = useState<Aircraft[]>([])
|
||||
const [liveGps, setLiveGps] = useState<{ lat: number; lon: number; satellites: number; status: string } | null>(null)
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [newName, setNewName] = useState('')
|
||||
const leftPanel = useResizablePanel(200, 150, 350)
|
||||
|
||||
const [aircraft, setAircraft] = useState<AircraftParams | null>(null)
|
||||
const [points, setPoints] = useState<FlightPoint[]>([])
|
||||
const [calculatedPointInfo, setCalculatedPointInfo] = useState<CalculatedPointInfo[]>([])
|
||||
const [rectangles, setRectangles] = useState<MapRectangle[]>([])
|
||||
const [actionMode, setActionMode] = useState<ActionMode>('points')
|
||||
const [wind, setWind] = useState<WindParams>({ direction: 0, speed: 0 })
|
||||
const [initialAltitude, setInitialAltitude] = useState(1000)
|
||||
const [currentPosition, setCurrentPosition] = useState<{ lat: number; lng: number }>({ lat: 47.242, lng: 35.024 })
|
||||
const [locationInput, setLocationInput] = useState('')
|
||||
|
||||
const [altDialog, setAltDialog] = useState<{ open: boolean; point: FlightPoint | null; isEdit: boolean }>({ open: false, point: null, isEdit: false })
|
||||
const [jsonDialog, setJsonDialog] = useState({ open: false, text: '' })
|
||||
|
||||
useEffect(() => {
|
||||
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
|
||||
setAircraft(getMockAircraftParams())
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => setCurrentPosition({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
|
||||
() => {},
|
||||
)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFlight) { setWaypoints([]); return }
|
||||
api.get<Waypoint[]>(`/api/flights/${selectedFlight.id}/waypoints`).then(setWaypoints).catch(() => {})
|
||||
if (!selectedFlight) { setPoints([]); return }
|
||||
api.get<Waypoint[]>(`/api/flights/${selectedFlight.id}/waypoints`)
|
||||
.then(wps => {
|
||||
setPoints(wps.sort((a, b) => a.order - b.order).map(wp => ({
|
||||
id: wp.id,
|
||||
position: { lat: wp.latitude, lng: wp.longitude },
|
||||
altitude: 300,
|
||||
meta: [PURPOSES[0].value, PURPOSES[1].value],
|
||||
})))
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [selectedFlight])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFlight || mode !== 'gps') return
|
||||
return createSSE(`/api/flights/${selectedFlight.id}/live-gps`, (data: any) => setLiveGps(data))
|
||||
return createSSE<{ lat: number; lon: number; satellites: number; status: string }>(`/api/flights/${selectedFlight.id}/live-gps`, (data) => setLiveGps(data))
|
||||
}, [selectedFlight, mode])
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newName.trim()) return
|
||||
await api.post('/api/flights', { name: newName.trim() })
|
||||
setNewName('')
|
||||
useEffect(() => {
|
||||
if (!aircraft || points.length < 2) { setCalculatedPointInfo([{ bat: 100, time: 0 }]); return }
|
||||
calculateAllPoints(points, aircraft, initialAltitude).then(setCalculatedPointInfo)
|
||||
}, [points, aircraft, initialAltitude])
|
||||
|
||||
const handleCreateFlight = async (name: string) => {
|
||||
await api.post('/api/flights', { name })
|
||||
refreshFlights()
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
const handleDeleteFlight = async () => {
|
||||
if (!deleteId) return
|
||||
await api.delete(`/api/flights/${deleteId}`)
|
||||
if (selectedFlight?.id === deleteId) selectFlight(null)
|
||||
@@ -48,105 +84,203 @@ export default function FlightsPage() {
|
||||
refreshFlights()
|
||||
}
|
||||
|
||||
const handleAddWaypoint = async () => {
|
||||
if (!selectedFlight) return
|
||||
await api.post(`/api/flights/${selectedFlight.id}/waypoints`, {
|
||||
name: `Point ${waypoints.length}`,
|
||||
latitude: 50.45, longitude: 30.52, order: waypoints.length,
|
||||
const addPoint = useCallback((lat: number, lng: number) => {
|
||||
const pt: FlightPoint = { id: newGuid(), position: { lat, lng }, altitude: initialAltitude, meta: [PURPOSES[0].value, PURPOSES[1].value] }
|
||||
setPoints(prev => [...prev, pt])
|
||||
}, [initialAltitude])
|
||||
|
||||
const updatePointPosition = useCallback((index: number, pos: { lat: number; lng: number }) => {
|
||||
setPoints(prev => prev.map((p, i) => i === index ? { ...p, position: pos } : p))
|
||||
}, [])
|
||||
|
||||
const removePoint = useCallback((id: string) => {
|
||||
setPoints(prev => prev.filter(p => p.id !== id))
|
||||
}, [])
|
||||
|
||||
const handlePolylineClick = useCallback((e: L.LeafletMouseEvent) => {
|
||||
const click = e.latlng
|
||||
let closestIdx = -1, minDist = Infinity
|
||||
points.forEach((p, i) => {
|
||||
if (i < points.length - 1) {
|
||||
const dist = L.LineUtil.pointToSegmentDistance(
|
||||
L.point(click.lng, click.lat),
|
||||
L.point(p.position.lng, p.position.lat),
|
||||
L.point(points[i + 1].position.lng, points[i + 1].position.lat),
|
||||
)
|
||||
if (dist < minDist) { minDist = dist; closestIdx = i }
|
||||
}
|
||||
})
|
||||
const wps = await api.get<Waypoint[]>(`/api/flights/${selectedFlight.id}/waypoints`)
|
||||
setWaypoints(wps)
|
||||
if (closestIdx !== -1) {
|
||||
const alt = (points[closestIdx].altitude + points[closestIdx + 1].altitude) / 2
|
||||
const pt: FlightPoint = { id: newGuid(), position: { lat: click.lat, lng: click.lng }, altitude: alt, meta: [PURPOSES[0].value, PURPOSES[1].value] }
|
||||
setPoints(prev => { const u = [...prev]; u.splice(closestIdx + 1, 0, pt); return u })
|
||||
}
|
||||
}, [points])
|
||||
|
||||
const openEditDialog = (point: FlightPoint) => {
|
||||
setAltDialog({ open: true, point: { ...point }, isEdit: true })
|
||||
}
|
||||
const handleAltSubmit = () => {
|
||||
if (!altDialog.point) return
|
||||
if (altDialog.isEdit) {
|
||||
setPoints(prev => prev.map(p => p.id === altDialog.point!.id ? altDialog.point! : p))
|
||||
} else {
|
||||
setPoints(prev => [...prev, altDialog.point!])
|
||||
}
|
||||
setAltDialog({ open: false, point: null, isEdit: false })
|
||||
}
|
||||
|
||||
const handleDeleteWaypoint = async (wpId: string) => {
|
||||
if (!selectedFlight) return
|
||||
await api.delete(`/api/flights/${selectedFlight.id}/waypoints/${wpId}`)
|
||||
setWaypoints(prev => prev.filter(w => w.id !== wpId))
|
||||
const handleEditJson = () => {
|
||||
const data = {
|
||||
operational_height: { currentAltitude: initialAltitude },
|
||||
geofences: { polygons: rectangles.map(r => {
|
||||
const sw = r.bounds.getSouthWest(), ne = r.bounds.getNorthEast()
|
||||
return { northWest: { lat: ne.lat, lon: sw.lng }, southEast: { lat: sw.lat, lon: ne.lng }, fence_type: r.color === 'red' ? 'EXCLUSION' : 'INCLUSION' }
|
||||
})},
|
||||
action_points: points.map(p => ({ point: { lat: p.position.lat, lon: p.position.lng }, height: p.altitude, action: 'search', action_specific: { targets: p.meta } })),
|
||||
}
|
||||
setJsonDialog({ open: true, text: JSON.stringify(data, null, 2) })
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
const data = {
|
||||
operational_height: { currentAltitude: initialAltitude },
|
||||
geofences: { polygons: rectangles.map(r => {
|
||||
const sw = r.bounds.getSouthWest(), ne = r.bounds.getNorthEast()
|
||||
return { northWest: { lat: ne.lat, lon: sw.lng }, southEast: { lat: sw.lat, lon: ne.lng }, fence_type: r.color === 'red' ? 'EXCLUSION' : 'INCLUSION' }
|
||||
})},
|
||||
action_points: points.map(p => ({ point: { lat: p.position.lat, lon: p.position.lng }, height: p.altitude, action: 'search', action_specific: { targets: p.meta } })),
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${selectedFlight?.name ?? 'flight-plan'}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const handleImport = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'application/json'
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
const text = await file.text()
|
||||
handleJsonSave(text)
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const handleJsonSave = (json: string) => {
|
||||
try {
|
||||
const data = JSON.parse(json)
|
||||
if (data.action_points) {
|
||||
setPoints(data.action_points.map((ap: Record<string, unknown>) => ({
|
||||
id: newGuid(),
|
||||
position: {
|
||||
lat: (ap.point as Record<string, number>)?.lat ?? (ap.lat as number),
|
||||
lng: (ap.point as Record<string, number>)?.lon ?? (ap.lon as number),
|
||||
},
|
||||
altitude: (ap.height as number) || 300,
|
||||
meta: (ap.action_specific as Record<string, string[]>)?.targets || [PURPOSES[0].value, PURPOSES[1].value],
|
||||
})))
|
||||
}
|
||||
if (data.geofences?.polygons) {
|
||||
setRectangles(data.geofences.polygons.map((p: { southEast: { lat: number; lon: number }; northWest: { lat: number; lon: number }; fence_type?: string }) => ({
|
||||
id: newGuid(),
|
||||
bounds: L.latLngBounds([p.southEast.lat, p.northWest.lon], [p.northWest.lat, p.southEast.lon]),
|
||||
color: p.fence_type === 'EXCLUSION' ? 'red' as const : 'green' as const,
|
||||
})))
|
||||
}
|
||||
if (data.operational_height?.currentAltitude) setInitialAltitude(data.operational_height.currentAltitude)
|
||||
setJsonDialog({ open: false, text: '' })
|
||||
} catch { alert(t('flights.planner.invalidJson')) }
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedFlight) return
|
||||
const existing = await api.get<Waypoint[]>(`/api/flights/${selectedFlight.id}/waypoints`).catch(() => [] as Waypoint[])
|
||||
for (const wp of existing) {
|
||||
await api.delete(`/api/flights/${selectedFlight.id}/waypoints/${wp.id}`).catch(() => {})
|
||||
}
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
await api.post(`/api/flights/${selectedFlight.id}/waypoints`, {
|
||||
name: `Point ${i + 1}`,
|
||||
latitude: points[i].position.lat,
|
||||
longitude: points[i].position.lng,
|
||||
order: i,
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
const handleLocationSearch = () => {
|
||||
const coords = parseCoordinates(locationInput)
|
||||
if (coords) setCurrentPosition(coords)
|
||||
}
|
||||
|
||||
const lastInfo = calculatedPointInfo[calculatedPointInfo.length - 1]
|
||||
const batteryStatus = !lastInfo ? { label: '', color: '#6c757d' }
|
||||
: lastInfo.bat > 12 ? { label: t('flights.planner.statusGood'), color: '#40c057' }
|
||||
: lastInfo.bat > 5 ? { label: t('flights.planner.statusCaution'), color: '#FFFF00' }
|
||||
: { label: t('flights.planner.statusLow'), color: '#fa5252' }
|
||||
|
||||
const totalDist = aircraft && points.length > 1
|
||||
? points.reduce((acc, p, i) => i === 0 ? 0 : acc + calculateDistance(points[i - 1], p, aircraft.type, initialAltitude, aircraft.downang, aircraft.upang), 0).toFixed(1) + t('flights.planner.km')
|
||||
: ''
|
||||
const totalTimeStr = lastInfo && lastInfo.time > 0
|
||||
? `${Math.floor(lastInfo.time) >= 1 ? Math.floor(lastInfo.time) + t('flights.planner.hour') : ''}${Math.floor((lastInfo.time - Math.floor(lastInfo.time)) * 60)}${t('flights.planner.minutes')}`
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Flight list sidebar */}
|
||||
<div style={{ width: leftPanel.width }} className="bg-az-panel border-r border-az-border flex flex-col shrink-0">
|
||||
<div className="p-2 border-b border-az-border">
|
||||
<div className="flex gap-1">
|
||||
<input
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleCreate()}
|
||||
placeholder={t('flights.create')}
|
||||
className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text outline-none"
|
||||
/>
|
||||
<button onClick={handleCreate} className="bg-az-orange text-white text-xs px-2 py-1 rounded">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{flights.map(f => (
|
||||
<div
|
||||
key={f.id}
|
||||
onClick={() => selectFlight(f)}
|
||||
className={`px-2 py-1.5 cursor-pointer border-b border-az-border text-sm ${
|
||||
selectedFlight?.id === f.id ? 'bg-az-bg text-white' : 'text-az-text hover:bg-az-bg'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="truncate">{f.name}</span>
|
||||
<button onClick={e => { e.stopPropagation(); setDeleteId(f.id) }} className="text-az-muted hover:text-az-red text-xs">×</button>
|
||||
</div>
|
||||
<div className="text-xs text-az-muted">{new Date(f.createdDate).toLocaleDateString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<FlightListSidebar
|
||||
flights={flights}
|
||||
selectedFlight={selectedFlight}
|
||||
onSelect={selectFlight}
|
||||
onCreate={handleCreateFlight}
|
||||
onDelete={(id) => setDeleteId(id)}
|
||||
/>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div onMouseDown={leftPanel.onMouseDown} className="w-1 cursor-col-resize bg-az-border hover:bg-az-orange shrink-0" />
|
||||
|
||||
{/* Left params panel */}
|
||||
{selectedFlight && (
|
||||
<div className="w-64 bg-az-panel border-r border-az-border flex flex-col shrink-0 overflow-y-auto">
|
||||
<div className="flex border-b border-az-border">
|
||||
<button
|
||||
onClick={() => setMode('params')}
|
||||
className={`flex-1 py-1.5 text-xs ${mode === 'params' ? 'bg-az-bg text-white' : 'text-az-muted'}`}
|
||||
>
|
||||
{collapsed ? (
|
||||
<div className="w-10 bg-az-panel border-r border-az-border flex flex-col items-center py-2 gap-2 shrink-0">
|
||||
<button onClick={() => setCollapsed(false)} title="Expand"
|
||||
className="w-8 h-8 rounded border border-az-border text-az-text hover:border-az-orange hover:text-az-orange text-sm">»</button>
|
||||
<button onClick={() => setActionMode('points')} title={t('flights.planner.addPoints')}
|
||||
className={`w-8 h-8 rounded border text-sm ${actionMode === 'points' ? 'border-az-orange text-az-orange bg-az-orange/20' : 'border-az-border text-az-text hover:border-az-orange'}`}>●</button>
|
||||
<button onClick={() => setActionMode('workArea')} title={t('flights.planner.workArea')}
|
||||
className={`w-8 h-8 rounded border text-az-green text-sm ${actionMode === 'workArea' ? 'border-az-green bg-az-green/20' : 'border-az-border hover:border-az-green'}`}>▣</button>
|
||||
<button onClick={() => setActionMode('prohibitedArea')} title={t('flights.planner.prohibitedArea')}
|
||||
className={`w-8 h-8 rounded border text-az-red text-sm ${actionMode === 'prohibitedArea' ? 'border-az-red bg-az-red/20' : 'border-az-border hover:border-az-red'}`}>▣</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-80 bg-az-panel border-r border-az-border flex flex-col shrink-0">
|
||||
<div className="flex border-b border-az-border items-stretch">
|
||||
<button onClick={() => setMode('params')}
|
||||
className={`flex-1 py-1.5 text-[10px] ${mode === 'params' ? 'bg-az-bg text-white' : 'text-az-muted'}`}>
|
||||
{t('flights.params')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('gps')}
|
||||
className={`flex-1 py-1.5 text-xs ${mode === 'gps' ? 'bg-az-bg text-white' : 'text-az-muted'}`}
|
||||
>
|
||||
<button onClick={() => setMode('gps')}
|
||||
className={`flex-1 py-1.5 text-[10px] ${mode === 'gps' ? 'bg-az-bg text-white' : 'text-az-muted'}`}>
|
||||
{t('flights.gpsDenied')}
|
||||
</button>
|
||||
<button onClick={() => setCollapsed(true)} title="Collapse"
|
||||
className="px-2 text-az-muted hover:text-az-orange text-sm border-l border-az-border">«</button>
|
||||
</div>
|
||||
|
||||
{mode === 'params' && (
|
||||
<div className="p-2 space-y-2 text-xs">
|
||||
<div>
|
||||
<label className="text-az-muted block mb-0.5">{t('flights.aircraft')}</label>
|
||||
<select className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text">
|
||||
{aircrafts.map(a => <option key={a.id} value={a.id}>{a.model}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-az-muted block mb-0.5">{t('flights.height')}</label>
|
||||
<input type="number" className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text" defaultValue={100} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-az-muted">{t('flights.waypoints')}</label>
|
||||
<button onClick={handleAddWaypoint} className="text-az-orange text-xs">+ Add</button>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{waypoints.map(wp => (
|
||||
<div key={wp.id} className="flex items-center justify-between bg-az-bg rounded px-1.5 py-0.5">
|
||||
<span className="text-az-text">{wp.name}</span>
|
||||
<button onClick={() => handleDeleteWaypoint(wp.id)} className="text-az-muted hover:text-az-red">×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FlightParamsPanel
|
||||
points={points} calculatedPointInfo={calculatedPointInfo} aircrafts={aircrafts}
|
||||
initialAltitude={initialAltitude} actionMode={actionMode}
|
||||
wind={wind} locationInput={locationInput} currentPosition={currentPosition}
|
||||
totalDistance={totalDist} totalTime={totalTimeStr} batteryStatus={batteryStatus}
|
||||
onInitialAltitudeChange={setInitialAltitude} onActionModeChange={setActionMode}
|
||||
onWindChange={setWind} onLocationInputChange={setLocationInput}
|
||||
onLocationSearch={handleLocationSearch}
|
||||
onReorderPoints={setPoints} onEditPoint={openEditDialog} onRemovePoint={removePoint}
|
||||
onSave={handleSave} onUpload={handleImport} onEditJson={handleEditJson} onExport={handleExport}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'gps' && (
|
||||
@@ -172,16 +306,46 @@ export default function FlightsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map view */}
|
||||
<div className="flex-1 relative">
|
||||
<FlightMap waypoints={waypoints} />
|
||||
</div>
|
||||
<FlightMap
|
||||
points={points} calculatedPointInfo={calculatedPointInfo}
|
||||
currentPosition={currentPosition} rectangles={rectangles} setRectangles={setRectangles}
|
||||
rectangleColor={actionMode === 'workArea' ? 'green' : 'red'}
|
||||
actionMode={actionMode}
|
||||
onAddPoint={addPoint} onUpdatePoint={updatePointPosition} onRemovePoint={removePoint}
|
||||
onAltitudeChange={(i, alt) => setPoints(prev => prev.map((p, idx) => idx === i ? { ...p, altitude: alt } : p))}
|
||||
onMetaChange={(i, meta) => setPoints(prev => prev.map((p, idx) => idx === i ? { ...p, meta } : p))}
|
||||
onPolylineClick={handlePolylineClick}
|
||||
onPositionChange={setCurrentPosition}
|
||||
onMapMove={() => {}}
|
||||
/>
|
||||
|
||||
<AltitudeDialog
|
||||
open={altDialog.open}
|
||||
isEditMode={altDialog.isEdit}
|
||||
latitude={altDialog.point?.position.lat ?? 0}
|
||||
longitude={altDialog.point?.position.lng ?? 0}
|
||||
altitude={altDialog.point?.altitude ?? 300}
|
||||
meta={altDialog.point?.meta ?? []}
|
||||
onLatitudeChange={v => setAltDialog(prev => ({ ...prev, point: prev.point ? { ...prev.point, position: { ...prev.point.position, lat: v } } : null }))}
|
||||
onLongitudeChange={v => setAltDialog(prev => ({ ...prev, point: prev.point ? { ...prev.point, position: { ...prev.point.position, lng: v } } : null }))}
|
||||
onAltitudeChange={v => setAltDialog(prev => ({ ...prev, point: prev.point ? { ...prev.point, altitude: v } : null }))}
|
||||
onMetaChange={v => setAltDialog(prev => ({ ...prev, point: prev.point ? { ...prev.point, meta: v } : null }))}
|
||||
onSubmit={handleAltSubmit}
|
||||
onClose={() => setAltDialog({ open: false, point: null, isEdit: false })}
|
||||
/>
|
||||
|
||||
<JsonEditorDialog
|
||||
open={jsonDialog.open}
|
||||
jsonText={jsonDialog.text}
|
||||
onClose={() => setJsonDialog({ open: false, text: '' })}
|
||||
onSave={handleJsonSave}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteId}
|
||||
title={t('common.delete')}
|
||||
message="Delete this flight and all its data?"
|
||||
onConfirm={handleDelete}
|
||||
onConfirm={handleDeleteFlight}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user