energyml_section/read/surfaces.rs
1//! Reading a surface or a grid, and cutting it along every section
2//! (ADR-0015).
3//!
4//! Contract: [`read_surface`].
5//!
6//! The order is the order of refusals, cheapest first, each one naming the
7//! object: not in the package, not a representation this build reads (an IJK
8//! grid has its own reading, [`crate::read::grids`], and its own refusals), a CRS that cannot be
9//! read, a read that fails, **no triangles** (it draws as points or lines - the
10//! cut is of triangles), points that cannot be brought into the scene. An
11//! object that gets through is cut and never empty-handed by accident: a
12//! section it does not cross has no entry, and an object crossing none is the
13//! caller's `Missed`, which is not a refusal.
14
15use energyml_core::identity;
16use energyml_core::value::{FieldValue, Reflect};
17use energyml_domain::ArraySource as ValueSource;
18use energyml_domain::color::{Cascade, Decision, GraphicalSet, Paint, Request, Scope};
19use energyml_domain::package_index::PackageIndex;
20use energyml_geometry::fence;
21use energyml_geometry::scene::{self, Placement, SceneObject};
22use energyml_geometry::section;
23
24use crate::model::{
25 CutShape, CutSource, ObjectId, Refused, SectionTrace, SurfaceCut, SurfaceRead, TraceRole,
26};
27use crate::read::Scene;
28use crate::read::frame::{frame_of, to_scene};
29use crate::read::grids::Grid;
30
31/// The uuid a uri names: `eml:///resqml22.Foo(uuid)` gives `uuid`, and a text
32/// that is not a uri is taken as the uuid itself - `ObjectSource::object` is
33/// asked by uuid.
34///
35/// Lower case, since a store keys its objects that way.
36pub fn uuid_of_uri(uri: &str) -> String {
37 energyml_core::identity::parse_address(uri).0
38}
39
40/// One surface (anything with triangles) or IJK grid, read in the scene's
41/// system and cut along each section trace.
42// A refusal is the cold path, met once per object, and it carries the object's
43// whole identity on purpose: boxing it would change every construction site
44// for no gain anyone could measure.
45#[allow(clippy::result_large_err)]
46pub fn read_surface(
47 source: &dyn ValueSource,
48 store: &str,
49 uri: &str,
50 sections: &[SectionTrace],
51 scene: &Scene<'_>,
52) -> Result<SurfaceRead, Refused> {
53 read_surface_coloured(source, store, uri, sections, scene, Colouring::default())
54}
55
56/// Where a surface's colour is looked for: the package's sets, merged, and -
57/// when the face has it - the package's index, for the inheritance order.
58#[derive(Clone, Copy, Default)]
59pub struct Colouring<'a> {
60 /// The package's `GraphicalInformationSet`s, merged.
61 pub set: Option<&'a GraphicalSet>,
62 /// The package's index.
63 pub index: Option<&'a PackageIndex>,
64 /// The face's resolver ([`crate::read::PackageContext::colour_of`]): asked
65 /// instead of the cascade when given.
66 pub resolver: Option<&'a crate::read::ColourOf<'a>>,
67}
68
69/// [`read_surface`], with the package's colour sets merged - the surface takes
70/// the colour the file gives it ([`file_colour`]).
71#[allow(clippy::result_large_err)]
72pub fn read_surface_coloured(
73 source: &dyn ValueSource,
74 store: &str,
75 uri: &str,
76 sections: &[SectionTrace],
77 scene: &Scene<'_>,
78 colours: Colouring<'_>,
79) -> Result<SurfaceRead, Refused> {
80 Ok(cut_prepared(
81 &prepare_surface(source, store, uri, scene, colours)?,
82 sections,
83 ))
84}
85
86/// A surface read and brought into the scene, **before** any section cuts it:
87/// what does not depend on the trace, and so what a cache keeps when the trace
88/// moves (`read::cache`).
89#[derive(Clone, Debug)]
90pub struct Prepared {
91 /// What the drawing calls it.
92 pub object: ObjectId,
93 /// What it is - from its interpretation.
94 pub role: TraceRole,
95 /// In time, carried onto the depth axis.
96 pub time: bool,
97 /// The colour the file gives it.
98 pub colour: Option<String>,
99 /// Its age ([`age_of`]), when its interpretation states one.
100 pub age: Option<[f64; 2]>,
101 /// The feature its interpretation interprets (`feature_of`).
102 pub feature: Option<String>,
103 /// Its points, in the scene's system.
104 pub points: Vec<[f64; 3]>,
105 /// Its triangles.
106 pub triangles: Vec<[u32; 3]>,
107 /// **The node numbering, reconciled**: `nodes[i]` is the number, in the
108 /// representation's own numbering (the running sum of its patches' points -
109 /// the numbering a front end's colour buffer indexes, so the front end's per-node
110 /// paint), of `points[i]`. The two differ whenever the reader dropped a
111 /// node that holds no location (`Geometry::drawable` compacts them out and
112 /// renumbers): a lattice with holes - Lutetia's horizons in grid form - is
113 /// the ordinary case. Empty when the reader cannot say (a grid, or a shape
114 /// whose dropped nodes are not just the non-finite ones): the cut then
115 /// carries no sources and is drawn in the flat colour.
116 pub nodes: Vec<u32>,
117 /// An IJK grid's cells, in the scene's system - then `points` and
118 /// `triangles` are empty, and a section cuts its envelope ([`Grid::cut`]).
119 pub grid: Option<Grid>,
120}
121
122/// The first half of [`read_surface_coloured`]: read, judged, placed in the
123/// scene and coloured - every refusal is here, none in the cut.
124#[allow(clippy::result_large_err)]
125pub fn prepare_surface(
126 source: &dyn ValueSource,
127 store: &str,
128 uri: &str,
129 scene: &Scene<'_>,
130 colours: Colouring<'_>,
131) -> Result<Prepared, Refused> {
132 let mut object = ObjectId {
133 store: store.to_string(),
134 uri: uri.to_string(),
135 uuid: uuid_of_uri(uri),
136 title: String::new(),
137 qualified_type: String::new(),
138 };
139 let refuse = |object: &ObjectId, reason: String| Refused {
140 object: object.clone(),
141 reason,
142 };
143
144 let held = source
145 // By the uri the section was given, never by its uuid: two versions of
146 // an object share a uuid, and only the uri tells them apart.
147 .read_object_at(uri)
148 .map_err(|why| refuse(&object, format!("`{uri}` cannot be read from the package: {why}")))?;
149 let representation = held.as_reflect();
150 let info = representation.type_info();
151 if let Some(uuid) = identity::uuid_of_reflect(representation) {
152 object.uuid = uuid.to_ascii_lowercase();
153 }
154 object.title = identity::title_of_reflect(representation)
155 .unwrap_or_default()
156 .to_string();
157 object.qualified_type = energyml_core::json::qualified_type_of(info);
158
159 // An IJK grid draws as cells, but what a section shows of it is its
160 // envelope, and cutting the cell faces would draw every cell wall of the
161 // grid: it has its own reading (`grids`).
162 if info.type_key == "IjkGridRepresentation" {
163 let frame = frame_of(source, representation).map_err(|why| refuse(&object, why))?;
164 let named = |why: String| format!("`{}` ({}): {why}", object.title, object.qualified_type);
165 let read = scene::object_of(representation, source, uri)
166 .map_err(|why| refuse(&object, named(format!("{why}"))))?;
167 let placed = read
168 .geometry_in(Placement::Projected)
169 .map_err(|why| refuse(&object, named(format!("{why}"))))?;
170 let patch = placed
171 .patches
172 .into_iter()
173 .next()
174 .ok_or_else(|| refuse(&object, named("the grid has no patch".to_string())))?;
175 let mut patch = patch;
176 to_scene(&mut patch.points, &frame, scene).map_err(|why| refuse(&object, named(why)))?;
177 let grid = Grid::from_patch(&patch).map_err(|why| refuse(&object, named(why)))?;
178 let colour = file_colour_of(colours, &object, source);
179 return Ok(Prepared {
180 role: role_of(representation, &read, source),
181 time: frame.time,
182 colour,
183 age: age_of_represented(representation, source),
184 feature: feature_of(&read),
185 object,
186 points: Vec::new(),
187 triangles: Vec::new(),
188 nodes: Vec::new(),
189 grid: Some(grid),
190 });
191 }
192 if !energyml_geometry::geometry::is_supported(representation) {
193 return Err(refuse(
194 &object,
195 format!(
196 "`{}` is a {}, which this build does not read as a shape",
197 object.title, object.qualified_type
198 ),
199 ));
200 }
201
202 let frame = frame_of(source, representation).map_err(|why| refuse(&object, why))?;
203 let read =
204 scene::object_of(representation, source, uri).map_err(|why| refuse(&object, format!("{why}")))?;
205 let drawn = read
206 .drawable_in(Placement::Projected)
207 .map_err(|why| refuse(&object, format!("{why}")))?;
208 if drawn.triangles.is_empty() {
209 return Err(refuse(
210 &object,
211 format!(
212 "`{}` ({}) has no triangles - {} point(s) and {} line segment(s): only a triangulated shape is cut",
213 object.title,
214 object.qualified_type,
215 drawn.points.len(),
216 drawn.lines.len()
217 ),
218 ));
219 }
220
221 let nodes = drawn_nodes(&read.geometry, drawn.points.len());
222 let mut points = drawn.points;
223 // Named: the reason of `to_scene` speaks of "the object", and a list of
224 // refusals is read without the rows' titles beside it (the CLI's).
225 to_scene(&mut points, &frame, scene).map_err(|why| {
226 refuse(
227 &object,
228 format!("`{}` ({}): {why}", object.title, object.qualified_type),
229 )
230 })?;
231
232 let colour = file_colour_of(colours, &object, source);
233 Ok(Prepared {
234 role: role_of(representation, &read, source),
235 time: frame.time,
236 colour,
237 age: age_of_represented(representation, source),
238 feature: feature_of(&read),
239 object,
240 points,
241 triangles: drawn.triangles,
242 nodes,
243 grid: None,
244 })
245}
246
247/// The representation's node number of each point `drawable` kept
248/// ([`Prepared::nodes`]).
249///
250/// `drawable` keeps the nodes that are locations, in order, and renumbers: so
251/// the kept nodes are the finite ones, and the i-th kept is the i-th finite of
252/// the raw numbering. **The count is the check**: if the finite nodes are not
253/// exactly as many as the points kept (a volume's node that no cell names is
254/// dropped too, `orphans_as_holes`), the mapping is not this one and nothing is
255/// claimed - empty.
256pub fn drawn_nodes(geometry: &energyml_geometry::geometry::Geometry, kept: usize) -> Vec<u32> {
257 let finite: Vec<u32> = geometry
258 .patches
259 .iter()
260 .flat_map(|patch| patch.points.iter())
261 .enumerate()
262 .filter(|(_, point)| point.iter().all(|c| c.is_finite()))
263 .map(|(at, _)| at as u32)
264 .collect();
265 if finite.len() == kept { finite } else { Vec::new() }
266}
267
268/// The second half of [`read_surface_coloured`]: a prepared surface cut along
269/// every section. Pure arithmetic - nothing is read, nothing refused.
270pub fn cut_prepared(prepared: &Prepared, sections: &[SectionTrace]) -> SurfaceRead {
271 if let Some(grid) = &prepared.grid {
272 return SurfaceRead {
273 object: prepared.object.clone(),
274 role: prepared.role,
275 time: prepared.time,
276 colour: prepared.colour.clone(),
277 age: prepared.age,
278 feature: prepared.feature.clone(),
279 cuts: grid.cut(sections),
280 };
281 }
282 // One pass for every section (`cut_many`): the sheet's boxes are computed
283 // once, and a cross section a few hundred metres long tests a few
284 // triangles of a surface of millions.
285 let traces: Vec<&[[f64; 2]]> = sections.iter().map(|one| one.trace.as_slice()).collect();
286 let all = section::cut_many_sourced(&prepared.points, &prepared.triangles, &traces);
287
288 let mut cuts = Vec::new();
289 for (trace, found) in sections.iter().zip(all) {
290 // A section the surface does not cross has no entry - `Missed`, if it
291 // crosses none, is said by the row, not by an empty cut here.
292 if found.is_empty() {
293 continue;
294 }
295 let made = fence::curve_sourced(&found);
296 // The edge each point lies on, in the representation's numbering (the
297 // paint's - `Prepared::nodes`). One list per span, one source per
298 // point; if any node cannot be translated, the cut says nothing
299 // rather than a part of it.
300 let sources = sources_of(prepared, &made.sources).unwrap_or_default();
301 cuts.push(SurfaceCut {
302 section: trace.key,
303 shape: CutShape::Trace {
304 spans: made.curve.spans,
305 },
306 notes: made.curve.notes,
307 sources,
308 });
309 }
310
311 SurfaceRead {
312 object: prepared.object.clone(),
313 role: prepared.role,
314 time: prepared.time,
315 colour: prepared.colour.clone(),
316 age: prepared.age,
317 feature: prepared.feature.clone(),
318 cuts,
319 }
320}
321
322/// The cut's sources, translated into the representation's node numbering -
323/// `None` when the preparation holds no numbering, or a node is out of it.
324///
325/// A source whose end was **clipped** to the section's window lies inside its
326/// triangle, not on an edge: it carries the nearer crossing's edge and `t`
327/// (`CutEnd::clipped`), so its colour is that crossing's - the contract has no
328/// way to say more, and the clipped end of a span is a fraction of one triangle
329/// from the crossing it takes.
330fn sources_of(prepared: &Prepared, sources: &[Vec<section::CutEnd>]) -> Option<Vec<Vec<CutSource>>> {
331 if prepared.nodes.is_empty() {
332 return None;
333 }
334 let number = |at: u32| prepared.nodes.get(at as usize).copied();
335 sources
336 .iter()
337 .map(|span| {
338 span.iter()
339 .map(|end| {
340 Some(CutSource {
341 a: number(end.a)?,
342 b: number(end.b)?,
343 t: end.t,
344 })
345 })
346 .collect::<Option<Vec<_>>>()
347 })
348 .collect()
349}
350
351/// What the surface is, from the **type of the interpretation** it represents -
352/// `RepresentedInterpretation` in RESQML 2.0.1, `RepresentedObject` in 2.2, the
353/// pair `scene::SceneObject::interpretation` already reads. Never the title: a
354/// horizon called "Fault_3" is a horizon.
355///
356/// The interpretation is resolved in the package when it can be (its own type
357/// is the fact), and the reference's qualified type is the fallback. No
358/// interpretation, or one that is neither: `Other`.
359/// The colour the package gives an object: the library's cascade over the
360/// package's sets - the representation, then its interpretation, then its
361/// feature (`energyml_domain::color::Cascade`, where the 3D viewer's ladder
362/// starts). Only a **constant** colour answers here: a map colours by a
363/// property, and a section draws a surface as one line. `None` when nothing in
364/// the file speaks - the drawing then falls back on the role's colour, and never
365/// invents one in the file's name.
366///
367/// What this does not walk: the inheritance through collections and models
368/// (`color_inherit`), which needs the package's index - a face that has it
369/// passes the colour in the view instead (`View::colours`).
370pub fn file_colour(colouring: Colouring<'_>, uuid: &str, source: &dyn ValueSource) -> Option<String> {
371 let set = colouring.set?;
372 cascade_colour(set, colouring.index, uuid, source)
373}
374
375/// [`file_colour`] for an object of the package: the face's resolver, when
376/// given, is asked by the object's **full uri** - a uuid would hand both
377/// versions of a surface the colour of one.
378pub fn file_colour_of(
379 colouring: Colouring<'_>,
380 object: &ObjectId,
381 source: &dyn ValueSource,
382) -> Option<String> {
383 // The face already knows: no search is made again.
384 if let Some(resolver) = colouring.resolver {
385 return resolver(&object.uri);
386 }
387 file_colour(colouring, &object.uuid, source)
388}
389
390/// The cascade's constant colour for `uuid` over `set`.
391fn cascade_colour(
392 set: &GraphicalSet,
393 index: Option<&PackageIndex>,
394 uuid: &str,
395 source: &dyn ValueSource,
396) -> Option<String> {
397 let cascade = Cascade::new(set);
398 let decision = match index {
399 // The whole order - the 3D viewer's (`Cascade::inherited`): Lutetia
400 // colours its horizons through the collections they belong to, which
401 // the shorter walk below never reaches.
402 Some(index) => cascade
403 .inherited(index, source, uuid)
404 .ok()
405 .and_then(|walked| walked.resolution)
406 .map(|found| found.value),
407 None => {
408 let request = Request {
409 representation: uuid,
410 property: None,
411 kind_chain: &[],
412 element: Scope::WholeObject,
413 };
414 cascade.resolve(&request, source).ok().flatten()
415 }
416 };
417 match decision {
418 Some(Decision {
419 paint: Paint::Constant(colour),
420 ..
421 }) => Some(colour.to_hex()),
422 _ => None,
423 }
424}
425
426fn role_of(representation: &dyn Reflect, read: &SceneObject, source: &dyn ValueSource) -> TraceRole {
427 let Some(related) = &read.interpretation else {
428 return TraceRole::Other;
429 };
430 // Followed by the representation's reference, so the version it names is
431 // the one whose type decides the role.
432 let resolved = scene::interpretation_of(representation, source);
433 let key: &str = match &resolved {
434 Some(found) => found.as_reflect().type_info().type_key,
435 // A qualified type (`resqml22.FaultInterpretation`) or, in 2.0.1, a
436 // content type (`…;version=2.0;type=obj_FaultInterpretation`).
437 None => related.qualified_type.rsplit(['.', '=']).next().unwrap_or(""),
438 };
439 // `obj_` prefixes a 2.0.1 type in a qualified type, never in a type key.
440 match key.strip_prefix("obj_").unwrap_or(key) {
441 "HorizonInterpretation" => TraceRole::Horizon,
442 "FaultInterpretation" => TraceRole::Fault,
443 _ => TraceRole::Other,
444 }
445}
446
447/// The age of the interpretation a surface represents, `[old, young]` in
448/// millions of years before present - **an interval, never a bare value**: a
449/// horizon may state a span (`HasOccurredDuring`, or `OlderPossibleAge` and
450/// `YoungerPossibleAge`) and the detection judges only what is disjoint, so a
451/// point is the interval `[a, a]` and nothing is lost by it.
452///
453/// Where the interpretation says it, first found wins:
454///
455/// 1. `HasOccurredDuring` → `GeologicTimeBasedTimeInterval`, whose `Start` and
456/// `End` each carry an `AgeOffsetAttribute` in years (positive into the
457/// past, eml 2.3) - the reading `energyml_domain::stratigraphy` gives a
458/// unit's age;
459/// 2. `OlderPossibleAge` **and** `YoungerPossibleAge` (RESQML 2.2, years);
460/// 3. `AbsoluteAge` (a `GeologicTime`), as the point.
461///
462/// `None` is the rule, not an error: RESQML 2.0.1 states no age on a horizon
463/// interpretation (its feature's `AbsoluteAge` is a timestamp, not an age), a
464/// stratigraphic unit's age is on the **unit** and no horizon names its unit
465/// here, and the corpus dates almost nothing. Nothing is derived from a name, a
466/// depth or the order of a column.
467pub fn age_of(interpretation: &dyn Reflect) -> Option<[f64; 2]> {
468 let years = |object: &dyn Reflect, name: &str| -> Option<f64> {
469 match object.field(name)? {
470 FieldValue::Scalar(scalar) => scalar
471 .to_string()
472 .trim()
473 .parse::<i64>()
474 .ok()
475 .map(|y| y as f64 / 1_000_000.0),
476 _ => None,
477 }
478 };
479 let ordered = |a: f64, b: f64| [a.max(b), a.min(b)];
480 if let Some(FieldValue::Object(interval)) = interpretation.field("HasOccurredDuring") {
481 let bound = |name: &str| match interval.field(name)? {
482 FieldValue::Object(time) => years(time, "AgeOffsetAttribute"),
483 _ => None,
484 };
485 if let (Some(start), Some(stop)) = (bound("Start"), bound("End")) {
486 return Some(ordered(start, stop));
487 }
488 }
489 if let (Some(older), Some(younger)) = (
490 years(interpretation, "OlderPossibleAge"),
491 years(interpretation, "YoungerPossibleAge"),
492 ) {
493 return Some(ordered(older, younger));
494 }
495 if let Some(FieldValue::Object(time)) = interpretation.field("AbsoluteAge")
496 && let Some(age) = years(time, "AgeOffsetAttribute")
497 {
498 return Some([age, age]);
499 }
500 None
501}
502
503/// The feature the interpretation of `read` interprets - the reflection the
504/// scene already does (`SceneObject::feature`: `InterpretedFeature` in 2.0.1 and
505/// 2.2, through the interpretation), as a lowercase uuid. `None` when the
506/// object names no interpretation, or the package cannot be followed to the
507/// feature: two surfaces are then never taken for one horizon.
508fn feature_of(read: &SceneObject) -> Option<String> {
509 read.feature
510 .as_ref()
511 .map(|f| f.uuid.to_ascii_lowercase())
512 .filter(|u| !u.is_empty())
513}
514
515/// The age of what `read` represents, when it is in the package and states one.
516fn age_of_represented(representation: &dyn Reflect, source: &dyn ValueSource) -> Option<[f64; 2]> {
517 let held = scene::interpretation_of(representation, source)?;
518 age_of(held.as_reflect())
519}
520
521/// Whether an object of this qualified type is one a section cuts: a
522/// triangulated surface, a 2D grid (a horizon on a lattice - Lutetia's are), or
523/// an IJK grid (its envelope).
524///
525/// What "every surface of the package" means for a face that lists a package
526/// (the CLI's default) - decided here, so the front end and the CLI agree.
527pub fn is_cut_type(qualified_type: &str) -> bool {
528 [
529 "TriangulatedSetRepresentation",
530 "Grid2dRepresentation",
531 "IjkGridRepresentation",
532 ]
533 .iter()
534 .any(|kind| qualified_type.ends_with(kind))
535}