import React, { useState, useEffect } from 'react'; import { num, navSearch, fmsList } from '../api/useXplane.js'; // G1000 ACTIVE FLIGHT PLAN page (MFD page group + PFD window). Shows the shared // plan as WPT / DTK / DIS / CUM / ALT, active leg in magenta. Edit: type an // ident to insert/append (resolved via X-Plane navdata), ✕ deletes, tap a row to // make it the active leg; CLEAR / INVERT / EXPORT(.fms). const R_NM = 3440.065, rad = (d) => d * Math.PI / 180, deg = (r) => r * 180 / Math.PI; function distNm(a, b) { const dLat = rad(b.lat - a.lat), dLon = rad(b.lon - a.lon); const s = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2; return 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(s))); } function brng(a, b) { const y = Math.sin(rad(b.lon - a.lon)) * Math.cos(rad(b.lat)); const x = Math.cos(rad(a.lat)) * Math.sin(rad(b.lat)) - Math.sin(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.cos(rad(b.lon - a.lon)); return (deg(Math.atan2(y, x)) + 360) % 360; } const fmtHrs = (h) => { const m = Math.round(h * 60); return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}`; }; export default function FplPage({ xp, full = false, onClose }) { const { flightPlan, fp, values, exportMsg } = xp; const wps = flightPlan.waypoints || []; const active = Math.max(1, Math.min(wps.length - 1, flightPlan.activeLeg ?? 1)); const [entry, setEntry] = useState(''); const [hits, setHits] = useState([]); const [sel, setSel] = useState(-1); // selected row (insert cursor) const [plans, setPlans] = useState(null); // saved .fms list (load picker) const openLoad = async () => setPlans(await fmsList()); useEffect(() => { const q = entry.trim(); if (q.length < 2 || /[,\s]/.test(q)) { setHits([]); return; } let alive = true; navSearch(q).then((r) => alive && setHits(r.slice(0, 6))); return () => { alive = false; }; }, [entry]); const addAt = async (ident, index) => { const id = (ident || '').trim().toUpperCase(); if (!id) return; const hits2 = await navSearch(id); const hit = hits2[0]; if (!hit) return; const next = wps.slice(); next.splice(index == null ? next.length : index, 0, { id: hit.id, lat: hit.lat, lon: hit.lon, type: hit.type || 'WPT', alt: null }); fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 }); setEntry(''); setHits([]); }; const invert = () => { if (wps.length < 2) return; fp.set({ name: 'ACTIVE', waypoints: wps.slice().reverse(), activeLeg: 1 }); }; // rows with leg + cumulative distance let cum = 0; const rows = wps.map((w, i) => { const prev = wps[i - 1]; const d = prev ? distNm(prev, w) : 0; cum += d; return { w, i, d, cum, dtk: prev ? Math.round(brng(prev, w)) : null, orig: i === 0 }; }); const total = cum; const gs = num(values.groundspeed) * 1.94384; const ete = gs > 30 ? total / gs : null; // CURRENT VNV PROFILE: descent to the next waypoint with a lower target // altitude (manual S.64/107). VS TGT for a -3° path, VS REQ to make it, V DEV // from the path, time-to-top-of-descent. const alt = num(values.altitude); let vnav = null; if (gs > 40) { let c = 0, pl = num(values.lat), po = num(values.lon); for (let i = Math.max(1, active); i < wps.length; i++) { c += distNm({ lat: pl, lon: po }, wps[i]); pl = wps[i].lat; po = wps[i].lon; const t = num(wps[i].alt); if (t > 0 && t < alt - 50 && (wps[i].dsgn ?? true)) { const tan = Math.tan((3 * Math.PI) / 180); const vsTgt = -gs * tan * 101.27; const vsReq = c > 0 ? (t - alt) / (c / gs * 60) : 0; const vDev = alt - (t + c * 6076.12 * tan); const todNm = c - (alt - t) / (6076.12 * tan); vnav = { wptId: wps[i].id, tgtAlt: t, vsTgt, vsReq, vDev, fpa: 3.0, todSec: todNm > 0 ? (todNm / gs) * 3600 : 0 }; break; } } } const fmtSec = (s) => { const m = Math.floor(s / 60), ss = Math.round(s % 60); return `${m}:${String(ss).padStart(2, '0')}`; }; return (
{full ? 'ACTIVE FLIGHT PLAN' : 'FLIGHTPLAN'} {total.toFixed(0)} NM{ete ? ` · ${fmtHrs(ete)}` : ''}
{!full && wps.length > 0 && (
{wps[0].id} / {wps[wps.length - 1].id}
)}
WPTDTKDISCUMALT
{rows.length === 0 &&
— leer — Ident unten eingeben
} {rows.map(({ w, i, d, cum, dtk, orig }) => (
{ setSel(i); if (i >= 1) fp.setActive(i); }}> {w.id}{w.type} {dtk == null ? '___' : `${String(dtk).padStart(3, '0')}°`} {orig ? '—' : d.toFixed(1)} {orig ? '—' : cum.toFixed(0)} { e.stopPropagation(); if (!w.alt) return; const next = wps.map((x, j) => (j === i ? { ...x, dsgn: !(x.dsgn ?? true) } : x)); fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 }); }}>{w.alt ? `${w.alt}FT` : '_____'}
))}
{full && (
CURRENT VNV PROFILE
{vnav ? (
ACTIVE VNV WPT{vnav.tgtAlt}FT at {vnav.wptId} VS TGT{Math.round(vnav.vsTgt)}FPMFPA{vnav.fpa.toFixed(1)}° VS REQ{Math.round(vnav.vsReq)}FPMTIME TO TOD{fmtSec(vnav.todSec)} V DEV{vnav.vDev >= 0 ? '+' : ''}{Math.round(vnav.vDev)}FT
) :
— no active VNAV profile —
}
)}
{hits.length > 0 && (
{hits.map((h) => ( ))}
)}
setEntry(e.target.value.toUpperCase())} onKeyDown={(e) => e.key === 'Enter' && addAt(entry, sel >= 0 ? sel : null)} placeholder={sel >= 0 ? `Einfügen vor #${sel + 1}` : 'IDENT anhängen (z.B. ELN)'} autoCapitalize="characters" autoCorrect="off" spellCheck="false" />
{exportMsg &&
{exportMsg.ok ? 'Exportiert ✓' : exportMsg.error}
}
{plans && (
setPlans(null)}>
e.stopPropagation()}>
Gespeicherte Flugpläne
{plans.length === 0 &&
keine .fms in „Output/FMS plans"
} {plans.map((n) => ( ))}
)}
); }