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:
@@ -18,7 +18,7 @@ export function setPlan(next) {
|
||||
const wps = Array.isArray(next?.waypoints)
|
||||
? next.waypoints
|
||||
.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;
|
||||
plan = { name: next?.name || 'ACTIVE', waypoints: wps, activeLeg: Math.max(1, Math.min(wps.length - 1, wantLeg)) || 1 };
|
||||
|
||||
@@ -121,6 +121,10 @@ export function procedureLegs(icao, type, name, trans) {
|
||||
|
||||
const out = [];
|
||||
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) {
|
||||
if (!leg.fix) continue; // heading/altitude legs w/o a fix
|
||||
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
|
||||
seen.add(leg.fix);
|
||||
out.push({ 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' && isRwy) break;
|
||||
const wp = { id: leg.fix, lat: pt.lat, lon: pt.lon, type: isRwy ? 'APT' : 'WPT', alt: leg.alt };
|
||||
if (TYPE === 'APPCH') wp.seg = inMissed ? 'missed' : 'approach';
|
||||
out.push(wp);
|
||||
if (TYPE === 'APPCH' && isRwy) inMissed = true; // everything past the runway = missed
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,9 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t
|
||||
const refreshAirspace = async () => {
|
||||
const layer = aspLayerRef.current;
|
||||
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();
|
||||
try {
|
||||
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 (dcltr > 0) navLayerRef.current?.clearLayers();
|
||||
else map.fire('moveend'); // triggers refreshNav to redraw symbols
|
||||
refreshAirspaceRef.current && refreshAirspaceRef.current(); // re-eval SUA declutter (DCLTR-2)
|
||||
}, [dcltr]); // eslint-disable-line
|
||||
|
||||
// Smooth ownship motion. The sim streams position/heading at ~10 Hz; setting
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function Proc({ xp, onClose }) {
|
||||
const [selProc, setSelProc] = useState(null); // { name, transitions }
|
||||
const [selTrans, setSelTrans] = useState('');
|
||||
const [legs, setLegs] = useState([]);
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
// Fetch the procedure summary whenever the airport changes.
|
||||
useEffect(() => {
|
||||
@@ -54,12 +55,27 @@ export default function Proc({ xp, onClose }) {
|
||||
const load = () => {
|
||||
if (!legs.length) return;
|
||||
const existing = wps.slice();
|
||||
// Departures go to the front, arrivals/approaches to the end.
|
||||
const merged = cat === 'departure' ? [...legs, ...existing] : [...existing, ...legs];
|
||||
// Approaches carry the missed-approach segment too (server-tagged via `seg`):
|
||||
// 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 });
|
||||
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;
|
||||
|
||||
// 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-head">PROCEDURES</div>
|
||||
<div className="proc-menu">
|
||||
{item('ACTIVATE VECTOR-TO-FINAL', () => {})}
|
||||
{item('ACTIVATE APPROACH', () => {})}
|
||||
{item('ACTIVATE MISSED APPROACH', () => {})}
|
||||
{item('ACTIVATE VECTOR-TO-FINAL', () => activate(lastIdx((w) => w.appr), 'Approach'))}
|
||||
{item('ACTIVATE APPROACH', () => activate(firstIdx((w) => w.appr), 'Approach'))}
|
||||
{item('ACTIVATE MISSED APPROACH', () => activate(firstIdx((w) => w.missed), 'Missed Approach'))}
|
||||
{item('SELECT APPROACH', () => sel('approach'), true)}
|
||||
{item('SELECT ARRIVAL', () => sel('arrival'))}
|
||||
{item('SELECT DEPARTURE', () => sel('departure'))}
|
||||
</div>
|
||||
{note && <div className="proc-note">{note}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -120,8 +137,8 @@ export default function Proc({ xp, onClose }) {
|
||||
<div className="proc-preview">
|
||||
<div className="proc-coltitle">{legs.length} FIXES</div>
|
||||
{legs.map((l, i) => (
|
||||
<div key={l.id + i} className="proc-leg">
|
||||
<b>{l.id}</b>{l.alt ? <u>{l.alt}ft</u> : null}
|
||||
<div key={l.id + i} className={`proc-leg ${l.seg === 'missed' ? 'missed' : ''}`}>
|
||||
<b>{l.id}</b>{l.alt ? <u>{l.alt}ft</u> : null}{l.seg === 'missed' ? <i className="missed-tag">MA</i> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -382,6 +382,9 @@ body {
|
||||
.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 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 */
|
||||
.nav-divicon { background: none; border: none; }
|
||||
.nav-sym { position: relative; width: 18px; height: 18px; }
|
||||
|
||||
Reference in New Issue
Block a user