[2/3] Improve join point placement.

This commit is contained in:
Dan Albert 2021-07-15 18:49:07 -07:00
parent e03d710d53
commit d444d716f5
5 changed files with 243 additions and 35 deletions

View File

@ -1 +1,2 @@
from .ipzonegeometry import IpZoneGeometry
from .joinzonegeometry import JoinZoneGeometry

View File

@ -0,0 +1,84 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import shapely.ops
from dcs import Point
from shapely.geometry import Point as ShapelyPoint, Polygon
from game.theater import ConflictTheater
from game.utils import nautical_miles
if TYPE_CHECKING:
from game.coalition import Coalition
class JoinZoneGeometry:
"""Defines the zones used for finding optimal join point placement.
The zones themselves are stored in the class rather than just the resulting join
point so that the zones can be drawn in the map for debugging purposes.
"""
def __init__(
self,
target: Point,
home: Point,
ip: Point,
coalition: Coalition,
theater: ConflictTheater,
) -> None:
# 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
# want to enforce that the join point is:
#
# * Not closer to the target than the IP.
# * Not too close to the home airfield.
# * Not threatened.
# * A minimum distance from the IP.
# * Not too sharp a turn at the ingress point.
self.ip = ShapelyPoint(ip.x, ip.y)
self.threat_zone = coalition.opponent.threat_zone.all
self.home = ShapelyPoint(home.x, home.y)
self.ip_bubble = self.ip.buffer(coalition.doctrine.join_distance.meters)
ip_distance = ip.distance_to_point(target)
self.target_bubble = ShapelyPoint(target.x, target.y).buffer(ip_distance)
# The minimum distance between the home location and the IP.
min_distance_from_home = nautical_miles(5)
self.home_bubble = self.home.buffer(min_distance_from_home.meters)
self.excluded_zone = shapely.ops.unary_union(
[self.home_bubble, self.ip_bubble, self.target_bubble, self.threat_zone]
)
ip_heading = target.heading_between_point(ip)
# Arbitrarily large since this is later constrained by the map boundary, and
# we'll be picking a location close to the IP anyway. Just used to avoid real
# distance calculations to project to the map edge.
large_distance = nautical_miles(400).meters
turn_limit = 40
ip_limit_ccw = ip.point_from_heading(ip_heading - turn_limit, large_distance)
ip_limit_cw = ip.point_from_heading(ip_heading + turn_limit, large_distance)
ip_direction_limit_wedge = Polygon(
[
(ip.x, ip.y),
(ip_limit_ccw.x, ip_limit_ccw.y),
(ip_limit_cw.x, ip_limit_cw.y),
]
)
self.permissible_line = (
coalition.nav_mesh.map_bounds(theater)
.intersection(ip_direction_limit_wedge)
.intersection(self.excluded_zone.boundary)
)
def find_best_join_point(self) -> Point:
join, _ = shapely.ops.nearest_points(self.permissible_line, self.home)
return Point(join.x, join.y)

View File

