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]** 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]** 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]** 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
|
## Fixes
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
|
from .holdzonegeometry import HoldZoneGeometry
|
||||||
from .ipzonegeometry import IpZoneGeometry
|
from .ipzonegeometry import IpZoneGeometry
|
||||||
from .joinzonegeometry import JoinZoneGeometry
|
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
|
import shapely.ops
|
||||||
from dcs import Point
|
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
|
from game.utils import nautical_miles, meters
|
||||||
|
|
||||||
@ -81,10 +81,14 @@ class IpZoneGeometry:
|
|||||||
# the home bubble.
|
# the home bubble.
|
||||||
self.permissible_zone = self.ip_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)
|
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:
|
def _unsafe_ip(self) -> ShapelyPoint:
|
||||||
unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
|
unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
|
||||||
if unthreatened_home_zone.is_empty:
|
if unthreatened_home_zone.is_empty:
|
||||||
@ -104,10 +108,10 @@ class IpZoneGeometry:
|
|||||||
def _safe_ip(self) -> ShapelyPoint:
|
def _safe_ip(self) -> ShapelyPoint:
|
||||||
# We have a zone of possible IPs that are safe, close enough, and in range. Pick
|
# 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.
|
# 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:
|
def find_best_ip(self) -> Point:
|
||||||
if self.safe_zone.is_empty:
|
if self.safe_zones.is_empty:
|
||||||
ip = self._unsafe_ip()
|
ip = self._unsafe_ip()
|
||||||
else:
|
else:
|
||||||
ip = self._safe_ip()
|
ip = self._safe_ip()
|
||||||
|
|||||||
@ -4,7 +4,12 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import shapely.ops
|
import shapely.ops
|
||||||
from dcs import Point
|
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.theater import ConflictTheater
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
@ -51,10 +56,14 @@ class JoinZoneGeometry:
|
|||||||
|
|
||||||
self.home_bubble = self.home.buffer(min_distance_from_home.meters)
|
self.home_bubble = self.home.buffer(min_distance_from_home.meters)
|
||||||
|
|
||||||
self.excluded_zone = shapely.ops.unary_union(
|
excluded_zones = shapely.ops.unary_union(
|
||||||
[self.home_bubble, self.ip_bubble, self.target_bubble, self.threat_zone]
|
[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)
|
ip_heading = target.heading_between_point(ip)
|
||||||
|
|
||||||
# Arbitrarily large since this is later constrained by the map boundary, and
|
# Arbitrarily large since this is later constrained by the map boundary, and
|
||||||
@ -73,12 +82,14 @@ class JoinZoneGeometry:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.permissible_line = (
|
permissible_lines = ip_direction_limit_wedge.intersection(
|
||||||
coalition.nav_mesh.map_bounds(theater)
|
self.excluded_zones.boundary
|
||||||
.intersection(ip_direction_limit_wedge)
|
).difference(self.home_bubble)
|
||||||
.intersection(self.excluded_zone.boundary)
|
|
||||||
)
|
if not isinstance(permissible_lines, MultiLineString):
|
||||||
|
permissible_lines = MultiLineString([permissible_lines])
|
||||||
|
self.permissible_lines = permissible_lines
|
||||||
|
|
||||||
def find_best_join_point(self) -> Point:
|
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)
|
return Point(join.x, join.y)
|
||||||
|
|||||||
@ -20,7 +20,7 @@ from dcs.unit import Unit
|
|||||||
from shapely.geometry import Point as ShapelyPoint
|
from shapely.geometry import Point as ShapelyPoint
|
||||||
|
|
||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
|
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
Airfield,
|
Airfield,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@ -975,31 +975,6 @@ class FlightPlanBuilder:
|
|||||||
WaypointBuilder.perturb(join_point),
|
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:
|
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
"""Generates a strike flight plan.
|
"""Generates a strike flight plan.
|
||||||
|
|
||||||
@ -1675,48 +1650,10 @@ class FlightPlanBuilder:
|
|||||||
origin = flight.departure.position
|
origin = flight.departure.position
|
||||||
target = self.package.target.position
|
target = self.package.target.position
|
||||||
join = self.package.waypoints.join
|
join = self.package.waypoints.join
|
||||||
origin_to_join = origin.distance_to_point(join)
|
ip = self.package.waypoints.ingress
|
||||||
if meters(origin_to_join) < self.doctrine.push_distance:
|
return HoldZoneGeometry(
|
||||||
# If the origin airfield is closer to the join point, than the minimum push
|
target, origin, ip, join, self.coalition, self.theater
|
||||||
# distance. Plan the hold point such that it retreats from the origin
|
).find_best_hold_point()
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||||
def generate_rtb_waypoint(
|
def generate_rtb_waypoint(
|
||||||
@ -1779,59 +1716,6 @@ class FlightPlanBuilder:
|
|||||||
lead_time=lead_time,
|
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:
|
def package_airfield(self) -> ControlPoint:
|
||||||
# We'll always have a package, but if this is being planned via the UI
|
# We'll always have a package, but if this is being planned via the UI
|
||||||
# it could be the first flight in the package.
|
# 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 import Point
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.vehicles import vehicle_map
|
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 import Game
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
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.navmesh import NavMesh, NavMeshPoly
|
||||||
from game.profiling import logged_duration
|
from game.profiling import logged_duration
|
||||||
from game.theater import (
|
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]
|
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):
|
class ControlPointJs(QObject):
|
||||||
nameChanged = Signal()
|
nameChanged = Signal()
|
||||||
blueChanged = Signal()
|
blueChanged = Signal()
|
||||||
@ -802,36 +814,36 @@ class IpZonesJs(QObject):
|
|||||||
homeBubbleChanged = Signal()
|
homeBubbleChanged = Signal()
|
||||||
ipBubbleChanged = Signal()
|
ipBubbleChanged = Signal()
|
||||||
permissibleZoneChanged = Signal()
|
permissibleZoneChanged = Signal()
|
||||||
safeZoneChanged = Signal()
|
safeZonesChanged = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home_bubble: list[LeafletPoly],
|
home_bubble: LeafletPoly,
|
||||||
ip_bubble: list[LeafletPoly],
|
ip_bubble: LeafletPoly,
|
||||||
permissible_zone: list[LeafletPoly],
|
permissible_zone: LeafletPoly,
|
||||||
safe_zone: list[LeafletPoly],
|
safe_zones: list[LeafletPoly],
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._home_bubble = home_bubble
|
self._home_bubble = home_bubble
|
||||||
self._ip_bubble = ip_bubble
|
self._ip_bubble = ip_bubble
|
||||||
self._permissible_zone = permissible_zone
|
self._permissible_zone = permissible_zone
|
||||||
self._safe_zone = safe_zone
|
self._safe_zones = safe_zones
|
||||||
|
|
||||||
@Property(list, notify=homeBubbleChanged)
|
@Property(list, notify=homeBubbleChanged)
|
||||||
def homeBubble(self) -> list[LeafletPoly]:
|
def homeBubble(self) -> LeafletPoly:
|
||||||
return self._home_bubble
|
return self._home_bubble
|
||||||
|
|
||||||
@Property(list, notify=ipBubbleChanged)
|
@Property(list, notify=ipBubbleChanged)
|
||||||
def ipBubble(self) -> list[LeafletPoly]:
|
def ipBubble(self) -> LeafletPoly:
|
||||||
return self._ip_bubble
|
return self._ip_bubble
|
||||||
|
|
||||||
@Property(list, notify=permissibleZoneChanged)
|
@Property(list, notify=permissibleZoneChanged)
|
||||||
def permissibleZone(self) -> list[LeafletPoly]:
|
def permissibleZone(self) -> LeafletPoly:
|
||||||
return self._permissible_zone
|
return self._permissible_zone
|
||||||
|
|
||||||
@Property(list, notify=safeZoneChanged)
|
@Property(list, notify=safeZonesChanged)
|
||||||
def safeZone(self) -> list[LeafletPoly]:
|
def safeZones(self) -> list[LeafletPoly]:
|
||||||
return self._safe_zone
|
return self._safe_zones
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def empty(cls) -> IpZonesJs:
|
def empty(cls) -> IpZonesJs:
|
||||||
@ -845,10 +857,10 @@ class IpZonesJs(QObject):
|
|||||||
home = flight.departure
|
home = flight.departure
|
||||||
geometry = IpZoneGeometry(target.position, home.position, game.blue)
|
geometry = IpZoneGeometry(target.position, home.position, game.blue)
|
||||||
return IpZonesJs(
|
return IpZonesJs(
|
||||||
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
|
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
|
||||||
shapely_to_leaflet_polys(geometry.ip_bubble, game.theater),
|
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
|
||||||
shapely_to_leaflet_polys(geometry.permissible_zone, game.theater),
|
shapely_poly_to_leaflet_points(geometry.permissible_zone, game.theater),
|
||||||
shapely_to_leaflet_polys(geometry.safe_zone, game.theater),
|
shapely_to_leaflet_polys(geometry.safe_zones, game.theater),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -856,43 +868,43 @@ class JoinZonesJs(QObject):
|
|||||||
homeBubbleChanged = Signal()
|
homeBubbleChanged = Signal()
|
||||||
targetBubbleChanged = Signal()
|
targetBubbleChanged = Signal()
|
||||||
ipBubbleChanged = Signal()
|
ipBubbleChanged = Signal()
|
||||||
excludedZoneChanged = Signal()
|
excludedZonesChanged = Signal()
|
||||||
permissibleLineChanged = Signal()
|
permissibleLinesChanged = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
home_bubble: list[LeafletPoly],
|
home_bubble: LeafletPoly,
|
||||||
target_bubble: list[LeafletPoly],
|
target_bubble: LeafletPoly,
|
||||||
ip_bubble: list[LeafletPoly],
|
ip_bubble: LeafletPoly,
|
||||||
excluded_zone: list[LeafletPoly],
|
excluded_zones: list[LeafletPoly],
|
||||||
permissible_line: list[LeafletLatLon],
|
permissible_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_zone = excluded_zone
|
self._excluded_zones = excluded_zones
|
||||||
self._permissible_line = permissible_line
|
self._permissible_lines = permissible_lines
|
||||||
|
|
||||||
@Property(list, notify=homeBubbleChanged)
|
@Property(list, notify=homeBubbleChanged)
|
||||||
def homeBubble(self) -> list[LeafletPoly]:
|
def homeBubble(self) -> LeafletPoly:
|
||||||
return self._home_bubble
|
return self._home_bubble
|
||||||
|
|
||||||
@Property(list, notify=targetBubbleChanged)
|
@Property(list, notify=targetBubbleChanged)
|
||||||
def targetBubble(self) -> list[LeafletPoly]:
|
def targetBubble(self) -> LeafletPoly:
|
||||||
return self._target_bubble
|
return self._target_bubble
|
||||||
|
|
||||||
@Property(list, notify=ipBubbleChanged)
|
@Property(list, notify=ipBubbleChanged)
|
||||||
def ipBubble(self) -> list[LeafletPoly]:
|
def ipBubble(self) -> LeafletPoly:
|
||||||
return self._ip_bubble
|
return self._ip_bubble
|
||||||
|
|
||||||
@Property(list, notify=excludedZoneChanged)
|
@Property(list, notify=excludedZonesChanged)
|
||||||
def excludedZone(self) -> list[LeafletPoly]:
|
def excludedZones(self) -> list[LeafletPoly]:
|
||||||
return self._excluded_zone
|
return self._excluded_zones
|
||||||
|
|
||||||
@Property(list, notify=permissibleLineChanged)
|
@Property(list, notify=permissibleLinesChanged)
|
||||||
def permissibleLine(self) -> list[LeafletLatLon]:
|
def permissibleLines(self) -> list[list[LeafletLatLon]]:
|
||||||
return self._permissible_line
|
return self._permissible_lines
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def empty(cls) -> JoinZonesJs:
|
def empty(cls) -> JoinZonesJs:
|
||||||
@ -911,11 +923,87 @@ class JoinZonesJs(QObject):
|
|||||||
target.position, home.position, ip, game.blue, game.theater
|
target.position, home.position, ip, game.blue, game.theater
|
||||||
)
|
)
|
||||||
return JoinZonesJs(
|
return JoinZonesJs(
|
||||||
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
|
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
|
||||||
shapely_to_leaflet_polys(geometry.target_bubble, game.theater),
|
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
|
||||||
shapely_to_leaflet_polys(geometry.ip_bubble, game.theater),
|
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
|
||||||
shapely_to_leaflet_polys(geometry.excluded_zone, game.theater),
|
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
|
||||||
shapely_line_to_leaflet_points(geometry.permissible_line, 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()
|
unculledZonesChanged = Signal()
|
||||||
ipZonesChanged = Signal()
|
ipZonesChanged = Signal()
|
||||||
joinZonesChanged = Signal()
|
joinZonesChanged = Signal()
|
||||||
|
holdZonesChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, game_model: GameModel) -> None:
|
def __init__(self, game_model: GameModel) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -952,6 +1041,7 @@ class MapModel(QObject):
|
|||||||
self._unculled_zones = []
|
self._unculled_zones = []
|
||||||
self._ip_zones = IpZonesJs.empty()
|
self._ip_zones = IpZonesJs.empty()
|
||||||
self._join_zones = JoinZonesJs.empty()
|
self._join_zones = JoinZonesJs.empty()
|
||||||
|
self._hold_zones = HoldZonesJs.empty()
|
||||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||||
@ -1067,11 +1157,14 @@ class MapModel(QObject):
|
|||||||
if selected_flight is None:
|
if selected_flight is None:
|
||||||
self._ip_zones = IpZonesJs.empty()
|
self._ip_zones = IpZonesJs.empty()
|
||||||
self._join_zones = JoinZonesJs.empty()
|
self._join_zones = JoinZonesJs.empty()
|
||||||
|
self._hold_zones = HoldZonesJs.empty()
|
||||||
else:
|
else:
|
||||||
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
|
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
|
||||||
self._join_zones = JoinZonesJs.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.ipZonesChanged.emit()
|
||||||
self.joinZonesChanged.emit()
|
self.joinZonesChanged.emit()
|
||||||
|
self.holdZonesChanged.emit()
|
||||||
|
|
||||||
@Property(list, notify=flightsChanged)
|
@Property(list, notify=flightsChanged)
|
||||||
def flights(self) -> List[FlightJs]:
|
def flights(self) -> List[FlightJs]:
|
||||||
@ -1208,6 +1301,10 @@ class MapModel(QObject):
|
|||||||
def joinZones(self) -> JoinZonesJs:
|
def joinZones(self) -> JoinZonesJs:
|
||||||
return self._join_zones
|
return self._join_zones
|
||||||
|
|
||||||
|
@Property(HoldZonesJs, notify=holdZonesChanged)
|
||||||
|
def holdZones(self) -> HoldZonesJs:
|
||||||
|
return self._hold_zones
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def game(self) -> Game:
|
def game(self) -> Game:
|
||||||
if self.game_model.game is None:
|
if self.game_model.game is None:
|
||||||
|
|||||||
@ -198,8 +198,10 @@ const exclusionZones = L.layerGroup();
|
|||||||
const seaZones = L.layerGroup();
|
const seaZones = L.layerGroup();
|
||||||
const unculledZones = L.layerGroup();
|
const unculledZones = L.layerGroup();
|
||||||
|
|
||||||
|
const noWaypointZones = L.layerGroup();
|
||||||
const ipZones = L.layerGroup();
|
const ipZones = L.layerGroup();
|
||||||
const joinZones = L.layerGroup().addTo(map);
|
const joinZones = L.layerGroup();
|
||||||
|
const holdZones = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
const debugControlGroups = {
|
const debugControlGroups = {
|
||||||
"Blue Threat Zones": {
|
"Blue Threat Zones": {
|
||||||
@ -231,8 +233,10 @@ const debugControlGroups = {
|
|||||||
|
|
||||||
if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
||||||
debugControlGroups["Waypoint Zones"] = {
|
debugControlGroups["Waypoint Zones"] = {
|
||||||
|
None: noWaypointZones,
|
||||||
"IP Zones": ipZones,
|
"IP Zones": ipZones,
|
||||||
"Join Zones": joinZones,
|
"Join Zones": joinZones,
|
||||||
|
"Hold Zones": holdZones,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,6 +314,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
|||||||
game.unculledZonesChanged.connect(drawUnculledZones);
|
game.unculledZonesChanged.connect(drawUnculledZones);
|
||||||
game.ipZonesChanged.connect(drawIpZones);
|
game.ipZonesChanged.connect(drawIpZones);
|
||||||
game.joinZonesChanged.connect(drawJoinZones);
|
game.joinZonesChanged.connect(drawJoinZones);
|
||||||
|
game.holdZonesChanged.connect(drawHoldZones);
|
||||||
});
|
});
|
||||||
|
|
||||||
function recenterMap(center) {
|
function recenterMap(center) {
|
||||||
@ -1014,11 +1019,13 @@ function drawIpZones() {
|
|||||||
interactive: false,
|
interactive: false,
|
||||||
}).addTo(ipZones);
|
}).addTo(ipZones);
|
||||||
|
|
||||||
L.polygon(game.ipZones.safeZone, {
|
for (const zone of game.ipZones.safeZones) {
|
||||||
color: Colors.Green,
|
L.polygon(zone, {
|
||||||
fillOpacity: 0.1,
|
color: Colors.Green,
|
||||||
interactive: false,
|
fillOpacity: 0.1,
|
||||||
}).addTo(ipZones);
|
interactive: false,
|
||||||
|
}).addTo(ipZones);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawJoinZones() {
|
function drawJoinZones() {
|
||||||
@ -1042,17 +1049,59 @@ function drawJoinZones() {
|
|||||||
interactive: false,
|
interactive: false,
|
||||||
}).addTo(joinZones);
|
}).addTo(joinZones);
|
||||||
|
|
||||||
L.polygon(game.joinZones.excludedZone, {
|
for (const zone of game.joinZones.excludedZones) {
|
||||||
color: "#ffa500",
|
L.polygon(zone, {
|
||||||
fillOpacity: 0.2,
|
color: "#ffa500",
|
||||||
stroke: false,
|
fillOpacity: 0.2,
|
||||||
interactive: false,
|
stroke: false,
|
||||||
}).addTo(joinZones);
|
interactive: false,
|
||||||
|
}).addTo(joinZones);
|
||||||
|
}
|
||||||
|
|
||||||
L.polyline(game.joinZones.permissibleLine, {
|
for (const line of game.joinZones.permissibleLines) {
|
||||||
color: Colors.Green,
|
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,
|
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() {
|
function drawInitialMap() {
|
||||||
@ -1068,6 +1117,7 @@ function drawInitialMap() {
|
|||||||
drawUnculledZones();
|
drawUnculledZones();
|
||||||
drawIpZones();
|
drawIpZones();
|
||||||
drawJoinZones();
|
drawJoinZones();
|
||||||
|
drawHoldZones();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAllLayers() {
|
function clearAllLayers() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user