///|
/// The wind triangle: what a wind does to an aeroplane.
///
/// An aeroplane flying at a true airspeed through a moving air mass is
/// carried with the air: its path over the ground is the sum of the two
/// velocities. The wind triangle solves that sum for flight planning — given
/// the course to make good, the airspeed to fly and the wind, it gives the
/// heading to steer, the ground speed that results, and the two components a
/// pilot reads a wind in: headwind and crosswind.
///
/// Components follow the aviation convention: a headwind is positive when
/// the wind comes from ahead (a tailwind is a negative headwind), and a
/// crosswind is positive when the wind comes from the right of the track,
/// which is the side a wind correction angle steers into.
///|
/// A flight that cannot be planned: a wind triangle with no solution, or a
/// leg with no forecast or no ground speed to time it by.
pub suberror FlightError {
/// The airspeed must be positive.
BadAirspeed(Double)
/// The wind blows sideways faster than the aeroplane flies, so no heading
/// can hold the course. The payloads are the crosswind and the airspeed.
CrosswindExceedsAirspeed(Double, Double)
/// No level is forecast at this altitude, which is the payload: the station
/// reports its levels only up to the top of the product.
NoForecast(Int32)
/// The wind along the leg beats the aeroplane, leaving a ground speed of
/// zero or less — a headwind stronger than the airspeed — so the leg takes
/// no time that means anything. The payload is the ground speed.
NoGroundSpeed(Double)
}
///|
/// The solution of one wind triangle: the inputs, and the heading, ground
/// speed and wind components they give.
pub struct WindTriangle {
/// The course to make good, in degrees true.
course : Double
/// The true airspeed, in knots.
airspeed : Double
/// The wind, as a vector.
wind : WindVector
/// The heading to steer, in degrees true: the course plus the wind
/// correction angle.
heading : Double
/// The wind correction angle, in degrees: positive when the heading is to
/// the right of the course, steering into a right crosswind.
wind_correction_angle : Double
/// The ground speed, in knots.
ground_speed : Double
/// The wind along the track, in knots: positive against the aircraft, so a
/// tailwind is negative.
headwind : Double
/// The wind across the track, in knots: positive from the right.
crosswind : Double
}
///|
pub fn WindTriangle::course(self : WindTriangle) -> Double {
self.course
}
///|
pub fn WindTriangle::airspeed(self : WindTriangle) -> Double {
self.airspeed
}
///|
pub fn WindTriangle::wind(self : WindTriangle) -> WindVector {
self.wind
}
///|
pub fn WindTriangle::heading(self : WindTriangle) -> Double {
self.heading
}
///|
pub fn WindTriangle::wind_correction_angle(self : WindTriangle) -> Double {
self.wind_correction_angle
}
///|
pub fn WindTriangle::ground_speed(self : WindTriangle) -> Double {
self.ground_speed
}
///|
pub fn WindTriangle::headwind(self : WindTriangle) -> Double {
self.headwind
}
///|
pub fn WindTriangle::crosswind(self : WindTriangle) -> Double {
self.crosswind
}
///|
/// The wind along the direction whose sine and cosine of the course are
/// `sin_course` and `cos_course`, in knots: positive when it comes from ahead,
/// so a tailwind is negative.
fn headwind_of(
wind : WindVector,
sin_course : Double,
cos_course : Double,
) -> Double {
-(wind.u * sin_course + wind.v * cos_course)
}
///|
/// The wind across that direction, in knots: positive when it comes from the
/// right, the side that makes an aircraft steer a positive wind correction
/// angle.
fn crosswind_of(
wind : WindVector,
sin_course : Double,
cos_course : Double,
) -> Double {
wind.v * sin_course - wind.u * cos_course
}
///|
/// The wind along `direction`, in degrees true, in knots.
///
/// Positive when the wind comes from ahead, so a tailwind is negative. A
/// light and variable wind has no direction, and contributes nothing.
pub fn WindVector::headwind_component(
self : WindVector,
direction : Double,
) -> Double {
let radians = direction * @math.PI / 180.0
headwind_of(self, @math.sin(radians), @math.cos(radians))
}
///|
/// The wind across `direction`, in degrees true, in knots.
///
/// Positive when the wind comes from the right, the side that makes an
/// aircraft steer a positive wind correction angle.
pub fn WindVector::crosswind_component(
self : WindVector,
direction : Double,
) -> Double {
let radians = direction * @math.PI / 180.0
crosswind_of(self, @math.sin(radians), @math.cos(radians))
}
///|
/// Bring degrees into `[0, 360)`.
fn normalize_degrees(degrees : Double) -> Double {
let mut out = degrees
while out < 0.0 {
out += 360.0
}
while out >= 360.0 {
out -= 360.0
}
out
}
///|
/// Solve the wind triangle.
///
/// The heading to steer is the course plus the wind correction angle, which
/// is read off the two components of the wind along the track: the angle
/// whose sine is the crosswind over the airspeed. When the crosswind exceeds
/// the airspeed there is no such angle, and the triangle cannot be flown.
pub fn WindTriangle::solve(
course : Double,
airspeed : Double,
wind : WindVector,
) -> WindTriangle raise FlightError {
if airspeed <= 0.0 {
raise FlightError::BadAirspeed(airspeed)
}
// One turn of the course is all the triangle needs of it: both components
// and the correction angle are read off the same sine and cosine.
let radians = course * @math.PI / 180.0
let sin_course = @math.sin(radians)
let cos_course = @math.cos(radians)
let crosswind = crosswind_of(wind, sin_course, cos_course)
let headwind = headwind_of(wind, sin_course, cos_course)
let along = airspeed * airspeed - crosswind * crosswind
if along < 0.0 {
raise FlightError::CrosswindExceedsAirspeed(crosswind, airspeed)
}
let across = along.sqrt()
let wind_correction_angle = @math.atan2(crosswind, across) * 180.0 / @math.PI
let heading = normalize_degrees(course + wind_correction_angle)
let ground_speed = across - headwind
{
course,
airspeed,
wind,
heading,
wind_correction_angle,
ground_speed,
headwind,
crosswind,
}
}