Merge pull request #193 from DanAlbert/tot

Plan waypoint TOTs.
This commit is contained in:
C. Perreau 2020-10-09 13:25:08 +02:00 committed by GitHub
commit 63bc3bd46e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 782 additions and 226 deletions

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import logging
import random
from dataclasses import dataclass
@ -28,7 +30,7 @@ from dcs.planes import (
SpitfireLFMkIXCW,
Su_33,
)
from dcs.point import PointAction
from dcs.point import MovingPoint, PointAction
from dcs.task import (
AntishipStrike,
AttackGroup,
@ -40,8 +42,6 @@ from dcs.task import (
EngageTargets,
Escort,
GroundAttack,
MainTask,
NoTask,
OptROE,
OptRTBOnBingoFuel,
OptRTBOnOutOfAmmo,
@ -64,10 +64,10 @@ from dcs.unittype import FlyingType, UnitType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import nm_to_meter
from game.utils import meter_to_nm, nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.flight import (
Flight,
@ -76,9 +76,10 @@ from gen.flights.flight import (
FlightWaypointType,
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from theater import MissionTarget, TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType
from .naming import namegen
from .conflictgen import Conflict
from .naming import namegen
WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500
@ -532,6 +533,148 @@ AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
@dataclass(frozen=True)
class PackageWaypointTiming:
#: The package being scheduled.
package: Package
#: The package join time.
join: int
#: The ingress waypoint TOT.
ingress: int
#: The egress waypoint TOT.
egress: int
#: The package split time.
split: int
@property
def target(self) -> int:
"""The package time over target."""
assert self.package.time_over_target is not None
return self.package.time_over_target
@property
def race_track_start(self) -> Optional[int]:
cap_types = (FlightType.BARCAP, FlightType.CAP)
if self.package.primary_task in cap_types:
# CAP flights don't have hold points, and we don't calculate takeoff
# times yet or adjust the TOT based on when the flight can arrive,
# so if we set a TOT that gives the flight a lot of extra time it
# will just fly to the start point slowly, possibly slowly enough to
# stall and crash. Just don't set a TOT for these points and let the
# CAP get on station ASAP.
return None
else:
return self.ingress
@property
def race_track_end(self) -> int:
cap_types = (FlightType.BARCAP, FlightType.CAP)
if self.package.primary_task in cap_types:
return self.target + CAP_DURATION * 60
else:
return self.egress
def push_time(self, flight: Flight, hold_point: Point) -> int:
assert self.package.waypoints is not None
return self.join - self.travel_time(
hold_point,
self.package.waypoints.join,
self.flight_ground_speed(flight)
)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
target_types = (
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
ingress_types = (
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
)
if waypoint.waypoint_type == FlightWaypointType.JOIN:
return self.join
elif waypoint.waypoint_type in ingress_types:
return self.ingress
elif waypoint.waypoint_type in target_types:
return self.target
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
return self.egress
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
return self.split
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
return self.race_track_start
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
flight: Flight) -> Optional[int]:
if waypoint.waypoint_type == FlightWaypointType.LOITER:
return self.push_time(flight, Point(waypoint.x, waypoint.y))
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
return self.race_track_end
return None
@classmethod
def for_package(cls, package: Package) -> PackageWaypointTiming:
assert package.time_over_target is not None
assert package.waypoints is not None
group_ground_speed = cls.package_ground_speed(package)
ingress = package.time_over_target - cls.travel_time(
package.waypoints.ingress,
package.target.position,
group_ground_speed
)
join = ingress - cls.travel_time(
package.waypoints.join,
package.waypoints.ingress,
group_ground_speed
)
egress = package.time_over_target + cls.travel_time(
package.target.position,
package.waypoints.egress,
group_ground_speed
)
split = egress + cls.travel_time(
package.waypoints.egress,
package.waypoints.split,
group_ground_speed
)
return cls(package, join, ingress, egress, split)
@classmethod
def package_ground_speed(cls, package: Package) -> int:
speeds = []
for flight in package.flights:
speeds.append(cls.flight_ground_speed(flight))
return min(speeds) # knots
@staticmethod
def flight_ground_speed(_flight: Flight) -> int:
# TODO: Gather data so this is useful.
return 400 # knots
@staticmethod
def travel_time(a: Point, b: Point, speed: float) -> int:
error_factor = 1.1
distance = meter_to_nm(a.distance_to_point(b))
hours = distance / speed
seconds = hours * 3600
return int(seconds * error_factor)
class AircraftConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game, radio_registry: RadioRegistry):
@ -634,7 +777,8 @@ class AircraftConflictGenerator:
arrival=departure_runway,
# TODO: Support for divert airfields.
divert=None,
waypoints=[first_point] + flight.points,
# Waypoints are added later, after they've had their TOTs set.
waypoints=[],
intra_flight_channel=channel
))
@ -817,6 +961,7 @@ class AircraftConflictGenerator:
self.clear_parking_slots()
for package in ato.packages:
timing = PackageWaypointTiming.for_package(package)
for flight in package.flights:
culled = self.game.position_culled(flight.from_cp.position)
if flight.client_count == 0 and culled:
@ -825,8 +970,7 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country,
flight)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_flight_group(group, flight, timing, dynamic_runways)
self.setup_group_activation_trigger(flight, group)
def setup_group_activation_trigger(self, flight, group):
@ -936,134 +1080,334 @@ class AircraftConflictGenerator:
flight.group = group
return group
def setup_flight_group(self, group, flight, flight_type,
dynamic_runways: Dict[str, RunwayData]):
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]:
group.task = CAP.name
self._setup_group(group, CAP, flight, dynamic_runways)
# group.points[0].tasks.clear()
group.points[0].tasks.clear()
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air]))
# group.tasks.append(EngageTargets(max_distance=nm_to_meter(120), targets=[Targets.All.Air]))
if flight.unit_type not in GUNFIGHTERS:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.AAM))
else:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Cannon))
elif flight_type in [FlightType.CAS, FlightType.BAI]:
group.task = CAS.name
self._setup_group(group, CAS, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles]))
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree))
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Unguided))
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
group.task = SEAD.name
self._setup_group(group, SEAD, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(NoTask())
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM))
elif flight_type in [FlightType.STRIKE]:
group.task = PinpointStrike.name
self._setup_group(group, GroundAttack, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type in [FlightType.ANTISHIP]:
group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type == FlightType.ESCORT:
group.task = Escort.name
self._setup_group(group, Escort, flight, dynamic_runways)
# TODO: Cleanup duplication...
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
@staticmethod
def configure_behavior(
group: FlyingGroup,
react_on_threat: Optional[OptReactOnThreat.Values] = None,
roe: Optional[OptROE.Values] = None,
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
restrict_jettison: Optional[bool] = None) -> None:
group.points[0].tasks.clear()
if react_on_threat is not None:
group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
if roe is not None:
group.points[0].tasks.append(OptROE(roe))
if restrict_jettison is not None:
group.points[0].tasks.append(OptRestrictJettison(restrict_jettison))
if rtb_winchester is not None:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
group.points[0].tasks.append(OptRestrictAfterburner(True))
@staticmethod
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
if hasattr(flight.unit_type, 'eplrs'):
if flight.unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
for i, point in enumerate(flight.points):
if not point.only_for_player or (point.only_for_player and flight.client_count > 0):
pt = group.add_waypoint(Point(point.x, point.y), point.alt)
if point.waypoint_type == FlightWaypointType.PATROL_TRACK:
action = ControlledTask(OrbitAction(altitude=pt.alt, pattern=OrbitAction.OrbitPattern.RaceTrack))
action.stop_after_duration(CAP_DURATION * 60)
#for tgt in point.targets:
# if hasattr(tgt, "position"):
# engagetgt = EngageTargetsInZone(tgt.position, radius=CAP_DEFAULT_ENGAGE_DISTANCE, targets=[Targets.All.Air])
# pt.tasks.append(engagetgt)
elif point.waypoint_type == FlightWaypointType.LANDING_POINT:
pt.type = "Land"
pt.action = PointAction.Landing
elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE:
def configure_cap(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = CAP.name
self._setup_group(group, CAP, flight, dynamic_runways)
if group.units[0].unit_type == B_17G:
if len(point.targets) > 0:
bcenter = Point(0,0)
for j, t in enumerate(point.targets):
bcenter.x += t.position.x
bcenter.y += t.position.y
bcenter.x = bcenter.x / len(point.targets)
bcenter.y = bcenter.y / len(point.targets)
bombing = Bombing(bcenter)
bombing.params["expend"] = "All"
bombing.params["attackQtyLimit"] = False
bombing.params["directionEnabled"] = False
bombing.params["altitudeEnabled"] = False
bombing.params["weaponType"] = 2032
bombing.params["groupAttack"] = True
pt.tasks.append(bombing)
else:
for j, t in enumerate(point.targets):
print(t.position)
pt.tasks.append(Bombing(t.position))
if group.units[0].unit_type == JF_17 and j < 4:
group.add_nav_target_point(t.position, "PP" + str(j + 1))
if group.units[0].unit_type == F_14B and j == 0:
group.add_nav_target_point(t.position, "ST")
if group.units[0].unit_type == AJS37 and j < 9:
group.add_nav_target_point(t.position, "M" + str(j + 1))
elif point.waypoint_type == FlightWaypointType.INGRESS_SEAD:
if flight.unit_type not in GUNFIGHTERS:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
tgroup = self.m.find_group(point.targetGroup.group_identifier)
if tgroup is not None:
task = AttackGroup(tgroup.id)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["weaponType"] = 268402702 # Guided Weapons
task.params["groupAttack"] = True
pt.tasks.append(task)
self.configure_behavior(group, rtb_winchester=ammo_type)
for j, t in enumerate(point.targets):
if group.units[0].unit_type == JF_17 and j < 4:
group.add_nav_target_point(t.position, "PP" + str(j + 1))
if group.units[0].unit_type == F_14B and j == 0:
group.add_nav_target_point(t.position, "ST")
if group.units[0].unit_type == AJS37 and j < 9:
group.add_nav_target_point(t.position, "M" + str(j + 1))
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
if pt is not None:
pt.alt_type = point.alt_type
pt.name = String(point.name)
def configure_cas(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = CAS.name
self._setup_group(group, CAS, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFireWeaponFree,
rtb_winchester=OptRTBOnOutOfAmmo.Values.Unguided,
restrict_jettison=True)
group.points[0].tasks.append(
EngageTargets(max_distance=nm_to_meter(10),
targets=[Targets.All.GroundUnits.GroundVehicles])
)
def configure_sead(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = SEAD.name
self._setup_group(group, SEAD, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
restrict_jettison=True)
def configure_strike(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = PinpointStrike.name
self._setup_group(group, GroundAttack, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_anti_ship(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_escort(self, group: FlyingGroup, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = Escort.name
self._setup_group(group, Escort, flight, dynamic_runways)
self.configure_behavior(group, roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_unknown_task(self, group: FlyingGroup,
flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
timing: PackageWaypointTiming,
dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
FlightType.INTERCEPTION]:
self.configure_cap(group, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, flight, dynamic_runways)
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
self.configure_sead(group, flight, dynamic_runways)
elif flight_type in [FlightType.STRIKE]:
self.configure_strike(group, flight, dynamic_runways)
elif flight_type in [FlightType.ANTISHIP]:
self.configure_anti_ship(group, flight, dynamic_runways)
elif flight_type == FlightType.ESCORT:
self.configure_escort(group, flight, dynamic_runways)
else:
self.configure_unknown_task(group, flight)
self.configure_eplrs(group, flight)
for waypoint in flight.points:
waypoint.tot = None
for point in flight.points:
if point.only_for_player and not flight.client_count:
continue
PydcsWaypointBuilder.for_waypoint(
point, group, flight, timing, self.m
).build()
# Set here rather than when the FlightData is created so they waypoints
# have their TOTs set.
self.flights[-1].waypoints = flight.points
self._setup_custom_payload(flight, group)
class PydcsWaypointBuilder:
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
flight: Flight, timing: PackageWaypointTiming,
mission: Mission) -> None:
self.waypoint = waypoint
self.group = group
self.flight = flight
self.timing = timing
self.mission = mission
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt)
waypoint.alt_type = self.waypoint.alt_type
waypoint.name = String(self.waypoint.name)
return waypoint
def set_waypoint_tot(self, waypoint: MovingPoint, tot: int) -> None:
self.waypoint.tot = tot
waypoint.ETA = tot
waypoint.ETA_locked = True
waypoint.speed_locked = False
@classmethod
def for_waypoint(cls, waypoint: FlightWaypoint,
group: FlyingGroup,
flight: Flight,
timing: PackageWaypointTiming,
mission: Mission) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.EGRESS: EgressPointBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder,
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.SPLIT: SplitPointBuilder,
FlightWaypointType.TARGET_GROUP_LOC: TargetPointBuilder,
FlightWaypointType.TARGET_POINT: TargetPointBuilder,
FlightWaypointType.TARGET_SHIP: TargetPointBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, flight, timing, mission)
class DefaultWaypointBuilder(PydcsWaypointBuilder):
pass
class HoldPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
loiter = ControlledTask(OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle
))
loiter.stop_after_time(
self.timing.push_time(self.flight, waypoint.position))
waypoint.add_task(loiter)
return waypoint
class EgressPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.egress)
return waypoint
class IngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.ingress)
return waypoint
class SeadIngressBuilder(IngressBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target_group = self.waypoint.targetGroup
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_identifier)
if tgroup is not None:
task = AttackGroup(tgroup.id)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["weaponType"] = 268402702 # Guided Weapons
task.params["groupAttack"] = True
waypoint.tasks.append(task)
for i, t in enumerate(self.waypoint.targets):
if self.group.units[0].unit_type == JF_17 and i < 4:
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
if self.group.units[0].unit_type == F_14B and i == 0:
self.group.add_nav_target_point(t.position, "ST")
if self.group.units[0].unit_type == AJS37 and i < 9:
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
return waypoint
class StrikeIngressBuilder(IngressBuilder):
def build(self) -> MovingPoint:
if self.group.units[0].unit_type == B_17G:
return self.build_bombing()
else:
return self.build_strike()
def build_bombing(self) -> MovingPoint:
waypoint = super().build()
targets = self.waypoint.targets
if not targets:
return waypoint
center = Point(0, 0)
for target in targets:
center.x += target.position.x
center.y += target.position.y
center.x /= len(targets)
center.y /= len(targets)
bombing = Bombing(center)
bombing.params["expend"] = "All"
bombing.params["attackQtyLimit"] = False
bombing.params["directionEnabled"] = False
bombing.params["altitudeEnabled"] = False
bombing.params["weaponType"] = 2032
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
return waypoint
def build_strike(self) -> MovingPoint:
waypoint = super().build()
for i, t in enumerate(self.waypoint.targets):
waypoint.tasks.append(Bombing(t.position))
if self.group.units[0].unit_type == JF_17 and i < 4:
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
if self.group.units[0].unit_type == F_14B and i == 0:
self.group.add_nav_target_point(t.position, "ST")
if self.group.units[0].unit_type == AJS37 and i < 9:
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
return waypoint
class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.join)
return waypoint
class LandingPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
waypoint.type = "Land"
waypoint.action = PointAction.Landing
return waypoint
class RaceTrackBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
racetrack = ControlledTask(OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack
))
start = self.timing.race_track_start
if start is not None:
self.set_waypoint_tot(waypoint, start)
racetrack.stop_after_time(self.timing.race_track_end)
waypoint.add_task(racetrack)
return waypoint
class SplitPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.split)
return waypoint
class TargetPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.target)
return waypoint

