Draw supply routes on the react map.

https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
Dan Albert 2022-03-02 23:10:11 -08:00
parent 0bdb4ac894
commit 9a2c10a98f
15 changed files with 280 additions and 1 deletions

View File

@ -0,0 +1,28 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../app/store";
import SupplyRoute from "./supplyroute";
interface SupplyRoutesState {
routes: SupplyRoute[];
}
const initialState: SupplyRoutesState = {
routes: [],
};
export const supplyRoutesSlice = createSlice({
name: "supplyRoutes",
initialState,
reducers: {
setSupplyRoutes: (state, action: PayloadAction<SupplyRoute[]>) => {
state.routes = action.payload;
},
},
});
export const { setSupplyRoutes } = supplyRoutesSlice.actions;
export const selectSupplyRoutes = (state: RootState) => state.supplyRoutes;
export default supplyRoutesSlice.reducer;

View File

@ -0,0 +1,11 @@
import { LatLng } from "leaflet";
export interface SupplyRoute {
points: LatLng[];
front_active: boolean;
is_sea: boolean;
blue: boolean;
active_transports: string[];
}
export default SupplyRoute;

View File

@ -2,12 +2,14 @@ import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
import controlPointsReducer from "../api/controlPointsSlice"; import controlPointsReducer from "../api/controlPointsSlice";
import flightsReducer from "../api/flightsSlice"; import flightsReducer from "../api/flightsSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice"; import tgosReducer from "../api/tgosSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
flights: flightsReducer, flights: flightsReducer,
controlPoints: controlPointsReducer, controlPoints: controlPointsReducer,
supplyRoutes: supplyRoutesReducer,
tgos: tgosReducer, tgos: tgosReducer,
}, },
}); });

View File

@ -7,6 +7,7 @@ import { BasemapLayer } from "react-esri-leaflet";
import ControlPointsLayer from "../controlpointslayer"; import ControlPointsLayer from "../controlpointslayer";
import FlightPlansLayer from "../flightplanslayer"; import FlightPlansLayer from "../flightplanslayer";
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import SupplyRoutesLayer from "../supplyrouteslayer";
import { TgoType } from "../../api/tgo"; import { TgoType } from "../../api/tgo";
import TgosLayer from "../tgoslayer/TgosLayer"; import TgosLayer from "../tgoslayer/TgosLayer";
@ -38,6 +39,9 @@ export default function LiberationMap(props: GameProps) {
</LayersControl.Overlay> </LayersControl.Overlay>
); );
})} })}
<LayersControl.Overlay name="Supply routes" checked>
<SupplyRoutesLayer />
</LayersControl.Overlay>
<LayersControl.Overlay name="Enemy SAM threat range" checked> <LayersControl.Overlay name="Enemy SAM threat range" checked>
<AirDefenseRangeLayer blue={false} /> <AirDefenseRangeLayer blue={false} />
</LayersControl.Overlay> </LayersControl.Overlay>

View File

@ -0,0 +1,20 @@
interface SplitLinesProps {
items: string[];
}
const SplitLines = (props: SplitLinesProps) => {
return (
<>
{props.items.map((text) => {
return (
<>
{text}
<br />
</>
);
})}
</>
);
};
export default SplitLines;

View File

@ -0,0 +1,68 @@
import { Polyline, Tooltip } from "react-leaflet";
import { useEffect, useRef } from "react";
import { Polyline as LPolyline } from "leaflet";
import SplitLines from "../splitlines/SplitLines";
import { SupplyRoute as SupplyRouteModel } from "../../api/supplyroute";
interface SupplyRouteProps {
route: SupplyRouteModel;
}
function SupplyRouteTooltip(props: SupplyRouteProps) {
if (!props.route.active_transports.length) {
return <Tooltip>This supply route is inactive.</Tooltip>;
}
return (
<Tooltip>
<SplitLines items={props.route.active_transports} />
</Tooltip>
);
}
function ActiveSupplyRouteHighlight(props: SupplyRouteProps) {
if (!props.route.active_transports.length) {
return <></>;
}
return (
<Polyline positions={props.route.points} color={"#ffffff"} weight={2} />
);
}
function colorFor(route: SupplyRouteModel) {
if (route.front_active) {
return "#c85050";
}
if (route.blue) {
return "#2d3e50";
}
return "#8c1414";
}
export default function SupplyRoute(props: SupplyRouteProps) {
const color = colorFor(props.route);
const weight = props.route.is_sea ? 4 : 6;
const path = useRef<LPolyline | null>();
useEffect(() => {
// Ensure that the highlight line draws on top of this. We have to bring
// this to the back rather than bringing the highlight to the front because
// the highlight won't necessarily be drawn yet.
path.current?.bringToBack();
});
return (
<Polyline
positions={props.route.points}
color={color}
weight={weight}
ref={(ref) => (path.current = ref)}
>
<SupplyRouteTooltip {...props} />
<ActiveSupplyRouteHighlight {...props} />
</Polyline>
);
}

View File

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

View File

@ -0,0 +1,15 @@
import { LayerGroup } from "react-leaflet";
import SupplyRoute from "../supplyroute/SupplyRoute";
import { selectSupplyRoutes } from "../../api/supplyRoutesSlice";
import { useAppSelector } from "../../app/hooks";
export default function SupplyRoutesLayer() {
const routes = useAppSelector(selectSupplyRoutes).routes;
return (
<LayerGroup>
{routes.map((route, idx) => {
return <SupplyRoute key={idx} route={route} />;
})}
</LayerGroup>
);
}

View File

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

View File

