Source code for pyBADA.geodesic

# -*- coding: utf-8 -*-
"""
pyBADA
Geodesic calculation module
Developped @EUROCONTROL (EIH)
2024
"""

from math import (
    tan,
    atan2,
    sin,
    asin,
    cos,
    radians,
    degrees,
    sqrt,
    pi,
    log,
    log2,
    acos,
)
from pyBADA.aircraft import Airplane as airplane
from pyBADA import conversions as conv
from pyBADA import constants as const


[docs] class Haversine: """This class implements the geodesic calculations on sherical earth (ignoring ellipsoidal effects). .. note:: https://www.movable-type.co.uk/scripts/latlong.html """ def __init__(self): pass
[docs] @staticmethod def distance(LAT_init, LON_init, LAT_final, LON_final): """Calculate the great-circle distance between two points on the Earth's surface using the haversine formula. The great-circle distance is the shortest distance between two points over the Earth's surface, ignoring elevation changes (i.e., hills or mountains). :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float :returns: Great-circle distance between the two points in meters. :rtype: float """ phi_init = radians(LAT_init) phi_final = radians(LAT_final) delta_phi = phi_final - phi_init lambda_init = radians(LON_init) lambda_final = radians(LON_final) delta_lambda = lambda_final - lambda_init a = pow(sin(delta_phi / 2), 2) + cos(phi_init) * cos(phi_final) * pow( sin(delta_lambda / 2), 2 ) c = 2 * atan2(sqrt(a), sqrt(1 - a)) d = const.AVG_EARTH_RADIUS_KM * 1000 * c return d
[docs] @staticmethod def destinationPoint(LAT_init, LON_init, distance, bearing): """Calculate the destination point given an initial point, distance, and bearing. Given an initial latitude and longitude, this function calculates the destination point after traveling the specified distance along the given initial bearing (direction). Note that the bearing may vary along the path, but this calculation assumes a constant bearing. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param distance: Distance to travel from the initial point in meters. :param bearing: Initial bearing (direction) in degrees. :type LAT_init: float :type LON_init: float :type distance: float :type bearing: float :returns: Tuple containing the destination latitude and longitude in degrees. :rtype: (float, float) """ delta = distance / (const.AVG_EARTH_RADIUS_KM * 1000) theta = radians(bearing) phi_init = radians(LAT_init) lambda_init = radians(LON_init) sinPhi_final = sin(phi_init) * cos(delta) + cos(phi_init) * sin( delta ) * cos(theta) phi_final = asin(sinPhi_final) y = sin(theta) * sin(delta) * cos(phi_init) x = cos(delta) - sin(phi_init) * sinPhi_final lambda_final = lambda_init + atan2(y, x) lat = degrees(phi_final) lon = degrees(lambda_final) return (lat, lon)
[docs] @staticmethod def bearing(LAT_init, LON_init, LAT_final, LON_final): """Calculate the initial bearing between two points along a great-circle path. The initial bearing (forward azimuth) is the direction one would need to travel in a straight line along the great-circle arc from the start point to the end point. This bearing is measured clockwise from true north. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float :returns: Initial bearing in degrees (0° to 360°). :rtype: float """ bearing = atan2( sin(radians(LON_final) - radians(LON_init)) * cos(radians(LAT_final)), cos(radians(LAT_init)) * sin(radians(LAT_final)) - sin(radians(LAT_init)) * cos(radians(LAT_final)) * cos(radians(LON_final) - radians(LON_init)), ) bearing = (degrees(bearing) + 360) % 360 return bearing
[docs] class Vincenty(object): """This class implements the vincenty calculations of geodesics on the ellipsoid-model earth .. note:: https://www.movable-type.co.uk/scripts/latlong-vincenty.html """
[docs] @staticmethod def distance_bearing(LAT_init, LON_init, LAT_final, LON_final): """Calculate the geodesic distance, initial bearing, and final bearing between two points on the Earth's surface. This method uses the Vincenty formula to account for the Earth's ellipsoidal shape, providing accurate calculations for long distances. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float :returns: Tuple containing distance in meters, initial bearing in degrees, and final bearing in degrees. :rtype: (float, float, float) """ LON2 = radians(LON_final) LON1 = radians(LON_init) LAT2 = radians(LAT_final) LAT1 = radians(LAT_init) L = LON2 - LON1 tanU1 = (1 - const.f) * tan(LAT1) cosU1 = 1 / sqrt(1 + tanU1 * tanU1) sinU1 = tanU1 * cosU1 tanU2 = (1 - const.f) * tan(LAT2) cosU2 = 1 / sqrt(1 + tanU2 * tanU2) sinU2 = tanU2 * cosU2 antipodal = False if abs(L) > pi / 2 or abs(LAT2 - LAT1) > pi / 2: antipodal = True lambd = L lambd_new = 0.0 iterations = 0 while iterations == 0 or ( abs(lambd - lambd_new) > 1e-12 and iterations < 1000 ): iterations += 1 sinlambda = sin(lambd) coslambda = cos(lambd) sinSqsigma = pow((cosU2 * sinlambda), 2) + pow( (cosU1 * sinU2 - sinU1 * cosU2 * coslambda), 2 ) sinsigma = sqrt(sinSqsigma) cossigma = sinU1 * sinU2 + cosU1 * cosU2 * coslambda sigma = atan2(sinsigma, cossigma) sinalpha = cosU1 * cosU2 * sinlambda / sinsigma cosSqalpha = 1 - pow(sinalpha, 2) if cosSqalpha != 0.0: cos2sigmam = cossigma - 2 * sinU1 * sinU2 / cosSqalpha else: cos2sigmam = 0.0 C = ( const.f / 16 * cosSqalpha * (4 + const.f * (4 - 3 * cosSqalpha)) ) lambd_new = lambd lambd = L + (1 - C) * const.f * sinalpha * ( sigma + C * sinsigma * ( cos2sigmam + C * cossigma * (-1 + 2 * cos2sigmam * cos2sigmam) ) ) if antipodal: iterationcheck = abs(lambd) - pi else: iterationcheck = abs(lambd) if iterationcheck > pi: return [None, None, None] # vincenty formula failed to converge if iterations >= 1000: return [None, None, None] uSq = ( cosSqalpha * (pow(const.a, 2) - pow(const.b, 2)) / pow(const.b, 2) ) A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) deltaSigma = ( B * sinsigma * ( cos2sigmam + B / 4 * ( cossigma * (-1 + 2 * pow(cos2sigmam, 2)) - B / 6 * cos2sigmam * (-3 + 4 * pow(sinsigma, 2)) * (-3 + 4 * pow(cos2sigmam, 2)) ) ) ) s = const.b * A * (sigma - deltaSigma) # initial bearing alpha1 = atan2( cosU2 * sinlambda, cosU1 * sinU2 - sinU1 * cosU2 * coslambda ) alpha1 = (degrees(alpha1) + 360) % 360 # final bearing alpha2 = atan2( cosU1 * sinlambda, -sinU1 * cosU2 + cosU1 * sinU2 * coslambda ) alpha2 = (degrees(alpha2) + 360) % 360 return (s, alpha1, alpha2)
[docs] @staticmethod def distance(LAT_init, LON_init, LAT_final, LON_final): """Calculate the geodesic distance between two latitude/longitude points on the Earth's surface. This method uses an accurate ellipsoidal model (Vincenty's formula) for calculating the distance, which is particularly useful for long distances across the globe. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float :returns: The geodesic distance in meters. :rtype: float """ dist_bearing = Vincenty.distance_bearing( LAT_init, LON_init, LAT_final, LON_final ) return dist_bearing[0]
[docs] @staticmethod def bearing_initial(LAT_init, LON_init, LAT_final, LON_final): """Calculate the initial bearing (forward azimuth) from the initial point to the final point. This function returns the initial bearing that, if followed in a straight line along a great-circle path, will take you from the start point to the end point. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float :returns: The initial bearing in degrees. :rtype: float """ b_initial = Vincenty.distance_bearing( LAT_init, LON_init, LAT_final, LON_final ) return b_initial[1]
[docs] @staticmethod def bearing_final(LAT_init, LON_init, LAT_final, LON_final): """Calculate the final bearing (reverse azimuth) from the final point to the initial point. This function calculates the final bearing at the destination point, which is the direction one would need to take to return to the initial point along the great-circle path. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float :returns: The final bearing in degrees. :rtype: float """ b_final = Vincenty.distance_bearing( LAT_init, LON_init, LAT_final, LON_final ) return b_final[2]
[docs] @staticmethod def destinationPoint_finalBearing(LAT_init, LON_init, distance, bearing): """Calculate the destination point and final bearing given an initial point, distance, and bearing. This method calculates the latitude and longitude of the destination point after traveling a specified distance along the given bearing from the starting point. It also returns the final bearing at the destination point. Note: The bearing normally varies along the path due to the Earth's curvature. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param distance: Distance traveled from the initial point in meters. :param bearing: Initial bearing (direction) in degrees. :type LAT_init: float :type LON_init: float :type distance: float :type bearing: float :returns: Tuple containing the destination latitude, destination longitude, and final bearing (degrees). :rtype: (float, float, float) """ LON1 = radians(LON_init) LAT1 = radians(LAT_init) sinalpha1 = sin(radians(bearing)) cosalpha1 = cos(radians(bearing)) tanU1 = (1 - const.f) * tan(LAT1) cosU1 = 1 / sqrt(1 + tanU1 * tanU1) sinU1 = tanU1 * cosU1 sigma1 = atan2(tanU1, cosalpha1) sinalpha = cosU1 * sinalpha1 cosSqalpha = 1 - pow(sinalpha, 2) uSq = ( cosSqalpha * (pow(const.a, 2) - pow(const.b, 2)) / pow(const.b, 2) ) A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) sigma = distance / (const.b * A) sigma_new = 0.0 iterations = 0 while iterations == 0 or ( abs(sigma - sigma_new) > 1e-12 and iterations < 1000 ): iterations += 1 cos2sigmam = cos(2 * sigma1 + sigma) sinsigma = sin(sigma) cossigma = cos(sigma) deltaSigma = ( B * sinsigma * ( cos2sigmam + B / 4 * ( cossigma * (-1 + 2 * cos2sigmam * cos2sigmam) - B / 6 * cos2sigmam * (-3 + 4 * sinsigma * sinsigma) * (-3 + 4 * cos2sigmam * cos2sigmam) ) ) ) sigma_new = sigma sigma = distance / (const.b * A) + deltaSigma # vincenty formula failed to converge if iterations >= 1000: return [None, None, None] # print(distance, sigma,sigma_new,abs(sigma-sigma_new)) # print(sinsigma) x = sinU1 * sinsigma - cosU1 * cossigma * cosalpha1 LAT2 = atan2( sinU1 * cossigma + cosU1 * sinsigma * cosalpha1, (1 - const.f) * sqrt(sinalpha * sinalpha + x * x), ) lambd = atan2( sinsigma * sinalpha1, cosU1 * cossigma - sinU1 * sinsigma * cosalpha1, ) C = const.f / 16 * cosSqalpha * (4 + const.f * (4 - 3 * cosSqalpha)) L = lambd - (1 - C) * const.f * sinalpha * ( sigma + C * sinsigma * (cos2sigmam + C + cossigma * (-1 + 2 * cos2sigmam * cos2sigmam)) ) LON2 = LON1 + L alpha2 = atan2(sinalpha, -x) finalBearing = (degrees(alpha2) + 360) % 360 return (degrees(LAT2), degrees(LON2), finalBearing)
[docs] @staticmethod def destinationPoint(LAT_init, LON_init, distance, bearing): """Calculate the destination point after traveling a specified distance on a given bearing. This method returns the latitude and longitude of the destination point after traveling the given distance on the specified initial bearing, following a great-circle path. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param distance: Distance to be traveled from the initial point in meters. :param bearing: Initial bearing (direction) in degrees. :type LAT_init: float :type LON_init: float :type distance: float :type bearing: float :returns: Tuple containing the destination latitude and longitude in degrees. :rtype: (float, float) """ dest = Vincenty.destinationPoint_finalBearing( LAT_init, LON_init, distance, bearing ) return (dest[0], dest[1])
[docs] class RhumbLine(object): """This class implements the rhumb line (loxodrome) calculations of geodesics on the ellipsoid-model earth .. note:: https://github.com/SpyrosMouselinos/distancly/blob/master/distancly/rhumbline.py """
[docs] @staticmethod def simple_project(latitiude: float) -> float: """ Applies a projection to the latitude for use in rhumbline calculations. The projection is based on the Mercator projection, where latitudes are projected to account for the curvature of the Earth. This formula ensures that the calculations along the rhumbline are accurate. :param latitiude: Latitude in radians. :return: The projected latitude in radians. """ return tan(pi / 4 + latitiude / 2)
[docs] @staticmethod def distance(LAT_init, LON_init, LAT_final, LON_final) -> float: """ Calculates the rhumbline distance between two geographical points in meters. The rhumbline is a path of constant bearing that crosses all meridians at the same angle, unlike a great-circle route which is the shortest distance between two points on the Earth's surface. This method adjusts for longitudes that span more than half of the globe. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :return: The rhumbline distance in meters. """ lat_a = radians(LAT_init) lon_a = radians(LON_init) lat_b = radians(LAT_final) lon_b = radians(LON_final) delta_phi = lat_b - lat_a delta_psi = log( RhumbLine.simple_project(lat_b) / RhumbLine.simple_project(lat_a) ) delta_lambda = lon_b - lon_a if abs(delta_psi) > 10e-12: q = delta_phi / delta_psi else: q = cos(lat_a) if abs(delta_lambda) > pi: if delta_lambda > 0: delta_lambda = -(2 * pi - delta_lambda) else: delta_lambda = 2 * pi + delta_lambda dist = ( sqrt(delta_phi * delta_phi + q * q * delta_lambda * delta_lambda) * const.AVG_EARTH_RADIUS_KM ) return dist * 1000
[docs] @staticmethod def bearing(LAT_init, LON_init, LAT_final, LON_final) -> float: """ Calculates the rhumbline bearing from the initial point to the final point. This returns the constant bearing (direction) required to travel along a rhumbline between the two points. The bearing is adjusted for longitudes that cross the 180-degree meridian. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :return: The rhumbline bearing in degrees. """ lat_a = radians(LAT_init) lon_a = radians(LON_init) lat_b = radians(LAT_final) lon_b = radians(LON_final) delta_psi = log( RhumbLine.simple_project(lat_b) / RhumbLine.simple_project(lat_a) ) delta_lambda = lon_b - lon_a if abs(delta_lambda) > pi: if delta_lambda > 0: delta_lambda = -(2 * pi - delta_lambda) else: delta_lambda = 2 * pi + delta_lambda return degrees(atan2(delta_lambda, delta_psi)) % 360
[docs] @staticmethod def destinationPoint(LAT_init, LON_init, bearing, distance) -> tuple: """ Calculates the destination point given an initial point, a bearing, and a distance traveled. This method computes the final latitude and longitude after traveling along a rhumbline for a given distance in meters from the initial point at a constant bearing. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param bearing: The constant bearing in degrees. :param distance: The distance to travel from the initial point in meters. :return: A tuple containing the destination latitude and longitude in degrees. """ lat_a = radians(LAT_init) lon_a = radians(LON_init) theta = radians(bearing) delta = (distance / 1000) / const.AVG_EARTH_RADIUS_KM delta_phi = delta * cos(theta) lat_b = lat_a + delta_phi delta_psi = log( RhumbLine.simple_project(lat_b) / RhumbLine.simple_project(lat_a) ) if abs(delta_psi) > 10e-12: q = delta_phi / delta_psi else: q = cos(lat_a) delta_lambda = delta * sin(theta) / q lon_b = lon_a + delta_lambda # Normalise latitude if abs(lat_b) > pi / 2: if lat_b > 0: lat_b = pi - lat_b else: lat_b = -pi - lat_b lat_b = degrees(lat_b) lon_b = degrees(lon_b) # Normalize longitude lon_b = (540 + lon_b) % 360 - 180 return (lat_b, lon_b)
[docs] @staticmethod def loxodromic_mid_point( LAT_init, LON_init, LAT_final, LON_final ) -> tuple: """ Calculates the midpoint along a rhumbline between two geographical points. The midpoint is calculated using the rhumbline path between the initial and final points. This takes into account the Earth's curvature by projecting the latitudes. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :return: A tuple representing the midpoint's latitude and longitude in degrees. """ lat_a = radians(LAT_init) lon_a = radians(LON_init) lat_b = radians(LAT_final) lon_b = radians(LON_final) # Anti - Meridian Crossing if abs(lon_b - lon_a) > pi: lon_a += 2 * pi lat_mid = (lat_a + lat_b) / 2 f1 = RhumbLine.simple_project(lat_a) f2 = RhumbLine.simple_project(lat_b) f3 = RhumbLine.simple_project(lat_mid) if abs(f2 - f1) < 1e-6: lon_mid = lon_a + lon_b / 2 else: lon_mid = ( (lon_b - lon_a) * log(f3) + lon_a * log(f2) - lon_b * log(f1) ) / log(f2 / f1) lat_mid = degrees(lat_mid) lon_mid = degrees(lon_mid) # Normalize longitude lon_mid = (540 + lon_mid) % 360 - 180 return lat_mid, lon_mid
[docs] @staticmethod def loxodromic_power_interpolation( LAT_init, LON_init, LAT_final, LON_final, n_points: int ) -> list: """ Generates a specified number of points between two geographical locations along a rhumbline path. This method recursively calculates intermediate points between two points on the Earth's surface, following a constant bearing rhumbline path. The number of points should be a power of 2 minus 1. :param LAT_init: Initial latitude in degrees. :param LON_init: Initial longitude in degrees. :param LAT_final: Final latitude in degrees. :param LON_final: Final longitude in degrees. :param n_points: Number of intermediate points to generate. Must be a power of 2 minus 1. :return: A list of tuples, where each tuple represents an interpolated point's latitude and longitude in degrees. """ n_points = int(n_points) if not log2(n_points + 1).is_integer(): print( "N_Points must be an power of 2 minus 1 Number! e.g. 1,3,7,15,..." ) return [] lmp = RhumbLine.loxodromic_mid_point # Recursive Solution # def solution(a, b, idx): if idx == 1: return lmp(a[0], a[1], b[0], b[1]) else: return ( solution(a, solution(a, b, 1), (idx - 1) / 2), solution(a, b, 1), solution(solution(a, b, 1), b, (idx - 1) / 2), ) points = solution( (LAT_init, LON_init), (LAT_final, LON_final), n_points ) # Decouple points decoupled_points = [] if len(points) == 2: decoupled_points.append(points) else: for midpoint in points: decoupled_points.append(midpoint) return decoupled_points
[docs] class Turn(object): """This class implements the calculations of geodesics turns"""
[docs] @staticmethod def destinationPoint_finalBearing( LAT_init, LON_init, bearingInit, TAS, rateOfTurn, timeOfTurn, directionOfTurn, centerPoint=None, ): """Calculates the destination point and final bearing after traveling for a given time with a specified turn. This function computes the aircraft's final position and bearing after making a turn at a specified rate of turn, direction, and true airspeed (TAS). If TAS is zero, the aircraft rotates in place. The calculation accounts for turning radius and bank angle. :param LAT_init: Initial latitude [deg]. :param LON_init: Initial longitude [deg]. :param timeOfTurn: Time spent in turn [s]. :param bearingInit: Initial bearing [deg]. :param TAS: True Airspeed (TAS) [m/s]. :param rateOfTurn: Rate of turn [deg/s]. :param directionOfTurn: Direction of turn ('LEFT' or 'RIGHT'). :param centerPoint: Optional latitude and longitude of the rotation center (defaults to None) [deg, deg]. :type LAT_init: float. :type LON_init: float. :type timeOfTurn: float. :type bearingInit: float. :type TAS: float. :type rateOfTurn: float. :type directionOfTurn: str. :type centerPoint: tuple(float, float). :returns: Destination point's latitude, longitude, and final bearing [deg, deg, deg]. :rtype: tuple(float, float, float). """ if TAS == 0: arcLength = ( rateOfTurn * timeOfTurn ) # amount of degrees to do the rotation if directionOfTurn == "RIGHT": bearing_final = (bearingInit + arcLength) % 360 elif directionOfTurn == "LEFT": bearing_final = (bearingInit - arcLength) % 360 return (LAT_init, LON_init, bearing_final) else: bankAngle = airplane.bankAngle( rateOfTurn=rateOfTurn, v=TAS ) # [degrees] arcLength = ( rateOfTurn * timeOfTurn ) # amount of degrees to do the rotation turnRadius = airplane.turnRadius_bankAngle( v=TAS, ba=bankAngle ) # [m] # find center of rotation, which is at (bearingInit + 90 degrees) and distance of turnRadius if directionOfTurn == "RIGHT": centerAngle = bearingInit + 90 elif directionOfTurn == "LEFT": centerAngle = bearingInit - 90 if centerPoint is None: centerPoint = RhumbLine.destinationPoint( LAT_init=LAT_init, LON_init=LON_init, distance=turnRadius, bearing=centerAngle, ) # calcualte new angle after the rotation from the center point to new destination point if directionOfTurn == "RIGHT": newAngle = (centerAngle + arcLength + 180) % 360 elif directionOfTurn == "LEFT": newAngle = (centerAngle - arcLength + 180) % 360 # calcualte the new destination point after the rotation from the center point, using the same distance finalPoint = RhumbLine.destinationPoint( LAT_init=centerPoint[0], LON_init=centerPoint[1], distance=turnRadius, bearing=newAngle, ) if directionOfTurn == "RIGHT": bearing_final = (bearingInit + arcLength) % 360 elif directionOfTurn == "LEFT": bearing_final = (bearingInit - arcLength) % 360 dist = RhumbLine.distance( LAT_init=LAT_init, LON_init=LON_init, LAT_final=centerPoint[0], LON_final=centerPoint[1], ) return (finalPoint[0], finalPoint[1], bearing_final)
[docs] @staticmethod def distance(rateOfTurn, TAS, timeOfTurn): """Calculates the distance traveled during a turn based on the rate of turn, true airspeed, and time. This function computes the total distance traveled during a constant turn, based on the aircraft's rate of turn, true airspeed, and the duration of the turn. :param rateOfTurn: Rate of turn [deg/s]. :param TAS: True Airspeed (TAS) [m/s]. :param timeOfTurn: Duration of the turn [s]. :type rateOfTurn: float. :type TAS: float. :type timeOfTurn: float. :returns: Distance traveled during the turn [m]. :rtype: float. """ if TAS == 0: return 0 else: bankAngle = airplane.bankAngle(rateOfTurn=rateOfTurn, v=TAS) arcLengthDegrees = ( rateOfTurn * timeOfTurn ) # amount of degrees to do the rotation turnRadius = airplane.turnRadius_bankAngle( v=TAS, ba=bankAngle ) # [m] distance = radians(arcLengthDegrees) * turnRadius # arcLength [m] return distance