use energyml_core::naming;
use energyml_core::reflect::TypeKind;
use energyml_core::registry::Registry;
use serde::{Deserialize, Serialize};
const SYMBOL_OWNER: &str = "GraphicalInformationForNodes";
const SYMBOL_FIELD: &str = "Symbol";
pub const CONTOUR: &str = "contour";
pub const PLACES: [&str; 3] = ["head", "bottom", "md"];
const DRAWN: [&str; 11] = [
"circle",
"cross",
"cube",
"diamond",
"plus",
"point",
"pyramid",
"sphere",
"star",
"tetrahedron",
CONTOUR,
];
pub fn is_drawn(id: &str) -> bool {
DRAWN.contains(&id)
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeclaredShape {
pub id: String,
pub label: String,
pub label_key: String,
pub format: Vec<(String, String)>,
pub drawn: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeclaredPlace {
pub id: String,
pub label: String,
pub label_key: String,
pub takes_value: bool,
}
pub fn declared(registry: &Registry) -> (Vec<DeclaredShape>, Vec<DeclaredPlace>) {
let mut shapes: Vec<DeclaredShape> = Vec::new();
for info in registry
.types()
.filter(|t| t.kind == TypeKind::Struct && t.name == SYMBOL_OWNER)
{
let Some(field) = info.field(SYMBOL_FIELD) else {
continue;
};
let Some(enumeration) = field.ty.type_info() else {
continue;
};
let qualified = format!(
"{}{}.{}",
info.package,
naming::compact_version(naming::release_of(enumeration.version)),
enumeration.name
);
for variant in field.ty.allowed_values().iter().filter(|v| !v.is_other) {
let pair = (qualified.clone(), variant.value.to_string());
match shapes.iter_mut().find(|s| s.id == variant.value) {
Some(shape) => {
if !shape.format.contains(&pair) {
shape.format.push(pair);
}
}
None => shapes.push(shape(variant.value, vec![pair])),
}
}
}
shapes.push(shape(CONTOUR, Vec::new()));
let places = PLACES
.iter()
.map(|&id| DeclaredPlace {
id: id.to_string(),
label: match id {
"head" => "Top of the well",
"bottom" => "Bottom of the well",
_ => "At a measured depth",
}
.to_string(),
label_key: format!("sectionView.markers.place.{id}"),
takes_value: id == "md",
})
.collect();
(shapes, places)
}
fn shape(id: &str, format: Vec<(String, String)>) -> DeclaredShape {
let label = if id == CONTOUR {
"Contour (a wide line under the object)".to_string()
} else {
let mut chars = id.chars();
chars
.next()
.map(|first| first.to_uppercase().chain(chars).collect())
.unwrap_or_default()
};
DeclaredShape {
id: id.to_string(),
label,
label_key: format!("sectionView.markers.shape.{id}"),
format,
drawn: is_drawn(id),
}
}
pub const DEFAULT_SIZE_PX: f64 = 12.0;
pub fn from_spec(
text: &str,
rows: &[crate::model::ObjectRow],
shown_in: Option<crate::model::SectionKey>,
) -> Result<crate::model::Marker, String> {
use crate::model::{Marker, MarkerPlace, ObjectRow, Outcome};
use energyml_storage::resource::{title_matches, type_matches};
let (name, rest) = text
.split_once(':')
.ok_or_else(|| format!("`{text}`: expected NAME:shape=…,colour=…,at=…"))?;
let name = name.trim();
if name.is_empty() {
return Err(format!("`{text}`: a marker needs a name, the legend shows it"));
}
let (mut shape, mut colour, mut at, mut md, mut size, mut types, mut title) =
(None, None, None, None, None, None, None);
for pair in rest.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let (key, value) = pair
.split_once('=')
.ok_or_else(|| format!("`{pair}`: expected name=value"))?;
let value = value.trim().to_string();
match key.trim() {
"shape" => shape = Some(value),
"colour" | "color" => colour = Some(value),
"at" => at = Some(value),
"md" => {
md = Some(
value
.parse::<f64>()
.map_err(|_| format!("`md={value}` is not a number"))?,
)
}
"size" => {
size = Some(
value
.parse::<f64>()
.map_err(|_| format!("`size={value}` is not a number"))?,
)
}
"type" => types = Some(value),
"title" => title = Some(value),
other => {
return Err(format!(
"a marker has no parameter `{other}`: it has [shape, colour, at, md, size, type, title]"
));
}
}
}
let shape = shape.ok_or("a marker needs `shape=`")?;
if !is_drawn(&shape) {
return Err(format!(
"shape `{shape}` is not one this build draws: one of {}",
DRAWN.join(", ")
));
}
let colour = colour.ok_or("a marker needs `colour=` (#rrggbb or #rrggbbaa)")?;
let hex = colour.strip_prefix('#').unwrap_or("");
if !matches!(hex.len(), 6 | 8) || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!("colour `{colour}`: expected #rrggbb or #rrggbbaa"));
}
let at = match (at.as_deref(), md) {
(Some("head"), None) => MarkerPlace::Head,
(Some("bottom"), None) => MarkerPlace::Bottom,
(Some("md"), Some(value)) => MarkerPlace::Md { value },
(Some("md"), None) => return Err("`at=md` needs `md=VALUE`, in the well's MD unit".into()),
(Some("head" | "bottom"), Some(_)) => return Err("`md=` goes with `at=md` only".into()),
(Some(other), _) => return Err(format!("`at={other}`: one of {}", PLACES.join(", "))),
(None, _) => return Err(format!("a marker needs `at=`: one of {}", PLACES.join(", "))),
};
let size_px = size.unwrap_or(DEFAULT_SIZE_PX);
if !(size_px.is_finite() && size_px > 0.0) {
return Err(format!("`size={size_px}`: a size is a positive number of pixels"));
}
if types.is_none() && title.is_none() {
return Err("a marker names its objects: give `type=` or `title=` (or both)".into());
}
let drawn_in = |row: &ObjectRow| match &row.outcome {
Outcome::Drawn { sections } => shown_in.is_none_or(|key| sections.contains(&key)),
_ => false,
};
let shown = |row: &ObjectRow| match &row.outcome {
Outcome::Merged { into } => rows.iter().any(|other| other.object == *into && drawn_in(other)),
_ => drawn_in(row),
};
let keys: Vec<String> = rows
.iter()
.filter(|row| shown(row))
.filter(|row| {
types.as_deref().is_none_or(|types| {
types
.split('|')
.any(|one| type_matches(&row.object.qualified_type, one.trim()))
}) && title
.as_deref()
.is_none_or(|title| title_matches(&row.object.title, title))
})
.map(|row| row.object.key())
.collect();
if keys.is_empty() {
return Err(format!("marker `{name}` matches no object of the section"));
}
Ok(Marker {
name: name.to_string(),
shape,
colour,
at,
keys,
size_px,
hidden: false,
})
}