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,91 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { num, navSearch } from '../api/useXplane.js';
|
||||
|
||||
// G1000 Direct-To (D→) dialog. Type or pick a waypoint ident; ACTIVATE flies a
|
||||
// direct magenta leg from the present position to it. We model that by setting
|
||||
// the shared flight plan to [PPOS → target] (the map/HSI already draw the leg)
|
||||
// and also firing the in-sim "direct" command so the real G1000 follows along.
|
||||
const R_NM = 3440.065;
|
||||
const rad = (d) => (d * Math.PI) / 180;
|
||||
function distBrg(la1, lo1, la2, lo2) {
|
||||
const dLat = rad(la2 - la1), dLon = rad(lo2 - lo1);
|
||||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(rad(la1)) * Math.cos(rad(la2)) * Math.sin(dLon / 2) ** 2;
|
||||
const dist = 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(a)));
|
||||
const y = Math.sin(rad(lo2 - lo1)) * Math.cos(rad(la2));
|
||||
const x = Math.cos(rad(la1)) * Math.sin(rad(la2)) - Math.sin(rad(la1)) * Math.cos(rad(la2)) * Math.cos(rad(lo2 - lo1));
|
||||
const brg = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
|
||||
return { dist, brg };
|
||||
}
|
||||
|
||||
export default function DirectTo({ xp, onClose }) {
|
||||
const { values, fp, command } = xp;
|
||||
const [entry, setEntry] = useState('');
|
||||
const [hits, setHits] = useState([]);
|
||||
const [sel, setSel] = useState(null); // chosen { id, lat, lon, type }
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => { inputRef.current?.focus(); }, []);
|
||||
|
||||
// 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 lat = num(values.lat), lon = num(values.lon);
|
||||
const preview = sel && isFinite(lat) ? distBrg(lat, lon, sel.lat, sel.lon) : null;
|
||||
|
||||
const activate = () => {
|
||||
if (!sel) return;
|
||||
fp.set({ name: 'ACTIVE', waypoints: [
|
||||
{ id: 'PPOS', lat, lon, type: 'USR' },
|
||||
{ id: sel.id, lat: sel.lat, lon: sel.lon, type: sel.type || 'WPT' },
|
||||
] });
|
||||
command('direct'); // mirror to the in-sim G1000
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dlg-backdrop" onClick={onClose}>
|
||||
<div className="dlg dto" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dlg-head"><span className="dto-arrow">D→</span> DIRECT TO</div>
|
||||
<div className="dto-body">
|
||||
<label className="dto-lbl">WAYPOINT</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="dto-input"
|
||||
value={entry}
|
||||
onChange={(e) => { setEntry(e.target.value.toUpperCase()); setSel(null); }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && sel) activate(); if (e.key === 'Escape') onClose(); }}
|
||||
placeholder="IDENT (z.B. KSEA, SEA, ELN)"
|
||||
autoCapitalize="characters" autoCorrect="off" spellCheck="false"
|
||||
/>
|
||||
{hits.length > 0 && (
|
||||
<div className="dto-hits">
|
||||
{hits.map((h) => (
|
||||
<button key={h.id + h.lat} className={sel && sel.id === h.id ? 'on' : ''}
|
||||
onClick={() => { setSel(h); setEntry(h.id); setHits([]); }}>
|
||||
<b>{h.id}</b><i>{h.type}</i><span>{h.lat.toFixed(2)}, {h.lon.toFixed(2)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{sel && (
|
||||
<div className="dto-sel">
|
||||
<span className="dto-id">{sel.id}</span>
|
||||
<span className="dto-type">{sel.type}</span>
|
||||
{preview && <span className="dto-vec">{String(Math.round(preview.brg)).padStart(3, '0')}° · {preview.dist.toFixed(1)} NM</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="dlg-actions">
|
||||
<button className="fbtn" onClick={onClose}>CANCEL</button>
|
||||
<button className="fbtn add" disabled={!sel} onClick={activate}>ACTIVATE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user