use super::{ComplianceItem, ComplianceResult, DesignResult, LightingStandard, Region};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Cjj45Class {
ClassI,
ClassII,
ClassIII,
ClassIV,
}
impl std::fmt::Display for Cjj45Class {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ClassI => write!(f, "Class I (快速路/主干路)"),
Self::ClassII => write!(f, "Class II (次干路)"),
Self::ClassIII => write!(f, "Class III (支路)"),
Self::ClassIV => write!(f, "Class IV (居住区道路)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Cjj45Criteria {
pub avg_illuminance_lux: f64,
pub min_uniformity_min_avg: f64,
}
impl Cjj45Class {
pub fn criteria(self) -> Cjj45Criteria {
let (avg, u0) = match self {
Self::ClassI => (20.0, 0.4),
Self::ClassII => (15.0, 0.4),
Self::ClassIII => (8.0, 0.35),
Self::ClassIV => (5.0, 0.3),
};
Cjj45Criteria {
avg_illuminance_lux: avg,
min_uniformity_min_avg: u0,
}
}
pub fn failure_overlay(self) -> crate::street::FailureOverlay {
crate::street::FailureOverlay::ratio(self.criteria().min_uniformity_min_avg)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Cjj45Standard;
impl LightingStandard for Cjj45Standard {
type Selection = Cjj45Class;
fn name(&self) -> &'static str {
"CJJ 45-2015 (illuminance)"
}
fn region(&self) -> Region {
Region::Cn
}
fn check_design(
&self,
class: &Self::Selection,
design: &DesignResult,
) -> Option<ComplianceResult> {
let crit = class.criteria();
let achieved_u0 = if design.avg_illuminance_lux > 0.0 {
design.min_illuminance_lux / design.avg_illuminance_lux
} else {
0.0
};
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 U₀ (min/avg)".into(),
required: format!("≥ {:.2}", crit.min_uniformity_min_avg),
achieved: format!("{:.2}", achieved_u0),
passed: achieved_u0 >= crit.min_uniformity_min_avg,
},
];
Some(ComplianceResult {
standard: format!("CJJ 45-2015 ({class})").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 class_i_passes_with_high_quality_design() {
let d = design(25.0, 12.0); let result = Cjj45Standard.check_design(&Cjj45Class::ClassI, &d).unwrap();
assert!(result.passed());
assert_eq!(result.region, Region::Cn);
}
#[test]
fn class_iv_fails_on_uniformity_only() {
let d = design(10.0, 2.0); let result = Cjj45Standard
.check_design(&Cjj45Class::ClassIV, &d)
.unwrap();
assert!(!result.passed());
assert_eq!(result.failure_count(), 1);
assert!(result.items[0].passed, "avg illuminance passes");
assert!(!result.items[1].passed, "uniformity fails");
}
#[test]
fn class_criteria_monotonic_by_importance() {
use Cjj45Class::*;
let avgs: Vec<f64> = [ClassI, ClassII, ClassIII, ClassIV]
.iter()
.map(|c| c.criteria().avg_illuminance_lux)
.collect();
for w in avgs.windows(2) {
assert!(
w[0] >= w[1],
"higher-tier class should have higher illuminance target"
);
}
}
#[test]
fn failure_overlay_matches_uniformity_u0() {
use crate::street::FailureOverlay;
match Cjj45Class::ClassI.failure_overlay() {
FailureOverlay::RatioFloor { min_over_avg } => assert_eq!(min_over_avg, 0.4),
other => panic!("expected RatioFloor, got {other:?}"),
}
match Cjj45Class::ClassIV.failure_overlay() {
FailureOverlay::RatioFloor { min_over_avg } => assert_eq!(min_over_avg, 0.3),
other => panic!("expected RatioFloor, got {other:?}"),
}
}
}