Add websocket handling for selected flights.

This commit is contained in:
Dan Albert 2022-03-01 20:42:59 -08:00
parent 6d29bfdf65
commit 8e8bbe84f3
13 changed files with 190 additions and 13 deletions

View File

@ -32,6 +32,7 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/leaflet": "^1.7.9",
"@types/websocket": "^1.0.5",
"electron": "^17.1.0",
"electron-is-dev": "^2.0.0",
"react-scripts": "5.0.0",
@ -4219,6 +4220,15 @@
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
"dev": true
},
"node_modules/@types/websocket": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz",
"integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ws": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
@ -21771,6 +21781,15 @@
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
"dev": true
},
"@types/websocket": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz",
"integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/ws": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",

View File

@ -50,6 +50,7 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/leaflet": "^1.7.9",
"@types/websocket": "^1.0.5",
"electron": "^17.1.0",
"electron-is-dev": "^2.0.0",
"react-scripts": "5.0.0",

View File

@ -2,12 +2,14 @@ import "./App.css";
import { LatLng } from "leaflet";
import LiberationMap from "./components/liberationmap";
import useInitialGameState from "./api/useInitialGameState";
import useEventStream from "./hooks/useEventSteam";
import useInitialGameState from "./hooks/useInitialGameState";
function App() {
const mapCenter: LatLng = new LatLng(25.58, 54.9);
useInitialGameState();
useEventStream();
return (
<div className="App">

View File

@ -4,4 +4,6 @@ export const backend = axios.create({
baseURL: "http://[::1]:5000/",
});
export const WEBSOCKET_URL = "ws://[::1]:5000/eventstream";
export default backend;

View File

@ -0,0 +1,36 @@
import { deselectFlight, selectFlight } from "./flightsSlice";
import { AppDispatch } from "../app/store";
import { Flight } from "./flight";
import { LatLng } from "leaflet";
// Placeholder. We don't use this yet. This is just here so we can flesh out the
// update events model.
interface FrozenCombat {}
interface GameUpdateEvents {
updated_flight_positions: { [id: string]: LatLng };
new_combats: FrozenCombat[];
updated_combats: FrozenCombat[];
ended_combats: string[];
navmesh_updates: boolean[];
unculled_zones_updated: boolean;
threat_zones_updated: boolean;
new_flights: Flight[];
updated_flights: string[];
deleted_flights: string[];
selected_flight: string | null;
deselected_flight: boolean;
}
export const handleStreamedEvents = (
dispatch: AppDispatch,
events: GameUpdateEvents
) => {
if (events.deselected_flight) {
dispatch(deselectFlight());
}
if (events.selected_flight != null) {
dispatch(selectFlight(events.selected_flight));
}
};

View File

@ -6,11 +6,13 @@ import { RootState } from "../app/store";
interface FlightsState {
blue: { [id: string]: Flight };
red: { [id: string]: Flight };
selected: Flight | null;
}
const initialState: FlightsState = {
blue: {},
red: {},
selected: null,
};
export const flightsSlice = createSlice({
@ -42,11 +44,23 @@ export const flightsSlice = createSlice({
);
}
},
deselectFlight: (state) => {
state.selected = null;
},
selectFlight: (state, action: PayloadAction<string>) => {
const id = action.payload;
state.selected = state.blue[id];
},
},
});
export const { clearFlights, registerFlight, unregisterFlight } =
flightsSlice.actions;
export const {
clearFlights,
registerFlight,
unregisterFlight,
deselectFlight,
selectFlight,
} = flightsSlice.actions;
export const selectFlights = (state: RootState) => state.flights;

View File

