mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Move waypoints and commit boundaries to FastAPI.
This commit is contained in:
parent
b533633494
commit
c16ca40894
@ -98,6 +98,10 @@ class Flight:
|
||||
state["id"] = uuid.uuid4()
|
||||
self.__dict__.update(state)
|
||||
|
||||
@property
|
||||
def blue(self) -> bool:
|
||||
return self.squadron.player
|
||||
|
||||
@property
|
||||
def departure(self) -> ControlPoint:
|
||||
return self.squadron.location
|
||||
|
||||
@ -1,20 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import field
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Sequence, TYPE_CHECKING, Union
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unit import Unit
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from game.utils import Distance, meters
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.theater import LatLon
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class FlightWaypoint:
|
||||
@dataclass
|
||||
class BaseFlightWaypoint:
|
||||
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):
|
||||
control_point: ControlPoint | None = None
|
||||
|
||||
# TODO: Merge with pretty_name.
|
||||
# Only used in the waypoint list in the flight edit page. No sense
|
||||
# having three names. A short and long form is enough.
|
||||
description: str = ""
|
||||
|
||||
targets: Sequence[MissionTarget | Unit] = []
|
||||
obj_name: str = ""
|
||||
pretty_name: str = ""
|
||||
only_for_player: bool = False
|
||||
flyover: bool = False
|
||||
|
||||
# The minimum amount of fuel remaining at this waypoint in pounds.
|
||||
min_fuel: float | None = None
|
||||
|
||||
# These are set very late by the air conflict generator (part of mission
|
||||
# generation). We do it late so that we don't need to propagate changes
|
||||
# to waypoint times whenever the player alters the package TOT or the
|
||||
# flight's offset in the UI.
|
||||
tot: timedelta | None = None
|
||||
departure_time: timedelta | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
waypoint_type: FlightWaypointType,
|
||||
@ -34,32 +129,10 @@ class FlightWaypoint:
|
||||
control_point: The control point to associate with this waypoint. Needed for
|
||||
landing points.
|
||||
"""
|
||||
self.waypoint_type = waypoint_type
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.alt = alt
|
||||
super().__init__(
|
||||
name="", waypoint_type=waypoint_type, x=x, y=y, alt=alt, alt_type="BARO"
|
||||
)
|
||||
self.control_point = control_point
|
||||
self.alt_type = "BARO"
|
||||
self.name = ""
|
||||
# TODO: Merge with pretty_name.
|
||||
# Only used in the waypoint list in the flight edit page. No sense
|
||||
# having three names. A short and long form is enough.
|
||||
self.description = ""
|
||||
self.targets: Sequence[Union[MissionTarget, Unit]] = []
|
||||
self.obj_name = ""
|
||||
self.pretty_name = ""
|
||||
self.only_for_player = False
|
||||
self.flyover = False
|
||||
# The minimum amount of fuel remaining at this waypoint in pounds.
|
||||
self.min_fuel: Optional[float] = None
|
||||
|
||||
# These are set very late by the air conflict generator (part of mission
|
||||
# generation). We do it late so that we don't need to propagate changes
|
||||
# to waypoint times whenever the player alters the package TOT or the
|
||||
# flight's offset in the UI.
|
||||
self.tot: Optional[timedelta] = None
|
||||
self.departure_time: Optional[timedelta] = None
|
||||
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
return Point(self.x, self.y)
|
||||
def __hash__(self) -> int:
|
||||
return hash(id(self))
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from enum import Enum
|
||||
from enum import IntEnum, unique
|
||||
|
||||
|
||||
class FlightWaypointType(Enum):
|
||||
@unique
|
||||
class FlightWaypointType(IntEnum):
|
||||
"""Enumeration of waypoint types.
|
||||
|
||||
The value of the enum has no meaning but should remain stable to prevent breaking
|
||||
|
||||
@ -187,9 +187,9 @@ class KneeboardPage:
|
||||
|
||||
@staticmethod
|
||||
def format_ll(ll: LatLon) -> str:
|
||||
ns = "N" if ll.latitude >= 0 else "S"
|
||||
ew = "E" if ll.longitude >= 0 else "W"
|
||||
return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}"
|
||||
ns = "N" if ll.lat >= 0 else "S"
|
||||
ew = "E" if ll.lng >= 0 else "W"
|
||||
return f"{ll.lat:.4}°{ns} {ll.lng:.4}°{ew}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
from . import debuggeometries, eventstream
|
||||
from . import debuggeometries, eventstream, flights, waypoints
|
||||
from .security import ApiKeyManager
|
||||
|
||||
app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)])
|
||||
app.include_router(debuggeometries.router)
|
||||
app.include_router(eventstream.router)
|
||||
app.include_router(flights.router)
|
||||
app.include_router(waypoints.router)
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
from game import Game
|
||||
from qt_ui.models import GameModel
|
||||
|
||||
|
||||
class GameContext:
|
||||
_game: Game | None
|
||||
_game_model: GameModel
|
||||
|
||||
@classmethod
|
||||
def set(cls, game: Game | None) -> None:
|
||||
cls._game = game
|
||||
def set_model(cls, game_model: GameModel) -> None:
|
||||
cls._game_model = game_model
|
||||
|
||||
@classmethod
|
||||
def get(cls) -> Game:
|
||||
if cls._game is None:
|
||||
if cls._game_model.game is None:
|
||||
raise RuntimeError("GameContext has no Game set")
|
||||
return cls._game
|
||||
return cls._game_model.game
|
||||
|
||||
@classmethod
|
||||
def get_model(cls) -> GameModel:
|
||||
return cls._game_model
|
||||
|
||||
1
game/server/flights/__init__.py
Normal file
1
game/server/flights/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
34
game/server/flights/routes.py
Normal file
34
game/server/flights/routes.py
Normal file
@ -0,0 +1,34 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game import Game
|
||||
from game.server import GameContext
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
from gen.flights.flightplan import CasFlightPlan, PatrollingFlightPlan
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/flights")
|
||||
|
||||
|
||||
@router.get("/{flight_id}/commit-boundary")
|
||||
def commit_boundary(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||
) -> LeafletPoly:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
if not isinstance(flight.flight_plan, PatrollingFlightPlan):
|
||||
return []
|
||||
start = flight.flight_plan.patrol_start
|
||||
end = flight.flight_plan.patrol_end
|
||||
if isinstance(flight.flight_plan, CasFlightPlan):
|
||||
center = flight.flight_plan.target.position
|
||||
commit_center = ShapelyPoint(center.x, center.y)
|
||||
else:
|
||||
commit_center = LineString(
|
||||
[
|
||||
ShapelyPoint(start.x, start.y),
|
||||
ShapelyPoint(end.x, end.y),
|
||||
]
|
||||
)
|
||||
bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters)
|
||||
return ShapelyUtil.poly_to_leaflet(bubble, game.theater)
|
||||
1
game/server/waypoints/__init__.py
Normal file
1
game/server/waypoints/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
81
game/server/waypoints/routes.py
Normal file
81
game/server/waypoints/routes.py
Normal file
@ -0,0 +1,81 @@
|
||||
from datetime import timedelta
|
||||
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.flightwaypointtype import FlightWaypointType
|
||||
from game.server import GameContext
|
||||
from game.theater import LatLon
|
||||
from game.utils import meters
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/waypoints")
|
||||
|
||||
|
||||
@router.get("/{flight_id}", response_model=list[BaseFlightWaypoint])
|
||||
def all_waypoints_for_flight(
|
||||
flight_id: UUID, game: Game = Depends(GameContext.get)
|
||||
) -> list[FlightWaypoint]:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
departure = FlightWaypoint(
|
||||
FlightWaypointType.TAKEOFF,
|
||||
flight.departure.position.x,
|
||||
flight.departure.position.y,
|
||||
meters(0),
|
||||
)
|
||||
departure.alt_type = "RADIO"
|
||||
points = [departure] + flight.flight_plan.waypoints
|
||||
for point in points:
|
||||
point.update_latlng(game.theater)
|
||||
return points
|
||||
|
||||
|
||||
@router.post("/{flight_id}/{waypoint_idx}/position")
|
||||
def set_position(
|
||||
flight_id: UUID,
|
||||
waypoint_idx: int,
|
||||
position: LatLon,
|
||||
game: Game = Depends(GameContext.get),
|
||||
) -> None:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
if waypoint_idx == 0:
|
||||
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
|
||||
package_model = (
|
||||
GameContext.get_model()
|
||||
.ato_model_for(flight.blue)
|
||||
.find_matching_package_model(flight.package)
|
||||
)
|
||||
if package_model is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Could not find PackageModel owning {flight}",
|
||||
)
|
||||
package_model.update_tot()
|
||||
|
||||
|
||||
@router.get("/{flight_id}/{waypoint_idx}/timing")
|
||||
def waypoint_timing(
|
||||
flight_id: UUID, waypoint_idx: int, game: Game = Depends(GameContext.get)
|
||||
) -> str | None:
|
||||
flight = game.db.flights.get(flight_id)
|
||||
if waypoint_idx == 0:
|
||||
return f"Depart T+{flight.flight_plan.takeoff_time()}"
|
||||
|
||||
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
|
||||
prefix = "TOT"
|
||||
time = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
prefix = "Depart"
|
||||
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
return ""
|
||||
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
|
||||
@ -4,17 +4,17 @@ import datetime
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
|
||||
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.terrain import (
|
||||
caucasus,
|
||||
marianaislands,
|
||||
nevada,
|
||||
normandy,
|
||||
persiangulf,
|
||||
syria,
|
||||
thechannel,
|
||||
marianaislands,
|
||||
)
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from pyproj import CRS, Transformer
|
||||
@ -262,7 +262,7 @@ class ConflictTheater:
|
||||
return LatLon(lat, lon)
|
||||
|
||||
def ll_to_point(self, ll: LatLon) -> Point:
|
||||
x, y = self.ll_to_point_transformer.transform(ll.latitude, ll.longitude)
|
||||
x, y = self.ll_to_point_transformer.transform(ll.lat, ll.lng)
|
||||
return Point(x, y)
|
||||
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LatLon:
|
||||
latitude: float
|
||||
longitude: float
|
||||
# These names match Leaflet for easier interop.
|
||||
lat: float
|
||||
lng: float
|
||||
|
||||
def as_list(self) -> List[float]:
|
||||
return [self.latitude, self.longitude]
|
||||
return [self.lat, self.lng]
|
||||
|
||||
@staticmethod
|
||||
def _components(dimension: float) -> Tuple[int, int, float]:
|
||||
@ -28,7 +30,7 @@ class LatLon:
|
||||
precision = 2 if include_decimal_seconds else 0
|
||||
return " ".join(
|
||||
[
|
||||
self._format_component(self.latitude, ("N", "S"), precision),
|
||||
self._format_component(self.longitude, ("E", "W"), precision),
|
||||
self._format_component(self.lat, ("N", "S"), precision),
|
||||
self._format_component(self.lng, ("E", "W"), precision),
|
||||
]
|
||||
)
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import itertools
|
||||
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
|
||||
|
||||
1
mypy.ini
1
mypy.ini
@ -15,6 +15,7 @@ warn_redundant_casts = True
|
||||
# warn_return_any = True
|
||||
warn_unreachable = True
|
||||
warn_unused_ignores = True
|
||||
plugins = pydantic.mypy
|
||||
|
||||
[mypy-faker.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@ -18,7 +18,7 @@ from game.data.weapons import Pylon, Weapon, WeaponGroup
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.factions import FACTIONS
|
||||
from game.profiling import logged_duration
|
||||
from game.server import EventStream, GameContext, Server
|
||||
from game.server import EventStream, Server
|
||||
from game.settings import Settings
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
||||
from pydcs_extensions import load_mods
|
||||
@ -137,7 +137,6 @@ def run_ui(game: Optional[Game]) -> None:
|
||||
|
||||
# Apply CSS (need works)
|
||||
GameUpdateSignal()
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(GameContext.set)
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(EventStream.drain)
|
||||
|
||||
# Start window
|
||||
|
||||
@ -529,3 +529,8 @@ class GameModel:
|
||||
self.game = game
|
||||
self.ato_model.replace_from_game(player=True)
|
||||
self.red_ato_model.replace_from_game(player=False)
|
||||
|
||||
def get(self) -> Game:
|
||||
if self.game is None:
|
||||
raise RuntimeError("GameModel has no Game set")
|
||||
return self.game
|
||||
|
||||
@ -62,7 +62,7 @@ class ControlPointJs(QObject):
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.control_point.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
return [ll.lat, ll.lng]
|
||||
|
||||
@Property(bool, notify=mobileChanged)
|
||||
def mobile(self) -> bool:
|
||||
|
||||
@ -1,41 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game.ato import Flight, FlightWaypoint
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.server.leaflet import LeafletLatLon, LeafletPoly, ShapelyUtil
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
from game.theater import ConflictTheater
|
||||
from game.utils import meters
|
||||
from gen.flights.flightplan import CasFlightPlan, PatrollingFlightPlan
|
||||
from qt_ui.models import AtoModel
|
||||
from .waypointjs import WaypointJs
|
||||
|
||||
|
||||
class FlightJs(QObject):
|
||||
idChanged = Signal()
|
||||
positionChanged = Signal()
|
||||
flightPlanChanged = Signal()
|
||||
blueChanged = Signal()
|
||||
selectedChanged = Signal()
|
||||
commitBoundaryChanged = Signal()
|
||||
|
||||
originChanged = Signal()
|
||||
|
||||
@Property(list, notify=originChanged)
|
||||
def origin(self) -> LeafletLatLon:
|
||||
return self._waypoints[0].position
|
||||
|
||||
targetChanged = Signal()
|
||||
|
||||
@Property(list, notify=targetChanged)
|
||||
def target(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.flight.package.target.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -49,42 +27,18 @@ class FlightJs(QObject):
|
||||
self._selected = selected
|
||||
self.theater = theater
|
||||
self.ato_model = ato_model
|
||||
self._waypoints = self.make_waypoints()
|
||||
|
||||
@Property(str, notify=idChanged)
|
||||
def id(self) -> str:
|
||||
return str(self.flight.id)
|
||||
|
||||
def update_waypoints(self) -> None:
|
||||
for waypoint in self._waypoints:
|
||||
waypoint.timingChanged.emit()
|
||||
|
||||
def make_waypoints(self) -> List[WaypointJs]:
|
||||
departure = FlightWaypoint(
|
||||
FlightWaypointType.TAKEOFF,
|
||||
self.flight.departure.position.x,
|
||||
self.flight.departure.position.y,
|
||||
meters(0),
|
||||
)
|
||||
departure.alt_type = "RADIO"
|
||||
waypoints = []
|
||||
for point in [departure] + self.flight.points:
|
||||
waypoint = WaypointJs(point, self, self.theater, self.ato_model)
|
||||
waypoint.positionChanged.connect(self.update_waypoints)
|
||||
waypoints.append(waypoint)
|
||||
return waypoints
|
||||
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
if isinstance(self.flight.state, InFlight):
|
||||
ll = self.theater.point_to_ll(self.flight.state.estimate_position())
|
||||
return [ll.latitude, ll.longitude]
|
||||
return [ll.lat, ll.lng]
|
||||
return []
|
||||
|
||||
@Property(list, notify=flightPlanChanged)
|
||||
def flightPlan(self) -> List[WaypointJs]:
|
||||
return self._waypoints
|
||||
|
||||
@Property(bool, notify=blueChanged)
|
||||
def blue(self) -> bool:
|
||||
return self.flight.departure.captured
|
||||
@ -104,24 +58,3 @@ class FlightJs(QObject):
|
||||
def set_selected(self, value: bool) -> None:
|
||||
self._selected = value
|
||||
self.selectedChanged.emit()
|
||||
|
||||
@Property(list, notify=commitBoundaryChanged)
|
||||
def commitBoundary(self) -> LeafletPoly:
|
||||
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
return []
|
||||
start = self.flight.flight_plan.patrol_start
|
||||
end = self.flight.flight_plan.patrol_end
|
||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||
center = self.flight.flight_plan.target.position
|
||||
commit_center = ShapelyPoint(center.x, center.y)
|
||||
else:
|
||||
commit_center = LineString(
|
||||
[
|
||||
ShapelyPoint(start.x, start.y),
|
||||
ShapelyPoint(end.x, end.y),
|
||||
]
|
||||
)
|
||||
bubble = commit_center.buffer(
|
||||
self.flight.flight_plan.engagement_distance.meters
|
||||
)
|
||||
return ShapelyUtil.poly_to_leaflet(bubble, self.theater)
|
||||
|
||||
@ -30,7 +30,7 @@ class FrontLineJs(QObject):
|
||||
self.front_line.attack_heading.left.degrees, nautical_miles(2).meters
|
||||
)
|
||||
)
|
||||
return [[a.latitude, a.longitude], [b.latitude, b.longitude]]
|
||||
return [[a.lat, a.lng], [b.lat, b.lng]]
|
||||
|
||||
@Slot()
|
||||
def showPackageDialog(self) -> None:
|
||||
|
||||
@ -101,7 +101,7 @@ class GroundObjectJs(QObject):
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.tgo.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
return [ll.lat, ll.lng]
|
||||
|
||||
@Property(bool, notify=deadChanged)
|
||||
def dead(self) -> bool:
|
||||
|
||||
@ -160,7 +160,7 @@ class MapModel(QObject):
|
||||
@staticmethod
|
||||
def leaflet_coord_for(point: Point, theater: ConflictTheater) -> LeafletLatLon:
|
||||
ll = theater.point_to_ll(point)
|
||||
return [ll.latitude, ll.longitude]
|
||||
return [ll.lat, ll.lng]
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.game_model.game is None:
|
||||
@ -183,7 +183,7 @@ class MapModel(QObject):
|
||||
|
||||
def reset_map_center(self, theater: ConflictTheater) -> None:
|
||||
ll = theater.point_to_ll(theater.terrain.map_view_default.position)
|
||||
self._map_center = [ll.latitude, ll.longitude]
|
||||
self._map_center = [ll.lat, ll.lng]
|
||||
self.mapCenterChanged.emit(self._map_center)
|
||||
|
||||
@Property(str, notify=apiKeyChanged)
|
||||
|
||||
@ -30,5 +30,5 @@ class UnculledZone(QObject):
|
||||
for zone in game.get_culling_zones():
|
||||
ll = game.theater.point_to_ll(zone)
|
||||
yield UnculledZone(
|
||||
[ll.latitude, ll.longitude], game.settings.perf_culling_distance * 1000
|
||||
[ll.lat, ll.lng], game.settings.perf_culling_distance * 1000
|
||||
)
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
|
||||
from game.ato import Flight, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
from game.theater import ConflictTheater, LatLon
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
from qt_ui.models import AtoModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .flightjs import FlightJs
|
||||
|
||||
|
||||
class WaypointJs(QObject):
|
||||
positionChanged = Signal()
|
||||
altitudeFtChanged = Signal()
|
||||
altitudeReferenceChanged = Signal()
|
||||
nameChanged = Signal()
|
||||
timingChanged = Signal()
|
||||
isTargetPointChanged = Signal()
|
||||
isTakeoffChanged = Signal()
|
||||
isLandingChanged = Signal()
|
||||
isDivertChanged = Signal()
|
||||
isBullseyeChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
waypoint: FlightWaypoint,
|
||||
flight_model: FlightJs,
|
||||
theater: ConflictTheater,
|
||||
ato_model: AtoModel,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.waypoint = waypoint
|
||||
self.flight_model = flight_model
|
||||
self.theater = theater
|
||||
self.ato_model = ato_model
|
||||
|
||||
@property
|
||||
def flight(self) -> Flight:
|
||||
return self.flight_model.flight
|
||||
|
||||
@property
|
||||
def flight_plan(self) -> FlightPlan:
|
||||
return self.flight.flight_plan
|
||||
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.waypoint.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
@Property(int, notify=altitudeFtChanged)
|
||||
def altitudeFt(self) -> int:
|
||||
return int(self.waypoint.alt.feet)
|
||||
|
||||
@Property(str, notify=altitudeReferenceChanged)
|
||||
def altitudeReference(self) -> str:
|
||||
return "AGL" if self.waypoint.alt_type == "RADIO" else "MSL"
|
||||
|
||||
@Property(str, notify=nameChanged)
|
||||
def name(self) -> str:
|
||||
return self.waypoint.name
|
||||
|
||||
@Property(str, notify=timingChanged)
|
||||
def timing(self) -> str:
|
||||
prefix = "TOT"
|
||||
time = self.flight_plan.tot_for_waypoint(self.waypoint)
|
||||
if time is None:
|
||||
prefix = "Depart"
|
||||
time = self.flight_plan.depart_time_for_waypoint(self.waypoint)
|
||||
if time is None:
|
||||
return ""
|
||||
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
|
||||
|
||||
@Property(bool, notify=isTargetPointChanged)
|
||||
def isTargetPoint(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.TARGET_POINT
|
||||
|
||||
@Property(bool, notify=isTakeoffChanged)
|
||||
def isTakeoff(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.TAKEOFF
|
||||
|
||||
@Property(bool, notify=isLandingChanged)
|
||||
def isLanding(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.LANDING_POINT
|
||||
|
||||
@Property(bool, notify=isDivertChanged)
|
||||
def isDivert(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT
|
||||
|
||||
@Property(bool, notify=isBullseyeChanged)
|
||||
def isBullseye(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.BULLSEYE
|
||||
|
||||
@Slot(list, result=str)
|
||||
def setPosition(self, position: LeafletLatLon) -> str:
|
||||
point = self.theater.ll_to_point(LatLon(*position))
|
||||
self.waypoint.x = point.x
|
||||
self.waypoint.y = point.y
|
||||
package = self.ato_model.find_matching_package_model(self.flight.package)
|
||||
if package is None:
|
||||
return "Could not find package model containing modified flight"
|
||||
package.update_tot()
|
||||
self.positionChanged.emit()
|
||||
self.flight_model.commitBoundaryChanged.emit()
|
||||
return ""
|
||||
@ -20,7 +20,7 @@ from PySide2.QtWidgets import (
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, VERSION, persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.server import EventStream
|
||||
from game.server import EventStream, GameContext
|
||||
from game.server.security import ApiKeyManager
|
||||
from qt_ui import liberation_install
|
||||
from qt_ui.dialogs import Dialog
|
||||
@ -54,6 +54,7 @@ class QLiberationWindow(QMainWindow):
|
||||
self.sim_controller = SimController(self.game)
|
||||
self.sim_controller.sim_update.connect(EventStream.put_nowait)
|
||||
self.game_model = GameModel(game, self.sim_controller)
|
||||
GameContext.set_model(self.game_model)
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
|
||||
@ -3,6 +3,8 @@ const ENABLE_EXPENSIVE_DEBUG_TOOLS = false;
|
||||
const HTTP_BACKEND = "http://[::1]:5000";
|
||||
const WS_BACKEND = "ws://[::1]:5000/eventstream";
|
||||
|
||||
METERS_TO_FEET = 3.28084;
|
||||
|
||||
// Uniquely generated at startup and passed to use by the QWebChannel.
|
||||
var API_KEY = null;
|
||||
|
||||
@ -14,6 +16,17 @@ function getJson(endpoint) {
|
||||
}).then((response) => response.json());
|
||||
}
|
||||
|
||||
function postJson(endpoint, data) {
|
||||
return fetch(`${HTTP_BACKEND}${endpoint}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": API_KEY,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then((response) => response.json());
|
||||
}
|
||||
|
||||
const Colors = Object.freeze({
|
||||
Blue: "#0084ff",
|
||||
Red: "#c85050",
|
||||
@ -752,79 +765,50 @@ class Waypoint {
|
||||
this.number = number;
|
||||
this.flight = flight;
|
||||
this.marker = this.makeMarker();
|
||||
this.waypoint.positionChanged.connect(() => this.relocate());
|
||||
this.waypoint.timingChanged.connect(() => this.updateDescription());
|
||||
}
|
||||
|
||||
position() {
|
||||
return this.waypoint.position;
|
||||
return this.waypoint.latlng;
|
||||
}
|
||||
|
||||
shouldMark() {
|
||||
// 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.
|
||||
return !(
|
||||
this.waypoint.isTakeoff ||
|
||||
this.waypoint.isLanding ||
|
||||
this.waypoint.isDivert ||
|
||||
this.waypoint.isBullseye
|
||||
);
|
||||
return this.waypoint.should_mark;
|
||||
}
|
||||
|
||||
draggable() {
|
||||
// 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.
|
||||
return !(
|
||||
this.waypoint.isTargetPoint ||
|
||||
this.waypoint.isTakeoff ||
|
||||
this.waypoint.isLanding ||
|
||||
this.waypoint.isDivert ||
|
||||
this.waypoint.isBullseye
|
||||
);
|
||||
async timing(dragging) {
|
||||
if (dragging) {
|
||||
return "Waiting to recompute TOT...";
|
||||
}
|
||||
return await getJson(`/waypoints/${this.flight.id}/${this.number}/timing`);
|
||||
}
|
||||
|
||||
description(dragging) {
|
||||
const timing = dragging
|
||||
? "Waiting to recompute TOT..."
|
||||
: this.waypoint.timing;
|
||||
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";
|
||||
return (
|
||||
`${this.number} ${this.waypoint.name}<br />` +
|
||||
`${this.waypoint.altitudeFt} ft ${this.waypoint.altitudeReference}<br />` +
|
||||
`${timing}`
|
||||
`${alt} ft ${altRef}<br />` +
|
||||
`${await this.timing(dragging)}`
|
||||
);
|
||||
}
|
||||
|
||||
relocate() {
|
||||
this.marker.setLatLng(this.waypoint.position);
|
||||
this.marker.setLatLng(this.position());
|
||||
}
|
||||
|
||||
updateDescription(dragging) {
|
||||
this.marker.setTooltipContent(this.description(dragging));
|
||||
this.description(dragging).then((description) => {
|
||||
this.marker.setTooltipContent(description);
|
||||
});
|
||||
}
|
||||
|
||||
makeMarker() {
|
||||
const zoom = map.getZoom();
|
||||
return L.marker(this.waypoint.position, { draggable: this.draggable() })
|
||||
.bindTooltip(this.description(), {
|
||||
permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM,
|
||||
})
|
||||
const marker = L.marker(this.position(), {
|
||||
draggable: this.waypoint.is_movable,
|
||||
})
|
||||
.on("dragstart", (e) => {
|
||||
this.updateDescription(true);
|
||||
})
|
||||
@ -836,19 +820,35 @@ class Waypoint {
|
||||
.on("dragend", (e) => {
|
||||
const marker = e.target;
|
||||
const destination = marker.getLatLng();
|
||||
this.waypoint
|
||||
.setPosition([destination.lat, destination.lng])
|
||||
.then((err) => {
|
||||
postJson(
|
||||
`/waypoints/${this.flight.id}/${this.number}/position`,
|
||||
destination
|
||||
)
|
||||
.then(() => {
|
||||
this.waypoint.position = destination;
|
||||
this.updateDescription(false);
|
||||
this.flight.drawCommitBoundary();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err) {
|
||||
this.relocate();
|
||||
console.log(err);
|
||||
marker.bindPopup(err);
|
||||
marker.bindPopup(`${err}`).openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.description(false).then((description) =>
|
||||
marker.bindTooltip(description, {
|
||||
permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM,
|
||||
})
|
||||
);
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
includeInPath() {
|
||||
return !this.waypoint.isDivert && !this.waypoint.isBullseye;
|
||||
return this.waypoint.include_in_path;
|
||||
}
|
||||
}
|
||||
|
||||
@ -858,16 +858,11 @@ class Flight {
|
||||
constructor(flight) {
|
||||
this.flight = flight;
|
||||
this.id = flight.id;
|
||||
this.flightPlan = this.flight.flightPlan.map(
|
||||
(p, idx) => new Waypoint(p, idx, this)
|
||||
);
|
||||
this.aircraft = null;
|
||||
this.path = null;
|
||||
this.markers = [];
|
||||
this.commitBoundary = null;
|
||||
this.flight.selectedChanged.connect(() => this.draw());
|
||||
this.flight.flightPlanChanged.connect(() => this.drawFlightPlan());
|
||||
this.flight.commitBoundaryChanged.connect(() => this.drawCommitBoundary());
|
||||
Flight.registerFlight(this);
|
||||
}
|
||||
|
||||
@ -948,16 +943,18 @@ class Flight {
|
||||
.removeFrom(allFlightPlansLayer);
|
||||
}
|
||||
if (this.flight.selected) {
|
||||
if (this.flight.commitBoundary) {
|
||||
this.commitBoundary = L.polyline(this.flight.commitBoundary, {
|
||||
color: Colors.Highlight,
|
||||
weight: 1,
|
||||
interactive: false,
|
||||
})
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
}
|
||||
getJson(`/flights/${this.flight.id}/commit-boundary`).then((boundary) => {
|
||||
if (boundary) {
|
||||
this.commitBoundary = L.polyline(boundary, {
|
||||
color: Colors.Highlight,
|
||||
weight: 1,
|
||||
interactive: false,
|
||||
})
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -993,21 +990,24 @@ class Flight {
|
||||
// ATO before drawing.
|
||||
return;
|
||||
}
|
||||
const path = [];
|
||||
this.flightPlan.forEach((waypoint) => {
|
||||
if (waypoint.includeInPath()) {
|
||||
path.push(waypoint.position());
|
||||
}
|
||||
if (this.shouldMark(waypoint)) {
|
||||
waypoint.marker
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
this.markers.push(waypoint.marker);
|
||||
}
|
||||
});
|
||||
|
||||
this.drawPath(path);
|
||||
getJson(`/waypoints/${this.flight.id}`).then((waypoints) => {
|
||||
const path = [];
|
||||
waypoints.map((raw, idx) => {
|
||||
const waypoint = new Waypoint(raw, idx, this);
|
||||
if (waypoint.includeInPath()) {
|
||||
path.push(waypoint.position());
|
||||
}
|
||||
if (this.shouldMark(waypoint)) {
|
||||
waypoint.marker
|
||||
.addTo(selectedFlightPlansLayer)
|
||||
.addTo(this.flightPlanLayer())
|
||||
.addTo(allFlightPlansLayer);
|
||||
this.markers.push(waypoint.marker);
|
||||
}
|
||||
});
|
||||
this.drawPath(path);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user