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,100 @@
|
||||
// Shared flight plan: one plan, synced to every connected tablet. Can resolve
|
||||
// idents via navdata, and export an X-Plane .fms file the sim can load.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { lookup, xplaneRoot } from './navdata.js';
|
||||
|
||||
// waypoint: { id, lat, lon, type, alt? }
|
||||
// activeLeg = index of the waypoint the active (magenta) leg flies TO. The leg
|
||||
// runs from waypoints[activeLeg-1] to waypoints[activeLeg]. Defaults to 1.
|
||||
let plan = { name: 'ACTIVE', waypoints: [], activeLeg: 1 };
|
||||
|
||||
const clampLeg = (i) => Math.max(1, Math.min(plan.waypoints.length - 1, i | 0));
|
||||
|
||||
export const getPlan = () => plan;
|
||||
|
||||
export function setPlan(next) {
|
||||
const wps = Array.isArray(next?.waypoints)
|
||||
? next.waypoints
|
||||
.filter((w) => isFinite(w.lat) && isFinite(w.lon))
|
||||
.map((w) => ({ id: String(w.id || 'WPT'), lat: +w.lat, lon: +w.lon, type: w.type || 'WPT', alt: w.alt ?? null }))
|
||||
: [];
|
||||
const wantLeg = Number.isFinite(next?.activeLeg) ? next.activeLeg : 1;
|
||||
plan = { name: next?.name || 'ACTIVE', waypoints: wps, activeLeg: Math.max(1, Math.min(wps.length - 1, wantLeg)) || 1 };
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function setActiveLeg(index) {
|
||||
if (plan.waypoints.length >= 2) plan.activeLeg = clampLeg(index);
|
||||
return plan;
|
||||
}
|
||||
|
||||
// Add a waypoint by ident (resolved against navdata) or raw "lat,lon".
|
||||
export function addWaypoint(input) {
|
||||
const raw = String(input || '').trim();
|
||||
const m = raw.match(/^(-?\d+(?:\.\d+)?)[ ,]+(-?\d+(?:\.\d+)?)$/);
|
||||
if (m) {
|
||||
plan.waypoints.push({ id: 'USR', lat: +m[1], lon: +m[2], type: 'USR', alt: null });
|
||||
return { ok: true, plan };
|
||||
}
|
||||
const hit = lookup(raw);
|
||||
if (!hit) return { ok: false, error: `unknown ident: ${raw}` };
|
||||
plan.waypoints.push({ ...hit, alt: null });
|
||||
return { ok: true, plan };
|
||||
}
|
||||
|
||||
export function removeWaypoint(index) {
|
||||
if (index >= 0 && index < plan.waypoints.length) plan.waypoints.splice(index, 1);
|
||||
if (plan.waypoints.length >= 2) plan.activeLeg = clampLeg(plan.activeLeg);
|
||||
return plan;
|
||||
}
|
||||
|
||||
// ---- great-circle helpers (nm + degrees) ----
|
||||
const R_NM = 3440.065;
|
||||
const rad = (d) => (d * Math.PI) / 180;
|
||||
const deg = (r) => (r * 180) / Math.PI;
|
||||
|
||||
export function legDistanceNm(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)));
|
||||
}
|
||||
|
||||
export function legBearing(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;
|
||||
}
|
||||
|
||||
// ---- X-Plane .fms (v1100) export ----
|
||||
function fmsType(t) {
|
||||
return { APT: 1, NDB: 2, VOR: 3, WPT: 11, USR: 28 }[t] || 11;
|
||||
}
|
||||
|
||||
export function exportFms(name = 'WEBFPL') {
|
||||
const wp = plan.waypoints;
|
||||
if (wp.length < 2) return { ok: false, error: 'need at least 2 waypoints' };
|
||||
|
||||
const lines = ['I', '1100 Version', 'CYCLE 2501'];
|
||||
lines.push(`ADEP ${wp[0].id}`);
|
||||
lines.push(`ADES ${wp[wp.length - 1].id}`);
|
||||
lines.push(`NUMENR ${wp.length}`);
|
||||
for (const w of wp) {
|
||||
const alt = w.alt ?? 0;
|
||||
lines.push(`${fmsType(w.type)} ${w.id} ${alt.toFixed(6)} ${w.lat.toFixed(6)} ${w.lon.toFixed(6)}`);
|
||||
}
|
||||
const content = lines.join('\n') + '\n';
|
||||
|
||||
const root = xplaneRoot();
|
||||
const dir = root ? path.join(root, 'Output', 'FMS plans') : path.join(process.cwd(), 'fms-out');
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, `${name}.fms`);
|
||||
fs.writeFileSync(file, content);
|
||||
return { ok: true, file, intoXplane: !!root };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user