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 ( from game.utils import (
Distance, Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL, SPEED_OF_SOUND_AT_SEA_LEVEL,
ImperialUnits,
MetricUnits,
NauticalUnits,
Speed, Speed,
UnitSystem,
feet, feet,
knots, knots,
kph, kph,
@ -154,8 +158,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
# main weapon. It'll RTB when it doesn't have gun ammo left. # main weapon. It'll RTB when it doesn't have gun ammo left.
gunfighter: bool gunfighter: bool
# If true, kneeboards will be generated in metric units # UnitSystem to use for the kneeboard, defaults to Nautical (kt/nm/ft)
metric_kneeboard: bool kneeboard_units: UnitSystem
# If true, kneeboards will display zulu times # If true, kneeboards will display zulu times
utc_kneeboard: bool utc_kneeboard: bool
@ -376,6 +380,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
except KeyError: except KeyError:
introduction = "No data." 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]): for variant in data.get("variants", [aircraft.id]):
yield AircraftType( yield AircraftType(
dcs_unit_type=aircraft, dcs_unit_type=aircraft,
@ -401,6 +412,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
intra_flight_radio=radio_config.intra_flight, intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator, channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer, channel_namer=radio_config.channel_namer,
metric_kneeboard=data.get("metric_kneeboard", False), kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False), 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.radio.radios import RadioFrequency
from game.theater import ConflictTheater, LatLon, TheaterGroundObject from game.theater import ConflictTheater, LatLon, TheaterGroundObject
from game.theater.bullseye import Bullseye 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 game.weather import Weather
from gen.runways import RunwayData from gen.runways import RunwayData
from .aircraft.flightdata import FlightData from .aircraft.flightdata import FlightData
@ -202,12 +202,12 @@ class FlightPlanBuilder:
WAYPOINT_DESC_MAX_LEN = 25 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.start_time = start_time
self.rows: List[List[str]] = [] self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = [] self.target_points: List[NumberedWaypoint] = []
self.last_waypoint: Optional[FlightWaypoint] = None self.last_waypoint: Optional[FlightWaypoint] = None
self.is_metric = is_metric self.units = units
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None: def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT: 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 ''}" return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
def _format_alt(self, alt: Distance) -> str: def _format_alt(self, alt: Distance) -> str:
if self.is_metric: return f"{self.units.distance_short(alt):.0f}"
return f"{alt.meters:.0f} m"
else:
return f"{alt.feet:.0f} ft"
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str: def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None: if self.last_waypoint is None:
return "-" return "-"
if self.is_metric: distance = meters(
distance = meters( self.last_waypoint.position.distance_to_point(waypoint.position)
self.last_waypoint.position.distance_to_point(waypoint.position) )
)
return f"{(distance.meters / 1000):.1f} km" return f"{self.units.distance_long(distance):.1f}"
else:
distance = meters(
self.last_waypoint.position.distance_to_point(waypoint.position)
)
return f"{distance.nautical_miles:.1f} nm"
def _waypoint_bearing(self, waypoint: FlightWaypoint) -> str: def _waypoint_bearing(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None: if self.last_waypoint is None:
return "-" return "-"
bearing = self.last_waypoint.position.heading_between_point(waypoint.position) 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: def _ground_speed(self, waypoint: FlightWaypoint) -> str:
if self.last_waypoint is None: if self.last_waypoint is None:
@ -310,20 +302,19 @@ class FlightPlanBuilder:
else: else:
return "-" return "-"
distance = meters( speed = mps(
self.last_waypoint.position.distance_to_point(waypoint.position) 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 return f"{self.units.speed(speed):.0f}"
def _format_min_fuel(min_fuel: Optional[float]) -> str:
def _format_min_fuel(self, min_fuel: Optional[float]) -> str:
if min_fuel is None: if min_fuel is None:
return "" 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]]: def build(self) -> List[List[str]]:
return self.rows return self.rows
@ -373,13 +364,29 @@ class BriefingPage(KneeboardPage):
) )
writer.heading("Flight Plan") 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): for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint) 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( writer.table(
flight_plan_builder.build(), flight_plan_builder.build() + uom_row,
headers=[ headers=[
"#", "#",
"Action", "Action",
@ -404,15 +411,18 @@ class BriefingPage(KneeboardPage):
) )
writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa") 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), f"{units.mass(pounds(fl.bingo_fuel)):.0f} {units.mass_uom}",
] f"{units.mass(pounds(fl.joker_fuel)):.0f} {units.mass_uom}",
], ]
["Bingo", "Joker"], ],
) ["Bingo", "Joker"],
)
if any(self.flight.laser_codes): if any(self.flight.laser_codes):
codes: list[list[str]] = [] codes: list[list[str]] = []

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
import itertools import itertools
import math import math
@ -14,16 +15,149 @@ METERS_TO_FEET = 3.28084
FEET_TO_METERS = 1 / METERS_TO_FEET FEET_TO_METERS = 1 / METERS_TO_FEET
NM_TO_METERS = 1852 NM_TO_METERS = 1852
METERS_TO_NM = 1 / NM_TO_METERS METERS_TO_NM = 1 / NM_TO_METERS
MILES_TO_METERS = 1609.34
METERS_TO_MILES = 1 / MILES_TO_METERS
KNOTS_TO_KPH = 1.852 KNOTS_TO_KPH = 1.852
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
MS_TO_KPH = 3.6 MS_TO_KPH = 3.6
KPH_TO_MS = 1 / MS_TO_KPH 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_HPA = 33.86389
INHG_TO_MMHG = 25.400002776728 INHG_TO_MMHG = 25.400002776728
LBS_TO_KG = 0.453592 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) @dataclass(frozen=True)
@ -42,6 +176,14 @@ class Distance:
def nautical_miles(self) -> float: def nautical_miles(self) -> float:
return self.distance_in_meters * METERS_TO_NM 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 @classmethod
def from_feet(cls, value: float) -> Distance: def from_feet(cls, value: float) -> Distance:
return cls(value * FEET_TO_METERS) return cls(value * FEET_TO_METERS)
@ -119,6 +261,10 @@ class Speed:
def meters_per_second(self) -> float: def meters_per_second(self) -> float:
return self.speed_in_kph * KPH_TO_MS 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: def mach(self, altitude: Distance = meters(0)) -> float:
c_sound = mach(1, altitude) c_sound = mach(1, altitude)
return self.speed_in_kph / c_sound.kph return self.speed_in_kph / c_sound.kph
@ -272,6 +418,27 @@ def inches_hg(value: float) -> Pressure:
return Pressure(value) 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") PairwiseT = TypeVar("PairwiseT")

View File

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

View File

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

View File

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

View File

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

View File

@ -10,3 +10,4 @@ price: 22
role: Air-Superiority Fighter role: Air-Superiority Fighter
variants: variants:
J-11A Flanker-L: {} 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 # 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 # communications with FAC and ground units, which don't currently have
# radios assigned, so no channels to configure. # radios assigned, so no channels to configure.
kneeboard_units: "metric"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,3 +28,4 @@ role: Multirole Fighter
max_range: 150 max_range: 150
variants: variants:
MiG-29S Fulcrum-C: {} 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 role: Light Bomber, Fighter Bomber, Night Fighter, Maritime Strike Aircraft, Photo Recon Aircraft
variants: variants:
MosquitoFBMkVI: {} MosquitoFBMkVI: {}
kneeboard_units: "imperial"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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