Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/components/data-selector/manual-tab.component.ts is written in an unsupported language. File is not indexed.

0001 import { ChangeDetectionStrategy, Component, computed, inject, signal, untracked } from '@angular/core';
0002 import { MatIcon } from '@angular/material/icon';
0003 import type { DataSource } from '@dexvis/firebird-core';
0004 import { ConfigService } from '@dexvis/app-features';
0005 import {
0006   DataCatalogService,
0007   DataSelectionService,
0008   GEOMETRY_ROOT_FILTER_CONFIG,
0009   isRootSource,
0010   OPTIMIZE_EDIT_RULE_SET,
0011   resolveRootGeometryRules,
0012   ROOT_GEOMETRY_RULES,
0013   type RootGeometryEditRuleSet,
0014 } from '@dexvis/firebird-ng/api';
0015 import { FileOpenRouterService } from '../../services/file-open-router.service';
0016 import { RootEventPickerComponent } from './root-event-picker.component';
0017 import { SourcePickerComponent } from './source-picker.component';
0018 
0019 /**
0020  * Manual tab: geometry and events each by URL (a known one from the catalog,
0021  * or typed) or from a local file. A ROOT event source gets the event-range
0022  * and collection-group picker under it.
0023  *
0024  * A `.root` file holds geometry OR events, and a user does not always know
0025  * which: every `.root` source is probed through FileOpenRouterService, and one
0026  * dropped into the wrong field moves to the right one with a note.
0027  */
0028 @Component({
0029   selector: 'firebird-manual-tab',
0030   changeDetection: ChangeDetectionStrategy.OnPush,
0031   imports: [MatIcon, SourcePickerComponent, RootEventPickerComponent],
0032   templateUrl: './manual-tab.component.html',
0033   styleUrl: './manual-tab.component.scss',
0034 })
0035 export class ManualTabComponent {
0036   private readonly selection = inject(DataSelectionService);
0037   private readonly catalog = inject(DataCatalogService);
0038   private readonly router = inject(FileOpenRouterService);
0039   private readonly config = inject(ConfigService);
0040 
0041   readonly geometryOptions = this.catalog.geometrySources;
0042   readonly eventOptions = this.catalog.eventSources;
0043 
0044   // Fields start from what is configured; only what the user changes goes to the draft
0045   readonly geometry = signal<DataSource | ''>(this.selection.configuredGeometry());
0046   readonly events = signal<DataSource | ''>(this.selection.configuredEvents());
0047   readonly eventRange = signal(this.selection.configuredEventRange());
0048   readonly collections = signal(this.selection.configuredCollections());
0049   readonly note = signal<string | null>(null);
0050   // Bumped when a typed source leaves a field, so that field shows its value again
0051   readonly geometryResync = signal(0);
0052   readonly eventsResync = signal(0);
0053   readonly eventsIsRoot = computed(() => this.events() !== '' && isRootSource(this.events() as DataSource));
0054 
0055   /**
0056    * "Optimize geometry" switches config `geometry.rootFilterName` between
0057    * the edit rule set OPTIMIZE_EDIT_RULE_SET and 'off'; the finer pipeline
0058    * options stay on the config page. Creation is untracked: declaring a key
0059    * applies pending layer values (signal writes), which a computed() forbids.
0060    */
0061   private readonly rootFilter = untracked(() => this.config.declare(GEOMETRY_ROOT_FILTER_CONFIG));
0062   readonly optimize = computed(() => this.rootFilter.valueSignal() !== 'off');
0063 
0064   /**
0065    * The rule set the toggle selects, or null while none is known. Without a
0066    * pack that registers it the toggle stays hidden: it would select nothing.
0067    * Rule data may arrive through a dynamic import, so it resolves once here.
0068    */
0069   readonly optimizeRuleSet = signal<RootGeometryEditRuleSet | null>(null);
0070 
0071   constructor() {
0072     const ruleSources = inject(ROOT_GEOMETRY_RULES, { optional: true }) ?? [];
0073     if (ruleSources.length === 0) return;
0074     resolveRootGeometryRules(ruleSources).then(
0075       rules => this.optimizeRuleSet.set(rules.editRules?.[OPTIMIZE_EDIT_RULE_SET] ?? null),
0076       error => console.error('[DataSelector] Geometry rules failed to load:', error),
0077     );
0078   }
0079 
0080   toggleOptimize(): void {
0081     this.rootFilter.value = this.optimize() ? 'off' : OPTIMIZE_EDIT_RULE_SET;
0082   }
0083 
0084   onGeometryChange(value: DataSource | ''): void {
0085     this.note.set(null);
0086     void this.placeSource(value, 'geometry');
0087   }
0088 
0089   onEventsChange(value: DataSource | ''): void {
0090     this.note.set(null);
0091     void this.placeSource(value, 'events');
0092   }
0093 
0094   onEventRangeChange(value: string): void {
0095     this.eventRange.set(value);
0096     this.selection.updateDraft({ eventRange: value });
0097   }
0098 
0099   onCollectionsChange(value: string): void {
0100     this.collections.set(value);
0101     this.selection.updateDraft({ collections: value });
0102   }
0103 
0104   /** Bumped per change of each field; a probe for an older value is not applied. */
0105   private readonly placeTokens = { geometry: 0, events: 0 };
0106 
0107   /**
0108    * Puts the source into its field — after asking the loaders which field a
0109    * `.root` source belongs to. Probes run in parallel; when a field changes
0110    * again before its probe answers, only the latest value is placed.
0111    */
0112   private async placeSource(value: DataSource | '', field: 'geometry' | 'events'): Promise<void> {
0113     const token = ++this.placeTokens[field];
0114     const isLatest = () => token === this.placeTokens[field];
0115     let target = field;
0116     if (value !== '' && isRootSource(value)) {
0117       try {
0118         const route = await this.router.route(value);
0119         if (!isLatest()) return;
0120         if (route.kind === 'unknown') {
0121           this.note.set(route.message);
0122         } else if (route.kind !== field) {
0123           target = route.kind;
0124           const name = typeof value === 'string' ? value : value.name;
0125           this.note.set(`'${name}' holds ${route.kind}, so it went to the ${route.kind} field.`);
0126           (field === 'geometry' ? this.geometryResync : this.eventsResync).update(token => token + 1);
0127         }
0128       } catch (error) {
0129         if (!isLatest()) return;
0130         // The probe could not read the file (offline, CORS, server path
0131         // without pyrobird): keep the value where it was typed, Show still
0132         // tries the load, and the reason is on screen
0133         this.note.set(`Could not look inside the file: ${error instanceof Error ? error.message : error}`);
0134       }
0135     }
0136     if (target === 'geometry') {
0137       this.geometry.set(value);
0138       this.selection.updateDraft({ geometry: value });
0139     } else {
0140       this.events.set(value);
0141       this.selection.updateDraft({ events: value });
0142     }
0143   }
0144 }