mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Draw flight plan paths in the react UI.
https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
parent
bd8aa0296b
commit
406a64ae3f
@ -1,12 +1,14 @@
|
|||||||
import { LatLng } from "leaflet";
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
import { LiberationMap } from "./map/liberationmap/LiberationMap";
|
|
||||||
import { ControlPoint } from "./game/controlpoint";
|
import { ControlPoint } from "./game/controlpoint";
|
||||||
import { useEffect } from "react";
|
import { Flight } from "./game/flight";
|
||||||
import { useAppDispatch } from "./app/hooks";
|
import { LatLng } from "leaflet";
|
||||||
import { setControlPoints } from "./game/theater/theaterSlice";
|
import { LiberationMap } from "./map/liberationmap/LiberationMap";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { registerFlight } from "./game/ato/atoSlice";
|
||||||
|
import { setControlPoints } from "./game/theater/theaterSlice";
|
||||||
|
import { useAppDispatch } from "./app/hooks";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const mapCenter: LatLng = new LatLng(25.58, 54.9);
|
const mapCenter: LatLng = new LatLng(25.58, 54.9);
|
||||||
@ -22,6 +24,16 @@ function App() {
|
|||||||
dispatch(setControlPoints(response.data as ControlPoint[]));
|
dispatch(setControlPoints(response.data as ControlPoint[]));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
axios
|
||||||
|
.get("http://[::1]:5000/flights?with_waypoints=true")
|
||||||
|
.catch((error) => console.log(`Error fetching flights: ${error}`))
|
||||||
|
.then((response) => {
|
||||||
|
if (response != null) {
|
||||||
|
for (const flight of response.data) {
|
||||||
|
dispatch(registerFlight(flight as Flight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`mapCenter=${mapCenter}`);
|
console.log(`mapCenter=${mapCenter}`);
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
|
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
import atoReducer from "../game/ato/atoSlice";
|
||||||
import theaterReducer from "../game/theater/theaterSlice";
|
import theaterReducer from "../game/theater/theaterSlice";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
|
atos: atoReducer,
|
||||||
theater: theaterReducer,
|
theater: theaterReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
53
client/src/game/ato/atoSlice.ts
Normal file
53
client/src/game/ato/atoSlice.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
import { Flight } from "../flight";
|
||||||
|
import { RootState } from "../../app/store";
|
||||||
|
|
||||||
|
interface AtoState {
|
||||||
|
blue: { [id: string]: Flight };
|
||||||
|
red: { [id: string]: Flight };
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AtoState = {
|
||||||
|
blue: {},
|
||||||
|
red: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const atoSlice = createSlice({
|
||||||
|
name: "ato",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearFlights: (state) => {
|
||||||
|
state.blue = {};
|
||||||
|
state.red = {};
|
||||||
|
},
|
||||||
|
registerFlight: (state, action: PayloadAction<Flight>) => {
|
||||||
|
const flight = action.payload;
|
||||||
|
const ato = flight.blue ? state.blue : state.red;
|
||||||
|
if (flight.id in ato) {
|
||||||
|
console.log(`Overriding flight with ID: ${flight.id}`);
|
||||||
|
}
|
||||||
|
ato[flight.id] = flight;
|
||||||
|
},
|
||||||
|
unregisterFlight: (state, action: PayloadAction<string>) => {
|
||||||
|
const id = action.payload;
|
||||||
|
if (id in state.blue) {
|
||||||
|
delete state.blue[id];
|
||||||
|
} else if (id in state.red) {
|
||||||
|
delete state.red[id];
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Could not delete flight with ID ${id} because no flight with that ` +
|
||||||
|
`ID exists`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { clearFlights, registerFlight, unregisterFlight } =
|
||||||
|
atoSlice.actions;
|
||||||
|
|
||||||
|
export const selectAtos = (state: RootState) => state.atos;
|
||||||
|
|
||||||
|
export default atoSlice.reducer;
|
||||||
10
client/src/game/flight.ts
Normal file
10
client/src/game/flight.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { LatLng } from "leaflet";
|
||||||
|
import { Waypoint } from "./waypoint";
|
||||||
|
|
||||||
|
export interface Flight {
|
||||||
|
id: string;
|
||||||
|
blue: boolean;
|
||||||
|
position: LatLng;
|
||||||
|
sidc: string;
|
||||||
|
waypoints: Waypoint[] | null;
|
||||||
|
}
|
||||||
11
client/src/game/waypoint.ts
Normal file
11
client/src/game/waypoint.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { LatLng } from "leaflet";
|
||||||
|
|
||||||
|
export interface Waypoint {
|
||||||
|
name: string;
|
||||||
|
position: LatLng;
|
||||||
|
altitude_ft: number;
|
||||||
|
altitude_reference: string;
|
||||||
|
is_movable: boolean;
|
||||||
|
should_mark: boolean;
|
||||||
|
include_in_path: boolean;
|
||||||
|
}
|
||||||
33
client/src/map/flightplan/FlightPlan.tsx
Normal file
33
client/src/map/flightplan/FlightPlan.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Flight } from "../../game/flight";
|
||||||
|
import { Polyline } from "react-leaflet";
|
||||||
|
|
||||||
|
const BLUE_PATH = "#0084ff";
|
||||||
|
const RED_PATH = "#c85050";
|
||||||
|
|
||||||
|
interface FlightPlanProps {
|
||||||
|
flight: Flight;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlightPlanPath(props: FlightPlanProps) {
|
||||||
|
const color = props.flight.blue ? BLUE_PATH : RED_PATH;
|
||||||
|
const waypoints = props.flight.waypoints;
|
||||||
|
if (waypoints == null) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
const points = waypoints.map((waypoint) => waypoint.position);
|
||||||
|
return (
|
||||||
|
<Polyline
|
||||||
|
positions={points}
|
||||||
|
pathOptions={{ color: color, interactive: false }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlightPlan(props: FlightPlanProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FlightPlanPath {...props} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
client/src/map/flightplanslayer/FlightPlansLayer.tsx
Normal file
20
client/src/map/flightplanslayer/FlightPlansLayer.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { FlightPlan } from "../flightplan/FlightPlan";
|
||||||
|
import { LayerGroup } from "react-leaflet";
|
||||||
|
import { selectAtos } from "../../game/ato/atoSlice";
|
||||||
|
import { useAppSelector } from "../../app/hooks";
|
||||||
|
|
||||||
|
interface FlightPlansLayerProps {
|
||||||
|
blue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlightPlansLayer(props: FlightPlansLayerProps) {
|
||||||
|
const atos = useAppSelector(selectAtos);
|
||||||
|
const flights = props.blue ? atos.blue : atos.red;
|
||||||
|
return (
|
||||||
|
<LayerGroup>
|
||||||
|
{Object.values(flights).map((flight) => {
|
||||||
|
return <FlightPlan key={flight.id} flight={flight} selected={false} />;
|
||||||
|
})}
|
||||||
|
</LayerGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { BasemapLayer } from "react-esri-leaflet";
|
|||||||
import { ControlPointsLayer } from "../controlpointslayer/ControlPointsLayer";
|
import { ControlPointsLayer } from "../controlpointslayer/ControlPointsLayer";
|
||||||
import "./LiberationMap.css";
|
import "./LiberationMap.css";
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
|
import { FlightPlansLayer } from "../flightplanslayer/FlightPlansLayer";
|
||||||
|
|
||||||
interface GameProps {
|
interface GameProps {
|
||||||
mapCenter: LatLng;
|
mapCenter: LatLng;
|
||||||
@ -13,6 +14,8 @@ export function LiberationMap(props: GameProps) {
|
|||||||
<MapContainer zoom={8} center={props.mapCenter}>
|
<MapContainer zoom={8} center={props.mapCenter}>
|
||||||
<BasemapLayer name="ImageryClarity" />
|
<BasemapLayer name="ImageryClarity" />
|
||||||
<ControlPointsLayer />
|
<ControlPointsLayer />
|
||||||
|
<FlightPlansLayer blue={true} />
|
||||||
|
<FlightPlansLayer blue={false} />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,9 @@ class GameUpdateEventsJs(BaseModel):
|
|||||||
navmesh_updates=events.navmesh_updates,
|
navmesh_updates=events.navmesh_updates,
|
||||||
unculled_zones_updated=events.unculled_zones_updated,
|
unculled_zones_updated=events.unculled_zones_updated,
|
||||||
threat_zones_updated=events.threat_zones_updated,
|
threat_zones_updated=events.threat_zones_updated,
|
||||||
new_flights=[FlightJs.for_flight(f) for f in events.new_flights],
|
new_flights=[
|
||||||
|
FlightJs.for_flight(f, with_waypoints=True) for f in events.new_flights
|
||||||
|
],
|
||||||
updated_flights=events.updated_flights,
|
updated_flights=events.updated_flights,
|
||||||
deleted_flights=events.deleted_flights,
|
deleted_flights=events.deleted_flights,
|
||||||
selected_flight=events.selected_flight,
|
selected_flight=events.selected_flight,
|
||||||
|
|||||||
@ -7,6 +7,8 @@ from pydantic import BaseModel
|
|||||||
from game.ato import Flight
|
from game.ato import Flight
|
||||||
from game.ato.flightstate import InFlight
|
from game.ato.flightstate import InFlight
|
||||||
from game.server.leaflet import LeafletPoint
|
from game.server.leaflet import LeafletPoint
|
||||||
|
from game.server.waypoints.models import FlightWaypointJs
|
||||||
|
from game.server.waypoints.routes import waypoints_for_flight
|
||||||
|
|
||||||
|
|
||||||
class FlightJs(BaseModel):
|
class FlightJs(BaseModel):
|
||||||
@ -14,9 +16,10 @@ class FlightJs(BaseModel):
|
|||||||
blue: bool
|
blue: bool
|
||||||
position: LeafletPoint | None
|
position: LeafletPoint | None
|
||||||
sidc: str
|
sidc: str
|
||||||
|
waypoints: list[FlightWaypointJs] | None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def for_flight(flight: Flight) -> FlightJs:
|
def for_flight(flight: Flight, with_waypoints: bool) -> FlightJs:
|
||||||
# Don't provide a location for aircraft that aren't in the air. Later we can
|
# Don't provide a location for aircraft that aren't in the air. Later we can
|
||||||
# expand the model to include the state data for the UI so that it can make its
|
# expand the model to include the state data for the UI so that it can make its
|
||||||
# own decisions about whether or not to draw the aircraft, but for now we'll
|
# own decisions about whether or not to draw the aircraft, but for now we'll
|
||||||
@ -24,6 +27,13 @@ class FlightJs(BaseModel):
|
|||||||
position = None
|
position = None
|
||||||
if isinstance(flight.state, InFlight):
|
if isinstance(flight.state, InFlight):
|
||||||
position = flight.position().latlng()
|
position = flight.position().latlng()
|
||||||
|
waypoints = None
|
||||||
|
if with_waypoints:
|
||||||
|
waypoints = waypoints_for_flight(flight)
|
||||||
return FlightJs(
|
return FlightJs(
|
||||||
id=flight.id, blue=flight.blue, position=position, sidc=str(flight.sidc())
|
id=flight.id,
|
||||||
|
blue=flight.blue,
|
||||||
|
position=position,
|
||||||
|
sidc=str(flight.sidc()),
|
||||||
|
waypoints=waypoints,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,19 +13,23 @@ router: APIRouter = APIRouter(prefix="/flights")
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
def list_flights(game: Game = Depends(GameContext.get)) -> list[FlightJs]:
|
def list_flights(
|
||||||
|
with_waypoints: bool = False, game: Game = Depends(GameContext.get)
|
||||||
|
) -> list[FlightJs]:
|
||||||
flights = []
|
flights = []
|
||||||
for coalition in game.coalitions:
|
for coalition in game.coalitions:
|
||||||
for package in coalition.ato.packages:
|
for package in coalition.ato.packages:
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
flights.append(FlightJs.for_flight(flight))
|
flights.append(FlightJs.for_flight(flight, with_waypoints))
|
||||||
return flights
|
return flights
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{flight_id}")
|
@router.get("/{flight_id}")
|
||||||
def get_flight(flight_id: UUID, game: Game = Depends(GameContext.get)) -> FlightJs:
|
def get_flight(
|
||||||
|
flight_id: UUID, with_waypoints: bool = False, game: Game = Depends(GameContext.get)
|
||||||
|
) -> FlightJs:
|
||||||
flight = game.db.flights.get(flight_id)
|
flight = game.db.flights.get(flight_id)
|
||||||
return FlightJs.for_flight(flight)
|
return FlightJs.for_flight(flight, with_waypoints)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{flight_id}/commit-boundary")
|
@router.get("/{flight_id}/commit-boundary")
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from dcs.mapping import LatLng, Point
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from game.ato import Flight
|
||||||
from game.ato.flightwaypoint import FlightWaypoint
|
from game.ato.flightwaypoint import FlightWaypoint
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.server import GameContext
|
from game.server import GameContext
|
||||||
@ -15,11 +16,7 @@ from game.utils import meters
|
|||||||
router: APIRouter = APIRouter(prefix="/waypoints")
|
router: APIRouter = APIRouter(prefix="/waypoints")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{flight_id}", response_model=list[FlightWaypointJs])
|
def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
|
||||||
def all_waypoints_for_flight(
|
|
||||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
|
||||||
) -> list[FlightWaypointJs]:
|
|
||||||
flight = game.db.flights.get(flight_id)
|
|
||||||
departure = FlightWaypointJs.for_waypoint(
|
departure = FlightWaypointJs.for_waypoint(
|
||||||
FlightWaypoint(
|
FlightWaypoint(
|
||||||
"TAKEOFF",
|
"TAKEOFF",
|
||||||
@ -34,6 +31,13 @@ def all_waypoints_for_flight(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{flight_id}", response_model=list[FlightWaypointJs])
|
||||||
|
def all_waypoints_for_flight(
|
||||||
|
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||||
|
) -> list[FlightWaypointJs]:
|
||||||
|
return waypoints_for_flight(game.db.flights.get(flight_id))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{flight_id}/{waypoint_idx}/position")
|
@router.post("/{flight_id}/{waypoint_idx}/position")
|
||||||
def set_position(
|
def set_position(
|
||||||
flight_id: UUID,
|
flight_id: UUID,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user