@ -20,7 +20,7 @@ from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint
from game.data.doctrine import Doctrine
from game.flightplan import IpZoneGeometry
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.theater import (
Airfield,
ControlPoint,
@ -949,20 +949,22 @@ class FlightPlanBuilder:
def regenerate_package_waypoints(self) -> None:
from gen.ato import PackageWaypoints
package_airfield = self.package_airfield()
# Start by picking the best IP for the attack.
ingress_point = IpZoneGeometry(
self.package.target.position,
self.package_airfield().position,
package_airfield.position,
self.coalition,
).find_best_ip()
# Pick the join point based on the best route to the IP.
join_point = self.preferred_join_point(ingress_point)
if join_point is None:
# The entire path to the target is threatened. Use the fallback behavior for
# now.
self.legacy_package_waypoints_impl(ingress_point)
return
join_point = JoinZoneGeometry(
self.package.target.position,
package_airfield.position,
ingress_point,
self.coalition,
self.theater,
).find_best_join_point()
# And the split point based on the best route from the IP. Since that's no
# different than the best route *to* the IP, this is the same as the join point.

View File

@ -12,6 +12,7 @@ from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPo
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.flightplan import JoinZoneGeometry
from game.navmesh import NavMesh, NavMeshPoly
from game.profiling import logged_duration
from game.theater import (
@ -82,6 +83,12 @@ def shapely_to_leaflet_polys(
return [shapely_poly_to_leaflet_points(poly, theater) for poly in polys]
def shapely_line_to_leaflet_points(
line: LineString, theater: ConflictTheater
) -> list[LeafletLatLon]:
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
class ControlPointJs(QObject):
nameChanged = Signal()
blueChanged = Signal()
@ -822,16 +829,20 @@ class IpZonesJs(QObject):
def permissibleZone(self) -> list[LeafletPoly]:
return self._permissible_zone
@Property(list, notify=permissibleZoneChanged)
@Property(list, notify=safeZoneChanged)
def safeZone(self) -> list[LeafletPoly]:
return self._safe_zone
@classmethod
def empty(cls) -> IpZonesJs:
return IpZonesJs([], [], [], [])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return IpZonesJs.empty()
target = flight.package.target
home = flight.departure
if not ENABLE_EXPENSIVE_DEBUG_TOOLS or target == home:
return IpZonesJs([], [], [], [])
geometry = IpZoneGeometry(target.position, home.position, game.blue)
return IpZonesJs(
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
@ -841,6 +852,73 @@ class IpZonesJs(QObject):
)
class JoinZonesJs(QObject):
homeBubbleChanged = Signal()
targetBubbleChanged = Signal()
ipBubbleChanged = Signal()
excludedZoneChanged = Signal()
permissibleLineChanged = Signal()
def __init__(
self,
home_bubble: list[LeafletPoly],
target_bubble: list[LeafletPoly],
ip_bubble: list[LeafletPoly],
excluded_zone: list[LeafletPoly],
permissible_line: list[LeafletLatLon],
) -> None:
super().__init__()
self._home_bubble = home_bubble
self._target_bubble = target_bubble
self._ip_bubble = ip_bubble
self._excluded_zone = excluded_zone
self._permissible_line = permissible_line
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> list[LeafletPoly]:
return self._home_bubble
@Property(list, notify=targetBubbleChanged)
def targetBubble(self) -> list[LeafletPoly]:
return self._target_bubble
@Property(list, notify=ipBubbleChanged)
def ipBubble(self) -> list[LeafletPoly]:
return self._ip_bubble
@Property(list, notify=excludedZoneChanged)
def excludedZone(self) -> list[LeafletPoly]:
return self._excluded_zone
@Property(list, notify=permissibleLineChanged)
def permissibleLine(self) -> list[LeafletLatLon]:
return self._permissible_line
@classmethod
def empty(cls) -> JoinZonesJs:
return JoinZonesJs([], [], [], [], [])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return JoinZonesJs.empty()
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return JoinZonesJs.empty()
ip = flight.package.waypoints.ingress
geometry = JoinZoneGeometry(
target.position, home.position, ip, game.blue, game.theater
)
return JoinZonesJs(
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
shapely_to_leaflet_polys(geometry.target_bubble, game.theater),
shapely_to_leaflet_polys(geometry.ip_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zone, game.theater),
shapely_line_to_leaflet_points(geometry.permissible_line, game.theater),
)
class MapModel(QObject):
cleared = Signal()
@ -855,6 +933,7 @@ class MapModel(QObject):
mapZonesChanged = Signal()
unculledZonesChanged = Signal()
ipZonesChanged = Signal()
joinZonesChanged = Signal()
def __init__(self, game_model: GameModel) -> None:
super().__init__()
@ -871,7 +950,8 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._ip_zones = IpZonesJs([], [], [], [])
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._selected_flight_index: Optional[Tuple[int, int]] = None
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
@ -895,7 +975,7 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._ip_zones = IpZonesJs([], [], [], [])
self._ip_zones = IpZonesJs.empty()
self.cleared.emit()
def set_package_selection(self, index: int) -> None:
@ -984,11 +1064,14 @@ class MapModel(QObject):
) + self._flights_in_ato(self.game.red.ato, blue=False)
self.flightsChanged.emit()
selected_flight = self._get_selected_flight()
if selected_flight is not None:
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
if selected_flight is None:
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
else:
self._ip_zones = IpZonesJs([], [], [], [])
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
self.ipZonesChanged.emit()
self.joinZonesChanged.emit()
@Property(list, notify=flightsChanged)
def flights(self) -> List[FlightJs]:
@ -1121,6 +1204,10 @@ class MapModel(QObject):
def ipZones(self) -> IpZonesJs:
return self._ip_zones
@Property(JoinZonesJs, notify=joinZonesChanged)
def joinZones(self) -> JoinZonesJs:
return self._join_zones
@property
def game(self) -> Game:
if self.game_model.game is None:

View File

@ -198,10 +198,8 @@ const exclusionZones = L.layerGroup();
const seaZones = L.layerGroup();
const unculledZones = L.layerGroup();
const homeBubble = L.layerGroup();
const ipBubble = L.layerGroup();
const permissibleZone = L.layerGroup();
const safeZone = L.layerGroup();
const ipZones = L.layerGroup();
const joinZones = L.layerGroup().addTo(map);
const debugControlGroups = {
"Blue Threat Zones": {
@ -232,11 +230,9 @@ const debugControlGroups = {
};
if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
debugControlGroups["IP Zones"] = {
"Home bubble": homeBubble,
"IP bubble": ipBubble,
"Permissible zone": permissibleZone,
"Safe zone": safeZone,
debugControlGroups["Waypoint Zones"] = {
"IP Zones": ipZones,
"Join Zones": joinZones,
};
}
@ -287,7 +283,12 @@ L.control
L.control
.groupedLayers(null, debugControlGroups, {
position: "topleft",
exclusiveGroups: ["Blue Threat Zones", "Red Threat Zones", "Navmeshes"],
exclusiveGroups: [
"Blue Threat Zones",
"Red Threat Zones",
"Navmeshes",
"Waypoint Zones",
],
groupCheckboxes: true,
})
.addTo(map);
@ -308,6 +309,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.mapZonesChanged.connect(drawMapZones);
game.unculledZonesChanged.connect(drawUnculledZones);
game.ipZonesChanged.connect(drawIpZones);
game.joinZonesChanged.connect(drawJoinZones);
});
function recenterMap(center) {
@ -992,34 +994,65 @@ function drawUnculledZones() {
}
function drawIpZones() {
homeBubble.clearLayers();
ipBubble.clearLayers();
permissibleZone.clearLayers();
safeZone.clearLayers();
ipZones.clearLayers();
L.polygon(game.ipZones.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(homeBubble);
}).addTo(ipZones);
L.polygon(game.ipZones.ipBubble, {
color: "#bb89ff",
fillOpacity: 0.1,
interactive: false,
}).addTo(ipBubble);
}).addTo(ipZones);
L.polygon(game.ipZones.permissibleZone, {
color: "#ffffff",
fillOpacity: 0.1,
interactive: false,
}).addTo(permissibleZone);
}).addTo(ipZones);
L.polygon(game.ipZones.safeZone, {
color: Colors.Green,
fillOpacity: 0.1,
interactive: false,
}).addTo(safeZone);
}).addTo(ipZones);
}
function drawJoinZones() {
joinZones.clearLayers();
L.polygon(game.joinZones.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
L.polygon(game.joinZones.targetBubble, {
color: "#bb89ff",
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
L.polygon(game.joinZones.ipBubble, {
color: "#ffffff",
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
L.polygon(game.joinZones.excludedZone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
interactive: false,
}).addTo(joinZones);
L.polyline(game.joinZones.permissibleLine, {
color: Colors.Green,
interactive: false,
}).addTo(joinZones);
}
function drawInitialMap() {
@ -1034,6 +1067,7 @@ function drawInitialMap() {
drawMapZones();
drawUnculledZones();
drawIpZones();
drawJoinZones();
}
function clearAllLayers() {