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";
interface ControlPointsState {
controlPoints: ControlPoint[];
controlPoints: { [key: number]: ControlPoint };
}
const initialState: ControlPointsState = {
controlPoints: [],
controlPoints: {},
};
export const controlPointsSlice = createSlice({
@ -16,12 +16,20 @@ export const controlPointsSlice = createSlice({
initialState,
reducers: {
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;

View File

@ -1,8 +1,14 @@
import { deselectFlight, selectFlight } from "./flightsSlice";
import { AppDispatch } from "../app/store";
import { ControlPoint } from "./controlpoint";
import { Flight } from "./flight";
import FrontLine from "./frontline";
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
// update events model.
@ -21,6 +27,11 @@ interface GameUpdateEvents {
deleted_flights: string[];
selected_flight: string | null;
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 = (
@ -33,4 +44,16 @@ export const handleStreamedEvents = (
if (events.selected_flight != null) {
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";
export enum TgoType {
AIR_DEFENSE = "Air defenses",
FACTORY = "Factories",
SHIP = "Ships",
OTHER = "Other ground objects",
}
export interface Tgo {
id: string;
name: string;

View File

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

View File

@ -49,19 +49,12 @@ interface AirDefenseRangeLayerProps {
}
export const AirDefenseRangeLayer = (props: AirDefenseRangeLayerProps) => {
const tgos = useAppSelector(selectTgos);
var allTgos: Tgo[] = [];
for (const tgoType of Object.values(tgos.tgosByType)) {
for (const tgo of tgoType) {
if (tgo.blue === props.blue) {
allTgos.push(tgo);
}
}
}
const tgos = Object.values(useAppSelector(selectTgos).tgos);
var tgosForSide = tgos.filter((tgo) => tgo.blue === props.blue);
return (
<LayerGroup>
{allTgos.map((tgo) => {
{tgosForSide.map((tgo) => {
return (
<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 { Symbol as MilSymbol } from "milsymbol";
import backend from "../../api/backend";
function iconForControlPoint(cp: ControlPointModel) {
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
// is always the clickable thing.
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>
<h3 style={{ margin: 0 }}>{props.controlPoint.name}</h3>

View File

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

View File

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

View File

@ -1,16 +1,19 @@
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;
categories?: string[];
exclude?: true;
}
export default function TgosLayer(props: TgosLayerProps) {
const allTgos = useAppSelector(selectTgos);
const tgos = allTgos.tgosByType[props.type];
const allTgos = Object.values(useAppSelector(selectTgos).tgos);
const categoryFilter = props.categories ?? [];
const tgos = allTgos.filter(
(tgo) => categoryFilter.includes(tgo.category) === (props.exclude ?? false)
);
return (
<LayerGroup>
{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 .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:
control_points.append(ControlPointJs.for_control_point(control_point))
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 game.theater import MissionTarget, TheaterGroundObject
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
if TYPE_CHECKING:
from game import Game
@ -32,9 +32,11 @@ class QtCallbacks:
self,
create_new_package: Callable[[MissionTarget], None],
show_tgo_info: Callable[[TheaterGroundObject], None],
show_control_point_info: Callable[[ControlPoint], None],
) -> None:
self.create_new_package = create_new_package
self.show_tgo_info = show_tgo_info
self.show_control_point_info = show_control_point_info
class QtContext:

View File

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

View File

@ -1,6 +1,6 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, status
from game import Game
from ..dependencies import GameContext, QtCallbacks, QtContext
@ -33,3 +33,33 @@ def show_tgo_info(
qt: QtCallbacks = Depends(QtContext.get),
) -> None:
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:
from game.ato import Flight, Package
from game.sim.combat import FrozenCombat
from game.theater import FrontLine, TheaterGroundObject
from game.theater import ControlPoint, FrontLine, TheaterGroundObject
@dataclass
@ -31,6 +31,7 @@ class GameUpdateEvents:
updated_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_control_points: set[int] = field(default_factory=set)
shutting_down: bool = False
@property
@ -116,6 +117,10 @@ class GameUpdateEvents:
self.updated_tgos.add(tgo.id)
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:
self.shutting_down = True
return self

View File

@ -748,6 +748,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.base.set_strength_to_minimum()
self._clear_front_lines(events)
self._create_missing_front_lines(events)
events.update_control_point(self)
@property
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.dependencies import QtCallbacks, QtContext
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.dialogs import Dialog
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.windows.GameUpdateSignal import GameUpdateSignal
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.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.logs.QLogsWindow import QLogsWindow
@ -51,6 +52,7 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QLiberationWindow(QMainWindow):
new_package_signal = Signal(MissionTarget)
tgo_info_signal = Signal(TheaterGroundObject)
control_point_info_signal = Signal(ControlPoint)
def __init__(self, game: Optional[Game], new_map: bool) -> None:
super().__init__()
@ -66,10 +68,12 @@ class QLiberationWindow(QMainWindow):
lambda target: Dialog.open_new_package_dialog(target, self)
)
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(
QtCallbacks(
lambda target: self.new_package_signal.emit(target),
lambda tgo: self.tgo_info_signal.emit(tgo),
lambda cp: self.control_point_info_signal.emit(cp),
)
)
Dialog.set_game(self.game_model)
@ -446,6 +450,10 @@ class QLiberationWindow(QMainWindow):
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
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:
return QSettings("DCS Liberation", "Qt UI")