View File

@ -8,14 +8,15 @@ example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight.
"""
import logging
from collections import defaultdict
from dataclasses import dataclass, field
import logging
from typing import Dict, Iterator, List, Optional
from typing import Dict, List, Optional
from dcs.mapping import Point
from .flights.flight import Flight, FlightType
from theater.missiontarget import MissionTarget
from .flights.flight import Flight, FlightType
@dataclass(frozen=True)
@ -29,6 +30,14 @@ class Task:
location: str
@dataclass(frozen=True)
class PackageWaypoints:
join: Point
ingress: Point
egress: Point
split: Point
@dataclass
class Package:
"""A mission package."""
@ -42,10 +51,10 @@ class Package:
delay: int = field(default=0)
join_point: Optional[Point] = field(default=None, init=False, hash=False)
split_point: Optional[Point] = field(default=None, init=False, hash=False)
ingress_point: Optional[Point] = field(default=None, init=False, hash=False)
egress_point: Optional[Point] = field(default=None, init=False, hash=False)
#: Desired TOT measured in seconds from mission start.
time_over_target: Optional[int] = field(default=None)
waypoints: Optional[PackageWaypoints] = field(default=None)
def add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package."""
@ -55,8 +64,7 @@ class Package:
"""Removes a flight from the package."""
self.flights.remove(flight)
if not self.flights:
self.ingress_point = None
self.egress_point = None
self.waypoints = None
@property
def primary_task(self) -> Optional[FlightType]:

