Make waypoints draggable.

This commit is contained in:
Dan Albert 2021-05-15 15:20:18 -07:00
parent 95b107ffad
commit 53cb68f82c
3 changed files with 188 additions and 44 deletions

View File

@ -177,8 +177,6 @@ class PackageModel(QAbstractListModel):
def set_tot(self, tot: datetime.timedelta) -> None: def set_tot(self, tot: datetime.timedelta) -> None:
self.package.time_over_target = tot self.package.time_over_target = tot
self.update_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: def set_asap(self, asap: bool) -> None:
self.package.auto_asap = asap self.package.auto_asap = asap
@ -188,6 +186,8 @@ class PackageModel(QAbstractListModel):
if self.package.auto_asap: if self.package.auto_asap:
self.package.set_tot_asap() self.package.set_tot_asap()
self.tot_changed.emit() self.tot_changed.emit()
# For some reason this is needed to make the UI update quickly.
self.layoutChanged.emit()
@property @property
def mission_target(self) -> MissionTarget: def mission_target(self) -> MissionTarget:
@ -291,6 +291,12 @@ class AtoModel(QAbstractListModel):
"""Returns a model for the package at the given index.""" """Returns a model for the package at the given index."""
return self.package_models.acquire(self.package_at_index(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 @property
def packages(self) -> Iterator[PackageModel]: def packages(self) -> Iterator[PackageModel]:
"""Iterates over all the packages in the ATO.""" """Iterates over all the packages in the ATO."""
@ -376,6 +382,11 @@ class GameModel:
self.ato_model = AtoModel(self, self.game.blue_ato) self.ato_model = AtoModel(self, self.game.blue_ato)
self.red_ato_model = AtoModel(self, self.game.red_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: def set(self, game: Optional[Game]) -> None:
"""Updates the managed Game object. """Updates the managed Game object.

View File

@ -23,7 +23,7 @@ from gen.ato import AirTaskingOrder
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from gen.flights.flightplan import FlightPlan, PatrollingFlightPlan from gen.flights.flightplan import FlightPlan, PatrollingFlightPlan
from qt_ui.dialogs import Dialog 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.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
@ -316,20 +316,27 @@ class WaypointJs(QObject):
altitudeReferenceChanged = Signal() altitudeReferenceChanged = Signal()
nameChanged = Signal() nameChanged = Signal()
timingChanged = Signal() timingChanged = Signal()
isTakeoffChanged = Signal()
isDivertChanged = Signal() isDivertChanged = Signal()
def __init__( def __init__(
self, self,
waypoint: FlightWaypoint, waypoint: FlightWaypoint,
number: int, number: int,
flight_plan: FlightPlan, flight: Flight,
theater: ConflictTheater, theater: ConflictTheater,
ato_model: AtoModel,
) -> None: ) -> None:
super().__init__() super().__init__()
self.waypoint = waypoint self.waypoint = waypoint
self._number = number self._number = number
self.flight_plan = flight_plan self.flight = flight
self.theater = theater self.theater = theater
self.ato_model = ato_model
@property
def flight_plan(self) -> FlightPlan:
return self.flight.flight_plan
@Property(int, notify=numberChanged) @Property(int, notify=numberChanged)
def number(self) -> int: def number(self) -> int:
@ -363,10 +370,26 @@ class WaypointJs(QObject):
return "" return ""
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}" 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) @Property(bool, notify=isDivertChanged)
def isDivert(self) -> bool: def isDivert(self) -> bool:
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT 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): class FlightJs(QObject):
flightPlanChanged = Signal() flightPlanChanged = Signal()
@ -375,16 +398,26 @@ class FlightJs(QObject):
commitBoundaryChanged = Signal() commitBoundaryChanged = Signal()
def __init__( def __init__(
self, flight: Flight, selected: bool, theater: ConflictTheater, faction: Faction self,
flight: Flight,
selected: bool,
theater: ConflictTheater,
faction: Faction,
ato_model: AtoModel,
) -> None: ) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight = flight
self._selected = selected self._selected = selected
self.theater = theater self.theater = theater
self.faction = faction self.faction = faction
self.ato_model = ato_model
self._waypoints = self.make_waypoints() self._waypoints = self.make_waypoints()
self._commit_boundary = self.make_commit_boundary() 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]: def make_waypoints(self) -> List[WaypointJs]:
departure = FlightWaypoint( departure = FlightWaypoint(
FlightWaypointType.TAKEOFF, FlightWaypointType.TAKEOFF,
@ -393,10 +426,12 @@ class FlightJs(QObject):
meters(0), meters(0),
) )
departure.alt_type = "RADIO" departure.alt_type = "RADIO"
return [ waypoints = []
WaypointJs(p, i, self.flight.flight_plan, self.theater) for idx, point in enumerate([departure] + self.flight.points):
for i, p 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]]: def make_commit_boundary(self) -> Optional[List[LeafletLatLon]]:
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan): 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, selected=blue and (p_idx, f_idx) == self._selected_flight_index,
theater=self.game.theater, theater=self.game.theater,
faction=self.game.faction_for(blue), faction=self.game.faction_for(blue),
ato_model=self.game_model.ato_model_for(blue),
) )
) )
return flights return flights

