Add fallback locations for join zones.

It's rare with the current 5NM buffer around the origin, but if we use
the hold distance as the buffer like we maybe should it's possible for
the preferred join locations to fall entirely within the home zone. In
that case, fall back to a location within the max-turn-zone that's
outside the home zone and is nearest the IP.
This commit is contained in:
Dan Albert 2021-07-18 14:41:22 -07:00
parent 2580fe6b79
commit e22e8669e1
4 changed files with 59 additions and 32 deletions

View File

@ -11,7 +11,6 @@ from shapely.geometry import (
MultiLineString, MultiLineString,
) )
from game.theater import ConflictTheater
from game.utils import nautical_miles from game.utils import nautical_miles
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,12 +25,7 @@ class JoinZoneGeometry:
""" """
def __init__( def __init__(
self, self, target: Point, home: Point, ip: Point, coalition: Coalition
target: Point,
home: Point,
ip: Point,
coalition: Coalition,
theater: ConflictTheater,
) -> None: ) -> None:
# Normal join placement is based on the path from home to the IP. If no path is # Normal join placement is based on the path from home to the IP. If no path is
# found it means that the target is on a direct path. In that case we instead # found it means that the target is on a direct path. In that case we instead
@ -82,14 +76,28 @@ class JoinZoneGeometry:
] ]
) )
permissible_lines = ip_direction_limit_wedge.intersection( permissible_zones = ip_direction_limit_wedge.difference(
self.excluded_zones
).difference(self.home_bubble)
if permissible_zones.is_empty:
permissible_zones = MultiPolygon([])
if not isinstance(permissible_zones, MultiPolygon):
permissible_zones = MultiPolygon([permissible_zones])
self.permissible_zones = permissible_zones
preferred_lines = ip_direction_limit_wedge.intersection(
self.excluded_zones.boundary self.excluded_zones.boundary
).difference(self.home_bubble) ).difference(self.home_bubble)
if not isinstance(permissible_lines, MultiLineString): if preferred_lines.is_empty:
permissible_lines = MultiLineString([permissible_lines]) preferred_lines = MultiLineString([])
self.permissible_lines = permissible_lines if not isinstance(preferred_lines, MultiLineString):
preferred_lines = MultiLineString([preferred_lines])
self.preferred_lines = preferred_lines
def find_best_join_point(self) -> Point: def find_best_join_point(self) -> Point:
join, _ = shapely.ops.nearest_points(self.permissible_lines, self.home) if self.preferred_lines.is_empty:
join, _ = shapely.ops.nearest_points(self.permissible_zones, self.ip)
else:
join, _ = shapely.ops.nearest_points(self.preferred_lines, self.home)
return Point(join.x, join.y) return Point(join.x, join.y)

View File

@ -975,7 +975,6 @@ class FlightPlanBuilder:
package_airfield.position, package_airfield.position,
ingress_point, ingress_point,
self.coalition, self.coalition,
self.theater,
).find_best_join_point() ).find_best_join_point()
# And the split point based on the best route from the IP. Since that's no # And the split point based on the best route from the IP. Since that's no

View File