@ -11,7 +11,6 @@ interface TgosLayerProps {
export default function TgosLayer(props: TgosLayerProps) { export default function TgosLayer(props: TgosLayerProps) {
const allTgos = useAppSelector(selectTgos); const allTgos = useAppSelector(selectTgos);
const tgos = allTgos.tgosByType[props.type]; const tgos = allTgos.tgosByType[props.type];
console.dir(Object.entries(TgoType));
return ( return (
<LayerGroup> <LayerGroup>
{tgos.map((tgo) => { {tgos.map((tgo) => {

View File

@ -1,9 +1,11 @@
import { ControlPoint } from "../api/controlpoint"; import { ControlPoint } from "../api/controlpoint";
import { Flight } from "../api/flight"; import { Flight } from "../api/flight";
import SupplyRoute from "../api/supplyroute";
import Tgo from "../api/tgo"; import Tgo from "../api/tgo";
import backend from "../api/backend"; import backend from "../api/backend";
import { registerFlight } from "../api/flightsSlice"; import { registerFlight } from "../api/flightsSlice";
import { setControlPoints } from "../api/controlPointsSlice"; import { setControlPoints } from "../api/controlPointsSlice";
import { setSupplyRoutes } from "../api/supplyRoutesSlice";
import { setTgos } from "../api/tgosSlice"; import { setTgos } from "../api/tgosSlice";
import { useAppDispatch } from "../app/hooks"; import { useAppDispatch } from "../app/hooks";
import { useEffect } from "react"; import { useEffect } from "react";
@ -31,6 +33,14 @@ export const useInitialGameState = () => {
dispatch(setTgos(response.data as Tgo[])); dispatch(setTgos(response.data as Tgo[]));
} }
}); });
backend
.get("/supply-routes")
.catch((error) => console.log(`Error fetching supply routes: ${error}`))
.then((response) => {
if (response != null) {
dispatch(setSupplyRoutes(response.data as SupplyRoute[]));
}
});
backend backend
.get("/flights?with_waypoints=true") .get("/flights?with_waypoints=true")
.catch((error) => console.log(`Error fetching flights: ${error}`)) .catch((error) => console.log(`Error fetching flights: ${error}`))

View File

@ -8,6 +8,7 @@ from . import (
flights, flights,
mapzones, mapzones,
navmesh, navmesh,
supplyroutes,
tgos, tgos,
waypoints, waypoints,
) )
@ -25,6 +26,7 @@ app.include_router(eventstream.router)
app.include_router(flights.router) app.include_router(flights.router)
app.include_router(mapzones.router) app.include_router(mapzones.router)
app.include_router(navmesh.router) app.include_router(navmesh.router)
app.include_router(supplyroutes.router)
app.include_router(tgos.router) app.include_router(tgos.router)
app.include_router(waypoints.router) app.include_router(waypoints.router)

View File

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

View File

@ -0,0 +1,83 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING
from dcs import Point
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint
if TYPE_CHECKING:
from game import Game
from game.theater import ControlPoint
from game.transfers import MultiGroupTransport, TransportMap
class TransportFinder:
def __init__(
self, game: Game, control_point_a: ControlPoint, control_point_b: ControlPoint
) -> None:
self.game = game
self.control_point_a = control_point_a
self.control_point_b = control_point_b
def find_in_transport_map(
self, transport_map: TransportMap[Any]
) -> list[MultiGroupTransport]:
transports = []
transport = transport_map.find_transport(
self.control_point_a, self.control_point_b
)
if transport is not None:
transports.append(transport)
transport = transport_map.find_transport(
self.control_point_b, self.control_point_a
)
if transport is not None:
transports.append(transport)
return transports
def find_transports(self, sea_route: bool) -> list[MultiGroupTransport]:
if sea_route:
return self.find_in_transport_map(
self.game.blue.transfers.cargo_ships
) + self.find_in_transport_map(self.game.red.transfers.cargo_ships)
return self.find_in_transport_map(
self.game.blue.transfers.convoys
) + self.find_in_transport_map(self.game.red.transfers.convoys)
def describe_active_transports(self, sea_route: bool) -> list[str]:
transports = self.find_transports(sea_route)
if not transports:
return []
descriptions = []
for transport in transports:
units = "units" if transport.size > 1 else "unit"
descriptions.append(
f"{transport.size} {units} transferring from {transport.origin} to "
f"{transport.destination}"
)
return descriptions
class SupplyRouteJs(BaseModel):
points: list[LeafletPoint]
front_active: bool
is_sea: bool
blue: bool
active_transports: list[str]
@staticmethod
def for_link(
game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool
) -> SupplyRouteJs:
return SupplyRouteJs(
points=[p.latlng() for p in points],
front_active=not sea and a.front_is_active(b),
is_sea=sea,
blue=a.captured,
active_transports=TransportFinder(game, a, b).describe_active_transports(
sea
),
)

View File

@ -0,0 +1,34 @@
from fastapi import APIRouter, Depends
from game import Game
from .models import SupplyRouteJs
from ..dependencies import GameContext
router: APIRouter = APIRouter(prefix="/supply-routes")
@router.get("/")
def list_supply_routes(game: Game = Depends(GameContext.get)) -> list[SupplyRouteJs]:
seen = set()
routes = []
for control_point in game.theater.controlpoints:
seen.add(control_point)
for destination, route in control_point.convoy_routes.items():
if destination in seen:
continue
routes.append(
SupplyRouteJs.for_link(
game, control_point, destination, list(route), sea=False
)
)
for destination, route in control_point.shipping_lanes.items():
if destination in seen:
continue
if not destination.is_friendly_to(control_point):
continue
routes.append(
SupplyRouteJs.for_link(
game, control_point, destination, list(route), sea=True
)
)
return routes