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%; }