Skip to main content

energyml_cache/
colours.rs

1//! The colour of an object, **one rule for every face** - the walks that
2//! a WebAssembly front end used to keep for itself (the inheritance-resolution order), now held by the [`Cache`].
3//!
4//! All the deciding is `energyml_domain::color_inherit` (the order) over
5//! `energyml_domain::inherit` (the walk) and `energyml_domain::color_walks` (the
6//! walk of one object, its targets, its cache); this module asks them with the
7//! package index. Nothing here knows a rank.
8//!
9//! Before the cache existed the front end answered by those walks and the command line, the
10//! scene's colour ramps and `well_strata` by `Cascade::resolve` - a shorter
11//! order that ignores the collections and the model: **two colour rules
12//! depending on the face** (ADR-0009). The command line now asks
13//! [`Cache::decision`] and [`of_package`], the front end asks the same.
14//!
15//! # What is kept
16//!
17//! A `ColourSlot`, beside the package index: the walk of each object asked,
18//! and the merged `GraphicalInformationSet`. Both are exact **for the index and
19//! the store** because every question first brings the index up to date and
20//! then drops what the update disturbed (`IndexSlot::take_disturbed`) - the
21//! walks that looked at an object that changed, or that gained or lost a
22//! reference, and the walks of the objects an entry of the set decorated
23//! differently. With the layer off the slot is a fresh one, dropped after the
24//! call.
25
26use std::collections::{HashMap, HashSet};
27
28use energyml_domain::color::{Cascade, Decision, GraphicalSet, Paint, Request, Scope};
29use energyml_domain::color_inherit::Ladder;
30use energyml_domain::color_walks::{ColourCache, Stats, changed_targets};
31use energyml_domain::package_index::PackageIndex;
32use energyml_domain::property::{self, Property};
33use energyml_domain::property_kind::Resolver;
34use energyml_storage::{EpcStore, ObjectSummary};
35
36use crate::index::{Consumer, Disturbed};
37use crate::{Cache, Error};
38
39/// One row asked for: the object (its full uri), its kind chain when it is a
40/// property, the property when it is.
41///
42/// **Build it with [`Row::at`]** from the object's name: `at` is the one place
43/// that turns a name into the full identity the walk beneath
44/// (`energyml_domain::color_walks`) is keyed by, refusing a name that cannot
45/// say which of two versions is meant.
46pub struct Row<'a> {
47    /// The object's full uri, the key the walk beneath works in.
48    pub key: String,
49    /// For a property, its kind then the kind's parents, nearest first.
50    pub kinds: Vec<String>,
51    /// The property asked about, if the row is one.
52    pub property: Option<&'a str>,
53}
54
55impl<'a> Row<'a> {
56    /// The row of the object `name` names: a full uri (the form to pass), a
57    /// short uri or a bare uuid (adapters, refused by name when the package
58    /// holds the object in two versions and the name cannot say which).
59    /// A name the package does not hold is walked as given: a cascade may ask
60    /// about a target that lives elsewhere.
61    pub fn at(
62        store: &EpcStore,
63        name: &str,
64        kinds: Vec<String>,
65        property: Option<&'a str>,
66    ) -> Result<Self, Error> {
67        Ok(Row {
68            key: walk_key(store, name)?,
69            kinds,
70            property,
71        })
72    }
73}
74
75/// The full uri the walks are keyed by, of the object `name` names, through
76/// the store's one door ([`EpcStore::resolve`]): the version a uri names is the
77/// object, a name that fits two is [`energyml_storage::Error::Ambiguous`].
78/// `name` itself (lowercased) when the package holds no such object: a cascade
79/// may ask about a target that lives elsewhere.
80fn walk_key(store: &EpcStore, name: &str) -> Result<String, Error> {
81    match store.resolve(name.trim()) {
82        Ok(summary) => Ok(summary.uri.clone()),
83        Err(energyml_storage::Error::NotFound(_)) => Ok(name.trim().to_ascii_lowercase()),
84        Err(other) => Err(other.into()),
85    }
86}
87
88/// What a cache keeps of its colours: the walks, and the set they read.
89#[derive(Default)]
90pub(crate) struct ColourSlot {
91    cache: ColourCache,
92    /// The package's `GraphicalInformationSet`s read as one - `None` when it has
93    /// none, once `read` is true.
94    set: Option<GraphicalSet>,
95    /// The uuids of the sets that were read.
96    sets: HashSet<String>,
97    read: bool,
98}
99
100impl ColourSlot {
101    pub(crate) fn len(&self) -> usize {
102        self.cache.len() + usize::from(self.read && self.set.is_some())
103    }
104}
105
106/// The switches of the cascade a caller may turn: the same two that
107/// [`Cascade`] has, and the same defaults.
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct Options {
110    /// Whether to walk up past the object itself. `true` is the decided
111    /// default; off, only what the object carries.
112    pub inherit: bool,
113    /// Only entries for this viewer kind, plus those that name none.
114    pub viewer_kind: Option<String>,
115}
116
117impl Default for Options {
118    fn default() -> Self {
119        Self {
120            inherit: true,
121            viewer_kind: Some("3d".to_string()),
122        }
123    }
124}
125
126/// Every `GraphicalInformationSet` of the package, read, with its (lowercase)
127/// uuid - what `energyml_domain::well_strata` asks all of.
128pub fn graphical_sets(store: &EpcStore) -> Result<Vec<(String, GraphicalSet)>, Error> {
129    let mut sets = Vec::new();
130    for summary in store.resources() {
131        if !summary.qualified_type.ends_with("GraphicalInformationSet") {
132            continue;
133        }
134        let object = store.get_object(&summary.uri)?;
135        sets.push((
136            summary.uuid.to_ascii_lowercase(),
137            GraphicalSet::read(object.as_reflect()),
138        ));
139    }
140    Ok(sets)
141}
142
143/// The package's sets read as **one** (`GraphicalSet::merged`, in package
144/// order - the first alone hid, on Lutetia, the chart's colours behind the
145/// strata table's set), or `None` when it has none.
146pub fn graphical_set(store: &EpcStore) -> Result<Option<GraphicalSet>, Error> {
147    let sets = graphical_sets(store)?;
148    Ok((!sets.is_empty()).then(|| GraphicalSet::merged(sets.into_iter().map(|(_, set)| set))))
149}
150
151/// A property's kind then the kind's parents, nearest first - because a colour
152/// set on `length` applies to every kind derived from it, and that inheritance
153/// is most of the point of colouring by kind at all. Empty when the property
154/// states no kind or the kind does not resolve.
155pub fn kind_chain(store: &EpcStore, read: &Property<'_>) -> Vec<String> {
156    let Some(reference) = read.kind.as_ref() else {
157        return Vec::new();
158    };
159    match Resolver::new(store).resolve(reference) {
160        Ok(resolution) => resolution.chain.into_iter().map(|kind| kind.uuid).collect(),
161        Err(_) => Vec::new(),
162    }
163}
164
165/// [`kind_chain`] of the property `name` names: a full uri (the version it
166/// names is the property read), or a bare uuid as an adapter. Empty when the
167/// store holds no such property; an error when the name fits two versions.
168pub fn kind_chain_at(store: &EpcStore, name: &str) -> Result<Vec<String>, Error> {
169    let summary = match store.resolve(name.trim()) {
170        Ok(summary) => summary,
171        Err(energyml_storage::Error::NotFound(_)) => return Ok(Vec::new()),
172        Err(other) => return Err(other.into()),
173    };
174    Ok(store
175        .get_object(&summary.uri)
176        .ok()
177        .and_then(|object| {
178            property::read(object.as_reflect())
179                .ok()
180                .map(|read| kind_chain(store, &read))
181        })
182        .unwrap_or_default())
183}
184
185/// [`kind_chain_at`] for a caller that cannot refuse: **empty** for a name that
186/// fits two versions. Kept for the callers that still hold a bare uuid.
187pub fn kind_chain_of(store: &EpcStore, property_uuid: &str) -> Vec<String> {
188    kind_chain_at(store, property_uuid).unwrap_or_default()
189}
190
191/// Bring the slot up to date with what the index's updates disturbed.
192///
193/// The walks that looked at a disturbed object go; the set is read again when
194/// one of the objects is a `GraphicalInformationSet` (or was), and the walks of
195/// the objects whose entries changed go with it.
196fn settle(
197    store: &EpcStore,
198    slot: &mut ColourSlot,
199    index: &PackageIndex,
200    dirty: Disturbed,
201) -> Result<(), Error> {
202    let ColourSlot {
203        cache,
204        set,
205        sets,
206        read,
207    } = slot;
208    let touches_a_set = dirty.everything
209        || dirty.uuids.iter().any(|uuid| {
210            sets.contains(uuid)
211                || index
212                    .node(uuid)
213                    .is_some_and(|node| node.type_key.ends_with("GraphicalInformationSet"))
214        });
215    if dirty.everything {
216        cache.clear();
217    } else {
218        cache.disturb(&dirty.uuids);
219    }
220    if !*read || touches_a_set {
221        let found = graphical_sets(store)?;
222        let uuids: HashSet<String> = found.iter().map(|(uuid, _)| uuid.clone()).collect();
223        let fresh = if found.is_empty() {
224            None
225        } else {
226            Some(GraphicalSet::merged(found.into_iter().map(|(_, one)| one)))
227        };
228        if *read && !dirty.everything {
229            let nothing = GraphicalSet::default();
230            let changed = changed_targets(
231                set.as_ref().unwrap_or(&nothing),
232                fresh.as_ref().unwrap_or(&nothing),
233            );
234            cache.disturb(&changed);
235        }
236        *set = fresh;
237        *sets = uuids;
238        *read = true;
239    }
240    Ok(())
241}
242
243/// [`Cache::decision`] over an index the caller already holds - what a loop
244/// over many objects asks, so that a cache that keeps nothing builds its index
245/// once for the loop and not once per object.
246fn decide_in(
247    index: &PackageIndex,
248    store: &EpcStore,
249    set: &GraphicalSet,
250    representation: &str,
251    property: Option<&str>,
252    chain: &[String],
253    options: &Options,
254) -> Result<Option<Decision>, Error> {
255    let mut cascade = Cascade::new(set);
256    cascade.inherit = options.inherit;
257    cascade.viewer_kind = options.viewer_kind.clone();
258    let walked = match property {
259        Some(property) => cascade.inherited_property(index, store, property, chain),
260        None => cascade.inherited(index, store, representation),
261    }?;
262    Ok(walked.resolution.map(|resolution| {
263        let mut decision = resolution.value;
264        for note in resolution.notes {
265            if !decision.notes.contains(&note) {
266                decision.notes.push(note);
267            }
268        }
269        decision
270    }))
271}
272
273impl Cache {
274    /// The ladders of `rows`, in order, **from the walks the cache keeps**: one
275    /// walker for all of them, the walk of an object done once, a target the
276    /// tiers already read does not answer resuming it. `None` when the package
277    /// holds no `GraphicalInformationSet` - the caller answers `null` per row.
278    pub fn ladders(&self, store: &EpcStore, rows: &[Row<'_>]) -> Result<Option<Vec<Ladder>>, Error> {
279        let mut fresh = ColourSlot::default();
280        let mut held = self.colours.try_borrow_mut();
281        // A question asked while another is being answered finds the slot held:
282        // it answers from a slot of its own rather than panic (as
283        // `IndexSlot::with`). Only the real slot reads the index's account of
284        // what was disturbed - a throwaway one must not eat it.
285        let (slot, kept): (&mut ColourSlot, bool) = match held.as_deref_mut() {
286            Ok(slot) if self.layers().colours => (slot, true),
287            _ => (&mut fresh, false),
288        };
289        self.with_index(store, |index| -> Result<Option<Vec<Ladder>>, Error> {
290            let dirty = if kept {
291                self.index.take_disturbed(Consumer::Colours)
292            } else {
293                Disturbed::default()
294            };
295            settle(store, slot, index, dirty)?;
296            let ColourSlot { cache, set, .. } = slot;
297            let Some(set) = set.as_ref() else {
298                return Ok(None);
299            };
300            let cascade = Cascade::new(set);
301            let requests: Vec<Request<'_>> = rows
302                .iter()
303                .map(|row| Request {
304                    representation: &row.key,
305                    property: row.property,
306                    kind_chain: &row.kinds,
307                    element: Scope::WholeObject,
308                })
309                .collect();
310            Ok(Some(cascade.ladders_cached(cache, index, store, &requests)?))
311        })
312    }
313
314    /// The colour the **file** gives each object `names` name, as the 3D view's
315    /// ladder decides it (a constant colour, `#rrggbbaa`, or `None` when no
316    /// rung answers or the answer is a map), **keyed by the object's full uri**
317    /// (the name itself for an object the package does not hold). One walker
318    /// for the lot, from the walks the cache keeps: what the section view asks
319    /// instead of walking again.
320    ///
321    /// A name that fits two versions is refused by name. What each version
322    /// *answers* is bounded by the walk beneath, which compares uuids: the
323    /// answers are two, one per uri, and they agree until the domain's walk is
324    /// keyed by identity too.
325    pub fn file_colours_at(
326        &self,
327        store: &EpcStore,
328        names: &[String],
329    ) -> Result<HashMap<String, Option<String>>, Error> {
330        let mut keys = Vec::with_capacity(names.len());
331        let mut rows: Vec<Row<'_>> = Vec::with_capacity(names.len());
332        for name in names {
333            let key = match store.resolve(name.trim()) {
334                Ok(summary) => summary.uri.clone(),
335                Err(energyml_storage::Error::NotFound(_)) => name.trim().to_string(),
336                Err(other) => return Err(other.into()),
337            };
338            rows.push(Row::at(store, name, Vec::new(), None)?);
339            keys.push(key);
340        }
341        let Some(ladders) = self.ladders(store, &rows)? else {
342            return Ok(keys.into_iter().map(|key| (key, None)).collect());
343        };
344        Ok(keys
345            .into_iter()
346            .zip(ladders)
347            .map(|(key, ladder)| {
348                let colour = match ladder.walked.resolution.map(|found| found.value) {
349                    Some(Decision {
350                        paint: Paint::Constant(colour),
351                        ..
352                    }) => Some(colour.to_hex()),
353                    _ => None,
354                };
355                (key, colour)
356            })
357            .collect())
358    }
359
360    /// [`Cache::file_colours_at`] for the callers that hold a bare uuid: the
361    /// answer is keyed by the **lowercase uuid** asked. A uuid the package
362    /// holds in two versions is refused by name; ask by uri.
363    pub fn file_colours(
364        &self,
365        store: &EpcStore,
366        uuids: &[String],
367    ) -> Result<HashMap<String, Option<String>>, Error> {
368        let by_uri = self.file_colours_at(store, uuids)?;
369        let mut out = HashMap::with_capacity(uuids.len());
370        for uuid in uuids {
371            let key = match store.resolve(uuid.trim()) {
372                Ok(summary) => summary.uri.clone(),
373                Err(_) => uuid.trim().to_string(),
374            };
375            out.insert(uuid.to_ascii_lowercase(), by_uri.get(&key).cloned().flatten());
376        }
377        Ok(out)
378    }
379
380    /// **The colour rule, once**: what one object - or one property of it -
381    /// inherits, by the inheritance-resolution order: the object, its
382    /// collections, its parents and theirs, then the model; for a property, its
383    /// kind and the kind's parents and nothing beyond. The answer carries its
384    /// level; the notes of the walk join the decision's. `None` means no level
385    /// answered - an answer, not a failure: the standard defines no default
386    /// colour.
387    ///
388    /// `representation` is a **uuid** - an entry's target is one, and the walk
389    /// up to the interpretation and the feature yields them; passing the uri
390    /// (which the front end once did) meant a colour set on the
391    /// representation itself was never found. A caller that holds the uri asks
392    /// [`Cache::decision_at`], which turns it into that key. `chain` is the property's kind
393    /// chain ([`kind_chain`]). `set` is read by the caller, once, for many
394    /// questions ([`graphical_set`]).
395    pub fn decision(
396        &self,
397        store: &EpcStore,
398        set: &GraphicalSet,
399        representation: &str,
400        property: Option<&str>,
401        chain: &[String],
402        options: &Options,
403    ) -> Result<Option<Decision>, Error> {
404        self.with_index(store, |index| {
405            decide_in(index, store, set, representation, property, chain, options)
406        })
407    }
408
409    /// [`Cache::decision`] for the object `name` names: its **full uri** (a
410    /// short uri or a bare uuid are adapters, refused by name when they fit two
411    /// versions). The one place a uri becomes the walk's key.
412    pub fn decision_at(
413        &self,
414        store: &EpcStore,
415        set: &GraphicalSet,
416        name: &str,
417        property: Option<&str>,
418        chain: &[String],
419        options: &Options,
420    ) -> Result<Option<Decision>, Error> {
421        let key = walk_key(store, name)?;
422        self.decision(store, set, &key, property, chain, options)
423    }
424
425    /// What the colour walks did so far - walks begun and dropped, tiers read,
426    /// objects read. **Work, not time**: what a test asserts to say a walk was
427    /// not done twice, and what an invalidation cost.
428    pub fn colour_walk_stats(&self) -> Stats {
429        self.colours
430            .try_borrow()
431            .map(|slot| slot.cache.stats())
432            .unwrap_or_default()
433    }
434
435    /// Whether the cache holds the walk of this object - for the tests of the
436    /// invalidation, which say what was thrown away and what was kept.
437    #[doc(hidden)]
438    pub fn colour_walk_held(&self, uuid: &str) -> bool {
439        self.colours
440            .try_borrow()
441            .map(|slot| slot.cache.holds(uuid))
442            .unwrap_or(false)
443    }
444}
445
446/// A cache and the store it is the cache of - what the scene asks its colours
447/// of (`energyml_geometry::scene::Request::colours`).
448pub struct Colouring<'a> {
449    /// The cache.
450    pub cache: &'a Cache,
451    /// The store it answers for.
452    pub store: &'a EpcStore,
453}
454
455impl energyml_geometry::scene::Decide for Colouring<'_> {
456    fn decide(&self, set: &GraphicalSet, request: &Request<'_>) -> Option<Decision> {
457        self.cache
458            .decision(
459                self.store,
460                set,
461                request.representation,
462                request.property,
463                request.kind_chain,
464                &Options::default(),
465            )
466            .ok()
467            .flatten()
468    }
469}
470
471// ---------------------------------------------------------------------------
472// The package, object by object
473// ---------------------------------------------------------------------------
474
475/// What [`of_package`] is asked.
476#[derive(Clone, Debug, Default)]
477pub struct Wanted<'a> {
478    /// Only this object, by **uri** (a bare uuid is an adapter, refused by name
479    /// when the package holds it in two versions). `None` for every
480    /// representation and property.
481    pub object: Option<&'a str>,
482    /// The cascade's switches.
483    pub options: Options,
484}
485
486/// One object and the colour the package gives it.
487#[derive(Clone, Debug)]
488pub struct Coloured {
489    /// The object.
490    pub summary: ObjectSummary,
491    /// What its walk decided; `None` when no level answered.
492    pub decision: Option<Decision>,
493}
494
495/// The colours of a package, object by object.
496#[derive(Clone, Debug, Default)]
497pub struct Report {
498    /// How many `GraphicalInformationSet`s the package holds. `0` is the
499    /// standard, not a gap, for a RESQML 2.0.1 package: that release has no
500    /// notion of colour at all.
501    pub sets: usize,
502    /// What reading the sets said on the way.
503    pub notes: Vec<String>,
504    /// Every representation and property asked, in package order.
505    pub rows: Vec<Coloured>,
506    /// How many of them have a colour - the count a test can assert, so that
507    /// an empty answer does not pass for a true one.
508    pub coloured: usize,
509}
510
511/// What every representation and every property of the package inherits, by
512/// [`Cache::decision`] - **what `energyml colors` prints**, and what the front end's
513/// `colour_of` answers object by object.
514///
515/// A property is walked through its own chain (its kind and the kind's
516/// parents, [`kind_chain`]) with its supporting representation; a property
517/// that does not read is left out, as it always was.
518pub fn of_package(cache: &Cache, store: &EpcStore, wanted: &Wanted<'_>) -> Result<Report, Error> {
519    let sets: Vec<GraphicalSet> = graphical_sets(store)?.into_iter().map(|(_, set)| set).collect();
520    if sets.is_empty() {
521        return Ok(Report::default());
522    }
523    let count = sets.len();
524    let set = GraphicalSet::merged(sets);
525    let notes = set.notes.clone();
526    // One door: the object asked is the one the store resolves the name to,
527    // compared by its full uri so that one version of a uuid is not the other.
528    let wanted_uri: Option<String> = match wanted.object {
529        None => None,
530        Some(name) => match store.resolve(name.trim()) {
531            Ok(summary) => Some(summary.uri.clone()),
532            Err(energyml_storage::Error::NotFound(_)) => Some(name.trim().to_string()),
533            Err(other) => return Err(other.into()),
534        },
535    };
536
537    // One index for the whole walk: a cache that keeps nothing builds it once.
538    let rows = cache.with_index(store, |index| -> Result<Vec<Coloured>, Error> {
539        let mut rows = Vec::new();
540        for summary in store.resources() {
541            let is_representation = summary.qualified_type.contains("Representation");
542            let is_property = summary.qualified_type.ends_with("Property");
543            if !is_representation && !is_property {
544                continue;
545            }
546            if let Some(wanted) = &wanted_uri
547                && summary.uri != *wanted
548            {
549                continue;
550            }
551            let decision = if is_property {
552                let Ok(object) = store.get_object(&summary.uri) else {
553                    continue;
554                };
555                let Ok(read) = property::read(object.as_reflect()) else {
556                    continue;
557                };
558                let chain = kind_chain(store, &read);
559                // By the uri the property names, so the version it names is the
560                // one whose colour it inherits.
561                let representation = read
562                    .supporting_representation_uri
563                    .clone()
564                    .or_else(|| read.supporting_representation.clone())
565                    .unwrap_or_default();
566                decide_in(
567                    index,
568                    store,
569                    &set,
570                    &representation,
571                    Some(&summary.uri),
572                    &chain,
573                    &wanted.options,
574                )?
575            } else {
576                decide_in(index, store, &set, &summary.uri, None, &[], &wanted.options)?
577            };
578            rows.push(Coloured {
579                summary: summary.clone(),
580                decision,
581            });
582        }
583        Ok(rows)
584    })?;
585    let coloured = rows.iter().filter(|row| row.decision.is_some()).count();
586    Ok(Report {
587        sets: count,
588        notes,
589        rows,
590        coloured,
591    })
592}