View File

@ -16,6 +16,7 @@ const Colors = Object.freeze({
Blue: "#0084ff", Blue: "#0084ff",
Red: "#c85050", Red: "#c85050",
Green: "#80BA80", Green: "#80BA80",
Highlight: "#ffff00",
}); });
function metersToNauticalMiles(meters) { function metersToNauticalMiles(meters) {
@ -401,44 +402,140 @@ function drawFrontLines() {
const SHOW_WAYPOINT_INFO_AT_ZOOM = 9; const SHOW_WAYPOINT_INFO_AT_ZOOM = 9;
function drawFlightPlan(flight) { class Waypoint {
const layer = flight.blue ? blueFlightPlansLayer : redFlightPlansLayer; constructor(waypoint, flight) {
const color = flight.blue ? Colors.Blue : Colors.Red; this.waypoint = waypoint;
const highlight = "#ffff00"; this.flight = flight;
// We don't need a marker for the departure waypoint (and it's likely this.marker = this.makeMarker();
// coincident with the landing waypoint, so hard to see). We do want to draw this.waypoint.positionChanged.connect(() => this.relocate());
// the path from it though. this.waypoint.timingChanged.connect(() => this.updateDescription());
const points = [flight.flightPlan[0].position]; }
const zoom = map.getZoom();
flight.flightPlan.slice(1).forEach((waypoint) => {
if (!waypoint.isDivert) {
points.push(waypoint.position);
}
if (flight.selected) { position() {
L.marker(waypoint.position) return this.waypoint.position;
.bindTooltip( }
`${waypoint.number} ${waypoint.name}<br />` +
`${waypoint.altitudeFt} ft ${waypoint.altitudeReference}<br />` + shouldMark() {
`${waypoint.timing}`, // We don't need a marker for the departure waypoint (and it's likely
{ permanent: zoom >= SHOW_WAYPOINT_INFO_AT_ZOOM } // 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}<br />` +
`${this.waypoint.altitudeFt} ft ${this.waypoint.altitudeReference}<br />` +
`${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(layer)
.addTo(selectedFlightPlansLayer); .addTo(selectedFlightPlansLayer);
} else {
this.path = L.polyline(path, { color: color }).addTo(layer);
} }
}); }
if (flight.selected) { drawCommitBoundary() {
L.polyline(points, { color: highlight }) if (this.flight.selected) {
.addTo(layer) if (this.flight.commitBoundary) {
.addTo(selectedFlightPlansLayer); L.polyline(this.flight.commitBoundary, {
if (flight.commitBoundary) { color: Colors.Highlight,
L.polyline(flight.commitBoundary, { color: highlight, weight: 1 }).addTo( weight: 1,
layer.addTo(selectedFlightPlansLayer) })
); .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) { if (flight.selected) {
selected = flight; selected = flight;
} else { } else {
drawFlightPlan(flight); new Flight(flight).draw();
} }
}); });
if (selected != null) { if (selected != null) {
drawFlightPlan(selected); new Flight(selected).draw();
} }
} }