import React, { useState } from 'react'; import { num } from '../api/useXplane.js'; import MapView from './MapView.jsx'; import Nearest from './Nearest.jsx'; import FplPage from './FplPage.jsx'; const arr = (v, i = 0, d = 0) => (Array.isArray(v) ? num(v[i], d) : num(v, d)); const KG_PER_GAL = 2.72; // avgas const navF = (v) => (num(v) / 100).toFixed(2); const comF = (v) => (num(v) / 100).toFixed(3); // Active flight-plan leg: distance / desired track / ETE to the active waypoint // (great-circle from the aircraft), for the MFD nav data bar. Mirrors the PFD's // activeNav so the two displays agree. const R_NM = 3440.065, D2R = Math.PI / 180, R2D = 180 / Math.PI; function legNav(V, fp) { const wps = fp?.waypoints || []; const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1)); const wp = wps[ai]; const lat = num(V.lat), lon = num(V.lon); if (!wp || (!lat && !lon)) return null; const dLat = (wp.lat - lat) * D2R, dLon = (wp.lon - lon) * D2R; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat * D2R) * Math.cos(wp.lat * D2R) * Math.sin(dLon / 2) ** 2; const dist = 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(a))); const y = Math.sin(dLon) * Math.cos(wp.lat * D2R); const x = Math.cos(lat * D2R) * Math.sin(wp.lat * D2R) - Math.sin(lat * D2R) * Math.cos(wp.lat * D2R) * Math.cos(dLon); const dtk = (Math.atan2(y, x) * R2D + 360) % 360; const gs = num(V.groundspeed) * 1.94384; return { id: wp.id, dist, dtk, ete: gs > 20 ? (dist / gs) * 3600 : null }; } const fmtEte = (s) => { if (s == null) return '__:__'; const m = Math.floor(s / 60), ss = Math.round(s % 60); return m < 60 ? `${m}:${String(ss).padStart(2, '0')}` : `${Math.floor(m / 60)}+${String(m % 60).padStart(2, '0')}`; }; // G1000 MFD — full-width NAV/COM bar on top, the engine instrument strip (EIS) // down the left as real bar gauges, and the moving map (X-Plane nav data) with // G1000 chrome (compass rose, range, NORTH UP, mode) filling the rest. const MFD_PAGES = [{ id: 'map', name: 'MAP' }, { id: 'fpl', name: 'FPL' }, { id: 'nrst', name: 'NRST' }]; export default function MFD({ values: V, flightPlan, fp, mapMode, page = 'map', onCycle, xp, vnav, onVnav }) { const [rangeNm, setRangeNm] = useState(8); const idx = Math.max(0, MFD_PAGES.findIndex((p) => p.id === page)); return (
{/* MapView stays mounted (keeps tiles warm) but is hidden under NRST */}
setRangeNm(rangeNm)} />
{page === 'map' && mapMode?.profile && } {page === 'nrst' && } {page === 'fpl' && xp && } {/* page-group indicator (bottom-right), like the real G1000 — selected by the FMS knob; tappable as a touch fallback. */}
); } /* ---------------- top NAV/COM bar ---------------- */ function MfdTopBar({ V, fp }) { const gs = Math.round(num(V.groundspeed) * 1.94384); const trk = String(Math.round(num(V.track)) % 360).padStart(3, '0'); const leg = legNav(V, fp); const dtk = leg ? `${String(Math.round(leg.dtk) % 360).padStart(3, '0')}°` : '___°'; const swap = (x, y) => ; return ( {[300, 660].map((x) => )} {/* NAV1 / NAV2 — standby LEFT (cyan, boxed), active RIGHT (white) per manual */} NAV1 {navF(V.nav1Sb)} {swap(150, 27)} {navF(V.nav1)} NAV2 {navF(V.nav2Sb)} {navF(V.nav2)} {/* centre: GS/DTK/TRK/ETE + active mode line */} GS {gs} KT DTK {dtk} TRK {trk}° ETE {fmtEte(leg?.ete)} {/* active leg (centre): → waypoint + distance, or no-flight-plan note */} {leg ? ( {leg.id} {leg.dist.toFixed(1)} NM ) : ( NO ACTIVE WAYPOINT )} {/* COM1 / COM2 */} {comF(V.com1)} {swap(818, 27)} {comF(V.com1Sb)} COM1 {comF(V.com2)} {comF(V.com2Sb)} COM2 ); } /* ---------------- vertical profile (PROFILE softkey) ---------------- */ // Altitude-vs-distance view along the active flight plan: the aircraft at the // left, upcoming waypoints with their target altitudes, and the planned descent // path between them. Pure geometry from the plan + current altitude. function VertProfile({ V, fp }) { const R = 3440.065, rad = (d) => (d * Math.PI) / 180; const dist = (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 * Math.asin(Math.min(1, Math.sqrt(s))); }; const wps = fp?.waypoints || []; const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1)); const alt = num(V.altitude); const pts = []; let cum = 0, prev = { lat: num(V.lat), lon: num(V.lon) }; for (let i = ai; i < wps.length; i++) { cum += dist(prev, wps[i]); prev = wps[i]; pts.push({ d: cum, alt: num(wps[i].alt) || null, id: wps[i].id }); } const maxD = Math.max(10, cum); const altMax = Math.max(alt, ...pts.map((p) => p.alt || 0), 3000) * 1.15; const W = 1000, H = 200, padL = 46, padR = 12, padT = 12, padB = 22; const x = (d) => padL + (d / maxD) * (W - padL - padR); const y = (a) => (H - padB) - (a / altMax) * (H - padT - padB); const altPts = pts.filter((p) => p.alt != null); const path = [`${x(0)},${y(alt)}`, ...altPts.map((p) => `${x(p.d)},${y(p.alt)}`)].join(' '); const grid = [0, altMax / 2, altMax].map((a) => Math.round(a / 500) * 500); return (
{grid.map((a) => ( {Math.round(a / 100) * 100} ))} {/* planned vertical path through waypoint target altitudes */} {altPts.length > 0 && } {/* waypoints */} {pts.map((p, i) => ( {p.alt != null && } {p.id} {p.alt != null && {p.alt}} ))} {/* own aircraft at the left edge */} {Math.round(alt)}
); } /* ---------------- engine instrument strip (EIS) ---------------- */ function EisStrip({ V }) { const rpm = arr(V.engRpm); const ffGph = (arr(V.fuelFlow) * 3600) / KG_PER_GAL; const oilPsi = arr(V.oilPress); // X-Plane's temperature indicator datarefs may already honor the user's unit // (°F) despite the "_deg_C" name. Auto-detect: only convert if it still looks // like Celsius, so we don't double-convert (which pegged the gauges red). const oilT = arr(V.oilTemp), egtT = arr(V.egt); const oilF = oilT > 150 ? oilT : oilT * 9 / 5 + 32; const egtF = egtT > 900 ? egtT : egtT * 9 / 5 + 32; const fuelL = arr(V.fuelQty, 0) / KG_PER_GAL; const fuelR = arr(V.fuelQty, 1) / KG_PER_GAL; const voltsM = arr(V.volts, 0, 28); // main bus const voltsE = arr(V.volts, 1, voltsM); // essential bus (falls back to main) const ampsM = arr(V.genAmps, 0); // alternator (M) const ampsS = arr(V.amps, 0); // battery (S) const engHrs = num(V.engHrs) / 3600; return ( {/* fuel totalizer (SYSTEM → DEC/INC/RST FUEL): pilot-set remaining + used */} {(() => { const rem = num(V.fuelTot) / KG_PER_GAL; const used = Math.max(0, (num(V.fuelMax) - num(V.fuelTot)) / KG_PER_GAL); return (<> CALC {rem.toFixed(1)} GAL USED {used.toFixed(0)} ); })()} ENG {engHrs.toFixed(1)} HRS – ELECTRICAL – M BUS E {voltsM.toFixed(1)} VOLTS {voltsE.toFixed(1)} M BATT S {ampsM >= 0 ? '+' : ''}{ampsM.toFixed(1)} AMPS {ampsS >= 0 ? '+' : ''}{ampsS.toFixed(1)} ); } function Bar({ y, label, val, min, max, value, zones }) { const x0 = 8, x1 = 182, bw = x1 - x0; const px = (v) => x0 + bw * Math.max(0, Math.min(1, (v - min) / (max - min))); const p = px(value); return ( {label} {val != null && {val}} {zones.map((z, i) => )} ); } // Fuel quantity: one bar per the C172's two tanks, with L and R pointers on a // shared 0–10–20–F (gal) scale; yellow/red caution zone at the low end. function FuelBar({ y, left, right }) { const x0 = 8, x1 = 182, bw = x1 - x0, max = 26.5; const px = (g) => x0 + bw * Math.max(0, Math.min(1, g / max)); const tick = (g, lbl) => ( {lbl} ); const ptr = (g, lbl) => ( {lbl} ); return ( FUEL QTY GAL {tick(0, '0')}{tick(8.83, '10')}{tick(17.66, '20')}{tick(max, 'F')} {ptr(left, 'L')}{ptr(right, 'R')} ); } function RpmArc({ rpm }) { const max = 2700, frac = Math.max(0, Math.min(1, rpm / max)); const a0 = -210, a1 = 30, ang = a0 + (a1 - a0) * frac; const cx = 95, cy = 62, r = 42; const pt = (deg, rr) => [cx + rr * Math.cos((deg * Math.PI) / 180), cy + rr * Math.sin((deg * Math.PI) / 180)]; const arc = (s, e, color, w) => { const [x0, y0] = pt(s, r), [x1, y1] = pt(e, r); return 180 ? 1 : 0} 1 ${x1} ${y1}`} fill="none" stroke={color} strokeWidth={w} />; }; const [nx, ny] = pt(ang, r - 2); return ( {arc(a0, a1, '#2a2a2a', 7)} {arc(a0, -30, '#0c0', 7)} {arc(0, a1, '#c00', 7)} RPM {Math.round(rpm)} ); } /* ---------------- map chrome overlay (compass rose / range / mode) ---------------- */ const NICE = [0.5, 1, 1.5, 2, 2.5, 4, 5, 7.5, 10, 15, 20, 25, 40, 50, 75, 100, 150, 200, 250, 500]; function niceRange(nm) { let r = NICE[0]; for (const s of NICE) if (nm >= s) r = s; return r; } function MapChrome({ V, rangeNm }) { const gs = Math.round(num(V.groundspeed) * 1.94384); const rng = niceRange(rangeNm); const wd = ((Math.round(num(V.windDir)) % 360) + 360) % 360, ws = Math.round(num(V.windSpd)); return (
{/* the compass rose now lives in MapView, anchored to the aircraft */}
{gs} KTNORTH UP
{ws >= 1 ? (<>{String(wd).padStart(3, '0')}° {ws}kt) : CALM}
{rng} NM
); }