use energyml_core::value::Reflect;
use crate::error::{Error, Result};
use crate::geometry::{Patch, Topology};
use crate::points::Point;
use crate::source::ValueSource;
use crate::values::{self, scalar_f64};
use energyml_domain::units;
pub(crate) fn datum_trajectory(
representation: &dyn Reflect,
source: &dyn ValueSource,
at: &str,
) -> Result<Vec<Patch>> {
for field in ["WitsmlTrajectory", "ParentIntersection"] {
if representation
.field(field)
.is_some_and(|value| value.as_object().is_some())
{
return Err(Error::unsupported(
"a wellbore trajectory whose shape is deferred to another object",
format!("{at}.{field}"),
));
}
}
let anchor = Anchor::of(representation, source, at)?;
let top = anchor.at(anchor.md_min);
let bottom = anchor.at(anchor.md_max);
Ok(vec![Patch {
index: 0,
node_offset: 0,
points: vec![top, bottom],
topology: Topology::Polylines(vec![2]),
closed: Vec::new(),
}])
}
pub fn head_of(trajectory: &dyn Reflect, source: &dyn ValueSource) -> Result<[f64; 3]> {
let anchor = Anchor::of(trajectory, source, "WellboreTrajectoryRepresentation")?;
Ok([anchor.x, anchor.y, anchor.elevation])
}
pub fn path_of(trajectory: &dyn Reflect, source: &dyn ValueSource) -> Result<crate::parametric::Path> {
let at = "WellboreTrajectoryRepresentation";
let Some(geometry) = crate::geometry::field_object(trajectory, "Geometry") else {
return Err(Error::unsupported(
"a wellbore trajectory with no `Geometry` - its only placement is the vertical reading from its datum, an assumption",
at,
));
};
let path = crate::parametric::Path::of(geometry, source)?;
if path.knots().is_none() {
return Err(Error::malformed(
at,
"a line that states no `ControlPointParameters`: its knots are then 0, 1, 2 โฆ, and no parameter along it is a measured depth",
));
}
Ok(path)
}
pub(crate) fn frame(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
let at = "WellboreFrameRepresentation";
let Some(node_md) = crate::geometry::field_object(representation, "NodeMd") else {
return Err(Error::malformed(at, "a frame with no `NodeMd`"));
};
let mds = values::read_doubles(node_md, source, &format!("{at}.NodeMd"))?;
if mds.is_empty() {
return Err(Error::malformed(at, "a frame with no measured depth"));
}
let Some(trajectory_uri) = reference_of(representation, "Trajectory") else {
return Err(Error::malformed(at, "a frame that names no trajectory"));
};
let Some(trajectory) = source.object_at(&trajectory_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the trajectory {trajectory_uri}"
))));
};
let trajectory = trajectory.as_reflect();
let points: Vec<Point> = match crate::geometry::field_object(trajectory, "Geometry") {
Some(geometry) => {
let line = crate::parametric::Line::single(geometry, source, at)?;
mds.iter().map(|md| line.at(*md)).collect()
}
None => {
let anchor = Anchor::of(trajectory, source, at)?;
mds.iter().map(|md| anchor.at(*md)).collect()
}
};
let count = points.len() as u32;
Ok(vec![Patch {
index: 0,
node_offset: 0,
points,
topology: Topology::Polylines(vec![count]),
closed: Vec::new(),
}])
}
const HEAD_SAMPLES: usize = 17;
pub fn head_portion(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Option<Vec<Point>>> {
let at = "WellboreFrameRepresentation";
let Some(node_md) = crate::geometry::field_object(representation, "NodeMd") else {
return Err(Error::malformed(at, "a frame with no `NodeMd`"));
};
let mds = values::read_doubles(node_md, source, &format!("{at}.NodeMd"))?;
let Some(&first_md) = mds.first() else {
return Err(Error::malformed(at, "a frame with no measured depth"));
};
if first_md <= 0.0 {
return Ok(None);
}
let Some(trajectory_uri) = reference_of(representation, "Trajectory") else {
return Err(Error::malformed(at, "a frame that names no trajectory"));
};
let Some(trajectory) = source.object_at(&trajectory_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the trajectory {trajectory_uri}"
))));
};
let trajectory = trajectory.as_reflect();
let anchor = Anchor::of(trajectory, source, "WellboreTrajectoryRepresentation");
let geometry = crate::geometry::field_object(trajectory, "Geometry");
let points = match (geometry, anchor.as_ref()) {
(Some(geometry), Ok(anchor)) if anchor.md_min <= 0.0 => {
let line = crate::parametric::Line::single(geometry, source, at)?;
(0..HEAD_SAMPLES)
.map(|i| first_md * (i as f64) / (HEAD_SAMPLES as f64 - 1.0))
.map(|md| line.at(md))
.collect()
}
(_, Ok(anchor)) => {
vec![anchor.at(0.0), anchor.at(first_md)]
}
(_, Err(_)) => {
return Err(Error::unsupported(
"a frame's head portion: neither the trajectory's own shape nor its datum can be read",
at,
));
}
};
Ok(Some(points))
}
const MD_INTERVAL: &str = "MdInterval";
const MD_MIN: &[&str] = &["MdMin", "StartMd"];
const MD_MAX: &[&str] = &["MdMax", "FinishMd"];
const MD_UOM: &[&str] = &["Uom", "MdUom"];
const MD_DATUM: &[&str] = &["Datum", "MdDatum"];
const DATUM_TYPES: &[&str] = energyml_domain::crs::DATUM_TYPES;
struct Anchor {
x: f64,
y: f64,
elevation: f64,
md_to_metres: f64,
md_min: f64,
md_max: f64,
}
impl Anchor {
fn coordinates(datum: &dyn Reflect, at: &str) -> Result<(f64, f64, f64)> {
if let Some(horizontal) = crate::geometry::field_object(datum, "HorizontalCoordinates") {
let x = scalar_f64(horizontal, "Coordinate1")
.ok_or_else(|| Error::malformed(at, "a reference point with no `Coordinate1`"))?;
let y = scalar_f64(horizontal, "Coordinate2")
.ok_or_else(|| Error::malformed(at, "a reference point with no `Coordinate2`"))?;
let vertical = scalar_f64(datum, "VerticalCoordinate")
.ok_or_else(|| Error::malformed(at, "a reference point with no `VerticalCoordinate`"))?;
return Ok((x, y, vertical));
}
let point = crate::geometry::field_object(datum, "Location")
.and_then(|location| crate::geometry::field_object(location, "Point3d"))
.ok_or_else(|| {
Error::malformed(at, "a datum with neither horizontal coordinates nor a `Location`")
})?;
let x = scalar_f64(point, "Coordinate1")
.ok_or_else(|| Error::malformed(at, "a datum location with no `Coordinate1`"))?;
let y = scalar_f64(point, "Coordinate2")
.ok_or_else(|| Error::malformed(at, "a datum location with no `Coordinate2`"))?;
let z = scalar_f64(point, "Coordinate3")
.ok_or_else(|| Error::malformed(at, "a datum location with no `Coordinate3`"))?;
Ok((x, y, z))
}
fn at(&self, md: f64) -> Point {
[self.x, self.y, self.elevation - md * self.md_to_metres]
}
fn of(trajectory: &dyn Reflect, source: &dyn ValueSource, at: &str) -> Result<Self> {
let interval = crate::geometry::field_object(trajectory, MD_INTERVAL).unwrap_or(trajectory);
let (Some(md_min), Some(md_max)) = (scalar_any(interval, MD_MIN), scalar_any(interval, MD_MAX))
else {
return Err(Error::unsupported(
"a wellbore trajectory with neither `Geometry` nor measured-depth bounds (`MdInterval`, or `StartMd`/`FinishMd`)",
at,
));
};
if md_min > md_max {
return Err(Error::malformed(
at,
format!("MdMin {md_min} is greater than MdMax {md_max}"),
));
}
let uom = string_any(interval, MD_UOM).unwrap_or_else(|| "m".to_string());
let md_to_metres = units::metres(1.0, &uom).map_err(Error::Domain)?;
let Some(datum_uri) = reference_any(interval, MD_DATUM) else {
return Err(Error::malformed(at, "depth bounds with no datum"));
};
let placed = place_datum(&datum_uri, source, at)?;
Ok(Self {
x: placed.x,
y: placed.y,
elevation: placed.elevation,
md_to_metres,
md_min,
md_max,
})
}
}
pub(crate) struct PlacedDatum {
pub(crate) x: f64,
pub(crate) y: f64,
pub(crate) elevation: f64,
pub(crate) compound: Option<energyml_domain::Crs>,
}
pub(crate) fn place_datum(datum_uri: &str, source: &dyn ValueSource, at: &str) -> Result<PlacedDatum> {
let Some(datum) = source.object_at(datum_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the datum {datum_uri}"
))));
};
let datum = datum.as_reflect();
let type_key = datum.type_info().type_key;
if !DATUM_TYPES.contains(&type_key) {
return Err(Error::unsupported(
crate::geometry::leak(type_key),
format!("{at}.Datum"),
));
}
let (x, y, vertical) = Anchor::coordinates(datum, at)?;
if let Some(compound) = energyml_domain::crs::compound_of_datum(datum, source).map_err(Error::Domain)? {
if compound.is_time() {
return Err(Error::unsupported(
"a measured-depth datum placed in a time CRS",
format!("{at}.Datum"),
));
}
let [x, y, up] = compound.to_projected([x, y, vertical]);
let elevation = match compound.vertical_uom.as_deref() {
Some(unit) => units::metres(up, unit).map_err(Error::Domain)?,
None => up,
};
return Ok(PlacedDatum {
x,
y,
elevation,
compound: Some(compound),
});
}
let elevation = match vertical_sense(datum, source, at)? {
VerticalSense::Up => vertical,
VerticalSense::Down => -vertical,
};
Ok(PlacedDatum {
x,
y,
elevation,
compound: None,
})
}
enum VerticalSense {
Up,
Down,
}
fn vertical_sense(datum: &dyn Reflect, source: &dyn ValueSource, at: &str) -> Result<VerticalSense> {
let vertical_crs = reference_of(datum, "VerticalCrs")
.or_else(|| {
crate::geometry::field_object(datum, "Location")
.and_then(|location| reference_of(location, "LocalCrs"))
})
.and_then(|uri| source.object_at(&uri));
let Some(vertical_crs) = vertical_crs else {
return Err(Error::unsupported(
"a reference point whose vertical CRS is not in the package",
at,
));
};
let crs = vertical_crs.as_reflect();
if let Some(code) = epsg_of(crs) {
match code {
5714 => return Ok(VerticalSense::Up),
5715 => return Ok(VerticalSense::Down),
_ => {}
}
}
if let Some(energyml_core::value::FieldValue::Scalar(energyml_core::value::Scalar::Bool(down))) =
crs.field("z_increasing_downward")
{
return Ok(if down {
VerticalSense::Down
} else {
VerticalSense::Up
});
}
match string_of(crs, "Direction").as_deref() {
Some("up") => Ok(VerticalSense::Up),
Some("down") => Ok(VerticalSense::Down),
_ => Err(Error::unsupported("a vertical CRS that states no direction", at)),
}
}
fn epsg_of(crs: &dyn Reflect) -> Option<i64> {
fn walk(object: &dyn Reflect, depth: u8) -> Option<i64> {
if depth > 3 {
return None;
}
for candidate in ["EpsgCode", "Epsg", "Code"] {
if let Some(code) = values::scalar_i64(object, candidate)
&& code > 0
{
return Some(code);
}
}
for field in object.type_info().fields {
if let Some(energyml_core::value::FieldValue::Object(child)) = object.field(field.name)
&& let Some(code) = walk(child, depth + 1)
{
return Some(code);
}
}
None
}
walk(crs, 0)
}
fn scalar_any(object: &dyn Reflect, names: &[&str]) -> Option<f64> {
names.iter().find_map(|name| scalar_f64(object, name))
}
fn string_any(object: &dyn Reflect, names: &[&str]) -> Option<String> {
names.iter().find_map(|name| string_of(object, name))
}
fn reference_any(object: &dyn Reflect, names: &[&str]) -> Option<String> {
names.iter().find_map(|name| reference_of(object, name))
}
pub(crate) fn reference_of(object: &dyn Reflect, field: &str) -> Option<String> {
let energyml_core::value::FieldValue::Object(reference) = object.field(field)? else {
return None;
};
crate::follow::address_of(reference)
}
pub(crate) fn same_uri(a: &str, b: &str) -> bool {
use energyml_core::identifiers::EtpUri;
match (EtpUri::parse(a), EtpUri::parse(b)) {
(Ok(a), Ok(b)) => {
a.uuid == b.uuid
&& match (&a.object_version, &b.object_version) {
(Some(x), Some(y)) => x == y,
_ => true,
}
}
_ => a.eq_ignore_ascii_case(b),
}
}
fn string_of(object: &dyn Reflect, field: &str) -> Option<String> {
match object.field(field)? {
energyml_core::value::FieldValue::Scalar(scalar) => scalar.as_str().map(|s| s.to_string()),
_ => None,
}
}