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: def remove_package(self, package: Package) -> None:
"""Removes a package from the ATO.""" """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) self.packages.remove(package)
def clear(self) -> None: def clear(self) -> None:
"""Removes all packages from the ATO.""" """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 self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None: ) -> None:
super().on_game_tick(events, time, duration) 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: def progress(self) -> float:
return ( return (

View File

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

View File

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

View File

@ -6,6 +6,7 @@ from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from game.server.combat.models import FrozenCombatJs from game.server.combat.models import FrozenCombatJs
from game.server.flights.models import FlightJs
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
if TYPE_CHECKING: if TYPE_CHECKING:
@ -14,18 +15,24 @@ if TYPE_CHECKING:
class GameUpdateEventsJs(BaseModel): class GameUpdateEventsJs(BaseModel):
updated_flights: dict[UUID, LeafletLatLon] updated_flight_positions: dict[UUID, LeafletLatLon]
new_combats: list[FrozenCombatJs] = [] new_combats: list[FrozenCombatJs]
updated_combats: list[FrozenCombatJs] = [] updated_combats: list[FrozenCombatJs]
navmesh_updates: set[bool] = set() navmesh_updates: set[bool]
unculled_zones_updated: bool = False unculled_zones_updated: bool
threat_zones_updated: bool = False threat_zones_updated: bool
new_flights: list[FlightJs]
updated_flights: set[UUID]
deleted_flights: set[UUID]
selected_flight: UUID | None
deselected_flight: bool
@classmethod @classmethod
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
return GameUpdateEventsJs( return GameUpdateEventsJs(
updated_flights={ updated_flight_positions={
f[0].id: f[1].latlng().as_list() for f in events.updated_flights f[0].id: f[1].latlng().as_list()
for f in events.updated_flight_positions
}, },
new_combats=[ new_combats=[
FrozenCombatJs.for_combat(c, game.theater) for c in events.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, navmesh_updates=events.navmesh_updates,
unculled_zones_updated=events.unculled_zones_updated, unculled_zones_updated=events.unculled_zones_updated,
threat_zones_updated=events.threat_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 shapely.geometry import LineString, Point as ShapelyPoint
from game import Game 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.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: 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") @router.get("/{flight_id}/commit-boundary")
def commit_boundary( def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.get) flight_id: UUID, game: Game = Depends(GameContext.get)

View File

@ -1,55 +1,91 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from uuid import UUID
from dcs import Point from dcs import Point
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato import Flight from game.ato import Flight, Package
from game.sim.combat import FrozenCombat from game.sim.combat import FrozenCombat
@dataclass
class GameUpdateEvents: class GameUpdateEvents:
def __init__(self) -> None: simulation_complete = False
self.simulation_complete = False new_combats: list[FrozenCombat] = field(default_factory=list)
self.new_combats: list[FrozenCombat] = [] updated_combats: list[FrozenCombat] = field(default_factory=list)
self.updated_combats: list[FrozenCombat] = [] updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list)
self.updated_flights: list[tuple[Flight, Point]] = [] navmesh_updates: set[bool] = field(default_factory=set)
self.navmesh_updates: set[bool] = set() unculled_zones_updated: bool = False
self.unculled_zones_updated: bool = False threat_zones_updated: bool = False
self.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 @property
def empty(self) -> bool: def empty(self) -> bool:
return not any( return self == GameUpdateEvents()
[
self.simulation_complete,
self.new_combats,
self.updated_combats,
self.updated_flights,
self.navmesh_updates,
self.unculled_zones_updated,
self.threat_zones_updated,
]
)
def complete_simulation(self) -> None: def complete_simulation(self) -> GameUpdateEvents:
self.simulation_complete = True 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) 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) self.updated_combats.append(combat)
return self
def update_flight(self, flight: Flight, new_position: Point) -> None: def update_flight_position(
self.updated_flights.append((flight, new_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) self.navmesh_updates.add(player)
return self
def update_unculled_zones(self) -> None: def update_unculled_zones(self) -> GameUpdateEvents:
self.unculled_zones_updated = True self.unculled_zones_updated = True
return self
def update_threat_zones(self) -> None: def update_threat_zones(self) -> GameUpdateEvents:
self.threat_zones_updated = True 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 faker import Faker
from game.ato import Flight, FlightType, Package from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings
from game.ato.flightplan import FlightPlanBuilder from game.ato.flightplan import FlightPlanBuilder
from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus from .pilot import Pilot, PilotStatus
from ..db.database import Database from ..db.database import Database
from ..utils import meters from ..utils import meters
@ -381,7 +381,6 @@ class Squadron:
for flight in list(package.flights): for flight in list(package.flights):
if flight.squadron == self and flight.flight_type is FlightType.FERRY: if flight.squadron == self and flight.flight_type is FlightType.FERRY:
package.remove_flight(flight) package.remove_flight(flight)
flight.return_pilots_and_aircraft()
if not package.flights: if not package.flights:
self.coalition.ato.remove_package(package) 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 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.flight import Flight
from game.ato.flightplan import FlightPlanBuilder
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.ato.package import Package from game.ato.package import Package
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -53,9 +56,6 @@ from game.theater.transitnetwork import (
TransitNetwork, TransitNetwork,
) )
from game.utils import meters, nautical_miles 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: if TYPE_CHECKING:
from game import Game from game import Game
@ -635,7 +635,6 @@ class PendingTransfers:
flight.package.remove_flight(flight) flight.package.remove_flight(flight)
if not flight.package.flights: if not flight.package.flights:
self.game.ato_for(self.player).remove_package(flight.package) self.game.ato_for(self.player).remove_package(flight.package)
flight.return_pilots_and_aircraft()
@cancel_transport.register @cancel_transport.register
def _cancel_transport_convoy( def _cancel_transport_convoy(

View File

@ -169,9 +169,6 @@ class PackageModel(QAbstractListModel):
"""Removes the given flight from the package.""" """Removes the given flight from the package."""
index = self.package.flights.index(flight) index = self.package.flights.index(flight)
self.beginRemoveRows(QModelIndex(), index, index) 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.package.remove_flight(flight)
self.endRemoveRows() self.endRemoveRows()
self.update_tot() self.update_tot()
@ -253,6 +250,8 @@ class AtoModel(QAbstractListModel):
"""Adds a package to the ATO.""" """Adds a package to the ATO."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.ato.add_package(package) 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() self.endInsertRows()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.client_slots_changed.emit() self.client_slots_changed.emit()
@ -264,14 +263,11 @@ class AtoModel(QAbstractListModel):
def delete_package(self, package: Package) -> None: def delete_package(self, package: Package) -> None:
"""Removes the given package from the ATO.""" """Removes the given package from the ATO."""
EventStream.put_nowait(GameUpdateEvents().delete_flights_in_package(package))
self.package_models.release(package) self.package_models.release(package)
index = self.ato.packages.index(package) index = self.ato.packages.index(package)
self.beginRemoveRows(QModelIndex(), index, index) self.beginRemoveRows(QModelIndex(), index, index)
self.ato.remove_package(package) 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() self.endRemoveRows()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.client_slots_changed.emit() self.client_slots_changed.emit()
@ -280,9 +276,7 @@ class AtoModel(QAbstractListModel):
def on_packages_changed(self) -> None: def on_packages_changed(self) -> None:
if self.game is not None: if self.game is not None:
self.game.compute_unculled_zones() self.game.compute_unculled_zones()
events = GameUpdateEvents() EventStream.put_nowait(GameUpdateEvents().update_unculled_zones())
events.update_unculled_zones()
EventStream.put_nowait(events)
def package_at_index(self, index: QModelIndex) -> Package: def package_at_index(self, index: QModelIndex) -> Package:
"""Returns the package at the given index.""" """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.flight import Flight
from game.ato.package import Package 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 ..delegates import TwoColumnRowDelegate
from ..models import AtoModel, GameModel, NullListModel, PackageModel from ..models import AtoModel, GameModel, NullListModel, PackageModel
@ -128,8 +129,10 @@ class QFlightList(QListView):
) )
def delete_flight(self, index: QModelIndex) -> None: 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) self.package_model.delete_flight_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
def contextMenuEvent(self, event: QContextMenuEvent) -> None: def contextMenuEvent(self, event: QContextMenuEvent) -> None:
index = self.indexAt(event.pos()) index = self.indexAt(event.pos())
@ -209,13 +212,13 @@ class QFlightPanel(QGroupBox):
self.delete_button.setEnabled(enabled) self.delete_button.setEnabled(enabled)
self.change_map_flight_selection(index) self.change_map_flight_selection(index)
@staticmethod def change_map_flight_selection(self, index: QModelIndex) -> None:
def change_map_flight_selection(index: QModelIndex) -> None: events = GameUpdateEvents()
if not index.isValid(): if not index.isValid():
GameUpdateSignal.get_instance().select_flight(None) events.deselect_flight()
return else:
events.select_flight(self.package_model.flight_at_index(index))
GameUpdateSignal.get_instance().select_flight(index.row()) EventStream.put_nowait(events)
def on_edit(self) -> None: def on_edit(self) -> None:
"""Opens the flight edit dialog.""" """Opens the flight edit dialog."""
@ -303,7 +306,6 @@ class QPackageList(QListView):
def delete_package(self, index: QModelIndex) -> None: def delete_package(self, index: QModelIndex) -> None:
self.ato_model.delete_package_at_index(index) 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: def on_new_packages(self, _parent: QModelIndex, first: int, _last: int) -> None:
# Select the newly created pacakges. This should only ever happen due to # 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: def change_map_package_selection(self, index: QModelIndex) -> None:
if not index.isValid(): if not index.isValid():
GameUpdateSignal.get_instance().select_package(None) EventStream.put_nowait(GameUpdateEvents().deselect_flight())
return return
package = self.ato_model.get_package_model(index) package = self.ato_model.get_package_model(index)
if package.rowCount() == 0: if package.rowCount() == 0:
GameUpdateSignal.get_instance().select_package(None) EventStream.put_nowait(GameUpdateEvents().deselect_flight())
else: 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: def on_edit(self) -> None:
"""Opens the package edit dialog.""" """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 from __future__ import annotations
import logging from typing import List, Optional
from typing import List, Optional, Tuple
from PySide2.QtCore import Property, QObject, Signal from PySide2.QtCore import Property, QObject, Signal
from dcs.mapping import LatLng from dcs.mapping import LatLng
from game import Game from game import Game
from game.ato.airtaaskingorder import AirTaskingOrder
from game.profiling import logged_duration from game.profiling import logged_duration
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
from game.server.security import ApiKeyManager from game.server.security import ApiKeyManager
@ -17,7 +15,6 @@ from game.theater import (
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from .controlpointjs import ControlPointJs from .controlpointjs import ControlPointJs
from .flightjs import FlightJs
from .frontlinejs import FrontLineJs from .frontlinejs import FrontLineJs
from .groundobjectjs import GroundObjectJs from .groundobjectjs import GroundObjectJs
from .supplyroutejs import SupplyRouteJs from .supplyroutejs import SupplyRouteJs
@ -47,9 +44,8 @@ class MapModel(QObject):
controlPointsChanged = Signal() controlPointsChanged = Signal()
groundObjectsChanged = Signal() groundObjectsChanged = Signal()
supplyRoutesChanged = Signal() supplyRoutesChanged = Signal()
flightsChanged = Signal()
frontLinesChanged = Signal() frontLinesChanged = Signal()
selectedFlightChanged = Signal(str) mapReset = Signal()
def __init__(self, game_model: GameModel) -> None: def __init__(self, game_model: GameModel) -> None:
super().__init__() super().__init__()
@ -58,79 +54,18 @@ class MapModel(QObject):
self._control_points = [] self._control_points = []
self._ground_objects = [] self._ground_objects = []
self._supply_routes = [] self._supply_routes = []
self._flights: dict[tuple[bool, int, int], FlightJs] = {}
self._front_lines = [] 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().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() self.reset()
def clear(self) -> None: def clear(self) -> None:
self._control_points = [] self._control_points = []
self._supply_routes = [] self._supply_routes = []
self._ground_objects = [] self._ground_objects = []
self._flights = {}
self._front_lines = [] self._front_lines = []
self.cleared.emit() 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: def reset(self) -> None:
if self.game_model.game is None: if self.game_model.game is None:
self.clear() self.clear()
@ -139,8 +74,8 @@ class MapModel(QObject):
self.reset_control_points() self.reset_control_points()
self.reset_ground_objects() self.reset_ground_objects()
self.reset_routes() self.reset_routes()
self.reset_atos()
self.reset_front_lines() self.reset_front_lines()
self.mapReset.emit()
def on_game_load(self, game: Optional[Game]) -> None: def on_game_load(self, game: Optional[Game]) -> None:
if game is not None: if game is not None:
@ -158,29 +93,6 @@ class MapModel(QObject):
def mapCenter(self) -> LeafletLatLon: def mapCenter(self) -> LeafletLatLon:
return self._map_center.as_list() 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: def reset_control_points(self) -> None:
self._control_points = [ self._control_points = [
ControlPointJs(c, self.game_model, self.game.theater) ControlPointJs(c, self.game_model, self.game.theater)

View File

@ -17,28 +17,12 @@ class GameUpdateSignal(QObject):
game_loaded = Signal(Game) 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): def __init__(self):
super(GameUpdateSignal, self).__init__() super(GameUpdateSignal, self).__init__()
GameUpdateSignal.instance = self GameUpdateSignal.instance = self
self.game_loaded.connect(self.updateGame) 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]): def updateGame(self, game: Optional[Game]):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.gameupdated.emit(game) self.gameupdated.emit(game)

View File

@ -5,9 +5,10 @@ from PySide2.QtWidgets import (
) )
from game.ato.flight import Flight 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.models import GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
@ -24,6 +25,7 @@ class QEditFlightDialog(QDialog):
super().__init__(parent=parent) super().__init__(parent=parent)
self.game_model = game_model self.game_model = game_model
self.flight = flight
self.setWindowTitle("Edit flight") self.setWindowTitle("Edit flight")
self.setWindowIcon(EVENT_ICONS["strike"]) self.setWindowIcon(EVENT_ICONS["strike"])
@ -37,5 +39,5 @@ class QEditFlightDialog(QDialog):
self.finished.connect(self.on_close) self.finished.connect(self.on_close)
def on_close(self, _result) -> None: 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() 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.flight import Flight
from game.ato.flightplan import FlightPlanBuilder, PlanningError
from game.ato.package import Package from game.ato.package import Package
from game.game import Game from game.game import Game
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.ato.flightplan import FlightPlanBuilder, PlanningError
from qt_ui.models import AtoModel, GameModel, PackageModel from qt_ui.models import AtoModel, GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.ato import QFlightList from qt_ui.widgets.ato import QFlightList
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
@ -141,9 +142,10 @@ class QPackageDialog(QDialog):
def on_cancel(self) -> None: def on_cancel(self) -> None:
pass pass
@staticmethod def on_close(self, _result) -> None:
def on_close(_result) -> None: EventStream.put_nowait(
GameUpdateSignal.get_instance().redraw_flight_paths() GameUpdateEvents().update_flights_in_package(self.package_model.package)
)
def on_save(self) -> None: def on_save(self) -> None:
self.save_tot() self.save_tot()
@ -183,13 +185,14 @@ class QPackageDialog(QDialog):
) )
try: try:
planner.populate_flight_plan(flight) planner.populate_flight_plan(flight)
self.package_model.update_tot()
EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
except PlanningError as ex: except PlanningError as ex:
self.package_model.delete_flight(flight) self.package_model.delete_flight(flight)
logging.exception("Could not create flight") logging.exception("Could not create flight")
QMessageBox.critical( QMessageBox.critical(
self, "Could not create flight", str(ex), QMessageBox.Ok self, "Could not create flight", str(ex), QMessageBox.Ok
) )
self.package_model.update_tot()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.package_changed.emit() self.package_changed.emit()
@ -252,7 +255,7 @@ class QNewPackageDialog(QPackageDialog):
def on_cancel(self) -> None: def on_cancel(self) -> None:
super().on_cancel() super().on_cancel()
for flight in self.package_model.package.flights: for flight in self.package_model.package.flights:
flight.return_pilots_and_aircraft() self.package_model.delete_flight(flight)
class QEditPackageDialog(QPackageDialog): class QEditPackageDialog(QPackageDialog):

View File

@ -382,48 +382,61 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.groundObjectsChanged.connect(drawGroundObjects); game.groundObjectsChanged.connect(drawGroundObjects);
game.supplyRoutesChanged.connect(drawSupplyRoutes); game.supplyRoutesChanged.connect(drawSupplyRoutes);
game.frontLinesChanged.connect(drawFrontLines); game.frontLinesChanged.connect(drawFrontLines);
game.flightsChanged.connect(drawAircraft); game.mapReset.connect(drawAircraft);
game.selectedFlightChanged.connect(updateSelectedFlight);
}); });
function handleStreamedEvents(events) { function handleStreamedEvents(events) {
for (const [flightId, position] of Object.entries(events.updated_flights)) { for (const [flightId, position] of Object.entries(
Flight.withId(flightId).drawAircraftLocation(position); events.updated_flight_positions
)) {
Flight.withId(flightId).updatePosition(position);
} }
for (const combat of events.new_combats) { for (const combat of events.new_combats) {
redrawCombat(combat); redrawCombat(combat);
} }
for (const combat of events.updated_combats) { for (const combat of events.updated_combats) {
redrawCombat(combat); redrawCombat(combat);
} }
for (const player of events.navmesh_updates) { for (const player of events.navmesh_updates) {
drawNavmesh(player); drawNavmesh(player);
} }
if (events.unculled_zones_updated) { if (events.unculled_zones_updated) {
drawUnculledZones(); drawUnculledZones();
} }
if (events.threat_zones_updated) { if (events.threat_zones_updated) {
drawThreatZones(); 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) { function recenterMap(center) {
map.setView(center, 8, { animate: true, duration: 1 }); 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 { class ControlPoint {
constructor(cp) { constructor(cp) {
this.cp = cp; this.cp = cp;
@ -857,15 +870,17 @@ class Waypoint {
class Flight { class Flight {
static registeredFlights = {}; static registeredFlights = {};
static selected = null;
constructor(flight) { constructor(flight) {
this.flight = flight; this.flight = flight;
this.id = flight.id; this.id = flight.id;
this.selected = false;
self.position = flight.position;
this.aircraft = null; this.aircraft = null;
this.path = null; this.path = null;
this.markers = []; this.markers = [];
this.commitBoundary = null; this.commitBoundary = null;
this.flight.selectedChanged.connect(() => this.draw());
Flight.registerFlight(this); Flight.registerFlight(this);
} }
@ -877,18 +892,67 @@ class Flight {
Flight.registeredFlights[flight.id] = 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) { static withId(id) {
return Flight.registeredFlights[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) { shouldMark(waypoint) {
return this.flight.selected && waypoint.shouldMark(); return this.selected && waypoint.shouldMark();
} }
flightPlanLayer() { flightPlanLayer() {
return this.flight.blue ? blueFlightPlansLayer : redFlightPlansLayer; return this.flight.blue ? blueFlightPlansLayer : redFlightPlansLayer;
} }
updatePosition(position) {
this.position = position;
this.drawAircraftLocation();
}
updatePath(idx, position) { updatePath(idx, position) {
const points = this.path.getLatLngs(); const points = this.path.getLatLngs();
points[idx] = position; points[idx] = position;
@ -898,7 +962,7 @@ class Flight {
drawPath(path) { drawPath(path) {
const color = this.flight.blue ? Colors.Blue : Colors.Red; const color = this.flight.blue ? Colors.Blue : Colors.Red;
const layer = this.flightPlanLayer(); const layer = this.flightPlanLayer();
if (this.flight.selected) { if (this.selected) {
this.path = L.polyline(path, { this.path = L.polyline(path, {
color: Colors.Highlight, color: Colors.Highlight,
interactive: false, interactive: false,
@ -919,46 +983,69 @@ class Flight {
this.drawCommitBoundary(); this.drawCommitBoundary();
} }
drawAircraftLocation(position = null) { clear() {
this.clearAircraftLocation();
this.clearFlightPlan();
this.clearCommitBoundary();
}
clearAircraftLocation() {
if (this.aircraft != null) { if (this.aircraft != null) {
this.aircraft.removeFrom(aircraftLayer); this.aircraft.removeFrom(aircraftLayer);
this.aircraft = null; this.aircraft = null;
} }
if (position == null) { }
position = this.flight.position;
} drawAircraftLocation() {
if (position.length > 0) { this.clearAircraftLocation();
this.aircraft = L.marker(position, { if (this.position != null) {
icon: Icons.AirIcons.icon( this.aircraft = L.marker(this.position, {
"fighter", icon: Icons.AirIcons.icon("fighter", this.flight.blue, this.selected),
this.flight.blue,
this.flight.selected
),
}).addTo(aircraftLayer); }).addTo(aircraftLayer);
} }
} }
drawCommitBoundary() { clearCommitBoundary() {
if (this.commitBoundary != null) { if (this.commitBoundary != null) {
this.commitBoundary this.commitBoundary
.removeFrom(selectedFlightPlansLayer) .removeFrom(selectedFlightPlansLayer)
.removeFrom(this.flightPlanLayer()) .removeFrom(this.flightPlanLayer())
.removeFrom(allFlightPlansLayer); .removeFrom(allFlightPlansLayer);
} }
if (this.flight.selected) { }
getJson(`/flights/${this.flight.id}/commit-boundary`).then((boundary) => {
if (boundary) { drawCommitBoundary() {
this.commitBoundary = L.polyline(boundary, { if (!this.selected) {
color: Colors.Highlight, this.clearCommitBoundary();
weight: 1, return;
interactive: false,
})
.addTo(selectedFlightPlansLayer)
.addTo(this.flightPlanLayer())
.addTo(allFlightPlansLayer);
}
});
} }
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() { clearFlightPlan() {
@ -978,39 +1065,23 @@ class Flight {
} }
drawFlightPlan() { drawFlightPlan() {
this.clearFlightPlan(); getJson(`/waypoints/${this.flight.id}`).then((waypoints) => {
this.flight.flightIsInAto().then((inAto) => { this.clearFlightPlan();
if (!inAto) { const path = [];
// HACK: The signal to redraw the ATO following package/flight deletion waypoints.map((raw, idx) => {
// and the signal to change flight/package selection due to UI selection const waypoint = new Waypoint(raw, idx, this);
// change come in an arbitrary order. If redraw signal comes first the if (waypoint.includeInPath()) {
// UI will clear the map and redraw, but then when the UI updates path.push(waypoint.position());
// selection away from the (now deleted) flight/package it calls }
// deselect, which redraws the deleted flight plan in its deselected if (this.shouldMark(waypoint)) {
// state. waypoint.marker
// .addTo(selectedFlightPlansLayer)
// Avoid this by checking that the flight is still in the coalition's .addTo(this.flightPlanLayer())
// ATO before drawing. .addTo(allFlightPlansLayer);
return; this.markers.push(waypoint.marker);
} }
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);
}); });
this.drawPath(path);
}); });
} }
} }
@ -1022,22 +1093,12 @@ function drawAircraft() {
redFlightPlansLayer.clearLayers(); redFlightPlansLayer.clearLayers();
selectedFlightPlansLayer.clearLayers(); selectedFlightPlansLayer.clearLayers();
allFlightPlansLayer.clearLayers(); allFlightPlansLayer.clearLayers();
let selected = null;
game.flights.forEach((flight) => { getJson("/flights").then((flights) => {
// Draw the selected waypoint last so it's on top. bringToFront only brings for (const flight of flights) {
// 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 {
new Flight(flight).draw(); new Flight(flight).draw();
} }
}); });
if (selected != null) {
new Flight(selected).draw();
}
} }
function _drawThreatZones(zones, layer, player) { function _drawThreatZones(zones, layer, player) {