Files
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
5.2 KiB
JavaScript

// 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 dir = fmsDir();
try {
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, `${name}.fms`);
fs.writeFileSync(file, content);
return { ok: true, file, intoXplane: !!xplaneRoot() };
} catch (e) {
return { ok: false, error: e.message };
}
}
// ---- load saved X-Plane .fms plans (Output/FMS plans) ----
function fmsDir() {
const root = xplaneRoot();
return root ? path.join(root, 'Output', 'FMS plans') : path.join(process.cwd(), 'fms-out');
}
const FMS_TYPE = { 1: 'APT', 2: 'NDB', 3: 'VOR', 11: 'WPT', 28: 'USR' };
// List the names of every saved .fms plan (X-Plane's own + our exports).
export function listPlans() {
try {
return fs.readdirSync(fmsDir())
.filter((f) => f.toLowerCase().endsWith('.fms'))
.map((f) => f.replace(/\.fms$/i, ''))
.sort((a, b) => a.localeCompare(b));
} catch { return []; }
}
// Parse a saved .fms (v1100/v3) into our waypoints and make it the active plan.
export function loadFms(name) {
const safe = String(name || '').replace(/[^\w .+-]/g, '');
const file = path.join(fmsDir(), `${safe}.fms`);
if (!fs.existsSync(file)) return { ok: false, error: `not found: ${safe}` };
const wps = [];
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const p = raw.trim().split(/\s+/);
// waypoint rows start with a numeric type code: <type> <ident> <alt> <lat> <lon>
if (p.length >= 5 && /^\d+$/.test(p[0]) && p[0] !== '1100') {
const lat = parseFloat(p[3]), lon = parseFloat(p[4]), alt = parseFloat(p[2]);
if (isFinite(lat) && isFinite(lon)) {
wps.push({ id: p[1], lat, lon, type: FMS_TYPE[+p[0]] || 'WPT', alt: alt > 0 ? Math.round(alt) : null });
}
}
}
if (wps.length < 1) return { ok: false, error: 'no waypoints in file' };
setPlan({ name: safe.toUpperCase(), waypoints: wps, activeLeg: 1 });
return { ok: true, plan, count: wps.length };
}