Draggable waypoints with timing info.

https://github.com/dcs-liberation/dcs_liberation/issues/2039
This commit is contained in:
Dan Albert 2022-03-04 02:21:22 -08:00
parent 6933470ce0
commit 811f46c289
6 changed files with 91 additions and 71 deletions

View File

@ -8,4 +8,5 @@ export interface Waypoint {
is_movable: boolean; is_movable: boolean;
should_mark: boolean; should_mark: boolean;
include_in_path: boolean; include_in_path: boolean;
timing: string;
} }

View File

@ -1,5 +1,6 @@
import { Flight } from "../../api/flight"; import { Flight } from "../../api/flight";
import WaypointMarker from "../waypointmarker"; import WaypointMarker from "../waypointmarker";
import { ReactElement } from "react";
import { Polyline } from "react-leaflet"; import { Polyline } from "react-leaflet";
const BLUE_PATH = "#0084ff"; const BLUE_PATH = "#0084ff";
@ -39,25 +40,23 @@ function FlightPlanPath(props: FlightPlanProps) {
} }
const WaypointMarkers = (props: FlightPlanProps) => { const WaypointMarkers = (props: FlightPlanProps) => {
if (!props.selected || props.flight.waypoints == null) { if (props.selected && props.flight.waypoints == null) {
return <></>; return <></>;
} }
return ( var markers: ReactElement[] = [];
<> props.flight.waypoints?.forEach((p, idx) => {
{props.flight.waypoints markers.push(
.filter((p) => p.should_mark) <WaypointMarker
.map((p, idx) => { key={idx}
return ( number={idx}
<WaypointMarker waypoint={p}
key={idx} flight={props.flight}
number={idx} />
waypoint={p} );
></WaypointMarker> });
);
})} return <>{markers}</>;
</>
);
}; };
export default function FlightPlan(props: FlightPlanProps) { export default function FlightPlan(props: FlightPlanProps) {

View File

@ -1,9 +1,11 @@
import backend from "../../api/backend";
import { Flight } from "../../api/flight";
import { Waypoint } from "../../api/waypoint"; import { Waypoint } from "../../api/waypoint";
import { Icon } from "leaflet"; import { Icon } from "leaflet";
import { Marker as LMarker } from "leaflet"; import { Marker as LMarker } from "leaflet";
import icon from "leaflet/dist/images/marker-icon.png"; import icon from "leaflet/dist/images/marker-icon.png";
import iconShadow from "leaflet/dist/images/marker-shadow.png"; import iconShadow from "leaflet/dist/images/marker-shadow.png";
import { MutableRefObject, useCallback, useRef } from "react"; import { MutableRefObject, useCallback, useEffect, useRef } from "react";
import { Marker, Tooltip, useMap, useMapEvent } from "react-leaflet"; import { Marker, Tooltip, useMap, useMapEvent } from "react-leaflet";
const WAYPOINT_ICON = new Icon({ const WAYPOINT_ICON = new Icon({
@ -15,6 +17,7 @@ const WAYPOINT_ICON = new Icon({
interface WaypointMarkerProps { interface WaypointMarkerProps {
number: number; number: number;
waypoint: Waypoint; waypoint: Waypoint;
flight: Flight;
} }
const WaypointMarker = (props: WaypointMarkerProps) => { const WaypointMarker = (props: WaypointMarkerProps) => {
@ -50,24 +53,42 @@ const WaypointMarker = (props: WaypointMarkerProps) => {
}, [map]); }, [map]);
useMapEvent("zoomend", rebindTooltip); useMapEvent("zoomend", rebindTooltip);
useEffect(() => {
const waypoint = props.waypoint;
marker.current?.setTooltipContent(
`${props.number} ${waypoint.name}<br />` +
`${waypoint.altitude_ft} ft ${waypoint.altitude_reference}<br />` +
waypoint.timing
);
});
const waypoint = props.waypoint; const waypoint = props.waypoint;
return ( return (
<Marker <Marker
position={waypoint.position} position={waypoint.position}
icon={WAYPOINT_ICON} icon={WAYPOINT_ICON}
draggable
eventHandlers={{
dragstart: (e) => {
const m: LMarker = e.target;
m.setTooltipContent("Waiting to recompute TOT...");
},
dragend: (e) => {
const m: LMarker = e.target;
const destination = m.getLatLng();
backend.post(
`/waypoints/${props.flight.id}/${props.number}/position`,
destination
);
},
}}
ref={(ref) => { ref={(ref) => {
if (ref != null) { if (ref != null) {
marker.current = ref; marker.current = ref;
} }
}} }}
> >
<Tooltip position={waypoint.position}> <Tooltip position={waypoint.position} />
{`${props.number} ${waypoint.name}`}
<br />
{`${waypoint.altitude_ft} ft ${waypoint.altitude_reference}`}
<br />
TODO: Timing info
</Tooltip>
</Marker> </Marker>
); );
}; };

View File

@ -1,12 +1,29 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from pydantic import BaseModel from pydantic import BaseModel
from game.ato import FlightWaypoint from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.server.leaflet import LeafletPoint from game.server.leaflet import LeafletPoint
def timing_info(flight: Flight, waypoint_idx: int) -> str:
if waypoint_idx == 0:
return f"Depart T+{flight.flight_plan.takeoff_time()}"
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
prefix = "TOT"
time = flight.flight_plan.tot_for_waypoint(waypoint)
if time is None:
prefix = "Depart"
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
if time is None:
return ""
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
class FlightWaypointJs(BaseModel): class FlightWaypointJs(BaseModel):
name: str name: str
position: LeafletPoint position: LeafletPoint
@ -15,9 +32,12 @@ class FlightWaypointJs(BaseModel):
is_movable: bool is_movable: bool
should_mark: bool should_mark: bool
include_in_path: bool include_in_path: bool
timing: str
@staticmethod @staticmethod
def for_waypoint(waypoint: FlightWaypoint) -> FlightWaypointJs: def for_waypoint(
waypoint: FlightWaypoint, flight: Flight, waypoint_idx: int
) -> FlightWaypointJs:
# Target *points* are the exact location of a unit, whereas the target area is # Target *points* are the exact location of a unit, whereas the target area is
# only the center of the objective. Allow moving the latter since its exact # only the center of the objective. Allow moving the latter since its exact
# location isn't very important. # location isn't very important.
@ -67,4 +87,5 @@ class FlightWaypointJs(BaseModel):
is_movable=is_movable, is_movable=is_movable,
should_mark=should_mark, should_mark=should_mark,
include_in_path=include_in_path, include_in_path=include_in_path,
timing=timing_info(flight, waypoint_idx),
) )

View File

@ -1,4 +1,3 @@
from datetime import timedelta
from uuid import UUID from uuid import UUID
from dcs.mapping import LatLng, Point from dcs.mapping import LatLng, Point
@ -11,6 +10,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
from game.server import GameContext from game.server import GameContext
from game.server.leaflet import LeafletPoint from game.server.leaflet import LeafletPoint
from game.server.waypoints.models import FlightWaypointJs from game.server.waypoints.models import FlightWaypointJs
from game.sim import GameUpdateEvents
from game.utils import meters from game.utils import meters
router: APIRouter = APIRouter(prefix="/waypoints") router: APIRouter = APIRouter(prefix="/waypoints")
@ -24,10 +24,13 @@ def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
flight.departure.position, flight.departure.position,
meters(0), meters(0),
"RADIO", "RADIO",
) ),
flight,
0,
) )
return [departure] + [ return [departure] + [
FlightWaypointJs.for_waypoint(w) for w in flight.flight_plan.waypoints FlightWaypointJs.for_waypoint(w, flight, i)
for i, w in enumerate(flight.flight_plan.waypoints, 1)
] ]
@ -45,6 +48,8 @@ def set_position(
position: LeafletPoint, position: LeafletPoint,
game: Game = Depends(GameContext.get), game: Game = Depends(GameContext.get),
) -> None: ) -> None:
from game.server import EventStream
flight = game.db.flights.get(flight_id) flight = game.db.flights.get(flight_id)
if waypoint_idx == 0: if waypoint_idx == 0:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
@ -64,22 +69,4 @@ def set_position(
detail=f"Could not find PackageModel owning {flight}", detail=f"Could not find PackageModel owning {flight}",
) )
package_model.update_tot() package_model.update_tot()
EventStream.put_nowait(GameUpdateEvents().update_flight(flight))
@router.get("/{flight_id}/{waypoint_idx}/timing")
def waypoint_timing(
flight_id: UUID, waypoint_idx: int, game: Game = Depends(GameContext.get)
) -> str | None:
flight = game.db.flights.get(flight_id)
if waypoint_idx == 0:
return f"Depart T+{flight.flight_plan.takeoff_time()}"
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
prefix = "TOT"
time = flight.flight_plan.tot_for_waypoint(waypoint)
if time is None:
prefix = "Depart"
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
if time is None:
return ""
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"

