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 (
);
}
/* ---------------- 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 (
);
}
/* ---------------- 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 (
);
}
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 */}