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 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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
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) {
|
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) => {
|
||||||
|
|||||||
@ -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}`))
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
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