Files
xplane-cockpit/web/src/components/Proc.jsx
T
karim 38b048ad41 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>
2026-06-02 02:17:06 +02:00

138 lines
6.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}