RndName 795df1a93f
Update Distance comparison operators
remove order=True and implement the comparison operators manually to fix typing error
2022-01-07 15:34:49 +01:00

302 lines
7.9 KiB
Python

from __future__ import annotations
import itertools
import math
import random
from collections.abc import Iterable
from dataclasses import dataclass
from typing import TypeVar, Union
from dcs import Point
from shapely.geometry import Point as ShapelyPoint
METERS_TO_FEET = 3.28084
FEET_TO_METERS = 1 / METERS_TO_FEET
NM_TO_METERS = 1852
METERS_TO_NM = 1 / NM_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
INHG_TO_HPA = 33.86389
INHG_TO_MMHG = 25.400002776728
LBS_TO_KG = 0.453592
@dataclass(frozen=True)
class Distance:
distance_in_meters: float
@property
def feet(self) -> float:
return self.distance_in_meters * METERS_TO_FEET
@property
def meters(self) -> float:
return self.distance_in_meters
@property
def nautical_miles(self) -> float:
return self.distance_in_meters * METERS_TO_NM
@classmethod
def from_feet(cls, value: float) -> Distance:
return cls(value * FEET_TO_METERS)
@classmethod
def from_meters(cls, value: float) -> Distance:
return cls(value)
@classmethod
def from_nautical_miles(cls, value: float) -> Distance:
return cls(value * NM_TO_METERS)
@classmethod
def inf(cls) -> Distance:
return cls.from_meters(math.inf)
def __add__(self, other: Distance) -> Distance:
return meters(self.meters + other.meters)
def __sub__(self, other: Distance) -> Distance:
return meters(self.meters - other.meters)
def __mul__(self, other: Union[float, int]) -> Distance:
return meters(self.meters * other)
__rmul__ = __mul__
def __truediv__(self, other: Union[float, int]) -> Distance:
return meters(self.meters / other)
def __floordiv__(self, other: Union[float, int]) -> Distance:
return meters(self.meters // other)
def __bool__(self) -> bool:
return not math.isclose(self.meters, 0.0)
def __lt__(self, other: Distance) -> bool:
return self.meters < other.meters
def __le__(self, other: Distance) -> bool:
return self.meters <= other.meters
def __gt__(self, other: Distance) -> bool:
return self.meters > other.meters
def __ge__(self, other: Distance) -> bool:
return self.meters >= other.meters
def feet(value: float) -> Distance:
return Distance.from_feet(value)
def meters(value: float) -> Distance:
return Distance.from_meters(value)
def nautical_miles(value: float) -> Distance:
return Distance.from_nautical_miles(value)
@dataclass(frozen=True, order=True)
class Speed:
speed_in_kph: float
@property
def knots(self) -> float:
return self.speed_in_kph * KPH_TO_KNOTS
@property
def kph(self) -> float:
return self.speed_in_kph
@property
def meters_per_second(self) -> float:
return self.speed_in_kph * KPH_TO_MS
def mach(self, altitude: Distance = meters(0)) -> float:
c_sound = mach(1, altitude)
return self.speed_in_kph / c_sound.kph
@classmethod
def from_knots(cls, value: float) -> Speed:
return cls(value * KNOTS_TO_KPH)
@classmethod
def from_kph(cls, value: float) -> Speed:
return cls(value)
@classmethod
def from_meters_per_second(cls, value: float) -> Speed:
return cls(value * MS_TO_KPH)
@classmethod
def from_mach(cls, value: float, altitude: Distance) -> Speed:
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
if altitude <= feet(36152):
temperature_f = 59 - 0.00356 * altitude.feet
else:
# There's another formula for altitudes over 82k feet, but we better
# not be planning waypoints that high...
temperature_f = -70
temperature_k = (temperature_f + 459.67) * (5 / 9)
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
# Dependent on temperature, but varies very little (+/-0.001)
# between -40F and 180F.
heat_capacity_ratio = 1.4
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
gas_constant = 286 # m^2/s^2/K
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
return mps(c_sound) * value
def __add__(self, other: Speed) -> Speed:
return kph(self.kph + other.kph)
def __sub__(self, other: Speed) -> Speed:
return kph(self.kph - other.kph)
def __mul__(self, other: Union[float, int]) -> Speed:
return kph(self.kph * other)
__rmul__ = __mul__
def __truediv__(self, other: Union[float, int]) -> Speed:
return kph(self.kph / other)
def __floordiv__(self, other: Union[float, int]) -> Speed:
return kph(self.kph // other)
def __bool__(self) -> bool:
return not math.isclose(self.kph, 0.0)
def knots(value: float) -> Speed:
return Speed.from_knots(value)
def kph(value: float) -> Speed:
return Speed.from_kph(value)
def mps(value: float) -> Speed:
return Speed.from_meters_per_second(value)
def mach(value: float, altitude: Distance) -> Speed:
return Speed.from_mach(value, altitude)
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
@dataclass(frozen=True, order=True)
class Heading:
heading_in_degrees: int
@property
def degrees(self) -> int:
return Heading.reduce_angle(self.heading_in_degrees)
@property
def radians(self) -> float:
return math.radians(Heading.reduce_angle(self.heading_in_degrees))
@property
def opposite(self) -> Heading:
return self + Heading.from_degrees(180)
@property
def right(self) -> Heading:
return self + Heading.from_degrees(90)
@property
def left(self) -> Heading:
return self - Heading.from_degrees(90)
def angle_between(self, other: Heading) -> Heading:
angle_between = abs(self.degrees - other.degrees)
if angle_between > 180:
angle_between = 360 - angle_between
return Heading.from_degrees(angle_between)
@staticmethod
def reduce_angle(angle: int) -> int:
return angle % 360
@classmethod
def from_degrees(cls, angle: Union[int, float]) -> Heading:
return cls(Heading.reduce_angle(round(angle)))
@classmethod
def from_radians(cls, angle: Union[int, float]) -> Heading:
deg = round(math.degrees(angle))
return cls(Heading.reduce_angle(deg))
@classmethod
def random(cls, min_angle: int = 0, max_angle: int = 359) -> Heading:
return Heading.from_degrees(random.randint(min_angle, max_angle))
def __add__(self, other: Heading) -> Heading:
return Heading.from_degrees(self.degrees + other.degrees)
def __sub__(self, other: Heading) -> Heading:
return Heading.from_degrees(self.degrees - other.degrees)
@dataclass(frozen=True, order=True)
class Pressure:
pressure_in_inches_hg: float
@property
def inches_hg(self) -> float:
return self.pressure_in_inches_hg
@property
def mm_hg(self) -> float:
return self.pressure_in_inches_hg * INHG_TO_MMHG
@property
def hecto_pascals(self) -> float:
return self.pressure_in_inches_hg * INHG_TO_HPA
def inches_hg(value: float) -> Pressure:
return Pressure(value)
PairwiseT = TypeVar("PairwiseT")
def pairwise(iterable: Iterable[PairwiseT]) -> Iterable[tuple[PairwiseT, PairwiseT]]:
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)
def interpolate(value1: float, value2: float, factor: float, clamp: bool) -> float:
"""Inerpolate between two values, factor 0-1"""
interpolated = value1 + (value2 - value1) * factor
if clamp:
bigger_value = max(value1, value2)
smaller_value = min(value1, value2)
return min(bigger_value, max(smaller_value, interpolated))
else:
return interpolated
def dcs_to_shapely_point(point: Point) -> ShapelyPoint:
return ShapelyPoint(point.x, point.y)