// 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 }; } }