Move FlightJs out of MapModel.

This commit is contained in:
Dan Albert 2022-02-22 20:40:58 -08:00
parent ad0d3412fb
commit 45e76e12b6
18 changed files with 333 additions and 326 deletions

View File

@ -17,8 +17,13 @@ class AirTaskingOrder:
def remove_package(self, package: Package) -> None:
"""Removes a package from the ATO."""
# Remove all the flights individually so the database gets updated.
for flight in list(package.flights):
package.remove_flight(flight)
self.packages.remove(package)
def clear(self) -> None:
"""Removes all packages from the ATO."""
self.packages.clear()
# Remove all packages individually so the database gets updated.
for package in self.packages:
self.remove_package(package)

View File

@ -22,7 +22,7 @@ class Navigating(InFlight):
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:
super().on_game_tick(events, time, duration)
events.update_flight(self.flight, self.estimate_position())
events.update_flight_position(self.flight, self.estimate_position())
def progress(self) -> float:
return (

View File

@ -129,6 +129,9 @@ class Package:
"""Removes a flight from the package."""
self.flights.remove(flight)
self._db.remove(flight.id)
flight.return_pilots_and_aircraft()
if flight.cargo is not None:
flight.cargo.transport = None
if not self.flights:
self.waypoints = None

View File

@ -92,5 +92,4 @@ class PackageBuilder:
"""Returns any planned flights to the inventory."""
flights = list(self.package.flights)
for flight in flights:
flight.return_pilots_and_aircraft()
self.package.remove_flight(flight)

View File

@ -6,6 +6,7 @@ from uuid import UUID
from pydantic import BaseModel
from game.server.combat.models import FrozenCombatJs
from game.server.flights.models import FlightJs
from game.server.leaflet import LeafletLatLon
if TYPE_CHECKING:
@ -14,18 +15,24 @@ if TYPE_CHECKING:
class GameUpdateEventsJs(BaseModel):
updated_flights: dict[UUID, LeafletLatLon]
new_combats: list[FrozenCombatJs] = []
updated_combats: list[FrozenCombatJs] = []
navmesh_updates: set[bool] = set()
unculled_zones_updated: bool = False
threat_zones_updated: bool = False
updated_flight_positions: dict[UUID, LeafletLatLon]
new_combats: list[FrozenCombatJs]
updated_combats: list[FrozenCombatJs]
navmesh_updates: set[bool]
unculled_zones_updated: bool
threat_zones_updated: bool
new_flights: list[FlightJs]
updated_flights: set[UUID]
deleted_flights: set[UUID]
selected_flight: UUID | None
deselected_flight: bool
@classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
return GameUpdateEventsJs(
updated_flights={
f[0].id: f[1].latlng().as_list() for f in events.updated_flights
updated_flight_positions={
f[0].id: f[1].latlng().as_list()
for f in events.updated_flight_positions
},
new_combats=[
FrozenCombatJs.for_combat(c, game.theater) for c in events.new_combats
@ -37,4 +44,9 @@ class GameUpdateEventsJs(BaseModel):
navmesh_updates=events.navmesh_updates,
unculled_zones_updated=events.unculled_zones_updated,
threat_zones_updated=events.threat_zones_updated,
new_flights=[FlightJs.for_flight(f) for f in events.new_flights],
updated_flights=events.updated_flights,
deleted_flights=events.deleted_flights,
selected_flight=events.selected_flight,
deselected_flight=events.deselected_flight,
)

View File

@ -0,0 +1,26 @@
from __future__ import annotations
from uuid import UUID
from dcs.mapping import LatLng
from pydantic import BaseModel
from game.ato import Flight
from game.ato.flightstate import InFlight
class FlightJs(BaseModel):
id: UUID
blue: bool
position: LatLng | None
@staticmethod
def for_flight(flight: Flight) -> FlightJs:
# Don't provide a location for aircraft that aren't in the air. Later we can
# expand the model to include the state data for the UI so that it can make its
# own decisions about whether or not to draw the aircraft, but for now we'll
# filter here.
position = None
if isinstance(flight.state, InFlight):
position = flight.position().latlng()
return FlightJs(id=flight.id, blue=flight.blue, position=position)

View File

@ -4,13 +4,30 @@ 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 game.ato.flightplan import CasFlightPlan, PatrollingFlightPlan
from game.server import GameContext
from game.server.flights.models import FlightJs
from game.server.leaflet import LeafletPoly, ShapelyUtil
router: APIRouter = APIRouter(prefix="/flights")
@router.get("/")
def list_flights(game: Game = Depends(GameContext.get)) -> list[FlightJs]:
flights = []
for coalition in game.coalitions:
for package in coalition.ato.packages:
for flight in package.flights:
flights.append(FlightJs.for_flight(flight))
return flights
@router.get("/{flight_id}")
def get_flight(flight_id: UUID, game: Game = Depends(GameContext.get)) -> FlightJs:
flight = game.db.flights.get(flight_id)
return FlightJs.for_flight(flight)
@router.get("/{flight_id}/commit-boundary")
def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.get)

View File

@ -1,55 +1,91 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from uuid import UUID
from dcs import Point
if TYPE_CHECKING:
from game.ato import Flight
from game.ato import Flight, Package
from game.sim.combat import FrozenCombat
@dataclass
class GameUpdateEvents:
def __init__(self) -> None:
self.simulation_complete = False
self.new_combats: list[FrozenCombat] = []
self.updated_combats: list[FrozenCombat] = []
self.updated_flights: list[tuple[Flight, Point]] = []
self.navmesh_updates: set[bool] = set()
self.unculled_zones_updated: bool = False
self.threat_zones_updated: bool = False
simulation_complete = False
new_combats: list[FrozenCombat] = field(default_factory=list)
updated_combats: list[FrozenCombat] = field(default_factory=list)
updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list)
navmesh_updates: set[bool] = field(default_factory=set)
unculled_zones_updated: bool = False
threat_zones_updated: bool = False
new_flights: set[Flight] = field(default_factory=set)
updated_flights: set[UUID] = field(default_factory=set)
deleted_flights: set[UUID] = field(default_factory=set)
selected_flight: UUID | None = None
deselected_flight: bool = False
@property
def empty(self) -> bool:
return not any(
[
self.simulation_complete,
self.new_combats,
self.updated_combats,
self.updated_flights,
self.navmesh_updates,
self.unculled_zones_updated,
self.threat_zones_updated,
]
)
return self == GameUpdateEvents()
def complete_simulation(self) -> None:
def complete_simulation(self) -> GameUpdateEvents:
self.simulation_complete = True
return self
def new_combat(self, combat: FrozenCombat) -> None:
def new_combat(self, combat: FrozenCombat) -> GameUpdateEvents:
self.new_combats.append(combat)
return self
def update_combat(self, combat: FrozenCombat) -> None:
def update_combat(self, combat: FrozenCombat) -> GameUpdateEvents:
self.updated_combats.append(combat)
return self
def update_flight(self, flight: Flight, new_position: Point) -> None:
self.updated_flights.append((flight, new_position))
def update_flight_position(
self, flight: Flight, new_position: Point
) -> GameUpdateEvents:
self.updated_flight_positions.append((flight, new_position))
return self
def update_navmesh(self, player: bool) -> None:
def update_navmesh(self, player: bool) -> GameUpdateEvents:
self.navmesh_updates.add(player)
return self
def update_unculled_zones(self) -> None:
def update_unculled_zones(self) -> GameUpdateEvents:
self.unculled_zones_updated = True
return self
def update_threat_zones(self) -> None:
def update_threat_zones(self) -> GameUpdateEvents:
self.threat_zones_updated = True
return self
def new_flight(self, flight: Flight) -> GameUpdateEvents:
self.new_flights.add(flight)
return self
def update_flight(self, flight: Flight) -> GameUpdateEvents:
self.updated_flights.add(flight.id)
return self
def update_flights_in_package(self, package: Package) -> GameUpdateEvents:
self.updated_flights.update({f.id for f in package.flights})
return self
def delete_flight(self, flight: Flight) -> GameUpdateEvents:
self.deleted_flights.add(flight.id)
return self
def delete_flights_in_package(self, package: Package) -> GameUpdateEvents:
self.deleted_flights.update({f.id for f in package.flights})
return self
def select_flight(self, flight: Flight) -> GameUpdateEvents:
self.selected_flight = flight.id
self.deselected_flight = False
return self
def deselect_flight(self) -> GameUpdateEvents:
self.deselected_flight = True
self.selected_flight = None
return self

View File

@ -9,8 +9,8 @@ from typing import Optional, Sequence, TYPE_CHECKING
from faker import Faker
from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings
from game.ato.flightplan import FlightPlanBuilder
from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus
from ..db.database import Database
from ..utils import meters
@ -381,7 +381,6 @@ class Squadron:
for flight in list(package.flights):
if flight.squadron == self and flight.flight_type is FlightType.FERRY:
package.remove_flight(flight)
flight.return_pilots_and_aircraft()
if not package.flights:
self.coalition.ato.remove_package(package)

View File

@ -40,7 +40,10 @@ from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, T
from dcs.mapping import Point
from game.ato.ai_flight_planner_db import aircraft_for_task
from game.ato.closestairfields import ObjectiveDistanceCache
from game.ato.flight import Flight
from game.ato.flightplan import FlightPlanBuilder
from game.ato.flighttype import FlightType
from game.ato.package import Package
from game.dcs.aircrafttype import AircraftType
@ -53,9 +56,6 @@ from game.theater.transitnetwork import (
TransitNetwork,
)
from game.utils import meters, nautical_miles
from game.ato.ai_flight_planner_db import aircraft_for_task
from game.ato.closestairfields import ObjectiveDistanceCache
from game.ato.flightplan import FlightPlanBuilder
if TYPE_CHECKING:
from game import Game
@ -635,7 +635,6 @@ class PendingTransfers:
flight.package.remove_flight(flight)
if not flight.package.flights:
self.game.ato_for(self.player).remove_package(flight.package)
flight.return_pilots_and_aircraft()
@cancel_transport.register
def _cancel_transport_convoy(

View File

@ -169,9 +169,6 @@ class PackageModel(QAbstractListModel):
"""Removes the given flight from the package."""
index = self.package.flights.index(flight)
self.beginRemoveRows(QModelIndex(), index, index)
if flight.cargo is not None:
flight.cargo.transport = None
flight.return_pilots_and_aircraft()
self.package.remove_flight(flight)
self.endRemoveRows()
self.update_tot()
@ -253,6 +250,8 @@ class AtoModel(QAbstractListModel):
"""Adds a package to the ATO."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.ato.add_package(package)
# We do not need to send events for new flights in the package here. Events were
# already sent when the flights were added to the in-progress package.
self.endInsertRows()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
@ -264,14 +263,11 @@ class AtoModel(QAbstractListModel):
def delete_package(self, package: Package) -> None:
"""Removes the given package from the ATO."""
EventStream.put_nowait(GameUpdateEvents().delete_flights_in_package(package))
self.package_models.release(package)
index = self.ato.packages.index(package)
self.beginRemoveRows(QModelIndex(), index, index)
self.ato.remove_package(package)
for flight in package.flights:
flight.return_pilots_and_aircraft()
if flight.cargo is not None:
flight.cargo.transport = None
self.endRemoveRows()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
@ -280,9 +276,7 @@ class AtoModel(QAbstractListModel):
def on_packages_changed(self) -> None:
if self.game is not None:
self.game.compute_unculled_zones()
events = GameUpdateEvents()
events.update_unculled_zones()
EventStream.put_nowait(events)
EventStream.put_nowait(GameUpdateEvents().update_unculled_zones())
def package_at_index(self, index: QModelIndex) -> Package:
"""Returns the package at the given index."""

View File

@ -27,7 +27,8 @@ from PySide2.QtWidgets import (
from game.ato.flight import Flight
from game.ato.package import Package
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from game.server import EventStream
from game.sim import GameUpdateEvents
from ..delegates import TwoColumnRowDelegate
from ..models import AtoModel, GameModel, NullListModel, PackageModel
@ -128,8 +129,10 @@ class QFlightList(QListView):
)
def delete_flight(self, index: QModelIndex) -> None:
EventStream.put_nowait(
GameUpdateEvents().delete_flight(self.package_model.flight_at_index(index))
)
self.package_model.delete_flight_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
def contextMenuEvent(self, event: QContextMenuEvent) -> None:
index = self.indexAt(event.pos())
@ -209,13 +212,13 @@ class QFlightPanel(QGroupBox):
self.delete_button.setEnabled(enabled)
self.change_map_flight_selection(index)
@staticmethod
def change_map_flight_selection(index: QModelIndex) -> None:
def change_map_flight_selection(self, index: QModelIndex) -> None:
events = GameUpdateEvents()
if not index.isValid():
GameUpdateSignal.get_instance().select_flight(None)
return
GameUpdateSignal.get_instance().select_flight(index.row())
events.deselect_flight()
else:
events.select_flight(self.package_model.flight_at_index(index))
EventStream.put_nowait(events)
def on_edit(self) -> None:
"""Opens the flight edit dialog."""
@ -303,7 +306,6 @@ class QPackageList(QListView):
def delete_package(self, index: QModelIndex) -> None:
self.ato_model.delete_package_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_new_packages(self, _parent: QModelIndex, first: int, _last: int) -> None:
# Select the newly created pacakges. This should only ever happen due to
@ -390,14 +392,18 @@ class QPackagePanel(QGroupBox):
def change_map_package_selection(self, index: QModelIndex) -> None:
if not index.isValid():
GameUpdateSignal.get_instance().select_package(None)
EventStream.put_nowait(GameUpdateEvents().deselect_flight())
return
package = self.ato_model.get_package_model(index)
if package.rowCount() == 0:
GameUpdateSignal.get_instance().select_package(None)
EventStream.put_nowait(GameUpdateEvents().deselect_flight())
else:
GameUpdateSignal.get_instance().select_package(index.row())
EventStream.put_nowait(
GameUpdateEvents().select_flight(
package.flight_at_index(package.index(0))
)
)
def on_edit(self) -> None:
"""Opens the package edit dialog."""

View File

@ -1,51 +0,0 @@
from __future__ import annotations
from PySide2.QtCore import Property, QObject, Signal, Slot
from game.ato import Flight
from game.ato.flightstate import InFlight
from game.server.leaflet import LeafletLatLon
from qt_ui.models import AtoModel
class FlightJs(QObject):
idChanged = Signal()
positionChanged = Signal()
blueChanged = Signal()
selectedChanged = Signal()
def __init__(self, flight: Flight, selected: bool, ato_model: AtoModel) -> None:
super().__init__()
self.flight = flight
self._selected = selected
self.ato_model = ato_model
@Property(str, notify=idChanged)
def id(self) -> str:
return str(self.flight.id)
@Property(list, notify=positionChanged)
def position(self) -> LeafletLatLon:
if isinstance(self.flight.state, InFlight):
return self.flight.state.estimate_position().latlng().as_list()
return []
@Property(bool, notify=blueChanged)
def blue(self) -> bool:
return self.flight.departure.captured
@Property(bool, notify=selectedChanged)
def selected(self) -> bool:
return self._selected
@Slot(result=bool)
def flightIsInAto(self) -> bool:
if self.flight.package not in self.flight.squadron.coalition.ato.packages:
return False
if self.flight not in self.flight.package.flights:
return False
return True
def set_selected(self, value: bool) -> None:
self._selected = value
self.selectedChanged.emit()

View File

@ -1,13 +1,11 @@
from __future__ import annotations
import logging
from typing import List, Optional, Tuple
from typing import List, Optional
from PySide2.QtCore import Property, QObject, Signal
from dcs.mapping import LatLng
from game import Game
from game.ato.airtaaskingorder import AirTaskingOrder
from game.profiling import logged_duration
from game.server.leaflet import LeafletLatLon
from game.server.security import ApiKeyManager
@ -17,7 +15,6 @@ from game.theater import (
from qt_ui.models import GameModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from .controlpointjs import ControlPointJs
from .flightjs import FlightJs
from .frontlinejs import FrontLineJs
from .groundobjectjs import GroundObjectJs
from .supplyroutejs import SupplyRouteJs
@ -47,9 +44,8 @@ class MapModel(QObject):
controlPointsChanged = Signal()
groundObjectsChanged = Signal()
supplyRoutesChanged = Signal()
flightsChanged = Signal()
frontLinesChanged = Signal()
selectedFlightChanged = Signal(str)
mapReset = Signal()
def __init__(self, game_model: GameModel) -> None:
super().__init__()
@ -58,79 +54,18 @@ class MapModel(QObject):
self._control_points = []
self._ground_objects = []
self._supply_routes = []
self._flights: dict[tuple[bool, int, int], FlightJs] = {}
self._front_lines = []
self._selected_flight_index: Optional[Tuple[int, int]] = None
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
GameUpdateSignal.get_instance().package_selection_changed.connect(
self.set_package_selection
)
GameUpdateSignal.get_instance().flight_selection_changed.connect(
self.set_flight_selection
)
self.reset()
def clear(self) -> None:
self._control_points = []
self._supply_routes = []
self._ground_objects = []
self._flights = {}
self._front_lines = []
self.cleared.emit()
def set_package_selection(self, index: int) -> None:
self.deselect_current_flight()
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
# zero automatically. We use -1 to indicate no selection.
if index == -1:
self._selected_flight_index = None
else:
self._selected_flight_index = index, 0
self.select_current_flight()
def set_flight_selection(self, index: int) -> None:
self.deselect_current_flight()
if self._selected_flight_index is None:
if index != -1:
# We don't know what order update_package_selection and
# update_flight_selection will be called in when the last
# package is removed. If no flight is selected, it's not a
# problem to also have no package selected.
logging.error("Flight was selected with no package selected")
return
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
# zero automatically. We use -1 to indicate no selection.
if index == -1:
self._selected_flight_index = self._selected_flight_index[0], None
self._selected_flight_index = self._selected_flight_index[0], index
self.select_current_flight()
@property
def _selected_flight(self) -> Optional[FlightJs]:
if self._selected_flight_index is None:
return None
package_index, flight_index = self._selected_flight_index
blue = True
return self._flights.get((blue, package_index, flight_index))
def deselect_current_flight(self) -> None:
flight = self._selected_flight
if flight is None:
return None
flight.set_selected(False)
def select_current_flight(self):
flight = self._selected_flight
if flight is None:
self.selectedFlightChanged.emit(None)
return None
flight.set_selected(True)
self.selectedFlightChanged.emit(str(flight.flight.id))
def reset(self) -> None:
if self.game_model.game is None:
self.clear()
@ -139,8 +74,8 @@ class MapModel(QObject):
self.reset_control_points()
self.reset_ground_objects()
self.reset_routes()
self.reset_atos()
self.reset_front_lines()
self.mapReset.emit()
def on_game_load(self, game: Optional[Game]) -> None:
if game is not None:
@ -158,29 +93,6 @@ class MapModel(QObject):
def mapCenter(self) -> LeafletLatLon:
return self._map_center.as_list()
def _flights_in_ato(
self, ato: AirTaskingOrder, blue: bool
) -> dict[tuple[bool, int, int], FlightJs]:
flights = {}
for p_idx, package in enumerate(ato.packages):
for f_idx, flight in enumerate(package.flights):
flights[blue, p_idx, f_idx] = FlightJs(
flight,
selected=blue and (p_idx, f_idx) == self._selected_flight_index,
ato_model=self.game_model.ato_model_for(blue),
)
return flights
def reset_atos(self) -> None:
self._flights = self._flights_in_ato(
self.game.blue.ato, blue=True
) | self._flights_in_ato(self.game.red.ato, blue=False)
self.flightsChanged.emit()
@Property(list, notify=flightsChanged)
def flights(self) -> list[FlightJs]:
return list(self._flights.values())
def reset_control_points(self) -> None:
self._control_points = [
ControlPointJs(c, self.game_model, self.game.theater)

View File

@ -17,28 +17,12 @@ class GameUpdateSignal(QObject):
game_loaded = Signal(Game)
flight_paths_changed = Signal()
package_selection_changed = Signal(int) # -1 indicates no selection.
flight_selection_changed = Signal(int) # -1 indicates no selection.
def __init__(self):
super(GameUpdateSignal, self).__init__()
GameUpdateSignal.instance = self
self.game_loaded.connect(self.updateGame)
def select_package(self, index: Optional[int]) -> None:
# noinspection PyUnresolvedReferences
self.package_selection_changed.emit(-1 if index is None else index)
def select_flight(self, index: Optional[int]) -> None:
# noinspection PyUnresolvedReferences
self.flight_selection_changed.emit(-1 if index is None else index)
def redraw_flight_paths(self) -> None:
# noinspection PyUnresolvedReferences
self.flight_paths_changed.emit()
def updateGame(self, game: Optional[Game]):
# noinspection PyUnresolvedReferences
self.gameupdated.emit(game)

View File

@ -5,9 +5,10 @@ from PySide2.QtWidgets import (
)
from game.ato.flight import Flight
from game.server import EventStream
from game.sim import GameUpdateEvents
from qt_ui.models import GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
@ -24,6 +25,7 @@ class QEditFlightDialog(QDialog):
super().__init__(parent=parent)
self.game_model = game_model
self.flight = flight
self.setWindowTitle("Edit flight")
self.setWindowIcon(EVENT_ICONS["strike"])
@ -37,5 +39,5 @@ class QEditFlightDialog(QDialog):
self.finished.connect(self.on_close)
def on_close(self, _result) -> None:
GameUpdateSignal.get_instance().redraw_flight_paths()
EventStream.put_nowait(GameUpdateEvents().update_flight(self.flight))
self.game_model.ato_model.client_slots_changed.emit()

View File

@ -16,14 +16,15 @@ from PySide2.QtWidgets import (
)
from game.ato.flight import Flight
from game.ato.flightplan import FlightPlanBuilder, PlanningError
from game.ato.package import Package
from game.game import Game
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater.missiontarget import MissionTarget
from game.ato.flightplan import FlightPlanBuilder, PlanningError
from qt_ui.models import AtoModel, GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.ato import QFlightList
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
@ -141,9 +142,10 @@ class QPackageDialog(QDialog):
def on_cancel(self) -> None:
pass
@staticmethod
def on_close(_result) -> None:
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_close(self, _result) -> None:
EventStream.put_nowait(
GameUpdateEvents().update_flights_in_package(self.package_model.package)
)
def on_save(self) -> None:
self.save_tot()
@ -183,13 +185,14 @@ class QPackageDialog(QDialog):
)
try:
planner.populate_flight_plan(flight)
self.package_model.update_tot()
EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
except PlanningError as ex:
self.package_model.delete_flight(flight)
logging.exception("Could not create flight")
QMessageBox.critical(
self, "Could not create flight", str(ex), QMessageBox.Ok
)
self.package_model.update_tot()
# noinspection PyUnresolvedReferences
self.package_changed.emit()
@ -252,7 +255,7 @@ class QNewPackageDialog(QPackageDialog):
def on_cancel(self) -> None:
super().on_cancel()
for flight in self.package_model.package.flights:
flight.return_pilots_and_aircraft()
self.package_model.delete_flight(flight)
class QEditPackageDialog(QPackageDialog):

View File

@ -382,48 +382,61 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.groundObjectsChanged.connect(drawGroundObjects);
game.supplyRoutesChanged.connect(drawSupplyRoutes);
game.frontLinesChanged.connect(drawFrontLines);
game.flightsChanged.connect(drawAircraft);
game.selectedFlightChanged.connect(updateSelectedFlight);
game.mapReset.connect(drawAircraft);
});
function handleStreamedEvents(events) {
for (const [flightId, position] of Object.entries(events.updated_flights)) {
Flight.withId(flightId).drawAircraftLocation(position);
for (const [flightId, position] of Object.entries(
events.updated_flight_positions
)) {
Flight.withId(flightId).updatePosition(position);
}
for (const combat of events.new_combats) {
redrawCombat(combat);
}
for (const combat of events.updated_combats) {
redrawCombat(combat);
}
for (const player of events.navmesh_updates) {
drawNavmesh(player);
}
if (events.unculled_zones_updated) {
drawUnculledZones();
}
if (events.threat_zones_updated) {
drawThreatZones();
}
if (events.deselected_flight && Flight.selected != null) {
Flight.deselectCurrent();
}
if (events.selected_flight != null) {
Flight.select(events.selected_flight);
}
for (const flight of events.new_flights) {
new Flight(flight).draw();
}
for (const flightId of events.updated_flights) {
Flight.withId(flightId).draw();
}
for (const flightId of events.deleted_flights) {
Flight.popId(flightId).clear();
}
}
function recenterMap(center) {
map.setView(center, 8, { animate: true, duration: 1 });
}
function updateSelectedFlight(id) {
if (id == null) {
holdZones.clearLayers();
ipZones.clearLayers();
joinZones.clearLayers();
return;
}
drawHoldZones(id);
drawIpZones(id);
drawJoinZones(id);
}
class ControlPoint {
constructor(cp) {
this.cp = cp;
@ -857,15 +870,17 @@ class Waypoint {
class Flight {
static registeredFlights = {};
static selected = null;
constructor(flight) {
this.flight = flight;
this.id = flight.id;
this.selected = false;
self.position = flight.position;
this.aircraft = null;
this.path = null;
this.markers = [];
this.commitBoundary = null;
this.flight.selectedChanged.connect(() => this.draw());
Flight.registerFlight(this);
}
@ -877,18 +892,67 @@ class Flight {
Flight.registeredFlights[flight.id] = flight;
}
static unregisterFlight(id) {
if (Flight.selected != null && Flight.selected.id == id) {
Flight.clearSelected();
}
delete Flight.registeredFlights[id];
}
static withId(id) {
return Flight.registeredFlights[id];
}
static popId(id) {
const flight = Flight.withId(id);
Flight.unregisterFlight(id);
return flight;
}
static clearSelected() {
holdZones.clearLayers();
ipZones.clearLayers();
joinZones.clearLayers();
Flight.selected = null;
}
static deselectCurrent() {
const flight = Flight.selected;
Flight.clearSelected();
if (flight != null) {
flight.selected = false;
flight.draw();
}
}
static select(id) {
if (Flight.selected != null && Flight.selected.id == id) {
return;
}
Flight.deselectCurrent();
const flight = Flight.withId(id);
Flight.selected = flight;
flight.selected = true;
flight.draw();
drawHoldZones(id);
drawIpZones(id);
drawJoinZones(id);
}
shouldMark(waypoint) {
return this.flight.selected && waypoint.shouldMark();
return this.selected && waypoint.shouldMark();
}
flightPlanLayer() {
return this.flight.blue ? blueFlightPlansLayer : redFlightPlansLayer;
}
updatePosition(position) {
this.position = position;
this.drawAircraftLocation();
}
updatePath(idx, position) {
const points = this.path.getLatLngs();
points[idx] = position;
@ -898,7 +962,7 @@ class Flight {
drawPath(path) {
const color = this.flight.blue ? Colors.Blue : Colors.Red;
const layer = this.flightPlanLayer();
if (this.flight.selected) {
if (this.selected) {
this.path = L.polyline(path, {
color: Colors.Highlight,
interactive: false,
@ -919,46 +983,69 @@ class Flight {
this.drawCommitBoundary();
}
drawAircraftLocation(position = null) {
clear() {
this.clearAircraftLocation();
this.clearFlightPlan();
this.clearCommitBoundary();
}
clearAircraftLocation() {
if (this.aircraft != null) {
this.aircraft.removeFrom(aircraftLayer);
this.aircraft = null;
}
if (position == null) {
position = this.flight.position;
}
if (position.length > 0) {
this.aircraft = L.marker(position, {
icon: Icons.AirIcons.icon(
"fighter",
this.flight.blue,
this.flight.selected
),
}
drawAircraftLocation() {
this.clearAircraftLocation();
if (this.position != null) {
this.aircraft = L.marker(this.position, {
icon: Icons.AirIcons.icon("fighter", this.flight.blue, this.selected),
}).addTo(aircraftLayer);
}
}
drawCommitBoundary() {
clearCommitBoundary() {
if (this.commitBoundary != null) {
this.commitBoundary
.removeFrom(selectedFlightPlansLayer)
.removeFrom(this.flightPlanLayer())
.removeFrom(allFlightPlansLayer);
}
if (this.flight.selected) {
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);
}
});
}
drawCommitBoundary() {
if (!this.selected) {
this.clearCommitBoundary();
return;
}
getJson(`/flights/${this.flight.id}/commit-boundary`).then((boundary) => {
// For a selected flight we wait to clear the commit boundary until after
// the backend responds. Otherwise if the package is reselected while
// waiting we may have the following execution order, with selections A
// and B:
//
// 1. A: clear
// 2. A: wait for backend
// 3. B: wait for backend
// 4. A: Add boundary to map
// 5. B: Add boundary to map
//
// Similarly, we need to recheck that we're still selected before
// continuing.
this.clearCommitBoundary();
if (boundary && this.selected) {
this.commitBoundary = L.polyline(boundary, {
color: Colors.Highlight,
weight: 1,
interactive: false,
})
.addTo(selectedFlightPlansLayer)
.addTo(this.flightPlanLayer())
.addTo(allFlightPlansLayer);
}
});
}
clearFlightPlan() {
@ -978,39 +1065,23 @@ class Flight {
}
drawFlightPlan() {
this.clearFlightPlan();
this.flight.flightIsInAto().then((inAto) => {
if (!inAto) {
// HACK: The signal to redraw the ATO following package/flight deletion
// and the signal to change flight/package selection due to UI selection
// change come in an arbitrary order. If redraw signal comes first the
// UI will clear the map and redraw, but then when the UI updates
// selection away from the (now deleted) flight/package it calls
// deselect, which redraws the deleted flight plan in its deselected
// state.
//
// Avoid this by checking that the flight is still in the coalition's
// ATO before drawing.
return;
}
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);
getJson(`/waypoints/${this.flight.id}`).then((waypoints) => {
this.clearFlightPlan();
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);
});
}
}
@ -1022,22 +1093,12 @@ function drawAircraft() {
redFlightPlansLayer.clearLayers();
selectedFlightPlansLayer.clearLayers();
allFlightPlansLayer.clearLayers();
let selected = null;
game.flights.forEach((flight) => {
// Draw the selected waypoint last so it's on top. bringToFront only brings
// it to the front of the *extant* elements, so any flights drawn later will
// be drawn on top. We could fight with manual Z-indexes but leaflet does a
// lot of that automatically so it'd be error prone.
if (flight.selected) {
selected = flight;
} else {
getJson("/flights").then((flights) => {
for (const flight of flights) {
new Flight(flight).draw();
}
});
if (selected != null) {
new Flight(selected).draw();
}
}
function _drawThreatZones(zones, layer, player) {