diff --git a/game/ato/flightwaypoint.py b/game/ato/flightwaypoint.py index 1d9b01b6..1eb84114 100644 --- a/game/ato/flightwaypoint.py +++ b/game/ato/flightwaypoint.py @@ -1,93 +1,31 @@ from __future__ import annotations from collections.abc import Sequence -from dataclasses import field +from dataclasses import dataclass, field from datetime import timedelta -from typing import Optional, TYPE_CHECKING +from typing import Literal, TYPE_CHECKING from dcs import Point -from dcs.unit import Unit -from pydantic.dataclasses import dataclass from game.ato.flightwaypointtype import FlightWaypointType -from game.theater import LatLon from game.theater.theatergroup import TheaterUnit from game.utils import Distance, meters if TYPE_CHECKING: - from game.theater import ConflictTheater, ControlPoint, MissionTarget + from game.theater import ControlPoint, MissionTarget + + +AltitudeReference = Literal["BARO", "RADIO"] @dataclass -class BaseFlightWaypoint: +class FlightWaypoint: name: str waypoint_type: FlightWaypointType x: float y: float - alt: Distance - alt_type: str - - is_movable: bool = field(init=False) - should_mark: bool = field(init=False) - include_in_path: bool = field(init=False) - - # Do not use unless you're sure it's up to date. Pydantic doesn't have support for - # serializing lazy properties so this needs to be stored in the class, but because - # updating it requires a reference to the ConflictTheater it may not always be set, - # or up to date. Call update_latlng to refresh. - latlng: LatLon | None = None - - def __post_init__(self) -> None: - # 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. - # - # Landing, and divert should be changed in the flight settings UI, takeoff - # cannot be changed because that's where the plane is. - # - # Moving the bullseye reference only makes it wrong. - self.is_movable = self.waypoint_type not in { - FlightWaypointType.BULLSEYE, - FlightWaypointType.DIVERT, - FlightWaypointType.LANDING_POINT, - FlightWaypointType.TAKEOFF, - FlightWaypointType.TARGET_POINT, - } - - # We don't need a marker for the departure waypoint (and it's likely - # coincident with the landing waypoint, so hard to see). We do want to draw - # the path from it though. - # - # We also don't need the landing waypoint since we'll be drawing that path - # as well, and it's clear what it is, and only obscured the CP icon. - # - # The divert waypoint also obscures the CP. We don't draw the path to it, - # but it can be seen in the flight settings page, so it's not really a - # problem to exclude it. - # - # Bullseye ought to be (but currently isn't) drawn *once* rather than as a - # flight waypoint. - self.should_mark = self.waypoint_type not in { - FlightWaypointType.BULLSEYE, - FlightWaypointType.DIVERT, - FlightWaypointType.LANDING_POINT, - FlightWaypointType.TAKEOFF, - } - - self.include_in_path = self.waypoint_type not in { - FlightWaypointType.BULLSEYE, - FlightWaypointType.DIVERT, - } - - @property - def position(self) -> Point: - return Point(self.x, self.y) - - def update_latlng(self, theater: ConflictTheater) -> None: - self.latlng = theater.point_to_ll(self.position) - - -class FlightWaypoint(BaseFlightWaypoint): + alt: Distance = meters(0) + alt_type: AltitudeReference = "BARO" control_point: ControlPoint | None = None # TODO: Merge with pretty_name. @@ -95,7 +33,7 @@ class FlightWaypoint(BaseFlightWaypoint): # having three names. A short and long form is enough. description: str = "" - targets: Sequence[MissionTarget | TheaterUnit] = [] + targets: Sequence[MissionTarget | TheaterUnit] = field(default_factory=list) obj_name: str = "" pretty_name: str = "" only_for_player: bool = False @@ -111,29 +49,9 @@ class FlightWaypoint(BaseFlightWaypoint): tot: timedelta | None = None departure_time: timedelta | None = None - def __init__( - self, - waypoint_type: FlightWaypointType, - x: float, - y: float, - alt: Distance = meters(0), - control_point: Optional[ControlPoint] = None, - ) -> None: - """Creates a flight waypoint. - - Args: - waypoint_type: The waypoint type. - x: X coordinate of the waypoint. - y: Y coordinate of the waypoint. - alt: Altitude of the waypoint. By default this is MSL, but it can be - changed to AGL by setting alt_type to "RADIO" - control_point: The control point to associate with this waypoint. Needed for - landing points. - """ - super().__init__( - name="", waypoint_type=waypoint_type, x=x, y=y, alt=alt, alt_type="BARO" - ) - self.control_point = control_point + @property + def position(self) -> Point: + return Point(self.x, self.y) def __hash__(self) -> int: return hash(id(self)) diff --git a/game/server/waypoints/models.py b/game/server/waypoints/models.py new file mode 100644 index 00000000..a959bf40 --- /dev/null +++ b/game/server/waypoints/models.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from pydantic.dataclasses import dataclass + +from game.ato import FlightWaypoint +from game.ato.flightwaypointtype import FlightWaypointType +from game.theater import ConflictTheater, LatLon + + +@dataclass +class FlightWaypointJs: + name: str + position: LatLon + altitude_ft: float + altitude_reference: str + is_movable: bool + should_mark: bool + include_in_path: bool + + @staticmethod + def for_waypoint( + waypoint: FlightWaypoint, theater: ConflictTheater + ) -> 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. + # + # Landing, and divert should be changed in the flight settings UI, takeoff + # cannot be changed because that's where the plane is. + # + # Moving the bullseye reference only makes it wrong. + is_movable = waypoint.waypoint_type not in { + FlightWaypointType.BULLSEYE, + FlightWaypointType.DIVERT, + FlightWaypointType.LANDING_POINT, + FlightWaypointType.TAKEOFF, + FlightWaypointType.TARGET_POINT, + } + + # We don't need a marker for the departure waypoint (and it's likely + # coincident with the landing waypoint, so hard to see). We do want to draw + # the path from it though. + # + # We also don't need the landing waypoint since we'll be drawing that path + # as well, and it's clear what it is, and only obscured the CP icon. + # + # The divert waypoint also obscures the CP. We don't draw the path to it, + # but it can be seen in the flight settings page, so it's not really a + # problem to exclude it. + # + # Bullseye ought to be (but currently isn't) drawn *once* rather than as a + # flight waypoint. + should_mark = waypoint.waypoint_type not in { + FlightWaypointType.BULLSEYE, + FlightWaypointType.DIVERT, + FlightWaypointType.LANDING_POINT, + FlightWaypointType.TAKEOFF, + } + + include_in_path = waypoint.waypoint_type not in { + FlightWaypointType.BULLSEYE, + FlightWaypointType.DIVERT, + } + + return FlightWaypointJs( + name=waypoint.name, + position=theater.point_to_ll(waypoint.position), + altitude_ft=waypoint.alt.feet, + altitude_reference=waypoint.alt_type, + is_movable=is_movable, + should_mark=should_mark, + include_in_path=include_in_path, + ) diff --git a/game/server/waypoints/routes.py b/game/server/waypoints/routes.py index 5400c5a2..367148d6 100644 --- a/game/server/waypoints/routes.py +++ b/game/server/waypoints/routes.py @@ -4,31 +4,36 @@ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from game import Game -from game.ato.flightwaypoint import BaseFlightWaypoint, FlightWaypoint +from game.ato.flightwaypoint import FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType from game.server import GameContext +from game.server.waypoints.models import FlightWaypointJs from game.theater import LatLon from game.utils import meters router: APIRouter = APIRouter(prefix="/waypoints") -@router.get("/{flight_id}", response_model=list[BaseFlightWaypoint]) +@router.get("/{flight_id}", response_model=list[FlightWaypointJs]) def all_waypoints_for_flight( flight_id: UUID, game: Game = Depends(GameContext.get) -) -> list[FlightWaypoint]: +) -> list[FlightWaypointJs]: flight = game.db.flights.get(flight_id) - departure = FlightWaypoint( - FlightWaypointType.TAKEOFF, - flight.departure.position.x, - flight.departure.position.y, - meters(0), + departure = FlightWaypointJs.for_waypoint( + FlightWaypoint( + "TAKEOFF", + FlightWaypointType.TAKEOFF, + flight.departure.position.x, + flight.departure.position.y, + meters(0), + "RADIO", + ), + game.theater, ) - departure.alt_type = "RADIO" - points = [departure] + flight.flight_plan.waypoints - for point in points: - point.update_latlng(game.theater) - return points + return [departure] + [ + FlightWaypointJs.for_waypoint(w, game.theater) + for w in flight.flight_plan.waypoints + ] @router.post("/{flight_id}/{waypoint_idx}/position") @@ -43,9 +48,6 @@ def set_position( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) waypoint = flight.flight_plan.waypoints[waypoint_idx - 1] - if not waypoint.is_movable: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - point = game.theater.ll_to_point(position) waypoint.x = point.x waypoint.y = point.y diff --git a/game/utils.py b/game/utils.py index aceeff24..ad8f8fed 100644 --- a/game/utils.py +++ b/game/utils.py @@ -5,10 +5,10 @@ import math import random from abc import ABC, abstractmethod from collections.abc import Iterable +from dataclasses import dataclass from typing import TypeVar, Union from dcs import Point -from pydantic.dataclasses import dataclass from shapely.geometry import Point as ShapelyPoint METERS_TO_FEET = 3.28084 diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index a1b21ab6..a3577ee7 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -24,7 +24,6 @@ from typing import ( ) from dcs.mapping import Point -from dcs.unit import Unit from shapely.geometry import Point as ShapelyPoint from game.ato.flighttype import FlightType @@ -700,10 +699,12 @@ class StrikeFlightPlan(FormationFlightPlan): @property def target_area_waypoint(self) -> FlightWaypoint: return FlightWaypoint( + "TARGET AREA", FlightWaypointType.TARGET_GROUP_LOC, self.package.target.position.x, self.package.target.position.y, meters(0), + "RADIO", ) @property @@ -903,10 +904,12 @@ class RefuelingFlightPlan(PatrollingFlightPlan): class PackageRefuelingFlightPlan(RefuelingFlightPlan): def target_area_waypoint(self) -> FlightWaypoint: return FlightWaypoint( + "TARGET AREA", FlightWaypointType.TARGET_GROUP_LOC, self.package.target.position.x, self.package.target.position.y, meters(0), + "RADIO", ) @property @@ -921,13 +924,13 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan): # Cheat in a FlightWaypoint for the split point. split: Point = self.package.waypoints.split split_waypoint: FlightWaypoint = FlightWaypoint( - FlightWaypointType.SPLIT, split.x, split.y, altitude + "SPLIT", FlightWaypointType.SPLIT, split.x, split.y, altitude ) # Cheat in a FlightWaypoint for the refuel point. refuel: Point = self.package.waypoints.refuel refuel_waypoint: FlightWaypoint = FlightWaypoint( - FlightWaypointType.REFUEL, refuel.x, refuel.y, altitude + "REFUEL", FlightWaypointType.REFUEL, refuel.x, refuel.y, altitude ) delay_target_to_split: timedelta = self.travel_time_between_waypoints( diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index b62fd669..8e205220 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -14,6 +14,8 @@ from typing import ( from dcs.mapping import Point +from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint +from game.ato.flightwaypointtype import FlightWaypointType from game.theater import ( ControlPoint, MissionTarget, @@ -22,8 +24,6 @@ from game.theater import ( TheaterUnit, ) from game.utils import Distance, meters, nautical_miles -from game.ato.flightwaypointtype import FlightWaypointType -from game.ato.flightwaypoint import FlightWaypoint if TYPE_CHECKING: from game.ato.flight import Flight @@ -68,25 +68,26 @@ class WaypointBuilder: """ position = departure.position if isinstance(departure, OffMapSpawn): - waypoint = FlightWaypoint( + return FlightWaypoint( + "NAV", FlightWaypointType.NAV, position.x, position.y, meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, + description="Enter theater", + pretty_name="Enter theater", ) - waypoint.name = "NAV" - waypoint.alt_type = "BARO" - waypoint.description = "Enter theater" - waypoint.pretty_name = "Enter theater" - else: - waypoint = FlightWaypoint( - FlightWaypointType.TAKEOFF, position.x, position.y, meters(0) - ) - waypoint.name = "TAKEOFF" - waypoint.alt_type = "RADIO" - waypoint.description = "Takeoff" - waypoint.pretty_name = "Takeoff" - return waypoint + + return FlightWaypoint( + "TAKEOFF", + FlightWaypointType.TAKEOFF, + position.x, + position.y, + meters(0), + alt_type="RADIO", + description="Takeoff", + pretty_name="Takeoff", + ) def land(self, arrival: ControlPoint) -> FlightWaypoint: """Create descent waypoint for the given arrival airfield or carrier. @@ -96,29 +97,27 @@ class WaypointBuilder: """ position = arrival.position if isinstance(arrival, OffMapSpawn): - waypoint = FlightWaypoint( + return FlightWaypoint( + "NAV", FlightWaypointType.NAV, position.x, position.y, meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, + description="Exit theater", + pretty_name="Exit theater", ) - waypoint.name = "NAV" - waypoint.alt_type = "BARO" - waypoint.description = "Exit theater" - waypoint.pretty_name = "Exit theater" - else: - waypoint = FlightWaypoint( - FlightWaypointType.LANDING_POINT, - position.x, - position.y, - meters(0), - control_point=arrival, - ) - waypoint.name = "LANDING" - waypoint.alt_type = "RADIO" - waypoint.description = "Land" - waypoint.pretty_name = "Land" - return waypoint + + return FlightWaypoint( + "LANDING", + FlightWaypointType.LANDING_POINT, + position.x, + position.y, + meters(0), + alt_type="RADIO", + description="Land", + pretty_name="Land", + control_point=arrival, + ) def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]: """Create divert waypoint for the given arrival airfield or carrier. @@ -130,6 +129,7 @@ class WaypointBuilder: return None position = divert.position + altitude_type: AltitudeReference if isinstance(divert, OffMapSpawn): if self.is_helo: altitude = meters(500) @@ -140,88 +140,94 @@ class WaypointBuilder: altitude = meters(0) altitude_type = "RADIO" - waypoint = FlightWaypoint( + return FlightWaypoint( + "DIVERT", FlightWaypointType.DIVERT, position.x, position.y, altitude, + alt_type=altitude_type, + description="Divert", + pretty_name="Divert", + only_for_player=True, control_point=divert, ) - waypoint.alt_type = altitude_type - waypoint.name = "DIVERT" - waypoint.description = "Divert" - waypoint.pretty_name = "Divert" - waypoint.only_for_player = True - return waypoint def bullseye(self) -> FlightWaypoint: - waypoint = FlightWaypoint( + return FlightWaypoint( + "BULLSEYE", FlightWaypointType.BULLSEYE, self._bullseye.position.x, self._bullseye.position.y, meters(0), + description="Bullseye", + pretty_name="Bullseye", + only_for_player=True, ) - waypoint.pretty_name = "Bullseye" - waypoint.description = "Bullseye" - waypoint.name = "BULLSEYE" - waypoint.only_for_player = True - return waypoint def hold(self, position: Point) -> FlightWaypoint: - waypoint = FlightWaypoint( + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + + return FlightWaypoint( + "HOLD", FlightWaypointType.LOITER, position.x, position.y, meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, + alt_type, + description="Wait until push time", + pretty_name="Hold", ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.pretty_name = "Hold" - waypoint.description = "Wait until push time" - waypoint.name = "HOLD" - return waypoint def join(self, position: Point) -> FlightWaypoint: - waypoint = FlightWaypoint( + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + + return FlightWaypoint( + "JOIN", FlightWaypointType.JOIN, position.x, position.y, meters(80) if self.is_helo else self.doctrine.ingress_altitude, + alt_type, + description="Rendezvous with package", + pretty_name="Join", ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.pretty_name = "Join" - waypoint.description = "Rendezvous with package" - waypoint.name = "JOIN" - return waypoint def refuel(self, position: Point) -> FlightWaypoint: - waypoint = FlightWaypoint( + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + + return FlightWaypoint( + "REFUEL", FlightWaypointType.REFUEL, position.x, position.y, meters(80) if self.is_helo else self.doctrine.ingress_altitude, + alt_type, + description="Refuel from tanker", + pretty_name="Refuel", ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.pretty_name = "Refuel" - waypoint.description = "Refuel from tanker" - waypoint.name = "REFUEL" - return waypoint def split(self, position: Point) -> FlightWaypoint: - waypoint = FlightWaypoint( + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + + return FlightWaypoint( + "SPLIT", FlightWaypointType.SPLIT, position.x, position.y, meters(80) if self.is_helo else self.doctrine.ingress_altitude, + alt_type, + description="Depart from package", + pretty_name="Split", ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.pretty_name = "Split" - waypoint.description = "Depart from package" - waypoint.name = "SPLIT" - return waypoint def ingress( self, @@ -229,33 +235,37 @@ class WaypointBuilder: position: Point, objective: MissionTarget, ) -> FlightWaypoint: - waypoint = FlightWaypoint( + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + + return FlightWaypoint( + "INGRESS", ingress_type, position.x, position.y, meters(60) if self.is_helo else self.doctrine.ingress_altitude, + alt_type, + description=f"INGRESS on {objective.name}", + pretty_name=f"INGRESS on {objective.name}", + targets=objective.strike_targets, ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.pretty_name = "INGRESS on " + objective.name - waypoint.description = "INGRESS on " + objective.name - waypoint.name = "INGRESS" - waypoint.targets = objective.strike_targets - return waypoint def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint: - waypoint = FlightWaypoint( + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + + return FlightWaypoint( + "EGRESS", FlightWaypointType.EGRESS, position.x, position.y, meters(60) if self.is_helo else self.doctrine.ingress_altitude, + alt_type, + description=f"EGRESS from {target.name}", + pretty_name=f"EGRESS from {target.name}", ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.pretty_name = "EGRESS from " + target.name - waypoint.description = "EGRESS from " + target.name - waypoint.name = "EGRESS" - return waypoint def bai_group(self, target: StrikeTarget) -> FlightWaypoint: return self._target_point(target, f"ATTACK {target.name}") @@ -271,21 +281,20 @@ class WaypointBuilder: @staticmethod def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint: - waypoint = FlightWaypoint( + return FlightWaypoint( + target.name, FlightWaypointType.TARGET_POINT, target.target.position.x, target.target.position.y, meters(0), + "RADIO", + description=description, + pretty_name=description, + # The target waypoints are only for the player's benefit. AI tasks for + # the target are set on the ingress point so that they begin their attack + # *before* reaching the target. + only_for_player=True, ) - waypoint.description = description - waypoint.pretty_name = description - waypoint.name = target.name - waypoint.alt_type = "RADIO" - # The target waypoints are only for the player's benefit. AI tasks for - # the target are set on the ingress point so they begin their attack - # *before* reaching the target. - waypoint.only_for_player = True - return waypoint def strike_area(self, target: MissionTarget) -> FlightWaypoint: return self._target_area(f"STRIKE {target.name}", target) @@ -304,15 +313,15 @@ class WaypointBuilder: name: str, location: MissionTarget, flyover: bool = False ) -> FlightWaypoint: waypoint = FlightWaypoint( + name, FlightWaypointType.TARGET_GROUP_LOC, location.position.x, location.position.y, meters(0), + "RADIO", + description=name, + pretty_name=name, ) - waypoint.description = name - waypoint.pretty_name = name - waypoint.name = name - waypoint.alt_type = "RADIO" # Most target waypoints are only for the player's benefit. AI tasks for # the target are set on the ingress point so they begin their attack @@ -328,17 +337,16 @@ class WaypointBuilder: return waypoint def cas(self, position: Point) -> FlightWaypoint: - waypoint = FlightWaypoint( + return FlightWaypoint( + "CAS", FlightWaypointType.CAS, position.x, position.y, meters(60) if self.is_helo else meters(1000), + "RADIO", + description="Provide CAS", + pretty_name="CAS", ) - waypoint.alt_type = "RADIO" - waypoint.description = "Provide CAS" - waypoint.name = "CAS" - waypoint.pretty_name = "CAS" - return waypoint @staticmethod def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint: @@ -348,13 +356,15 @@ class WaypointBuilder: position: Position of the waypoint. altitude: Altitude of the racetrack. """ - waypoint = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude + return FlightWaypoint( + "RACETRACK START", + FlightWaypointType.PATROL_TRACK, + position.x, + position.y, + altitude, + description="Orbit between this point and the next point", + pretty_name="Race-track start", ) - waypoint.name = "RACETRACK START" - waypoint.description = "Orbit between this point and the next point" - waypoint.pretty_name = "Race-track start" - return waypoint @staticmethod def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint: @@ -364,13 +374,15 @@ class WaypointBuilder: position: Position of the waypoint. altitude: Altitude of the racetrack. """ - waypoint = FlightWaypoint( - FlightWaypointType.PATROL, position.x, position.y, altitude + return FlightWaypoint( + "RACETRACK END", + FlightWaypointType.PATROL, + position.x, + position.y, + altitude, + description="Orbit between this point and the previous point", + pretty_name="Race-track end", ) - waypoint.name = "RACETRACK END" - waypoint.description = "Orbit between this point and the previous point" - waypoint.pretty_name = "Race-track end" - return waypoint def race_track( self, start: Point, end: Point, altitude: Distance @@ -396,11 +408,15 @@ class WaypointBuilder: altitude: Altitude of the racetrack. """ - waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude) - waypoint.name = "ORBIT" - waypoint.description = "Anchor and hold at this point" - waypoint.pretty_name = "Orbit" - return waypoint + return FlightWaypoint( + "ORBIT", + FlightWaypointType.LOITER, + start.x, + start.y, + altitude, + description="Anchor and hold at this point", + pretty_name="Orbit", + ) @staticmethod def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint: @@ -410,13 +426,15 @@ class WaypointBuilder: position: Position of the waypoint. altitude: Altitude of the sweep in meters. """ - waypoint = FlightWaypoint( - FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude + return FlightWaypoint( + "SWEEP START", + FlightWaypointType.INGRESS_SWEEP, + position.x, + position.y, + altitude, + description="Proceed to the target and engage enemy aircraft", + pretty_name="Sweep start", ) - waypoint.name = "SWEEP START" - waypoint.description = "Proceed to the target and engage enemy aircraft" - waypoint.pretty_name = "Sweep start" - return waypoint @staticmethod def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint: @@ -426,13 +444,15 @@ class WaypointBuilder: position: Position of the waypoint. altitude: Altitude of the sweep in meters. """ - waypoint = FlightWaypoint( - FlightWaypointType.EGRESS, position.x, position.y, altitude + return FlightWaypoint( + "SWEEP END", + FlightWaypointType.EGRESS, + position.x, + position.y, + altitude, + description="End of sweep", + pretty_name="Sweep end", ) - waypoint.name = "SWEEP END" - waypoint.description = "End of sweep" - waypoint.pretty_name = "Sweep end" - return waypoint def sweep( self, start: Point, end: Point, altitude: Distance @@ -457,6 +477,10 @@ class WaypointBuilder: ingress: The package ingress point. target: The mission target. """ + alt_type: AltitudeReference = "BARO" + if self.is_helo: + alt_type = "RADIO" + # This would preferably be no points at all, and instead the Escort task # would begin on the join point and end on the split point, however the # escort task does not appear to work properly (see the longer @@ -464,18 +488,16 @@ class WaypointBuilder: # the escort flights a flight plan including the ingress point and target area. ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target) - waypoint = FlightWaypoint( + return ingress_wp, FlightWaypoint( + "TARGET", FlightWaypointType.TARGET_GROUP_LOC, target.position.x, target.position.y, meters(60) if self.is_helo else self.doctrine.ingress_altitude, + alt_type, + description="Escort the package", + pretty_name="Target area", ) - if self.is_helo: - waypoint.alt_type = "RADIO" - waypoint.name = "TARGET" - waypoint.description = "Escort the package" - waypoint.pretty_name = "Target area" - return ingress_wp, waypoint @staticmethod def pickup(control_point: ControlPoint) -> FlightWaypoint: @@ -484,17 +506,16 @@ class WaypointBuilder: Args: control_point: Pick up location. """ - waypoint = FlightWaypoint( + return FlightWaypoint( + "PICKUP", FlightWaypointType.PICKUP, control_point.position.x, control_point.position.y, meters(0), + "RADIO", + description=f"Pick up cargo from {control_point}", + pretty_name="Pick up location", ) - waypoint.alt_type = "RADIO" - waypoint.name = "PICKUP" - waypoint.description = f"Pick up cargo from {control_point}" - waypoint.pretty_name = "Pick up location" - return waypoint @staticmethod def drop_off(control_point: ControlPoint) -> FlightWaypoint: @@ -503,18 +524,17 @@ class WaypointBuilder: Args: control_point: Drop-off location. """ - waypoint = FlightWaypoint( + return FlightWaypoint( + "DROP OFF", FlightWaypointType.PICKUP, control_point.position.x, control_point.position.y, meters(0), + "RADIO", + description=f"Drop off cargo at {control_point}", + pretty_name="Drop off location", control_point=control_point, ) - waypoint.alt_type = "RADIO" - waypoint.name = "DROP OFF" - waypoint.description = f"Drop off cargo at {control_point}" - waypoint.pretty_name = "Drop off location" - return waypoint @staticmethod def nav( @@ -527,15 +547,20 @@ class WaypointBuilder: altitude: Altitude of the waypoint. altitude_is_agl: True for altitude is AGL. False if altitude is MSL. """ - waypoint = FlightWaypoint( - FlightWaypointType.NAV, position.x, position.y, altitude - ) + alt_type: AltitudeReference = "BARO" if altitude_is_agl: - waypoint.alt_type = "RADIO" - waypoint.name = "NAV" - waypoint.description = "NAV" - waypoint.pretty_name = "Nav" - return waypoint + alt_type = "RADIO" + + return FlightWaypoint( + "NAV", + FlightWaypointType.NAV, + position.x, + position.y, + altitude, + alt_type, + description="NAV", + pretty_name="Nav", + ) def nav_path( self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index b23540a0..83136fbc 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -768,7 +768,7 @@ class Waypoint { } position() { - return this.waypoint.latlng; + return this.waypoint.position; } shouldMark() { @@ -783,10 +783,8 @@ class Waypoint { } async description(dragging) { - const alt = Math.floor( - this.waypoint.alt.distance_in_meters * METERS_TO_FEET - ); - const altRef = this.waypoint.alt_type == "BARO" ? "MSL" : "AGL"; + const alt = this.waypoint.altitude_ft; + const altRef = this.waypoint.altitude_reference; return ( `${this.number} ${this.waypoint.name}
` + `${alt} ft ${altRef}
` +