Improve IP selection near threat zone centers.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2754.
This commit is contained in:
Dan Albert 2023-07-11 21:55:40 -07:00
parent adabb617f3
commit 19976989ca
5 changed files with 88 additions and 8 deletions

View File

@ -4,6 +4,7 @@ Saves from 8.x are not compatible with 9.0.0.
## Features/Improvements ## Features/Improvements
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate. * **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
## Fixes ## Fixes

View File

@ -384,6 +384,8 @@ export type IpZones = {
ipBubble: LatLng[][]; ipBubble: LatLng[][];
permissibleZone: LatLng[][]; permissibleZone: LatLng[][];
safeZones: LatLng[][][]; safeZones: LatLng[][][];
preferredThreatenedZones: LatLng[][][];
tolerableThreatenedLines: LatLng[][];
}; };
export type JoinZones = { export type JoinZones = {
homeBubble: LatLng[][]; homeBubble: LatLng[][];

View File

@ -1,5 +1,5 @@
import { useGetDebugIpZonesQuery } from "../../api/liberationApi"; import { useGetDebugIpZonesQuery } from "../../api/liberationApi";
import { LayerGroup, Polygon } from "react-leaflet"; import { LayerGroup, Polygon, Polyline } from "react-leaflet";
interface IpZonesProps { interface IpZonesProps {
flightId: string; flightId: string;
@ -56,6 +56,29 @@ function IpZones(props: IpZonesProps) {
/> />
); );
})} })}
{data.preferredThreatenedZones.map((zone, idx) => {
return (
<Polygon
key={idx}
positions={zone}
color="#fe7d0a"
fillOpacity={0.1}
interactive={false}
/>
);
})}
{data.tolerableThreatenedLines.map((line, idx) => {
return (
<Polyline
key={idx}
positions={line}
color="#80BA80"
interactive={false}
/>
);
})}
</> </>
); );
} }

View File

@ -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 MultiPolygon, Point as ShapelyPoint from shapely.geometry import MultiPolygon, Point as ShapelyPoint, MultiLineString
from game.utils import meters, nautical_miles from game.utils import meters, nautical_miles
@ -90,6 +90,32 @@ class IpZoneGeometry:
safe_zones = MultiPolygon([safe_zones]) safe_zones = MultiPolygon([safe_zones])
self.safe_zones = safe_zones self.safe_zones = safe_zones
# See explanation where this is used in _unsafe_ip.
# https://github.com/dcs-liberation/dcs_liberation/issues/2754
preferred_threatened_zone_wiggle_room = nautical_miles(5)
threat_buffer_distance = self.permissible_zone.distance(
self.threat_zone.boundary
)
preferred_threatened_zone_mask = self.threat_zone.buffer(
-threat_buffer_distance - preferred_threatened_zone_wiggle_room.meters
)
preferred_threatened_zones = self.threat_zone.difference(
preferred_threatened_zone_mask
)
if not isinstance(preferred_threatened_zones, MultiPolygon):
preferred_threatened_zones = MultiPolygon([preferred_threatened_zones])
self.preferred_threatened_zones = preferred_threatened_zones
tolerable_threatened_lines = self.preferred_threatened_zones.intersection(
self.permissible_zone.boundary
)
if tolerable_threatened_lines.is_empty:
tolerable_threatened_lines = MultiLineString([])
elif not isinstance(tolerable_threatened_lines, MultiLineString):
tolerable_threatened_lines = MultiLineString([tolerable_threatened_lines])
self.tolerable_threatened_lines = tolerable_threatened_lines
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:
@ -100,8 +126,28 @@ class IpZoneGeometry:
self.permissible_zone, self.threat_zone.boundary self.permissible_zone, self.threat_zone.boundary
)[0] )[0]
# No safe point in the IP zone, but the home zone is safe. Pick the max- # No safe point in the IP zone, but the home zone is safe. Pick an IP within
# distance IP that's closest to the untreatened home zone. # both the permissible zone and preferred threatened zone that's as close to the
# unthreatened home zone as possible. This should get us a max-range IP that
# is roughly as safe as possible without unjustifiably long routes.
#
# If we do the obvious thing and pick the IP that minimizes threatened travel
# time (the IP closest to the threat boundary) and the objective is near the
# center of the threat zone (common when there is an airbase covered only by air
# defenses with shorter range than the BARCAP zone, and the target is a TGO near
# the CP), the IP could be placed such that the flight would fly all the way
# around the threat zone just to avoid a few more threatened miles of travel. To
# avoid that, we generate a set of preferred threatened areas that offer a
# trade-off between travel time and safety.
#
# https://github.com/dcs-liberation/dcs_liberation/issues/2754
if not self.tolerable_threatened_lines.is_empty:
return shapely.ops.nearest_points(
self.tolerable_threatened_lines, self.home
)[0]
# But if no part of the permissible zone is tolerably threatened, fall back to
# the old safety maximizing approach.
return shapely.ops.nearest_points( return shapely.ops.nearest_points(
self.permissible_zone, unthreatened_home_zone self.permissible_zone, unthreatened_home_zone
)[0] )[0]

View File

@ -64,14 +64,16 @@ class IpZonesJs(BaseModel):
ipBubble: LeafletPoly = Field(alias="ipBubble") ipBubble: LeafletPoly = Field(alias="ipBubble")
permissibleZone: LeafletPoly = Field(alias="permissibleZone") permissibleZone: LeafletPoly = Field(alias="permissibleZone")
safeZones: list[LeafletPoly] = Field(alias="safeZones") safeZones: list[LeafletPoly] = Field(alias="safeZones")
preferred_threatened_zones: list[LeafletPoly] = Field(
alias="preferredThreatenedZones"
)
tolerable_threatened_lines: list[LeafletLine] = Field(
alias="tolerableThreatenedLines"
)
class Config: class Config:
title = "IpZones" title = "IpZones"
@classmethod
def empty(cls) -> IpZonesJs:
return IpZonesJs(homeBubble=[], ipBubble=[], permissibleZone=[], safeZones=[])
@classmethod @classmethod
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs: def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
target = flight.package.target target = flight.package.target
@ -84,6 +86,12 @@ class IpZonesJs(BaseModel):
geometry.permissible_zone, game.theater geometry.permissible_zone, game.theater
), ),
safeZones=ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater), safeZones=ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater),
preferredThreatenedZones=ShapelyUtil.polys_to_leaflet(
geometry.preferred_threatened_zones, game.theater
),
tolerableThreatenedLines=ShapelyUtil.lines_to_leaflet(
geometry.tolerable_threatened_lines, game.theater
),
) )