use std::collections::{HashMap, HashSet};
use energyml_core::registry::DynObject;
use energyml_core::value::{FieldValue, Reflect};
use energyml_domain::property;
use energyml_domain::wellbore_table::{self as wt, Column, From, Merged, WellTable};
use energyml_storage::ObjectSummary;
use crate::source::ValueSource;
#[derive(Debug, Clone)]
pub struct Export {
pub merged: Merged,
pub tables: Vec<WellTable>,
pub notes: Vec<String>,
}
type UriAndObject = (String, Box<dyn DynObject>);
pub fn of_store(summaries: &[ObjectSummary], source: &dyn ValueSource, wanted: &[String]) -> Export {
let mut notes = Vec::new();
let mut by_support: HashMap<String, Vec<UriAndObject>> = HashMap::new();
for summary in summaries
.iter()
.filter(|one| one.qualified_type.ends_with("Property"))
{
let Some(object) = source.object_at(&summary.uri) else {
continue;
};
if property::read(object.as_reflect()).is_err() {
continue;
}
if let Some(support) = crate::well::reference_of(object.as_reflect(), "SupportingRepresentation") {
let bucket = energyml_core::identity::parse_address(&support).0;
by_support.entry(bucket).or_default().push((support, object));
}
}
let mut tables = Vec::new();
for summary in summaries
.iter()
.filter(|one| one.qualified_type.contains("WellboreFrameRepresentation"))
{
let Some(frame) = source.object_at(&summary.uri) else {
notes.push(format!("{}: the frame does not read", summary.uri));
continue;
};
let title = energyml_core::identity::title_of_reflect(frame.as_reflect())
.unwrap_or_default()
.to_string();
if !wanted.is_empty()
&& !wanted.iter().any(|one| {
one.eq_ignore_ascii_case(&summary.uuid)
|| *one == summary.uri
|| (!one.is_empty() && title.contains(one.as_str()))
})
{
continue;
}
let properties = by_support.get(&summary.uuid.to_ascii_lowercase());
let borrowed: Vec<&dyn Reflect> = properties
.map(|all| {
all.iter()
.filter(|(support, _)| crate::well::same_uri(support, &summary.uri))
.map(|(_, one)| one.as_reflect())
.collect()
})
.unwrap_or_default();
match wt::of_frame(frame.as_reflect(), &borrowed, source) {
Ok(table) => {
notes.extend(table.notes.iter().map(|one| format!("{}: {one}", table.well)));
tables.push(around(
table,
frame.as_reflect(),
&summary.uri,
source,
&mut notes,
));
}
Err(why) => notes.push(format!("{}: {why}", summary.uri)),
}
}
name_headers(&mut tables);
let mut merged = in_order(wt::merge(&tables), &tables);
if let Some(first) = merged.headers.first_mut() {
*first = "well name".to_string();
}
Export {
merged,
tables,
notes,
}
}
fn around(
table: WellTable,
frame: &dyn Reflect,
uri: &str,
source: &dyn ValueSource,
notes: &mut Vec<String>,
) -> WellTable {
let rows = table.rows;
let constant = |title: &str, what: &'static str, value: Option<String>| Column {
title: title.to_string(),
uom: None,
kind: None,
kind_title: Some(title.to_string()),
from: From::Wellbore(what),
values: vec![value; rows],
};
let interpretation = follow(source, frame, "RepresentedObject")
.or_else(|| follow(source, frame, "RepresentedInterpretation"));
let feature = interpretation
.as_ref()
.and_then(|one| follow(source, one.as_reflect(), "InterpretedFeature"));
let well = feature
.as_ref()
.and_then(|one| energyml_core::identity::title_of_reflect(one.as_reflect()))
.map(|title| wt::stem(title).to_string())
.unwrap_or_else(|| table.well.clone());
let mut columns: Vec<Column> = Vec::new();
let mut seen: HashMap<String, usize> = HashMap::new();
for owner in feature
.iter()
.map(|one| one.as_reflect())
.chain(std::iter::once(frame))
{
for (authority, identifier) in aliases(owner) {
let count = seen.entry(authority.clone()).or_default();
*count += 1;
let title = if *count == 1 {
authority.clone()
} else {
format!("{authority} {count}")
};
let mut column = constant(&title, "alias", Some(identifier));
column.kind_title = None;
columns.push(column);
}
}
let head = follow(source, frame, "Trajectory")
.ok_or_else(|| "the frame names no trajectory".to_string())
.and_then(|trajectory| {
crate::well::head_of(trajectory.as_reflect(), source).map_err(|why| why.to_string())
});
match head {
Ok([x, y, z]) => {
columns.push(constant("x coordinate", "head", Some(number(x))));
columns.push(constant("y coordinate", "head", Some(number(y))));
columns.push(constant("elevation", "head", Some(number(z))));
}
Err(why) => notes.push(format!("{well}: no wellhead - {why}")),
}
let placed = crate::scene::object_of(frame, source, uri);
let mut nodes: Option<Vec<[f64; 3]>> = None;
match &placed {
Ok(object) => {
if let Some(crs) = &object.crs {
columns.push(constant(
"projection code",
"crs",
crs.projected.epsg().map(|code| code.to_string()),
));
columns.push(constant(
"vertical projection code",
"crs",
crs.vertical.epsg().map(|code| code.to_string()),
));
}
let points: Vec<[f64; 3]> = object
.geometry
.patches
.iter()
.flat_map(|patch| patch.points.iter().copied())
.collect();
let by_passes = table
.columns
.first()
.is_some_and(|one| one.from == From::TopDepth);
if points.len() == rows {
nodes = Some(points);
} else if by_passes && points.len() == rows + 1 {
nodes = Some(points[1..].to_vec());
} else {
notes.push(format!(
"{well}: {} placed point(s) for {rows} node(s), so no node coordinate is written",
points.len()
));
}
}
Err(why) => notes.push(format!("{well}: the nodes are not placed - {why}")),
}
let by_passes = table
.columns
.first()
.is_some_and(|one| one.from == From::TopDepth);
let axes = if by_passes { 2 } else { 1 };
let mut properties = table.columns.into_iter();
columns.extend(properties.by_ref().take(axes));
if let Some(points) = nodes {
for (axis, title) in [(0, "node x"), (1, "node y"), (2, "node z")] {
columns.push(Column {
title: title.to_string(),
uom: None,
kind: None,
kind_title: None,
from: From::Wellbore("node"),
values: points.iter().map(|point| Some(number(point[axis]))).collect(),
});
}
}
columns.extend(properties);
let mut frame_uuid = constant("wellbore frame uuid", "uuid", Some(table.uuid.clone()));
frame_uuid.kind_title = None;
columns.push(frame_uuid);
if let Some(feature) = &feature {
let mut feature_uuid = constant(
"wellbore feature uuid",
"uuid",
energyml_core::identity::uuid_of_reflect(feature.as_reflect()).map(str::to_string),
);
feature_uuid.kind_title = None;
columns.push(feature_uuid);
}
WellTable {
well,
uuid: table.uuid,
columns,
rows,
notes: table.notes,
}
}
fn in_order(merged: Merged, tables: &[WellTable]) -> Merged {
let rank = |from: &From| -> u8 {
match from {
From::Wellbore("alias") => 1,
From::Wellbore("head") => 2,
From::Wellbore("crs") => 3,
From::TopDepth | From::MeasuredDepth => 4,
From::Wellbore("node") => 5,
From::Property(_) => 6,
From::Wellbore(_) => 7,
}
};
let mut of_header: HashMap<&str, u8> = HashMap::new();
for table in tables {
for column in &table.columns {
of_header
.entry(column.title.as_str())
.or_insert_with(|| rank(&column.from));
}
}
let mut positions: Vec<usize> = (1..merged.headers.len()).collect();
positions.sort_by_key(|at| {
of_header
.get(merged.headers[*at].as_str())
.copied()
.unwrap_or(u8::MAX)
});
let order: Vec<usize> = std::iter::once(0).chain(positions).collect();
Merged {
headers: order.iter().map(|at| merged.headers[*at].clone()).collect(),
rows: merged
.rows
.iter()
.map(|row| order.iter().map(|at| row.get(*at).cloned().flatten()).collect())
.collect(),
conflicts: merged.conflicts,
}
}
fn name_headers(tables: &mut [WellTable]) {
let key = |column: &Column| -> String {
match &column.from {
From::Property(_) => format!("property:{}", column.title),
From::MeasuredDepth => "md".to_string(),
From::TopDepth => "top".to_string(),
From::Wellbore(what) => format!("{what}:{}", column.title),
}
};
let mut order: Vec<(String, String, Option<String>, bool)> = Vec::new();
let mut known: HashSet<String> = HashSet::new();
for table in tables.iter() {
for column in &table.columns {
let id = key(column);
if known.insert(id.clone()) {
let structural = !matches!(column.from, From::Property(_));
order.push((id, column.title.clone(), column.kind_title.clone(), structural));
}
}
}
let mut claims: HashMap<String, usize> = HashMap::new();
for (_, _, preferred, _) in &order {
if let Some(preferred) = preferred {
*claims.entry(preferred.clone()).or_default() += 1;
}
}
let mut sorted: Vec<&(String, String, Option<String>, bool)> = order.iter().collect();
sorted.sort_by_key(|(_, _, _, structural)| !structural);
let mut used: HashSet<String> = HashSet::new();
let mut header: HashMap<String, String> = HashMap::new();
for (id, title, preferred, _) in sorted {
let mut chosen = match preferred {
Some(preferred) if claims.get(preferred) == Some(&1) && preferred != "unknown" => {
preferred.clone()
}
_ => title.clone(),
};
if used.contains(&chosen) {
chosen = title.clone();
}
let mut rank = 2;
let base = chosen.clone();
while used.contains(&chosen) {
chosen = format!("{base} ({rank})");
rank += 1;
}
used.insert(chosen.clone());
header.insert(id.clone(), chosen);
}
for table in tables.iter_mut() {
for column in &mut table.columns {
if let Some(name) = header.get(&key(column)) {
column.title = name.clone();
}
}
}
}
fn follow(source: &dyn ValueSource, object: &dyn Reflect, field: &str) -> Option<Box<dyn DynObject>> {
let FieldValue::Object(reference) = object.field(field)? else {
return None;
};
crate::follow::object_of(source, reference)
}
fn aliases(object: &dyn Reflect) -> Vec<(String, String)> {
let Some(FieldValue::List(items)) = object.field("Aliases") else {
return Vec::new();
};
items
.iter()
.filter_map(|item| {
let FieldValue::Object(alias) = item else {
return None;
};
let text = |field: &str| {
alias
.field(field)
.and_then(|value| value.as_scalar())
.map(|one| {
one.as_str()
.map(str::to_string)
.unwrap_or_else(|| one.to_string())
})
.filter(|one| !one.trim().is_empty())
};
let identifier = text("Identifier")?;
let authority = text("authority").or_else(|| text("Authority"));
let kind = text("IdentifierKind");
let header = match (kind, authority) {
(Some(kind), Some(authority)) => format!("{kind} ({authority})"),
(Some(one), None) | (None, Some(one)) => one,
(None, None) => "alias".to_string(),
};
Some((header, identifier))
})
.collect()
}
fn number(value: f64) -> String {
if value == value.trunc() && value.abs() < 1e15 {
format!("{}", value as i64)
} else {
format!("{value}")
}
}