use itertools::Itertools;
use std::{
cmp::max,
cmp::min,
collections::{HashMap, HashSet},
};
use crate::e173_types;
use crate::{schema, Error};
e173_types! {
pub struct DmxDriver {
pub clusters: Vec<ParameterCluster>,
pub chunks: HashMap<String, DmxChunk>,
}
pub struct ParameterCluster {
pub parameters: HashSet<String>,
pub combinations: Vec<ParameterCombo>,
}
pub struct DmxChunk {
pub offsets: Vec<u16>,
}
pub struct ParameterCombo {
pub constraints: HashMap<String, ParameterConstraint>,
}
#[derive(PartialEq, PartialOrd)]
#[serde(rename_all = "camelCase")]
pub struct ParameterConstraint {
pub param_range: ParamRange,
#[serde(skip_serializing_if = "Option::is_none")]
pub dmx_mapping: Option<DmxMapping>,
pub calculated: bool,
}
#[derive(PartialEq, PartialOrd)]
#[serde(untagged)]
pub enum ParamRange {
Numeric { start: f64, end: f64 },
Bool { start: bool, end: bool },
Null,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "camelCase")]
pub struct DmxMapping {
pub chunk_id: String,
pub start: u32,
pub end: u32,
}
}
#[derive(Clone, Debug)]
struct ParameterMapping {
constraints: HashMap<String, ParameterConstraint>,
mapped_param: String,
}
impl DmxDriver {
pub fn from(dmx: &schema::EstaDmx) -> Result<Self, Error> {
let chunks: HashMap<_, _> = dmx
.chunks
.iter()
.map(|(chunk_id, chunk)| -> Result<_, Error> {
Ok((chunk_id.clone(), get_chunk(chunk_id, chunk)?))
})
.collect::<Result<_, _>>()?;
let mappings: Vec<_> = dmx
.chunks
.iter()
.map(|(chunk_id, chunk)| get_mappings_for_chunk(dmx, chunk_id, chunk))
.flatten_ok()
.collect::<Result<_, _>>()?;
let combinations: Vec<_> = mappings
.into_iter()
.map(|mapping| ParameterCombo {
constraints: mapping.constraints,
})
.collect();
let clusters = transform_combos_to_clusters(&chunks, combinations);
Ok(DmxDriver { chunks, clusters })
}
}
impl ParameterCombo {
fn parameters(&self) -> HashSet<String> {
self.constraints.keys().cloned().collect()
}
}
fn get_chunk(chunk_id: &str, chunk: &schema::Chunk) -> Result<DmxChunk, Error> {
let num_offsets = chunk.offsets.len();
if num_offsets == 0 {
return Err(Error::invalid_dmx(
"Chunk has no offsets",
format!("/chunks/{chunk_id}/offsets"),
));
}
for (index, offset) in chunk.offsets.iter().enumerate() {
if *offset > 511 {
return Err(Error::invalid_dmx(
"Chunk offset out of range",
format!("/chunks/{chunk_id}/offsets/{index}"),
));
}
}
Ok(DmxChunk {
offsets: chunk.offsets.clone(),
})
}
fn transform_combos_to_clusters(
chunks: &HashMap<String, DmxChunk>,
combinations: Vec<ParameterCombo>,
) -> Vec<ParameterCluster> {
let mut clusters: Vec<ParameterCluster> = vec![];
let address_map = get_address_map(chunks, &combinations);
for combination in combinations {
let parameters = combination.parameters();
merge_clusters(&mut clusters, parameters, Some(combination));
}
for params in address_map.into_values() {
merge_clusters(&mut clusters, params, None);
}
for cluster in &mut clusters {
dedup_cluster(cluster);
}
for cluster in &mut clusters {
for combination in &mut cluster.combinations {
let parameters = combination.parameters();
for missing_param in cluster.parameters.difference(¶meters) {
combination.constraints.insert(
missing_param.clone(),
ParameterConstraint {
param_range: ParamRange::Null,
dmx_mapping: None,
calculated: true,
},
);
}
}
}
clusters
}
fn merge_clusters(
clusters: &mut Vec<ParameterCluster>,
parameters: HashSet<String>,
combination: Option<ParameterCombo>,
) {
let mut matching_clusters = vec![];
let mut i = 0;
while i < clusters.len() {
if clusters[i]
.parameters
.intersection(¶meters)
.next()
.is_some()
{
matching_clusters.push(clusters.remove(i));
} else {
i += 1;
}
}
if matching_clusters.is_empty() {
let combination =
combination.expect("If combination is not provided, a cluster must match");
clusters.push(ParameterCluster {
parameters,
combinations: vec![combination],
});
} else {
let cluster = ParameterCluster {
parameters,
combinations: combination.into_iter().collect(),
};
let cluster = matching_clusters
.into_iter()
.fold(cluster, |accum, cluster| {
let mut accum = accum;
let mut cluster = cluster;
accum.combinations.append(&mut cluster.combinations);
accum.parameters.extend(cluster.parameters);
accum
});
clusters.push(cluster);
}
}
fn dedup_cluster(cluster: &mut ParameterCluster) {
let mut i = 0;
while i < cluster.combinations.len() {
let (first, second) = cluster.combinations.split_at_mut(i + 1);
let combo = &mut first[i];
let mut merged = false;
for cluster in second {
if try_merge_combinations(combo, cluster) {
merged = true;
}
}
if merged {
cluster.combinations.remove(i);
} else {
i += 1;
}
}
}
fn try_merge_combinations(lhs: &mut ParameterCombo, rhs: &mut ParameterCombo) -> bool {
for (param, lhs_constraint) in &lhs.constraints {
if let Some(rhs_constraint) = rhs.constraints.get(param) {
if lhs_constraint.param_range != rhs_constraint.param_range {
return false;
}
}
if let Some(lhs_mapping) = &lhs_constraint.dmx_mapping {
for rhs_constraint in rhs.constraints.values() {
if let Some(rhs_mapping) = &rhs_constraint.dmx_mapping {
if lhs_mapping.chunk_id == rhs_mapping.chunk_id && lhs_mapping != rhs_mapping {
return false;
}
}
}
}
}
for (param, lhs_constraint) in &lhs.constraints {
if !rhs.constraints.contains_key(param) {
rhs.constraints
.insert(param.clone(), lhs_constraint.clone());
}
}
true
}
fn get_address_map(
chunks: &HashMap<String, DmxChunk>,
combinations: &Vec<ParameterCombo>,
) -> HashMap<u16, HashSet<String>> {
let mut map = HashMap::new();
for combination in combinations {
for (parameter, constraint) in &combination.constraints {
if let Some(chunk) = constraint
.dmx_mapping
.as_ref()
.and_then(|mapping| chunks.get(&mapping.chunk_id))
{
for address in &chunk.offsets {
map.entry(*address)
.or_insert(HashSet::new())
.insert(parameter.clone());
}
}
}
}
map
}
fn get_mappings_for_chunk(
dmx: &schema::EstaDmx,
chunk_id: &str,
chunk: &schema::Chunk,
) -> Result<Vec<ParameterMapping>, Error> {
chunk
.mapping_groups
.iter()
.enumerate()
.map(|(mapping_grp_index, mapping_group)| {
let json_path = format!("/chunks/{chunk_id}/mappingGroups/{mapping_grp_index}");
get_mappings_for_mapping_group(dmx, chunk_id, mapping_group, &json_path)
})
.flatten_ok()
.collect::<Result<_, _>>()
}
fn get_mappings_for_mapping_group(
dmx: &schema::EstaDmx,
chunk_id: &str,
mapping_group: &schema::MappingGroup,
json_path: &str,
) -> Result<Vec<ParameterMapping>, Error> {
let mappings = transform_mappings(chunk_id, &mapping_group.mappings, json_path)?;
if mapping_group.conditions.is_empty() {
return Ok(mappings);
}
assert!(mapping_group.conditions.len() == 1);
let condition_combos = get_combos_for_condition(
dmx,
&mapping_group.conditions[0],
&format!("{json_path}/conditions/0"),
)?;
let mut final_mappings = Vec::new();
for mapping in mappings {
for condition_combo in condition_combos.clone() {
let mut mapping = mapping.clone();
let expected_len = mapping.constraints.len() + condition_combo.constraints.len();
mapping.constraints.extend(condition_combo.constraints);
if mapping.constraints.len() != expected_len {
return Err(Error::invalid_dmx(
"Mapping group's condition referenced a duplicate parameter",
json_path,
));
}
final_mappings.push(mapping);
}
}
Ok(final_mappings)
}
fn transform_mappings(
chunk_id: &str,
mappings: &[schema::Mapping],
json_path: &str,
) -> Result<Vec<ParameterMapping>, Error> {
mappings
.iter()
.enumerate()
.map(|(mapping_index, mapping)| {
let json_path = format!("{json_path}/mappings/{mapping_index}");
transform_mapping(chunk_id, mapping, &json_path)
})
.flatten_ok()
.collect()
}
fn transform_mapping(
chunk_id: &str,
mapping: &schema::Mapping,
json_path: &str,
) -> Result<Vec<ParameterMapping>, Error> {
let mut unmapped_params = HashMap::new();
for unmapped_param in &mapping.unmapped_params {
unmapped_params.insert(
unmapped_param.parameter.clone(),
ParameterConstraint {
param_range: get_param_range(unmapped_param.start, unmapped_param.end, json_path)?,
calculated: false,
dmx_mapping: None,
},
);
}
if unmapped_params.len() != mapping.unmapped_params.len()
|| unmapped_params.contains_key(&mapping.mapped_param)
{
return Err(Error::invalid_dmx(
"Mapping object contains duplicate parameters",
json_path,
));
}
mapping
.ranges
.iter()
.map(|range| -> Result<_, Error> {
let mut constraints = unmapped_params.clone();
let insert_res = constraints.insert(
mapping.mapped_param.clone(),
ParameterConstraint {
param_range: get_param_range(range.start, range.end, json_path)?,
dmx_mapping: Some(DmxMapping {
chunk_id: chunk_id.into(),
start: range.chunk_start,
end: range.chunk_end,
}),
calculated: false,
},
);
assert!(insert_res.is_none()); Ok(ParameterMapping {
mapped_param: mapping.mapped_param.clone(),
constraints,
})
})
.collect()
}
fn get_combos_for_condition(
dmx: &schema::EstaDmx,
condition: &schema::Condition,
json_path: &str,
) -> Result<Vec<ParameterCombo>, Error> {
match condition.cond_match {
Some(schema::ConditionMatch::Any) => {
get_combinations_for_any_condition(dmx, condition, json_path)
}
Some(schema::ConditionMatch::All) => {
get_combinations_for_all_condition(dmx, condition, json_path)
}
None => get_combinations_for_simple_condition(dmx, condition, json_path),
}
}
fn get_combinations_for_any_condition(
dmx: &schema::EstaDmx,
condition: &schema::Condition,
json_path: &str,
) -> Result<Vec<ParameterCombo>, Error> {
if condition.conditions.is_empty() {
return Err(Error::invalid_dmx(
"Evaluation condition must contain one or more condition objects",
json_path,
));
}
if condition.chunk.is_some() || condition.chunk_start.is_some() || condition.chunk_end.is_some()
{
return Err(Error::invalid_dmx(
"Evaluation condition must not contain 'chunk', 'chunkStart' or 'chunkEnd' fields",
json_path,
));
}
condition
.conditions
.iter()
.enumerate()
.map(|(index, condition)| {
get_combos_for_condition(dmx, condition, &format!("{json_path}/conditions/{index}"))
})
.flatten_ok()
.collect()
}
fn get_combinations_for_all_condition(
_dmx: &schema::EstaDmx,
_condition: &schema::Condition,
_json_path: &str,
) -> Result<Vec<ParameterCombo>, Error> {
todo!("Currently there are no all conditions in the examples")
}
fn get_combinations_for_simple_condition(
dmx: &schema::EstaDmx,
condition: &schema::Condition,
json_path: &str,
) -> Result<Vec<ParameterCombo>, Error> {
if !condition.conditions.is_empty() {
return Err(Error::invalid_dmx(
"Simple condition object must not contain nested conditions",
json_path,
));
}
let Some(chunk_id) = &condition.chunk else {
return Err(Error::invalid_dmx(
"Simple condition object must contain 'chunk' field",
json_path,
));
};
let Some(chunk_start) = condition.chunk_start else {
return Err(Error::invalid_dmx(
"Simple condition object must contain 'chunkStart' field",
json_path,
));
};
let Some(chunk_end) = condition.chunk_end else {
return Err(Error::invalid_dmx(
"Simple condition object must contain 'chunkEnd' field",
json_path,
));
};
let Some(chunk) = dmx.chunks.get(chunk_id) else {
return Err(Error::invalid_dmx(
"Simple condition references a chunk that does not exist",
json_path,
));
};
let chunk_mappings = get_mappings_for_chunk(dmx, chunk_id, chunk)?;
let mut condition_combos = Vec::new();
for mut chunk_mapping in chunk_mappings {
let mapped_constraint = chunk_mapping
.constraints
.get_mut(&chunk_mapping.mapped_param)
.expect("constraints must contain mapped_param by contract");
if !scale_to_overlapping_range(
chunk_start,
chunk_end,
&mut mapped_constraint.param_range,
mapped_constraint
.dmx_mapping
.as_mut()
.expect("mapped_constraint must contain dmx_mapping by contract"),
) {
continue;
}
condition_combos.push(ParameterCombo {
constraints: chunk_mapping.constraints,
});
}
if condition_combos.is_empty() {
Err(Error::invalid_dmx(
"Conditions which evaluate to reserved DMX ranges are not currently supported",
json_path,
))
} else {
Ok(condition_combos)
}
}
fn get_param_range(
start: Option<schema::MappingBound>,
end: Option<schema::MappingBound>,
json_path: &str,
) -> Result<ParamRange, Error> {
let Some(start) = start else {
if end.is_some() {
return Err(Error::invalid_dmx(
"Mapping object: if start is absent, end must be absent",
json_path,
));
}
return Ok(ParamRange::Null);
};
match (start, end) {
(schema::MappingBound::Numeric(start), None) => {
Ok(ParamRange::Numeric { start, end: start })
}
(schema::MappingBound::Numeric(start), Some(schema::MappingBound::Numeric(end))) => {
Ok(ParamRange::Numeric { start, end })
}
(schema::MappingBound::Bool(start), None) => Ok(ParamRange::Bool { start, end: start }),
(schema::MappingBound::Bool(start), Some(schema::MappingBound::Bool(end))) => {
Ok(ParamRange::Bool { start, end })
}
_ => Err(Error::invalid_dmx(
"Mapping object: start and end are mismatched types",
json_path,
)),
}
}
fn scale_to_overlapping_range(
ref_start: u32,
ref_end: u32,
range: &mut ParamRange,
mapping: &mut DmxMapping,
) -> bool {
let (dmx_start, dmx_end) = (mapping.start, mapping.end);
let Some((overlap_start, overlap_end)) =
get_range_overlap((dmx_start, dmx_end), (ref_start, ref_end))
else {
return false;
};
mapping.start = overlap_start;
mapping.end = overlap_end;
let ParamRange::Numeric { start, end } = range else {
return true;
};
let new_start =
scale_parameter_range_to_value((*start, *end), (dmx_start, dmx_end), overlap_start);
let new_end = scale_parameter_range_to_value((*start, *end), (dmx_start, dmx_end), overlap_end);
*start = new_start;
*end = new_end;
true
}
fn scale_parameter_range_to_value(param_range: (f64, f64), mapping: (u32, u32), value: u32) -> f64 {
let descending = mapping.0 > mapping.1;
let (param_start, param_end) = if descending {
(param_range.1, param_range.0)
} else {
param_range
};
let (dmx_start, dmx_end) = if descending {
(mapping.1, mapping.0)
} else {
mapping
};
if dmx_start == dmx_end {
return param_start;
}
assert!(value >= dmx_start && value <= dmx_end);
let fraction = (value - dmx_start) as f64 / (dmx_end - dmx_start) as f64;
param_start + fraction * (param_end - param_start)
}
fn get_range_overlap(ours: (u32, u32), theirs: (u32, u32)) -> Option<(u32, u32)> {
let (ours_start, ours_end) = ours;
if ours_start <= ours_end {
let (theirs_start, theirs_end) = (min(theirs.0, theirs.1), max(theirs.0, theirs.1));
let overlap = (max(ours_start, theirs_start), min(ours_end, theirs_end));
if overlap.0 <= overlap.1 {
Some(overlap)
} else {
None
}
} else {
let (theirs_start, theirs_end) = (max(theirs.0, theirs.1), min(theirs.0, theirs.1));
let overlap = (min(ours_start, theirs_start), max(ours_end, theirs_end));
if overlap.0 >= overlap.1 {
Some(overlap)
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_range_overlap() {
assert_eq!(get_range_overlap((30, 50), (40, 60)), Some((40, 50)));
assert_eq!(get_range_overlap((30, 50), (60, 40)), Some((40, 50)));
assert_eq!(get_range_overlap((30, 50), (20, 40)), Some((30, 40)));
assert_eq!(get_range_overlap((30, 50), (20, 60)), Some((30, 50)));
assert_eq!(get_range_overlap((30, 50), (35, 45)), Some((35, 45)));
assert_eq!(get_range_overlap((30, 50), (51, 60)), None);
assert_eq!(get_range_overlap((30, 50), (50, 51)), Some((50, 50)));
assert_eq!(get_range_overlap((50, 30), (40, 60)), Some((50, 40)));
assert_eq!(get_range_overlap((50, 30), (60, 40)), Some((50, 40)));
assert_eq!(get_range_overlap((50, 30), (20, 40)), Some((40, 30)));
assert_eq!(get_range_overlap((50, 30), (40, 20)), Some((40, 30)));
assert_eq!(get_range_overlap((50, 30), (20, 60)), Some((50, 30)));
assert_eq!(get_range_overlap((50, 30), (60, 20)), Some((50, 30)));
assert_eq!(get_range_overlap((50, 30), (51, 60)), None);
assert_eq!(get_range_overlap((50, 30), (10, 0)), None);
}
mod scale_to_overlapping_range {
use super::*;
use approx::assert_ulps_eq;
fn parameter_mapping(
dmx_start: u32,
dmx_end: u32,
num_start: f64,
num_end: f64,
) -> (ParamRange, DmxMapping) {
(
ParamRange::Numeric {
start: num_start,
end: num_end,
},
DmxMapping {
start: dmx_start,
end: dmx_end,
chunk_id: "test".into(),
},
)
}
fn assert_is_numeric_range_with(param_range: &ParamRange, exp_start: f64, exp_end: f64) {
let ParamRange::Numeric { start, end } = param_range else {
panic!("Should be a numeric parameter range");
};
assert_ulps_eq!(*start, exp_start);
assert_ulps_eq!(*end, exp_end);
}
#[test]
fn ascending_range_is_scaled() {
let (mut range, mut mapping) = parameter_mapping(10, 40, 1.0, 4.0);
assert!(scale_to_overlapping_range(0, 20, &mut range, &mut mapping));
assert_is_numeric_range_with(&range, 1.0, 2.0);
assert_eq!(mapping.start, 10);
assert_eq!(mapping.end, 20);
}
#[test]
fn descending_range_is_scaled() {
let (mut range, mut mapping) = parameter_mapping(40, 10, 4.0, 1.0);
assert!(scale_to_overlapping_range(0, 20, &mut range, &mut mapping));
assert_is_numeric_range_with(&range, 2.0, 1.0);
assert_eq!(mapping.start, 20);
assert_eq!(mapping.end, 10);
}
#[test]
fn mixed_ranges_are_scaled() {
{
let (mut range, mut mapping) = parameter_mapping(10, 40, 4.0, 1.0);
assert!(scale_to_overlapping_range(0, 20, &mut range, &mut mapping));
assert_is_numeric_range_with(&range, 4.0, 3.0);
assert_eq!(mapping.start, 10);
assert_eq!(mapping.end, 20);
}
{
let (mut range, mut mapping) = parameter_mapping(40, 10, 1.0, 4.0);
assert!(scale_to_overlapping_range(0, 20, &mut range, &mut mapping));
assert_is_numeric_range_with(&range, 3.0, 4.0);
assert_eq!(mapping.start, 20);
assert_eq!(mapping.end, 10);
}
}
}
mod transform_combos_to_clusters {
use super::*;
#[test]
fn simple_combos() {
let combos = vec![ParameterCombo {
constraints: HashMap::from([(
"test1".into(),
ParameterConstraint {
param_range: ParamRange::Numeric {
start: 0.0,
end: 1.0,
},
dmx_mapping: Some(DmxMapping {
chunk_id: "test1".into(),
start: 0,
end: 32,
}),
calculated: false,
},
)]),
}];
let chunks = HashMap::from([("test1".into(), DmxChunk { offsets: vec![0] })]);
let clusters = transform_combos_to_clusters(&chunks, combos.clone());
assert_eq!(clusters.len(), 1);
let cluster = &clusters[0];
assert_eq!(cluster.parameters, HashSet::from(["test1".into()]));
}
}
}