Skip to main content

energyml_section/read/
frame.rs

1//! An object's own frame, and how its points reach the scene's
2//! (ADR-0015).
3//!
4//! Used for wells too. Contract:
5//! [`ObjectFrame`], [`frame_of`], [`to_scene`].
6//!
7//! # One reading, not a second one
8//!
9//! The scene's own answers are a front end's CRS lookup (an object's CRS, then
10//! `reproject::System::of`) and a front end's drawable `verticalIsTime`
11//! (`Crs::is_time`). This module asks the same two library functions -
12//! `crs::of` and `System::of` - rather than reading a CRS a second way: the
13//! section view and the scene must agree on which system an object is in, or
14//! one of them draws a model where it is not.
15//!
16//! # What is refused, by name
17//!
18//! Nothing is guessed: a point in an unknown system is not "probably the
19//! scene's" (a Lambert-93 model read as UTM looks right and is 600 km away).
20//!
21//! - the object names a system and the scene has none, or the reverse;
22//! - an object in time and no `time_factor` - which says how many metres of
23//!   depth a unit of time is worth in this scene;
24//! - whatever the reprojection backend refuses (a datum it does not know, a
25//!   unit it cannot convert), in the backend's own words.
26
27use energyml_core::value::Reflect;
28use energyml_domain::ArraySource as ValueSource;
29use energyml_domain::reproject::{self, System as DomainSystem};
30
31use crate::model::System;
32use crate::read::Scene;
33
34/// What an object's points are in: its horizontal system (`None` when its CRS
35/// names none) and whether its vertical axis is time.
36#[derive(Clone, Debug, Default, PartialEq)]
37pub struct ObjectFrame {
38    /// The horizontal system its placed points are in.
39    pub system: Option<System>,
40    /// Its vertical axis is a time, not a length.
41    pub time: bool,
42}
43
44/// The frame of a representation, read from its CRS - the same reading the
45/// scene makes (`workspace.systemOf`), never a second one.
46///
47/// An object that names **no** CRS has no system and no time: its local
48/// coordinates are the only ones there are, and that is [`to_scene`]'s to judge
49/// against the scene. A CRS that names no EPSG code gives no system either -
50/// `System::of` refuses it, and for the scene that is the same fact (nothing to
51/// reproject from), said by [`to_scene`] when it matters. A CRS that cannot be
52/// read at all is an error, never an absent frame.
53pub fn frame_of(source: &dyn ValueSource, representation: &dyn Reflect) -> Result<ObjectFrame, String> {
54    let Some(crs) = energyml_domain::crs::of(representation, source).map_err(|why| format!("{why}"))? else {
55        return Ok(ObjectFrame::default());
56    };
57    let system = DomainSystem::of(&crs).ok().map(|found| System {
58        epsg: found.epsg,
59        unit: found.unit,
60    });
61    Ok(ObjectFrame {
62        system,
63        time: crs.is_time(),
64    })
65}
66
67/// Bring placed points into the scene: reproject `[x, y]` when the object's
68/// system is not the scene's, and carry a time onto the depth axis by the
69/// scene's time factor. Each refusal names what is missing (no system on one
70/// side, no time factor for an object in time, a backend that refuses).
71///
72/// The points are **placed** ones (`Placement::Projected`): the CRS's offset,
73/// rotation and vertical flip are already applied, which is what
74/// `reproject_placed` wants - applying them twice is the bug that function
75/// exists to make impossible. On error the points are left as they were.
76pub fn to_scene(points: &mut [[f64; 3]], frame: &ObjectFrame, scene: &Scene<'_>) -> Result<(), String> {
77    // The time first, since it is a refusal that needs no work: nothing is
78    // reprojected for an object that will be refused anyway.
79    let factor = match (frame.time, scene.time_factor) {
80        (false, _) => None,
81        (true, Some(factor)) => Some(factor),
82        (true, None) => {
83            return Err(
84                "the object is in time and the scene has no time factor (the metres of depth a unit of \
85                 time is worth), so it cannot be carried onto the depth axis"
86                    .to_string(),
87            );
88        }
89    };
90
91    match (&frame.system, scene.system) {
92        (None, None) => {}
93        (Some(own), None) => {
94            return Err(format!(
95                "the object is in EPSG:{} ({}) and the scene names no system: it would be drawn against \
96                 coordinates it does not share",
97                own.epsg, own.unit
98            ));
99        }
100        (None, Some(target)) => {
101            return Err(format!(
102                "the object names no horizontal system (no CRS, or a CRS with no EPSG code) and the scene \
103                 is in EPSG:{}: there is nothing to reproject it from",
104                target.epsg
105            ));
106        }
107        (Some(own), Some(target)) => {
108            if own != target {
109                let from = DomainSystem {
110                    epsg: own.epsg,
111                    unit: own.unit.clone(),
112                };
113                let to = DomainSystem {
114                    epsg: target.epsg,
115                    unit: target.unit.clone(),
116                };
117                let moved =
118                    reproject::reproject_placed(points, &from, &to, scene.reprojector).map_err(|why| {
119                        format!(
120                            "reprojecting from EPSG:{} to EPSG:{}: {why}",
121                            own.epsg, target.epsg
122                        )
123                    })?;
124                points.copy_from_slice(&moved);
125            }
126        }
127    }
128
129    if let Some(factor) = factor {
130        for point in points.iter_mut() {
131            point[2] *= factor;
132        }
133    }
134    Ok(())
135}
136
137/// The system of the first of these objects that names one - what a face with
138/// a single package and no scene (the CLI) takes as the scene's system.
139/// `None` when none of them names one.
140pub fn first_system(source: &dyn ValueSource, uris: &[String]) -> Option<System> {
141    uris.iter().find_map(|uri| {
142        // By the uri: it names one version of an object, the uuid may not.
143        let object = source.object_at(uri)?;
144        frame_of(source, object.as_reflect()).ok()?.system
145    })
146}