Skip to main content

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}