Add threat zone support to the new map.

https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
Dan Albert 2022-03-06 19:30:23 -08:00
parent 30aebf2546
commit dc4762a03b
12 changed files with 199 additions and 5 deletions

View File

@ -21,8 +21,10 @@ import {
} from "./frontLinesSlice";
import FrontLine from "./frontline";
import reloadGameState from "./gamestate";
import { liberationApi } from "./liberationApi";
import Tgo from "./tgo";
import { updateTgo } from "./tgosSlice";
import { threatZonesUpdated } from "./threatZonesSlice";
import { LatLng } from "leaflet";
interface GameUpdateEvents {
@ -68,6 +70,16 @@ export const handleStreamedEvents = (
dispatch(endCombat(id));
}
if (events.threat_zones_updated) {
dispatch(liberationApi.endpoints.getThreatZones.initiate()).then(
(result) => {
if (result.data) {
dispatch(threatZonesUpdated(result.data));
}
}
);
}
for (const flight of events.new_flights) {
dispatch(registerFlight(flight));
}

View File

@ -1,6 +1,7 @@
import { ControlPoint } from "./controlpoint";
import { Flight } from "./flight";
import FrontLine from "./frontline";
import { ThreatZoneContainer } from "./liberationApi";
import SupplyRoute from "./supplyroute";
import Tgo from "./tgo";
import { LatLngLiteral } from "leaflet";
@ -11,5 +12,6 @@ export default interface Game {
supply_routes: SupplyRoute[];
front_lines: FrontLine[];
flights: Flight[];
threat_zones: ThreatZoneContainer;
map_center: LatLngLiteral | null;
}

View File

@ -0,0 +1,49 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { ThreatZoneContainer } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface ThreatZonesState {
zones: ThreatZoneContainer;
}
const initialState: ThreatZonesState = {
zones: {
blue: {
full: [],
aircraft: [],
air_defenses: [],
radar_sams: [],
},
red: {
full: [],
aircraft: [],
air_defenses: [],
radar_sams: [],
},
},
};
export const threatZonesSlice = createSlice({
name: "threatZonesState",
initialState,
reducers: {
updated: (state, action: PayloadAction<ThreatZoneContainer>) => {
state.zones = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.zones = action.payload.threat_zones;
});
builder.addCase(gameUnloaded, (state) => {
state.zones = initialState.zones;
});
},
});
export const { updated: threatZonesUpdated } = threatZonesSlice.actions;
export const selectThreatZones = (state: RootState) => state.threatZones;
export default threatZonesSlice.reducer;

View File

@ -6,6 +6,7 @@ import frontLinesReducer from "../api/frontLinesSlice";
import mapReducer from "../api/mapSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice";
import threatZonesReducer from "../api/threatZonesSlice";
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
@ -17,6 +18,7 @@ export const store = configureStore({
map: mapReducer,
supplyRoutes: supplyRoutesReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
},
middleware: (getDefaultMiddleware) =>

View File

@ -8,6 +8,11 @@ import FlightPlansLayer from "../flightplanslayer";
import FrontLinesLayer from "../frontlineslayer";
import SupplyRoutesLayer from "../supplyrouteslayer";
import TgosLayer from "../tgoslayer/TgosLayer";
import { CoalitionThreatZones } from "../threatzones";
import {
ThreatZonesLayer,
ThreatZoneFilter,
} from "../threatzones/ThreatZonesLayer";
import "./LiberationMap.css";
import { Map } from "leaflet";
import { useEffect, useRef } from "react";
@ -83,6 +88,10 @@ export default function LiberationMap() {
<FlightPlansLayer blue={false} />
</LayersControl.Overlay>
</LayersControl>
<LayersControl position="topleft">
<CoalitionThreatZones blue={true} />
<CoalitionThreatZones blue={false} />
</LayersControl>
</MapContainer>
);
}

View File

@ -0,0 +1,35 @@
import { ThreatZoneFilter, ThreatZonesLayer } from "./ThreatZonesLayer";
import { LayersControl } from "react-leaflet";
interface CoalitionThreatZonesProps {
blue: boolean;
}
export function CoalitionThreatZones(props: CoalitionThreatZonesProps) {
const color = props.blue ? "Blue" : "Red";
return (
<>
<LayersControl.Overlay name={`${color} threat zones: full`}>
<ThreatZonesLayer blue={props.blue} filter={ThreatZoneFilter.FULL} />
</LayersControl.Overlay>
<LayersControl.Overlay name={`${color} threat zones: aircraft`}>
<ThreatZonesLayer
blue={props.blue}
filter={ThreatZoneFilter.AIRCRAFT}
/>
</LayersControl.Overlay>
<LayersControl.Overlay name={`${color} threat zones: air defenses`}>
<ThreatZonesLayer
blue={props.blue}
filter={ThreatZoneFilter.AIR_DEFENSES}
/>
</LayersControl.Overlay>
<LayersControl.Overlay name={`${color} threat zones: radar SAMs`}>
<ThreatZonesLayer
blue={props.blue}
filter={ThreatZoneFilter.RADAR_SAMS}
/>
</LayersControl.Overlay>
</>
);
}