View File

@ -301,7 +301,7 @@ function handleStreamedEvents(events) {
} }
for (const flightId of events.updated_flights) { for (const flightId of events.updated_flights) {
Flight.withId(flightId).draw(); Flight.withId(flightId).update();
} }
for (const flightId of events.deleted_flights) { for (const flightId of events.deleted_flights) {
@ -775,20 +775,13 @@ class Waypoint {
return this.waypoint.should_mark; return this.waypoint.should_mark;
} }
async timing(dragging) { description() {
if (dragging) {
return "Waiting to recompute TOT...";
}
return await getJson(`/waypoints/${this.flight.id}/${this.number}/timing`);
}
async description(dragging) {
const alt = this.waypoint.altitude_ft; const alt = this.waypoint.altitude_ft;
const altRef = this.waypoint.altitude_reference; const altRef = this.waypoint.altitude_reference;
return ( return (
`${this.number} ${this.waypoint.name}<br />` + `${this.number} ${this.waypoint.name}<br />` +
`${alt} ft ${altRef}<br />` + `${alt} ft ${altRef}<br />` +
`${await this.timing(dragging)}` `${this.waypoint.timing}`
); );
} }
@ -796,19 +789,13 @@ class Waypoint {
this.marker.setLatLng(this.position()); this.marker.setLatLng(this.position());
} }
updateDescription(dragging) {
this.description(dragging).then((description) => {
this.marker.setTooltipContent(description);
});
}
makeMarker() { makeMarker() {
const zoom = map.getZoom(); const zoom = map.getZoom();
const marker = L.marker(this.position(), { const marker = L.marker(this.position(), {
draggable: this.waypoint.is_movable, draggable: this.waypoint.is_movable,
}) })
.on("dragstart", (e) => { .on("dragstart", (e) => {
this.updateDescription(true); this.marker.setTooltipContent("Waiting to recompute TOT...");
}) })
.on("drag", (e) => { .on("drag", (e) => {
const marker = e.target; const marker = e.target;
@ -824,7 +811,6 @@ class Waypoint {
) )
.then(() => { .then(() => {
this.waypoint.position = destination; this.waypoint.position = destination;
this.updateDescription(false);
this.flight.drawCommitBoundary(); this.flight.drawCommitBoundary();
}) })
.catch((err) => { .catch((err) => {
@ -837,11 +823,9 @@ class Waypoint {
}); });
if (this.flight.selected) { if (this.flight.selected) {
this.description(false).then((description) => marker.bindTooltip(this.description(), {
marker.bindTooltip(description, { permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM,
permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM, });
})
);
} }
return marker; return marker;
@ -961,6 +945,13 @@ class Flight {
} }
} }
update() {
getJson(`/flights/${this.id}?with_waypoints=true`).then((flight) => {
this.flight = flight;
this.draw();
});
}
draw() { draw() {
this.drawAircraftLocation(); this.drawAircraftLocation();
this.drawFlightPlan(); this.drawFlightPlan();