View File

@ -12,7 +12,7 @@ from game import db
from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information
from game.utils import nm_to_meter
from gen import Conflict
from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen.flights.ai_flight_planner_db import (
CAP_CAPABLE,
@ -496,10 +496,17 @@ class CoalitionMissionPlanner:
latest=90,
margin=5
)
for package in non_dca_packages:
package.delay = next(start_time)
for flight in package.flights:
flight.scheduled_in = package.delay
for package in self.ato.packages:
if package.primary_task in dca_types:
# All CAP missions should be on station in 15-25 minutes.
package.time_over_target = (20 + random.randint(-5, 5)) * 60
else:
# But other packages should be spread out a bit.
package.delay = next(start_time)
# TODO: Compute TOT based on package.
package.time_over_target = (package.delay + 40) * 60
for flight in package.flights:
flight.scheduled_in = package.delay
def message(self, title, text) -> None:
"""Emits a planning message to the player.

View File

@ -49,6 +49,7 @@ class FlightWaypointType(Enum):
CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16
SPLIT = 17
LOITER = 18
class PredefinedWaypointCategory(Enum):
@ -66,6 +67,15 @@ class FlightWaypoint:
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: int = 0) -> None:
"""Creates a flight waypoint.
Args:
waypoint_type: The waypoint type.
x: X cooidinate of the waypoint.
y: Y coordinate of the waypoint.
alt: Altitude of the waypoint. By default this is AGL, but it can be
changed to MSL by setting alt_type to "RADIO".
"""
self.waypoint_type = waypoint_type
self.x = x
self.y = y
@ -81,6 +91,12 @@ class FlightWaypoint:
self.only_for_player = False
self.data = None
# This is set very late by the air conflict generator (part of mission
# generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI.
self.tot: Optional[int] = None
@classmethod
def from_pydcs(cls, point: MovingPoint,

View File

@ -16,10 +16,10 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
from game.utils import nm_to_meter
from gen.ato import Package
from gen.ato import Package, PackageWaypoints
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .flight import Flight, FlightType, FlightWaypoint
from .waypointbuilder import WaypointBuilder
from ..conflictgen import Conflict
@ -55,7 +55,8 @@ class FlightPlanBuilder:
"""Creates a default flight plan for the given mission."""
if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package")
self.generate_missing_package_waypoints()
if self.package.waypoints is None:
self.regenerate_package_waypoints()
# TODO: Flesh out mission types.
try:
@ -105,15 +106,18 @@ class FlightPlanBuilder:
except InvalidObjectiveLocation as ex:
logging.error(f"Could not create flight plan: {ex}")
def generate_missing_package_waypoints(self) -> None:
if self.package.ingress_point is None:
self.package.ingress_point = self._ingress_point()
if self.package.egress_point is None:
self.package.egress_point = self._egress_point()
if self.package.join_point is None:
self.package.join_point = self._join_point()
if self.package.split_point is None:
self.package.split_point = self._split_point()
def regenerate_package_waypoints(self) -> None:
ingress_point = self._ingress_point()
egress_point = self._egress_point()
join_point = self._join_point(ingress_point)
split_point = self._split_point(egress_point)
self.package.waypoints = PackageWaypoints(
join_point,
ingress_point,
egress_point,
split_point,
)
def generate_strike(self, flight: Flight) -> None:
"""Generates a strike flight plan.
@ -121,6 +125,7 @@ class FlightPlanBuilder:
Args:
flight: The flight to generate the flight plan for.
"""
assert self.package.waypoints is not None
location = self.package.target
# TODO: Support airfield strikes.
@ -129,8 +134,9 @@ class FlightPlanBuilder:
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.join(self.package.join_point)
builder.ingress_strike(self.package.ingress_point, location)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.ingress_strike(self.package.waypoints.ingress, location)
if len(location.groups) > 0 and location.dcs_identifier == "AA":
# TODO: Replace with DEAD?
@ -157,8 +163,8 @@ class FlightPlanBuilder:
location
)
builder.egress(self.package.egress_point, location)
builder.split(self.package.split_point)
builder.egress(self.package.waypoints.egress, location)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
@ -215,6 +221,7 @@ class FlightPlanBuilder:
Args:
flight: The flight to generate the flight plan for.
"""
assert self.package.waypoints is not None
location = self.package.target
if not isinstance(location, FrontLine):
@ -246,7 +253,10 @@ class FlightPlanBuilder:
# Create points
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.race_track(orbit0p, orbit1p, patrol_alt)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
@ -258,6 +268,7 @@ class FlightPlanBuilder:
flight: The flight to generate the flight plan for.
custom_targets: Specific radar equipped units selected by the user.
"""
assert self.package.waypoints is not None
location = self.package.target
if not isinstance(location, TheaterGroundObject):
@ -268,21 +279,15 @@ class FlightPlanBuilder:
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.join(self.package.join_point)
builder.ingress_sead(self.package.ingress_point, location)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.ingress_sead(self.package.waypoints.ingress, location)
# TODO: Unify these.
# There doesn't seem to be any reason to treat the UI fragged missions
# different from the automatic missions.
if custom_targets:
for target in custom_targets:
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
builder.dead_point(target, location.name, location)
else:
@ -293,17 +298,22 @@ class FlightPlanBuilder:
else:
builder.sead_area(location)
builder.egress(self.package.egress_point, location)
builder.split(self.package.split_point)
builder.egress(self.package.waypoints.egress, location)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def _hold_point(self, flight: Flight) -> Point:
heading = flight.from_cp.position.heading_between_point(
self.package.target.position
)
return flight.from_cp.position.point_from_heading(
heading, nm_to_meter(15)
)
def generate_escort(self, flight: Flight) -> None:
# TODO: Decide common waypoints for the package ahead of time.
# Packages should determine some common points like push, ingress,
# egress, and split points ahead of time so they can be shared by all
# flights.
assert self.package.waypoints is not None
patrol_alt = random.randint(
self.doctrine.min_patrol_altitude,
@ -312,13 +322,11 @@ class FlightPlanBuilder:
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.join(self.package.join_point)
builder.race_track(
self.package.ingress_point,
self.package.egress_point,
patrol_alt
)
builder.split(self.package.split_point)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.race_track(self.package.waypoints.ingress,
self.package.waypoints.egress, patrol_alt)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp)
flight.points = builder.build()
@ -329,6 +337,7 @@ class FlightPlanBuilder:
Args:
flight: The flight to generate the flight plan for.
"""
assert self.package.waypoints is not None
location = self.package.target
if not isinstance(location, FrontLine):
@ -346,11 +355,12 @@ class FlightPlanBuilder:
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp, is_helo)
builder.join(self.package.join_point)
builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join)
builder.ingress_cas(ingress, location)
builder.cas(center, cap_alt)
builder.egress(egress, location)
builder.split(self.package.split_point)
builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp, is_helo)
flight.points = builder.build()
@ -386,16 +396,12 @@ class FlightPlanBuilder:
builder.land(arrival)
return builder.build()[0]
def _join_point(self) -> Point:
ingress_point = self.package.ingress_point
assert ingress_point is not None
def _join_point(self, ingress_point: Point) -> Point:
heading = self._heading_to_package_airfield(ingress_point)
return ingress_point.point_from_heading(heading,
-self.doctrine.join_distance)
def _split_point(self) -> Point:
egress_point = self.package.egress_point
assert egress_point is not None
def _split_point(self, egress_point: Point) -> Point:
heading = self._heading_to_package_airfield(egress_point)
return egress_point.point_from_heading(heading,
-self.doctrine.split_distance)

