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
+146
View File
@@ -0,0 +1,146 @@
import type { FlightPoint, CalculatedPointInfo, AircraftParams } from './types'
export function newGuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
export function calculateDistance(
point1: FlightPoint,
point2: FlightPoint,
aircraftType: string,
initialAltitude: number,
downang: number,
upang: number,
): number {
if (!point1?.position || !point2?.position) return 0
const R = 6371
const { lat: lat1, lng: lon1 } = point1.position
const { lat: lat2, lng: lon2 } = point2.position
const alt1 = point1.altitude || 0
const alt2 = point2.altitude || 0
const toRad = (value: number) => (value * Math.PI) / 180
const dLat = toRad(lat2 - lat1)
const dLon = toRad(lon2 - lon1)
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
const horizontalDistance = R * c
const initialAltitudeKm = initialAltitude / 1000
const altitude1Km = alt1 / 1000
const altitude2Km = alt2 / 1000
const descentAngleRad = toRad(downang || 0.01)
const ascentAngleRad = toRad(upang || 0.01)
if (aircraftType === 'Plane') {
const ascentDist = Math.max(0, (initialAltitudeKm - altitude1Km) / Math.sin(ascentAngleRad))
const descentDist = Math.max(0, (initialAltitudeKm - altitude2Km) / Math.sin(descentAngleRad))
const hAscent = Math.max(0, ascentDist * Math.cos(ascentAngleRad))
const hDescent = Math.max(0, descentDist * Math.cos(descentAngleRad))
return horizontalDistance - (hDescent + hAscent) + Math.max(0, descentDist) + Math.max(0, ascentDist)
}
const ascentVertical = Math.abs(initialAltitudeKm - altitude1Km)
const descentVertical = Math.abs(initialAltitudeKm - altitude2Km)
return ascentVertical + horizontalDistance + descentVertical
}
export async function getWeatherData(lat: number, lon: number) {
const apiKey = '335799082893fad97fa36118b131f919'
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`
try {
const res = await fetch(url)
const data = await res.json()
return { windSpeed: data.wind.speed as number, windAngle: data.wind.deg as number }
} catch {
return null
}
}
export async function calculateBatteryPercentUsed(
groundSpeed: number,
time: number,
position: { lat: number; lon: number },
aircraft: AircraftParams,
): Promise<number> {
const weatherData = await getWeatherData(position.lat, position.lon)
const airDensity = 1.05
const groundSpeedMs = groundSpeed / 3.6
const headwind = (weatherData?.windSpeed ?? 0) * Math.cos((Math.PI / 180) * (weatherData?.windAngle ?? 0))
const effectiveAirspeed = groundSpeedMs + headwind
const drag = 0.5 * airDensity * (effectiveAirspeed ** 2) * aircraft.dragCoefficient * aircraft.frontalArea
const adjustedDrag = drag + aircraft.weight * 9.8 * 0.05
let watts = aircraft.thrustWatts[aircraft.thrustWatts.length - 1].watts
for (const item of aircraft.thrustWatts) {
const thrustN = (item.thrust / 1000) * 9.8
if (thrustN > adjustedDrag) { watts = item.watts; break }
}
const power = watts / aircraft.propellerEfficiency
const energyUsed = power * time
return Math.min((energyUsed / aircraft.batteryCapacity) * 100, 100)
}
export async function calculateAllPoints(
points: FlightPoint[],
aircraft: AircraftParams,
initialAltitude: number,
): Promise<CalculatedPointInfo[]> {
const infos: CalculatedPointInfo[] = [{ bat: 100, time: 0 }]
for (let i = 1; i < points.length; i++) {
const p1 = points[i - 1], p2 = points[i]
const dist = calculateDistance(p1, p2, aircraft.type, initialAltitude, aircraft.downang, aircraft.upang)
const time = dist / aircraft.speed
const midPos = { lat: (p1.position.lat + p2.position.lat) / 2, lon: (p1.position.lng + p2.position.lng) / 2 }
const pct = await calculateBatteryPercentUsed(aircraft.speed, time, midPos, aircraft)
infos.push({ bat: infos[i - 1].bat - pct, time: infos[i - 1].time + time })
}
return infos
}
export function parseCoordinates(input: string): { lat: number; lng: number } | null {
const cleaned = input.trim().replace(/[°NSEW]/gi, '')
const parts = cleaned.split(/[,\s]+/).filter(Boolean)
if (parts.length >= 2) {
const lat = parseFloat(parts[0])
const lng = parseFloat(parts[1])
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
return { lat, lng }
}
}
return null
}
export function getMockAircraftParams(): AircraftParams {
return {
type: 'Plane',
downang: 40,
upang: 45,
weight: 3.4,
speed: 80,
frontalArea: 0.12,
dragCoefficient: 0.45,
batteryCapacity: 315,
thrustWatts: [
{ thrust: 500, watts: 55.5 },
{ thrust: 750, watts: 91.02 },
{ thrust: 1000, watts: 137.64 },
{ thrust: 1250, watts: 191 },
{ thrust: 1500, watts: 246 },
{ thrust: 1750, watts: 308 },
{ thrust: 2000, watts: 381 },
],
propellerEfficiency: 0.95,
}
}