Update the react map for some new events.

This commit is contained in:
Dan Albert 2022-03-03 23:31:07 -08:00
parent 4539e91fa9
commit 92236a5bc3
16 changed files with 145 additions and 65 deletions

View File

@ -4,11 +4,11 @@ import { ControlPoint } from "./controlpoint";
import { RootState } from "../app/store"; import { RootState } from "../app/store";
interface ControlPointsState { interface ControlPointsState {
controlPoints: ControlPoint[]; controlPoints: { [key: number]: ControlPoint };
} }
const initialState: ControlPointsState = { const initialState: ControlPointsState = {
controlPoints: [], controlPoints: {},
}; };
export const controlPointsSlice = createSlice({ export const controlPointsSlice = createSlice({
@ -16,12 +16,20 @@ export const controlPointsSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setControlPoints: (state, action: PayloadAction<ControlPoint[]>) => { setControlPoints: (state, action: PayloadAction<ControlPoint[]>) => {
state.controlPoints = action.payload; state.controlPoints = {};
for (const cp of action.payload) {
state.controlPoints[cp.id] = cp;
}
},
updateControlPoint: (state, action: PayloadAction<ControlPoint>) => {
const cp = action.payload;
state.controlPoints[cp.id] = cp;
}, },
}, },
}); });
export const { setControlPoints } = controlPointsSlice.actions; export const { setControlPoints, updateControlPoint } =
controlPointsSlice.actions;
export const selectControlPoints = (state: RootState) => state.controlPoints; export const selectControlPoints = (state: RootState) => state.controlPoints;

View File

@ -1,8 +1,14 @@
import { deselectFlight, selectFlight } from "./flightsSlice"; import { deselectFlight, selectFlight } from "./flightsSlice";
import { AppDispatch } from "../app/store"; import { AppDispatch } from "../app/store";
import { ControlPoint } from "./controlpoint";
import { Flight } from "./flight"; import { Flight } from "./flight";
import FrontLine from "./frontline";
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import Tgo from "./tgo";
import backend from "./backend";
import { updateControlPoint } from "./controlPointsSlice";
import { updateTgo } from "./tgosSlice";
// Placeholder. We don't use this yet. This is just here so we can flesh out the // Placeholder. We don't use this yet. This is just here so we can flesh out the
// update events model. // update events model.
@ -21,6 +27,11 @@ interface GameUpdateEvents {
deleted_flights: string[]; deleted_flights: string[];
selected_flight: string | null; selected_flight: string | null;
deselected_flight: boolean; deselected_flight: boolean;
new_front_lines: FrontLine[];
updated_front_lines: string[];
deleted_front_lines: string[];
updated_tgos: string[];
updated_control_points: number[];
} }
export const handleStreamedEvents = ( export const handleStreamedEvents = (
@ -33,4 +44,16 @@ export const handleStreamedEvents = (
if (events.selected_flight != null) { if (events.selected_flight != null) {
dispatch(selectFlight(events.selected_flight)); dispatch(selectFlight(events.selected_flight));
} }
for (const id of events.updated_tgos) {
backend.get(`/tgos/${id}`).then((response) => {
const tgo = response.data as Tgo;
dispatch(updateTgo(tgo));
});
}
for (const id of events.updated_control_points) {
backend.get(`/control-points/${id}`).then((response) => {
const cp = response.data as ControlPoint;
dispatch(updateControlPoint(cp));
});
}
}; };

View File

@ -1,12 +1,5 @@
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
export enum TgoType {
AIR_DEFENSE = "Air defenses",
FACTORY = "Factories",
SHIP = "Ships",
OTHER = "Other ground objects",
}
export interface Tgo { export interface Tgo {
id: string; id: string;
name: string; name: string;

View File

@ -1,16 +1,14 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { Tgo, TgoType } from "./tgo";
import { RootState } from "../app/store"; import { RootState } from "../app/store";
import { Tgo } from "./tgo";
interface TgosState { interface TgosState {
tgosByType: { [key: string]: Tgo[] }; tgos: { [key: string]: Tgo };
} }
const initialState: TgosState = { const initialState: TgosState = {
tgosByType: Object.fromEntries( tgos: {},
Object.values(TgoType).map((key) => [key, []])
),
}; };
export const tgosSlice = createSlice({ export const tgosSlice = createSlice({
@ -18,33 +16,19 @@ export const tgosSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setTgos: (state, action: PayloadAction<Tgo[]>) => { setTgos: (state, action: PayloadAction<Tgo[]>) => {
state.tgosByType = initialState.tgosByType; state.tgos = {};
for (const key of Object.values(TgoType)) {
state.tgosByType[key] = [];
}
for (const tgo of action.payload) { for (const tgo of action.payload) {
var type; state.tgos[tgo.id] = tgo;
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);
} }
}, },
updateTgo: (state, action: PayloadAction<Tgo>) => {
const tgo = action.payload;
state.tgos[tgo.id] = tgo;
},
}, },
}); });
export const { setTgos } = tgosSlice.actions; export const { setTgos, updateTgo } = tgosSlice.actions;
export const selectTgos = (state: RootState) => state.tgos; export const selectTgos = (state: RootState) => state.tgos;

