From 811f46c2892014a873a239ea9bf8796ad5fe5f23 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 4 Mar 2022 02:21:22 -0800 Subject: [PATCH] Draggable waypoints with timing info. https://github.com/dcs-liberation/dcs_liberation/issues/2039 --- client/src/api/waypoint.ts | 1 + .../src/components/flightplan/FlightPlan.tsx | 31 ++++++++-------- .../waypointmarker/WaypointMarker.tsx | 37 +++++++++++++++---- game/server/waypoints/models.py | 25 ++++++++++++- game/server/waypoints/routes.py | 31 +++++----------- resources/ui/map/map.js | 37 +++++++------------ 6 files changed, 91 insertions(+), 71 deletions(-) diff --git a/client/src/api/waypoint.ts b/client/src/api/waypoint.ts index 9908aef9..150d1c30 100644 --- a/client/src/api/waypoint.ts +++ b/client/src/api/waypoint.ts @@ -8,4 +8,5 @@ export interface Waypoint { is_movable: boolean; should_mark: boolean; include_in_path: boolean; + timing: string; } diff --git a/client/src/components/flightplan/FlightPlan.tsx b/client/src/components/flightplan/FlightPlan.tsx index 6075437f..ff20eb66 100644 --- a/client/src/components/flightplan/FlightPlan.tsx +++ b/client/src/components/flightplan/FlightPlan.tsx @@ -1,5 +1,6 @@ import { Flight } from "../../api/flight"; import WaypointMarker from "../waypointmarker"; +import { ReactElement } from "react"; import { Polyline } from "react-leaflet"; const BLUE_PATH = "#0084ff"; @@ -39,25 +40,23 @@ function FlightPlanPath(props: FlightPlanProps) { } const WaypointMarkers = (props: FlightPlanProps) => { - if (!props.selected || props.flight.waypoints == null) { + if (props.selected && props.flight.waypoints == null) { return <>; } - return ( - <> - {props.flight.waypoints - .filter((p) => p.should_mark) - .map((p, idx) => { - return ( - - ); - })} - - ); + var markers: ReactElement[] = []; + props.flight.waypoints?.forEach((p, idx) => { + markers.push( + + ); + }); + + return <>{markers}; }; export default function FlightPlan(props: FlightPlanProps) { diff --git a/client/src/components/waypointmarker/WaypointMarker.tsx b/client/src/components/waypointmarker/WaypointMarker.tsx index 28b30008..c53150c5 100644 --- a/client/src/components/waypointmarker/WaypointMarker.tsx +++ b/client/src/components/waypointmarker/WaypointMarker.tsx @@ -1,9 +1,11 @@ +import backend from "../../api/backend"; +import { Flight } from "../../api/flight"; import { Waypoint } from "../../api/waypoint"; import { Icon } from "leaflet"; import { Marker as LMarker } from "leaflet"; import icon from "leaflet/dist/images/marker-icon.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"; const WAYPOINT_ICON = new Icon({ @@ -15,6 +17,7 @@ const WAYPOINT_ICON = new Icon({ interface WaypointMarkerProps { number: number; waypoint: Waypoint; + flight: Flight; } const WaypointMarker = (props: WaypointMarkerProps) => { @@ -50,24 +53,42 @@ const WaypointMarker = (props: WaypointMarkerProps) => { }, [map]); useMapEvent("zoomend", rebindTooltip); + useEffect(() => { + const waypoint = props.waypoint; + marker.current?.setTooltipContent( + `${props.number} ${waypoint.name}
` + + `${waypoint.altitude_ft} ft ${waypoint.altitude_reference}
` + + waypoint.timing + ); + }); + const waypoint = props.waypoint; return ( { + 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) => { if (ref != null) { marker.current = ref; } }} > - - {`${props.number} ${waypoint.name}`} -
- {`${waypoint.altitude_ft} ft ${waypoint.altitude_reference}`} -
- TODO: Timing info -
+
); }; diff --git a/game/server/waypoints/models.py b/game/server/waypoints/models.py index d732c248..00b140d3 100644 --- a/game/server/waypoints/models.py +++ b/game/server/waypoints/models.py @@ -1,12 +1,29 @@ from __future__ import annotations +from datetime import timedelta + from pydantic import BaseModel -from game.ato import FlightWaypoint +from game.ato import Flight, FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType 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): name: str position: LeafletPoint @@ -15,9 +32,12 @@ class FlightWaypointJs(BaseModel): is_movable: bool should_mark: bool include_in_path: bool + timing: str @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 # only the center of the objective. Allow moving the latter since its exact # location isn't very important. @@ -67,4 +87,5 @@ class FlightWaypointJs(BaseModel): is_movable=is_movable, should_mark=should_mark, include_in_path=include_in_path, + timing=timing_info(flight, waypoint_idx), ) diff --git a/game/server/waypoints/routes.py b/game/server/waypoints/routes.py index ab1ea1af..0f4b6e86 100644 --- a/game/server/waypoints/routes.py +++ b/game/server/waypoints/routes.py @@ -1,4 +1,3 @@ -from datetime import timedelta from uuid import UUID from dcs.mapping import LatLng, Point @@ -11,6 +10,7 @@ from game.ato.flightwaypointtype import FlightWaypointType from game.server import GameContext from game.server.leaflet import LeafletPoint from game.server.waypoints.models import FlightWaypointJs +from game.sim import GameUpdateEvents from game.utils import meters router: APIRouter = APIRouter(prefix="/waypoints") @@ -24,10 +24,13 @@ def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]: flight.departure.position, meters(0), "RADIO", - ) + ), + flight, + 0, ) 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, game: Game = Depends(GameContext.get), ) -> None: + from game.server import EventStream + flight = game.db.flights.get(flight_id) if waypoint_idx == 0: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) @@ -64,22 +69,4 @@ def set_position( detail=f"Could not find PackageModel owning {flight}", ) package_model.update_tot() - - -@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()))}" + EventStream.put_nowait(GameUpdateEvents().update_flight(flight)) diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index 7abc0fec..9d5aa97d 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -301,7 +301,7 @@ function handleStreamedEvents(events) { } for (const flightId of events.updated_flights) { - Flight.withId(flightId).draw(); + Flight.withId(flightId).update(); } for (const flightId of events.deleted_flights) { @@ -775,20 +775,13 @@ class Waypoint { return this.waypoint.should_mark; } - async timing(dragging) { - if (dragging) { - return "Waiting to recompute TOT..."; - } - return await getJson(`/waypoints/${this.flight.id}/${this.number}/timing`); - } - - async description(dragging) { + description() { const alt = this.waypoint.altitude_ft; const altRef = this.waypoint.altitude_reference; return ( `${this.number} ${this.waypoint.name}
` + `${alt} ft ${altRef}
` + - `${await this.timing(dragging)}` + `${this.waypoint.timing}` ); } @@ -796,19 +789,13 @@ class Waypoint { this.marker.setLatLng(this.position()); } - updateDescription(dragging) { - this.description(dragging).then((description) => { - this.marker.setTooltipContent(description); - }); - } - makeMarker() { const zoom = map.getZoom(); const marker = L.marker(this.position(), { draggable: this.waypoint.is_movable, }) .on("dragstart", (e) => { - this.updateDescription(true); + this.marker.setTooltipContent("Waiting to recompute TOT..."); }) .on("drag", (e) => { const marker = e.target; @@ -824,7 +811,6 @@ class Waypoint { ) .then(() => { this.waypoint.position = destination; - this.updateDescription(false); this.flight.drawCommitBoundary(); }) .catch((err) => { @@ -837,11 +823,9 @@ class Waypoint { }); if (this.flight.selected) { - this.description(false).then((description) => - marker.bindTooltip(description, { - permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM, - }) - ); + marker.bindTooltip(this.description(), { + permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM, + }); } return marker; @@ -961,6 +945,13 @@ class Flight { } } + update() { + getJson(`/flights/${this.id}?with_waypoints=true`).then((flight) => { + this.flight = flight; + this.draw(); + }); + } + draw() { this.drawAircraftLocation(); this.drawFlightPlan();