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}