#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
mod builder;
mod cg_envelope;
mod fuel_tank;
mod station;
use crate::error::Error;
use crate::fp::MassAndBalance;
use crate::measurements::{Length, Mass};
use crate::{Fuel, FuelType};
pub use builder::AircraftBuilder;
pub use cg_envelope::{CGEnvelope, CGLimit};
pub use fuel_tank::FuelTank;
pub use station::{LoadedStation, Station};
#[derive(Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Aircraft {
registration: String, icao_type: String,
stations: Vec<Station>,
empty_mass: Mass,
empty_balance: Length,
fuel_type: FuelType,
tanks: Vec<FuelTank>,
cg_envelope: CGEnvelope,
notes: Option<String>,
}
impl Aircraft {
pub fn builder() -> AircraftBuilder {
AircraftBuilder::new()
}
pub fn registration(&self) -> &str {
&self.registration
}
pub fn icao_type(&self) -> &str {
&self.icao_type
}
pub fn stations(&self) -> &[Station] {
self.stations.as_slice()
}
pub fn empty_mass(&self) -> &Mass {
&self.empty_mass
}
pub fn empty_balance(&self) -> &Length {
&self.empty_balance
}
pub fn fuel_type(&self) -> &FuelType {
&self.fuel_type
}
pub fn tanks(&self) -> &[FuelTank] {
self.tanks.as_slice()
}
pub fn cg_envelope(&self) -> &CGEnvelope {
&self.cg_envelope
}
pub fn notes(&self) -> Option<&str> {
self.notes.as_deref()
}
pub fn usable_fuel(&self) -> Option<Fuel> {
self.tanks
.iter()
.map(|tank| Fuel::from_volume(*tank.capacity(), self.fuel_type))
.reduce(|acc, fuel| acc + fuel)
}
pub fn is_balanced(&self, mb: &MassAndBalance) -> bool {
self.cg_envelope.contains(mb)
}
pub fn mb(
&self,
mass_on_ramp: &[Mass],
mass_after_landing: &[Mass],
fuel_on_ramp: &[Fuel],
fuel_after_landing: &[Fuel],
) -> Result<MassAndBalance, Error> {
let mut loaded_stations: Vec<LoadedStation> = Vec::new();
loaded_stations.append(&mut self.stations_from_mass(mass_on_ramp, mass_after_landing)?);
loaded_stations.append(&mut self.stations_from_fuel(fuel_on_ramp, fuel_after_landing)?);
Ok(MassAndBalance::new(&loaded_stations))
}
pub fn mb_from_equally_distributed_fuel(
&self,
mass_on_ramp: &[Mass],
mass_after_landing: &[Mass],
on_ramp: &Fuel,
after_landing: &Fuel,
) -> Result<MassAndBalance, Error> {
let n = self.tanks.len();
self.mb(
mass_on_ramp,
mass_after_landing,
&vec![*on_ramp / n; n],
&vec![*after_landing / n; n],
)
}
pub fn mb_from_const_mass_and_equally_distributed_fuel(
&self,
mass: &[Mass],
on_ramp: &Fuel,
after_landing: &Fuel,
) -> Result<MassAndBalance, Error> {
self.mb_from_equally_distributed_fuel(mass, mass, on_ramp, after_landing)
}
fn empty(&self) -> LoadedStation {
LoadedStation {
station: Station::new(self.empty_balance, Some(String::from("Empty Aircraft"))),
on_ramp: self.empty_mass,
after_landing: self.empty_mass,
}
}
fn stations_from_mass(
&self,
on_ramp: &[Mass],
after_landing: &[Mass],
) -> Result<Vec<LoadedStation>, Error> {
let n = self.stations.len();
if on_ramp.len() == n && after_landing.len() == n {
let mut loaded_stations = vec![self.empty()];
for i in 0..n {
loaded_stations.push(LoadedStation {
station: self.stations[i].clone(),
on_ramp: on_ramp[i],
after_landing: after_landing[i],
});
}
Ok(loaded_stations)
} else {
Err(Error::UnexpectedMassesForStations)
}
}
fn stations_from_fuel(
&self,
on_ramp: &[Fuel],
after_landing: &[Fuel],
) -> Result<Vec<LoadedStation>, Error> {
let n = self.tanks.len();
if on_ramp.len() == n && after_landing.len() == n {
let mut loaded_stations: Vec<LoadedStation> = Vec::new();
for i in 0..n {
let fuel_on_ramp = on_ramp[i];
let fuel_after_landing = after_landing[i];
let tank = self.tanks[i];
if &fuel_on_ramp.volume() > tank.capacity() {
return Err(Error::ExceededFuelCapacityOnRamp);
}
if &fuel_after_landing.volume() > tank.capacity() {
return Err(Error::ExceededFuelCapacityAfterLanding);
}
loaded_stations.push(LoadedStation {
station: Station::new(*tank.arm(), None),
on_ramp: fuel_on_ramp.mass,
after_landing: fuel_after_landing.mass,
});
}
Ok(loaded_stations)
} else {
Err(Error::UnexpectedNumberOfFuelStations)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measurements::Volume;
#[test]
fn usable_fuel_matches_tank_capacity() {
let ac = Aircraft {
registration: String::from("N12345"),
icao_type: "ZZZZ".to_string(),
stations: vec![],
empty_mass: Mass::kg(0.0),
empty_balance: Length::m(0.0),
fuel_type: FuelType::Diesel,
tanks: vec![
FuelTank::new(Volume::l(40.0), Length::m(1.0)),
FuelTank::new(Volume::l(40.0), Length::m(1.0)),
],
cg_envelope: CGEnvelope::new(vec![]),
notes: None,
};
assert_eq!(ac.usable_fuel(), Some(diesel!(Volume::l(80.0))));
}
#[test]
#[should_panic(expected = "UnexpectedMassesForStations")]
fn create_stations_with_missing_mass() {
let ac = Aircraft {
registration: String::from("N12345"),
icao_type: "ZZZZ".to_string(),
stations: vec![
Station::new(Length::m(1.0), None),
Station::new(Length::m(2.0), None),
],
empty_mass: Mass::kg(0.0),
empty_balance: Length::m(0.0),
fuel_type: FuelType::Diesel,
tanks: vec![],
cg_envelope: CGEnvelope::new(vec![]),
notes: None,
};
ac.stations_from_mass(&vec![], &vec![]).unwrap();
}
#[test]
fn stations_include_empty_mass() {
let ac = Aircraft {
registration: String::from("N12345"),
icao_type: "ZZZZ".to_string(),
stations: vec![],
empty_mass: Mass::kg(800.0),
empty_balance: Length::m(1.0),
fuel_type: FuelType::Diesel,
tanks: vec![],
cg_envelope: CGEnvelope::new(vec![]),
notes: None,
};
let stations = ac.stations_from_mass(&vec![], &vec![]).unwrap();
assert_eq!(stations.len(), 1);
assert_eq!(stations[0], ac.empty());
}
}