mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Migrate IP placement to WaypointSolver.
This commit is contained in:
parent
643dafd2c8
commit
8b04dd878d
@ -50,14 +50,6 @@ const injectedRtkApi = api.injectEndpoints({
|
|||||||
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
|
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
getDebugIpZones: build.query<
|
|
||||||
GetDebugIpZonesApiResponse,
|
|
||||||
GetDebugIpZonesApiArg
|
|
||||||
>({
|
|
||||||
query: (queryArg) => ({
|
|
||||||
url: `/debug/waypoint-geometries/ip/${queryArg.flightId}`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
getDebugJoinZones: build.query<
|
getDebugJoinZones: build.query<
|
||||||
GetDebugJoinZonesApiResponse,
|
GetDebugJoinZonesApiResponse,
|
||||||
GetDebugJoinZonesApiArg
|
GetDebugJoinZonesApiArg
|
||||||
@ -245,11 +237,6 @@ export type GetDebugHoldZonesApiResponse =
|
|||||||
export type GetDebugHoldZonesApiArg = {
|
export type GetDebugHoldZonesApiArg = {
|
||||||
flightId: string;
|
flightId: string;
|
||||||
};
|
};
|
||||||
export type GetDebugIpZonesApiResponse =
|
|
||||||
/** status 200 Successful Response */ IpZones;
|
|
||||||
export type GetDebugIpZonesApiArg = {
|
|
||||||
flightId: string;
|
|
||||||
};
|
|
||||||
export type GetDebugJoinZonesApiResponse =
|
export type GetDebugJoinZonesApiResponse =
|
||||||
/** status 200 Successful Response */ JoinZones;
|
/** status 200 Successful Response */ JoinZones;
|
||||||
export type GetDebugJoinZonesApiArg = {
|
export type GetDebugJoinZonesApiArg = {
|
||||||
@ -379,14 +366,6 @@ export type HoldZones = {
|
|||||||
permissibleZones: LatLng[][][];
|
permissibleZones: LatLng[][][];
|
||||||
preferredLines: LatLng[][];
|
preferredLines: LatLng[][];
|
||||||
};
|
};
|
||||||
export type IpZones = {
|
|
||||||
homeBubble: LatLng[][];
|
|
||||||
ipBubble: LatLng[][];
|
|
||||||
permissibleZone: LatLng[][];
|
|
||||||
safeZones: LatLng[][][];
|
|
||||||
preferredThreatenedZones: LatLng[][][];
|
|
||||||
tolerableThreatenedLines: LatLng[][];
|
|
||||||
};
|
|
||||||
export type JoinZones = {
|
export type JoinZones = {
|
||||||
homeBubble: LatLng[][];
|
homeBubble: LatLng[][];
|
||||||
targetBubble: LatLng[][];
|
targetBubble: LatLng[][];
|
||||||
@ -500,7 +479,6 @@ export const {
|
|||||||
useSetControlPointDestinationMutation,
|
useSetControlPointDestinationMutation,
|
||||||
useClearControlPointDestinationMutation,
|
useClearControlPointDestinationMutation,
|
||||||
useGetDebugHoldZonesQuery,
|
useGetDebugHoldZonesQuery,
|
||||||
useGetDebugIpZonesQuery,
|
|
||||||
useGetDebugJoinZonesQuery,
|
useGetDebugJoinZonesQuery,
|
||||||
useListFlightsQuery,
|
useListFlightsQuery,
|
||||||
useGetFlightByIdQuery,
|
useGetFlightByIdQuery,
|
||||||
|
|||||||
@ -30,11 +30,6 @@ export const liberationApi = _liberationApi.enhanceEndpoints({
|
|||||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
getDebugIpZones: {
|
|
||||||
providesTags: (result, error, arg) => [
|
|
||||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
getDebugJoinZones: {
|
getDebugJoinZones: {
|
||||||
providesTags: (result, error, arg) => [
|
providesTags: (result, error, arg) => [
|
||||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
import { useGetDebugIpZonesQuery } from "../../api/liberationApi";
|
|
||||||
import { LayerGroup, Polygon, Polyline } from "react-leaflet";
|
|
||||||
|
|
||||||
interface IpZonesProps {
|
|
||||||
flightId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function IpZones(props: IpZonesProps) {
|
|
||||||
const { data, error, isLoading } = useGetDebugIpZonesQuery({
|
|
||||||
flightId: props.flightId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error while loading waypoint IP zone info", error);
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
console.log("Waypoint IP zone returned empty response");
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Polygon
|
|
||||||
positions={data.homeBubble}
|
|
||||||
color="#ffff00"
|
|
||||||
fillOpacity={0.1}
|
|
||||||
interactive={false}
|
|
||||||
/>
|
|
||||||
<Polygon
|
|
||||||
positions={data.ipBubble}
|
|
||||||
color="#bb89ff"
|
|
||||||
fillOpacity={0.1}
|
|
||||||
interactive={false}
|
|
||||||
/>
|
|
||||||
<Polygon
|
|
||||||
positions={data.permissibleZone}
|
|
||||||
color="#ffffff"
|
|
||||||
fillOpacity={0.1}
|
|
||||||
interactive={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{data.safeZones.map((zone, idx) => {
|
|
||||||
return (
|
|
||||||
<Polygon
|
|
||||||
key={idx}
|
|
||||||
positions={zone}
|
|
||||||
color="#80BA80"
|
|
||||||
fillOpacity={0.1}
|
|
||||||
interactive={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IpZonesLayerProps {
|
|
||||||
flightId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IpZonesLayer(props: IpZonesLayerProps) {
|
|
||||||
return (
|
|
||||||
<LayerGroup>
|
|
||||||
{props.flightId ? <IpZones flightId={props.flightId} /> : <></>}
|
|
||||||
</LayerGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { selectSelectedFlightId } from "../../api/flightsSlice";
|
import { selectSelectedFlightId } from "../../api/flightsSlice";
|
||||||
import { useAppSelector } from "../../app/hooks";
|
import { useAppSelector } from "../../app/hooks";
|
||||||
import { HoldZonesLayer } from "./HoldZones";
|
import { HoldZonesLayer } from "./HoldZones";
|
||||||
import { IpZonesLayer } from "./IpZones";
|
|
||||||
import { JoinZonesLayer } from "./JoinZones";
|
import { JoinZonesLayer } from "./JoinZones";
|
||||||
import { LayersControl } from "react-leaflet";
|
import { LayersControl } from "react-leaflet";
|
||||||
|
|
||||||
@ -16,9 +15,6 @@ export function WaypointDebugZonesControls() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LayersControl.Overlay name="IP zones">
|
|
||||||
<IpZonesLayer flightId={selectedFlightId} />
|
|
||||||
</LayersControl.Overlay>
|
|
||||||
<LayersControl.Overlay name="Join zones">
|
<LayersControl.Overlay name="Join zones">
|
||||||
<JoinZonesLayer flightId={selectedFlightId} />
|
<JoinZonesLayer flightId={selectedFlightId} />
|
||||||
</LayersControl.Overlay>
|
</LayersControl.Overlay>
|
||||||
|
|||||||
@ -44,7 +44,11 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
def _generate_package_waypoints_if_needed(self) -> None:
|
def _generate_package_waypoints_if_needed(self) -> None:
|
||||||
if self.package.waypoints is None:
|
# Package waypoints are only valid for offensive missions. Skip this if the
|
||||||
|
# target is friendly.
|
||||||
|
if self.package.waypoints is None and not self.package.target.is_friendly(
|
||||||
|
self.is_player
|
||||||
|
):
|
||||||
self.package.waypoints = PackageWaypoints.create(
|
self.package.waypoints = PackageWaypoints.create(
|
||||||
self.package, self.coalition
|
self.package, self.coalition
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,13 +2,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
|
from game.flightplan import JoinZoneGeometry
|
||||||
|
from game.flightplan.ipsolver import IpSolver
|
||||||
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||||
|
from game.utils import dcs_to_shapely_point
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -29,11 +31,15 @@ class PackageWaypoints:
|
|||||||
origin = package.departure_closest_to_target()
|
origin = package.departure_closest_to_target()
|
||||||
|
|
||||||
# Start by picking the best IP for the attack.
|
# Start by picking the best IP for the attack.
|
||||||
ingress_point = IpZoneGeometry(
|
ingress_point_shapely = IpSolver(
|
||||||
package.target.position,
|
dcs_to_shapely_point(origin.position),
|
||||||
origin.position,
|
dcs_to_shapely_point(package.target.position),
|
||||||
coalition,
|
coalition.doctrine,
|
||||||
).find_best_ip()
|
coalition.opponent.threat_zone.all,
|
||||||
|
).solve()
|
||||||
|
ingress_point = origin.position.new_in_same_map(
|
||||||
|
ingress_point_shapely.x, ingress_point_shapely.y
|
||||||
|
)
|
||||||
|
|
||||||
hdg = package.target.position.heading_between_point(ingress_point)
|
hdg = package.target.position.heading_between_point(ingress_point)
|
||||||
# Generate a waypoint randomly between 7 & 9 NM
|
# Generate a waypoint randomly between 7 & 9 NM
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
from .holdzonegeometry import HoldZoneGeometry
|
from .holdzonegeometry import HoldZoneGeometry
|
||||||
from .ipzonegeometry import IpZoneGeometry
|
|
||||||
from .joinzonegeometry import JoinZoneGeometry
|
from .joinzonegeometry import JoinZoneGeometry
|
||||||
|
|||||||
173
game/flightplan/ipsolver.py
Normal file
173
game/flightplan/ipsolver.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from shapely.geometry import MultiPolygon, Point
|
||||||
|
from shapely.geometry.base import BaseGeometry
|
||||||
|
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.flightplan.waypointsolver import WaypointSolver
|
||||||
|
from game.flightplan.waypointstrategy import WaypointStrategy
|
||||||
|
from game.utils import meters, nautical_miles
|
||||||
|
|
||||||
|
MIN_DISTANCE_FROM_DEPARTURE = nautical_miles(5)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreatTolerantIpStrategy(WaypointStrategy):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
departure: Point,
|
||||||
|
target: Point,
|
||||||
|
doctrine: Doctrine,
|
||||||
|
threat_zones: MultiPolygon,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(threat_zones)
|
||||||
|
self.prerequisite(target).min_distance_from(
|
||||||
|
departure, doctrine.min_ingress_distance
|
||||||
|
)
|
||||||
|
self.require().at_least(MIN_DISTANCE_FROM_DEPARTURE).away_from(departure)
|
||||||
|
self.require().at_most(meters(departure.distance(target))).away_from(departure)
|
||||||
|
self.require().at_least(doctrine.min_ingress_distance).away_from(target)
|
||||||
|
max_ip_range = min(
|
||||||
|
doctrine.max_ingress_distance, meters(departure.distance(target))
|
||||||
|
)
|
||||||
|
self.require().at_most(max_ip_range).away_from(target)
|
||||||
|
self.threat_tolerance(target, max_ip_range, nautical_miles(5))
|
||||||
|
self.nearest(departure)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsafeIpStrategy(WaypointStrategy):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
departure: Point,
|
||||||
|
target: Point,
|
||||||
|
doctrine: Doctrine,
|
||||||
|
threat_zones: MultiPolygon,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(threat_zones)
|
||||||
|
self.prerequisite(target).min_distance_from(
|
||||||
|
departure, doctrine.min_ingress_distance
|
||||||
|
)
|
||||||
|
self.require().at_least(MIN_DISTANCE_FROM_DEPARTURE).away_from(
|
||||||
|
departure, "departure"
|
||||||
|
)
|
||||||
|
self.require().at_most(meters(departure.distance(target))).away_from(
|
||||||
|
departure, "departure"
|
||||||
|
)
|
||||||
|
self.require().at_least(doctrine.min_ingress_distance).away_from(
|
||||||
|
target, "target"
|
||||||
|
)
|
||||||
|
max_ip_range = min(
|
||||||
|
doctrine.max_ingress_distance, meters(departure.distance(target))
|
||||||
|
)
|
||||||
|
self.require().at_most(max_ip_range).away_from(target, "target")
|
||||||
|
self.nearest(departure)
|
||||||
|
|
||||||
|
|
||||||
|
class SafeIpStrategy(WaypointStrategy):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
departure: Point,
|
||||||
|
target: Point,
|
||||||
|
doctrine: Doctrine,
|
||||||
|
threat_zones: MultiPolygon,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(threat_zones)
|
||||||
|
self.prerequisite(departure).is_safe()
|
||||||
|
self.prerequisite(target).min_distance_from(
|
||||||
|
departure, doctrine.min_ingress_distance
|
||||||
|
)
|
||||||
|
self.require().at_least(MIN_DISTANCE_FROM_DEPARTURE).away_from(
|
||||||
|
departure, "departure"
|
||||||
|
)
|
||||||
|
self.require().at_most(meters(departure.distance(target))).away_from(
|
||||||
|
departure, "departure"
|
||||||
|
)
|
||||||
|
self.require().at_least(doctrine.min_ingress_distance).away_from(
|
||||||
|
target, "target"
|
||||||
|
)
|
||||||
|
self.require().at_most(
|
||||||
|
min(doctrine.max_ingress_distance, meters(departure.distance(target)))
|
||||||
|
).away_from(target, "target")
|
||||||
|
self.require().safe()
|
||||||
|
self.nearest(departure)
|
||||||
|
|
||||||
|
|
||||||
|
class SafeBackTrackingIpStrategy(WaypointStrategy):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
departure: Point,
|
||||||
|
target: Point,
|
||||||
|
doctrine: Doctrine,
|
||||||
|
threat_zones: MultiPolygon,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(threat_zones)
|
||||||
|
self.require().at_least(MIN_DISTANCE_FROM_DEPARTURE).away_from(
|
||||||
|
departure, "departure"
|
||||||
|
)
|
||||||
|
self.require().at_least(doctrine.min_ingress_distance).away_from(
|
||||||
|
target, "target"
|
||||||
|
)
|
||||||
|
self.require().at_most(doctrine.max_ingress_distance).away_from(
|
||||||
|
target, "target"
|
||||||
|
)
|
||||||
|
self.require().safe()
|
||||||
|
self.nearest(departure)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsafeBackTrackingIpStrategy(WaypointStrategy):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
departure: Point,
|
||||||
|
target: Point,
|
||||||
|
doctrine: Doctrine,
|
||||||
|
threat_zones: MultiPolygon,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(threat_zones)
|
||||||
|
self.require().at_least(MIN_DISTANCE_FROM_DEPARTURE).away_from(
|
||||||
|
departure, "departure"
|
||||||
|
)
|
||||||
|
self.require().at_least(doctrine.min_ingress_distance).away_from(
|
||||||
|
target, "target"
|
||||||
|
)
|
||||||
|
self.require().at_most(doctrine.max_ingress_distance).away_from(
|
||||||
|
target, "target"
|
||||||
|
)
|
||||||
|
self.nearest(departure)
|
||||||
|
|
||||||
|
|
||||||
|
class IpSolver(WaypointSolver):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
departure: Point,
|
||||||
|
target: Point,
|
||||||
|
doctrine: Doctrine,
|
||||||
|
threat_zones: MultiPolygon,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.departure = departure
|
||||||
|
self.target = target
|
||||||
|
self.doctrine = doctrine
|
||||||
|
self.threat_zones = threat_zones
|
||||||
|
|
||||||
|
self.add_strategy(SafeIpStrategy(departure, target, doctrine, threat_zones))
|
||||||
|
self.add_strategy(
|
||||||
|
ThreatTolerantIpStrategy(departure, target, doctrine, threat_zones)
|
||||||
|
)
|
||||||
|
self.add_strategy(UnsafeIpStrategy(departure, target, doctrine, threat_zones))
|
||||||
|
self.add_strategy(
|
||||||
|
SafeBackTrackingIpStrategy(departure, target, doctrine, threat_zones)
|
||||||
|
)
|
||||||
|
# TODO: The cases that require this are not covered by any tests.
|
||||||
|
self.add_strategy(
|
||||||
|
UnsafeBackTrackingIpStrategy(departure, target, doctrine, threat_zones)
|
||||||
|
)
|
||||||
|
|
||||||
|
def describe_metadata(self) -> dict[str, Any]:
|
||||||
|
return {"doctrine": self.doctrine.name}
|
||||||
|
|
||||||
|
def describe_inputs(self) -> Iterator[tuple[str, BaseGeometry]]:
|
||||||
|
yield "departure", self.departure
|
||||||
|
yield "target", self.target
|
||||||
|
yield "threat_zones", self.threat_zones
|
||||||
@ -1,165 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import shapely.ops
|
|
||||||
from dcs import Point
|
|
||||||
from shapely.geometry import MultiPolygon, Point as ShapelyPoint, MultiLineString
|
|
||||||
|
|
||||||
from game.utils import meters, nautical_miles
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.coalition import Coalition
|
|
||||||
|
|
||||||
|
|
||||||
class IpZoneGeometry:
|
|
||||||
"""Defines the zones used for finding optimal IP placement.
|
|
||||||
|
|
||||||
The zones themselves are stored in the class rather than just the resulting IP so
|
|
||||||
that the zones can be drawn in the map for debugging purposes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
target: Point,
|
|
||||||
home: Point,
|
|
||||||
coalition: Coalition,
|
|
||||||
) -> None:
|
|
||||||
self._target = target
|
|
||||||
self.threat_zone = coalition.opponent.threat_zone.all
|
|
||||||
self.home = ShapelyPoint(home.x, home.y)
|
|
||||||
|
|
||||||
max_ip_distance = coalition.doctrine.max_ingress_distance
|
|
||||||
min_ip_distance = coalition.doctrine.min_ingress_distance
|
|
||||||
|
|
||||||
# The minimum distance between the home location and the IP.
|
|
||||||
min_distance_from_home = nautical_miles(5)
|
|
||||||
|
|
||||||
# The distance that is expected to be needed between the beginning of the attack
|
|
||||||
# and weapon release. This buffers the threat zone to give a 5nm window between
|
|
||||||
# the edge of the "safe" zone and the actual threat so that "safe" IPs are less
|
|
||||||
# likely to end up with the attacker entering a threatened area.
|
|
||||||
attack_distance_buffer = nautical_miles(5)
|
|
||||||
|
|
||||||
home_threatened = coalition.opponent.threat_zone.threatened(home)
|
|
||||||
|
|
||||||
shapely_target = ShapelyPoint(target.x, target.y)
|
|
||||||
home_to_target_distance = meters(home.distance_to_point(target))
|
|
||||||
|
|
||||||
self.home_bubble = self.home.buffer(home_to_target_distance.meters).difference(
|
|
||||||
self.home.buffer(min_distance_from_home.meters)
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the home zone is not threatened and home is within LAR, constrain the max
|
|
||||||
# range to the home-to-target distance to prevent excessive backtracking.
|
|
||||||
#
|
|
||||||
# If the home zone *is* threatened, we need to back out of the zone to
|
|
||||||
# rendezvous anyway.
|
|
||||||
if not home_threatened and (
|
|
||||||
min_ip_distance < home_to_target_distance < max_ip_distance
|
|
||||||
):
|
|
||||||
max_ip_distance = home_to_target_distance
|
|
||||||
max_ip_bubble = shapely_target.buffer(max_ip_distance.meters)
|
|
||||||
min_ip_bubble = shapely_target.buffer(min_ip_distance.meters)
|
|
||||||
self.ip_bubble = max_ip_bubble.difference(min_ip_bubble)
|
|
||||||
|
|
||||||
# The intersection of the home bubble and IP bubble will be all the points that
|
|
||||||
# are within the valid IP range that are not farther from home than the target
|
|
||||||
# is. However, if the origin airfield is threatened but there are safe
|
|
||||||
# placements for the IP, we should not constrain to the home zone. In this case
|
|
||||||
# we'll either end up with a safe zone outside the home zone and pick the
|
|
||||||
# closest point in to to home (minimizing backtracking), or we'll have no safe
|
|
||||||
# IP anywhere within range of the target, and we'll later pick the IP nearest
|
|
||||||
# the edge of the threat zone.
|
|
||||||
if home_threatened:
|
|
||||||
self.permissible_zone = self.ip_bubble
|
|
||||||
else:
|
|
||||||
self.permissible_zone = self.ip_bubble.intersection(self.home_bubble)
|
|
||||||
|
|
||||||
if self.permissible_zone.is_empty:
|
|
||||||
# If home is closer to the target than the min range, there will not be an
|
|
||||||
# IP solution that's close enough to home, in which case we need to ignore
|
|
||||||
# the home bubble.
|
|
||||||
self.permissible_zone = self.ip_bubble
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
|
|
||||||
if unthreatened_home_zone.is_empty:
|
|
||||||
# Nowhere in our home zone is safe. The package will need to exit the
|
|
||||||
# threatened area to hold and rendezvous. Pick the IP closest to the
|
|
||||||
# edge of the threat zone.
|
|
||||||
return shapely.ops.nearest_points(
|
|
||||||
self.permissible_zone, self.threat_zone.boundary
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# No safe point in the IP zone, but the home zone is safe. Pick an IP within
|
|
||||||
# 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(
|
|
||||||
self.permissible_zone, unthreatened_home_zone
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
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_zones, self.home)[0]
|
|
||||||
|
|
||||||
def find_best_ip(self) -> Point:
|
|
||||||
if self.safe_zones.is_empty:
|
|
||||||
ip = self._unsafe_ip()
|
|
||||||
else:
|
|
||||||
ip = self._safe_ip()
|
|
||||||
return self._target.new_in_same_map(ip.x, ip.y)
|
|
||||||
@ -4,7 +4,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.ato import Flight
|
from game.ato import Flight
|
||||||
from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
|
from game.flightplan import HoldZoneGeometry, JoinZoneGeometry
|
||||||
from ..leaflet import LeafletLine, LeafletPoly, ShapelyUtil
|
from ..leaflet import LeafletLine, LeafletPoly, ShapelyUtil
|
||||||
|
|
||||||
|
|
||||||
@ -59,42 +59,6 @@ class HoldZonesJs(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IpZonesJs(BaseModel):
|
|
||||||
home_bubble: LeafletPoly = Field(alias="homeBubble")
|
|
||||||
ipBubble: LeafletPoly = Field(alias="ipBubble")
|
|
||||||
permissibleZone: LeafletPoly = Field(alias="permissibleZone")
|
|
||||||
safeZones: list[LeafletPoly] = Field(alias="safeZones")
|
|
||||||
preferred_threatened_zones: list[LeafletPoly] = Field(
|
|
||||||
alias="preferredThreatenedZones"
|
|
||||||
)
|
|
||||||
tolerable_threatened_lines: list[LeafletLine] = Field(
|
|
||||||
alias="tolerableThreatenedLines"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = "IpZones"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
|
|
||||||
target = flight.package.target
|
|
||||||
home = flight.departure
|
|
||||||
geometry = IpZoneGeometry(target.position, home.position, game.blue)
|
|
||||||
return IpZonesJs(
|
|
||||||
homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater),
|
|
||||||
ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater),
|
|
||||||
permissibleZone=ShapelyUtil.poly_to_leaflet(
|
|
||||||
geometry.permissible_zone, 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
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class JoinZonesJs(BaseModel):
|
class JoinZonesJs(BaseModel):
|
||||||
home_bubble: LeafletPoly = Field(alias="homeBubble")
|
home_bubble: LeafletPoly = Field(alias="homeBubble")
|
||||||
target_bubble: LeafletPoly = Field(alias="targetBubble")
|
target_bubble: LeafletPoly = Field(alias="targetBubble")
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.server import GameContext
|
from game.server import GameContext
|
||||||
from .models import HoldZonesJs, IpZonesJs, JoinZonesJs
|
from .models import HoldZonesJs, JoinZonesJs
|
||||||
|
|
||||||
router: APIRouter = APIRouter(prefix="/debug/waypoint-geometries")
|
router: APIRouter = APIRouter(prefix="/debug/waypoint-geometries")
|
||||||
|
|
||||||
@ -18,13 +18,6 @@ def hold_zones(
|
|||||||
return HoldZonesJs.for_flight(game.db.flights.get(flight_id), game)
|
return HoldZonesJs.for_flight(game.db.flights.get(flight_id), game)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/ip/{flight_id}", operation_id="get_debug_ip_zones", response_model=IpZonesJs
|
|
||||||
)
|
|
||||||
def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.require)) -> IpZonesJs:
|
|
||||||
return IpZonesJs.for_flight(game.db.flights.get(flight_id), game)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/join/{flight_id}", operation_id="get_debug_join_zones", response_model=JoinZonesJs
|
"/join/{flight_id}", operation_id="get_debug_join_zones", response_model=JoinZonesJs
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user