use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::vessel::Vessel;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum MassCategory {
Lightship,
Deadweight,
#[default]
Other,
}
impl std::fmt::Display for MassCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MassCategory::Lightship => write!(f, "Lightship"),
MassCategory::Deadweight => write!(f, "Deadweight"),
MassCategory::Other => write!(f, "Other"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MassItem {
pub name: String,
pub mass: f64,
pub cog: [f64; 3],
#[serde(default)]
pub category: MassCategory,
}
impl MassItem {
pub fn new(name: &str, mass: f64, cog: [f64; 3]) -> Self {
Self {
name: name.to_string(),
mass,
cog,
category: MassCategory::default(),
}
}
pub fn with_category(mut self, category: MassCategory) -> Self {
self.category = category;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadingCondition {
pub name: String,
masses: Vec<MassItem>,
tank_fills: HashMap<String, f64>,
}
impl LoadingCondition {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
masses: Vec::new(),
tank_fills: HashMap::new(),
}
}
pub fn add_mass(&mut self, item: MassItem) {
self.masses.push(item);
}
pub fn add_mass_simple(&mut self, name: &str, mass: f64, cog: [f64; 3]) {
self.masses.push(MassItem::new(name, mass, cog));
}
pub fn remove_mass(&mut self, name: &str) -> bool {
if let Some(idx) = self.masses.iter().position(|m| m.name == name) {
self.masses.remove(idx);
true
} else {
false
}
}
pub fn masses(&self) -> &[MassItem] {
&self.masses
}
pub fn num_masses(&self) -> usize {
self.masses.len()
}
pub fn set_tank_fill(&mut self, tank_name: &str, fill_level: f64) {
self.tank_fills
.insert(tank_name.to_string(), fill_level.clamp(0.0, 1.0));
}
pub fn set_tank_fill_percent(&mut self, tank_name: &str, fill_percent: f64) {
self.set_tank_fill(tank_name, fill_percent / 100.0);
}
pub fn remove_tank_fill(&mut self, tank_name: &str) -> bool {
self.tank_fills.remove(tank_name).is_some()
}
pub fn tank_fills(&self) -> &HashMap<String, f64> {
&self.tank_fills
}
pub fn num_tank_overrides(&self) -> usize {
self.tank_fills.len()
}
pub fn apply(&self, vessel: &Vessel) {
for (tank_name, &fill_level) in &self.tank_fills {
if let Some(tank) = vessel.get_tank_by_name(tank_name) {
tank.write().unwrap().set_fill_level(fill_level);
}
}
}
pub fn save_tank_fills(vessel: &Vessel) -> HashMap<String, f64> {
let mut saved = HashMap::new();
for tank_arc in vessel.tanks() {
let tank = tank_arc.read().unwrap();
saved.insert(tank.name().to_string(), tank.fill_level());
}
saved
}
pub fn restore_tank_fills(vessel: &Vessel, saved: &HashMap<String, f64>) {
for tank_arc in vessel.tanks() {
let mut tank = tank_arc.write().unwrap();
if let Some(&fill) = saved.get(tank.name()) {
tank.set_fill_level(fill);
}
}
}
pub fn item_displacement(&self) -> f64 {
self.masses.iter().map(|m| m.mass).sum()
}
pub fn total_displacement(&self, vessel: &Vessel) -> f64 {
let masses_total = self.item_displacement();
let tanks_total: f64 = vessel.get_total_tanks_mass();
masses_total + tanks_total
}
pub fn total_cog(&self, vessel: &Vessel) -> [f64; 3] {
let total_disp = self.total_displacement(vessel);
if total_disp <= 0.0 {
return [0.0, 0.0, 0.0];
}
let mut moment = [0.0f64; 3];
for m in &self.masses {
moment[0] += m.mass * m.cog[0];
moment[1] += m.mass * m.cog[1];
moment[2] += m.mass * m.cog[2];
}
for tank_arc in vessel.tanks() {
let tank = tank_arc.read().unwrap();
let mass = tank.fluid_mass();
if mass > 0.0 {
let cog = tank.center_of_gravity();
moment[0] += mass * cog[0];
moment[1] += mass * cog[1];
moment[2] += mass * cog[2];
}
}
[
moment[0] / total_disp,
moment[1] / total_disp,
moment[2] / total_disp,
]
}
pub fn item_cog(&self) -> [f64; 3] {
let disp = self.item_displacement();
if disp <= 0.0 {
return [0.0, 0.0, 0.0];
}
let mut moment = [0.0f64; 3];
for m in &self.masses {
moment[0] += m.mass * m.cog[0];
moment[1] += m.mass * m.cog[1];
moment[2] += m.mass * m.cog[2];
}
[moment[0] / disp, moment[1] / disp, moment[2] / disp]
}
pub fn resolve_items(&self) -> (f64, [f64; 3]) {
(self.item_displacement(), self.item_cog())
}
pub fn resolve(&self, vessel: &Vessel) -> (f64, [f64; 3]) {
(self.total_displacement(vessel), self.total_cog(vessel))
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_json_file(&self, path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let json = self.to_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn from_json_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
let json = std::fs::read_to_string(path)?;
let lc = Self::from_json(&json)?;
Ok(lc)
}
pub fn from_csv(csv_content: &str) -> Result<Self, Box<dyn std::error::Error>> {
#[derive(Deserialize)]
struct CsvRow {
#[serde(rename = "Type")]
item_type: String,
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Mass")]
mass: Option<f64>,
#[serde(rename = "LCG")]
lcg: Option<f64>,
#[serde(rename = "TCG")]
tcg: Option<f64>,
#[serde(rename = "VCG")]
vcg: Option<f64>,
#[serde(rename = "Category")]
category: Option<String>,
#[serde(rename = "FillPercent")]
fill_percent: Option<f64>,
}
let mut lc = Self::new("Imported Loading Condition");
let mut rdr = csv::Reader::from_reader(csv_content.as_bytes());
for result in rdr.deserialize() {
let row: CsvRow = result?;
match row.item_type.to_lowercase().as_str() {
"mass" => {
let mass = row.mass.unwrap_or(0.0);
let lcg = row.lcg.unwrap_or(0.0);
let tcg = row.tcg.unwrap_or(0.0);
let vcg = row.vcg.unwrap_or(0.0);
let mut item = MassItem::new(&row.name, mass, [lcg, tcg, vcg]);
if let Some(cat_str) = row.category {
let cat = match cat_str.to_lowercase().as_str() {
"lightship" => MassCategory::Lightship,
"deadweight" => MassCategory::Deadweight,
_ => MassCategory::Other,
};
item = item.with_category(cat);
}
lc.add_mass(item);
}
"tank" => {
if let Some(fill) = row.fill_percent {
lc.set_tank_fill_percent(&row.name, fill);
}
}
_ => { }
}
}
Ok(lc)
}
pub fn from_csv_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let mut lc = Self::from_csv(&content)?;
if let Some(file_stem) = path.file_stem() {
if let Some(name_str) = file_stem.to_str() {
lc.name = name_str.to_string();
}
}
Ok(lc)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_csv() {
let csv_data = "Type,Name,Mass,LCG,TCG,VCG,Category,FillPercent
Mass,Lightship,1000.0,10.0,0.0,2.0,Lightship,
Mass,Cargo,500.0,15.0,0.0,3.0,Deadweight,
Tank,Tank_1,,,,,,85.5
Tank,Tank_2,,,,,,10.0";
let lc = LoadingCondition::from_csv(csv_data).unwrap();
assert_eq!(lc.name, "Imported Loading Condition");
assert_eq!(lc.num_masses(), 2);
assert_eq!(lc.num_tank_overrides(), 2);
assert_eq!(lc.masses()[0].name, "Lightship");
assert_eq!(lc.masses()[0].mass, 1000.0);
assert_eq!(lc.masses()[0].cog, [10.0, 0.0, 2.0]);
assert_eq!(lc.masses()[0].category, MassCategory::Lightship);
assert_eq!(lc.masses()[1].name, "Cargo");
assert_eq!(lc.masses()[1].mass, 500.0);
assert_eq!(lc.masses()[1].cog, [15.0, 0.0, 3.0]);
assert_eq!(lc.masses()[1].category, MassCategory::Deadweight);
let tanks = lc.tank_fills();
assert_eq!(tanks.get("Tank_1"), Some(&0.855)); assert_eq!(tanks.get("Tank_2"), Some(&0.1));
}
}