PROC: activate approach/missed/vector-to-final; DCLTR-2 declutters airspace

PROC menu actions were empty stubs. Now: procedures.js tags approach legs with
seg ('approach'|'missed' — everything past the runway threshold = missed,
previously dropped); Proc.jsx flags loaded legs appr/missed (preserved by
flightplan.setPlan) and the ACTIVATE APPROACH / MISSED APPROACH / VECTOR-TO-FINAL
items set the active (magenta) leg to the matching segment, with a hint when
nothing is loaded. Missed legs shown dimmed in the preview.

DCLTR-2 now hides the airspace (SUA) overlay, matching the manual (p.56), in
addition to the existing nav-symbol declutter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:43:36 +02:00
parent fb6f0182cc
commit 6d61c122e1
5 changed files with 40 additions and 12 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ export function setPlan(next) {
const wps = Array.isArray(next?.waypoints) const wps = Array.isArray(next?.waypoints)
? next.waypoints ? next.waypoints
.filter((w) => isFinite(w.lat) && isFinite(w.lon)) .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, ...(w.dsgn != null ? { dsgn: !!w.dsgn } : {}), ...(w.appr ? { appr: true } : {}) })) .map((w) => ({ id: String(w.id || 'WPT'), lat: +w.lat, lon: +w.lon, type: w.type || 'WPT', alt: w.alt ?? null, ...(w.dsgn != null ? { dsgn: !!w.dsgn } : {}), ...(w.appr ? { appr: true } : {}), ...(w.missed ? { missed: true } : {}) }))
: []; : [];
const wantLeg = Number.isFinite(next?.activeLeg) ? next.activeLeg : 1; 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 }; plan = { name: next?.name || 'ACTIVE', waypoints: wps, activeLeg: Math.max(1, Math.min(wps.length - 1, wantLeg)) || 1 };
+8 -3
View File
@@ -121,6 +121,10 @@ export function procedureLegs(icao, type, name, trans) {
const out = []; const out = [];
const seen = new Set(); const seen = new Set();
// For approaches, the runway threshold is the Missed-Approach Point: legs after
// it are the missed-approach segment. We tag (rather than drop) them so the
// FMS can hold them un-sequenced and "Activate Missed Approach" can fly them.
let inMissed = false;
for (const leg of seq) { for (const leg of seq) {
if (!leg.fix) continue; // heading/altitude legs w/o a fix if (!leg.fix) continue; // heading/altitude legs w/o a fix
if (seen.has(leg.fix)) continue; // de-dupe repeated fixes if (seen.has(leg.fix)) continue; // de-dupe repeated fixes
@@ -133,9 +137,10 @@ export function procedureLegs(icao, type, name, trans) {
} }
if (!pt) continue; // unresolved fix → skip if (!pt) continue; // unresolved fix → skip
seen.add(leg.fix); seen.add(leg.fix);
out.push({ id: leg.fix, lat: pt.lat, lon: pt.lon, type: isRwy ? 'APT' : 'WPT', alt: leg.alt }); const wp = { id: leg.fix, lat: pt.lat, lon: pt.lon, type: isRwy ? 'APT' : 'WPT', alt: leg.alt };
// An approach ends at the runway threshold — drop the missed-approach legs. if (TYPE === 'APPCH') wp.seg = inMissed ? 'missed' : 'approach';
if (TYPE === 'APPCH' && isRwy) break; out.push(wp);
if (TYPE === 'APPCH' && isRwy) inMissed = true; // everything past the runway = missed
} }
return out; return out;
} }
+4 -1
View File
@@ -175,7 +175,9 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
const refreshAirspace = async () => { const refreshAirspace = async () => {
const layer = aspLayerRef.current; const layer = aspLayerRef.current;
if (!layer) return; if (!layer) return;
if (!aspOnRef.current || map.getZoom() < 6) { layer.clearLayers(); return; } // DCLTR-2 and above declutter Special-Use Airspace (manual p.56), so hide
// the airspace overlay at those levels even when the AIRSPACE key is on.
if (!aspOnRef.current || map.getZoom() < 6 || (dcltrRef.current || 0) >= 2) { layer.clearLayers(); return; }
const b = map.getBounds(); const b = map.getBounds();
try { try {
const res = await fetch(`/api/airspace/bbox?s=${b.getSouth()}&w=${b.getWest()}&n=${b.getNorth()}&e=${b.getEast()}&limit=400`); const res = await fetch(`/api/airspace/bbox?s=${b.getSouth()}&w=${b.getWest()}&n=${b.getNorth()}&e=${b.getEast()}&limit=400`);
@@ -309,6 +311,7 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
if (!map) return; if (!map) return;
if (dcltr > 0) navLayerRef.current?.clearLayers(); if (dcltr > 0) navLayerRef.current?.clearLayers();
else map.fire('moveend'); // triggers refreshNav to redraw symbols else map.fire('moveend'); // triggers refreshNav to redraw symbols
refreshAirspaceRef.current && refreshAirspaceRef.current(); // re-eval SUA declutter (DCLTR-2)
}, [dcltr]); // eslint-disable-line }, [dcltr]); // eslint-disable-line
// Smooth ownship motion. The sim streams position/heading at ~10 Hz; setting // Smooth ownship motion. The sim streams position/heading at ~10 Hz; setting
+24 -7
View File
@@ -25,6 +25,7 @@ export default function Proc({ xp, onClose }) {
const [selProc, setSelProc] = useState(null); // { name, transitions } const [selProc, setSelProc] = useState(null); // { name, transitions }
const [selTrans, setSelTrans] = useState(''); const [selTrans, setSelTrans] = useState('');
const [legs, setLegs] = useState([]); const [legs, setLegs] = useState([]);
const [note, setNote] = useState('');
// Fetch the procedure summary whenever the airport changes. // Fetch the procedure summary whenever the airport changes.
useEffect(() => { useEffect(() => {
@@ -54,12 +55,27 @@ export default function Proc({ xp, onClose }) {
const load = () => { const load = () => {
if (!legs.length) return; if (!legs.length) return;
const existing = wps.slice(); const existing = wps.slice();
// Departures go to the front, arrivals/approaches to the end. // Approaches carry the missed-approach segment too (server-tagged via `seg`):
const merged = cat === 'departure' ? [...legs, ...existing] : [...existing, ...legs]; // flag approach legs `appr` and missed legs `missed` so the FMS can activate
// each on demand. Departures go to the front, arrivals/approaches to the end.
const tagged = cat === 'approach'
? legs.map((l) => (l.seg === 'missed' ? { ...l, missed: true } : { ...l, appr: true }))
: legs;
const merged = cat === 'departure' ? [...tagged, ...existing] : [...existing, ...tagged];
fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: cat === 'departure' ? 1 : existing.length || 1 }); fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: cat === 'departure' ? 1 : existing.length || 1 });
onClose(); onClose();
}; };
// Activate a segment already loaded in the plan, like the real PROC menu.
// setActive(i) makes the leg ENDING at waypoint i the magenta (active) leg.
const activate = (find, label) => {
const i = find(flightPlan?.waypoints || []);
if (i > 0) { fp.setActive(i); onClose(); }
else setNote(`Kein ${label} im Flugplan — erst SELECT APPROACH → LOAD`);
};
const firstIdx = (pred) => (ws) => ws.findIndex(pred);
const lastIdx = (pred) => (ws) => { for (let i = ws.length - 1; i >= 0; i--) if (pred(ws[i])) return i; return -1; };
const catLabel = CATS.find((c) => c.id === cat).label; const catLabel = CATS.find((c) => c.id === cat).label;
// The PDF's action menu. SELECT … opens our picker for that category; // The PDF's action menu. SELECT … opens our picker for that category;
@@ -74,13 +90,14 @@ export default function Proc({ xp, onClose }) {
<div className="dlg proc menu" onClick={(e) => e.stopPropagation()}> <div className="dlg proc menu" onClick={(e) => e.stopPropagation()}>
<div className="dlg-head">PROCEDURES</div> <div className="dlg-head">PROCEDURES</div>
<div className="proc-menu"> <div className="proc-menu">
{item('ACTIVATE VECTOR-TO-FINAL', () => {})} {item('ACTIVATE VECTOR-TO-FINAL', () => activate(lastIdx((w) => w.appr), 'Approach'))}
{item('ACTIVATE APPROACH', () => {})} {item('ACTIVATE APPROACH', () => activate(firstIdx((w) => w.appr), 'Approach'))}
{item('ACTIVATE MISSED APPROACH', () => {})} {item('ACTIVATE MISSED APPROACH', () => activate(firstIdx((w) => w.missed), 'Missed Approach'))}
{item('SELECT APPROACH', () => sel('approach'), true)} {item('SELECT APPROACH', () => sel('approach'), true)}
{item('SELECT ARRIVAL', () => sel('arrival'))} {item('SELECT ARRIVAL', () => sel('arrival'))}
{item('SELECT DEPARTURE', () => sel('departure'))} {item('SELECT DEPARTURE', () => sel('departure'))}
</div> </div>
{note && <div className="proc-note">{note}</div>}
</div> </div>
</div> </div>
); );
@@ -120,8 +137,8 @@ export default function Proc({ xp, onClose }) {
<div className="proc-preview"> <div className="proc-preview">
<div className="proc-coltitle">{legs.length} FIXES</div> <div className="proc-coltitle">{legs.length} FIXES</div>
{legs.map((l, i) => ( {legs.map((l, i) => (
<div key={l.id + i} className="proc-leg"> <div key={l.id + i} className={`proc-leg ${l.seg === 'missed' ? 'missed' : ''}`}>
<b>{l.id}</b>{l.alt ? <u>{l.alt}ft</u> : null} <b>{l.id}</b>{l.alt ? <u>{l.alt}ft</u> : null}{l.seg === 'missed' ? <i className="missed-tag">MA</i> : null}
</div> </div>
))} ))}
</div> </div>
+3
View File
@@ -382,6 +382,9 @@ body {
.proc-empty { color: #6f808d; font-size: 11px; padding: 8px; } .proc-empty { color: #6f808d; font-size: 11px; padding: 8px; }
.proc-leg { display: flex; align-items: baseline; gap: 8px; padding: 5px 8px; border-bottom: 1px solid #11161b; font-size: 13px; } .proc-leg { display: flex; align-items: baseline; gap: 8px; padding: 5px 8px; border-bottom: 1px solid #11161b; font-size: 13px; }
.proc-leg b { color: #0ff; } .proc-leg u { color: #39d3c0; font-size: 10px; text-decoration: none; margin-left: auto; } .proc-leg b { color: #0ff; } .proc-leg u { color: #39d3c0; font-size: 10px; text-decoration: none; margin-left: auto; }
.proc-leg.missed b { color: #8aa0ad; } /* missed-approach legs shown dimmed */
.proc-leg .missed-tag { color: #6f808d; font-style: normal; font-size: 9px; border: 1px solid #2a3640; border-radius: 2px; padding: 0 3px; margin-left: 6px; }
.proc-note { color: #ffce46; font-size: 11px; padding: 8px 12px; border-top: 1px solid #11161b; }
/* G1000 vector nav symbology drawn from X-Plane's own nav data */ /* G1000 vector nav symbology drawn from X-Plane's own nav data */
.nav-divicon { background: none; border: none; } .nav-divicon { background: none; border: none; }
.nav-sym { position: relative; width: 18px; height: 18px; } .nav-sym { position: relative; width: 18px; height: 18px; }