@ -3,14 +3,25 @@ import { Polyline } from "react-leaflet";
const BLUE_PATH = "#0084ff";
const RED_PATH = "#c85050";
const SELECTED_PATH = "#ffff00";
interface FlightPlanProps {
flight: Flight;
selected: boolean;
}
const pathColor = (props: FlightPlanProps) => {
if (props.selected) {
return SELECTED_PATH;
} else if (props.flight.blue) {
return BLUE_PATH;
} else {
return RED_PATH;
}
};
function FlightPlanPath(props: FlightPlanProps) {
const color = props.flight.blue ? BLUE_PATH : RED_PATH;
const color = pathColor(props);
const waypoints = props.flight.waypoints;
if (waypoints == null) {
return <></>;

View File

@ -1,3 +1,4 @@
import { Flight } from "../../api/flight";
import FlightPlan from "../flightplan";
import { LayerGroup } from "react-leaflet";
import { selectFlights } from "../../api/flightsSlice";
@ -10,11 +11,29 @@ interface FlightPlansLayerProps {
export default function FlightPlansLayer(props: FlightPlansLayerProps) {
const atos = useAppSelector(selectFlights);
const flights = props.blue ? atos.blue : atos.red;
const isNotSelected = (flight: Flight) => {
if (atos.selected == null) {
return true;
}
return atos.selected.id !== flight.id;
};
const selectedFlight = atos.selected ? (
<FlightPlan key={atos.selected.id} flight={atos.selected} selected={true} />
) : (
<></>
);
return (
<LayerGroup>
{Object.values(flights).map((flight) => {
return <FlightPlan key={flight.id} flight={flight} selected={false} />;
{Object.values(flights)
.filter(isNotSelected)
.map((flight) => {
return (
<FlightPlan key={flight.id} flight={flight} selected={false} />
);
})}
{selectedFlight}
</LayerGroup>
);
}

View File

@ -0,0 +1,36 @@
import { ReactChild, createContext, useEffect, useState } from "react";
import { WEBSOCKET_URL } from "../../api/backend";
const socket = new WebSocket(WEBSOCKET_URL);
export const SocketContext = createContext(socket);
interface SocketProviderProps {
children: ReactChild;
}
export const SocketProvider = (props: SocketProviderProps) => {
const [ws, setWs] = useState<WebSocket>(socket);
useEffect(() => {
const onClose = () => {
setWs(new WebSocket(WEBSOCKET_URL));
};
const onError = (error: Event) => {
console.log(`Websocket error: ${error}`);
};
ws.addEventListener("close", onClose);
ws.addEventListener("error", onError);
return () => {
ws.removeEventListener("close", onClose);
ws.removeEventListener("error", onError);
};
});
return (
<SocketContext.Provider value={ws}>{props.children}</SocketContext.Provider>
);
};

View File

@ -0,0 +1,26 @@
import { useCallback, useEffect } from "react";
import { handleStreamedEvents } from "../api/eventstream";
import { useAppDispatch } from "../app/hooks";
import { useSocket } from "./useSocket";
export const useEventStream = () => {
const ws = useSocket();
const dispatch = useAppDispatch();
const onMessage = useCallback(
(message) => {
handleStreamedEvents(dispatch, JSON.parse(message.data));
},
[dispatch]
);
useEffect(() => {
ws.addEventListener("message", onMessage);
return () => {
ws.removeEventListener("message", onMessage);
};
});
};
export default useEventStream;

View File

@ -1,8 +1,8 @@
import { ControlPoint } from "./controlpoint";
import { Flight } from "./flight";
import backend from "./backend";
import { registerFlight } from "./flightsSlice";
import { setControlPoints } from "./controlPointsSlice";
import { ControlPoint } from "../api/controlpoint";
import { Flight } from "../api/flight";
import backend from "../api/backend";
import { registerFlight } from "../api/flightsSlice";
import { setControlPoints } from "../api/controlPointsSlice";
import { useAppDispatch } from "../app/hooks";
import { useEffect } from "react";

View File

@ -0,0 +1,8 @@
import { SocketContext } from "../components/socketprovider/socketprovider";
import { useContext } from "react";
export const useSocket = () => {
const socket = useContext(SocketContext);
return socket;
};

View File

@ -6,12 +6,15 @@ import App from "./App";
import { Provider } from "react-redux";
import React from "react";
import ReactDOM from "react-dom";
import { SocketProvider } from "./components/socketprovider/socketprovider";
import { store } from "./app/store";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<SocketProvider>
<App />
</SocketProvider>
</Provider>
</React.StrictMode>,
document.getElementById("root")