Un-pydantic FlightWaypoint.

Apparently it's a bad idea to try to make the core data pydantic models,
and those should really be treated more as a view-model. Doing otherwise
causes odd patterns (like the UI info I had leaked into the core type),
and makes it harder to interop with third-party types.
This commit is contained in:
Dan Albert 2022-02-21 23:10:28 -08:00
parent c5ab0431a9
commit 3e08e0e8b6
7 changed files with 305 additions and 286 deletions

View File

@ -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))

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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}<br />` +
`${alt} ft ${altRef}<br />` +