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 __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from typing import Optional, TYPE_CHECKING from typing import Literal, TYPE_CHECKING
from dcs import Point from dcs import Point
from dcs.unit import Unit
from pydantic.dataclasses import dataclass
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.theater import LatLon
from game.theater.theatergroup import TheaterUnit from game.theater.theatergroup import TheaterUnit
from game.utils import Distance, meters from game.utils import Distance, meters
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater import ConflictTheater, ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
AltitudeReference = Literal["BARO", "RADIO"]
@dataclass @dataclass
class BaseFlightWaypoint: class FlightWaypoint:
name: str name: str
waypoint_type: FlightWaypointType waypoint_type: FlightWaypointType
x: float x: float
y: float y: float
alt: Distance alt: Distance = meters(0)
alt_type: str alt_type: AltitudeReference = "BARO"
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):
control_point: ControlPoint | None = None control_point: ControlPoint | None = None
# TODO: Merge with pretty_name. # TODO: Merge with pretty_name.
@ -95,7 +33,7 @@ class FlightWaypoint(BaseFlightWaypoint):
# having three names. A short and long form is enough. # having three names. A short and long form is enough.
description: str = "" description: str = ""
targets: Sequence[MissionTarget | TheaterUnit] = [] targets: Sequence[MissionTarget | TheaterUnit] = field(default_factory=list)
obj_name: str = "" obj_name: str = ""
pretty_name: str = "" pretty_name: str = ""
only_for_player: bool = False only_for_player: bool = False
@ -111,29 +49,9 @@ class FlightWaypoint(BaseFlightWaypoint):
tot: timedelta | None = None tot: timedelta | None = None
departure_time: timedelta | None = None departure_time: timedelta | None = None
def __init__( @property
self, def position(self) -> Point:
waypoint_type: FlightWaypointType, return Point(self.x, self.y)
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
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(id(self)) 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 fastapi import APIRouter, Depends, HTTPException, status
from game import Game 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.ato.flightwaypointtype import FlightWaypointType
from game.server import GameContext from game.server import GameContext
from game.server.waypoints.models import FlightWaypointJs
from game.theater import LatLon from game.theater import LatLon
from game.utils import meters from game.utils import meters
router: APIRouter = APIRouter(prefix="/waypoints") 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( def all_waypoints_for_flight(
flight_id: UUID, game: Game = Depends(GameContext.get) flight_id: UUID, game: Game = Depends(GameContext.get)
) -> list[FlightWaypoint]: ) -> list[FlightWaypointJs]:
flight = game.db.flights.get(flight_id) flight = game.db.flights.get(flight_id)
departure = FlightWaypoint( departure = FlightWaypointJs.for_waypoint(
FlightWaypointType.TAKEOFF, FlightWaypoint(
flight.departure.position.x, "TAKEOFF",
flight.departure.position.y, FlightWaypointType.TAKEOFF,
meters(0), flight.departure.position.x,
flight.departure.position.y,
meters(0),
"RADIO",
),
game.theater,
) )
departure.alt_type = "RADIO" return [departure] + [
points = [departure] + flight.flight_plan.waypoints FlightWaypointJs.for_waypoint(w, game.theater)
for point in points: for w in flight.flight_plan.waypoints
point.update_latlng(game.theater) ]
return points
@router.post("/{flight_id}/{waypoint_idx}/position") @router.post("/{flight_id}/{waypoint_idx}/position")
@ -43,9 +48,6 @@ def set_position(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1] 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) point = game.theater.ll_to_point(position)
waypoint.x = point.x waypoint.x = point.x
waypoint.y = point.y waypoint.y = point.y

View File

@ -5,10 +5,10 @@ import math
import random import random
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass
from typing import TypeVar, Union from typing import TypeVar, Union
from dcs import Point from dcs import Point
from pydantic.dataclasses import dataclass
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
METERS_TO_FEET = 3.28084 METERS_TO_FEET = 3.28084

View File

