diff --git a/qt_ui/models.py b/qt_ui/models.py
index b07e7020..c614b176 100644
--- a/qt_ui/models.py
+++ b/qt_ui/models.py
@@ -177,8 +177,6 @@ class PackageModel(QAbstractListModel):
def set_tot(self, tot: datetime.timedelta) -> None:
self.package.time_over_target = tot
self.update_tot()
- # For some reason this is needed to make the UI update quickly.
- self.layoutChanged.emit()
def set_asap(self, asap: bool) -> None:
self.package.auto_asap = asap
@@ -188,6 +186,8 @@ class PackageModel(QAbstractListModel):
if self.package.auto_asap:
self.package.set_tot_asap()
self.tot_changed.emit()
+ # For some reason this is needed to make the UI update quickly.
+ self.layoutChanged.emit()
@property
def mission_target(self) -> MissionTarget:
@@ -291,6 +291,12 @@ class AtoModel(QAbstractListModel):
"""Returns a model for the package at the given index."""
return self.package_models.acquire(self.package_at_index(index))
+ def find_matching_package_model(self, package: Package) -> Optional[PackageModel]:
+ for model in self.packages:
+ if model.package == package:
+ return model
+ return None
+
@property
def packages(self) -> Iterator[PackageModel]:
"""Iterates over all the packages in the ATO."""
@@ -376,6 +382,11 @@ class GameModel:
self.ato_model = AtoModel(self, self.game.blue_ato)
self.red_ato_model = AtoModel(self, self.game.red_ato)
+ def ato_model_for(self, player: bool) -> AtoModel:
+ if player:
+ return self.ato_model
+ return self.red_ato_model
+
def set(self, game: Optional[Game]) -> None:
"""Updates the managed Game object.
diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py
index 0462ed20..a2ecff73 100644
--- a/qt_ui/widgets/map/mapmodel.py
+++ b/qt_ui/widgets/map/mapmodel.py
@@ -23,7 +23,7 @@ from gen.ato import AirTaskingOrder
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from gen.flights.flightplan import FlightPlan, PatrollingFlightPlan
from qt_ui.dialogs import Dialog
-from qt_ui.models import GameModel
+from qt_ui.models import GameModel, AtoModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
@@ -316,20 +316,27 @@ class WaypointJs(QObject):
altitudeReferenceChanged = Signal()
nameChanged = Signal()
timingChanged = Signal()
+ isTakeoffChanged = Signal()
isDivertChanged = Signal()
def __init__(
self,
waypoint: FlightWaypoint,
number: int,
- flight_plan: FlightPlan,
+ flight: Flight,
theater: ConflictTheater,
+ ato_model: AtoModel,
) -> None:
super().__init__()
self.waypoint = waypoint
self._number = number
- self.flight_plan = flight_plan
+ self.flight = flight
self.theater = theater
+ self.ato_model = ato_model
+
+ @property
+ def flight_plan(self) -> FlightPlan:
+ return self.flight.flight_plan
@Property(int, notify=numberChanged)
def number(self) -> int:
@@ -363,10 +370,26 @@ class WaypointJs(QObject):
return ""
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
+ @Property(bool, notify=isTakeoffChanged)
+ def isTakeoff(self) -> bool:
+ return self.waypoint.waypoint_type is FlightWaypointType.TAKEOFF
+
@Property(bool, notify=isDivertChanged)
def isDivert(self) -> bool:
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT
+ @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()
+ return ""
+
class FlightJs(QObject):
flightPlanChanged = Signal()
@@ -375,16 +398,26 @@ class FlightJs(QObject):
commitBoundaryChanged = Signal()
def __init__(
- self, flight: Flight, selected: bool, theater: ConflictTheater, faction: Faction
+ self,
+ flight: Flight,
+ selected: bool,
+ theater: ConflictTheater,
+ faction: Faction,
+ ato_model: AtoModel,
) -> None:
super().__init__()
self.flight = flight
self._selected = selected
self.theater = theater
self.faction = faction
+ self.ato_model = ato_model
self._waypoints = self.make_waypoints()
self._commit_boundary = self.make_commit_boundary()
+ def update_waypoints(self) -> None:
+ for waypoint in self._waypoints:
+ waypoint.timingChanged.emit()
+
def make_waypoints(self) -> List[WaypointJs]:
departure = FlightWaypoint(
FlightWaypointType.TAKEOFF,
@@ -393,10 +426,12 @@ class FlightJs(QObject):
meters(0),
)
departure.alt_type = "RADIO"
- return [
- WaypointJs(p, i, self.flight.flight_plan, self.theater)
- for i, p in enumerate([departure] + self.flight.points)
- ]
+ waypoints = []
+ for idx, point in enumerate([departure] + self.flight.points):
+ waypoint = WaypointJs(point, idx, self.flight, self.theater, self.ato_model)
+ waypoint.positionChanged.connect(self.update_waypoints)
+ waypoints.append(waypoint)
+ return waypoints
def make_commit_boundary(self) -> Optional[List[LeafletLatLon]]:
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
@@ -533,6 +568,7 @@ class MapModel(QObject):
selected=blue and (p_idx, f_idx) == self._selected_flight_index,
theater=self.game.theater,
faction=self.game.faction_for(blue),
+ ato_model=self.game_model.ato_model_for(blue),
)
)
return flights
diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js
index bb927be8..7d8ad399 100644
--- a/resources/ui/map/map.js
+++ b/resources/ui/map/map.js
@@ -16,6 +16,7 @@ const Colors = Object.freeze({
Blue: "#0084ff",
Red: "#c85050",
Green: "#80BA80",
+ Highlight: "#ffff00",
});
function metersToNauticalMiles(meters) {
@@ -401,44 +402,140 @@ function drawFrontLines() {
const SHOW_WAYPOINT_INFO_AT_ZOOM = 9;
-function drawFlightPlan(flight) {
- const layer = flight.blue ? blueFlightPlansLayer : redFlightPlansLayer;
- const color = flight.blue ? Colors.Blue : Colors.Red;
- const highlight = "#ffff00";
- // 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.
- const points = [flight.flightPlan[0].position];
- const zoom = map.getZoom();
- flight.flightPlan.slice(1).forEach((waypoint) => {
- if (!waypoint.isDivert) {
- points.push(waypoint.position);
- }
+class Waypoint {
+ constructor(waypoint, flight) {
+ this.waypoint = waypoint;
+ this.flight = flight;
+ this.marker = this.makeMarker();
+ this.waypoint.positionChanged.connect(() => this.relocate());
+ this.waypoint.timingChanged.connect(() => this.updateDescription());
+ }
- if (flight.selected) {
- L.marker(waypoint.position)
- .bindTooltip(
- `${waypoint.number} ${waypoint.name}
` +
- `${waypoint.altitudeFt} ft ${waypoint.altitudeReference}
` +
- `${waypoint.timing}`,
- { permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM }
- )
+ position() {
+ return this.waypoint.position;
+ }
+
+ 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.
+ return !this.waypoint.isTakeoff;
+ }
+
+ description(dragging) {
+ const timing = dragging
+ ? "Waiting to recompute TOT..."
+ : this.waypoint.timing;
+ return (
+ `${this.waypoint.number} ${this.waypoint.name}
` +
+ `${this.waypoint.altitudeFt} ft ${this.waypoint.altitudeReference}
` +
+ `${timing}`
+ );
+ }
+
+ relocate() {
+ this.marker.setLatLng(this.waypoint.position);
+ }
+
+ updateDescription(dragging) {
+ this.marker.setTooltipContent(this.description(dragging));
+ }
+
+ makeMarker() {
+ const zoom = map.getZoom();
+ return L.marker(this.waypoint.position, { draggable: true })
+ .bindTooltip(this.description(), {
+ permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM,
+ })
+ .on("dragstart", (e) => {
+ this.updateDescription(true);
+ })
+ .on("drag", (e) => {
+ const marker = e.target;
+ const destination = marker.getLatLng();
+ this.flight.updatePath(this.waypoint.number, destination);
+ })
+ .on("dragend", (e) => {
+ const marker = e.target;
+ const destination = marker.getLatLng();
+ this.waypoint
+ .setPosition([destination.lat, destination.lng])
+ .then((err) => {
+ if (err) {
+ console.log(err);
+ marker.bindPopup(err);
+ }
+ });
+ });
+ }
+
+ includeInPath() {
+ return !this.waypoint.isDivert;
+ }
+}
+
+class Flight {
+ constructor(flight) {
+ this.flight = flight;
+ this.flightPlan = this.flight.flightPlan.map((p) => new Waypoint(p, this));
+ this.path = null;
+ this.flight.flightPlanChanged.connect(() => this.draw());
+ }
+
+ shouldMark(waypoint) {
+ return this.flight.selected && waypoint.shouldMark();
+ }
+
+ flightPlanLayer() {
+ return this.flight.blue ? blueFlightPlansLayer : redFlightPlansLayer;
+ }
+
+ updatePath(idx, position) {
+ const points = this.path.getLatLngs();
+ points[idx] = position;
+ this.path.setLatLngs(points);
+ }
+
+ drawPath(path) {
+ const color = this.flight.blue ? Colors.Blue : Colors.Red;
+ const layer = this.flightPlanLayer();
+ if (this.flight.selected) {
+ this.path = L.polyline(path, { color: Colors.Highlight })
.addTo(layer)
.addTo(selectedFlightPlansLayer);
+ } else {
+ this.path = L.polyline(path, { color: color }).addTo(layer);
}
- });
+ }
- if (flight.selected) {
- L.polyline(points, { color: highlight })
- .addTo(layer)
- .addTo(selectedFlightPlansLayer);
- if (flight.commitBoundary) {
- L.polyline(flight.commitBoundary, { color: highlight, weight: 1 }).addTo(
- layer.addTo(selectedFlightPlansLayer)
- );
+ drawCommitBoundary() {
+ if (this.flight.selected) {
+ if (this.flight.commitBoundary) {
+ L.polyline(this.flight.commitBoundary, {
+ color: Colors.Highlight,
+ weight: 1,
+ })
+ .addTo(this.flightPlanLayer())
+ .addTo(selectedFlightPlansLayer);
+ }
}
- } else {
- L.polyline(points, { color: color, weight: 1 }).addTo(layer);
+ }
+
+ draw() {
+ const path = [];
+ this.flightPlan.forEach((waypoint) => {
+ if (waypoint.includeInPath()) {
+ path.push(waypoint.position());
+ }
+ if (this.shouldMark(waypoint)) {
+ waypoint.marker
+ .addTo(this.flightPlanLayer())
+ .addTo(selectedFlightPlansLayer);
+ }
+ });
+
+ this.drawPath(path);
+ this.drawCommitBoundary();
}
}
@@ -455,12 +552,12 @@ function drawFlightPlans() {
if (flight.selected) {
selected = flight;
} else {
- drawFlightPlan(flight);
+ new Flight(flight).draw();
}
});
if (selected != null) {
- drawFlightPlan(selected);
+ new Flight(selected).draw();
}
}