mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
[3/3] Rework hold points.
This commit is contained in:
parent
d444d716f5
commit
82cca0a602
@ -7,6 +7,7 @@ Saves from 3.x are not compatible with 5.0.
|
||||
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
|
||||
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
|
||||
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
|
||||
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.
|
||||
|
||||
## Fixes
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
from .holdzonegeometry import HoldZoneGeometry
|
||||
from .ipzonegeometry import IpZoneGeometry
|
||||
from .joinzonegeometry import JoinZoneGeometry
|
||||
|
||||
108
game/flightplan/holdzonegeometry.py
Normal file
108
game/flightplan/holdzonegeometry.py
Normal file
@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import shapely.ops
|
||||
from dcs import Point
|
||||
from shapely.geometry import Point as ShapelyPoint, Polygon, MultiPolygon
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.utils import nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class HoldZoneGeometry:
|
||||
"""Defines the zones used for finding optimal hold point placement.
|
||||
|
||||
The zones themselves are stored in the class rather than just the resulting hold
|
||||
point so that the zones can be drawn in the map for debugging purposes.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target: Point,
|
||||
home: Point,
|
||||
ip: Point,
|
||||
join: Point,
|
||||
coalition: Coalition,
|
||||
theater: ConflictTheater,
|
||||
) -> None:
|
||||
# Hold points are placed one of two ways. Either approach guarantees:
|
||||
#
|
||||
# * Safe hold point.
|
||||
# * Minimum distance to the join point.
|
||||
# * Not closer to the target than the join point.
|
||||
#
|
||||
# 1. As near the join point as possible with a specific distance from the
|
||||
# departure airfield. This prevents loitering directly above the airfield but
|
||||
# also keeps the hold point close to the departure airfield.
|
||||
#
|
||||
# 2. Alternatively, if the entire home zone is excluded by the above criteria,
|
||||
# as neat the departure airfield as possible within a minimum distance from
|
||||
# the join point, with a restricted turn angle at the join point. This
|
||||
# handles the case where we need to backtrack from the departure airfield and
|
||||
# the join point to place the hold point, but the turn angle limit restricts
|
||||
# the maximum distance of the backtrack while maintaining the direction of
|
||||
# the flight plan.
|
||||
self.threat_zone = coalition.opponent.threat_zone.all
|
||||
self.home = ShapelyPoint(home.x, home.y)
|
||||
|
||||
self.join = ShapelyPoint(join.x, join.y)
|
||||
|
||||
self.join_bubble = self.join.buffer(coalition.doctrine.push_distance.meters)
|
||||
|
||||
join_to_target_distance = join.distance_to_point(target)
|
||||
self.target_bubble = ShapelyPoint(target.x, target.y).buffer(
|
||||
join_to_target_distance
|
||||
)
|
||||
|
||||
self.home_bubble = self.home.buffer(coalition.doctrine.hold_distance.meters)
|
||||
|
||||
excluded_zones = shapely.ops.unary_union(
|
||||
[self.join_bubble, self.target_bubble, self.threat_zone]
|
||||
)
|
||||
if not isinstance(excluded_zones, MultiPolygon):
|
||||
excluded_zones = MultiPolygon([excluded_zones])
|
||||
self.excluded_zones = excluded_zones
|
||||
|
||||
join_heading = ip.heading_between_point(join)
|
||||
|
||||
# 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
|
||||
join_limit_ccw = join.point_from_heading(
|
||||
join_heading - turn_limit, large_distance
|
||||
)
|
||||
join_limit_cw = join.point_from_heading(
|
||||
join_heading + turn_limit, large_distance
|
||||
)
|
||||
|
||||
join_direction_limit_wedge = Polygon(
|
||||
[
|
||||
(join.x, join.y),
|
||||
(join_limit_ccw.x, join_limit_ccw.y),
|
||||
(join_limit_cw.x, join_limit_cw.y),
|
||||
]
|
||||
)
|
||||
|
||||
permissible_zones = (
|
||||
coalition.nav_mesh.map_bounds(theater)
|
||||
.intersection(join_direction_limit_wedge)
|
||||
.difference(self.excluded_zones)
|
||||
.difference(self.home_bubble)
|
||||
)
|
||||
if not isinstance(permissible_zones, MultiPolygon):
|
||||
permissible_zones = MultiPolygon([permissible_zones])
|
||||
self.permissible_zones = permissible_zones
|
||||
self.preferred_lines = self.home_bubble.boundary.difference(self.excluded_zones)
|
||||
|
||||
def find_best_hold_point(self) -> Point:
|
||||
if self.preferred_lines.is_empty:
|
||||
hold, _ = shapely.ops.nearest_points(self.permissible_zones, self.home)
|
||||
else:
|
||||
hold, _ = shapely.ops.nearest_points(self.preferred_lines, self.join)
|
||||
return Point(hold.x, hold.y)
|
||||
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import shapely.ops
|
||||
from dcs import Point
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
from shapely.geometry import Point as ShapelyPoint, MultiPolygon
|
||||
|
||||
from game.utils import nautical_miles, meters
|
||||
|
||||
@ -81,10 +81,14 @@ class IpZoneGeometry:
|
||||
# the home bubble.
|
||||
self.permissible_zone = self.ip_bubble
|
||||
|
||||
self.safe_zone = self.permissible_zone.difference(
|
||||
safe_zones = self.permissible_zone.difference(
|
||||
self.threat_zone.buffer(attack_distance_buffer.meters)
|
||||
)
|
||||
|
||||
if not isinstance(safe_zones, MultiPolygon):
|
||||
safe_zones = MultiPolygon([safe_zones])
|
||||
self.safe_zones = safe_zones
|
||||
|
||||
def _unsafe_ip(self) -> ShapelyPoint:
|
||||
unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
|
||||
if unthreatened_home_zone.is_empty:
|
||||
@ -104,10 +108,10 @@ class IpZoneGeometry:
|
||||
def _safe_ip(self) -> ShapelyPoint:
|
||||
# We have a zone of possible IPs that are safe, close enough, and in range. Pick
|
||||
# the IP in the zone that's closest to the target.
|
||||
return shapely.ops.nearest_points(self.safe_zone, self.home)[0]
|
||||
return shapely.ops.nearest_points(self.safe_zones, self.home)[0]
|
||||
|
||||
def find_best_ip(self) -> Point:
|
||||
if self.safe_zone.is_empty:
|
||||
if self.safe_zones.is_empty:
|
||||
ip = self._unsafe_ip()
|
||||
else:
|
||||
ip = self._safe_ip()
|
||||
|
||||
@ -4,7 +4,12 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import shapely.ops
|
||||
from dcs import Point
|
||||
from shapely.geometry import Point as ShapelyPoint, Polygon
|
||||
from shapely.geometry import (
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
MultiPolygon,
|
||||
MultiLineString,
|
||||
)
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.utils import nautical_miles
|
||||
@ -51,10 +56,14 @@ class JoinZoneGeometry:
|
||||
|
||||
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]
|
||||
excluded_zones = shapely.ops.unary_union(
|
||||
[self.ip_bubble, self.target_bubble, self.threat_zone]
|
||||
)
|
||||
|
||||
if not isinstance(excluded_zones, MultiPolygon):
|
||||
excluded_zones = MultiPolygon([excluded_zones])
|
||||
self.excluded_zones = excluded_zones
|
||||
|
||||
ip_heading = target.heading_between_point(ip)
|
||||
|
||||
# Arbitrarily large since this is later constrained by the map boundary, and
|
||||
@ -73,12 +82,14 @@ class JoinZoneGeometry:
|
||||
]
|
||||
)
|
||||
|
||||
self.permissible_line = (
|
||||
coalition.nav_mesh.map_bounds(theater)
|
||||
.intersection(ip_direction_limit_wedge)
|
||||
.intersection(self.excluded_zone.boundary)
|
||||
)
|
||||
permissible_lines = ip_direction_limit_wedge.intersection(
|
||||
self.excluded_zones.boundary
|
||||
).difference(self.home_bubble)
|
||||
|
||||
if not isinstance(permissible_lines, MultiLineString):
|
||||
permissible_lines = MultiLineString([permissible_lines])
|
||||
self.permissible_lines = permissible_lines
|
||||
|
||||
def find_best_join_point(self) -> Point:
|
||||
join, _ = shapely.ops.nearest_points(self.permissible_line, self.home)
|
||||
join, _ = shapely.ops.nearest_points(self.permissible_lines, self.home)
|
||||
return Point(join.x, join.y)
|
||||
|
||||
@ -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, JoinZoneGeometry
|
||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -975,31 +975,6 @@ class FlightPlanBuilder:
|
||||
WaypointBuilder.perturb(join_point),
|
||||
)
|
||||
|
||||
def legacy_package_waypoints_impl(self, ingress_point: Point) -> None:
|
||||
from gen.ato import PackageWaypoints
|
||||
|
||||
join_point = self._rendezvous_point(ingress_point)
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
)
|
||||
|
||||
def safe_points_between(self, a: Point, b: Point) -> Iterator[Point]:
|
||||
for point in self.coalition.nav_mesh.shortest_path(a, b)[1:-1]:
|
||||
if not self.threat_zones.threatened(point):
|
||||
yield point
|
||||
|
||||
def preferred_join_point(self, ingress_point: Point) -> Optional[Point]:
|
||||
# Use non-threatened points along the path to the target as the join point. We
|
||||
# may need to try more than one in the event that the close non-threatened
|
||||
# points are closer than the ingress point itself.
|
||||
for join_point in self.safe_points_between(
|
||||
ingress_point, self.package_airfield().position
|
||||
):
|
||||
return join_point
|
||||
return None
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
"""Generates a strike flight plan.
|
||||
|
||||
@ -1675,48 +1650,10 @@ class FlightPlanBuilder:
|
||||
origin = flight.departure.position
|
||||
target = self.package.target.position
|
||||
join = self.package.waypoints.join
|
||||
origin_to_join = origin.distance_to_point(join)
|
||||
if meters(origin_to_join) < self.doctrine.push_distance:
|
||||
# If the origin airfield is closer to the join point, than the minimum push
|
||||
# distance. Plan the hold point such that it retreats from the origin
|
||||
# airfield.
|
||||
return join.point_from_heading(
|
||||
target.heading_between_point(origin), self.doctrine.push_distance.meters
|
||||
)
|
||||
|
||||
heading_to_join = origin.heading_between_point(join)
|
||||
hold_point = origin.point_from_heading(
|
||||
heading_to_join, self.doctrine.push_distance.meters
|
||||
)
|
||||
hold_distance = meters(hold_point.distance_to_point(join))
|
||||
if hold_distance >= self.doctrine.push_distance:
|
||||
# Hold point is between the origin airfield and the join point and
|
||||
# spaced sufficiently.
|
||||
return hold_point
|
||||
|
||||
# The hold point is between the origin airfield and the join point, but
|
||||
# the distance between the hold point and the join point is too short.
|
||||
# Bend the hold point out to extend the distance while maintaining the
|
||||
# minimum distance from the origin airfield to keep the AI flying
|
||||
# properly.
|
||||
origin_to_join = origin.distance_to_point(join)
|
||||
cos_theta = (
|
||||
self.doctrine.hold_distance.meters ** 2
|
||||
+ origin_to_join ** 2
|
||||
- self.doctrine.join_distance.meters ** 2
|
||||
) / (2 * self.doctrine.hold_distance.meters * origin_to_join)
|
||||
try:
|
||||
theta = math.acos(cos_theta)
|
||||
except ValueError:
|
||||
# No solution that maintains hold and join distances. Extend the
|
||||
# hold point away from the target.
|
||||
return origin.point_from_heading(
|
||||
target.heading_between_point(origin), self.doctrine.hold_distance.meters
|
||||
)
|
||||
|
||||
return origin.point_from_heading(
|
||||
heading_to_join - theta, self.doctrine.hold_distance.meters
|
||||
)
|
||||
ip = self.package.waypoints.ingress
|
||||
return HoldZoneGeometry(
|
||||
target, origin, ip, join, self.coalition, self.theater
|
||||
).find_best_hold_point()
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_rtb_waypoint(
|
||||
@ -1779,59 +1716,6 @@ class FlightPlanBuilder:
|
||||
lead_time=lead_time,
|
||||
)
|
||||
|
||||
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that retreats from the origin airfield."""
|
||||
return attack_transition.point_from_heading(
|
||||
self.package.target.position.heading_between_point(
|
||||
self.package_airfield().position
|
||||
),
|
||||
self.doctrine.join_distance.meters,
|
||||
)
|
||||
|
||||
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that advances toward the target."""
|
||||
heading = self._heading_to_package_airfield(attack_transition)
|
||||
return attack_transition.point_from_heading(
|
||||
heading, -self.doctrine.join_distance.meters
|
||||
)
|
||||
|
||||
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
|
||||
transition_target_distance = attack_transition.distance_to_point(
|
||||
self.package.target.position
|
||||
)
|
||||
origin_target_distance = self._distance_to_package_airfield(
|
||||
self.package.target.position
|
||||
)
|
||||
|
||||
# If the origin point is closer to the target than the ingress point,
|
||||
# the rendezvous point should be positioned in a position that retreats
|
||||
# from the origin airfield.
|
||||
return origin_target_distance < transition_target_distance
|
||||
|
||||
def _rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Returns the position of the rendezvous point.
|
||||
|
||||
Args:
|
||||
attack_transition: The ingress or target point for this rendezvous.
|
||||
"""
|
||||
if self._rendezvous_should_retreat(attack_transition):
|
||||
return self._retreating_rendezvous_point(attack_transition)
|
||||
return self._advancing_rendezvous_point(attack_transition)
|
||||
|
||||
def _ingress_point(self, heading: float) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180, self.doctrine.max_ingress_distance.meters
|
||||
)
|
||||
|
||||
def _target_heading_to_package_airfield(self) -> float:
|
||||
return self._heading_to_package_airfield(self.package.target.position)
|
||||
|
||||
def _heading_to_package_airfield(self, point: Point) -> float:
|
||||
return self.package_airfield().position.heading_between_point(point)
|
||||
|
||||
def _distance_to_package_airfield(self, point: Point) -> float:
|
||||
return self.package_airfield().position.distance_to_point(point)
|
||||
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
|
||||
@ -8,11 +8,17 @@ from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from dcs import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import vehicle_map
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPolygon
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
MultiPolygon,
|
||||
MultiLineString,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.flightplan import JoinZoneGeometry
|
||||
from game.flightplan import JoinZoneGeometry, HoldZoneGeometry
|
||||
from game.navmesh import NavMesh, NavMeshPoly
|
||||
from game.profiling import logged_duration
|
||||
from game.theater import (
|
||||
@ -89,6 +95,12 @@ def shapely_line_to_leaflet_points(
|
||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
|
||||
|
||||
|
||||
def shapely_lines_to_leaflet_points(
|
||||
lines: MultiLineString, theater: ConflictTheater
|
||||
) -> list[list[LeafletLatLon]]:
|
||||
return [shapely_line_to_leaflet_points(l, theater) for l in lines.geoms]
|
||||
|
||||
|
||||
class ControlPointJs(QObject):
|
||||
nameChanged = Signal()
|
||||
blueChanged = Signal()
|
||||
@ -802,36 +814,36 @@ class IpZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
ipBubbleChanged = Signal()
|
||||
permissibleZoneChanged = Signal()
|
||||
safeZoneChanged = Signal()
|
||||
safeZonesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: list[LeafletPoly],
|
||||
ip_bubble: list[LeafletPoly],
|
||||
permissible_zone: list[LeafletPoly],
|
||||
safe_zone: list[LeafletPoly],
|
||||
home_bubble: LeafletPoly,
|
||||
ip_bubble: LeafletPoly,
|
||||
permissible_zone: LeafletPoly,
|
||||
safe_zones: list[LeafletPoly],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._home_bubble = home_bubble
|
||||
self._ip_bubble = ip_bubble
|
||||
self._permissible_zone = permissible_zone
|
||||
self._safe_zone = safe_zone
|
||||
self._safe_zones = safe_zones
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> list[LeafletPoly]:
|
||||
def homeBubble(self) -> LeafletPoly:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=ipBubbleChanged)
|
||||
def ipBubble(self) -> list[LeafletPoly]:
|
||||
def ipBubble(self) -> LeafletPoly:
|
||||
return self._ip_bubble
|
||||
|
||||
@Property(list, notify=permissibleZoneChanged)
|
||||
def permissibleZone(self) -> list[LeafletPoly]:
|
||||
def permissibleZone(self) -> LeafletPoly:
|
||||
return self._permissible_zone
|
||||
|
||||
@Property(list, notify=safeZoneChanged)
|
||||
def safeZone(self) -> list[LeafletPoly]:
|
||||
return self._safe_zone
|
||||
@Property(list, notify=safeZonesChanged)
|
||||
def safeZones(self) -> list[LeafletPoly]:
|
||||
return self._safe_zones
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> IpZonesJs:
|
||||
@ -845,10 +857,10 @@ class IpZonesJs(QObject):
|
||||
home = flight.departure
|
||||
geometry = IpZoneGeometry(target.position, home.position, game.blue)
|
||||
return IpZonesJs(
|
||||
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.ip_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.permissible_zone, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.safe_zone, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.permissible_zone, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.safe_zones, game.theater),
|
||||
)
|
||||
|
||||
|
||||
@ -856,43 +868,43 @@ class JoinZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
targetBubbleChanged = Signal()
|
||||
ipBubbleChanged = Signal()
|
||||
excludedZoneChanged = Signal()
|
||||
permissibleLineChanged = Signal()
|
||||
excludedZonesChanged = Signal()
|
||||
permissibleLinesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: list[LeafletPoly],
|
||||
target_bubble: list[LeafletPoly],
|
||||
ip_bubble: list[LeafletPoly],
|
||||
excluded_zone: list[LeafletPoly],
|
||||
permissible_line: list[LeafletLatLon],
|
||||
home_bubble: LeafletPoly,
|
||||
target_bubble: LeafletPoly,
|
||||
ip_bubble: LeafletPoly,
|
||||
excluded_zones: list[LeafletPoly],
|
||||
permissible_lines: list[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
|
||||
self._excluded_zones = excluded_zones
|
||||
self._permissible_lines = permissible_lines
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> list[LeafletPoly]:
|
||||
def homeBubble(self) -> LeafletPoly:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=targetBubbleChanged)
|
||||
def targetBubble(self) -> list[LeafletPoly]:
|
||||
def targetBubble(self) -> LeafletPoly:
|
||||
return self._target_bubble
|
||||
|
||||
@Property(list, notify=ipBubbleChanged)
|
||||
def ipBubble(self) -> list[LeafletPoly]:
|
||||
def ipBubble(self) -> LeafletPoly:
|
||||
return self._ip_bubble
|
||||
|
||||
@Property(list, notify=excludedZoneChanged)
|
||||
def excludedZone(self) -> list[LeafletPoly]:
|
||||
return self._excluded_zone
|
||||
@Property(list, notify=excludedZonesChanged)
|
||||
def excludedZones(self) -> list[LeafletPoly]:
|
||||
return self._excluded_zones
|
||||
|
||||
@Property(list, notify=permissibleLineChanged)
|
||||
def permissibleLine(self) -> list[LeafletLatLon]:
|
||||
return self._permissible_line
|
||||
@Property(list, notify=permissibleLinesChanged)
|
||||
def permissibleLines(self) -> list[list[LeafletLatLon]]:
|
||||
return self._permissible_lines
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> JoinZonesJs:
|
||||
@ -911,11 +923,87 @@ class JoinZonesJs(QObject):
|
||||
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),
|
||||
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.ip_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
|
||||
shapely_lines_to_leaflet_points(geometry.permissible_lines, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class HoldZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
targetBubbleChanged = Signal()
|
||||
joinBubbleChanged = Signal()
|
||||
excludedZonesChanged = Signal()
|
||||
permissibleZonesChanged = Signal()
|
||||
permissibleLinesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: LeafletPoly,
|
||||
target_bubble: LeafletPoly,
|
||||
join_bubble: LeafletPoly,
|
||||
excluded_zones: list[LeafletPoly],
|
||||
permissible_zones: list[LeafletPoly],
|
||||
permissible_lines: list[list[LeafletLatLon]],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._home_bubble = home_bubble
|
||||
self._target_bubble = target_bubble
|
||||
self._join_bubble = join_bubble
|
||||
self._excluded_zones = excluded_zones
|
||||
self._permissible_zones = permissible_zones
|
||||
self._permissible_lines = permissible_lines
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> LeafletPoly:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=targetBubbleChanged)
|
||||
def targetBubble(self) -> LeafletPoly:
|
||||
return self._target_bubble
|
||||
|
||||
@Property(list, notify=joinBubbleChanged)
|
||||
def joinBubble(self) -> LeafletPoly:
|
||||
return self._join_bubble
|
||||
|
||||
@Property(list, notify=excludedZonesChanged)
|
||||
def excludedZones(self) -> list[LeafletPoly]:
|
||||
return self._excluded_zones
|
||||
|
||||
@Property(list, notify=permissibleZonesChanged)
|
||||
def permissibleZones(self) -> list[LeafletPoly]:
|
||||
return self._permissible_zones
|
||||
|
||||
@Property(list, notify=permissibleLinesChanged)
|
||||
def permissibleLines(self) -> list[list[LeafletLatLon]]:
|
||||
return self._permissible_lines
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> HoldZonesJs:
|
||||
return HoldZonesJs([], [], [], [], [], [])
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs:
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
|
||||
return JoinZonesJs.empty()
|
||||
target = flight.package.target
|
||||
home = flight.departure
|
||||
if flight.package.waypoints is None:
|
||||
return HoldZonesJs.empty()
|
||||
ip = flight.package.waypoints.ingress
|
||||
join = flight.package.waypoints.join
|
||||
geometry = HoldZoneGeometry(
|
||||
target.position, home.position, ip, join, game.blue, game.theater
|
||||
)
|
||||
return HoldZonesJs(
|
||||
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.join_bubble, 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_lines, game.theater),
|
||||
)
|
||||
|
||||
|
||||
@ -934,6 +1022,7 @@ class MapModel(QObject):
|
||||
unculledZonesChanged = Signal()
|
||||
ipZonesChanged = Signal()
|
||||
joinZonesChanged = Signal()
|
||||
holdZonesChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
@ -952,6 +1041,7 @@ class MapModel(QObject):
|
||||
self._unculled_zones = []
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
self._hold_zones = HoldZonesJs.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)
|
||||
@ -1067,11 +1157,14 @@ class MapModel(QObject):
|
||||
if selected_flight is None:
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
self._hold_zones = HoldZonesJs.empty()
|
||||
else:
|
||||
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
|
||||
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
|
||||
self._hold_zones = HoldZonesJs.for_flight(selected_flight, self.game)
|
||||
self.ipZonesChanged.emit()
|
||||
self.joinZonesChanged.emit()
|
||||
self.holdZonesChanged.emit()
|
||||
|
||||
@Property(list, notify=flightsChanged)
|
||||
def flights(self) -> List[FlightJs]:
|
||||
@ -1208,6 +1301,10 @@ class MapModel(QObject):
|
||||
def joinZones(self) -> JoinZonesJs:
|
||||
return self._join_zones
|
||||
|
||||
@Property(HoldZonesJs, notify=holdZonesChanged)
|
||||
def holdZones(self) -> HoldZonesJs:
|
||||
return self._hold_zones
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
|
||||
@ -198,8 +198,10 @@ const exclusionZones = L.layerGroup();
|
||||
const seaZones = L.layerGroup();
|
||||
const unculledZones = L.layerGroup();
|
||||
|
||||
const noWaypointZones = L.layerGroup();
|
||||
const ipZones = L.layerGroup();
|
||||
const joinZones = L.layerGroup().addTo(map);
|
||||
const joinZones = L.layerGroup();
|
||||
const holdZones = L.layerGroup().addTo(map);
|
||||
|
||||
const debugControlGroups = {
|
||||
"Blue Threat Zones": {
|
||||
@ -231,8 +233,10 @@ const debugControlGroups = {
|
||||
|
||||
if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
||||
debugControlGroups["Waypoint Zones"] = {
|
||||
None: noWaypointZones,
|
||||
"IP Zones": ipZones,
|
||||
"Join Zones": joinZones,
|
||||
"Hold Zones": holdZones,
|
||||
};
|
||||
}
|
||||
|
||||
@ -310,6 +314,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game.unculledZonesChanged.connect(drawUnculledZones);
|
||||
game.ipZonesChanged.connect(drawIpZones);
|
||||
game.joinZonesChanged.connect(drawJoinZones);
|
||||
game.holdZonesChanged.connect(drawHoldZones);
|
||||
});
|
||||
|
||||
function recenterMap(center) {
|
||||
@ -1014,11 +1019,13 @@ function drawIpZones() {
|
||||
interactive: false,
|
||||
}).addTo(ipZones);
|
||||
|
||||
L.polygon(game.ipZones.safeZone, {
|
||||
color: Colors.Green,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(ipZones);
|
||||
for (const zone of game.ipZones.safeZones) {
|
||||
L.polygon(zone, {
|
||||
color: Colors.Green,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(ipZones);
|
||||
}
|
||||
}
|
||||
|
||||
function drawJoinZones() {
|
||||
@ -1042,17 +1049,59 @@ function drawJoinZones() {
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
|
||||
L.polygon(game.joinZones.excludedZone, {
|
||||
color: "#ffa500",
|
||||
fillOpacity: 0.2,
|
||||
stroke: false,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
for (const zone of game.joinZones.excludedZones) {
|
||||
L.polygon(zone, {
|
||||
color: "#ffa500",
|
||||
fillOpacity: 0.2,
|
||||
stroke: false,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
}
|
||||
|
||||
L.polyline(game.joinZones.permissibleLine, {
|
||||
color: Colors.Green,
|
||||
for (const line of game.joinZones.permissibleLines) {
|
||||
L.polyline(line, {
|
||||
color: Colors.Green,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
}
|
||||
}
|
||||
|
||||
function drawHoldZones() {
|
||||
holdZones.clearLayers();
|
||||
|
||||
L.polygon(game.holdZones.homeBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
}).addTo(holdZones);
|
||||
|
||||
L.polygon(game.holdZones.targetBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(holdZones);
|
||||
|
||||
L.polygon(game.holdZones.joinBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(holdZones);
|
||||
|
||||
for (const zone of game.holdZones.excludedZones) {
|
||||
L.polygon(zone, {
|
||||
color: "#ffa500",
|
||||
fillOpacity: 0.2,
|
||||
stroke: false,
|
||||
interactive: false,
|
||||
}).addTo(holdZones);
|
||||
}
|
||||
|
||||
for (const zone of game.holdZones.permissibleZones) {
|
||||
L.polygon(zone, {
|
||||
color: Colors.Green,
|
||||
interactive: false,
|
||||
}).addTo(holdZones);
|
||||
}
|
||||
}
|
||||
|
||||
function drawInitialMap() {
|
||||
@ -1068,6 +1117,7 @@ function drawInitialMap() {
|
||||
drawUnculledZones();
|
||||
drawIpZones();
|
||||
drawJoinZones();
|
||||
drawHoldZones();
|
||||
}
|
||||
|
||||
function clearAllLayers() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user