mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:11:10 +00:00
Migrate src/features/flights to the v2 tactical-ops design — the last page still on the legacy az-* palette — keeping all existing planner behavior (Leaflet map, draw modes, import/export, altitude dialog). - Restyle every flights surface to v2 tokens and shared classes: flight roster sidebar (search, rows, telemetry card), params panel, waypoint list, altitude/JSON dialogs, map-point popup, altitude chart, wind inputs, mini-map. - Rebuild the params panel to the mockup order (draw-mode selector, Mission Config, Waypoints) with existing controls appended. - Add HUD overlays on the real Leaflet map (telemetry, legend, compass, zoom/recenter toolbar, bottom status strip); disable the default zoom control, add a dark tactical-grid backdrop, and use the legend glyphs (diamond/square/octagon) plus a pulsing amber current-position beacon. - Add a functional GPS-Denied panel: orthophoto upload (local), live-GPS readout fed by the existing SSE stream, and a GPS-correction form that patches waypoint coordinates. - Extract a shared drawModes config used by the panel and collapse rail. - Add flights.v2 i18n keys to en.json and ua.json (parity preserved).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polyline, Rectangle, useMap, useMapEvents } from 'react-leaflet'
|
||||
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'
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import DrawControl from './DrawControl'
|
||||
import MapPoint from './MapPoint'
|
||||
import MiniMap from './MiniMap'
|
||||
import { defaultIcon } from './mapIcons'
|
||||
import { currentPositionIcon } from './mapIcons'
|
||||
import { getTileUrl } from './types'
|
||||
import type { FlightPoint, CalculatedPointInfo, MapRectangle, ActionMode, MovingPointInfo } from './types'
|
||||
|
||||
@@ -35,9 +35,9 @@ function MapEvents({ points, handlePolylineClick, containerRef, onMapMove }: Map
|
||||
|
||||
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)
|
||||
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: '#228be6' } }) }],
|
||||
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)
|
||||
}
|
||||
@@ -61,6 +61,12 @@ function SetView({ center }: { center: L.LatLngExpression }) {
|
||||
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[]
|
||||
@@ -77,21 +83,29 @@ interface Props {
|
||||
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<HTMLDivElement>(null)
|
||||
const [movingPoint, setMovingPoint] = useState<MovingPointInfo | null>(null)
|
||||
const [draggablePoints, setDraggablePoints] = useState(points)
|
||||
const polylineClickRef = useRef(false)
|
||||
const [mapInstance, setMapInstance] = useState<L.Map | null>(null)
|
||||
|
||||
useEffect(() => { setDraggablePoints(points) }, [points])
|
||||
|
||||
const handleMapReady = useCallback((m: L.Map) => { setMapInstance(m) }, [])
|
||||
|
||||
function ClickHandler() {
|
||||
useMapEvents({
|
||||
click(e) {
|
||||
@@ -117,9 +131,23 @@ export default function FlightMap({
|
||||
setDraggablePoints(updated)
|
||||
}
|
||||
|
||||
const displayLat = liveGps?.lat ?? currentPosition.lat
|
||||
const displayLon = liveGps?.lon ?? currentPosition.lng
|
||||
const satelliteCount = liveGps?.satellites ?? 12
|
||||
|
||||
return (
|
||||
<div className="flex-1 relative" ref={containerRef}>
|
||||
<MapContainer center={currentPosition} zoom={15} className="h-full w-full">
|
||||
<MapContainer center={currentPosition} zoom={15} className="h-full w-full"
|
||||
zoomControl={false} attributionControl={false}
|
||||
style={{
|
||||
backgroundColor: '#0F1318',
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),' +
|
||||
'linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px),' +
|
||||
'radial-gradient(ellipse at 30% 40%, rgba(54,214,197,0.04), transparent 60%),' +
|
||||
'radial-gradient(ellipse at 80% 70%, rgba(255,157,61,0.03), transparent 65%)',
|
||||
backgroundSize: '60px 60px, 60px 60px, 100% 100%, 100% 100%',
|
||||
}}>
|
||||
<ClickHandler />
|
||||
<TileLayer
|
||||
url={getTileUrl()}
|
||||
@@ -128,6 +156,7 @@ export default function FlightMap({
|
||||
/>
|
||||
<MapEvents points={draggablePoints} handlePolylineClick={handlePolylineClick} containerRef={containerRef} onMapMove={onMapMove} />
|
||||
<SetView center={currentPosition} />
|
||||
<MapRefCapture onReady={handleMapReady} />
|
||||
|
||||
{movingPoint && <MiniMap pointPosition={movingPoint} />}
|
||||
|
||||
@@ -144,16 +173,8 @@ export default function FlightMap({
|
||||
/>
|
||||
))}
|
||||
|
||||
{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
|
||||
<Marker position={currentPosition} icon={currentPositionIcon} draggable
|
||||
eventHandlers={{ dragend: (e) => onPositionChange((e.target as L.Marker).getLatLng()) }}>
|
||||
<Popup>{t('flights.planner.currentLocation')}</Popup>
|
||||
</Marker>
|
||||
@@ -166,11 +187,227 @@ export default function FlightMap({
|
||||
<DrawControl color={rectangleColor} actionMode={actionMode} rectangles={rectangles} setRectangles={setRectangles} />
|
||||
</MapContainer>
|
||||
|
||||
{/* v2 drawing-hint HUD — restyled to v2 tokens */}
|
||||
{(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
|
||||
className="top-2 left-1/2 -translate-x-1/2 bracket panel micro pointer-events-none"
|
||||
style={{ position: 'absolute', zIndex: 500, padding: '4px 12px', color: 'var(--accent-amber)', background: 'rgba(19,23,28,0.92)' }}
|
||||
>
|
||||
{t(actionMode === 'workArea' ? 'flights.v2.drawHintWork' : 'flights.v2.drawHintNoGo')}
|
||||
<span className="br" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ======================================================= */}
|
||||
{/* Compass rosette — top-left */}
|
||||
{/* ======================================================= */}
|
||||
<div
|
||||
className="bracket panel flex items-center justify-center pointer-events-none"
|
||||
style={{ position: 'absolute', top: 48, left: 16, width: 80, height: 80, background: 'rgba(19,23,28,0.6)', backdropFilter: 'blur(2px)', zIndex: 500 }}
|
||||
>
|
||||
<svg width="60" height="60" viewBox="-30 -30 60 60" style={{ color: 'var(--accent-amber)' }}>
|
||||
<circle r="24" fill="none" stroke="currentColor" strokeOpacity="0.3" strokeWidth="0.7" />
|
||||
<circle r="20" fill="none" stroke="currentColor" strokeOpacity="0.2" strokeWidth="0.5" />
|
||||
<line x1="0" y1="-26" x2="0" y2="-20" stroke="currentColor" strokeWidth="1.5" />
|
||||
<line x1="0" y1="20" x2="0" y2="26" stroke="currentColor" strokeOpacity="0.4" strokeWidth="0.8" />
|
||||
<line x1="-26" y1="0" x2="-20" y2="0" stroke="currentColor" strokeOpacity="0.4" strokeWidth="0.8" />
|
||||
<line x1="20" y1="0" x2="26" y2="0" stroke="currentColor" strokeOpacity="0.4" strokeWidth="0.8" />
|
||||
<text x="0" y="-12" textAnchor="middle" fontFamily="JetBrains Mono" fontSize="7" fill="currentColor" fontWeight="700">N</text>
|
||||
<polygon points="0,-16 -3,-8 0,-10 3,-8" fill="currentColor" />
|
||||
</svg>
|
||||
<span className="br" />
|
||||
</div>
|
||||
|
||||
{/* ======================================================= */}
|
||||
{/* Telemetry HUD — top-right */}
|
||||
{/* ======================================================= */}
|
||||
<div
|
||||
className="bracket panel"
|
||||
style={{ position: 'absolute', top: 16, right: 16, width: 240, background: 'rgba(19,23,28,0.92)', backdropFilter: 'blur(4px)', padding: 12, zIndex: 500 }}
|
||||
>
|
||||
<header
|
||||
className="flex items-center justify-between"
|
||||
style={{ marginBottom: 10, paddingBottom: 8, borderBottom: '1px solid var(--border-hair)' }}
|
||||
>
|
||||
<span
|
||||
className="flex items-center gap-2 mono"
|
||||
style={{ fontSize: 10, color: 'var(--accent-cyan)', letterSpacing: '0.14em' }}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full live"
|
||||
style={{ background: 'var(--accent-cyan)' }}
|
||||
/>
|
||||
{t('flights.v2.hud.liveConnected')}
|
||||
</span>
|
||||
<span className="micro" style={{ color: 'var(--text-muted)' }}>{flightLabel}</span>
|
||||
</header>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="micro">{t('flights.v2.hud.sat')}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--accent-green)' }}>{satelliteCount} / 14</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="micro">{t('flights.v2.hud.lat')}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-primary)' }}>{displayLat.toFixed(5)}° N</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="micro">{t('flights.v2.hud.lon')}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-primary)' }}>{displayLon.toFixed(5)}° E</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="micro">{t('flights.v2.hud.alt')}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-primary)' }}>320 M / AGL</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="micro">{t('flights.v2.hud.hdg')}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--accent-amber)' }}>047° NE</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="micro">{t('flights.v2.hud.spd')}</span>
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-primary)' }}>11.4 M/S</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-between"
|
||||
style={{ paddingTop: 6, marginTop: 6, borderTop: '1px solid var(--border-hair)' }}
|
||||
>
|
||||
<span className="micro">{t('flights.v2.hud.link')}</span>
|
||||
<span className="mono" style={{ fontSize: 11, color: 'var(--accent-green)' }}>RSSI -52 DBM</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="br" />
|
||||
</div>
|
||||
|
||||
{/* ======================================================= */}
|
||||
{/* Legend — bottom-left */}
|
||||
{/* ======================================================= */}
|
||||
<div
|
||||
className="bracket panel pointer-events-none"
|
||||
style={{ position: 'absolute', bottom: 48, left: 16, width: 200, background: 'rgba(19,23,28,0.92)', padding: 12, zIndex: 500 }}
|
||||
>
|
||||
<header style={{ marginBottom: 8, paddingBottom: 6, borderBottom: '1px solid var(--border-hair)' }}>
|
||||
<span className="sect-head">// {t('flights.v2.hud.mapLegend')}</span>
|
||||
</header>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 11 }}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<svg width="22" height="6">
|
||||
<line x1="0" y1="3" x2="22" y2="3" stroke="#FF4756" strokeWidth="1.5" strokeDasharray="3 3" />
|
||||
</svg>
|
||||
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>
|
||||
{t('flights.v2.hud.plannedOriginal')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<svg width="22" height="6">
|
||||
<line x1="0" y1="3" x2="22" y2="3" stroke="#36D6C5" strokeWidth="2" />
|
||||
</svg>
|
||||
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>
|
||||
{t('flights.v2.hud.correctedLive')}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2.5"
|
||||
style={{ paddingTop: 6, borderTop: '1px solid var(--border-hair)' }}
|
||||
>
|
||||
<div style={{ width: 10, height: 10, background: 'var(--accent-green)', transform: 'rotate(45deg)', flexShrink: 0 }} />
|
||||
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>
|
||||
{t('flights.v2.hud.originStart')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div style={{ width: 10, height: 10, background: 'transparent', border: '1.5px solid var(--accent-cyan)', flexShrink: 0 }} />
|
||||
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>
|
||||
{t('flights.v2.hud.waypoint')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div style={{ width: 11, height: 11, background: 'var(--accent-red)', clipPath: 'polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%)', flexShrink: 0 }} />
|
||||
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>
|
||||
{t('flights.v2.hud.targetFinish')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="br" />
|
||||
</div>
|
||||
|
||||
{/* ======================================================= */}
|
||||
{/* Map toolbar — right edge */}
|
||||
{/* ======================================================= */}
|
||||
<div
|
||||
className="absolute flex flex-col gap-1.5 pointer-events-auto"
|
||||
style={{ top: '50%', right: 16, transform: 'translateY(-50%)', zIndex: 500 }}
|
||||
>
|
||||
<button
|
||||
className="flex items-center justify-center border border-border-hair panel mono"
|
||||
style={{ width: 32, height: 32, color: 'var(--text-primary)', fontSize: 16, background: 'var(--surface-1)' }}
|
||||
title={t('flights.v2.hud.zoomIn')}
|
||||
onClick={() => mapInstance?.zoomIn()}
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center justify-center border border-border-hair panel mono"
|
||||
style={{ width: 32, height: 32, color: 'var(--text-primary)', fontSize: 16, background: 'var(--surface-1)' }}
|
||||
title={t('flights.v2.hud.zoomOut')}
|
||||
onClick={() => mapInstance?.zoomOut()}
|
||||
type="button"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<div style={{ width: 32, height: 1, background: 'var(--border-hair)' }} />
|
||||
<button
|
||||
className="flex items-center justify-center border border-border-hair panel"
|
||||
style={{ width: 32, height: 32, color: 'var(--accent-amber)', background: 'var(--surface-1)' }}
|
||||
title={t('flights.v2.hud.recenter')}
|
||||
onClick={() => mapInstance?.setView([currentPosition.lat, currentPosition.lng])}
|
||||
type="button"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
<line x1="12" y1="2" x2="12" y2="4" />
|
||||
<line x1="12" y1="20" x2="12" y2="22" />
|
||||
<line x1="2" y1="12" x2="4" y2="12" />
|
||||
<line x1="20" y1="12" x2="22" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center justify-center border border-border-hair panel"
|
||||
style={{ width: 32, height: 32, color: 'var(--text-secondary)', background: 'var(--surface-1)' }}
|
||||
title={t('flights.v2.hud.layers')}
|
||||
type="button"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2" />
|
||||
<polyline points="2 17 12 22 22 17" />
|
||||
<polyline points="2 12 12 17 22 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ======================================================= */}
|
||||
{/* Bottom status strip */}
|
||||
{/* ======================================================= */}
|
||||
<div
|
||||
className="absolute left-0 right-0 flex items-center gap-4 border-t border-border-hair pointer-events-none"
|
||||
style={{ bottom: 0, height: 28, padding: '0 12px', background: 'var(--surface-1)', zIndex: 500 }}
|
||||
>
|
||||
<span className="pill pill-green">
|
||||
<span className="dot live" />
|
||||
{t('flights.v2.strip.telemetryLive')}
|
||||
</span>
|
||||
<span className="micro" style={{ color: 'var(--text-muted)' }}>SSE</span>
|
||||
<span className="mono micro" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('flights.v2.strip.frame')} 12,847 / 18,400
|
||||
</span>
|
||||
<span className="micro" style={{ color: 'var(--text-muted)' }}>·</span>
|
||||
<span className="mono micro" style={{ color: 'var(--text-secondary)' }}>
|
||||
{displayLat.toFixed(5)} N · {displayLon.toFixed(5)} E
|
||||
</span>
|
||||
<span className="ml-auto micro" style={{ color: 'var(--text-muted)' }}>
|
||||
{t('flights.v2.strip.lastPing')} +0.42S
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user