use std::collections::{HashMap, HashSet};
use energyml_domain::color::{Cascade, Decision, GraphicalSet, Paint, Request, Scope};
use energyml_domain::color_inherit::Ladder;
use energyml_domain::color_walks::{ColourCache, Stats, changed_targets};
use energyml_domain::package_index::PackageIndex;
use energyml_domain::property::{self, Property};
use energyml_domain::property_kind::Resolver;
use energyml_storage::{EpcStore, ObjectSummary};
use crate::index::{Consumer, Disturbed};
use crate::{Cache, Error};
pub struct Row<'a> {
pub key: String,
pub kinds: Vec<String>,
pub property: Option<&'a str>,
}
impl<'a> Row<'a> {
pub fn at(
store: &EpcStore,
name: &str,
kinds: Vec<String>,
property: Option<&'a str>,
) -> Result<Self, Error> {
Ok(Row {
key: walk_key(store, name)?,
kinds,
property,
})
}
}
fn walk_key(store: &EpcStore, name: &str) -> Result<String, Error> {
match store.resolve(name.trim()) {
Ok(summary) => Ok(summary.uri.clone()),
Err(energyml_storage::Error::NotFound(_)) => Ok(name.trim().to_ascii_lowercase()),
Err(other) => Err(other.into()),
}
}
#[derive(Default)]
pub(crate) struct ColourSlot {
cache: ColourCache,
set: Option<GraphicalSet>,
sets: HashSet<String>,
read: bool,
}
impl ColourSlot {
pub(crate) fn len(&self) -> usize {
self.cache.len() + usize::from(self.read && self.set.is_some())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Options {
pub inherit: bool,
pub viewer_kind: Option<String>,
}
impl Default for Options {
fn default() -> Self {
Self {
inherit: true,
viewer_kind: Some("3d".to_string()),
}
}
}
pub fn graphical_sets(store: &EpcStore) -> Result<Vec<(String, GraphicalSet)>, Error> {
let mut sets = Vec::new();
for summary in store.resources() {
if !summary.qualified_type.ends_with("GraphicalInformationSet") {
continue;
}
let object = store.get_object(&summary.uri)?;
sets.push((
summary.uuid.to_ascii_lowercase(),
GraphicalSet::read(object.as_reflect()),
));
}
Ok(sets)
}
pub fn graphical_set(store: &EpcStore) -> Result<Option<GraphicalSet>, Error> {
let sets = graphical_sets(store)?;
Ok((!sets.is_empty()).then(|| GraphicalSet::merged(sets.into_iter().map(|(_, set)| set))))
}
pub fn kind_chain(store: &EpcStore, read: &Property<'_>) -> Vec<String> {
let Some(reference) = read.kind.as_ref() else {
return Vec::new();
};
match Resolver::new(store).resolve(reference) {
Ok(resolution) => resolution.chain.into_iter().map(|kind| kind.uuid).collect(),
Err(_) => Vec::new(),
}
}
pub fn kind_chain_at(store: &EpcStore, name: &str) -> Result<Vec<String>, Error> {
let summary = match store.resolve(name.trim()) {
Ok(summary) => summary,
Err(energyml_storage::Error::NotFound(_)) => return Ok(Vec::new()),
Err(other) => return Err(other.into()),
};
Ok(store
.get_object(&summary.uri)
.ok()
.and_then(|object| {
property::read(object.as_reflect())
.ok()
.map(|read| kind_chain(store, &read))
})
.unwrap_or_default())
}
pub fn kind_chain_of(store: &EpcStore, property_uuid: &str) -> Vec<String> {
kind_chain_at(store, property_uuid).unwrap_or_default()
}
fn settle(
store: &EpcStore,
slot: &mut ColourSlot,
index: &PackageIndex,
dirty: Disturbed,
) -> Result<(), Error> {
let ColourSlot {
cache,
set,
sets,
read,
} = slot;
let touches_a_set = dirty.everything
|| dirty.uuids.iter().any(|uuid| {
sets.contains(uuid)
|| index
.node(uuid)
.is_some_and(|node| node.type_key.ends_with("GraphicalInformationSet"))
});
if dirty.everything {
cache.clear();
} else {
cache.disturb(&dirty.uuids);
}
if !*read || touches_a_set {
let found = graphical_sets(store)?;
let uuids: HashSet<String> = found.iter().map(|(uuid, _)| uuid.clone()).collect();
let fresh = if found.is_empty() {
None
} else {
Some(GraphicalSet::merged(found.into_iter().map(|(_, one)| one)))
};
if *read && !dirty.everything {
let nothing = GraphicalSet::default();
let changed = changed_targets(
set.as_ref().unwrap_or(¬hing),
fresh.as_ref().unwrap_or(¬hing),
);
cache.disturb(&changed);
}
*set = fresh;
*sets = uuids;
*read = true;
}
Ok(())
}
fn decide_in(
index: &PackageIndex,
store: &EpcStore,
set: &GraphicalSet,
representation: &str,
property: Option<&str>,
chain: &[String],
options: &Options,
) -> Result<Option<Decision>, Error> {
let mut cascade = Cascade::new(set);
cascade.inherit = options.inherit;
cascade.viewer_kind = options.viewer_kind.clone();
let walked = match property {
Some(property) => cascade.inherited_property(index, store, property, chain),
None => cascade.inherited(index, store, representation),
}?;
Ok(walked.resolution.map(|resolution| {
let mut decision = resolution.value;
for note in resolution.notes {
if !decision.notes.contains(¬e) {
decision.notes.push(note);
}
}
decision
}))
}
impl Cache {
pub fn ladders(&self, store: &EpcStore, rows: &[Row<'_>]) -> Result<Option<Vec<Ladder>>, Error> {
let mut fresh = ColourSlot::default();
let mut held = self.colours.try_borrow_mut();
let (slot, kept): (&mut ColourSlot, bool) = match held.as_deref_mut() {
Ok(slot) if self.layers().colours => (slot, true),
_ => (&mut fresh, false),
};
self.with_index(store, |index| -> Result<Option<Vec<Ladder>>, Error> {
let dirty = if kept {
self.index.take_disturbed(Consumer::Colours)
} else {
Disturbed::default()
};
settle(store, slot, index, dirty)?;
let ColourSlot { cache, set, .. } = slot;
let Some(set) = set.as_ref() else {
return Ok(None);
};
let cascade = Cascade::new(set);
let requests: Vec<Request<'_>> = rows
.iter()
.map(|row| Request {
representation: &row.key,
property: row.property,
kind_chain: &row.kinds,
element: Scope::WholeObject,
})
.collect();
Ok(Some(cascade.ladders_cached(cache, index, store, &requests)?))
})
}
pub fn file_colours_at(
&self,
store: &EpcStore,
names: &[String],
) -> Result<HashMap<String, Option<String>>, Error> {
let mut keys = Vec::with_capacity(names.len());
let mut rows: Vec<Row<'_>> = Vec::with_capacity(names.len());
for name in names {
let key = match store.resolve(name.trim()) {
Ok(summary) => summary.uri.clone(),
Err(energyml_storage::Error::NotFound(_)) => name.trim().to_string(),
Err(other) => return Err(other.into()),
};
rows.push(Row::at(store, name, Vec::new(), None)?);
keys.push(key);
}
let Some(ladders) = self.ladders(store, &rows)? else {
return Ok(keys.into_iter().map(|key| (key, None)).collect());
};
Ok(keys
.into_iter()
.zip(ladders)
.map(|(key, ladder)| {
let colour = match ladder.walked.resolution.map(|found| found.value) {
Some(Decision {
paint: Paint::Constant(colour),
..
}) => Some(colour.to_hex()),
_ => None,
};
(key, colour)
})
.collect())
}
pub fn file_colours(
&self,
store: &EpcStore,
uuids: &[String],
) -> Result<HashMap<String, Option<String>>, Error> {
let by_uri = self.file_colours_at(store, uuids)?;
let mut out = HashMap::with_capacity(uuids.len());
for uuid in uuids {
let key = match store.resolve(uuid.trim()) {
Ok(summary) => summary.uri.clone(),
Err(_) => uuid.trim().to_string(),
};
out.insert(uuid.to_ascii_lowercase(), by_uri.get(&key).cloned().flatten());
}
Ok(out)
}
pub fn decision(
&self,
store: &EpcStore,
set: &GraphicalSet,
representation: &str,
property: Option<&str>,
chain: &[String],
options: &Options,
) -> Result<Option<Decision>, Error> {
self.with_index(store, |index| {
decide_in(index, store, set, representation, property, chain, options)
})
}
pub fn decision_at(
&self,
store: &EpcStore,
set: &GraphicalSet,
name: &str,
property: Option<&str>,
chain: &[String],
options: &Options,
) -> Result<Option<Decision>, Error> {
let key = walk_key(store, name)?;
self.decision(store, set, &key, property, chain, options)
}
pub fn colour_walk_stats(&self) -> Stats {
self.colours
.try_borrow()
.map(|slot| slot.cache.stats())
.unwrap_or_default()
}
#[doc(hidden)]
pub fn colour_walk_held(&self, uuid: &str) -> bool {
self.colours
.try_borrow()
.map(|slot| slot.cache.holds(uuid))
.unwrap_or(false)
}
}
pub struct Colouring<'a> {
pub cache: &'a Cache,
pub store: &'a EpcStore,
}
impl energyml_geometry::scene::Decide for Colouring<'_> {
fn decide(&self, set: &GraphicalSet, request: &Request<'_>) -> Option<Decision> {
self.cache
.decision(
self.store,
set,
request.representation,
request.property,
request.kind_chain,
&Options::default(),
)
.ok()
.flatten()
}
}
#[derive(Clone, Debug, Default)]
pub struct Wanted<'a> {
pub object: Option<&'a str>,
pub options: Options,
}
#[derive(Clone, Debug)]
pub struct Coloured {
pub summary: ObjectSummary,
pub decision: Option<Decision>,
}
#[derive(Clone, Debug, Default)]
pub struct Report {
pub sets: usize,
pub notes: Vec<String>,
pub rows: Vec<Coloured>,
pub coloured: usize,
}
pub fn of_package(cache: &Cache, store: &EpcStore, wanted: &Wanted<'_>) -> Result<Report, Error> {
let sets: Vec<GraphicalSet> = graphical_sets(store)?.into_iter().map(|(_, set)| set).collect();
if sets.is_empty() {
return Ok(Report::default());
}
let count = sets.len();
let set = GraphicalSet::merged(sets);
let notes = set.notes.clone();
let wanted_uri: Option<String> = match wanted.object {
None => None,
Some(name) => match store.resolve(name.trim()) {
Ok(summary) => Some(summary.uri.clone()),
Err(energyml_storage::Error::NotFound(_)) => Some(name.trim().to_string()),
Err(other) => return Err(other.into()),
},
};
let rows = cache.with_index(store, |index| -> Result<Vec<Coloured>, Error> {
let mut rows = Vec::new();
for summary in store.resources() {
let is_representation = summary.qualified_type.contains("Representation");
let is_property = summary.qualified_type.ends_with("Property");
if !is_representation && !is_property {
continue;
}
if let Some(wanted) = &wanted_uri
&& summary.uri != *wanted
{
continue;
}
let decision = if is_property {
let Ok(object) = store.get_object(&summary.uri) else {
continue;
};
let Ok(read) = property::read(object.as_reflect()) else {
continue;
};
let chain = kind_chain(store, &read);
let representation = read
.supporting_representation_uri
.clone()
.or_else(|| read.supporting_representation.clone())
.unwrap_or_default();
decide_in(
index,
store,
&set,
&representation,
Some(&summary.uri),
&chain,
&wanted.options,
)?
} else {
decide_in(index, store, &set, &summary.uri, None, &[], &wanted.options)?
};
rows.push(Coloured {
summary: summary.clone(),
decision,
});
}
Ok(rows)
})?;
let coloured = rows.iter().filter(|row| row.decision.is_some()).count();
Ok(Report {
sets: count,
notes,
rows,
coloured,
})
}