diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index 61d42b23..0462ed20 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -97,21 +97,32 @@ class ControlPointJs(QObject): return [] return self.theater.point_to_ll(self.control_point.target_position).as_list() + def destination_in_range(self, destination: Point) -> bool: + from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE + + move_distance = meters( + destination.distance_to_point(self.control_point.position) + ) + return move_distance <= MAX_SHIP_DISTANCE + + @Slot(list, result=bool) + def destinationInRange(self, destination: LeafletLatLon) -> bool: + return self.destination_in_range(self.theater.ll_to_point(LatLon(*destination))) + @Slot(list, result=str) def setDestination(self, destination: LeafletLatLon) -> str: + from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE + if not self.control_point.moveable: return f"{self.control_point} is not mobile" if not self.control_point.captured: return f"{self.control_point} is not owned by player" - point = self.theater.ll_to_point(LatLon(*destination)) - from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE - move_distance = meters(point.distance_to_point(self.control_point.position)) - if move_distance > MAX_SHIP_DISTANCE: + point = self.theater.ll_to_point(LatLon(*destination)) + if not self.destination_in_range(point): return ( f"Cannot move {self.control_point} more than " - f"{MAX_SHIP_DISTANCE.nautical_miles}nm. Attempted " - f"{move_distance.nautical_miles}nm" + f"{MAX_SHIP_DISTANCE.nautical_miles}nm." ) self.control_point.target_position = point self.destinationChanged.emit(destination) diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index 792e50fa..1e1a91d7 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -15,8 +15,21 @@ const Colors = Object.freeze({ Blue: "#0084ff", Red: "#c85050", + Green: "#80BA80", }); +function metersToNauticalMiles(meters) { + return meters * 0.000539957; +} + +function formatLatLng(latLng) { + const lat = latLng.lat.toFixed(2); + const lng = latLng.lng.toFixed(2); + const ns = lat >= 0 ? "N" : "S"; + const ew = lng >= 0 ? "E" : "W"; + return `${lat}°${ns} ${lng}°${ew}`; +} + const map = L.map("map").setView([0, 0], 3); L.control.scale({ maxWidth: 200 }).addTo(map); @@ -152,14 +165,35 @@ class ControlPoint { this.cp.setDestination([destination.lat, destination.lng]).then((err) => { if (err) { console.log(`Could not set control point destination: ${err}`); + // Reset markers and paths on error. On success this happens when we get + // the destinationChanged signal from the backend. + this.onDestinationChanged(); } - // No need to update destination positions. The backend will emit an event - // that causes that if we've successfully changed the destination. If it - // was not successful, we've already reset the origin so no need for a - //change. }); } + onDrag(destination) { + this.path.setLatLngs([this.cp.position, destination]); + this.path.addTo(controlPointsLayer); + const distance = metersToNauticalMiles( + destination.distanceTo(this.cp.position) + ); + this.primaryMarker.unbindTooltip(); + this.primaryMarker.bindTooltip( + `Move ${distance.toFixed(1)}nm to ${formatLatLng(destination)}`, + { + permanent: true, + } + ); + this.cp + .destinationInRange([destination.lat, destination.lng]) + .then((inRange) => { + this.path.setStyle({ + color: inRange ? Colors.Green : Colors.Red, + }); + }); + } + detachTooltipsAndHandlers() { this.primaryMarker.unbindTooltip(); this.primaryMarker.off("click"); @@ -169,12 +203,13 @@ class ControlPoint { this.secondaryMarker.off("contextmenu"); } - attachTooltipsAndHandlers() { + attachTooltipsAndHandlers(dragging = false) { this.detachTooltipsAndHandlers(); const zoom = map.getZoom(); - const locationMarker = this.hasDestination() - ? this.secondaryMarker - : this.primaryMarker; + const locationMarker = + this.hasDestination() || dragging + ? this.secondaryMarker + : this.primaryMarker; const destinationMarker = this.hasDestination() ? this.primaryMarker : null; locationMarker .bindTooltip(`

${this.cp.name}

`, { @@ -187,7 +222,15 @@ class ControlPoint { this.cp.showPackageDialog(); }); if (destinationMarker != null) { - destinationMarker.bindTooltip(`${this.cp.name} destination`); + const origin = locationMarker.getLatLng(); + const destination = destinationMarker.getLatLng(); + const distance = metersToNauticalMiles( + destination.distanceTo(origin) + ).toFixed(1); + const dest = formatLatLng(destination); + destinationMarker.bindTooltip( + `${this.cp.name} moving ${distance}nm to ${dest} next turn` + ); destinationMarker.on("contextmenu", () => this.cp.cancelTravel()); destinationMarker.addTo(map); } @@ -206,6 +249,15 @@ class ControlPoint { draggable: this.cp.mobile, autoPan: true, }) + .on("dragstart", () => { + this.secondaryMarker.addTo(controlPointsLayer); + this.attachTooltipsAndHandlers(true); + }) + .on("drag", (event) => { + const marker = event.target; + const newPosition = marker.getLatLng(); + this.onDrag(newPosition); + }) .on("dragend", (event) => { const marker = event.target; const newPosition = marker.getLatLng(); @@ -224,7 +276,7 @@ class ControlPoint { makePath() { const destination = this.hasDestination() ? this.cp.destination : [0, 0]; return L.polyline([this.cp.position, destination], { - color: "#80BA80", + color: Colors.Green, weight: 1, }); } @@ -235,6 +287,7 @@ class ControlPoint { this.secondaryMarker.addTo(controlPointsLayer); this.path.setLatLngs([this.cp.position, this.cp.destination]); this.path.addTo(controlPointsLayer); + this.path.setStyle({ color: Colors.Green }); } else { this.hideDestination(); this.primaryMarker.setLatLng(this.cp.position);