thewildofficial
feat(sim): improve inference control flow and runway approach behavior
b209d78
Raw
History Blame Contribute Delete
8.15 kB
import React, { useState, useEffect, useRef } from 'react';
import { DEFAULT_CENTER } from './utils/geo';
import RadarMap from './components/RadarMap';
import FlightRoster from './components/FlightRoster';
import DevConsole from './components/DevConsole';
import FlightModal from './components/FlightModal';
import AdminPanel from './components/AdminPanel';
import RouteSidebar from './components/RouteSidebar';
import './App.css';
const INITIAL_STATE = {
simulation_time: 0,
is_terminal: false,
active_runways: [],
wind_heading: 0,
wind_speed: 0,
time_scale: 1,
aircrafts: {},
events: [],
airspace: { nodes: [], edges: [] },
airports: [],
config: null
};
function App() {
const [data, setData] = useState(INITIAL_STATE);
const [logs, setLogs] = useState([]);
const [selectedFlight, setSelectedFlight] = useState(null);
const [activeAirport, setActiveAirport] = useState({ ...DEFAULT_CENTER, name: "Loading..." });
const [hoveredWaypoint, setHoveredWaypoint] = useState(null);
// Drafting States (Unified)
const [draftingMode, setDraftingMode] = useState(null); // null | 'airport' | 'runway' | 'waypoint' | 'route'
const [airportName, setAirportName] = useState("");
const [runwayPoints, setRunwayPoints] = useState([]);
const [mousePos, setMousePos] = useState(null);
const [isRunwayBidirectional, setIsRunwayBidirectional] = useState(true);
// STAR Builder specific state
const [starDraft, setStarDraft] = useState({
name: '',
gate: 'N',
runway_id: null,
sequence: [] // List of Waypoint IDs
});
const [sidDraft, setSidDraft] = useState({
name: '',
runway_id: null,
gate: 'N',
sequence: []
});
const wsRef = useRef(null);
const sendWSMessage = (type, payload) => {
// 1. Optimistic/Local Update for Development
if (type === 'create_airport') {
setData(prev => ({
...prev,
airports: [...(prev.airports || []), { ...payload, name: payload.name || `Airport ${prev.airports.length + 1}`, runways: [] }]
}));
}
if (type === 'create_runway') {
setData(prev => {
const airports = (prev.airports || []).map(ap => {
if (ap.airport_code === payload.airport_code) {
const rwys = [{
id: payload.runway_id || `RWY_${(ap.runways || []).length + 1}`,
heading: payload.heading || 0,
start: payload.start,
end: payload.end
}];
if (payload.bidirectional) {
const rw_id = payload.runway_id || `RWY_${(ap.runways || []).length + 1}`;
rwys.push({
id: `${rw_id}_REV`,
heading: (payload.heading || 0 + 180) % 360,
start: payload.end,
end: payload.start
});
}
const updated = { ...ap, runways: [...(ap.runways || []), ...rwys] };
if (activeAirport?.airport_code === ap.airport_code) {
setActiveAirport(updated);
}
return updated;
}
return ap;
});
return { ...prev, airports };
});
}
// 2. Network Send
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type, payload }));
}
};
useEffect(() => {
// 1. Real-time: Connect to WebSocket with Auto-Reconnect
const connectWS = () => {
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => console.log('✅ Connected to RL Backend');
ws.onmessage = (event) => {
try {
const incomingData = JSON.parse(event.data);
if (incomingData && typeof incomingData === 'object') {
// 1. Update Simulation State
setData(prevData => ({ ...prevData, ...incomingData }));
// 2. Accumulate Logs (Independent State)
if (incomingData.events && incomingData.events.length > 0) {
setLogs(prevLogs => [...prevLogs, ...incomingData.events].slice(-500));
}
}
} catch (err) {
console.error("❌ WS Message Error:", err);
}
};
ws.onerror = (error) => console.error('⚠️ WebSocket Error:', error);
ws.onclose = (e) => {
console.log(`🔌 Disconnected (Code: ${e.code}). Retrying in 2s...`);
setTimeout(connectWS, 2000);
};
};
connectWS();
return () => {
if (wsRef.current) wsRef.current.close();
};
}, []);
// Update active airport when airports list changes
useEffect(() => {
if (data.airports && data.airports.length > 0) {
setActiveAirport(prevActive => {
if (!prevActive || prevActive.name === "Loading...") {
return data.airports[0];
}
return data.airports.find(a => a.name === prevActive.name) || data.airports[0];
});
}
}, [data.airports]);
const handleFlightSelect = (flight) => setSelectedFlight(flight);
const handleSendCommand = (cmd) => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'command', payload: cmd }));
}
};
const handleCloseModal = () => setSelectedFlight(null);
const flightsList = data.aircrafts ? Object.values(data.aircrafts) : [];
return (
<div className="dashboard-container">
<AdminPanel
gameState={data}
airports={data.airports || []}
activeAirport={activeAirport}
activeAirportConfig={data.config}
onSelectAirport={(ap) => {
setActiveAirport(ap);
sendWSMessage('select_airport', { code: ap.airport_code });
}}
sendWSMessage={sendWSMessage}
draftingMode={draftingMode}
setDraftingMode={setDraftingMode}
airportName={airportName}
setAirportName={setAirportName}
isRunwayBidirectional={isRunwayBidirectional}
setIsRunwayBidirectional={setIsRunwayBidirectional}
starDraft={starDraft}
setStarDraft={setStarDraft}
sidDraft={sidDraft}
setSidDraft={setSidDraft}
/>
<RadarMap
flights={flightsList}
selectedFlight={selectedFlight}
onSelectFlight={handleFlightSelect}
airspace={data.airspace || { nodes: [], edges: [] }}
airports={data.airports || []}
activeAirport={activeAirport}
setActiveAirport={setActiveAirport}
activeAirportConfig={data.config}
activeRunways={data.active_runways || []}
windHeading={data.wind_heading || 0}
windSpeed={data.wind_speed || 0}
onSelectAirport={(ap) => {
setActiveAirport(ap);
sendWSMessage('select_airport', { code: ap.airport_code });
}}
sendWSMessage={sendWSMessage}
draftingMode={draftingMode}
setDraftingMode={setDraftingMode}
airportName={airportName}
setAirportName={setAirportName}
runwayPoints={runwayPoints}
setRunwayPoints={setRunwayPoints}
mousePos={mousePos}
setMousePos={setMousePos}
isRunwayBidirectional={isRunwayBidirectional}
starDraft={starDraft}
setStarDraft={setStarDraft}
setHoveredWaypoint={setHoveredWaypoint}
/>
<div className="sidebar-panel">
<div className="sidebar-roster-section">
<FlightRoster
gameState={data}
flights={flightsList}
onSelectFlight={handleFlightSelect}
sendWSMessage={sendWSMessage}
hoveredWaypoint={hoveredWaypoint}
/>
</div>
<div className="sidebar-routes-section">
<RouteSidebar gameState={data} />
</div>
</div>
<DevConsole actions={logs} onSendCommand={handleSendCommand} />
{/* Renders over everything when a flight is selected */}
<FlightModal
flight={selectedFlight}
onClose={handleCloseModal}
/>
</div>
);
}
export default App;