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(¬hing),
232 fresh.as_ref().unwrap_or(¬hing),
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(¬e) {
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}