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 }