Initial commit: X-Plane G1000 web cockpit + bridge + Tauri desktop app
- server/: Node bridge (datarefs/commands, navdata, CIFP procedures, flight plan) - web/: React cockpit (PFD/MFD/Map, VFR six-pack, AFCS, FMS CDU), PWA, collapsible sidebar - desktop/: Tauri 2 launcher (Bun sidecar, system tray, updater) + Linux build via Docker - scripts/: prep-desktop, build-linux, Gitea release + latest.json Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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 [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();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dlg-backdrop" onClick={onClose}>
|
||||
<div className="dlg proc" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dlg-head">PROCEDURES</div>
|
||||
<div className="proc-body">
|
||||
<div className="proc-apt">
|
||||
<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-tabs">
|
||||
{CATS.map((c) => (
|
||||
<button key={c.id} className={cat === c.id ? 'on' : ''}
|
||||
onClick={() => { setCat(c.id); setSelProc(null); setSelTrans(''); }}>{c.label}</button>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user