+ ACTIVE VNV WPT{vnav.tgtAlt}FT at {vnav.wptId}
+ VS TGT{Math.round(vnav.vsTgt)}FPMFPA{vnav.fpa.toFixed(1)}°
+ VS REQ{Math.round(vnav.vsReq)}FPMTIME TO TOD{fmtSec(vnav.todSec)}
+ V DEV{vnav.vDev >= 0 ? '+' : ''}{Math.round(vnav.vDev)}FT
+
+ ) :
— no active VNAV profile —
}
+
+ )}
{hits.length > 0 && (
diff --git a/web/src/components/PFD.jsx b/web/src/components/PFD.jsx
index 1e0a397..a5bc5b6 100644
--- a/web/src/components/PFD.jsx
+++ b/web/src/components/PFD.jsx
@@ -58,6 +58,10 @@ function fmtEte(s) {
}
// VNAV: nearest downstream waypoint with a lower altitude constraint, and the
// vertical speed required to meet it at the current groundspeed.
+// VNAV descent profile to the next waypoint that has a (lower) target altitude:
+// target VS for a -3° flight path, required VS to make the restriction, vertical
+// deviation from that path, and time-to-top-of-descent. (Manual S.64 / S.107.)
+const VNAV_FPA = 3.0; // default flight-path angle (degrees)
function vnavInfo(V, fp) {
const wps = fp?.waypoints || [];
const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1));
@@ -71,9 +75,16 @@ function vnavInfo(V, fp) {
prevLat = wps[i].lat; prevLon = wps[i].lon;
const tgt = num(wps[i].alt);
if (tgt > 0 && tgt < alt - 50) {
+ const tan = Math.tan((VNAV_FPA * Math.PI) / 180);
const tMin = (cum / gs) * 60;
- const vsReq = tMin > 0 ? (tgt - alt) / tMin : 0;
- return { wptId: wps[i].id, tgtAlt: tgt, dist: cum, vsReq };
+ const vsReq = tMin > 0 ? (tgt - alt) / tMin : 0; // fpm to make the fix
+ const vsTgt = -gs * tan * 101.27; // fpm for the FPA at this GS
+ const desiredAltNow = tgt + cum * 6076.12 * tan; // path altitude at present position
+ const vDev = alt - desiredAltNow; // + = above path
+ const descentNm = (alt - tgt) / (6076.12 * tan); // distance the descent itself takes
+ const todNm = cum - descentNm; // distance ahead until TOD
+ const todSec = todNm > 0 ? (todNm / gs) * 3600 : 0;
+ return { wptId: wps[i].id, tgtAlt: tgt, dist: cum, vsReq, vsTgt, vDev, fpa: VNAV_FPA, todSec };
}
}
return null;
diff --git a/web/src/styles.css b/web/src/styles.css
index e2fece0..3e617e5 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -247,7 +247,17 @@ body {
.r-wpt { color: #0ff; font-weight: 700; } .r-wpt i { color: #0a8; font-style: normal; font-size: 10px; margin-left: 6px; }
.r-wpt b { font-weight: 700; } .r-wpt b.cur { background: #19b8e6; color: #042230; padding: 0 4px; border-radius: 1px; }
.r-dtk, .r-dis, .r-cum { color: #e7edf2; text-align: right; }
-.r-alt { color: #0ff; text-align: right; }
+.r-alt { color: #6f808d; text-align: right; }
+.r-alt.dsgn { color: #4fa8ff; } /* designated (VNAV) altitude = blue, per manual S.105 */
+/* CURRENT VNV PROFILE panel (MFD flight-plan page) */
+.fpl-vnav { border-top: 1px solid #2c343c; padding: 6px 12px 8px; font-family: 'Roboto Mono', monospace; }
+.fpl-vnav-h { color: #36d2ff; font-size: 11px; letter-spacing: 1px; margin-bottom: 5px; }
+.fpl-vnav-grid { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 3px 10px; align-items: baseline; }
+.fpl-vnav-grid b { color: #6f808d; font-weight: normal; font-size: 11px; }
+.fpl-vnav-grid span { color: #fff; font-size: 14px; }
+.fpl-vnav-grid span u { color: #6f808d; font-size: 9px; text-decoration: none; margin-left: 1px; }
+.fpl-vnav-grid .vwpt { color: #4fa8ff; }
+.fpl-vnav-none { color: #6f808d; font-size: 12px; }
/* ORIG / DEST subtitle (PFD window) */
.fpl-od { color: #36d2ff; text-align: center; font-family: 'Roboto Mono', monospace; font-size: 14px; padding: 3px 0; border-bottom: 1px solid #1c242c; letter-spacing: 1px; }
/* compact window: DTK/DIS only (drop CUM/ALT), no editor — like the real FPL window */