Partial implementation of TGO display.

No threat/detection circles yet.

https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
Dan Albert 2022-03-02 00:57:58 -08:00
parent 1cd77a4a77
commit 64b01c471b
13 changed files with 216 additions and 0 deletions

23
client/src/api/tgo.ts Normal file
View File

@ -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;

View File

@ -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<Tgo[]>) => {
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;

View File

@ -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,
},
});

View File

@ -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) {
<LayersControl.Overlay name="Control points" checked>
<ControlPointsLayer />
</LayersControl.Overlay>
{Object.values(TgoType).map((type) => {
return (
<LayersControl.Overlay name={type} checked>
<TgosLayer type={type as TgoType} />
</LayersControl.Overlay>
);
})}
<LayersControl.Overlay name="All blue flight plans" checked>
<FlightPlansLayer blue={true} />
</LayersControl.Overlay>

View File

@ -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 (
<Marker position={props.tgo.position} icon={iconForTgo(props.tgo)}>
<Popup>
{`${props.tgo.name} (${props.tgo.control_point_name})`}
<br />
{props.tgo.units.map((unit) => (
<>
{unit}
<br />
</>
))}
</Popup>
</Marker>
);
}

View File

@ -0,0 +1 @@
export { default } from "./Tgo";

View File

@ -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 (
<LayerGroup>
{tgos.map((tgo) => {
return <Tgo key={tgo.name} tgo={tgo} />;
})}
</LayerGroup>
);
}

View File

@ -0,0 +1 @@
export { default } from "./TgosLayer";

View File

@ -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}`))

View File

@ -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)

View File

@ -0,0 +1 @@
from .routes import router

View File

@ -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()),
)

View File

@ -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