use super::{ComplianceItem, ComplianceResult, DesignResult, LightingStandard, Region};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RoadClass {
Major,
Collector,
Local,
}
impl std::fmt::Display for RoadClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Major => write!(f, "Major"),
Self::Collector => write!(f, "Collector"),
Self::Local => write!(f, "Local"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PedestrianConflict {
High,
Medium,
Low,
}
impl std::fmt::Display for PedestrianConflict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::High => write!(f, "High"),
Self::Medium => write!(f, "Medium"),
Self::Low => write!(f, "Low"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rp8Selection {
pub road_class: RoadClass,
pub pedestrian_conflict: PedestrianConflict,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rp8Criteria {
pub avg_illuminance_lux: f64,
pub max_uniformity_avg_min: f64,
}
impl Rp8Selection {
pub fn criteria(&self) -> Rp8Criteria {
use PedestrianConflict::*;
use RoadClass::*;
let (avg, unif) = match (self.road_class, self.pedestrian_conflict) {
(Major, High) => (17.0, 3.0),
(Major, Medium) => (13.0, 3.0),
(Major, Low) => (9.0, 3.0),
(Collector, High) => (12.0, 4.0),
(Collector, Medium) => (9.0, 4.0),
(Collector, Low) => (6.0, 4.0),
(Local, High) => (9.0, 6.0),
(Local, Medium) => (7.0, 6.0),
(Local, Low) => (4.0, 6.0),
};
Rp8Criteria {
avg_illuminance_lux: avg,
max_uniformity_avg_min: unif,
}
}
pub fn failure_overlay(&self) -> crate::street::FailureOverlay {
let crit = self.criteria();
crate::street::FailureOverlay::ratio(1.0 / crit.max_uniformity_avg_min)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Rp8Standard;
impl LightingStandard for Rp8Standard {
type Selection = Rp8Selection;
fn name(&self) -> &'static str {
"ANSI/IES RP-8 (illuminance)"
}
fn region(&self) -> Region {
Region::Us
}
fn check_design(
&self,
selection: &Self::Selection,
design: &DesignResult,
) -> Option<ComplianceResult> {
let crit = selection.criteria();
let achieved_unif = if design.min_illuminance_lux > 0.0 {
design.avg_illuminance_lux / design.min_illuminance_lux
} else {
f64::INFINITY
};
let items = vec![
ComplianceItem {
parameter: "Average Illuminance".into(),
required: format!("≥ {:.1} lux", crit.avg_illuminance_lux),
achieved: format!("{:.1} lux", design.avg_illuminance_lux),
passed: design.avg_illuminance_lux >= crit.avg_illuminance_lux,
},
ComplianceItem {
parameter: "Uniformity (avg/min)".into(),
required: format!("≤ {:.1}", crit.max_uniformity_avg_min),
achieved: if achieved_unif.is_finite() {
format!("{:.2}", achieved_unif)
} else {
"∞".into()
},
passed: achieved_unif.is_finite() && achieved_unif <= crit.max_uniformity_avg_min,
},
];
Some(ComplianceResult {
standard: format!(
"ANSI/IES RP-8 ({}/{})",
selection.road_class, selection.pedestrian_conflict
)
.into(),
region: self.region(),
items,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn design(avg: f64, min: f64) -> DesignResult {
DesignResult {
avg_illuminance_lux: avg,
min_illuminance_lux: min,
max_illuminance_lux: avg * 1.5,
avg_luminance_cd_m2: None,
uniformity_overall: if avg > 0.0 { min / avg } else { 0.0 },
uniformity_longitudinal: None,
threshold_increment_pct: None,
}
}
#[test]
fn passing_design_on_major_medium() {
let sel = Rp8Selection {
road_class: RoadClass::Major,
pedestrian_conflict: PedestrianConflict::Medium,
};
let d = design(15.0, 7.0); let result = Rp8Standard.check_design(&sel, &d).unwrap();
assert!(result.passed(), "{result:?}");
assert_eq!(result.items.len(), 2);
}
#[test]
fn fails_illuminance_on_major_high() {
let sel = Rp8Selection {
road_class: RoadClass::Major,
pedestrian_conflict: PedestrianConflict::High,
};
let d = design(12.0, 5.0);
let result = Rp8Standard.check_design(&sel, &d).unwrap();
assert!(!result.passed());
assert_eq!(result.failure_count(), 1, "only illuminance fails");
assert!(!result.items[0].passed);
assert!(result.items[1].passed);
}
#[test]
fn fails_uniformity_on_local_low() {
let sel = Rp8Selection {
road_class: RoadClass::Local,
pedestrian_conflict: PedestrianConflict::Low,
};
let d = design(5.0, 0.5);
let result = Rp8Standard.check_design(&sel, &d).unwrap();
assert!(!result.passed());
assert_eq!(result.failure_count(), 1);
assert!(result.items[0].passed);
assert!(!result.items[1].passed);
}
#[test]
fn zero_min_illuminance_reports_infinity_and_fails() {
let sel = Rp8Selection {
road_class: RoadClass::Collector,
pedestrian_conflict: PedestrianConflict::Low,
};
let d = design(7.0, 0.0);
let result = Rp8Standard.check_design(&sel, &d).unwrap();
assert_eq!(result.items[1].achieved, "∞");
assert!(!result.items[1].passed);
}
#[test]
fn check_file_returns_none() {
let sel = Rp8Selection {
road_class: RoadClass::Local,
pedestrian_conflict: PedestrianConflict::Low,
};
let ldt = crate::Eulumdat::default();
assert!(Rp8Standard.check_file(&sel, &ldt).is_none());
}
#[test]
fn end_to_end_with_real_layout() {
use crate::street::{Arrangement, StreetLayout};
let ldt_content =
std::fs::read_to_string("../eulumdat-wasm/templates/road_luminaire.ldt").unwrap();
let ldt = crate::Eulumdat::parse(&ldt_content).unwrap();
let layout = StreetLayout {
arrangement: Arrangement::Staggered,
pole_spacing_m: 30.0,
..Default::default()
};
let area = layout.compute(&ldt, 0.8);
let design = layout.design_result(&area);
let sel = Rp8Selection {
road_class: RoadClass::Local,
pedestrian_conflict: PedestrianConflict::Low,
};
let result = Rp8Standard.check_design(&sel, &design).unwrap();
assert_eq!(result.items.len(), 2);
assert_eq!(result.region, Region::Us);
}
#[test]
fn failure_overlay_is_inverse_of_uniformity_ratio() {
use crate::street::FailureOverlay;
let sel = Rp8Selection {
road_class: RoadClass::Major,
pedestrian_conflict: PedestrianConflict::Medium,
};
match sel.failure_overlay() {
FailureOverlay::RatioFloor { min_over_avg } => {
assert!((min_over_avg - 1.0 / 3.0).abs() < 1e-6, "{min_over_avg}");
}
other => panic!("expected RatioFloor, got {other:?}"),
}
let sel = Rp8Selection {
road_class: RoadClass::Local,
pedestrian_conflict: PedestrianConflict::Low,
};
match sel.failure_overlay() {
FailureOverlay::RatioFloor { min_over_avg } => {
assert!((min_over_avg - 1.0 / 6.0).abs() < 1e-6);
}
other => panic!("expected RatioFloor, got {other:?}"),
}
}
}