use energyml_core::value::{FieldValue, Reflect};
use energyml_domain::crs::{self, Crs};
use energyml_domain::metadata::{Metadata, Spec};
use crate::error::Result;
use crate::geometry::{self, Geometry};
use crate::points::Point;
use crate::readers::Readers;
use crate::shape::Shape;
use crate::source::ValueSource;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Placement {
Local,
#[default]
Projected,
Metres,
}
impl Placement {
pub fn name(self) -> &'static str {
match self {
Placement::Local => "local",
Placement::Projected => "projected",
Placement::Metres => "metres",
}
}
pub fn parse(name: &str) -> std::result::Result<Placement, String> {
match name {
"local" => Ok(Placement::Local),
"projected" => Ok(Placement::Projected),
"metres" | "meters" => Ok(Placement::Metres),
other => Err(format!(
"unknown placement `{other}`: one of `local` (the representation's own axes), \
`projected` (rotation, offset and depth flip applied) or `metres` (and refused when the CRS \
states a unit this build cannot convert)"
)),
}
}
}
#[derive(Clone, Debug)]
pub struct SceneObject {
pub uuid: String,
pub uri: String,
pub title: String,
pub qualified_type: String,
pub geometry: Geometry,
pub crs: Option<Crs>,
pub interpretation: Option<Related>,
pub feature: Option<Related>,
pub metadata: Metadata,
pub attributes: Vec<Attribute>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Attribute {
pub uuid: String,
pub name: String,
pub kind: Option<String>,
pub uom: Option<String>,
pub scope: AttributeScope,
pub components: u64,
pub values: Vec<f64>,
pub lut: Vec<[f64; 4]>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AttributeScope {
Points,
Cells,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Related {
pub uuid: String,
pub title: String,
pub qualified_type: String,
}
impl SceneObject {
pub fn points_in(&self, placement: Placement) -> Result<Vec<Point>> {
let (points, _) = self.geometry.merged();
self.place(points, placement)
}
pub fn geometry_in(&self, placement: Placement) -> Result<Geometry> {
let mut out = self.geometry.clone();
for patch in &mut out.patches {
patch.points = self.place(std::mem::take(&mut patch.points), placement)?;
}
Ok(out)
}
pub fn mesh_in(&self, placement: Placement) -> Result<(Vec<Point>, Vec<[u32; 3]>)> {
let (points, triangles) = self.geometry.merged();
Ok((self.place(points, placement)?, triangles))
}
pub fn drawable_in(&self, placement: Placement) -> Result<crate::geometry::Drawable> {
let drawn = self.geometry.drawable();
Ok(crate::geometry::Drawable {
points: self.place(drawn.points, placement)?,
triangles: drawn.triangles,
lines: drawn.lines,
dropped: drawn.dropped,
triangle_cells: drawn.triangle_cells,
node_cells: drawn.node_cells,
})
}
pub fn place_points(&self, points: Vec<Point>, placement: Placement) -> Result<Vec<Point>> {
self.place(points, placement)
}
fn place(&self, points: Vec<Point>, placement: Placement) -> Result<Vec<Point>> {
let Some(crs) = &self.crs else {
return Ok(points);
};
match placement {
Placement::Local => Ok(points),
Placement::Projected => Ok(points.into_iter().map(|p| crs.to_projected(p)).collect()),
Placement::Metres => points
.into_iter()
.map(|p| crs.to_metres(p))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into),
}
}
pub fn epsg(&self) -> Option<u64> {
self.crs.as_ref().and_then(|crs| crs.projected.epsg())
}
pub fn as_shape(&self, shape: Shape) -> Result<std::borrow::Cow<'_, SceneObject>> {
if self
.geometry
.patches
.iter()
.all(|patch| patch.topology.is_already(shape))
{
return Ok(std::borrow::Cow::Borrowed(self));
}
let cells_before = self.geometry.cell_count();
let points_before = self.geometry.point_count();
let geometry = self.geometry.as_shape(shape)?;
let attributes = if shape.is_summary() || geometry.point_count() != points_before {
Vec::new()
} else if geometry.cell_count() == cells_before {
self.attributes.clone()
} else {
self.attributes
.iter()
.filter(|attribute| attribute.scope == AttributeScope::Points)
.cloned()
.collect()
};
Ok(std::borrow::Cow::Owned(SceneObject {
geometry,
attributes,
..self.clone()
}))
}
pub fn drawable_only(&self) -> Option<SceneObject> {
let compaction = self.geometry.defined_only()?;
let points_before = self.geometry.point_count();
let cells_before = self.geometry.cell_count();
let attributes = self
.attributes
.iter()
.map(|attribute| {
let (map, expected) = match attribute.scope {
AttributeScope::Points => (&compaction.points, points_before),
AttributeScope::Cells => (&compaction.cells, cells_before),
};
let components = attribute.components.max(1) as usize;
if attribute.values.len() != expected * components {
return attribute.clone();
}
let mut values = Vec::with_capacity(map.len() * components);
for element in map {
let start = element * components;
values.extend_from_slice(&attribute.values[start..start + components]);
}
Attribute {
values,
..attribute.clone()
}
})
.collect();
Some(SceneObject {
geometry: compaction.geometry,
attributes,
..self.clone()
})
}
}
pub fn object_of(representation: &dyn Reflect, source: &dyn ValueSource, uri: &str) -> Result<SceneObject> {
object_of_with(
crate::readers::default_readers(),
representation,
source,
uri,
None,
)
}
pub fn object_of_with(
readers: &Readers,
representation: &dyn Reflect,
source: &dyn ValueSource,
uri: &str,
spec: Option<&Spec>,
) -> Result<SceneObject> {
let info = representation.type_info();
let geometry = geometry::of_with(readers, representation, source)?;
let crs = crs::of(representation, source).unwrap_or(None);
let interpretation = related(representation, INTERPRETS);
let feature = interpretation_of(representation, source)
.and_then(|object| related(object.as_reflect(), REPRESENTS_FEATURE));
let metadata = match spec {
Some(spec) => spec.read(representation, source),
None => Metadata::default(),
};
Ok(SceneObject {
uuid: energyml_core::identity::uuid_of_reflect(representation)
.unwrap_or_default()
.to_string(),
uri: uri.to_string(),
title: title_of(representation),
qualified_type: energyml_core::json::qualified_type_of(info),
geometry,
crs,
interpretation,
feature,
metadata,
attributes: Vec::new(),
})
}
pub trait Decide {
fn decide(
&self,
set: &energyml_domain::color::GraphicalSet,
request: &energyml_domain::color::Request<'_>,
) -> Option<energyml_domain::color::Decision>;
}
pub fn colour_ramp(
objects: &dyn energyml_domain::source::ObjectSource,
cascade: &energyml_domain::color::Cascade<'_>,
colours: Option<&dyn Decide>,
kinds: &energyml_domain::property_kind::Resolver<'_>,
object: &SceneObject,
attribute: &Attribute,
) -> Vec<[f64; 4]> {
use energyml_domain::color::{Paint, Request, Scope};
use energyml_domain::property;
let Some(object_of_property) = objects.object(&attribute.uuid) else {
return Vec::new();
};
let Ok(read) = property::read(object_of_property.as_reflect()) else {
return Vec::new();
};
let chain: Vec<String> = read
.kind
.as_ref()
.and_then(|reference| kinds.resolve(reference).ok())
.map(|resolution| {
resolution
.chain
.iter()
.map(|kind| kind.uuid.clone())
.filter(|uuid| !uuid.is_empty())
.collect()
})
.unwrap_or_default();
let asked = Request {
representation: &object.uri,
property: Some(&attribute.uuid),
kind_chain: &chain,
element: Scope::WholeObject,
};
let decision = match colours {
Some(colours) => colours.decide(cascade.set, &asked),
None => cascade.resolve(&asked, objects).ok().flatten(),
};
let Some(decision) = decision else {
return Vec::new();
};
let Paint::Mapped { map, scaling, .. } = &decision.paint else {
return Vec::new();
};
let finite: Vec<f64> = attribute
.values
.iter()
.copied()
.filter(|v| v.is_finite())
.collect();
let (low, high) = match (
finite.iter().copied().reduce(f64::min),
finite.iter().copied().reduce(f64::max),
) {
(Some(low), Some(high)) if high > low => (low, high),
_ => return Vec::new(),
};
(0..256)
.filter_map(|step| {
let value = low + (high - low) * (step as f64) / 255.0;
map.sample(Some(value), scaling)
.map(|rgba| [rgba.r, rgba.g, rgba.b, rgba.a])
})
.collect()
}
fn intervals_onto_nodes(values: &[f64], components: usize, point_count: usize, segments: usize) -> Vec<f64> {
let mut out = Vec::with_capacity(point_count * components);
for node in 0..point_count {
let interval = node.min(segments.saturating_sub(1));
let at = interval * components;
out.extend_from_slice(&values[at..at + components]);
}
out
}
pub struct PropertiesOf<'a> {
index: energyml_domain::property::Index,
by_uuid: std::collections::HashMap<String, &'a dyn Reflect>,
unread: Vec<String>,
}
impl<'a> PropertiesOf<'a> {
pub fn new(properties: &[&'a dyn Reflect]) -> Self {
let (index, unread) = energyml_domain::property::Index::build(properties.iter().copied());
let by_uuid = properties
.iter()
.filter_map(|property| {
energyml_core::identity::uuid_of_reflect(*property)
.map(|uuid| (uuid.to_ascii_lowercase(), *property))
})
.collect();
Self {
index,
by_uuid,
unread,
}
}
pub fn of(&self, representation_uuid: &str) -> Vec<&'a dyn Reflect> {
self.index
.of(representation_uuid)
.iter()
.filter_map(|entry| self.by_uuid.get(&entry.uuid.to_ascii_lowercase()).copied())
.collect()
}
pub fn unread(&self) -> &[String] {
&self.unread
}
}
pub fn attributes_for(
object: &SceneObject,
properties: &[&dyn Reflect],
source: &dyn ValueSource,
) -> (Vec<Attribute>, Vec<String>) {
use energyml_domain::property::{self, IndexableElement};
let point_count = object.geometry.point_count();
let cell_count = object.geometry.cell_count();
let one_line = matches!(
object.geometry.patches.as_slice(),
[patch] if matches!(&patch.topology, crate::Topology::Polylines(runs) if runs.len() == 1)
);
let mut out = Vec::new();
let mut refused = Vec::new();
for property in properties {
let read = match property::read(*property) {
Ok(read) => read,
Err(e) => {
refused.push(format!("a property did not read: {e}"));
continue;
}
};
if read
.supporting_representation
.as_deref()
.map(str::to_ascii_lowercase)
!= Some(object.uuid.to_ascii_lowercase())
{
continue;
}
let scope = match read.indexable {
IndexableElement::Nodes => AttributeScope::Points,
IndexableElement::Intervals if one_line => AttributeScope::Points,
IndexableElement::Cells
| IndexableElement::Faces
| IndexableElement::Triangles
| IndexableElement::Columns
| IndexableElement::Intervals => AttributeScope::Cells,
IndexableElement::IntervalsFromDatum => AttributeScope::Points,
other => {
refused.push(format!(
"`{}` hangs on {other}, which is neither the points nor the cells of the \
exported geometry; it was not attached",
read.title.as_deref().unwrap_or(&read.uuid)
));
continue;
}
};
let mut values = Vec::new();
let mut failed = None;
for patch in &read.patches {
match patch.read(source) {
Ok(read) => match (read.floating(), read.integer()) {
(Some(v), _) => values.extend_from_slice(v),
(_, Some(v)) => values.extend(v.iter().map(|n| *n as f64)),
_ => failed = Some("its values are neither numbers nor integers".to_string()),
},
Err(e) => failed = Some(e.to_string()),
}
}
if let Some(reason) = failed {
refused.push(format!(
"`{}`: {reason}",
read.title.as_deref().unwrap_or(&read.uuid)
));
continue;
}
let frame_intervals = read.indexable == IndexableElement::IntervalsFromDatum
|| (read.indexable == IndexableElement::Intervals && one_line);
if frame_intervals {
let components = read.values_per_element().max(1) as usize;
let segments = point_count.saturating_sub(cell_count);
if segments > 0 && values.len() == segments * components {
values = intervals_onto_nodes(&values, components, point_count, segments);
refused.push(format!(
"`{}` hangs on {}: its {segments} interval value(s) were moved onto \
the {point_count} node(s), each taking the interval below it and the \
deepest one the interval above. Nothing was averaged - these are \
categories - but the correspondence is this build's rule, not the \
standard's.",
read.title.as_deref().unwrap_or(&read.uuid),
read.indexable
));
}
}
let expected = match scope {
AttributeScope::Points => point_count,
AttributeScope::Cells => cell_count,
};
let mut components = read.values_per_element().max(1);
if components > 1 && expected > 0 && values.len() == expected {
refused.push(format!(
"`{}` declares {components} values per element and holds exactly one; it is \
attached as a scalar array, and the document's own count is what is wrong",
read.title.as_deref().unwrap_or(&read.uuid)
));
components = 1;
}
if expected == 0 || values.len() != expected * components as usize {
refused.push(format!(
"`{}` has {} value(s) for {expected} {} × {components} component(s); it was not \
attached, because lining it up would put values on elements they do not belong to",
read.title.as_deref().unwrap_or(&read.uuid),
values.len(),
match scope {
AttributeScope::Points => "point(s)",
AttributeScope::Cells => "cell(s)",
}
));
continue;
}
out.push(Attribute {
name: read.title.clone().unwrap_or_else(|| read.uuid.clone()),
kind: match &read.kind {
Some(energyml_domain::property::PropertyKindRef::Standard { name }) => Some(name.clone()),
Some(energyml_domain::property::PropertyKindRef::Object { title, .. }) => title.clone(),
None => None,
},
uom: read.uom.clone(),
scope,
components,
values,
uuid: read.uuid.clone(),
lut: Vec::new(),
});
}
(out, refused)
}
const INTERPRETS: &[&str] = &["RepresentedInterpretation", "RepresentedObject"];
const REPRESENTS_FEATURE: &[&str] = &["InterpretedFeature", "InterpretedObject"];
pub fn interpretation_of(
representation: &dyn Reflect,
source: &dyn energyml_domain::source::ObjectSource,
) -> Option<Box<dyn energyml_core::registry::DynObject>> {
INTERPRETS
.iter()
.find_map(|field| match representation.field(field) {
Some(FieldValue::Object(reference)) => crate::follow::object_of(source, reference),
_ => None,
})
}
fn related(object: &dyn Reflect, fields: &[&str]) -> Option<Related> {
for field in fields {
let Some(FieldValue::Object(reference)) = object.field(field) else {
continue;
};
let uuid = energyml_core::identity::uuid_of_reflect(reference)?;
if uuid.is_empty() {
continue;
}
return Some(Related {
uuid: uuid.to_string(),
title: string_field(reference, "Title").unwrap_or_default(),
qualified_type: string_field(reference, "QualifiedType")
.or_else(|| string_field(reference, "ContentType"))
.unwrap_or_default(),
});
}
None
}
fn title_of(object: &dyn Reflect) -> String {
match object.field("Citation") {
Some(FieldValue::Object(citation)) => string_field(citation, "Title").unwrap_or_default(),
_ => String::new(),
}
}
fn string_field(object: &dyn Reflect, field: &str) -> Option<String> {
match object.field(field)? {
FieldValue::Scalar(scalar) => scalar.as_str().map(str::to_string).filter(|s| !s.is_empty()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_node_takes_one_interval_and_nothing_is_averaged() {
let moved = intervals_onto_nodes(&[1.0, 2.0], 1, 3, 2);
assert_eq!(
moved,
vec![1.0, 2.0, 2.0],
"node 0 opens interval 0, node 1 opens interval 1, node 2 closes it"
);
assert!(moved.iter().all(|v| *v == 1.0 || *v == 2.0));
}
#[test]
fn two_nodes_and_one_interval_both_take_it() {
assert_eq!(intervals_onto_nodes(&[7.0], 1, 2, 1), vec![7.0, 7.0]);
}
#[test]
fn components_travel_together() {
let values = [10.0, 11.0, 12.0, 20.0, 21.0, 22.0];
assert_eq!(
intervals_onto_nodes(&values, 3, 3, 2),
vec![10.0, 11.0, 12.0, 20.0, 21.0, 22.0, 20.0, 21.0, 22.0]
);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Properties<'a> {
#[default]
None,
All,
Only(&'a [String]),
}
#[derive(Clone, Copy, Default)]
pub struct Request<'a> {
pub of_type: Option<&'a str>,
pub only: &'a [String],
pub readers: Option<&'a Readers>,
pub spec: Option<&'a Spec>,
pub properties: Properties<'a>,
pub onto: Option<crate::transfer::Scope>,
pub method: Option<crate::transfer::Method>,
pub colours: Option<&'a dyn Decide>,
}
#[derive(Debug, Default)]
pub struct Scene {
pub objects: Vec<SceneObject>,
pub unreadable: Vec<String>,
pub refused: Vec<String>,
pub unattached: Vec<String>,
}
pub fn of_package(
resources: &[energyml_storage::ObjectSummary],
source: &dyn ValueSource,
request: &Request<'_>,
) -> Scene {
let readers = match request.readers {
Some(readers) => readers,
None => crate::readers::default_readers(),
};
let mut out = Scene::default();
for summary in resources {
if let Some(filter) = request.of_type
&& !energyml_storage::resource::type_matches(&summary.qualified_type, filter)
{
continue;
}
if !request.only.is_empty()
&& !request
.only
.iter()
.any(|uuid| uuid.eq_ignore_ascii_case(&summary.uuid))
{
continue;
}
let object = match source.read_object_at(&summary.uri) {
Ok(object) => object,
Err(why) => {
out.unreadable
.push(format!("{} {} {why}", summary.uuid, summary.qualified_type));
continue;
}
};
if !geometry::is_supported(object.as_reflect()) {
continue;
}
match object_of_with(readers, object.as_reflect(), source, &summary.uri, request.spec) {
Ok(built) => out.objects.push(built),
Err(why) => out
.refused
.push(format!("{} {} {why}", summary.uuid, summary.qualified_type)),
}
}
attach(resources, source, request, &mut out);
out
}
fn attach(
resources: &[energyml_storage::ObjectSummary],
source: &dyn ValueSource,
request: &Request<'_>,
out: &mut Scene,
) {
let wanted: &[String] = match request.properties {
Properties::None => return,
Properties::All => &[],
Properties::Only(only) => only,
};
let mut properties: Vec<Box<dyn energyml_core::registry::DynObject>> = Vec::new();
for summary in resources {
if !summary.qualified_type.ends_with("Property") {
continue;
}
if !wanted.is_empty() && !wanted.iter().any(|uuid| uuid.eq_ignore_ascii_case(&summary.uuid)) {
continue;
}
match source.read_object_at(&summary.uri) {
Ok(object) => properties.push(object),
Err(why) => out.unattached.push(format!("{} {why}", summary.uuid)),
}
}
let reflected: Vec<&dyn Reflect> = properties.iter().map(|one| one.as_reflect()).collect();
let by_representation = PropertiesOf::new(&reflected);
out.unattached.extend(by_representation.unread().iter().cloned());
let sets: Vec<energyml_domain::color::GraphicalSet> = resources
.iter()
.filter(|summary| summary.qualified_type.ends_with("GraphicalInformationSet"))
.filter_map(|summary| source.object_at(&summary.uri))
.map(|object| energyml_domain::color::GraphicalSet::read(object.as_reflect()))
.collect();
let set = (!sets.is_empty()).then(|| energyml_domain::color::GraphicalSet::merged(sets));
let kinds = energyml_domain::property_kind::Resolver::new(source);
for object in &mut out.objects {
let (mut attributes, refused) = attributes_for(object, &by_representation.of(&object.uuid), source);
out.unattached.extend(refused);
if let Some(target) = request.onto {
let method = request.method.unwrap_or(crate::transfer::Method::Average);
out.unattached
.extend(move_attributes(&mut attributes, target, method, &object.geometry));
}
if let Some(set) = &set {
let cascade = energyml_domain::color::Cascade::new(set);
for attribute in &mut attributes {
attribute.lut = colour_ramp(source, &cascade, request.colours, &kinds, object, attribute);
}
}
object.attributes = attributes;
}
}
pub fn move_attributes(
attributes: &mut [Attribute],
target: crate::transfer::Scope,
method: crate::transfer::Method,
geometry: &Geometry,
) -> Vec<String> {
use crate::transfer::{self, Scope};
let mut refused = Vec::new();
for attribute in attributes.iter_mut() {
let from = match attribute.scope {
AttributeScope::Points => Scope::Points,
AttributeScope::Cells => Scope::Cells,
};
if from == target {
continue;
}
match transfer::transfer(
&attribute.values,
attribute.components as usize,
from,
target,
method,
geometry,
) {
Ok(values) => {
attribute.values = values;
attribute.scope = match target {
Scope::Points => AttributeScope::Points,
Scope::Cells => AttributeScope::Cells,
};
attribute.name = format!("{}__{from}_to_{target}_by_{}", attribute.name, method.name());
}
Err(why) => refused.push(format!(
"`{}` was not moved onto the {target}: {why}",
attribute.name
)),
}
}
refused
}