Files
xplane-cockpit/web/src/components/FMS.jsx
T
karim ebc33a78b7 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>
2026-06-01 15:07:03 +02:00

122 lines
4.8 KiB
React

import React, { useState, useEffect } from 'react';
import { num, navSearch } from '../api/useXplane.js';
const R_NM = 3440.065;
const rad = (d) => (d * Math.PI) / 180;
const 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 bearing(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;
}
export default function FMS({ xp }) {
const { flightPlan, fp, values, exportMsg } = xp;
const wps = flightPlan.waypoints || [];
const [entry, setEntry] = useState('');
const [hits, setHits] = useState([]);
// live ident search against X-Plane's nav database
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 add = (id) => { fp.add(id || entry.trim()); setEntry(''); setHits([]); };
let total = 0;
const rows = wps.map((w, i) => {
const prev = wps[i - 1];
const d = prev ? distNm(prev, w) : 0;
const brg = prev ? bearing(prev, w) : null;
total += d;
return { w, i, d, brg };
});
const gs = num(values.groundspeed) * 1.94384;
const ete = gs > 20 ? total / gs : null; // hours
const active = Math.max(1, Math.min(wps.length - 1, flightPlan?.activeLeg ?? 1));
return (
<div className="fms">
<div className="fms-head">
<span>FLIGHT PLAN</span>
<span className="fms-total">
{total.toFixed(0)} NM{ete ? ` · ETE ${fmtHrs(ete)}` : ''}
</span>
</div>
<div className="fms-rows">
<div className="fms-row fms-colhead">
<span>#</span><span>WPT</span><span>DTK</span><span>DIST</span><span></span>
</div>
{rows.length === 0 && <div className="fms-empty">Kein Flugplan Wegpunkt eingeben oder auf der Map tippen.</div>}
{rows.map(({ w, i, d, brg }) => (
<div className={`fms-row ${i === 0 ? 'orig' : ''} ${i === active ? 'active' : ''}`} key={i}
onClick={() => i >= 1 && fp.setActive(i)} title={i >= 1 ? 'Als aktives Bein setzen' : ''}>
<span className="idx">{i + 1}</span>
<span className="wid">{w.id}<i className="wtype">{w.type}</i></span>
<span className="dtk">{brg == null ? '—' : `${String(Math.round(brg)).padStart(3, '0')}°`}</span>
<span className="dist">{i === 0 ? 'ORIG' : `${d.toFixed(1)}`}</span>
<button className="del" onClick={(e) => { e.stopPropagation(); fp.remove(i); }}></button>
</div>
))}
</div>
<div className="fms-scratch">
{hits.length > 0 && (
<div className="fms-hits">
{hits.map((h) => (
<button key={h.id + h.lat} onClick={() => add(h.id)}>
<b>{h.id}</b> <i>{h.type}</i> <span>{h.lat.toFixed(2)}, {h.lon.toFixed(2)}</span>
</button>
))}
</div>
)}
<div className="fms-input">
<input
value={entry}
onChange={(e) => setEntry(e.target.value.toUpperCase())}
onKeyDown={(e) => e.key === 'Enter' && add()}
placeholder="IDENT (z.B. KSEA, SEA) oder LAT,LON"
autoCapitalize="characters" autoCorrect="off" spellCheck="false"
/>
<button className="fbtn add" onClick={() => add()}>ADD</button>
</div>
<div className="fms-actions">
<button className="fbtn" onClick={() => fp.clear()} disabled={!wps.length}>CLEAR</button>
<button className="fbtn export" onClick={() => fp.export('WEBFPL')} disabled={wps.length < 2}>
EXPORT X-PLANE (.fms)
</button>
</div>
{exportMsg && (
<div className={`fms-export ${exportMsg.ok ? 'ok' : 'err'}`}>
{exportMsg.ok
? (exportMsg.intoXplane
? `✓ Gespeichert in X-Plane: ${shorten(exportMsg.file)} — im Flieger-FMS unter „Load" wählen.`
: `✓ Datei geschrieben: ${shorten(exportMsg.file)} (X-Plane-Ordner nicht gefunden — XPLANE_ROOT setzen).`)
: `${exportMsg.error}`}
</div>
)}
</div>
</div>
);
}
function fmtHrs(h) {
const m = Math.round(h * 60);
return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}`;
}
function shorten(p) {
return p && p.length > 48 ? '…' + p.slice(-46) : p;
}