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