From 8e8bbe84f3868c5a06dc541f2128ffeb07ccf3b1 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 1 Mar 2022 20:42:59 -0800 Subject: [PATCH] Add websocket handling for selected flights. --- client/package-lock.json | 19 ++++++++++ client/package.json | 1 + client/src/App.tsx | 4 ++- client/src/api/backend.ts | 2 ++ client/src/api/eventstream.tsx | 36 +++++++++++++++++++ client/src/api/flightsSlice.ts | 18 ++++++++-- .../src/components/flightplan/FlightPlan.tsx | 13 ++++++- .../flightplanslayer/FlightPlansLayer.tsx | 25 +++++++++++-- .../socketprovider/socketprovider.tsx | 36 +++++++++++++++++++ client/src/hooks/useEventSteam.ts | 26 ++++++++++++++ .../{api => hooks}/useInitialGameState.tsx | 10 +++--- client/src/hooks/useSocket.ts | 8 +++++ client/src/index.tsx | 5 ++- 13 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 client/src/api/eventstream.tsx create mode 100644 client/src/components/socketprovider/socketprovider.tsx create mode 100644 client/src/hooks/useEventSteam.ts rename client/src/{api => hooks}/useInitialGameState.tsx (81%) create mode 100644 client/src/hooks/useSocket.ts diff --git a/client/package-lock.json b/client/package-lock.json index 5d2a7404..a43b9841 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index 7ef564a9..4b2ad76d 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/App.tsx b/client/src/App.tsx index 54bf3f0c..365062c3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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 (
diff --git a/client/src/api/backend.ts b/client/src/api/backend.ts index 7835e3b1..ca2e6133 100644 --- a/client/src/api/backend.ts +++ b/client/src/api/backend.ts @@ -4,4 +4,6 @@ export const backend = axios.create({ baseURL: "http://[::1]:5000/", }); +export const WEBSOCKET_URL = "ws://[::1]:5000/eventstream"; + export default backend; diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx new file mode 100644 index 00000000..41e815a9 --- /dev/null +++ b/client/src/api/eventstream.tsx @@ -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)); + } +}; diff --git a/client/src/api/flightsSlice.ts b/client/src/api/flightsSlice.ts index ff8fbea8..3897c3cb 100644 --- a/client/src/api/flightsSlice.ts +++ b/client/src/api/flightsSlice.ts @@ -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) => { + 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; diff --git a/client/src/components/flightplan/FlightPlan.tsx b/client/src/components/flightplan/FlightPlan.tsx index 0674c70e..7ef7375c 100644 --- a/client/src/components/flightplan/FlightPlan.tsx +++ b/client/src/components/flightplan/FlightPlan.tsx @@ -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 <>; diff --git a/client/src/components/flightplanslayer/FlightPlansLayer.tsx b/client/src/components/flightplanslayer/FlightPlansLayer.tsx index e094064c..253d7454 100644 --- a/client/src/components/flightplanslayer/FlightPlansLayer.tsx +++ b/client/src/components/flightplanslayer/FlightPlansLayer.tsx @@ -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 ? ( + + ) : ( + <> + ); + return ( - {Object.values(flights).map((flight) => { - return ; - })} + {Object.values(flights) + .filter(isNotSelected) + .map((flight) => { + return ( + + ); + })} + {selectedFlight} ); } diff --git a/client/src/components/socketprovider/socketprovider.tsx b/client/src/components/socketprovider/socketprovider.tsx new file mode 100644 index 00000000..9b5e48b3 --- /dev/null +++ b/client/src/components/socketprovider/socketprovider.tsx @@ -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(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 ( + {props.children} + ); +}; diff --git a/client/src/hooks/useEventSteam.ts b/client/src/hooks/useEventSteam.ts new file mode 100644 index 00000000..1545d2e8 --- /dev/null +++ b/client/src/hooks/useEventSteam.ts @@ -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; diff --git a/client/src/api/useInitialGameState.tsx b/client/src/hooks/useInitialGameState.tsx similarity index 81% rename from client/src/api/useInitialGameState.tsx rename to client/src/hooks/useInitialGameState.tsx index dac752f2..9e7f827d 100644 --- a/client/src/api/useInitialGameState.tsx +++ b/client/src/hooks/useInitialGameState.tsx @@ -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"; diff --git a/client/src/hooks/useSocket.ts b/client/src/hooks/useSocket.ts new file mode 100644 index 00000000..06176bb3 --- /dev/null +++ b/client/src/hooks/useSocket.ts @@ -0,0 +1,8 @@ +import { SocketContext } from "../components/socketprovider/socketprovider"; +import { useContext } from "react"; + +export const useSocket = () => { + const socket = useContext(SocketContext); + + return socket; +}; diff --git a/client/src/index.tsx b/client/src/index.tsx index 3468b9c5..5e231cac 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -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( - + + + , document.getElementById("root")