@ -24,7 +24,6 @@ from typing import (
) )
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
@ -700,10 +699,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property @property
def target_area_waypoint(self) -> FlightWaypoint: def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint( return FlightWaypoint(
"TARGET AREA",
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.x, self.package.target.position.x,
self.package.target.position.y, self.package.target.position.y,
meters(0), meters(0),
"RADIO",
) )
@property @property
@ -903,10 +904,12 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
class PackageRefuelingFlightPlan(RefuelingFlightPlan): class PackageRefuelingFlightPlan(RefuelingFlightPlan):
def target_area_waypoint(self) -> FlightWaypoint: def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint( return FlightWaypoint(
"TARGET AREA",
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.x, self.package.target.position.x,
self.package.target.position.y, self.package.target.position.y,
meters(0), meters(0),
"RADIO",
) )
@property @property
@ -921,13 +924,13 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
# Cheat in a FlightWaypoint for the split point. # Cheat in a FlightWaypoint for the split point.
split: Point = self.package.waypoints.split split: Point = self.package.waypoints.split
split_waypoint: FlightWaypoint = FlightWaypoint( 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. # Cheat in a FlightWaypoint for the refuel point.
refuel: Point = self.package.waypoints.refuel refuel: Point = self.package.waypoints.refuel
refuel_waypoint: FlightWaypoint = FlightWaypoint( 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( 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 dcs.mapping import Point
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.theater import ( from game.theater import (
ControlPoint, ControlPoint,
MissionTarget, MissionTarget,
@ -22,8 +24,6 @@ from game.theater import (
TheaterUnit, TheaterUnit,
) )
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
@ -68,25 +68,26 @@ class WaypointBuilder:
""" """
position = departure.position position = departure.position
if isinstance(departure, OffMapSpawn): if isinstance(departure, OffMapSpawn):
waypoint = FlightWaypoint( return FlightWaypoint(
"NAV",
FlightWaypointType.NAV, FlightWaypointType.NAV,
position.x, position.x,
position.y, position.y,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, 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" return FlightWaypoint(
waypoint.description = "Enter theater" "TAKEOFF",
waypoint.pretty_name = "Enter theater" FlightWaypointType.TAKEOFF,
else: position.x,
waypoint = FlightWaypoint( position.y,
FlightWaypointType.TAKEOFF, position.x, position.y, meters(0) meters(0),
) alt_type="RADIO",
waypoint.name = "TAKEOFF" description="Takeoff",
waypoint.alt_type = "RADIO" pretty_name="Takeoff",
waypoint.description = "Takeoff" )
waypoint.pretty_name = "Takeoff"
return waypoint
def land(self, arrival: ControlPoint) -> FlightWaypoint: def land(self, arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier. """Create descent waypoint for the given arrival airfield or carrier.
@ -96,29 +97,27 @@ class WaypointBuilder:
""" """
position = arrival.position position = arrival.position
if isinstance(arrival, OffMapSpawn): if isinstance(arrival, OffMapSpawn):
waypoint = FlightWaypoint( return FlightWaypoint(
"NAV",
FlightWaypointType.NAV, FlightWaypointType.NAV,
position.x, position.x,
position.y, position.y,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, 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" return FlightWaypoint(
waypoint.description = "Exit theater" "LANDING",
waypoint.pretty_name = "Exit theater" FlightWaypointType.LANDING_POINT,
else: position.x,
waypoint = FlightWaypoint( position.y,
FlightWaypointType.LANDING_POINT, meters(0),
position.x, alt_type="RADIO",
position.y, description="Land",
meters(0), pretty_name="Land",
control_point=arrival, control_point=arrival,
) )
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
return waypoint
def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]: def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier. """Create divert waypoint for the given arrival airfield or carrier.
@ -130,6 +129,7 @@ class WaypointBuilder:
return None return None
position = divert.position position = divert.position
altitude_type: AltitudeReference
if isinstance(divert, OffMapSpawn): if isinstance(divert, OffMapSpawn):
if self.is_helo: if self.is_helo:
altitude = meters(500) altitude = meters(500)
@ -140,88 +140,94 @@ class WaypointBuilder:
altitude = meters(0) altitude = meters(0)
altitude_type = "RADIO" altitude_type = "RADIO"
waypoint = FlightWaypoint( return FlightWaypoint(
"DIVERT",
FlightWaypointType.DIVERT, FlightWaypointType.DIVERT,
position.x, position.x,
position.y, position.y,
altitude, altitude,
alt_type=altitude_type,
description="Divert",
pretty_name="Divert",
only_for_player=True,
control_point=divert, 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: def bullseye(self) -> FlightWaypoint:
waypoint = FlightWaypoint( return FlightWaypoint(
"BULLSEYE",
FlightWaypointType.BULLSEYE, FlightWaypointType.BULLSEYE,
self._bullseye.position.x, self._bullseye.position.x,
self._bullseye.position.y, self._bullseye.position.y,
meters(0), 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: def hold(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( alt_type: AltitudeReference = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint(
"HOLD",
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
position.x, position.x,
position.y, position.y,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, 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: def join(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( alt_type: AltitudeReference = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint(
"JOIN",
FlightWaypointType.JOIN, FlightWaypointType.JOIN,
position.x, position.x,
position.y, position.y,
meters(80) if self.is_helo else self.doctrine.ingress_altitude, 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: def refuel(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( alt_type: AltitudeReference = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint(
"REFUEL",
FlightWaypointType.REFUEL, FlightWaypointType.REFUEL,
position.x, position.x,
position.y, position.y,
meters(80) if self.is_helo else self.doctrine.ingress_altitude, 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: def split(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( alt_type: AltitudeReference = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint(
"SPLIT",
FlightWaypointType.SPLIT, FlightWaypointType.SPLIT,
position.x, position.x,
position.y, position.y,
meters(80) if self.is_helo else self.doctrine.ingress_altitude, 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( def ingress(
self, self,
@ -229,33 +235,37 @@ class WaypointBuilder:
position: Point, position: Point,
objective: MissionTarget, objective: MissionTarget,
) -> FlightWaypoint: ) -> FlightWaypoint:
waypoint = FlightWaypoint( alt_type: AltitudeReference = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint(
"INGRESS",
ingress_type, ingress_type,
position.x, position.x,
position.y, position.y,
meters(60) if self.is_helo else self.doctrine.ingress_altitude, 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: 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, FlightWaypointType.EGRESS,
position.x, position.x,
position.y, position.y,
meters(60) if self.is_helo else self.doctrine.ingress_altitude, 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: def bai_group(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"ATTACK {target.name}") return self._target_point(target, f"ATTACK {target.name}")
@ -271,21 +281,20 @@ class WaypointBuilder:
@staticmethod @staticmethod
def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint: def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
waypoint = FlightWaypoint( return FlightWaypoint(
target.name,
FlightWaypointType.TARGET_POINT, FlightWaypointType.TARGET_POINT,
target.target.position.x, target.target.position.x,
target.target.position.y, target.target.position.y,
meters(0), 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: def strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"STRIKE {target.name}", target) return self._target_area(f"STRIKE {target.name}", target)
@ -304,15 +313,15 @@ class WaypointBuilder:
name: str, location: MissionTarget, flyover: bool = False name: str, location: MissionTarget, flyover: bool = False
) -> FlightWaypoint: ) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
name,
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
location.position.x, location.position.x,
location.position.y, location.position.y,
meters(0), 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 # 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 # the target are set on the ingress point so they begin their attack
@ -328,17 +337,16 @@ class WaypointBuilder:
return waypoint return waypoint
def cas(self, position: Point) -> FlightWaypoint: def cas(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( return FlightWaypoint(
"CAS",
FlightWaypointType.CAS, FlightWaypointType.CAS,
position.x, position.x,
position.y, position.y,
meters(60) if self.is_helo else meters(1000), 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 @staticmethod
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint: def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
@ -348,13 +356,15 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( return FlightWaypoint(
FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude "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 @staticmethod
def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint: def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint:
@ -364,13 +374,15 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( return FlightWaypoint(
FlightWaypointType.PATROL, position.x, position.y, altitude "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( def race_track(
self, start: Point, end: Point, altitude: Distance self, start: Point, end: Point, altitude: Distance
@ -396,11 +408,15 @@ class WaypointBuilder:
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude) return FlightWaypoint(
waypoint.name = "ORBIT" "ORBIT",
waypoint.description = "Anchor and hold at this point" FlightWaypointType.LOITER,
waypoint.pretty_name = "Orbit" start.x,
return waypoint start.y,
altitude,
description="Anchor and hold at this point",
pretty_name="Orbit",
)
@staticmethod @staticmethod
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint: def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
@ -410,13 +426,15 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
waypoint = FlightWaypoint( return FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude "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 @staticmethod
def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint: def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint:
@ -426,13 +444,15 @@ class WaypointBuilder:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
waypoint = FlightWaypoint( return FlightWaypoint(
FlightWaypointType.EGRESS, position.x, position.y, altitude "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( def sweep(
self, start: Point, end: Point, altitude: Distance self, start: Point, end: Point, altitude: Distance
@ -457,6 +477,10 @@ class WaypointBuilder:
ingress: The package ingress point. ingress: The package ingress point.
target: The mission target. 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 # 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 # 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 # 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. # the escort flights a flight plan including the ingress point and target area.
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target) ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint( return ingress_wp, FlightWaypoint(
"TARGET",
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
target.position.x, target.position.x,
target.position.y, target.position.y,
meters(60) if self.is_helo else self.doctrine.ingress_altitude, 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 @staticmethod
def pickup(control_point: ControlPoint) -> FlightWaypoint: def pickup(control_point: ControlPoint) -> FlightWaypoint:
@ -484,17 +506,16 @@ class WaypointBuilder:
Args: Args:
control_point: Pick up location. control_point: Pick up location.
""" """
waypoint = FlightWaypoint( return FlightWaypoint(
"PICKUP",
FlightWaypointType.PICKUP, FlightWaypointType.PICKUP,
control_point.position.x, control_point.position.x,
control_point.position.y, control_point.position.y,
meters(0), 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 @staticmethod
def drop_off(control_point: ControlPoint) -> FlightWaypoint: def drop_off(control_point: ControlPoint) -> FlightWaypoint:
@ -503,18 +524,17 @@ class WaypointBuilder:
Args: Args:
control_point: Drop-off location. control_point: Drop-off location.
""" """
waypoint = FlightWaypoint( return FlightWaypoint(
"DROP OFF",
FlightWaypointType.PICKUP, FlightWaypointType.PICKUP,
control_point.position.x, control_point.position.x,
control_point.position.y, control_point.position.y,
meters(0), meters(0),
"RADIO",
description=f"Drop off cargo at {control_point}",
pretty_name="Drop off location",
control_point=control_point, 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 @staticmethod
def nav( def nav(
@ -527,15 +547,20 @@ class WaypointBuilder:
altitude: Altitude of the waypoint. altitude: Altitude of the waypoint.
altitude_is_agl: True for altitude is AGL. False if altitude is MSL. altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
""" """
waypoint = FlightWaypoint( alt_type: AltitudeReference = "BARO"
FlightWaypointType.NAV, position.x, position.y, altitude
)
if altitude_is_agl: if altitude_is_agl:
waypoint.alt_type = "RADIO" alt_type = "RADIO"
waypoint.name = "NAV"
waypoint.description = "NAV" return FlightWaypoint(
waypoint.pretty_name = "Nav" "NAV",
return waypoint FlightWaypointType.NAV,
position.x,
position.y,
altitude,
alt_type,
description="NAV",
pretty_name="Nav",
)
def nav_path( def nav_path(
self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False

View File

@ -768,7 +768,7 @@ class Waypoint {
} }
position() { position() {
return this.waypoint.latlng; return this.waypoint.position;
} }
shouldMark() { shouldMark() {
@ -783,10 +783,8 @@ class Waypoint {
} }
async description(dragging) { async description(dragging) {
const alt = Math.floor( const alt = this.waypoint.altitude_ft;
this.waypoint.alt.distance_in_meters * METERS_TO_FEET const altRef = this.waypoint.altitude_reference;
);
const altRef = this.waypoint.alt_type == "BARO" ? "MSL" : "AGL";
return ( return (
`${this.number} ${this.waypoint.name}<br />` + `${this.number} ${this.waypoint.name}<br />` +
`${alt} ft ${altRef}<br />` + `${alt} ft ${altRef}<br />` +