From 53cb68f82c843cd3c7e43143c4beb483eab67708 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 15 May 2021 15:20:18 -0700 Subject: [PATCH] Make waypoints draggable. --- qt_ui/models.py | 15 +++- qt_ui/widgets/map/mapmodel.py | 52 +++++++++-- resources/ui/map/map.js | 165 +++++++++++++++++++++++++++------- 3 files changed, 188 insertions(+), 44 deletions(-) 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(); } }