diff --git a/server/bridge.js b/server/bridge.js index 013454f..8358f77 100644 --- a/server/bridge.js +++ b/server/bridge.js @@ -326,6 +326,17 @@ function startDemo() { engRpm: [2410], fuelFlow: [0.0072], oilTemp: [88], oilPress: [52], egt: [720], fuelQty: [60, 58], volts: [process.env.DEMO_ALERT ? 23.4 : 28.0, 27.8], amps: [-1.5], genAmps: [20.5], engHrs: 5040, fuelTot: 118 * 2.72, fuelMax: 144 * 2.72, // fuel totalizer: 118 of 144 gal (kg) — SYSTEM keys adjust + // TRAFFIC (TCAS) + NEXRAD demo data so those map overlays are demonstrable. + // relAlt = hundreds of ft vs own ship; vs +/- = climb/descend; thr = TA/RA. + traffic: [ + { lat: 47.52, lon: -122.28, relAlt: 12, vs: 1, thr: 0 }, + { lat: 47.40, lon: -122.45, relAlt: -8, vs: -1, thr: 1 }, + { lat: 47.47, lon: -122.18, relAlt: 2, vs: 0, thr: 2 }, + ], + wxCells: [ // synthetic NEXRAD precip: lat,lon,radiusNm,level(1 green·2 yellow·3 red) + { lat: 47.7, lon: -122.0, r: 8, lvl: 2 }, { lat: 47.75, lon: -121.9, r: 5, lvl: 3 }, + { lat: 47.2, lon: -122.6, r: 10, lvl: 1 }, { lat: 47.25, lon: -122.5, r: 6, lvl: 2 }, + ], }); // a sample plan so the map/FMS show something in demo mode fp.setPlan({ name: 'DEMO', waypoints: [ diff --git a/web/src/components/Bezel.jsx b/web/src/components/Bezel.jsx index 576a30c..a6bea41 100644 --- a/web/src/components/Bezel.jsx +++ b/web/src/components/Bezel.jsx @@ -76,6 +76,8 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, svtOpts else if (label === 'DCLTR') cycleDcltr(onMapMode); else if (label === 'AIRWAYS') onMapMode && onMapMode((m) => ({ ...m, airways: (((m.airways | 0) + 1) % 4) })); // off→all→Victor→Jet else if (label === 'AIRSPACE') onMapMode && onMapMode((m) => ({ ...m, airspace: !m.airspace })); + else if (label === 'TRAFFIC') onMapMode && onMapMode((m) => ({ ...m, traffic: !m.traffic })); + else if (label === 'NEXRAD') onMapMode && onMapMode((m) => ({ ...m, nexrad: !m.nexrad })); } else { if (label === 'PFD') setPage('pfd'); else if (label === 'BACK') setPage({ xpdrcode: 'xpdr', altunit: 'pfd' }[page] || 'root'); @@ -118,7 +120,7 @@ export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, svtOpts if (variant === 'mfd') return (label === 'TOPO' && mapMode?.base === 'topo') || (label === 'TERRAIN' && mapMode?.terrain) || (label === 'OSM' && mapMode?.base === 'osm') || (label === 'DCLTR' && mapMode?.dcltr > 0) || (label === 'AIRWAYS' && mapMode?.airways) - || (label === 'AIRSPACE' && mapMode?.airspace); + || (label === 'AIRSPACE' && mapMode?.airspace) || (label === 'TRAFFIC' && mapMode?.traffic) || (label === 'NEXRAD' && mapMode?.nexrad); return (label === 'SYN TERR' && svt3d) || (label === 'PATHWAY' && svtOpts?.pathway) || (label === 'APTSIGNS' && svtOpts?.aptSigns) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr) || (label === 'DME' && dme) || (label === 'OBS' && obs) || (label === 'CAUTION' && (alerts || hasAlerts)) diff --git a/web/src/components/MapView.jsx b/web/src/components/MapView.jsx index 957c820..d099110 100644 --- a/web/src/components/MapView.jsx +++ b/web/src/components/MapView.jsx @@ -57,6 +57,19 @@ const TILES = { dark: null, }; +// TCAS target symbol: diamond coloured by threat (other/proximate/TA/RA), with +// relative altitude (hundreds of ft, ± vs own ship) and a climb/descend arrow. +const TCAS_COLOR = ['#cfd6dd', '#19d3ff', '#ffce00', '#ff3b3b']; +function tcasSymbol(t) { + const C = TCAS_COLOR[t.thr || 0]; + const filled = (t.thr || 0) >= 1; + const arrow = t.vs > 0 ? '▲' : t.vs < 0 ? '▼' : ''; + const rel = (t.relAlt >= 0 ? '+' : '−') + String(Math.abs(Math.round(t.relAlt))).padStart(2, '0'); + const diamond = ``; + const html = `
${diamond}${rel}${arrow}
`; + return L.marker([t.lat, t.lon], { icon: L.divIcon({ className: 'tcas-divicon', html, iconSize: [16, 16], iconAnchor: [8, 8] }), interactive: false, zIndexOffset: 1200 }); +} + // G1000 / aeronautical-chart airspace styling, keyed by our coarse class. B/C/D // follow chart convention (B solid blue, C solid magenta, D dashed blue); // special-use areas use warm hues. Class A/E are omitted (they blanket huge @@ -88,6 +101,8 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t const aspLayerRef = useRef(null); const aspOnRef = useRef(false); const refreshAirspaceRef = useRef(null); + const wxLayerRef = useRef(null); + const trafficLayerRef = useRef(null); const baseRef = useRef(null); const terrRef = useRef(null); const zoomingRef = useRef(false); @@ -102,6 +117,8 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t const base = mapMode?.base || 'topo'; const airways = (mapMode?.airways | 0); // 0 off · 1 all · 2 Victor (low) · 3 Jet (high) const airspace = !!mapMode?.airspace; + const traffic = !!mapMode?.traffic; + const nexrad = !!mapMode?.nexrad; aspOnRef.current = airspace; const dcltrRef = useRef(dcltr); dcltrRef.current = dcltr; @@ -135,11 +152,13 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t map.getPane('terrain').style.zIndex = 250; map.getPane('terrain').style.pointerEvents = 'none'; - aspLayerRef.current = L.layerGroup().addTo(map); // airspace polygons (bottom overlay) + wxLayerRef.current = L.layerGroup().addTo(map); // NEXRAD precip (bottom) + aspLayerRef.current = L.layerGroup().addTo(map); // airspace polygons awyLayerRef.current = L.layerGroup().addTo(map); // airways navLayerRef.current = L.layerGroup().addTo(map); // real airports/navaids/fixes routeRef.current = L.layerGroup().addTo(map); // flight-plan legs (white + magenta active) wpLayerRef.current = L.layerGroup().addTo(map); + trafficLayerRef.current = L.layerGroup().addTo(map); // TCAS targets (top) // AIRWAYS overlay (Victor/Jet routes from X-Plane's earth_awy.dat). Light // blue lines with the airway name at the segment midpoint (labels ≥ z8). @@ -259,6 +278,26 @@ export default function MapView({ values, flightPlan, fp, inset = false, hud = t // redraw airspace when the AIRSPACE toggle changes useEffect(() => { refreshAirspaceRef.current && refreshAirspaceRef.current(); }, [airspace]); // eslint-disable-line + // NEXRAD precip cells (green/yellow/red) — toggled by the MFD NEXRAD softkey. + useEffect(() => { + const layer = wxLayerRef.current; if (!layer) return; + layer.clearLayers(); + if (!nexrad) return; + const col = { 1: '#1fa83a', 2: '#e6c200', 3: '#e23131' }; + for (const c of (values.wxCells || [])) { + if (!isFinite(c.lat)) continue; + L.circle([c.lat, c.lon], { radius: (c.r || 5) * 1852, stroke: false, fillColor: col[c.lvl] || col[1], fillOpacity: 0.32, interactive: false }).addTo(layer); + } + }, [nexrad, values.wxCells]); // eslint-disable-line + + // TCAS traffic targets — toggled by the MFD TRAFFIC softkey. + useEffect(() => { + const layer = trafficLayerRef.current; if (!layer) return; + layer.clearLayers(); + if (!traffic) return; + for (const tgt of (values.traffic || [])) { if (isFinite(tgt.lat)) layer.addLayer(tcasSymbol(tgt)); } + }, [traffic, values.traffic]); // eslint-disable-line + // TERRAIN AWARENESS overlay: colour the elevation grid (from the FlyWithLua // terrain probe) relative to aircraft altitude — red within 100 ft below/above, // yellow 100–1000 ft below, transparent otherwise (G1000 TAWS colours). Only diff --git a/web/src/styles.css b/web/src/styles.css index 00a3169..fec4d2b 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -411,6 +411,10 @@ body { .nav-sym { position: relative; width: 18px; height: 18px; } .nav-lbl { position: absolute; left: 19px; top: 1px; font: 700 11px/1 monospace; white-space: nowrap; text-shadow: 0 0 2px #000, 0 0 2px #000, 1px 1px 1px #000; } +/* TCAS traffic target: diamond + relative-altitude label */ +.tcas-sym { position: relative; width: 16px; height: 16px; } +.tcas-lbl { position: absolute; left: 50%; top: -11px; transform: translateX(-50%); font: 700 10px/1 monospace; white-space: nowrap; + text-shadow: 0 0 2px #000, 0 0 2px #000, 1px 1px 1px #000; } /* Bank pivots about the aircraft reference (attitude centre), which sits ~28% down the full-screen terrain box — so terrain roll tracks the attitude. */ .svt-canvas { width: 100%; height: 100%; transform-origin: 50% 28%; }