use crate::min_curvature::{self as stations, Measured};
use energyml_core::measure;
use energyml_core::value::Reflect;
use energyml_domain::units;
use crate::error::{Error, Result};
use crate::geometry::{Patch, Topology, field_list, field_object};
use crate::source::ValueSource;
use crate::well::{PlacedDatum, place_datum, reference_of};
const AT: &str = "Trajectory";
const MINIMUM_CURVATURE: &str = "minimum curvature";
pub(crate) fn declared(trajectory: &dyn Reflect) -> Option<u64> {
Some(field_list(trajectory, "TrajectoryStation").len() as u64)
}
pub(crate) fn read(trajectory: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
let listed = field_list(trajectory, "TrajectoryStation");
if listed.is_empty() {
return Err(Error::malformed(
AT,
"a trajectory with no `TrajectoryStation`: nothing states where the hole is",
));
}
stated_method(trajectory, "NominalCalcAlgorithm", AT)?;
let default_datum = reference_of(trajectory, "DefaultMdDatum");
let mut datum: Option<String> = None;
let mut measured = Vec::with_capacity(listed.len());
for (at, station) in listed.iter().enumerate() {
let here = format!("{AT}.TrajectoryStation[{}]", at + 1);
if measure::enum_value(*station, "StatusTrajStation").as_deref() == Some("rejected") {
return Err(Error::unsupported(
"a trajectory holding a rejected station - whether it is left out is not decided here",
here,
));
}
stated_method(*station, "CalcAlgorithm", &here)?;
let holder =
field_object(*station, "Md").ok_or_else(|| Error::malformed(&here, "a station with no `Md`"))?;
let md = measured_depth(holder, &here)?;
let counted_from = reference_of(holder, "Datum").or_else(|| default_datum.clone()).ok_or_else(|| {
Error::unsupported(
"a WITSML trajectory whose measured depths name no datum - neither `DefaultMdDatum` nor the station's `Md.Datum`",
&here,
)
})?;
match &datum {
None => datum = Some(counted_from),
Some(first) if crate::well::same_uri(first, &counted_from) => {}
Some(first) => {
return Err(Error::unsupported(
"a WITSML trajectory whose stations count their depths from two datums",
format!("{here}: {counted_from}, where the stations before count from {first}"),
));
}
}
measured.push(Measured {
md,
inclination: angle(*station, "Incl", &here)?,
azimuth: angle(*station, "Azi", &here)?,
});
}
let datum = datum.expect("one station at least, each with a datum");
if let Some(interval) = field_object(trajectory, "MdInterval")
&& let Some(stated) = reference_of(interval, "Datum")
&& !crate::well::same_uri(&stated, &datum)
{
return Err(Error::malformed(
AT,
format!(
"`MdInterval.Datum` names {stated} and the stations count from {datum}: the CRS the trajectory is given is read from the first, the hole would be drawn from the second"
),
));
}
if measured[0].md != 0.0 {
return Err(Error::unsupported(
"a WITSML trajectory whose first station is not at its datum - where the hole runs above it is not stated",
format!("{AT}.TrajectoryStation[1]: MD {} m", measured[0].md),
));
}
let located = stations::positions(&measured).map_err(|why| Error::malformed(AT, why))?;
let placed = place_datum(&datum, source, AT)?;
let moves = located.iter().any(|one| one.north != 0.0 || one.east != 0.0);
let horizontal = if moves {
Some(Horizontal::of(trajectory, &datum, &placed, source)?)
} else {
None
};
let points = located
.iter()
.map(|one| {
let (dx, dy) = horizontal
.as_ref()
.map_or((0.0, 0.0), |frame| frame.offset(one.east, one.north));
[placed.x + dx, placed.y + dy, placed.elevation - one.tvd]
})
.collect::<Vec<_>>();
let count = points.len() as u32;
Ok(vec![Patch {
index: 0,
node_offset: 0,
points,
topology: Topology::Polylines(vec![count]),
closed: Vec::new(),
}])
}
fn stated_method(object: &dyn Reflect, field: &str, at: &str) -> Result<()> {
match measure::enum_value(object, field) {
Some(method) if !method.eq_ignore_ascii_case(MINIMUM_CURVATURE) => Err(Error::unsupported(
"a survey computed by a method other than minimum curvature - drawing it by minimum curvature would move the hole from where its producer put it",
format!("{at}.{field} = `{method}`"),
)),
_ => Ok(()),
}
}
fn measured_depth(holder: &dyn Reflect, at: &str) -> Result<f64> {
let found = measure::read(holder)
.or_else(|| field_object(holder, "MeasuredDepth").and_then(measure::read))
.ok_or_else(|| Error::malformed(at, "an `Md` holding no number"))?;
let unit = found
.uom
.ok_or_else(|| Error::malformed(at, "an `Md` with no unit: a length in nothing is not a depth"))?;
Ok(units::metres(found.value, &unit)?)
}
fn angle(station: &dyn Reflect, field: &str, at: &str) -> Result<Option<f64>> {
let Some(holder) = field_object(station, field) else {
return Ok(None);
};
let found =
measure::read(holder).ok_or_else(|| Error::malformed(format!("{at}.{field}"), "no number"))?;
let unit = found.uom.ok_or_else(|| {
Error::malformed(
format!("{at}.{field}"),
"an angle with no unit - degrees or radians is not a guess to make",
)
})?;
Ok(Some(units::convert(found.value, &unit, "dega")?))
}
enum Horizontal {
Grid { metres_per_unit: f64 },
Plant {
compound: Box<energyml_domain::Crs>,
metres_per_unit: f64,
},
}
impl Horizontal {
fn offset(&self, east: f64, north: f64) -> (f64, f64) {
match self {
Horizontal::Grid { metres_per_unit } => (east / metres_per_unit, north / metres_per_unit),
Horizontal::Plant {
compound,
metres_per_unit,
} => {
let there = compound.to_projected([east / metres_per_unit, north / metres_per_unit, 0.0]);
let origin = compound.to_projected([0.0, 0.0, 0.0]);
(there[0] - origin[0], there[1] - origin[1])
}
}
}
fn of(
trajectory: &dyn Reflect,
datum_uri: &str,
placed: &PlacedDatum,
source: &dyn ValueSource,
) -> Result<Self> {
let at = format!("{AT}.AziRef");
let Some(north) = measure::enum_value(trajectory, "AziRef") else {
return Err(Error::unsupported(
"a WITSML trajectory that leaves the vertical and states no `AziRef` - which north its azimuths turn from is not said (WITSML TRG 2.1 §16.7 p. 352)",
at,
));
};
let datum = source
.object_at(datum_uri)
.ok_or_else(|| Error::Missing(format!("the datum {datum_uri}")))?;
let datum = datum.as_reflect();
match (north.as_str(), &placed.compound) {
("grid north", Some(compound)) => {
if let Some(order) = compound_plane(datum, source).and_then(|plane| {
field_object(plane.as_reflect(), "OriginProjectedCrs")
.and_then(|p| measure::enum_value(p, "AxisOrder"))
}) {
east_then_north(&order, &at)?;
}
Ok(Horizontal::Grid {
metres_per_unit: horizontal_unit(compound.horizontal_uom.as_deref(), &at)?,
})
}
("grid north", None) => {
let projected = field_object(datum, "HorizontalCoordinates")
.and_then(|holder| reference_of(holder, "Crs"))
.and_then(|uri| source.object_at(&uri))
.filter(|crs| crs.as_reflect().type_info().type_key == "ProjectedCrs")
.ok_or_else(|| {
Error::unsupported(
"a grid-north survey whose datum is not placed in a projected CRS - the grid it turns from is not named",
&at,
)
})?;
let projected = projected.as_reflect();
match measure::enum_value(projected, "AxisOrder") {
Some(order) => east_then_north(&order, &at)?,
None => {
return Err(Error::unsupported(
"a projected CRS that states no axis order",
&at,
));
}
}
Ok(Horizontal::Grid {
metres_per_unit: horizontal_unit(measure::enum_value(projected, "uom").as_deref(), &at)?,
})
}
("plant north", Some(compound)) => {
let axes = compound_plane(datum, source).and_then(|plane| {
let axes = field_object(plane.as_reflect(), "HorizontalAxes")?;
Some((
measure::enum_value(axes, "Direction1"),
measure::enum_value(axes, "Direction2"),
))
});
match axes {
Some((Some(first), Some(second))) if first == "east" && second == "north" => {}
Some((first, second)) => {
return Err(Error::unsupported(
"a plant-north survey in a compound whose axes are not east then north",
format!(
"{at}: `{}`, `{}`",
first.unwrap_or_default(),
second.unwrap_or_default()
),
));
}
None => {
return Err(Error::unsupported(
"a plant-north survey whose compound's horizontal axes cannot be read - which way its north points is not known",
at,
));
}
}
Ok(Horizontal::Plant {
compound: Box::new(compound.clone()),
metres_per_unit: horizontal_unit(compound.horizontal_uom.as_deref(), &at)?,
})
}
("plant north", None) => Err(Error::unsupported(
"a plant-north survey whose datum is written in no local engineering compound - no plant is named to turn from",
at,
)),
(_, _) => Err(Error::unsupported(
"an azimuth reference other than grid or plant north - it needs a convergence or a declination this build does not apply",
format!("{at} = `{north}`"),
)),
}
}
}
fn compound_plane(
datum: &dyn Reflect,
source: &dyn ValueSource,
) -> Option<Box<dyn energyml_core::registry::DynObject>> {
let compound = source.object_at(&reference_of(datum, "Crs")?)?;
source.object_at(&reference_of(compound.as_reflect(), "LocalEngineering2dCrs")?)
}
fn east_then_north(order: &str, at: &str) -> Result<()> {
if order == "easting northing" {
return Ok(());
}
Err(Error::unsupported(
"a grid-north survey in a projected system whose axes are not easting then northing",
format!("{at}: AxisOrder `{order}`"),
))
}
fn horizontal_unit(unit: Option<&str>, at: &str) -> Result<f64> {
let unit = unit.ok_or_else(|| {
Error::unsupported(
"a projected system that states no unit - a displacement in metres cannot be added to it",
at,
)
})?;
Ok(units::metres(1.0, unit)?)
}