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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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")

View File

@ -29,3 +29,4 @@ radios:
channels:
type: viggen
namer: viggen
kneeboard_units: "metric"

View File

@ -17,3 +17,4 @@ role: Fighter
gunfighter: true
variants:
"Bf 109 K-4 Kurf\xFCrst": {}
kneeboard_units: "metric"

View File

@ -28,3 +28,4 @@ role: Fighter
gunfighter: true
variants:
Fw 190 A-8 Anton: {}
kneeboard_units: "metric"

View File

@ -17,3 +17,4 @@ role: Fighter
gunfighter: true
variants:
Fw 190 D-9 Dora: {}
kneeboard_units: "metric"

View File

@ -10,3 +10,4 @@ price: 22
role: Air-Superiority Fighter
variants:
J-11A Flanker-L: {}
kneeboard_units: "metric"

View File

@ -20,3 +20,4 @@ radios:
# The R-800L1 doesn't have preset channels, and the other radio is for
# communications with FAC and ground units, which don't currently have
# radios assigned, so no channels to configure.
kneeboard_units: "metric"

View File

@ -12,3 +12,4 @@ role: Light Attack
gunfighter: true
variants:
L-39ZA Albatros: {}
kneeboard_units: "metric"

View File

@ -19,6 +19,6 @@ manufacturer: Mil
origin: USSR/Russia
price: 14
role: Attack/Transport
metric_kneeboard: true
kneeboard_units: "metric"
variants:
Mi-24P Hind-F: {}

View File

@ -10,3 +10,4 @@ price: 5
role: Transport/Light Attack
variants:
Mi-8MTV2 Hip: {}
kneeboard_units: "metric"

View File

@ -21,3 +21,4 @@ variants:
radios:
intra_flight: RSI-6K HF
inter_flight: RSI-6K HF
kneeboard_units: "metric"

View File

@ -27,3 +27,4 @@ radios:
channels:
type: farmer
namer: single
kneeboard_units: "metric"

View File

@ -27,3 +27,4 @@ radios:
namer: single
intra_flight_radio_index: 1
inter_flight_radio_index: 1
kneeboard_units: "metric"

View File

@ -28,3 +28,4 @@ role: Multirole Fighter
max_range: 150
variants:
MiG-29A Fulcrum-A: {}
kneeboard_units: "metric"

View File

@ -28,3 +28,4 @@ role: Multirole Fighter
max_range: 150
variants:
MiG-29G Fulcrum-A: {}
kneeboard_units: "metric"

View File

@ -28,3 +28,4 @@ role: Multirole Fighter
max_range: 150
variants:
MiG-29S Fulcrum-C: {}
kneeboard_units: "metric"

View File

@ -6,3 +6,4 @@ price: 6
role: Light Bomber, Fighter Bomber, Night Fighter, Maritime Strike Aircraft, Photo Recon Aircraft
variants:
MosquitoFBMkVI: {}
kneeboard_units: "imperial"

View File

@ -29,3 +29,4 @@ radios:
channels:
type: SCR-522
namer: SCR-522
kneeboard_units: "imperial"

View File

@ -29,3 +29,4 @@ radios:
channels:
type: SCR-522
namer: SCR-522
kneeboard_units: "imperial"

View File

@ -28,4 +28,5 @@ radios:
inter_flight: SCR522
channels:
type: SCR-522
namer: SCR-522
namer: SCR-522
kneeboard_units: "imperial"

View File

@ -30,3 +30,4 @@ radios:
channels:
type: SCR-522
namer: SCR-522
kneeboard_units: "imperial"

View File

@ -30,3 +30,4 @@ radios:
channels:
type: SCR-522
namer: SCR-522
kneeboard_units: "imperial"

View File

@ -15,3 +15,4 @@ price: 5
role: Light Attack
variants:
SA 342L Gazelle: {}
kneeboard_units: "metric"

View File

@ -18,3 +18,4 @@ variants:
introduced: 1974
manufacturer: Westland
SA 342M Gazelle: {}
kneeboard_units: "metric"

View File

@ -1,3 +1,4 @@
price: 4
variants:
SA342Minigun: null
kneeboard_units: "metric"

View File

@ -15,3 +15,4 @@ price: 8
role: Light Attack
variants:
SA 342M Gazelle Mistral: {}
kneeboard_units: "metric"

View File

@ -41,3 +41,4 @@ role: Fighter
gunfighter: true
variants:
Spitfire LF Mk IX: {}
kneeboard_units: "imperial"

View File

@ -41,3 +41,4 @@ role: Fighter
gunfighter: true
variants:
Spitfire LF Mk IX (Clipped Wings): {}
kneeboard_units: "imperial"

View File

@ -14,3 +14,4 @@ role: Close Air Support/Attack
max_range: 200
variants:
Su-25 Frogfoot: {}
kneeboard_units: "metric"

View File

@ -14,3 +14,4 @@ role: Close Air Support/Attack
max_range: 200
variants:
Su-25T Frogfoot: {}
kneeboard_units: "metric"

View File

@ -17,3 +17,4 @@ role: Air-Superiority Fighter
max_range: 300
variants:
Su-27 Flanker-B: {}
kneeboard_units: "metric"

View File

@ -29,3 +29,4 @@ variants:
origin: China
role: Carrier-based Multirole Fighter
Su-33 Flanker-D: {}
kneeboard_units: "metric"