use crate::ifc::Storey;
pub const STOREY_ELEVATION_TOLERANCE_FEET: f64 = 1e-3;
pub const MIN_DISTINCT_ELEVATIONS: usize = 2;
fn quantise(value: f64) -> i64 {
(value * 10_000.0).round() as i64
}
pub fn distinct_base_elevations_feet(values: impl IntoIterator<Item = f64>) -> Vec<f64> {
let mut keys: Vec<i64> = values
.into_iter()
.filter(|v| v.is_finite())
.map(quantise)
.collect();
keys.sort_unstable();
keys.dedup();
keys.into_iter().map(|key| key as f64 / 10_000.0).collect()
}
pub fn elevation_storey_name(elevation_feet: f64) -> String {
format!("Elevation {elevation_feet:.3} ft")
}
pub fn storeys_lack_elevation_evidence(storeys: &[Storey]) -> bool {
let mut iter = storeys.iter().map(|s| quantise(s.elevation_feet));
let Some(first) = iter.next() else {
return true;
};
iter.all(|key| key == first)
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ElementRecordStoreyRecovery {
pub storeys: Vec<Storey>,
pub named_from_levels: usize,
pub unplaced_level_names: Vec<String>,
}
pub fn storeys_from_base_elevations(
elevations_feet: &[f64],
level_names: &[String],
) -> ElementRecordStoreyRecovery {
if elevations_feet.len() < MIN_DISTINCT_ELEVATIONS {
return ElementRecordStoreyRecovery::default();
}
let mut storeys: Vec<Storey> = elevations_feet
.iter()
.map(|&elevation_feet| Storey {
name: elevation_storey_name(elevation_feet),
elevation_feet,
})
.collect();
if level_names.len() == storeys.len() {
let ordered = crate::partition_arc_walls::order_building_storey_names(level_names);
for (storey, name) in storeys.iter_mut().zip(ordered) {
storey.name = name;
}
return ElementRecordStoreyRecovery {
named_from_levels: storeys.len(),
storeys,
unplaced_level_names: Vec::new(),
};
}
ElementRecordStoreyRecovery {
storeys,
named_from_levels: 0,
unplaced_level_names: level_names.to_vec(),
}
}
pub fn unique_storey_index_for_elevation(storeys: &[Storey], elevation_feet: f64) -> Option<usize> {
let mut hit = None;
for (index, storey) in storeys.iter().enumerate() {
if (storey.elevation_feet - elevation_feet).abs() < STOREY_ELEVATION_TOLERANCE_FEET {
if hit.is_some() {
return None;
}
hit = Some(index);
}
}
hit
}
#[cfg(test)]
mod tests {
use super::*;
fn storey(name: &str, elevation_feet: f64) -> Storey {
Storey {
name: name.to_string(),
elevation_feet,
}
}
#[test]
fn distinct_elevations_are_deduplicated_and_sorted() {
let got = distinct_base_elevations_feet([76.0, 0.0, 31.0, 76.0, 0.0, 151.0]);
assert_eq!(got, vec![0.0, 31.0, 76.0, 151.0]);
}
#[test]
fn non_finite_elevations_are_dropped() {
let got = distinct_base_elevations_feet([0.0, f64::NAN, f64::INFINITY, 15.0]);
assert_eq!(got, vec![0.0, 15.0]);
}
#[test]
fn a_single_elevation_is_not_a_distribution() {
let recovery = storeys_from_base_elevations(&[0.0], &[]);
assert!(recovery.storeys.is_empty());
}
#[test]
fn elevations_become_storeys_named_after_their_elevation() {
let recovery = storeys_from_base_elevations(&[0.0, 31.0, 46.0], &[]);
assert_eq!(recovery.storeys.len(), 3);
assert_eq!(recovery.storeys[1].name, "Elevation 31.000 ft");
assert_eq!(recovery.storeys[1].elevation_feet, 31.0);
assert_eq!(recovery.named_from_levels, 0);
}
#[test]
fn level_names_transfer_only_when_the_counts_match() {
let names = vec!["Level 2".to_string(), "Level 1".to_string()];
let matched = storeys_from_base_elevations(&[0.0, 15.0], &names);
assert_eq!(matched.named_from_levels, 2);
assert_eq!(matched.storeys[0].name, "Level 1");
assert_eq!(matched.storeys[1].name, "Level 2");
assert!(matched.unplaced_level_names.is_empty());
let mismatched = storeys_from_base_elevations(&[0.0, 15.0, 30.0], &names);
assert_eq!(mismatched.named_from_levels, 0);
assert_eq!(mismatched.storeys[0].name, "Elevation 0.000 ft");
assert_eq!(mismatched.unplaced_level_names, names);
}
#[test]
fn elevation_join_is_exact_and_fails_closed() {
let storeys = vec![storey("A", 0.0), storey("B", 76.0)];
assert_eq!(unique_storey_index_for_elevation(&storeys, 76.0), Some(1));
assert_eq!(
unique_storey_index_for_elevation(&storeys, 76.000_5),
Some(1)
);
assert_eq!(unique_storey_index_for_elevation(&storeys, 75.0), None);
let dense = vec![storey("A", 76.0), storey("B", 76.000_1)];
assert_eq!(unique_storey_index_for_elevation(&dense, 76.0), None);
}
#[test]
fn elevation_evidence_detection() {
assert!(storeys_lack_elevation_evidence(&[]));
assert!(storeys_lack_elevation_evidence(&[
storey("A", 0.0),
storey("B", 0.0)
]));
assert!(!storeys_lack_elevation_evidence(&[
storey("A", 0.0),
storey("B", 15.0)
]));
}
}