import React, { useState, useEffect, useRef } from 'react'; import { num } from '../../api/useXplane.js'; import { useEased, useEasedAngle } from '../../api/ease.js'; // ============================================================================ // Citation X — Multi-Function Display (Honeywell Primus 2000 arc map). // Built against the manual (pages 32-33): // 1 Heading bug · 2 Heading · 3 Compass arc · 4 FMS source · 5 future leg (white) // 6 active leg (magenta) · 7 range arc · 8 ETE/SAT/TAS/GSPD group · 9 RNG // 10 V-SPEEDS · 11 EICAS SYS · 12 ET/FT timer · 13 MFD setup (TRAFFIC/TERRAIN/ // APTS/VOR) · 14 PFD setup · 15 RTN · 16 WX status · 17 ownship · 18 airport // 19 navaid · 20 digital heading bug // ============================================================================ const RNGS = [10, 20, 40, 80, 160]; const mod360 = (d) => ((d % 360) + 360) % 360; const toRad = (d) => (d * Math.PI) / 180; // great-circle distance (NM) + initial bearing (deg) from a→b function geo(aLat, aLon, bLat, bLon) { const φ1 = toRad(aLat), φ2 = toRad(bLat), dφ = toRad(bLat - aLat), dλ = toRad(bLon - aLon); const h = Math.sin(dφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(dλ / 2) ** 2; const dist = 3440.065 * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); const y = Math.sin(dλ) * Math.cos(φ2); const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(dλ); return { dist, brg: mod360((Math.atan2(y, x) * 180) / Math.PI) }; } export default function CitMFD({ xp }) { const V = xp.values || {}; const fp = xp.flightPlan || { waypoints: [] }; const [rng, setRng] = useState(40); const [ov, setOv] = useState({ traffic: true, terrain: false, apts: true, vor: true }); const [setup, setSetup] = useState(null); // null | 'mfd' | 'eicas' | 'pfd' const [vspd, setVspd] = useState(false); const [et, setEt] = useState(0); const etRun = useRef(false); useEffect(() => { const id = setInterval(() => etRun.current && setEt((t) => t + 1), 1000); return () => clearInterval(id); }, []); // smooth ownship + compass (same rAF glide as the G1000 map) const lat = useEased(num(V.lat), 0.14); const lon = useEased(num(V.lon), 0.14); const hdg = useEasedAngle(num(V.heading), 0.10); const trk = num(V.track); // arc map geometry: ownship near bottom, ~120° forward arc const W = 760, H = 760, cx = W / 2, cy = 600, R = 470; // compass radius const pxPerNm = R / rng; const project = (d, brg) => { // heading-up const rel = toRad(brg - hdg); return [cx + Math.sin(rel) * d * pxPerNm, cy - Math.cos(rel) * d * pxPerNm]; }; // build route polyline from waypoints relative to ownship const wps = (fp.waypoints || []).map((w) => { if (!isFinite(w.lat) || !isFinite(w.lon)) return null; const g = geo(lat, lon, w.lat, w.lon); const [x, y] = project(g.dist, g.brg); return { ...w, x, y, dist: g.dist }; }).filter(Boolean); const active = num(fp.activeLeg ?? 1); // compass arc ticks const ticks = []; for (let i = -60; i <= 60; i += 5) { const a = toRad(i), x1 = cx + Math.sin(a) * R, y1 = cy - Math.cos(a) * R; const len = i % 30 === 0 ? 20 : i % 10 === 0 ? 14 : 8; const x2 = cx + Math.sin(a) * (R - len), y2 = cy - Math.cos(a) * (R - len); ticks.push(); if (i % 30 === 0) { const h = mod360(hdg + i), lx = cx + Math.sin(a) * (R - 36), ly = cy - Math.cos(a) * (R - 36); ticks.push({String(Math.round(h / 10) % 36).padStart(2, '0')}); } } const gs = Math.round(num(V.groundspeed) * 1.94384); const tas = Math.round(num(V.tas)); const sat = Math.round(num(V.oat)); // ETE to destination (last wp) at current GS const destDist = wps.length ? wps[wps.length - 1].dist : 0; const eteMin = gs > 20 ? destDist / gs * 60 : 0; const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`; const now = new Date(); const SK = ({ label, on, onClick }) => ; return (
{/* heading box (#2,#20) + FMS source (#4) */} HDG {String(Math.round(mod360(num(V.apHdgBug)))).padStart(3, '0')} {String(Math.round(mod360(hdg))).padStart(3, '0')}° FMS1 {/* compass arc (#3) */} {ticks} {/* range arc (#7) at mid range */} {rng / 2} {/* NEXRAD weather (#16 WX) */} {ov.terrain && (V.wxCells || []).map((c, i) => { const g = geo(lat, lon, c.lat, c.lon); const [x, y] = project(g.dist, g.brg); return ; })} {/* flight-plan route (#5 white future, #6 magenta active) */} {wps.length > 1 && wps.map((w, i) => i === 0 ? null : ( ))} {wps.map((w, i) => ( {(w.type === 'APT') ? (ov.apts && ) : (ov.vor && )} {w.id} ))} {/* TCAS traffic (#13 TRAFFIC) */} {ov.traffic && (V.traffic || []).map((t, i) => { const g = geo(lat, lon, t.lat, t.lon); const [x, y] = project(g.dist, g.brg); const col = t.thr === 2 ? '#ff3b30' : t.thr === 1 ? '#ffb000' : '#19c3e0'; return {t.relAlt > 0 ? '+' : ''}{t.relAlt}; })} {/* ownship (#17) */} {/* heading bug on arc (#1) */} {(() => { const rel = mod360(num(V.apHdgBug) - hdg); const a = toRad(rel > 180 ? rel - 360 : rel); if (Math.abs(rel > 180 ? rel - 360 : rel) > 60) return null; const x = cx + Math.sin(a) * R, y = cy - Math.cos(a) * R; return ; })()} {/* data group (#8) bottom-right */} NM {rng} ETE{eteMin > 0 ? fmt(eteMin * 60) : '– –'} SAT{sat}°C TAS{tas} GSPD{gs} {/* V-SPEEDS reference card (#10) — Citation X operating speeds, manual p80 */} {vspd && ( V-SPEEDS · CITATION X {[['Vr (rotate)', '145'], ['Vfe (flaps)', '180'], ['Vmo SL-8000', '270'], ['Vmo >8000', '350'], ['Mmo', '0.935'], ['Vle/Vlo gear', '210'], ['Vref landing', '132'], ['Vso stall (ldg)', '115'], ['Vs1 stall (clean)', '136']].map(([k, v], i) => ( {k} {v} ))} )} {/* clock / ET + WX status (#12,#16) bottom-left */} {now.toTimeString().slice(0, 8)} CLOCK ET {fmt(et)} WX T0.0 G100%
{setup === 'mfd' ? ( <> setOv((o) => ({ ...o, traffic: !o.traffic }))} /> setOv((o) => ({ ...o, terrain: !o.terrain }))} /> setOv((o) => ({ ...o, apts: !o.apts }))} /> setOv((o) => ({ ...o, vor: !o.vor }))} /> setSetup(null)} /> ) : ( <>
RNG {rng}
)}
); }