Improve unit system support in kneeboards.

* Factor out unit systems.
* Add support for more unit systems (nautical and imperial).
* Fuel units support.
* Data for many more aircraft.
This commit is contained in:
bbirchnz
2022-01-13 12:21:06 +11:00
committed by GitHub
parent cd97565cce
commit 4b99ae957e
34 changed files with 262 additions and 44 deletions

View File

@@ -33,7 +33,11 @@ from game.radio.channels import (
from game.utils import (
Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL,
ImperialUnits,
MetricUnits,
NauticalUnits,
Speed,
UnitSystem,
feet,
knots,
kph,
@@ -154,8 +158,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
# main weapon. It'll RTB when it doesn't have gun ammo left.
gunfighter: bool
# If true, kneeboards will be generated in metric units
metric_kneeboard: bool
# UnitSystem to use for the kneeboard, defaults to Nautical (kt/nm/ft)
kneeboard_units: UnitSystem
# If true, kneeboards will display zulu times
utc_kneeboard: bool
@@ -376,6 +380,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
except KeyError:
introduction = "No data."
units_data = data.get("kneeboard_units", "nautical").lower()
units: UnitSystem = NauticalUnits()
if units_data == "imperial":
units = ImperialUnits()
if units_data == "metric":
units = MetricUnits()
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
@@ -401,6 +412,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,
metric_kneeboard=data.get("metric_kneeboard", False),
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
)

View File

@@ -44,7 +44,7 @@ from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency
from game.theater import ConflictTheater, LatLon, TheaterGroundObject
from game.theater.bullseye import Bullseye
from game.utils import Distance, meters
from game.utils import Distance, UnitSystem, meters, mps, pounds
from game.weather import Weather
from gen.runways import RunwayData
from .aircraft.flightdata import FlightData
@@ -202,12 +202,12 @@ class FlightPlanBuilder:
WAYPOINT_DESC_MAX_LEN = 25
def __init__(self, start_time: datetime.datetime, is_metric: bool) -> None:
def __init__(self, start_time: datetime.datetime, units: UnitSystem) -> None:
self.start_time = start_time
self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = []
self.last_waypoint: Optional[FlightWaypoint] = None
self.is_metric = is_metric
self.units = units
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
@@ -269,32 +269,24 @@ class FlightPlanBuilder:
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
def _format_alt(self, alt: Distance) -> str:
if self.is_metric:
return f"{alt.meters:.0f} m"
else:
return f"{alt.feet:.0f} ft"
return f"{self.units.distance_short(alt):.0f}"
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None:
return "-"
if self.is_metric:
distance = meters(
self.last_waypoint.position.distance_to_point(waypoint.position)
)
return f"{(distance.meters / 1000):.1f} km"
else:
distance = meters(
self.last_waypoint.position.distance_to_point(waypoint.position)
)
return f"{distance.nautical_miles:.1f} nm"
distance = meters(
self.last_waypoint.position.distance_to_point(waypoint.position)
)
return f"{self.units.distance_long(distance):.1f}"
def _waypoint_bearing(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None:
return "-"
bearing = self.last_waypoint.position.heading_between_point(waypoint.position)
return f"{(bearing):.0f} T"
return f"{(bearing):.0f}"
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None:
@@ -310,20 +302,19 @@ class FlightPlanBuilder:
else:
return "-"
distance = meters(
speed = mps(
self.last_waypoint.position.distance_to_point(waypoint.position)
/ (waypoint.tot - last_time).total_seconds()
)
duration = (waypoint.tot - last_time).total_seconds() / 3600
if self.is_metric:
return f"{int((distance.meters / 1000) / duration)} km/h"
else:
return f"{int(distance.nautical_miles / duration)} kt"
@staticmethod
def _format_min_fuel(min_fuel: Optional[float]) -> str:
return f"{self.units.speed(speed):.0f}"
def _format_min_fuel(self, min_fuel: Optional[float]) -> str:
if min_fuel is None:
return ""
return str(math.ceil(min_fuel / 100) * 100)
mass = pounds(min_fuel)
return f"{math.ceil(self.units.mass(mass) / 100) * 100:.0f}"
def build(self) -> List[List[str]]:
return self.rows
@@ -373,13 +364,29 @@ class BriefingPage(KneeboardPage):
)
writer.heading("Flight Plan")
flight_plan_builder = FlightPlanBuilder(
self.start_time, self.flight.aircraft_type.metric_kneeboard
)
units = self.flight.aircraft_type.kneeboard_units
flight_plan_builder = FlightPlanBuilder(self.start_time, units)
for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint)
uom_row = [
[
"",
"",
units.distance_short_uom,
units.distance_long_uom,
"T",
units.speed_uom,
"",
"",
units.mass_uom,
]
]
writer.table(
flight_plan_builder.build(),
flight_plan_builder.build() + uom_row,
headers=[
"#",
"Action",
@@ -404,15 +411,18 @@ class BriefingPage(KneeboardPage):
)
writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa")
writer.table(
[
fl = self.flight
if fl.bingo_fuel and fl.joker_fuel:
writer.table(
[
"{}lbs".format(self.flight.bingo_fuel),
"{}lbs".format(self.flight.joker_fuel),
]
],
["Bingo", "Joker"],
)
[
f"{units.mass(pounds(fl.bingo_fuel)):.0f} {units.mass_uom}",
f"{units.mass(pounds(fl.joker_fuel)):.0f} {units.mass_uom}",
]
],
["Bingo", "Joker"],
)
if any(self.flight.laser_codes):
codes: list[list[str]] = []

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import itertools
import math
@@ -14,16 +15,149 @@ METERS_TO_FEET = 3.28084
FEET_TO_METERS = 1 / METERS_TO_FEET
NM_TO_METERS = 1852
METERS_TO_NM = 1 / NM_TO_METERS
MILES_TO_METERS = 1609.34
METERS_TO_MILES = 1 / MILES_TO_METERS
KNOTS_TO_KPH = 1.852
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
MS_TO_KPH = 3.6
KPH_TO_MS = 1 / MS_TO_KPH
KPH_TO_MPH = 0.621371
MPH_TO_KPH = 1 / KPH_TO_MPH
INHG_TO_HPA = 33.86389
INHG_TO_MMHG = 25.400002776728
LBS_TO_KG = 0.453592
KG_TO_LBS = 1 / LBS_TO_KG
class UnitSystem(ABC):
@abstractmethod
def distance_short(self, dist: Distance) -> float:
pass
@abstractmethod
def distance_long(self, dist: Distance) -> float:
pass
@property
@abstractmethod
def distance_short_uom(self) -> str:
pass
@property
@abstractmethod
def distance_long_uom(self) -> str:
pass
@abstractmethod
def speed(self, speed: Speed) -> float:
pass
@property
@abstractmethod
def speed_uom(self) -> str:
pass
@abstractmethod
def mass(self, mass: Mass) -> float:
pass
@property
@abstractmethod
def mass_uom(self) -> str:
pass
class NauticalUnits(UnitSystem):
def distance_short(self, dist: Distance) -> float:
return dist.feet
def distance_long(self, dist: Distance) -> float:
return dist.nautical_miles
@property
def distance_short_uom(self) -> str:
return "ft"
@property
def distance_long_uom(self) -> str:
return "nm"
def speed(self, speed: Speed) -> float:
return speed.knots
@property
def speed_uom(self) -> str:
return "kt"
def mass(self, mass: Mass) -> float:
return mass.pounds
@property
def mass_uom(self) -> str:
return "lb"
class MetricUnits(UnitSystem):
def distance_short(self, dist: Distance) -> float:
return dist.meters
def distance_long(self, dist: Distance) -> float:
return dist.kilometers
@property
def distance_short_uom(self) -> str:
return "m"
@property
def distance_long_uom(self) -> str:
return "km"
def speed(self, speed: Speed) -> float:
return speed.kph
@property
def speed_uom(self) -> str:
return "kph"
def mass(self, mass: Mass) -> float:
return mass.kgs
@property
def mass_uom(self) -> str:
return "kg"
class ImperialUnits(UnitSystem):
def distance_short(self, dist: Distance) -> float:
return dist.feet
def distance_long(self, dist: Distance) -> float:
return dist.miles
@property
def distance_short_uom(self) -> str:
return "ft"
@property
def distance_long_uom(self) -> str:
return "m"
def speed(self, speed: Speed) -> float:
return speed.mph
@property
def speed_uom(self) -> str:
return "mph"
def mass(self, mass: Mass) -> float:
return mass.pounds
@property
def mass_uom(self) -> str:
return "lb"
@dataclass(frozen=True)
@@ -42,6 +176,14 @@ class Distance:
def nautical_miles(self) -> float:
return self.distance_in_meters * METERS_TO_NM
@property
def kilometers(self) -> float:
return self.distance_in_meters / 1000
@property
def miles(self) -> float:
return self.distance_in_meters * METERS_TO_MILES
@classmethod
def from_feet(cls, value: float) -> Distance:
return cls(value * FEET_TO_METERS)
@@ -119,6 +261,10 @@ class Speed:
def meters_per_second(self) -> float:
return self.speed_in_kph * KPH_TO_MS
@property
def mph(self) -> float:
return self.speed_in_kph * KPH_TO_MPH
def mach(self, altitude: Distance = meters(0)) -> float:
c_sound = mach(1, altitude)
return self.speed_in_kph / c_sound.kph
@@ -272,6 +418,27 @@ def inches_hg(value: float) -> Pressure:
return Pressure(value)
@dataclass(frozen=True, order=True)
class Mass:
mass_in_kg: float
@property
def pounds(self) -> float:
return self.mass_in_kg * KG_TO_LBS
@property
def kgs(self) -> float:
return self.mass_in_kg
def pounds(value: float) -> Mass:
return Mass(value * LBS_TO_KG)
def kgs(value: float) -> Mass:
return Mass(value)
PairwiseT = TypeVar("PairwiseT")