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 { LiberationMap } from "./map/liberationmap/LiberationMap";
|
||||
import { ControlPoint } from "./game/controlpoint";
|
||||
import { useEffect } from "react";
|
||||
import { useAppDispatch } from "./app/hooks";
|
||||
import { setControlPoints } from "./game/theater/theaterSlice";
|
||||
import { Flight } from "./game/flight";
|
||||
import { LatLng } from "leaflet";
|
||||
import { LiberationMap } from "./map/liberationmap/LiberationMap";
|
||||
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() {
|
||||
const mapCenter: LatLng = new LatLng(25.58, 54.9);
|
||||
@ -22,6 +24,16 @@ function App() {
|
||||
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}`);
|
||||
|
||||
@ -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";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
atos: atoReducer,
|
||||
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 "./LiberationMap.css";
|
||||
import { LatLng } from "leaflet";
|
||||
import { FlightPlansLayer } from "../flightplanslayer/FlightPlansLayer";
|
||||
|
||||
interface GameProps {
|
||||
mapCenter: LatLng;
|
||||
@ -13,6 +14,8 @@ export function LiberationMap(props: GameProps) {
|
||||
<MapContainer zoom={8} center={props.mapCenter}>
|
||||
<BasemapLayer name="ImageryClarity" />
|
||||
<ControlPointsLayer />
|
||||
<FlightPlansLayer blue={true} />
|
||||
<FlightPlansLayer blue={false} />
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -46,7 +46,9 @@ class GameUpdateEventsJs(BaseModel):
|
||||
navmesh_updates=events.navmesh_updates,
|
||||
unculled_zones_updated=events.unculled_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,
|
||||
deleted_flights=events.deleted_flights,
|
||||
selected_flight=events.selected_flight,
|
||||
|
||||
@ -7,6 +7,8 @@ from pydantic import BaseModel
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
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):
|
||||
@ -14,9 +16,10 @@ class FlightJs(BaseModel):
|
||||
blue: bool
|
||||
position: LeafletPoint | None
|
||||
sidc: str
|
||||
waypoints: list[FlightWaypointJs] | None
|
||||
|
||||
@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
|
||||
# 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
|
||||
@ -24,6 +27,13 @@ class FlightJs(BaseModel):
|
||||
position = None
|
||||
if isinstance(flight.state, InFlight):
|
||||
position = flight.position().latlng()
|
||||
waypoints = None
|
||||
if with_waypoints:
|
||||
waypoints = waypoints_for_flight(flight)
|
||||
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("/")
|
||||
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 = []
|
||||
for coalition in game.coalitions:
|
||||
for package in coalition.ato.packages:
|
||||
for flight in package.flights:
|
||||
flights.append(FlightJs.for_flight(flight))
|
||||
flights.append(FlightJs.for_flight(flight, with_waypoints))
|
||||
return flights
|
||||
|
||||
|
||||
@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)
|
||||
return FlightJs.for_flight(flight)
|
||||
return FlightJs.for_flight(flight, with_waypoints)
|
||||
|
||||
|
||||
@router.get("/{flight_id}/commit-boundary")
|
||||
|
||||
@ -5,6 +5,7 @@ from dcs.mapping import LatLng, Point
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from game import Game
|
||||
from game.ato import Flight
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.server import GameContext
|
||||
@ -15,11 +16,7 @@ from game.utils import meters
|
||||
router: APIRouter = APIRouter(prefix="/waypoints")
|
||||
|
||||
|
||||
@router.get("/{flight_id}", response_model=list[FlightWaypointJs])
|
||||
def all_waypoints_for_flight(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||
) -> list[FlightWaypointJs]:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
|
||||
departure = FlightWaypointJs.for_waypoint(
|
||||
FlightWaypoint(
|
||||
"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")
|
||||
def set_position(
|
||||
flight_id: UUID,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user