View File

@ -25,6 +25,7 @@ class WaypointBuilder:
Args:
departure: Departure airfield or carrier.
is_helo: True if the flight is a helicopter.
"""
# TODO: Pick runway based on wind direction.
heading = departure.heading
@ -48,6 +49,7 @@ class WaypointBuilder:
Args:
arrival: Arrival airfield or carrier.
is_helo: True if the flight is a helicopter.
"""
# TODO: Pick runway based on wind direction.
# ControlPoint.heading is the departure heading.
@ -86,6 +88,18 @@ class WaypointBuilder:
waypoint.pretty_name = "Land"
self.waypoints.append(waypoint)
def hold(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.LOITER,
position.x,
position.y,
self.doctrine.rendezvous_altitude
)
waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time"
waypoint.name = "HOLD"
self.waypoints.append(waypoint)
def join(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.JOIN,
@ -170,7 +184,7 @@ class WaypointBuilder:
location)
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
description: str, location: MissionTarget) -> None:
description: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
@ -293,6 +307,7 @@ class WaypointBuilder:
Args:
arrival: Arrival airfield or carrier.
is_helo: True if the flight is a helicopter.
"""
self.descent(arrival, is_helo)
self.land(arrival)

View File

@ -24,6 +24,7 @@ aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
from collections import defaultdict
from dataclasses import dataclass
import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@ -106,7 +107,8 @@ class NumberedWaypoint:
class FlightPlanBuilder:
def __init__(self) -> None:
def __init__(self, start_time: datetime.datetime) -> None:
self.start_time = start_time
self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = []
@ -133,16 +135,24 @@ class FlightPlanBuilder:
self.rows.append([
f"{first_waypoint_num}-{last_waypoint_num}",
"Target points",
"0"
"0",
self._format_time(self.target_points[0].waypoint.tot),
])
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([
str(waypoint.number),
waypoint.waypoint.pretty_name,
str(int(units.meters_to_feet(waypoint.waypoint.alt)))
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
self._format_time(waypoint.waypoint.tot),
])
def _format_time(self, time: Optional[int]) -> str:
if time is None:
return ""
local_time = self.start_time + datetime.timedelta(seconds=time)
return local_time.strftime(f"%H:%M:%S LOCAL")
def build(self) -> List[List[str]]:
return self.rows
@ -151,12 +161,13 @@ class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information."""
def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo],
jtacs: List[JtacInfo]) -> None:
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.start_time = start_time
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
@ -172,11 +183,11 @@ class BriefingPage(KneeboardPage):
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
writer.heading("Flight Plan")
flight_plan_builder = FlightPlanBuilder()
flight_plan_builder = FlightPlanBuilder(self.start_time)
for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint)
writer.table(flight_plan_builder.build(),
headers=["STPT", "Action", "Alt"])
headers=["STPT", "Action", "Alt", "TOT"])
writer.heading("Comm Ladder")
comms = []
@ -297,6 +308,11 @@ class KneeboardGenerator(MissionInfoGenerator):
"""Returns a list of kneeboard pages for the given flight."""
return [
BriefingPage(
flight, self.comms, self.awacs, self.tankers, self.jtacs
flight,
self.comms,
self.awacs,
self.tankers,
self.jtacs,
self.mission.start_time
),
]