View File

@ -0,0 +1,24 @@
import { LatLng } from "leaflet";
import { Polygon } from "react-leaflet";
interface ThreatZoneProps {
poly: number[][];
blue: boolean;
}
export default function ThreatZone(props: ThreatZoneProps) {
const color = props.blue ? "#0084ff" : "#c85050";
// TODO: Fix response model so the type can be used directly.
const positions = props.poly.map(([lat, lng]) => new LatLng(lat, lng));
return (
<Polygon
positions={positions}
color={color}
weight={1}
fill
fillOpacity={0.4}
noClip
interactive={false}
/>
);
}

View File

@ -0,0 +1,43 @@
import { selectThreatZones } from "../../api/threatZonesSlice";
import { useAppSelector } from "../../app/hooks";
import ThreatZone from "./ThreatZone";
import { LayerGroup } from "react-leaflet";
export enum ThreatZoneFilter {
FULL,
AIRCRAFT,
AIR_DEFENSES,
RADAR_SAMS,
}
interface ThreatZonesLayerProps {
blue: boolean;
filter: ThreatZoneFilter;
}
export function ThreatZonesLayer(props: ThreatZonesLayerProps) {
const allZones = useAppSelector(selectThreatZones).zones;
const zones = props.blue ? allZones.blue : allZones.red;
var filtered;
switch (props.filter) {
case ThreatZoneFilter.FULL:
filtered = zones.full;
break;
case ThreatZoneFilter.AIRCRAFT:
filtered = zones.aircraft;
break;
case ThreatZoneFilter.AIR_DEFENSES:
filtered = zones.air_defenses;
break;
case ThreatZoneFilter.RADAR_SAMS:
filtered = zones.radar_sams;
break;
}
return (
<LayerGroup>
{filtered.map((poly, idx) => (
<ThreatZone key={idx} poly={poly} blue={props.blue} />
))}
</LayerGroup>
);
}

View File

@ -0,0 +1,2 @@
export { ThreatZonesLayer, ThreatZoneFilter } from "./ThreatZonesLayer";
export { CoalitionThreatZones } from "./CoalitionThreatZones";

View File

@ -8,6 +8,7 @@ from game.server.controlpoints.models import ControlPointJs
from game.server.flights.models import FlightJs
from game.server.frontlines.models import FrontLineJs
from game.server.leaflet import LeafletPoint
from game.server.mapzones.models import ThreatZoneContainerJs
from game.server.supplyroutes.models import SupplyRouteJs
from game.server.tgos.models import TgoJs
@ -21,6 +22,7 @@ class GameJs(BaseModel):
supply_routes: list[SupplyRouteJs]
front_lines: list[FrontLineJs]
flights: list[FlightJs]
threat_zones: ThreatZoneContainerJs
map_center: LeafletPoint
class Config:
@ -34,5 +36,6 @@ class GameJs(BaseModel):
supply_routes=SupplyRouteJs.all_in_game(game),
front_lines=FrontLineJs.all_in_game(game),
flights=FlightJs.all_in_game(game, with_waypoints=True),
threat_zones=ThreatZoneContainerJs.for_game(game),
map_center=game.theater.terrain.map_view_default.position.latlng(),
)

View File

@ -1,11 +1,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint, LeafletPoly, ShapelyUtil
from game.theater import ConflictTheater
from game.threatzones import ThreatZones
if TYPE_CHECKING:
from game import Game
class MapZonesJs(BaseModel):
inclusion: list[LeafletPoly]
@ -49,3 +54,14 @@ class ThreatZoneContainerJs(BaseModel):
class Config:
title = "ThreatZoneContainer"
@staticmethod
def for_game(game: Game) -> ThreatZoneContainerJs:
return ThreatZoneContainerJs(
blue=ThreatZonesJs.from_zones(
game.threat_zone_for(player=True), game.theater
),
red=ThreatZonesJs.from_zones(
game.threat_zone_for(player=False), game.theater
),
)

View File

@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from game import Game
from game.server import GameContext
from .models import MapZonesJs, ThreatZoneContainerJs, ThreatZonesJs, UnculledZoneJs
from .models import MapZonesJs, ThreatZoneContainerJs, UnculledZoneJs
from ..leaflet import ShapelyUtil
router: APIRouter = APIRouter(prefix="/map-zones")
@ -41,7 +41,4 @@ def get_unculled_zones(
def get_threat_zones(
game: Game = Depends(GameContext.require),
) -> ThreatZoneContainerJs:
return ThreatZoneContainerJs(
blue=ThreatZonesJs.from_zones(game.threat_zone_for(player=True), game.theater),
red=ThreatZonesJs.from_zones(game.threat_zone_for(player=False), game.theater),
)
return ThreatZoneContainerJs.for_game(game)