use std::io::Write;
use energyml_domain::reproject::Reprojector;
use energyml_geometry::{Placement, Point, SceneObject};
use crate::error::{Error, Result};
#[derive(Default)]
pub struct Options<'a> {
pub reprojector: Option<&'a dyn Reprojector>,
pub placement: Option<Placement>,
pub target_epsg: Option<u64>,
pub native_crs: bool,
pub shapes: crate::shapes::Shapes,
pub time_equivalence: Option<f64>,
}
impl<'a> Options<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_reprojector(mut self, reprojector: &'a dyn Reprojector) -> Self {
self.reprojector = Some(reprojector);
self
}
pub fn with_placement(mut self, placement: Placement) -> Self {
self.placement = Some(placement);
self
}
pub fn with_target_epsg(mut self, epsg: u64) -> Self {
self.target_epsg = Some(epsg);
self
}
pub fn with_native_crs(mut self) -> Self {
self.native_crs = true;
self
}
pub fn with_shapes(mut self, shapes: crate::shapes::Shapes) -> Self {
self.shapes = shapes;
self
}
pub fn with_time_equivalence(mut self, metres_per_second: f64) -> Self {
self.time_equivalence = Some(metres_per_second);
self
}
pub fn can_place_vertical(&self, object: &SceneObject) -> bool {
let Some(crs) = object.crs.as_ref() else {
return true;
};
if !crs.is_time() {
return true;
}
if self.time_equivalence.is_none() {
return false;
}
match crs.vertical_axis_uom() {
Some(unit) => energyml_domain::units::seconds(1.0, unit).is_ok(),
None => true,
}
}
pub fn time_equivalence_note(&self, object: &SceneObject) -> Option<String> {
let per_second = self.time_equivalence?;
if !object.crs.as_ref().is_some_and(|crs| crs.is_time()) {
return None;
}
Some(format!(
"vertical axis is TRAVEL TIME, written with the stated equivalence \
1 s = {per_second} m. This is a display equivalence, not a depth: \
the velocity model is not in the package, and RESQML does not say \
whether the time is one-way or two-way, so a real depth is half or \
double this. Do not measure from it."
))
}
pub fn points(&self, object: &SceneObject, placement: Placement) -> Result<Vec<Point>> {
if let Some(per_second) = self.time_equivalence
&& object.crs.as_ref().is_some_and(|crs| crs.is_time())
{
return self.time_points(object, per_second);
}
match self.target_epsg {
Some(target) => self.points_in_epsg(object, target),
None => Ok(object.points_in(placement)?),
}
}
fn time_points(&self, object: &SceneObject, per_second: f64) -> Result<Vec<Point>> {
let Some(crs) = object.crs.as_ref() else {
return Err(Error::Placement(format!(
"`{}` has a time equivalence to apply and names no CRS",
object.title
)));
};
let placed = object.points_in(Placement::Projected)?;
let horizontal = |value: f64| -> Result<f64> {
Ok(match &crs.horizontal_uom {
Some(unit) => energyml_domain::units::metres(value, unit)?,
None => value,
})
};
let vertical = |value: f64| -> Result<f64> {
let seconds = match crs.vertical_axis_uom() {
Some(unit) => energyml_domain::units::seconds(value, unit)?,
None => value,
};
Ok(seconds * per_second)
};
placed
.into_iter()
.map(|[x, y, z]| Ok([horizontal(x)?, horizontal(y)?, vertical(z)?]))
.collect()
}
pub fn points_in_epsg(&self, object: &SceneObject, target: u64) -> Result<Vec<Point>> {
let placed = object.points_in(Placement::Metres)?;
let Some(source) = object.epsg() else {
return Err(Error::Placement(format!(
"`{}` names no EPSG code, so it cannot be put in EPSG:{target}",
object.title
)));
};
if source == target {
return Ok(placed);
}
let Some(backend) = self.reprojector else {
return Err(Error::Placement(format!(
"`{}` is in EPSG:{source} and this export wants EPSG:{target}; \
supply a `Reprojector` - `energyml_proj::Proj4` is one",
object.title
)));
};
Ok(backend.reproject_all(&placed, source, target)?)
}
}
pub trait Exporter: Send + Sync {
fn name(&self) -> &'static str;
fn extension(&self) -> &'static str;
fn mime(&self) -> &'static str;
fn describe(&self) -> &'static str;
fn placement(&self) -> Placement {
Placement::Metres
}
fn handles(&self, object: &SceneObject) -> bool;
fn handles_with(&self, object: &SceneObject, options: &Options<'_>) -> bool {
self.handles(object) && options.can_place_vertical(object)
}
fn write(&self, object: &SceneObject, out: &mut dyn Write, options: &Options<'_>) -> Result<()>;
fn write_all(&self, objects: &[SceneObject], out: &mut dyn Write, options: &Options<'_>) -> Result<()> {
for object in objects {
self.write(object, out, options)?;
}
Ok(())
}
}
pub fn drawable(object: &SceneObject) -> std::borrow::Cow<'_, SceneObject> {
match object.drawable_only() {
Some(compacted) => std::borrow::Cow::Owned(compacted),
None => std::borrow::Cow::Borrowed(object),
}
}
pub fn prepare<'a>(
object: &'a SceneObject,
options: &Options<'_>,
) -> Result<std::borrow::Cow<'a, SceneObject>> {
let shape = options.shapes.shape_for(&object.qualified_type);
let reshaped = object.as_shape(shape)?;
Ok(match reshaped {
std::borrow::Cow::Borrowed(same) => drawable(same),
std::borrow::Cow::Owned(owned) => std::borrow::Cow::Owned(owned.drawable_only().unwrap_or(owned)),
})
}
pub struct Exporters {
entries: Vec<Box<dyn Exporter>>,
}
impl Default for Exporters {
fn default() -> Self {
Self::builtin()
}
}
impl Exporters {
pub fn none() -> Self {
Self { entries: Vec::new() }
}
pub fn builtin() -> Self {
let mut exporters = Self::none();
exporters.entries.push(Box::new(crate::obj::ObjExporter));
exporters.entries.push(Box::new(crate::csv::CsvExporter));
exporters.entries.push(Box::new(crate::geojson::GeoJsonExporter));
exporters
.entries
.push(Box::new(crate::vtk::VtkExporter::legacy()));
exporters
.entries
.push(Box::new(crate::vtk::VtkExporter::poly_data()));
exporters
.entries
.push(Box::new(crate::vtk::VtkExporter::unstructured_grid()));
exporters
}
pub fn push(&mut self, exporter: Box<dyn Exporter>) -> &mut Self {
self.entries.insert(0, exporter);
self
}
pub fn find(&self, name: &str) -> Option<&dyn Exporter> {
let wanted = name.trim().trim_start_matches('.').to_ascii_lowercase();
self.entries
.iter()
.find(|e| e.name() == wanted || e.extension() == wanted)
.map(|e| e.as_ref())
}
pub fn get(&self, name: &str) -> Result<&dyn Exporter> {
self.find(name).ok_or_else(|| Error::UnknownFormat {
name: name.to_string(),
available: self
.entries
.iter()
.map(|e| e.name())
.collect::<Vec<_>>()
.join(", "),
})
}
pub fn all(&self) -> Vec<&dyn Exporter> {
self.entries.iter().map(|e| e.as_ref()).collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}