G1000: two-way sim sync, more PFD/MFD fidelity, authentic dialogs
Sync (FlyWithLua companions in plugins/ + server/fmssync.js): - FMS flight-plan two-way sync (App <-> in-sim FMS) via fms-sync.lua - G1000 UI-state publish (page/range/inset) via ui-sync.lua + CDI source, baro, map-range follow - Terrain awareness: elevation grid probe (terrain-probe.lua) -> red/yellow MFD overlay vs aircraft altitude PFD: - AFCS mode annunciation bar from autopilot _status datarefs - CDI source GPS/VLOC colouring, BRG1/BRG2 pointers + DME windows, marker beacons - magenta speed/altitude trend vectors, selected-altitude alerting - time-based (frame-rate-independent) smoothing for attitude/heading/tapes MFD: - nav data bar (DTK/ETE/active leg), airways overlay from earth_awy.dat, compass rose anchored to the ownship Dialogs (NEAREST/FLIGHTPLAN/DIRECT-TO/PROCEDURES): - flat, square, embedded G1000 look (no shadow/rounded/transparency) - compact lower-right placement, no close X (softkey toggles), single window - NEAREST 2-line entries (ILS/VFR, COM freq, runway length), PROC action menu Service worker: network-first HTML so reloads pick up new builds (cache v2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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;
|
||||
|
||||
return (
|
||||
<div className={`fpl ${full ? 'full' : 'win'}`}>
|
||||
<div className="fpl-head">
|
||||
<span>{full ? 'ACTIVE FLIGHT PLAN' : 'FLIGHTPLAN'}</span>
|
||||
<span className="fpl-tot">{total.toFixed(0)} NM{ete ? ` · ${fmtHrs(ete)}` : ''}</span>
|
||||
</div>
|
||||
{!full && wps.length > 0 && (
|
||||
<div className="fpl-od">{wps[0].id} / {wps[wps.length - 1].id}</div>
|
||||
)}
|
||||
<div className="fpl-cols"><span>WPT</span><span>DTK</span><span>DIS</span><span>CUM</span><span>ALT</span></div>
|
||||
<div className="fpl-rows">
|
||||
{rows.length === 0 && <div className="fpl-empty">— leer — Ident unten eingeben</div>}
|
||||
{rows.map(({ w, i, d, cum, dtk, orig }) => (
|
||||
<div key={i} className={`fpl-row ${i === active ? 'act' : ''} ${i === sel ? 'sel' : ''}`}
|
||||
onClick={() => { setSel(i); if (i >= 1) fp.setActive(i); }}>
|
||||
<span className="r-wpt"><b className={i === active ? 'cur' : ''}>{w.id}</b><i>{w.type}</i></span>
|
||||
<span className="r-dtk">{dtk == null ? '___' : `${String(dtk).padStart(3, '0')}°`}</span>
|
||||
<span className="r-dis">{orig ? '—' : d.toFixed(1)}</span>
|
||||
<span className="r-cum">{orig ? '—' : cum.toFixed(0)}</span>
|
||||
<span className="r-alt">{w.alt ? `${w.alt}` : '_____'}</span>
|
||||
<button className="r-del" onClick={(e) => { e.stopPropagation(); fp.remove(i); }}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="fpl-entry">
|
||||
{hits.length > 0 && (
|
||||
<div className="fpl-hits">
|
||||
{hits.map((h) => (
|
||||
<button key={h.id + h.lat} onClick={() => addAt(h.id, sel >= 0 ? sel : null)}>
|
||||
<b>{h.id}</b><i>{h.type}</i><span>{h.lat.toFixed(2)}, {h.lon.toFixed(2)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="fpl-inrow">
|
||||
<input value={entry} onChange={(e) => 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" />
|
||||
<button className="fpl-btn add" onClick={() => addAt(entry, sel >= 0 ? sel : null)}>EINFÜGEN</button>
|
||||
</div>
|
||||
<div className="fpl-actions">
|
||||
<button className="fpl-btn" onClick={openLoad}>LADEN</button>
|
||||
<button className="fpl-btn" onClick={() => { setSel(-1); fp.clear(); }} disabled={!wps.length}>CLEAR</button>
|
||||
<button className="fpl-btn" onClick={invert} disabled={wps.length < 2}>INVERT</button>
|
||||
<button className="fpl-btn" onClick={() => fp.export('WEBFPL')} disabled={wps.length < 2}>EXPORT →.fms</button>
|
||||
</div>
|
||||
{exportMsg && <div className={`fpl-msg ${exportMsg.ok ? 'ok' : 'err'}`}>{exportMsg.ok ? 'Exportiert ✓' : exportMsg.error}</div>}
|
||||
</div>
|
||||
|
||||
{plans && (
|
||||
<div className="fpl-load" onClick={() => setPlans(null)}>
|
||||
<div className="fpl-load-box" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="fpl-load-head"><span>Gespeicherte Flugpläne</span><button onClick={() => setPlans(null)}>✕</button></div>
|
||||
<div className="fpl-load-list">
|
||||
{plans.length === 0 && <div className="fpl-empty">keine .fms in „Output/FMS plans"</div>}
|
||||
{plans.map((n) => (
|
||||
<button key={n} onClick={() => { fp.load(n); setPlans(null); }}>{n}<i>.fms</i></button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user