Spaces:
Sleeping
Sleeping
| 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; | |