mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Draw supply routes on the react map.
https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
parent
0bdb4ac894
commit
9a2c10a98f
28
client/src/api/supplyRoutesSlice.ts
Normal file
28
client/src/api/supplyRoutesSlice.ts
Normal 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;
|
||||
11
client/src/api/supplyroute.ts
Normal file
11
client/src/api/supplyroute.ts
Normal 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;
|
||||
@ -2,12 +2,14 @@ import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
import controlPointsReducer from "../api/controlPointsSlice";
|
||||
import flightsReducer from "../api/flightsSlice";
|
||||
import supplyRoutesReducer from "../api/supplyRoutesSlice";
|
||||
import tgosReducer from "../api/tgosSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
flights: flightsReducer,
|
||||
controlPoints: controlPointsReducer,
|
||||
supplyRoutes: supplyRoutesReducer,
|
||||
tgos: tgosReducer,
|
||||
},
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@ import { BasemapLayer } from "react-esri-leaflet";
|
||||
import ControlPointsLayer from "../controlpointslayer";
|
||||
import FlightPlansLayer from "../flightplanslayer";
|
||||
import { LatLng } from "leaflet";
|
||||
import SupplyRoutesLayer from "../supplyrouteslayer";
|
||||
import { TgoType } from "../../api/tgo";
|
||||
import TgosLayer from "../tgoslayer/TgosLayer";
|
||||
|
||||
@ -38,6 +39,9 @@ export default function LiberationMap(props: GameProps) {
|
||||
</LayersControl.Overlay>
|
||||
);
|
||||
})}
|
||||
<LayersControl.Overlay name="Supply routes" checked>
|
||||
<SupplyRoutesLayer />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="Enemy SAM threat range" checked>
|
||||
<AirDefenseRangeLayer blue={false} />
|
||||
</LayersControl.Overlay>
|
||||
|
||||
20
client/src/components/splitlines/SplitLines.tsx
Normal file
20
client/src/components/splitlines/SplitLines.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
interface SplitLinesProps {
|
||||
items: string[];
|
||||
}
|
||||
|
||||
const SplitLines = (props: SplitLinesProps) => {
|
||||
return (
|
||||
<>
|
||||
{props.items.map((text) => {
|
||||
return (
|
||||
<>
|
||||
{text}
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SplitLines;
|
||||
68
client/src/components/supplyroute/SupplyRoute.tsx
Normal file
68
client/src/components/supplyroute/SupplyRoute.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
client/src/components/supplyroute/index.ts
Normal file
1
client/src/components/supplyroute/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./SupplyRoute";
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
1
client/src/components/supplyrouteslayer/index.ts
Normal file
1
client/src/components/supplyrouteslayer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./SupplyRoutesLayer";
|
||||
@ -11,7 +11,6 @@ interface TgosLayerProps {
|
||||
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) => {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { ControlPoint } from "../api/controlpoint";
|
||||
import { Flight } from "../api/flight";
|
||||
import SupplyRoute from "../api/supplyroute";
|
||||
import Tgo from "../api/tgo";
|
||||
import backend from "../api/backend";
|
||||
import { registerFlight } from "../api/flightsSlice";
|
||||
import { setControlPoints } from "../api/controlPointsSlice";
|
||||
import { setSupplyRoutes } from "../api/supplyRoutesSlice";
|
||||
import { setTgos } from "../api/tgosSlice";
|
||||
import { useAppDispatch } from "../app/hooks";
|
||||
import { useEffect } from "react";
|
||||
@ -31,6 +33,14 @@ export const useInitialGameState = () => {
|
||||
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
|
||||
.get("/flights?with_waypoints=true")
|
||||
.catch((error) => console.log(`Error fetching flights: ${error}`))
|
||||
|
||||
@ -8,6 +8,7 @@ from . import (
|
||||
flights,
|
||||
mapzones,
|
||||
navmesh,
|
||||
supplyroutes,
|
||||
tgos,
|
||||
waypoints,
|
||||
)
|
||||
@ -25,6 +26,7 @@ app.include_router(eventstream.router)
|
||||
app.include_router(flights.router)
|
||||
app.include_router(mapzones.router)
|
||||
app.include_router(navmesh.router)
|
||||
app.include_router(supplyroutes.router)
|
||||
app.include_router(tgos.router)
|
||||
app.include_router(waypoints.router)
|
||||
|
||||
|
||||
1
game/server/supplyroutes/__init__.py
Normal file
1
game/server/supplyroutes/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
83
game/server/supplyroutes/models.py
Normal file
83
game/server/supplyroutes/models.py
Normal 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
|
||||
),
|
||||
)
|
||||
34
game/server/supplyroutes/routes.py
Normal file
34
game/server/supplyroutes/routes.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user