Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/api/src/lib/services/data-selection.service.ts is written in an unsupported language. File is not indexed.

0001 import { Injectable, computed, inject, signal } from '@angular/core';
0002 import type { DataCatalogEntry, DataSource } from '@dexvis/firebird-core';
0003 import { ConfigService } from '@dexvis/app-features';
0004 import { DataCatalogService } from './data-catalog.service';
0005 import {
0006   DEX_EVENTS_SOURCE_CONFIG,
0007   GEOMETRY_URL_CONFIG,
0008   ROOT_COLLECTIONS_CONFIG,
0009   ROOT_EVENT_RANGE_CONFIG,
0010   ROOT_EVENTS_SOURCE_CONFIG,
0011 } from '../config-keys';
0012 
0013 /**
0014  * What the user chose in the data selector. Each field: `undefined` = leave
0015  * the current setting alone, `''` = clear it, a string = a URL, a File = a
0016  * picked/dropped local file (read in place, never uploaded, never persisted).
0017  */
0018 export interface DataSelection {
0019   geometry?: DataSource | '';
0020   events?: DataSource | '';
0021   /** Event numbers to convert when `events` is a ROOT file: '0', '0-4', '1,3'. */
0022   eventRange?: string;
0023   /** Collection groups to convert when `events` is a ROOT file ('' = all). */
0024   collections?: string;
0025   /** Catalog entry the selection came from, when it did. */
0026   entry?: DataCatalogEntry;
0027 }
0028 
0029 /** Which halves of a selection changed — what a display reload has to touch. */
0030 export interface SelectionParts {
0031   geometry: boolean;
0032   events: boolean;
0033 }
0034 
0035 /**
0036  * Config keys the selection writes. Deep links, pyrobird config and yaml use
0037  * the same keys; their defaults and validators live in `config-keys.ts`.
0038  */
0039 export const SELECTION_CONFIG_KEYS = {
0040   geometry: GEOMETRY_URL_CONFIG.key,
0041   dexEvents: DEX_EVENTS_SOURCE_CONFIG.key,
0042   rootEvents: ROOT_EVENTS_SOURCE_CONFIG.key,
0043   rootEventRange: ROOT_EVENT_RANGE_CONFIG.key,
0044   rootCollections: ROOT_COLLECTIONS_CONFIG.key,
0045 } as const;
0046 
0047 /** A `.root` source goes to the ROOT event keys, anything else to the DEX key. */
0048 export function isRootSource(source: DataSource): boolean {
0049   const name = typeof source === 'string' ? source.split('?')[0].split('#')[0] : source.name;
0050   return /\.root$/i.test(name);
0051 }
0052 
0053 /**
0054  * The one "apply" path of the data selector, shared by the display toolbar
0055  * panel and the config page: the selection is written to the config keys
0056  * (`geometry.selectedGeometry`, `events.dexEventsSource`, ...), and a live
0057  * display then reloads from those keys — the same load the display runs at
0058  * startup. The config page has no display; it writes the keys and navigates,
0059  * and the display page picks them up on init.
0060  *
0061  * Picked local Files cannot live in config (a blob URL dies with the page and
0062  * loses the file name that decides the loader), so they wait in a stash that
0063  * the display's config load consumes first. The config keys keep their last
0064  * URL meanwhile: after a reload the URL loads again, the file is gone.
0065  */
0066 @Injectable({ providedIn: 'root' })
0067 export class DataSelectionService {
0068   private readonly config = inject(ConfigService);
0069   private readonly catalog = inject(DataCatalogService);
0070 
0071   /** The selection tabs build up; the host's Show button applies it. */
0072   readonly draft = signal<DataSelection>({});
0073 
0074   private pickedGeometryFile: File | null = null;
0075   private pickedEventsFile: File | null = null;
0076   private displayReload: ((parts: SelectionParts) => void) | null = null;
0077   /** Parts applied while no display was attached; the next display mount loads them. */
0078   private pendingReload: SelectionParts = { geometry: false, events: false };
0079 
0080   private readonly geometryKey = this.config.declare(GEOMETRY_URL_CONFIG);
0081   private readonly dexEventsKey = this.config.declare(DEX_EVENTS_SOURCE_CONFIG);
0082   private readonly rootEventsKey = this.config.declare(ROOT_EVENTS_SOURCE_CONFIG);
0083   private readonly rootEventRangeKey = this.config.declare(ROOT_EVENT_RANGE_CONFIG);
0084   private readonly rootCollectionsKey = this.config.declare(ROOT_COLLECTIONS_CONFIG);
0085 
0086   /** Configured geometry URL (reactive). */
0087   readonly configuredGeometry = computed(() => this.geometryKey.valueSignal() || '');
0088   /** Configured events URL — the DEX key, or the ROOT key when the DEX key is empty (reactive). */
0089   readonly configuredEvents = computed(() => this.dexEventsKey.valueSignal() || this.rootEventsKey.valueSignal() || '');
0090   readonly configuredEventRange = computed(() => this.rootEventRangeKey.valueSignal() || ROOT_EVENT_RANGE_CONFIG.default);
0091   readonly configuredCollections = computed(() => this.rootCollectionsKey.valueSignal() || '');
0092 
0093   /** The catalog entry whose geometry and events match the configured values, if any. */
0094   readonly activeEntry = computed<DataCatalogEntry | null>(() => {
0095     const geometry = this.configuredGeometry();
0096     const events = this.configuredEvents();
0097     return this.catalog.entries().find(entry =>
0098       (entry.geometry ?? '') === geometry && (entry.events ?? '') === events) ?? null;
0099   });
0100 
0101   /** True when the draft would change something. */
0102   readonly draftHasChanges = computed(() => {
0103     const draft = this.draft();
0104     return draft.geometry !== undefined || draft.events !== undefined
0105       || draft.eventRange !== undefined || draft.collections !== undefined;
0106   });
0107 
0108   /**
0109    * The selection a catalog entry stands for. An entry without geometry keeps
0110    * the loaded geometry (event-only datasets); an entry without events shows
0111    * none (a "geometry only" preset clears the events).
0112    */
0113   selectionForEntry(entry: DataCatalogEntry): DataSelection {
0114     const events = entry.events ?? '';
0115     const isRoot = events !== '' && isRootSource(events);
0116     return {
0117       geometry: entry.geometry,
0118       events,
0119       eventRange: isRoot ? (entry.eventRange ?? ROOT_EVENT_RANGE_CONFIG.default) : undefined,
0120       collections: isRoot ? (entry.collections ?? '') : undefined,
0121       entry,
0122     };
0123   }
0124 
0125   updateDraft(patch: DataSelection): void {
0126     this.draft.update(draft => ({ ...draft, ...patch }));
0127   }
0128 
0129   clearDraft(): void {
0130     this.draft.set({});
0131   }
0132 
0133   /**
0134    * The display registers how it reloads from config; `apply()` calls it
0135    * after writing the keys, with the parts the selection touched — an
0136    * events-only change must not reload the geometry (a picked geometry file
0137    * has no URL to dedupe on and would be replaced by the configured URL).
0138    * Registered by EventDisplayService while a display page is mounted, so
0139    * the selection service never imports the display stack.
0140    */
0141   attachDisplay(reload: (parts: SelectionParts) => void): void {
0142     this.displayReload = reload;
0143   }
0144 
0145   detachDisplay(): void {
0146     this.displayReload = null;
0147   }
0148 
0149   /**
0150    * Writes the selection to config, stashes picked files, and reloads a live
0151    * display. Without one (the config page), the applied parts are remembered
0152    * for the next display mount (`takePendingReload()`): the user asked for
0153    * them, so that mount loads them even when the config value did not change.
0154    */
0155   apply(selection: DataSelection = this.draft()): void {
0156     if (selection.geometry !== undefined) {
0157       if (selection.geometry instanceof File) {
0158         this.pickedGeometryFile = selection.geometry;
0159       } else {
0160         this.pickedGeometryFile = null;
0161         this.geometryKey.value = selection.geometry;
0162       }
0163     }
0164     if (selection.events !== undefined) {
0165       if (selection.events instanceof File) {
0166         this.pickedEventsFile = selection.events;
0167         // A stashed file loads INSTEAD of the URL keys, so both keys are
0168         // cleared to keep the startup load from also fetching the old URL
0169         this.dexEventsKey.value = '';
0170         this.rootEventsKey.value = '';
0171       } else {
0172         this.pickedEventsFile = null;
0173         // One events field, two keys: the other key is cleared so the startup
0174         // load (which runs both) does not load two datasets
0175         const isRoot = selection.events !== '' && isRootSource(selection.events);
0176         this.rootEventsKey.value = isRoot ? selection.events : '';
0177         this.dexEventsKey.value = isRoot ? '' : selection.events;
0178       }
0179     }
0180     if (selection.eventRange !== undefined) {
0181       this.rootEventRangeKey.value = selection.eventRange.trim() || ROOT_EVENT_RANGE_CONFIG.default;
0182     }
0183     if (selection.collections !== undefined) {
0184       this.rootCollectionsKey.value = selection.collections;
0185     }
0186     this.draft.set({});
0187     const parts: SelectionParts = {
0188       geometry: selection.geometry !== undefined,
0189       events: selection.events !== undefined || selection.eventRange !== undefined || selection.collections !== undefined,
0190     };
0191     if (!parts.geometry && !parts.events) return;
0192     if (this.displayReload) {
0193       this.displayReload(parts);
0194     } else {
0195       this.pendingReload = {
0196         geometry: this.pendingReload.geometry || parts.geometry,
0197         events: this.pendingReload.events || parts.events,
0198       };
0199     }
0200   }
0201 
0202   /** Hands out (once) the parts applied while no display was attached. */
0203   takePendingReload(): SelectionParts {
0204     const parts = this.pendingReload;
0205     this.pendingReload = { geometry: false, events: false };
0206     return parts;
0207   }
0208 
0209   /** Hands the stashed geometry file to the loader (once). */
0210   takePickedGeometryFile(): File | null {
0211     const file = this.pickedGeometryFile;
0212     this.pickedGeometryFile = null;
0213     return file;
0214   }
0215 
0216   /** Hands the stashed events file to the loader (once). */
0217   takePickedEventsFile(): File | null {
0218     const file = this.pickedEventsFile;
0219     this.pickedEventsFile = null;
0220     return file;
0221   }
0222 }