View File

@ -49,19 +49,12 @@ interface AirDefenseRangeLayerProps {
} }
export const AirDefenseRangeLayer = (props: AirDefenseRangeLayerProps) => { export const AirDefenseRangeLayer = (props: AirDefenseRangeLayerProps) => {
const tgos = useAppSelector(selectTgos); const tgos = Object.values(useAppSelector(selectTgos).tgos);
var allTgos: Tgo[] = []; var tgosForSide = tgos.filter((tgo) => tgo.blue === props.blue);
for (const tgoType of Object.values(tgos.tgosByType)) {
for (const tgo of tgoType) {
if (tgo.blue === props.blue) {
allTgos.push(tgo);
}
}
}
return ( return (
<LayerGroup> <LayerGroup>
{allTgos.map((tgo) => { {tgosForSide.map((tgo) => {
return ( return (
<TgoRangeCircles key={tgo.id} tgo={tgo} {...props}></TgoRangeCircles> <TgoRangeCircles key={tgo.id} tgo={tgo} {...props}></TgoRangeCircles>
); );

View File

@ -3,6 +3,7 @@ import { Marker, Tooltip } from "react-leaflet";
import { ControlPoint as ControlPointModel } from "../../api/controlpoint"; import { ControlPoint as ControlPointModel } from "../../api/controlpoint";
import { Symbol as MilSymbol } from "milsymbol"; import { Symbol as MilSymbol } from "milsymbol";
import backend from "../../api/backend";
function iconForControlPoint(cp: ControlPointModel) { function iconForControlPoint(cp: ControlPointModel) {
const symbol = new MilSymbol(cp.sidc, { const symbol = new MilSymbol(cp.sidc, {
@ -29,6 +30,16 @@ export default function ControlPoint(props: ControlPointProps) {
// other markers are helpful so we want to keep them, but make sure the CP // other markers are helpful so we want to keep them, but make sure the CP
// is always the clickable thing. // is always the clickable thing.
zIndexOffset={1000} zIndexOffset={1000}
eventHandlers={{
click: () => {
backend.post(`/qt/info/control-point/${props.controlPoint.id}`);
},
contextmenu: () => {
backend.post(
`/qt/create-package/control-point/${props.controlPoint.id}`
);
},
}}
> >
<Tooltip> <Tooltip>
<h3 style={{ margin: 0 }}>{props.controlPoint.name}</h3> <h3 style={{ margin: 0 }}>{props.controlPoint.name}</h3>

View File

@ -7,7 +7,7 @@ export default function ControlPointsLayer() {
const controlPoints = useAppSelector(selectControlPoints); const controlPoints = useAppSelector(selectControlPoints);
return ( return (
<LayerGroup> <LayerGroup>
{controlPoints.controlPoints.map((controlPoint) => { {Object.values(controlPoints.controlPoints).map((controlPoint) => {
return ( return (
<ControlPoint key={controlPoint.name} controlPoint={controlPoint} /> <ControlPoint key={controlPoint.name} controlPoint={controlPoint} />
); );

View File

@ -9,7 +9,6 @@ import FlightPlansLayer from "../flightplanslayer";
import FrontLinesLayer from "../frontlineslayer"; import FrontLinesLayer from "../frontlineslayer";
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import SupplyRoutesLayer from "../supplyrouteslayer"; import SupplyRoutesLayer from "../supplyrouteslayer";
import { TgoType } from "../../api/tgo";
import TgosLayer from "../tgoslayer/TgosLayer"; import TgosLayer from "../tgoslayer/TgosLayer";
interface GameProps { interface GameProps {
@ -33,13 +32,18 @@ export default function LiberationMap(props: GameProps) {
<LayersControl.Overlay name="Control points" checked> <LayersControl.Overlay name="Control points" checked>
<ControlPointsLayer /> <ControlPointsLayer />
</LayersControl.Overlay> </LayersControl.Overlay>
{Object.values(TgoType).map((type, idx) => { <LayersControl.Overlay name="Air defenses" checked>
return ( <TgosLayer categories={["aa"]} />
<LayersControl.Overlay key={idx} name={type} checked> </LayersControl.Overlay>
<TgosLayer type={type as TgoType} /> <LayersControl.Overlay name="Factories" checked>
</LayersControl.Overlay> <TgosLayer categories={["factory"]} />
); </LayersControl.Overlay>
})} <LayersControl.Overlay name="Ships" checked>
<TgosLayer categories={["ship"]} />
</LayersControl.Overlay>
<LayersControl.Overlay name="Other ground objects" checked>
<TgosLayer categories={["aa", "factories", "ships"]} exclude />
</LayersControl.Overlay>
<LayersControl.Overlay name="Supply routes" checked> <LayersControl.Overlay name="Supply routes" checked>
<SupplyRoutesLayer /> <SupplyRoutesLayer />
</LayersControl.Overlay> </LayersControl.Overlay>

View File

@ -1,16 +1,19 @@
import { LayerGroup } from "react-leaflet"; import { LayerGroup } from "react-leaflet";
import Tgo from "../tgos/Tgo"; import Tgo from "../tgos/Tgo";
import { TgoType } from "../../api/tgo";
import { selectTgos } from "../../api/tgosSlice"; import { selectTgos } from "../../api/tgosSlice";
import { useAppSelector } from "../../app/hooks"; import { useAppSelector } from "../../app/hooks";
interface TgosLayerProps { interface TgosLayerProps {
type: TgoType; categories?: string[];
exclude?: true;
} }
export default function TgosLayer(props: TgosLayerProps) { export default function TgosLayer(props: TgosLayerProps) {
const allTgos = useAppSelector(selectTgos); const allTgos = Object.values(useAppSelector(selectTgos).tgos);
const tgos = allTgos.tgosByType[props.type]; const categoryFilter = props.categories ?? [];
const tgos = allTgos.filter(
(tgo) => categoryFilter.includes(tgo.category) === (props.exclude ?? false)
);
return ( return (
<LayerGroup> <LayerGroup>
{tgos.map((tgo) => { {tgos.map((tgo) => {

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException, status
from game import Game from game import Game
from .models import ControlPointJs from .models import ControlPointJs
@ -13,3 +13,16 @@ def list_control_points(game: Game = Depends(GameContext.get)) -> list[ControlPo
for control_point in game.theater.controlpoints: for control_point in game.theater.controlpoints:
control_points.append(ControlPointJs.for_control_point(control_point)) control_points.append(ControlPointJs.for_control_point(control_point))
return control_points return control_points
@router.get("/{cp_id}")
def get_control_point(
cp_id: int, game: Game = Depends(GameContext.get)
) -> ControlPointJs:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail=f"Game has no control point with ID {cp_id}",
)
return ControlPointJs.for_control_point(cp)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Callable, TYPE_CHECKING from typing import Callable, TYPE_CHECKING
from game.theater import MissionTarget, TheaterGroundObject from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -32,9 +32,11 @@ class QtCallbacks:
self, self,
create_new_package: Callable[[MissionTarget], None], create_new_package: Callable[[MissionTarget], None],
show_tgo_info: Callable[[TheaterGroundObject], None], show_tgo_info: Callable[[TheaterGroundObject], None],
show_control_point_info: Callable[[ControlPoint], None],
) -> None: ) -> None:
self.create_new_package = create_new_package self.create_new_package = create_new_package
self.show_tgo_info = show_tgo_info self.show_tgo_info = show_tgo_info
self.show_control_point_info = show_control_point_info
class QtContext: class QtContext:

View File

@ -32,6 +32,7 @@ class GameUpdateEventsJs(BaseModel):
updated_front_lines: set[UUID] updated_front_lines: set[UUID]
deleted_front_lines: set[UUID] deleted_front_lines: set[UUID]
updated_tgos: set[UUID] updated_tgos: set[UUID]
updated_control_points: set[int]
@classmethod @classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
@ -64,4 +65,5 @@ class GameUpdateEventsJs(BaseModel):
updated_front_lines=events.updated_front_lines, updated_front_lines=events.updated_front_lines,
deleted_front_lines=events.deleted_front_lines, deleted_front_lines=events.deleted_front_lines,
updated_tgos=events.updated_tgos, updated_tgos=events.updated_tgos,
updated_control_points=events.updated_control_points,
) )

View File

@ -1,6 +1,6 @@
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException, status
from game import Game from game import Game
from ..dependencies import GameContext, QtCallbacks, QtContext from ..dependencies import GameContext, QtCallbacks, QtContext
@ -33,3 +33,33 @@ def show_tgo_info(
qt: QtCallbacks = Depends(QtContext.get), qt: QtCallbacks = Depends(QtContext.get),
) -> None: ) -> None:
qt.show_tgo_info(game.db.tgos.get(tgo_id)) qt.show_tgo_info(game.db.tgos.get(tgo_id))
@router.post("/create-package/control-point/{cp_id}")
def new_cp_package(
cp_id: int,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail=f"Game has no control point with ID {cp_id}",
)
qt.create_new_package(cp)
@router.post("/info/control-point/{cp_id}")
def show_control_point_info(
cp_id: int,
game: Game = Depends(GameContext.get),
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
cp = game.theater.find_control_point_by_id(cp_id)
if cp is None:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail=f"Game has no control point with ID {cp_id}",
)
qt.show_control_point_info(cp)

View File

@ -9,7 +9,7 @@ from dcs import Point
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight, Package from game.ato import Flight, Package
from game.sim.combat import FrozenCombat from game.sim.combat import FrozenCombat
from game.theater import FrontLine, TheaterGroundObject from game.theater import ControlPoint, FrontLine, TheaterGroundObject
@dataclass @dataclass
@ -31,6 +31,7 @@ class GameUpdateEvents:
updated_front_lines: set[UUID] = field(default_factory=set) updated_front_lines: set[UUID] = field(default_factory=set)
deleted_front_lines: set[UUID] = field(default_factory=set) deleted_front_lines: set[UUID] = field(default_factory=set)
updated_tgos: set[UUID] = field(default_factory=set) updated_tgos: set[UUID] = field(default_factory=set)
updated_control_points: set[int] = field(default_factory=set)
shutting_down: bool = False shutting_down: bool = False
@property @property
@ -116,6 +117,10 @@ class GameUpdateEvents:
self.updated_tgos.add(tgo.id) self.updated_tgos.add(tgo.id)
return self return self
def update_control_point(self, control_point: ControlPoint) -> GameUpdateEvents:
self.updated_control_points.add(control_point.id)
return self
def shut_down(self) -> GameUpdateEvents: def shut_down(self) -> GameUpdateEvents:
self.shutting_down = True self.shutting_down = True
return self return self

View File

@ -748,6 +748,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self._clear_front_lines(events) self._clear_front_lines(events)
self._create_missing_front_lines(events) self._create_missing_front_lines(events)
events.update_control_point(self)
@property @property
def required_aircraft_start_type(self) -> Optional[StartType]: def required_aircraft_start_type(self) -> Optional[StartType]:

View File

@ -24,7 +24,7 @@ from game.layout import LAYOUTS
from game.server import EventStream, GameContext from game.server import EventStream, GameContext
from game.server.dependencies import QtCallbacks, QtContext from game.server.dependencies import QtCallbacks, QtContext
from game.server.security import ApiKeyManager from game.server.security import ApiKeyManager
from game.theater import MissionTarget, TheaterGroundObject from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
from qt_ui import liberation_install from qt_ui import liberation_install
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
@ -36,6 +36,7 @@ from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap from qt_ui.widgets.map.QLiberationMap import QLiberationMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from qt_ui.windows.infos.QInfoPanel import QInfoPanel from qt_ui.windows.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.logs.QLogsWindow import QLogsWindow from qt_ui.windows.logs.QLogsWindow import QLogsWindow
@ -51,6 +52,7 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QLiberationWindow(QMainWindow): class QLiberationWindow(QMainWindow):
new_package_signal = Signal(MissionTarget) new_package_signal = Signal(MissionTarget)
tgo_info_signal = Signal(TheaterGroundObject) tgo_info_signal = Signal(TheaterGroundObject)
control_point_info_signal = Signal(ControlPoint)
def __init__(self, game: Optional[Game], new_map: bool) -> None: def __init__(self, game: Optional[Game], new_map: bool) -> None:
super().__init__() super().__init__()
@ -66,10 +68,12 @@ class QLiberationWindow(QMainWindow):
lambda target: Dialog.open_new_package_dialog(target, self) lambda target: Dialog.open_new_package_dialog(target, self)
) )
self.tgo_info_signal.connect(self.open_tgo_info_dialog) self.tgo_info_signal.connect(self.open_tgo_info_dialog)
self.control_point_info_signal.connect(self.open_control_point_info_dialog)
QtContext.set_callbacks( QtContext.set_callbacks(
QtCallbacks( QtCallbacks(
lambda target: self.new_package_signal.emit(target), lambda target: self.new_package_signal.emit(target),
lambda tgo: self.tgo_info_signal.emit(tgo), lambda tgo: self.tgo_info_signal.emit(tgo),
lambda cp: self.control_point_info_signal.emit(cp),
) )
) )
Dialog.set_game(self.game_model) Dialog.set_game(self.game_model)
@ -446,6 +450,10 @@ class QLiberationWindow(QMainWindow):
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None: def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show() QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()
def open_control_point_info_dialog(self, cp: ControlPoint) -> None:
self._cp_dialog = QBaseMenu2(None, cp, self.game_model)
self._cp_dialog.show()
def _qsettings(self) -> QSettings: def _qsettings(self) -> QSettings:
return QSettings("DCS Liberation", "Qt UI") return QSettings("DCS Liberation", "Qt UI")