38b048ad41
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>
138 lines
6.3 KiB
React
138 lines
6.3 KiB
React
import React, { useEffect, useState } from 'react';
|
||
import { num } from '../api/useXplane.js';
|
||
|
||
// G1000 PROC dialog. Pick a destination/airport, a category (Departure / Arrival
|
||
// / Approach), then a procedure + transition; LOAD inserts the procedure's leg
|
||
// fixes into the active flight plan. Procedures come from X-Plane's own CIFP data
|
||
// via /api/nav/procs and /api/nav/proc (resolved to coordinates server-side).
|
||
const CATS = [
|
||
{ id: 'approach', label: 'APPROACH', key: 'approaches', t: 'approach' },
|
||
{ id: 'arrival', label: 'ARRIVAL', key: 'stars', t: 'star' },
|
||
{ id: 'departure', label: 'DEPARTURE', key: 'sids', t: 'sid' },
|
||
];
|
||
|
||
export default function Proc({ xp, onClose }) {
|
||
const { flightPlan, fp, values } = xp;
|
||
// Default airport: the plan's destination if it's an airport, else blank.
|
||
const wps = flightPlan?.waypoints || [];
|
||
const destGuess = [...wps].reverse().find((w) => w.type === 'APT')?.id || '';
|
||
const [icao, setIcao] = useState(destGuess);
|
||
const [query, setQuery] = useState(destGuess);
|
||
const [procs, setProcs] = useState(null);
|
||
const [err, setErr] = useState('');
|
||
const [cat, setCat] = useState('approach');
|
||
const [view, setView] = useState('menu'); // 'menu' (PDF action list) | 'pick'
|
||
const [selProc, setSelProc] = useState(null); // { name, transitions }
|
||
const [selTrans, setSelTrans] = useState('');
|
||
const [legs, setLegs] = useState([]);
|
||
|
||
// Fetch the procedure summary whenever the airport changes.
|
||
useEffect(() => {
|
||
const id = icao.trim().toUpperCase();
|
||
if (id.length < 3) { setProcs(null); return; }
|
||
let alive = true;
|
||
setErr(''); setProcs(null); setSelProc(null); setSelTrans(''); setLegs([]);
|
||
fetch(`/api/nav/procs?icao=${id}`).then((r) => r.ok ? r.json() : Promise.reject(r.status))
|
||
.then((d) => { if (alive) setProcs(d); })
|
||
.catch(() => { if (alive) setErr(`keine Prozeduren für ${id}`); });
|
||
return () => { alive = false; };
|
||
}, [icao]);
|
||
|
||
// Preview the resolved legs when a procedure+transition is chosen.
|
||
useEffect(() => {
|
||
if (!procs || !selProc) { setLegs([]); return; }
|
||
const c = CATS.find((c) => c.id === cat);
|
||
let alive = true;
|
||
const t = encodeURIComponent(selTrans || '');
|
||
fetch(`/api/nav/proc?icao=${procs.icao}&type=${c.t}&name=${encodeURIComponent(selProc.name)}&trans=${t}`)
|
||
.then((r) => r.ok ? r.json() : []).then((d) => { if (alive) setLegs(d); });
|
||
return () => { alive = false; };
|
||
}, [procs, cat, selProc, selTrans]);
|
||
|
||
const catList = procs ? (procs[CATS.find((c) => c.id === cat).key] || []) : [];
|
||
|
||
const load = () => {
|
||
if (!legs.length) return;
|
||
const existing = wps.slice();
|
||
// Departures go to the front, arrivals/approaches to the end.
|
||
const merged = cat === 'departure' ? [...legs, ...existing] : [...existing, ...legs];
|
||
fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: cat === 'departure' ? 1 : existing.length || 1 });
|
||
onClose();
|
||
};
|
||
|
||
const catLabel = CATS.find((c) => c.id === cat).label;
|
||
|
||
// The PDF's action menu. SELECT … opens our picker for that category;
|
||
// ACTIVATE … are shown for authenticity (armed-procedure actions).
|
||
if (view === 'menu') {
|
||
const item = (label, onClick, sel) => (
|
||
<button className={`proc-menu-i ${sel ? 'sel' : ''}`} onClick={onClick}>{label}</button>
|
||
);
|
||
const sel = (c) => { setCat(c); setSelProc(null); setSelTrans(''); setView('pick'); };
|
||
return (
|
||
<div className="gwin-backdrop" onClick={onClose}>
|
||
<div className="dlg proc menu" onClick={(e) => e.stopPropagation()}>
|
||
<div className="dlg-head">PROCEDURES</div>
|
||
<div className="proc-menu">
|
||
{item('ACTIVATE VECTOR-TO-FINAL', () => {})}
|
||
{item('ACTIVATE APPROACH', () => {})}
|
||
{item('ACTIVATE MISSED APPROACH', () => {})}
|
||
{item('SELECT APPROACH', () => sel('approach'), true)}
|
||
{item('SELECT ARRIVAL', () => sel('arrival'))}
|
||
{item('SELECT DEPARTURE', () => sel('departure'))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="gwin-backdrop" onClick={onClose}>
|
||
<div className="dlg proc" onClick={(e) => e.stopPropagation()}>
|
||
<div className="dlg-head">{catLabel}</div>
|
||
<div className="proc-body">
|
||
<div className="proc-apt">
|
||
<button className="proc-back" onClick={() => setView('menu')}>‹</button>
|
||
<label>APT</label>
|
||
<input value={query} onChange={(e) => setQuery(e.target.value.toUpperCase())}
|
||
onKeyDown={(e) => e.key === 'Enter' && setIcao(query)}
|
||
placeholder="ICAO (z.B. KSEA)" autoCapitalize="characters" autoCorrect="off" spellCheck="false" />
|
||
<button className="fbtn" onClick={() => setIcao(query)}>LOAD</button>
|
||
</div>
|
||
{err && <div className="proc-err">{err}</div>}
|
||
|
||
<div className="proc-cols">
|
||
<div className="proc-list">
|
||
<div className="proc-coltitle">{procs ? `${catList.length}` : '—'} PROC</div>
|
||
{catList.map((p) => (
|
||
<button key={p.name} className={selProc?.name === p.name ? 'on' : ''}
|
||
onClick={() => { setSelProc(p); setSelTrans(p.transitions[0] || ''); }}>{p.name}</button>
|
||
))}
|
||
{procs && catList.length === 0 && <div className="proc-empty">keine</div>}
|
||
</div>
|
||
<div className="proc-list">
|
||
<div className="proc-coltitle">TRANS</div>
|
||
{selProc?.transitions.map((t) => (
|
||
<button key={t} className={selTrans === t ? 'on' : ''} onClick={() => setSelTrans(t)}>{t}</button>
|
||
))}
|
||
{selProc && selProc.transitions.length === 0 && <div className="proc-empty">—</div>}
|
||
</div>
|
||
<div className="proc-preview">
|
||
<div className="proc-coltitle">{legs.length} FIXES</div>
|
||
{legs.map((l, i) => (
|
||
<div key={l.id + i} className="proc-leg">
|
||
<b>{l.id}</b>{l.alt ? <u>{l.alt}ft</u> : null}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="dlg-actions">
|
||
<button className="fbtn" onClick={onClose}>CANCEL</button>
|
||
<button className="fbtn add" disabled={!legs.length} onClick={load}>LOAD → FPL</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|