use crate::atla::types::*;
use crate::{Eulumdat, LampSet, Symmetry as EulumdatSymmetry, TypeIndicator};
impl From<&Eulumdat> for LuminaireOpticalData {
fn from(ldt: &Eulumdat) -> Self {
let mut doc = LuminaireOpticalData::new();
doc.header = Header {
manufacturer: if ldt.identification.is_empty() {
None
} else {
Some(ldt.identification.clone())
},
catalog_number: if ldt.luminaire_number.is_empty() {
None
} else {
Some(ldt.luminaire_number.clone())
},
description: if ldt.luminaire_name.is_empty() {
None
} else {
Some(ldt.luminaire_name.clone())
},
laboratory: if ldt.identification.is_empty() {
None
} else {
Some(ldt.identification.clone())
},
report_number: if ldt.measurement_report_number.is_empty() {
None
} else {
Some(ldt.measurement_report_number.clone())
},
test_date: if ldt.date_user.is_empty() {
None
} else {
Some(ldt.date_user.clone())
},
report_date: if ldt.date_user.is_empty() {
None
} else {
Some(ldt.date_user.clone())
},
..Default::default()
};
if ldt.length > 0.0 || ldt.width > 0.0 || ldt.height > 0.0 {
let shape = if ldt.width == 0.0 {
LuminousOpeningShape::Circular
} else {
LuminousOpeningShape::Rectangular
};
doc.luminaire = Some(Luminaire {
dimensions: Some(Dimensions {
length: ldt.length,
width: ldt.width,
height: ldt.height,
}),
luminous_openings: vec![LuminousOpening {
shape,
dimensions: OpeningDimensions {
length: ldt.luminous_area_length,
width: if ldt.luminous_area_width > 0.0 {
Some(ldt.luminous_area_width)
} else {
None
},
},
position: None,
}],
mounting: None,
num_emitters: Some(ldt.lamp_sets.iter().map(|ls| ls.num_lamps as u32).sum()),
});
}
if ldt.lamp_sets.len() <= 1 {
let emitter = create_emitter_from_ldt(ldt);
doc.emitters.push(emitter);
} else {
for (i, ls) in ldt.lamp_sets.iter().enumerate() {
let emitter =
create_emitter_from_lamp_set(ls, if i == 0 { Some(ldt) } else { None });
doc.emitters.push(emitter);
}
}
doc
}
}
fn create_emitter_from_lamp_set(ls: &LampSet, ldt_for_intensity: Option<&Eulumdat>) -> Emitter {
let cct = parse_cct(&ls.color_appearance);
let color_rendering = parse_cri(&ls.color_rendering_group).map(|ra| ColorRendering {
ra: Some(ra),
r9: None,
rf: None,
rg: None,
});
let intensity_distribution = ldt_for_intensity.and_then(|ldt| {
if ldt.intensities.is_empty() {
None
} else {
let num_intensity_rows = ldt.intensities.len();
let horizontal_angles = if ldt.c_angles.len() == num_intensity_rows {
ldt.c_angles.clone()
} else {
ldt.c_angles[..num_intensity_rows.min(ldt.c_angles.len())].to_vec()
};
Some(IntensityDistribution {
photometry_type: PhotometryType::TypeC,
metric: IntensityMetric::Luminous,
units: IntensityUnits::CandelaPerKilolumen,
horizontal_angles,
vertical_angles: ldt.g_angles.clone(),
intensities: ldt.intensities.clone(),
..Default::default()
})
}
});
Emitter {
description: if ls.lamp_type.is_empty() {
None
} else {
Some(ls.lamp_type.clone())
},
quantity: ls.num_lamps as u32,
rated_lumens: Some(ls.total_luminous_flux),
measured_lumens: Some(ls.total_luminous_flux),
input_watts: Some(ls.wattage_with_ballast),
cct,
color_rendering,
intensity_distribution,
..Default::default()
}
}
fn create_emitter_from_ldt(ldt: &Eulumdat) -> Emitter {
let total_lumens: f64 = ldt.lamp_sets.iter().map(|ls| ls.total_luminous_flux).sum();
let total_watts: f64 = ldt.lamp_sets.iter().map(|ls| ls.wattage_with_ballast).sum();
let cct = ldt
.lamp_sets
.first()
.and_then(|ls| parse_cct(&ls.color_appearance));
let color_rendering = ldt.lamp_sets.first().and_then(|ls| {
parse_cri(&ls.color_rendering_group).map(|ra| ColorRendering {
ra: Some(ra),
r9: None,
rf: None,
rg: None,
})
});
let intensity_distribution = if !ldt.intensities.is_empty() {
let num_intensity_rows = ldt.intensities.len();
let horizontal_angles = if ldt.c_angles.len() == num_intensity_rows {
ldt.c_angles.clone()
} else {
ldt.c_angles[..num_intensity_rows.min(ldt.c_angles.len())].to_vec()
};
Some(IntensityDistribution {
photometry_type: PhotometryType::TypeC,
metric: IntensityMetric::Luminous,
units: IntensityUnits::CandelaPerKilolumen,
horizontal_angles,
vertical_angles: ldt.g_angles.clone(),
intensities: ldt.intensities.clone(),
..Default::default()
})
} else {
None
};
let description = if ldt.lamp_sets.is_empty() {
None
} else {
let lamp_desc: Vec<String> = ldt
.lamp_sets
.iter()
.filter(|ls| !ls.lamp_type.is_empty())
.map(|ls| format!("{}x {}", ls.num_lamps, ls.lamp_type))
.collect();
if lamp_desc.is_empty() {
None
} else {
Some(lamp_desc.join(", "))
}
};
Emitter {
id: None,
description,
quantity: ldt
.lamp_sets
.iter()
.map(|ls| ls.num_lamps as u32)
.sum::<u32>()
.max(1),
rated_lumens: if total_lumens > 0.0 {
Some(total_lumens)
} else {
None
},
measured_lumens: None,
input_watts: if total_watts > 0.0 {
Some(total_watts)
} else {
None
},
power_factor: None,
cct,
color_rendering,
sp_ratio: None,
data_generation: Some(DataGeneration {
source: DataSource::Measured,
scaled: false,
interpolated: false,
software: None,
uncertainty: None,
}),
intensity_distribution,
spectral_distribution: None,
..Default::default()
}
}
impl From<&LuminaireOpticalData> for Eulumdat {
fn from(doc: &LuminaireOpticalData) -> Self {
let mut ldt = Eulumdat::default();
if let Some(ref mfr) = doc.header.manufacturer {
ldt.identification = mfr.clone();
}
if let Some(ref cat) = doc.header.catalog_number {
ldt.luminaire_number = cat.clone();
}
if let Some(ref desc) = doc.header.description {
ldt.luminaire_name = desc.clone();
}
if let Some(ref report) = doc.header.report_number {
ldt.measurement_report_number = report.clone();
}
if let Some(ref date) = doc.header.test_date {
ldt.date_user = date.clone();
}
if let Some(ref luminaire) = doc.luminaire {
if let Some(ref dims) = luminaire.dimensions {
ldt.length = dims.length;
ldt.width = dims.width;
ldt.height = dims.height;
}
if let Some(opening) = luminaire.luminous_openings.first() {
ldt.luminous_area_length = opening.dimensions.length;
ldt.luminous_area_width = opening.dimensions.width.unwrap_or(0.0);
}
}
for emitter in &doc.emitters {
let lamp_set = LampSet {
num_lamps: emitter.quantity as i32,
lamp_type: emitter.description.clone().unwrap_or_default(),
total_luminous_flux: emitter
.measured_lumens
.or(emitter.rated_lumens)
.unwrap_or(0.0),
color_appearance: emitter
.cct
.map(|cct| format!("{}K", cct as i32))
.unwrap_or_default(),
color_rendering_group: emitter
.color_rendering
.as_ref()
.and_then(|cr| cr.ra)
.map(cri_to_group)
.unwrap_or_default(),
wattage_with_ballast: emitter.input_watts.unwrap_or(0.0),
};
ldt.lamp_sets.push(lamp_set);
}
if let Some(emitter) = doc
.emitters
.iter()
.find(|e| e.intensity_distribution.is_some())
{
if let Some(ref dist) = emitter.intensity_distribution {
ldt.c_angles = dist.horizontal_angles.clone();
ldt.g_angles = dist.vertical_angles.clone();
ldt.intensities = dist.intensities.clone();
ldt.num_c_planes = if dist.horizontal_angles.len() > 1 {
dist.horizontal_angles.len()
} else {
1
};
ldt.num_g_planes = dist.vertical_angles.len();
if dist.horizontal_angles.len() > 1 {
ldt.c_plane_distance = dist.horizontal_angles[1] - dist.horizontal_angles[0];
}
if dist.vertical_angles.len() > 1 {
ldt.g_plane_distance = dist.vertical_angles[1] - dist.vertical_angles[0];
}
ldt.symmetry = determine_symmetry(&dist.horizontal_angles);
let step = ldt.c_plane_distance;
if step > 0.0 {
let full_count = match ldt.symmetry {
EulumdatSymmetry::BothPlanes => (360.0 / step) as usize,
EulumdatSymmetry::PlaneC0C180 | EulumdatSymmetry::PlaneC90C270 => {
(360.0 / step) as usize
}
_ => dist.horizontal_angles.len(),
};
if full_count > dist.horizontal_angles.len() {
ldt.c_angles = (0..full_count).map(|i| i as f64 * step).collect();
ldt.num_c_planes = full_count;
}
}
}
}
if !ldt.intensities.is_empty() && !ldt.g_angles.is_empty() {
let (dff, lor) = calculate_flux_fractions(&ldt);
ldt.downward_flux_fraction = dff;
ldt.light_output_ratio = lor;
ldt.direct_ratios =
crate::PhotometricCalculations::calculate_direct_ratios(&ldt, "1.25");
}
ldt.type_indicator = if ldt.width == 0.0 {
TypeIndicator::PointSourceSymmetric
} else if ldt.length > ldt.width * 2.0 {
TypeIndicator::Linear
} else {
TypeIndicator::PointSourceOther
};
ldt
}
}
fn parse_cct(color_appearance: &str) -> Option<f64> {
if color_appearance.is_empty() {
return None;
}
let s = color_appearance.trim();
let lower = s.to_lowercase();
if lower == "n/a" || lower == "na" || lower == "none" || lower == "unknown" || lower == "-" {
return None;
}
let numbers: Vec<f64> = extract_numbers(s);
for &num in &numbers {
if (1000.0..=20000.0).contains(&num) {
return Some(num);
}
}
if lower.contains("warm") || lower.contains("ww") || lower.starts_with("ww") {
return Some(2700.0);
}
if lower.contains("neutral") || lower.contains("nw") || lower.starts_with("nw") {
return Some(4000.0);
}
if lower.contains("cool")
|| lower.contains("cold")
|| lower.contains("cw")
|| lower.starts_with("cw")
{
return Some(5000.0);
}
if lower.contains("daylight") || lower.contains("tw") || lower.starts_with("tw") {
return Some(6500.0);
}
None
}
fn extract_numbers(s: &str) -> Vec<f64> {
let mut numbers = Vec::new();
let mut current = String::new();
let mut has_dot = false;
for c in s.chars() {
if c.is_ascii_digit() {
current.push(c);
} else if c == '.' && !has_dot && !current.is_empty() {
current.push(c);
has_dot = true;
} else if !current.is_empty() {
if let Ok(num) = current.trim_end_matches('.').parse::<f64>() {
numbers.push(num);
}
current.clear();
has_dot = false;
}
}
if !current.is_empty() {
if let Ok(num) = current.trim_end_matches('.').parse::<f64>() {
numbers.push(num);
}
}
numbers
}
fn parse_cri(color_rendering_group: &str) -> Option<f64> {
if color_rendering_group.is_empty() {
return None;
}
let s = color_rendering_group.trim();
let lower = s.to_lowercase();
if lower == "n/a" || lower == "na" || lower == "none" || lower == "unknown" || lower == "-" {
return None;
}
let upper = s.to_uppercase();
let numbers = extract_numbers(s);
let cri_candidates: Vec<f64> = numbers
.iter()
.copied()
.filter(|&n| (20.0..=100.0).contains(&n))
.collect();
if let Some(&cri) = cri_candidates
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
{
return Some(cri);
}
for part in upper.split(&['/', '-', ' ', ':', '(', ')'][..]) {
let part = part.trim();
match part {
"1A" => return Some(90.0),
"1B" => return Some(80.0),
"2A" | "2" => return Some(70.0),
"2B" => return Some(60.0),
"3" => return Some(50.0),
"4" => return Some(40.0),
_ => {}
}
}
if upper.starts_with("1A") {
return Some(90.0);
}
if upper.starts_with("1B") {
return Some(80.0);
}
if upper.starts_with("2A") {
return Some(70.0);
}
if upper.starts_with("2B") {
return Some(60.0);
}
None
}
fn cri_to_group(cri: f64) -> String {
match cri as i32 {
90..=100 => "1A".to_string(),
80..=89 => "1B".to_string(),
70..=79 => "2A".to_string(),
60..=69 => "2B".to_string(),
40..=59 => "3".to_string(),
_ => "4".to_string(),
}
}
fn determine_symmetry(horizontal_angles: &[f64]) -> EulumdatSymmetry {
if horizontal_angles.len() <= 1 {
return EulumdatSymmetry::VerticalAxis;
}
let max_angle = horizontal_angles.iter().copied().fold(0.0_f64, f64::max);
let min_angle = horizontal_angles.iter().copied().fold(360.0_f64, f64::min);
if (max_angle - min_angle) < 1.0 {
EulumdatSymmetry::VerticalAxis
} else if max_angle <= 90.5 {
EulumdatSymmetry::BothPlanes
} else if max_angle <= 180.5 {
if min_angle < 0.5 {
EulumdatSymmetry::PlaneC0C180
} else {
EulumdatSymmetry::PlaneC90C270
}
} else {
EulumdatSymmetry::None
}
}
fn calculate_flux_fractions(ldt: &Eulumdat) -> (f64, f64) {
let mut downward_flux = 0.0;
let mut total_flux = 0.0;
for (g_idx, &g_angle) in ldt.g_angles.iter().enumerate() {
let sin_g = g_angle.to_radians().sin();
for c_plane in &ldt.intensities {
if let Some(&intensity) = c_plane.get(g_idx) {
let flux_contribution = intensity * sin_g;
total_flux += flux_contribution;
if g_angle <= 90.0 {
downward_flux += flux_contribution;
}
}
}
}
let dff = if total_flux > 0.0 {
(downward_flux / total_flux * 100.0).min(100.0)
} else {
0.0
};
let lor = 100.0;
(dff, lor)
}
impl LuminaireOpticalData {
pub fn from_eulumdat(ldt: &Eulumdat) -> Self {
ldt.into()
}
pub fn to_eulumdat(&self) -> Eulumdat {
self.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cct_parsing() {
assert_eq!(parse_cct("3000K"), Some(3000.0));
assert_eq!(parse_cct("4000"), Some(4000.0));
assert_eq!(parse_cct("3000.0"), Some(3000.0));
assert_eq!(parse_cct("tw/6500"), Some(6500.0));
assert_eq!(parse_cct("ww/2700"), Some(2700.0));
assert_eq!(parse_cct("cw/5000"), Some(5000.0));
assert_eq!(parse_cct("TW-6500"), Some(6500.0));
assert_eq!(parse_cct("warm white"), Some(2700.0));
assert_eq!(parse_cct("daylight"), Some(6500.0));
assert_eq!(parse_cct("neutral"), Some(4000.0));
assert_eq!(parse_cct("LED 3000K"), Some(3000.0));
assert_eq!(parse_cct("3000K LED"), Some(3000.0));
assert_eq!(parse_cct("CCT:4000"), Some(4000.0));
assert_eq!(parse_cct("CT3000"), Some(3000.0));
assert_eq!(parse_cct(""), None);
assert_eq!(parse_cct("n/a"), None);
assert_eq!(parse_cct("none"), None);
assert_eq!(parse_cct("-"), None);
}
#[test]
fn test_cri_parsing() {
assert_eq!(parse_cri("1A"), Some(90.0));
assert_eq!(parse_cri("1B"), Some(80.0));
assert_eq!(parse_cri("2A"), Some(70.0));
assert_eq!(parse_cri("1B/86"), Some(86.0));
assert_eq!(parse_cri("1A/95"), Some(95.0));
assert_eq!(parse_cri("1A-95"), Some(95.0));
assert_eq!(parse_cri("80"), Some(80.0));
assert_eq!(parse_cri("Ra>90"), Some(90.0));
assert_eq!(parse_cri("CRI 85"), Some(85.0));
assert_eq!(parse_cri("Ra80"), Some(80.0));
assert_eq!(parse_cri("R80"), Some(80.0));
assert_eq!(parse_cri(">80"), Some(80.0));
assert_eq!(parse_cri(">=90"), Some(90.0));
assert_eq!(parse_cri("1Bxyz"), Some(80.0));
assert_eq!(parse_cri(""), None);
assert_eq!(parse_cri("n/a"), None);
assert_eq!(parse_cri("none"), None);
}
#[test]
fn test_cri_to_group() {
assert_eq!(cri_to_group(95.0), "1A");
assert_eq!(cri_to_group(85.0), "1B");
assert_eq!(cri_to_group(75.0), "2A");
}
}
use crate::atla::error::{AtlaError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConversionPolicy {
Strict,
#[default]
Compatible,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConversionAction {
Preserved,
DefaultApplied,
Renamed,
TypeConverted,
Dropped,
Warning,
}
#[derive(Debug, Clone)]
pub struct ConversionLogEntry {
pub field: String,
pub action: ConversionAction,
pub original_value: Option<String>,
pub new_value: Option<String>,
pub message: String,
}
impl ConversionLogEntry {
fn new(field: &str, action: ConversionAction, message: &str) -> Self {
Self {
field: field.to_string(),
action,
original_value: None,
new_value: None,
message: message.to_string(),
}
}
fn with_values(mut self, original: Option<&str>, new: Option<&str>) -> Self {
self.original_value = original.map(|s| s.to_string());
self.new_value = new.map(|s| s.to_string());
self
}
}
pub fn atla_to_tm33(
doc: &LuminaireOpticalData,
policy: ConversionPolicy,
) -> Result<(LuminaireOpticalData, Vec<ConversionLogEntry>)> {
let mut converted = doc.clone();
let mut log = Vec::new();
converted.schema_version = SchemaVersion::Tm3323;
converted.version = "1.1".to_string();
if converted.header.description.is_none() {
match policy {
ConversionPolicy::Strict => {
return Err(AtlaError::MissingElement(
"Header.Description is required in TM-33-23".to_string(),
));
}
ConversionPolicy::Compatible => {
converted.header.description = Some("Not specified".to_string());
log.push(
ConversionLogEntry::new(
"Header.Description",
ConversionAction::DefaultApplied,
"Applied default value for required field",
)
.with_values(None, Some("Not specified")),
);
}
}
}
if converted.header.laboratory.is_none() {
match policy {
ConversionPolicy::Strict => {
return Err(AtlaError::MissingElement(
"Header.Laboratory is required in TM-33-23".to_string(),
));
}
ConversionPolicy::Compatible => {
converted.header.laboratory = Some("Not specified".to_string());
log.push(
ConversionLogEntry::new(
"Header.Laboratory",
ConversionAction::DefaultApplied,
"Applied default value for required field",
)
.with_values(None, Some("Not specified")),
);
}
}
}
if converted.header.report_number.is_none() {
match policy {
ConversionPolicy::Strict => {
return Err(AtlaError::MissingElement(
"Header.ReportNumber is required in TM-33-23".to_string(),
));
}
ConversionPolicy::Compatible => {
converted.header.report_number = Some("UNKNOWN".to_string());
log.push(
ConversionLogEntry::new(
"Header.ReportNumber",
ConversionAction::DefaultApplied,
"Applied default value for required field",
)
.with_values(None, Some("UNKNOWN")),
);
}
}
}
if converted.header.report_date.is_none() {
if let Some(ref test_date) = converted.header.test_date {
if let Some(date) = try_parse_date(test_date) {
converted.header.report_date = Some(date.clone());
log.push(
ConversionLogEntry::new(
"Header.ReportDate",
ConversionAction::TypeConverted,
"Converted from TestDate",
)
.with_values(Some(test_date), Some(&date)),
);
}
}
if converted.header.report_date.is_none() {
match policy {
ConversionPolicy::Strict => {
return Err(AtlaError::MissingElement(
"Header.ReportDate is required in TM-33-23".to_string(),
));
}
ConversionPolicy::Compatible => {
let today = current_date_string();
converted.header.report_date = Some(today.clone());
log.push(
ConversionLogEntry::new(
"Header.ReportDate",
ConversionAction::DefaultApplied,
"Applied current date as default",
)
.with_values(None, Some(&today)),
);
}
}
}
}
if let Some(ref gtin_str) = converted.header.gtin {
if converted.header.gtin_int.is_none() {
if let Ok(gtin_int) = gtin_str.parse::<i64>() {
converted.header.gtin_int = Some(gtin_int);
log.push(
ConversionLogEntry::new(
"Header.GTIN",
ConversionAction::TypeConverted,
"Converted string GTIN to integer",
)
.with_values(Some(gtin_str), Some(>in_int.to_string())),
);
} else {
log.push(ConversionLogEntry::new(
"Header.GTIN",
ConversionAction::Warning,
"GTIN could not be parsed as integer, will use string value",
));
}
}
}
for (i, emitter) in converted.emitters.iter_mut().enumerate() {
if emitter.description.is_none() {
match policy {
ConversionPolicy::Strict => {
return Err(AtlaError::MissingElement(format!(
"Emitter[{}].Description is required in TM-33-23",
i
)));
}
ConversionPolicy::Compatible => {
emitter.description = Some("Emitter".to_string());
log.push(
ConversionLogEntry::new(
&format!("Emitter[{}].Description", i),
ConversionAction::DefaultApplied,
"Applied default value for required field",
)
.with_values(None, Some("Emitter")),
);
}
}
}
if emitter.input_watts.is_none() {
return Err(AtlaError::MissingElement(format!(
"Emitter[{}].InputWattage is required in TM-33-23 (no sensible default)",
i
)));
} else {
log.push(ConversionLogEntry::new(
&format!("Emitter[{}].InputWatts", i),
ConversionAction::Renamed,
"Renamed to InputWattage for TM-33-23",
));
}
}
if let Some(ref cd) = converted.custom_data {
if converted.custom_data_items.is_empty() {
converted.custom_data_items.push(CustomDataItem {
name: cd
.namespace
.clone()
.unwrap_or_else(|| "migrated".to_string()),
unique_identifier: format!("migrated-{}", generate_uuid_stub()),
raw_content: cd.data.clone(),
});
log.push(ConversionLogEntry::new(
"CustomData",
ConversionAction::TypeConverted,
"Migrated S001 CustomData to TM-33-23 CustomDataItem",
));
}
}
Ok((converted, log))
}
pub fn tm33_to_atla(doc: &LuminaireOpticalData) -> (LuminaireOpticalData, Vec<ConversionLogEntry>) {
let mut converted = doc.clone();
let mut log = Vec::new();
converted.schema_version = SchemaVersion::AtlaS001;
converted.version = "1.0".to_string();
if let Some(gtin_int) = converted.header.gtin_int {
if converted.header.gtin.is_none() {
converted.header.gtin = Some(gtin_int.to_string());
log.push(
ConversionLogEntry::new(
"Header.GTIN",
ConversionAction::TypeConverted,
"Converted integer GTIN to string",
)
.with_values(Some(>in_int.to_string()), Some(>in_int.to_string())),
);
}
}
if converted.header.test_date.is_none() {
if let Some(ref report_date) = converted.header.report_date {
converted.header.test_date = Some(report_date.clone());
log.push(ConversionLogEntry::new(
"Header.ReportDate",
ConversionAction::Renamed,
"Copied to TestDate for S001 compatibility",
));
}
}
for (i, emitter) in converted.emitters.iter_mut().enumerate() {
if emitter.angular_spectral.is_some() {
emitter.angular_spectral = None;
log.push(ConversionLogEntry::new(
&format!("Emitter[{}].AngularSpectral", i),
ConversionAction::Dropped,
"AngularSpectral data not supported in S001 - DATA LOSS",
));
}
if emitter.angular_color.is_some() {
emitter.angular_color = None;
log.push(ConversionLogEntry::new(
&format!("Emitter[{}].AngularColor", i),
ConversionAction::Dropped,
"AngularColor data not supported in S001 - DATA LOSS",
));
}
if let Some(ref mut dist) = emitter.intensity_distribution {
if let Some(multiplier) = dist.multiplier {
if (multiplier - 1.0).abs() > 0.0001 {
for row in dist.intensities.iter_mut() {
for value in row.iter_mut() {
*value *= multiplier as f64;
}
}
log.push(
ConversionLogEntry::new(
&format!("Emitter[{}].IntensityDistribution.Multiplier", i),
ConversionAction::TypeConverted,
"Applied Multiplier to intensity values (S001 doesn't support Multiplier field)",
)
.with_values(Some(&multiplier.to_string()), None),
);
}
dist.multiplier = None;
}
if dist.symmetry.is_some() {
log.push(ConversionLogEntry::new(
&format!("Emitter[{}].IntensityDistribution.Symm", i),
ConversionAction::Dropped,
"Symmetry field not used in S001 (inferred from angles)",
));
dist.symmetry = None;
}
if dist.absolute_photometry.is_some() {
dist.absolute_photometry = None;
}
}
}
if !converted.custom_data_items.is_empty() && converted.custom_data.is_none() {
let first = &converted.custom_data_items[0];
converted.custom_data = Some(CustomData {
namespace: Some(first.name.clone()),
data: first.raw_content.clone(),
});
if converted.custom_data_items.len() > 1 {
log.push(ConversionLogEntry::new(
"CustomData",
ConversionAction::Dropped,
&format!(
"S001 only supports single CustomData - {} additional items dropped",
converted.custom_data_items.len() - 1
),
));
} else {
log.push(ConversionLogEntry::new(
"CustomData",
ConversionAction::TypeConverted,
"Converted TM-33-23 CustomDataItem to S001 CustomData",
));
}
converted.custom_data_items.clear();
}
(converted, log)
}
fn try_parse_date(s: &str) -> Option<String> {
let s = s.trim();
if s.len() == 10 && s.chars().nth(4) == Some('-') && s.chars().nth(7) == Some('-') {
return Some(s.to_string());
}
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 8 {
let year = &digits[0..4];
let month = &digits[4..6];
let day = &digits[6..8];
if year.parse::<u32>().is_ok()
&& month
.parse::<u32>()
.map(|m| (1..=12).contains(&m))
.unwrap_or(false)
&& day
.parse::<u32>()
.map(|d| (1..=31).contains(&d))
.unwrap_or(false)
{
return Some(format!("{}-{}-{}", year, month, day));
}
}
None
}
fn current_date_string() -> String {
"2024-01-01".to_string() }
fn generate_uuid_stub() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
format!("{:x}", timestamp)
}