From 64b01c471b0fda306d8f91cd5e283010d8f15ef4 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 2 Mar 2022 00:57:58 -0800 Subject: [PATCH] Partial implementation of TGO display. No threat/detection circles yet. https://github.com/dcs-liberation/dcs_liberation/issues/2039 --- client/src/api/tgo.ts | 23 +++++++++ client/src/api/tgosSlice.ts | 51 +++++++++++++++++++ client/src/app/store.ts | 2 + .../liberationmap/LiberationMap.tsx | 9 ++++ client/src/components/tgos/Tgo.tsx | 37 ++++++++++++++ client/src/components/tgos/index.ts | 1 + client/src/components/tgoslayer/TgosLayer.tsx | 22 ++++++++ client/src/components/tgoslayer/index.ts | 1 + client/src/hooks/useInitialGameState.tsx | 10 ++++ game/server/app.py | 2 + game/server/tgos/__init__.py | 1 + game/server/tgos/models.py | 40 +++++++++++++++ game/server/tgos/routes.py | 17 +++++++ 13 files changed, 216 insertions(+) create mode 100644 client/src/api/tgo.ts create mode 100644 client/src/api/tgosSlice.ts create mode 100644 client/src/components/tgos/Tgo.tsx create mode 100644 client/src/components/tgos/index.ts create mode 100644 client/src/components/tgoslayer/TgosLayer.tsx create mode 100644 client/src/components/tgoslayer/index.ts create mode 100644 game/server/tgos/__init__.py create mode 100644 game/server/tgos/models.py create mode 100644 game/server/tgos/routes.py diff --git a/client/src/api/tgo.ts b/client/src/api/tgo.ts new file mode 100644 index 00000000..d8abd3e2 --- /dev/null +++ b/client/src/api/tgo.ts @@ -0,0 +1,23 @@ +import { LatLng } from "leaflet"; + +export enum TgoType { + AIR_DEFENSE = "Air defenses", + FACTORY = "Factories", + SHIP = "Ships", + OTHER = "Other ground objects", +} + +export interface Tgo { + name: string; + control_point_name: string; + category: string; + blue: boolean; + position: LatLng; + units: string[]; + threat_ranges: number[]; + detection_ranges: number[]; + dead: boolean; + sidc: string; +} + +export default Tgo; diff --git a/client/src/api/tgosSlice.ts b/client/src/api/tgosSlice.ts new file mode 100644 index 00000000..0bc8bcc8 --- /dev/null +++ b/client/src/api/tgosSlice.ts @@ -0,0 +1,51 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { Tgo, TgoType } from "./tgo"; + +import { RootState } from "../app/store"; + +interface TgosState { + tgosByType: { [key: string]: Tgo[] }; +} + +const initialState: TgosState = { + tgosByType: Object.fromEntries( + Object.values(TgoType).map((key) => [key, []]) + ), +}; + +export const tgosSlice = createSlice({ + name: "tgos", + initialState, + reducers: { + setTgos: (state, action: PayloadAction) => { + state.tgosByType = initialState.tgosByType; + for (const key of Object.values(TgoType)) { + state.tgosByType[key] = []; + } + for (const tgo of action.payload) { + var type; + switch (tgo.category) { + case "aa": + type = TgoType.AIR_DEFENSE; + break; + case "factory": + type = TgoType.FACTORY; + break; + case "ship": + type = TgoType.SHIP; + break; + default: + type = TgoType.OTHER; + break; + } + state.tgosByType[type].push(tgo); + } + }, + }, +}); + +export const { setTgos } = tgosSlice.actions; + +export const selectTgos = (state: RootState) => state.tgos; + +export default tgosSlice.reducer; diff --git a/client/src/app/store.ts b/client/src/app/store.ts index baeca6dc..99c01f00 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -2,11 +2,13 @@ import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; import controlPointsReducer from "../api/controlPointsSlice"; import flightsReducer from "../api/flightsSlice"; +import tgosReducer from "../api/tgosSlice"; export const store = configureStore({ reducer: { flights: flightsReducer, controlPoints: controlPointsReducer, + tgos: tgosReducer, }, }); diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx index caa5f654..829e42a8 100644 --- a/client/src/components/liberationmap/LiberationMap.tsx +++ b/client/src/components/liberationmap/LiberationMap.tsx @@ -6,6 +6,8 @@ import { BasemapLayer } from "react-esri-leaflet"; import ControlPointsLayer from "../controlpointslayer"; import FlightPlansLayer from "../flightplanslayer"; import { LatLng } from "leaflet"; +import { TgoType } from "../../api/tgo"; +import TgosLayer from "../tgoslayer/TgosLayer"; interface GameProps { mapCenter: LatLng; @@ -28,6 +30,13 @@ export default function LiberationMap(props: GameProps) { + {Object.values(TgoType).map((type) => { + return ( + + + + ); + })} diff --git a/client/src/components/tgos/Tgo.tsx b/client/src/components/tgos/Tgo.tsx new file mode 100644 index 00000000..a4c34cef --- /dev/null +++ b/client/src/components/tgos/Tgo.tsx @@ -0,0 +1,37 @@ +import { Icon, Point } from "leaflet"; +import { Marker, Popup } from "react-leaflet"; + +import { Symbol as MilSymbol } from "milsymbol"; +import { Tgo as TgoModel } from "../../api/tgo"; + +function iconForTgo(cp: TgoModel) { + const symbol = new MilSymbol(cp.sidc, { + size: 24, + }); + + return new Icon({ + iconUrl: symbol.toDataURL(), + iconAnchor: new Point(symbol.getAnchor().x, symbol.getAnchor().y), + }); +} + +interface TgoProps { + tgo: TgoModel; +} + +export default function Tgo(props: TgoProps) { + return ( + + + {`${props.tgo.name} (${props.tgo.control_point_name})`} +
+ {props.tgo.units.map((unit) => ( + <> + {unit} +
+ + ))} +
+
+ ); +} diff --git a/client/src/components/tgos/index.ts b/client/src/components/tgos/index.ts new file mode 100644 index 00000000..3553f868 --- /dev/null +++ b/client/src/components/tgos/index.ts @@ -0,0 +1 @@ +export { default } from "./Tgo"; diff --git a/client/src/components/tgoslayer/TgosLayer.tsx b/client/src/components/tgoslayer/TgosLayer.tsx new file mode 100644 index 00000000..1f996e2d --- /dev/null +++ b/client/src/components/tgoslayer/TgosLayer.tsx @@ -0,0 +1,22 @@ +import { LayerGroup } from "react-leaflet"; +import Tgo from "../tgos/Tgo"; +import { TgoType } from "../../api/tgo"; +import { selectTgos } from "../../api/tgosSlice"; +import { useAppSelector } from "../../app/hooks"; + +interface TgosLayerProps { + type: TgoType; +} + +export default function TgosLayer(props: TgosLayerProps) { + const allTgos = useAppSelector(selectTgos); + const tgos = allTgos.tgosByType[props.type]; + console.dir(Object.entries(TgoType)); + return ( + + {tgos.map((tgo) => { + return ; + })} + + ); +} diff --git a/client/src/components/tgoslayer/index.ts b/client/src/components/tgoslayer/index.ts new file mode 100644 index 00000000..282c0a83 --- /dev/null +++ b/client/src/components/tgoslayer/index.ts @@ -0,0 +1 @@ +export { default } from "./TgosLayer"; diff --git a/client/src/hooks/useInitialGameState.tsx b/client/src/hooks/useInitialGameState.tsx index 9e7f827d..ca1d06ec 100644 --- a/client/src/hooks/useInitialGameState.tsx +++ b/client/src/hooks/useInitialGameState.tsx @@ -1,8 +1,10 @@ import { ControlPoint } from "../api/controlpoint"; import { Flight } from "../api/flight"; +import Tgo from "../api/tgo"; import backend from "../api/backend"; import { registerFlight } from "../api/flightsSlice"; import { setControlPoints } from "../api/controlPointsSlice"; +import { setTgos } from "../api/tgosSlice"; import { useAppDispatch } from "../app/hooks"; import { useEffect } from "react"; @@ -21,6 +23,14 @@ export const useInitialGameState = () => { dispatch(setControlPoints(response.data as ControlPoint[])); } }); + backend + .get("/tgos") + .catch((error) => console.log(`Error fetching TGOs: ${error}`)) + .then((response) => { + if (response != null) { + dispatch(setTgos(response.data as Tgo[])); + } + }); backend .get("/flights?with_waypoints=true") .catch((error) => console.log(`Error fetching flights: ${error}`)) diff --git a/game/server/app.py b/game/server/app.py index 6b3e4f67..55d17be4 100644 --- a/game/server/app.py +++ b/game/server/app.py @@ -8,6 +8,7 @@ from . import ( flights, mapzones, navmesh, + tgos, waypoints, ) from .security import ApiKeyManager @@ -24,6 +25,7 @@ app.include_router(eventstream.router) app.include_router(flights.router) app.include_router(mapzones.router) app.include_router(navmesh.router) +app.include_router(tgos.router) app.include_router(waypoints.router) diff --git a/game/server/tgos/__init__.py b/game/server/tgos/__init__.py new file mode 100644 index 00000000..3a27ef1c --- /dev/null +++ b/game/server/tgos/__init__.py @@ -0,0 +1 @@ +from .routes import router diff --git a/game/server/tgos/models.py b/game/server/tgos/models.py new file mode 100644 index 00000000..a7b44e8b --- /dev/null +++ b/game/server/tgos/models.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from pydantic import BaseModel + +from game.server.leaflet import LeafletPoint +from game.theater import TheaterGroundObject + + +class TgoJs(BaseModel): + name: str + control_point_name: str + category: str + blue: bool + position: LeafletPoint + units: list[str] + threat_ranges: list[float] + detection_ranges: list[float] + dead: bool + sidc: str + + @staticmethod + def for_tgo(tgo: TheaterGroundObject) -> TgoJs: + if not tgo.might_have_aa: + threat_ranges = [] + detection_ranges = [] + else: + threat_ranges = [tgo.threat_range(group).meters for group in tgo.groups] + detection_ranges = [tgo.threat_range(group).meters for group in tgo.groups] + return TgoJs( + name=tgo.name, + control_point_name=tgo.control_point.name, + category=tgo.category, + blue=tgo.control_point.captured, + position=tgo.position.latlng(), + units=[unit.display_name for unit in tgo.units], + threat_ranges=threat_ranges, + detection_ranges=detection_ranges, + dead=tgo.is_dead, + sidc=str(tgo.sidc()), + ) diff --git a/game/server/tgos/routes.py b/game/server/tgos/routes.py new file mode 100644 index 00000000..d9fc9986 --- /dev/null +++ b/game/server/tgos/routes.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, Depends + +from game import Game +from .models import TgoJs +from ..dependencies import GameContext + +router: APIRouter = APIRouter(prefix="/tgos") + + +@router.get("/") +def list_tgos(game: Game = Depends(GameContext.get)) -> list[TgoJs]: + tgos = [] + for control_point in game.theater.controlpoints: + for tgo in control_point.connected_objectives: + if not tgo.is_control_point: + tgos.append(TgoJs.for_tgo(tgo)) + return tgos