View File

@ -1,4 +1,6 @@
"""Qt data models for game objects."""
import datetime
from enum import auto, IntEnum
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
from PySide2.QtCore import (
@ -156,6 +158,9 @@ class PackageModel(QAbstractListModel):
"""Returns the flight located at the given index."""
return self.package.flights[index.row()]
def update_tot(self, tot: int) -> None:
self.package.time_over_target = tot
@property
def mission_target(self) -> MissionTarget:
"""Returns the mission target of the package."""
@ -178,6 +183,8 @@ class PackageModel(QAbstractListModel):
class AtoModel(QAbstractListModel):
"""The model for an AirTaskingOrder."""
PackageRole = Qt.UserRole
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
super().__init__()
self.game = game
@ -190,9 +197,11 @@ class AtoModel(QAbstractListModel):
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
package = self.ato.packages[index.row()]
if role == Qt.DisplayRole:
package = self.ato.packages[index.row()]
return f"{package.package_description} {package.target.name}"
elif role == AtoModel.PackageRole:
return package
return None
def add_package(self, package: Package) -> None:

View File

@ -1,8 +1,16 @@
"""Widgets for displaying air tasking orders."""
import datetime
import logging
from typing import Optional
from contextlib import contextmanager
from typing import ContextManager, Optional
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize, Qt
from PySide2.QtCore import (
QItemSelectionModel,
QModelIndex,
QSize,
Qt,
)
from PySide2.QtGui import QFont, QFontMetrics, QPainter
from PySide2.QtWidgets import (
QAbstractItemView,
QGroupBox,
@ -10,13 +18,13 @@ from PySide2.QtWidgets import (
QListView,
QPushButton,
QSplitter,
QVBoxLayout,
QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
)
from gen.ato import Package
from gen.flights.flight import Flight
from ..models import AtoModel, GameModel, NullListModel, PackageModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from ..models import AtoModel, GameModel, NullListModel, PackageModel
class QFlightList(QListView):
@ -138,6 +146,65 @@ class QFlightPanel(QGroupBox):
GameUpdateSignal.get_instance().redraw_flight_paths()
@contextmanager
def painter_context(painter: QPainter) -> ContextManager[None]:
try:
painter.save()
yield
finally:
painter.restore()
class PackageDelegate(QStyledItemDelegate):
FONT_SIZE = 12
HMARGIN = 4
VMARGIN = 4
def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font)
font.setPointSize(self.FONT_SIZE)
return font
@staticmethod
def package(index: QModelIndex) -> Package:
return index.data(AtoModel.PackageRole)
def left_text(self, index: QModelIndex) -> str:
package = self.package(index)
return f"{package.package_description} {package.target.name}"
def right_text(self, index: QModelIndex) -> str:
package = self.package(index)
if package.time_over_target is None:
return ""
tot = datetime.timedelta(seconds=package.time_over_target)
return f"TOT T+{tot}"
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
index: QModelIndex) -> None:
# Draw the list item with all the default selection styling, but with an
# invalid index so text formatting is left to us.
super().paint(painter, option, QModelIndex())
rect = option.rect.adjusted(self.HMARGIN, self.VMARGIN, -self.HMARGIN,
-self.VMARGIN)
with painter_context(painter):
painter.setFont(self.get_font(option))
painter.drawText(rect, Qt.AlignLeft, self.left_text(index))
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
def sizeHint(self, option: QStyleOptionViewItem,
index: QModelIndex) -> QSize:
metrics = QFontMetrics(self.get_font(option))
left = metrics.size(0, self.left_text(index))
right = metrics.size(0, self.right_text(index))
return QSize(max(left.width(), right.width()) + 2 * self.HMARGIN,
left.height() + right.height() + 2 * self.VMARGIN)
class QPackageList(QListView):
"""List view for displaying the packages of an ATO."""
@ -145,6 +212,7 @@ class QPackageList(QListView):
super().__init__()
self.ato_model = model
self.setModel(model)
self.setItemDelegate(PackageDelegate())
self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)