@ -869,7 +869,8 @@ class JoinZonesJs(QObject):
targetBubbleChanged = Signal() targetBubbleChanged = Signal()
ipBubbleChanged = Signal() ipBubbleChanged = Signal()
excludedZonesChanged = Signal() excludedZonesChanged = Signal()
permissibleLinesChanged = Signal() permissibleZonesChanged = Signal()
preferredLinesChanged = Signal()
def __init__( def __init__(
self, self,
@ -877,14 +878,16 @@ class JoinZonesJs(QObject):
target_bubble: LeafletPoly, target_bubble: LeafletPoly,
ip_bubble: LeafletPoly, ip_bubble: LeafletPoly,
excluded_zones: list[LeafletPoly], excluded_zones: list[LeafletPoly],
permissible_lines: list[list[LeafletLatLon]], permissible_zones: list[LeafletPoly],
preferred_lines: list[list[LeafletLatLon]],
) -> None: ) -> None:
super().__init__() super().__init__()
self._home_bubble = home_bubble self._home_bubble = home_bubble
self._target_bubble = target_bubble self._target_bubble = target_bubble
self._ip_bubble = ip_bubble self._ip_bubble = ip_bubble
self._excluded_zones = excluded_zones self._excluded_zones = excluded_zones
self._permissible_lines = permissible_lines self._permissible_zones = permissible_zones
self._preferred_lines = preferred_lines
@Property(list, notify=homeBubbleChanged) @Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> LeafletPoly: def homeBubble(self) -> LeafletPoly:
@ -902,13 +905,17 @@ class JoinZonesJs(QObject):
def excludedZones(self) -> list[LeafletPoly]: def excludedZones(self) -> list[LeafletPoly]:
return self._excluded_zones return self._excluded_zones
@Property(list, notify=permissibleLinesChanged) @Property(list, notify=permissibleZonesChanged)
def permissibleLines(self) -> list[list[LeafletLatLon]]: def permissibleZones(self) -> list[LeafletPoly]:
return self._permissible_lines return self._permissible_zones
@Property(list, notify=preferredLinesChanged)
def preferredLines(self) -> list[list[LeafletLatLon]]:
return self._preferred_lines
@classmethod @classmethod
def empty(cls) -> JoinZonesJs: def empty(cls) -> JoinZonesJs:
return JoinZonesJs([], [], [], [], []) return JoinZonesJs([], [], [], [], [], [])
@classmethod @classmethod
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs: def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
@ -919,15 +926,14 @@ class JoinZonesJs(QObject):
if flight.package.waypoints is None: if flight.package.waypoints is None:
return JoinZonesJs.empty() return JoinZonesJs.empty()
ip = flight.package.waypoints.ingress ip = flight.package.waypoints.ingress
geometry = JoinZoneGeometry( geometry = JoinZoneGeometry(target.position, home.position, ip, game.blue)
target.position, home.position, ip, game.blue, game.theater
)
return JoinZonesJs( return JoinZonesJs(
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater), shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater), shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater), shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater), shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
shapely_lines_to_leaflet_points(geometry.permissible_lines, game.theater), shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
shapely_lines_to_leaflet_points(geometry.preferred_lines, game.theater),
) )
@ -937,7 +943,7 @@ class HoldZonesJs(QObject):
joinBubbleChanged = Signal() joinBubbleChanged = Signal()
excludedZonesChanged = Signal() excludedZonesChanged = Signal()
permissibleZonesChanged = Signal() permissibleZonesChanged = Signal()
permissibleLinesChanged = Signal() preferredLinesChanged = Signal()
def __init__( def __init__(
self, self,
@ -946,7 +952,7 @@ class HoldZonesJs(QObject):
join_bubble: LeafletPoly, join_bubble: LeafletPoly,
excluded_zones: list[LeafletPoly], excluded_zones: list[LeafletPoly],
permissible_zones: list[LeafletPoly], permissible_zones: list[LeafletPoly],
permissible_lines: list[list[LeafletLatLon]], preferred_lines: list[list[LeafletLatLon]],
) -> None: ) -> None:
super().__init__() super().__init__()
self._home_bubble = home_bubble self._home_bubble = home_bubble
@ -954,7 +960,7 @@ class HoldZonesJs(QObject):
self._join_bubble = join_bubble self._join_bubble = join_bubble
self._excluded_zones = excluded_zones self._excluded_zones = excluded_zones
self._permissible_zones = permissible_zones self._permissible_zones = permissible_zones
self._permissible_lines = permissible_lines self._preferred_lines = preferred_lines
@Property(list, notify=homeBubbleChanged) @Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> LeafletPoly: def homeBubble(self) -> LeafletPoly:
@ -976,9 +982,9 @@ class HoldZonesJs(QObject):
def permissibleZones(self) -> list[LeafletPoly]: def permissibleZones(self) -> list[LeafletPoly]:
return self._permissible_zones return self._permissible_zones
@Property(list, notify=permissibleLinesChanged) @Property(list, notify=preferredLinesChanged)
def permissibleLines(self) -> list[list[LeafletLatLon]]: def preferredLines(self) -> list[list[LeafletLatLon]]:
return self._permissible_lines return self._preferred_lines
@classmethod @classmethod
def empty(cls) -> HoldZonesJs: def empty(cls) -> HoldZonesJs:
@ -1003,7 +1009,7 @@ class HoldZonesJs(QObject):
shapely_poly_to_leaflet_points(geometry.join_bubble, game.theater), shapely_poly_to_leaflet_points(geometry.join_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater), shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
shapely_to_leaflet_polys(geometry.permissible_zones, game.theater), shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
[], # shapely_to_leaflet_polys(geometry.permissible_lines, game.theater), shapely_lines_to_leaflet_points(geometry.preferred_lines, game.theater),
) )

View File

@ -1066,7 +1066,14 @@ function drawJoinZones() {
}).addTo(joinZones); }).addTo(joinZones);
} }
for (const line of game.joinZones.permissibleLines) { for (const zone of game.joinZones.permissibleZones) {
L.polygon(zone, {
color: Colors.Green,
interactive: false,
}).addTo(joinZones);
}
for (const line of game.joinZones.preferredLines) {
L.polyline(line, { L.polyline(line, {
color: Colors.Green, color: Colors.Green,
interactive: false, interactive: false,
@ -1114,6 +1121,13 @@ function drawHoldZones() {
interactive: false, interactive: false,
}).addTo(holdZones); }).addTo(holdZones);
} }
for (const line of game.holdZones.preferredLines) {
L.polyline(line, {
color: Colors.Green,
interactive: false,
}).addTo(holdZones);
}
} }
function drawInitialMap() { function drawInitialMap() {