Back to home page

EIC code displayed by LXR

 
 

    


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

0001 import { Injectable, computed, inject, signal } from '@angular/core';
0002 // Subpath import: this service is reachable from the initial bundle, and the
0003 // core barrel re-exports the painters, which pull three.js into startup.
0004 import {
0005   DataCatalog,
0006   DataCatalogEntry,
0007   catalogEventSources,
0008   catalogGeometrySources,
0009   mergeDataCatalogs,
0010 } from '@dexvis/firebird-core/data-catalog';
0011 import { DATA_CATALOGS } from '../tokens';
0012 import { ConfigService, ServerConfigService, isPersistableUrl } from '@dexvis/app-features';
0013 import type { ServerConfig } from './server-config';
0014 import { UrlService } from './url.service';
0015 
0016 /** Config key: URL of a remote catalog JSON file (a `DataCatalog` object), '' = none. */
0017 export const CATALOG_URL_CONFIG_KEY = 'catalog.url';
0018 
0019 /**
0020  * The merged data catalog: what the data selector offers as presets, physics
0021  * facets and manual pick lists.
0022  *
0023  * Sources, merged in this order (later entries append; same-key facets merge):
0024  * 1. `withDataCatalog()` features of the packs the app is assembled from
0025  * 2. the server's config.jsonc `dataCatalog` object (pyrobird passes it through)
0026  * 3. a remote catalog file named by the `catalog.url` config key
0027  *
0028  * The remote file is fetched on the first `ensureRemoteLoaded()` call — the
0029  * data selector makes it when it opens — never at app start, so a slow or
0030  * missing catalog host does not delay every page load.
0031  */
0032 @Injectable({ providedIn: 'root' })
0033 export class DataCatalogService {
0034   private readonly packCatalogs = inject(DATA_CATALOGS, { optional: true }) ?? [];
0035   private readonly serverConfig = inject<ServerConfigService<ServerConfig>>(ServerConfigService);
0036   private readonly config = inject(ConfigService);
0037   private readonly urls = inject(UrlService);
0038 
0039   private readonly remoteCatalog = signal<DataCatalog | null>(null);
0040   private remoteLoad: Promise<void> | null = null;
0041 
0042   /** Error text of the last failed remote fetch, for the UI to show. */
0043   readonly remoteError = signal<string | null>(null);
0044 
0045   private readonly catalogUrl = this.config.declare<string>({
0046     key: CATALOG_URL_CONFIG_KEY,
0047     default: '',
0048     validator: isPersistableUrl,
0049     label: 'Remote data catalog URL',
0050     group: 'Data',
0051   });
0052 
0053   /** Everything merged. Recomputes when the server config or the remote catalog arrives. */
0054   readonly catalog = computed<DataCatalog>(() => mergeDataCatalogs([
0055     ...this.packCatalogs,
0056     this.serverConfig.configSignal().dataCatalog,
0057     this.remoteCatalog(),
0058   ]));
0059 
0060   readonly entries = computed<DataCatalogEntry[]>(() => this.catalog().entries ?? []);
0061   readonly facets = computed(() => this.catalog().facets ?? []);
0062   readonly geometrySources = computed(() => catalogGeometrySources(this.catalog()));
0063   readonly eventSources = computed(() => catalogEventSources(this.catalog()));
0064 
0065   /** True when no source contributed anything. Catalog-only tabs hide themselves then. */
0066   readonly isEmpty = computed(() =>
0067     this.entries().length === 0 && this.geometrySources().length === 0 && this.eventSources().length === 0);
0068 
0069   /** Fetches the `catalog.url` file once. Resolves (never rejects) — failures land in `remoteError`. */
0070   ensureRemoteLoaded(): Promise<void> {
0071     if (this.remoteLoad) return this.remoteLoad;
0072     const url = (this.catalogUrl.value || '').trim();
0073     if (!url) return Promise.resolve();
0074     this.remoteLoad = (async () => {
0075       try {
0076         const response = await fetch(this.urls.resolveDownloadUrl(url));
0077         if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
0078         const catalog = await response.json() as DataCatalog;
0079         if (!catalog || typeof catalog !== 'object') throw new Error('not a JSON object');
0080         this.remoteCatalog.set(catalog);
0081         this.remoteError.set(null);
0082       } catch (error) {
0083         const message = error instanceof Error ? error.message : String(error);
0084         console.error(`[DataCatalog] failed to load remote catalog '${url}': ${message}`);
0085         this.remoteError.set(`Remote catalog '${url}' failed to load: ${message}`);
0086       }
0087     })();
0088     return this.remoteLoad;
0089   }
0090 }