View File

@ -2,12 +2,13 @@
import logging
from typing import Optional
from PySide2.QtCore import QItemSelection, Signal
from PySide2.QtCore import QItemSelection, QTime, Signal
from PySide2.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QTimeEdit,
QVBoxLayout,
)
@ -56,14 +57,39 @@ class QPackageDialog(QDialog):
self.summary_row = QHBoxLayout()
self.layout.addLayout(self.summary_row)
self.package_type_column = QHBoxLayout()
self.summary_row.addLayout(self.package_type_column)
self.package_type_label = QLabel("Package Type:")
self.package_type_text = QLabel(self.package_model.description)
# noinspection PyUnresolvedReferences
self.package_changed.connect(lambda: self.package_type_text.setText(
self.package_model.description
))
self.summary_row.addWidget(self.package_type_label)
self.summary_row.addWidget(self.package_type_text)
self.package_type_column.addWidget(self.package_type_label)
self.package_type_column.addWidget(self.package_type_text)
self.summary_row.addStretch(1)
self.tot_column = QHBoxLayout()
self.summary_row.addLayout(self.tot_column)
self.tot_label = QLabel("Time Over Target:")
self.tot_column.addWidget(self.tot_label)
if self.package_model.package.time_over_target is None:
time = None
else:
delay = self.package_model.package.time_over_target
hours = delay // 3600
minutes = delay // 60 % 60
seconds = delay % 60
time = QTime(hours, minutes, seconds)
self.tot_spinner = QTimeEdit(time)
self.tot_spinner.setMinimumTime(QTime(0, 0))
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
self.tot_column.addWidget(self.tot_spinner)
self.package_view = QFlightList(self.package_model)
self.package_view.selectionModel().selectionChanged.connect(
@ -90,8 +116,10 @@ class QPackageDialog(QDialog):
self.finished.connect(self.on_close)
@staticmethod
def on_close(_result) -> None:
def on_close(self, _result) -> None:
time = self.tot_spinner.time()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
self.package_model.update_tot(seconds)
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_selection_changed(self, selected: QItemSelection,

View File

@ -1,26 +1,33 @@
import datetime
import itertools
from PySide2.QtCore import QItemSelectionModel, QPoint
from PySide2.QtGui import QStandardItemModel, QStandardItem
from PySide2.QtWidgets import QTableView, QHeaderView
from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QHeaderView, QTableView
from game.utils import meter_to_feet
from gen.aircraft import PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
QWaypointItem
class QFlightWaypointList(QTableView):
def __init__(self, flight: Flight):
super(QFlightWaypointList, self).__init__()
def __init__(self, package: Package, flight: Flight):
super().__init__()
self.package = package
self.flight = flight
self.model = QStandardItemModel(self)
self.setModel(self.model)
self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.model.setHorizontalHeaderLabels(["Name", "Alt"])
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
header = self.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.flight = flight
if len(self.flight.points) > 0:
self.selectedPoint = self.flight.points[0]
self.update_list()
@ -33,18 +40,49 @@ class QFlightWaypointList(QTableView):
def update_list(self):
self.model.clear()
self.model.setHorizontalHeaderLabels(["Name", "Alt"])
takeoff = FlightWaypoint(self.flight.from_cp.position.x, self.flight.from_cp.position.y, 0)
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
timing = PackageWaypointTiming.for_package(self.package)
# The first waypoint is set up by pydcs at mission generation time, so
# we need to add that waypoint manually.
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
self.flight.from_cp.position.y, 0)
takeoff.description = "Take Off"
takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
self.model.appendRow(QWaypointItem(takeoff, 0))
item = QStandardItem("0 feet AGL")
item.setEditable(False)
self.model.setItem(0, 1, item)
for i, point in enumerate(self.flight.points):
self.model.insertRow(self.model.rowCount())
self.model.setItem(self.model.rowCount()-1, 0, QWaypointItem(point, i + 1))
item = QStandardItem(str(meter_to_feet(point.alt)) + " ft " + str(["AGL" if point.alt_type == "RADIO" else "MSL"][0]))
item.setEditable(False)
self.model.setItem(self.model.rowCount()-1, 1, item)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select)
takeoff.alt_type = "RADIO"
waypoints = itertools.chain([takeoff], self.flight.points)
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, waypoint, timing)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
QItemSelectionModel.Select)
def add_waypoint_row(self, row: int, waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> None:
self.model.insertRow(self.model.rowCount())
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
altitude = meter_to_feet(waypoint.alt)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
altitude_item.setEditable(False)
self.model.setItem(row, 1, altitude_item)
tot = self.tot_text(waypoint, timing)
tot_item = QStandardItem(tot)
tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item)
def tot_text(self, waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> str:
prefix = ""
time = timing.tot_for_waypoint(waypoint)
if time is None:
prefix = "Depart "
time = timing.depart_time_for_waypoint(waypoint, self.flight)
if time is None:
return ""
return f"{prefix}T+{datetime.timedelta(seconds=time)}"

View File

@ -44,7 +44,8 @@ class QFlightWaypointTab(QFrame):
def init_ui(self):
layout = QGridLayout()
self.flight_waypoint_list = QFlightWaypointList(self.flight)
self.flight_waypoint_list = QFlightWaypointList(self.package,
self.flight)
layout.addWidget(self.flight_waypoint_list, 0, 0)
rlayout = QVBoxLayout()