Warning, /firebird/packages/firebird-core/src/data-catalog.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Data catalog: the datasets an installation offers to open — named presets
0003 * (geometry + events), tagged so a UI can filter them by physics facets, and
0004 * plain URL lists for manual pick.
0005 *
0006 * Plain TS on purpose: pyrobird serves the same JSON shape through
0007 * config.jsonc (`dataCatalog`), a remote catalog file has this shape too, and
0008 * experiment packs contribute it through `withDataCatalog()` in the Angular
0009 * app. Helpers here are pure so the same filtering runs in any host.
0010 */
0011
0012 /** One dataset: what to load, and how it is described and tagged. */
0013 export interface DataCatalogEntry {
0014 /** Display name in preset lists. */
0015 name: string;
0016 /** One or two sentences shown under the name. */
0017 description?: string;
0018 /** Web page with more information about the dataset. */
0019 link?: string;
0020 /** Detector geometry URL. Absent: keep whatever geometry is loaded/configured. */
0021 geometry?: string;
0022 /** Event data URL: a DEX file, or a ROOT file the loaders can convert. */
0023 events?: string;
0024 /** Event numbers to convert when `events` is a ROOT file: '0', '0-4', '1,3'. */
0025 eventRange?: string;
0026 /** Collection groups to convert when `events` is a ROOT file ('' or absent = all). */
0027 collections?: string;
0028 /**
0029 * Facet values, e.g. `{ process: 'dis-nc', beam: '10x100', minQ2: '1000' }`.
0030 * Keys are free; the facets an installation declares decide which keys a
0031 * physics-selection UI turns into choices.
0032 */
0033 tags?: Record<string, string>;
0034 }
0035
0036 /** Description of one tag value: what a UI shows when the value is chosen. */
0037 export interface DataCatalogFacetValue {
0038 /** Chip caption; the raw tag value when absent. */
0039 label?: string;
0040 /** Explanation shown when the value is selected. */
0041 description?: string;
0042 /** Web page with more information. */
0043 link?: string;
0044 }
0045
0046 /** One selectable dimension of the physics picker: a tag key with its known values. */
0047 export interface DataCatalogFacet {
0048 /** Tag key in `DataCatalogEntry.tags`. */
0049 key: string;
0050 /** Row caption, e.g. 'Process', 'Beam'. */
0051 label: string;
0052 /**
0053 * Known values in display order, with captions and descriptions. Values
0054 * that entries use but this map omits are still offered, after these, in
0055 * first-seen order.
0056 */
0057 values?: Record<string, DataCatalogFacetValue>;
0058 }
0059
0060 /** Everything one source (pack, server, remote file) contributes. */
0061 export interface DataCatalog {
0062 entries?: DataCatalogEntry[];
0063 facets?: DataCatalogFacet[];
0064 /** Extra geometry URLs offered for manual pick, beyond those of `entries`. */
0065 geometrySources?: string[];
0066 /** Extra event data URLs offered for manual pick, beyond those of `entries`. */
0067 eventSources?: string[];
0068 }
0069
0070 /** Merges catalogs in order; facets with the same key merge their value maps (later wins). */
0071 export function mergeDataCatalogs(catalogs: ReadonlyArray<DataCatalog | null | undefined>): DataCatalog {
0072 const merged: Required<DataCatalog> = { entries: [], facets: [], geometrySources: [], eventSources: [] };
0073 const facetsByKey = new Map<string, DataCatalogFacet>();
0074 for (const catalog of catalogs) {
0075 if (!catalog) continue;
0076 merged.entries.push(...(catalog.entries ?? []));
0077 merged.geometrySources.push(...(catalog.geometrySources ?? []));
0078 merged.eventSources.push(...(catalog.eventSources ?? []));
0079 for (const facet of catalog.facets ?? []) {
0080 const existing = facetsByKey.get(facet.key);
0081 if (existing) {
0082 facetsByKey.set(facet.key, { ...existing, ...facet, values: { ...existing.values, ...facet.values } });
0083 } else {
0084 facetsByKey.set(facet.key, { ...facet, values: { ...facet.values } });
0085 }
0086 }
0087 }
0088 merged.facets = [...facetsByKey.values()];
0089 return merged;
0090 }
0091
0092 /** Distinct values of `key` across entries: the facet's declared order first, then first-seen order. */
0093 export function facetValues(entries: ReadonlyArray<DataCatalogEntry>, facet: DataCatalogFacet): string[] {
0094 const seen = new Set<string>();
0095 for (const entry of entries) {
0096 const value = entry.tags?.[facet.key];
0097 if (value !== undefined) seen.add(value);
0098 }
0099 const declared = Object.keys(facet.values ?? {}).filter(value => seen.has(value));
0100 const undeclared = [...seen].filter(value => !(value in (facet.values ?? {})));
0101 return [...declared, ...undeclared];
0102 }
0103
0104 /** Entries whose tags carry every selected value (an empty selection matches all). */
0105 export function matchEntries(
0106 entries: ReadonlyArray<DataCatalogEntry>,
0107 selectedTags: Readonly<Record<string, string>>,
0108 ): DataCatalogEntry[] {
0109 const wanted = Object.entries(selectedTags);
0110 return entries.filter(entry => wanted.every(([key, value]) => entry.tags?.[key] === value));
0111 }
0112
0113 /** Distinct, order-preserving list of geometry URLs the catalog offers. */
0114 export function catalogGeometrySources(catalog: DataCatalog): string[] {
0115 return distinct([
0116 ...(catalog.entries ?? []).map(entry => entry.geometry),
0117 ...(catalog.geometrySources ?? []),
0118 ]);
0119 }
0120
0121 /** Distinct, order-preserving list of event data URLs the catalog offers. */
0122 export function catalogEventSources(catalog: DataCatalog): string[] {
0123 return distinct([
0124 ...(catalog.entries ?? []).map(entry => entry.events),
0125 ...(catalog.eventSources ?? []),
0126 ]);
0127 }
0128
0129 function distinct(values: ReadonlyArray<string | undefined>): string[] {
0130 const result: string[] = [];
0131 const seen = new Set<string>();
0132 for (const value of values) {
0133 if (!value || seen.has(value)) continue;
0134 seen.add(value);
0135 result.push(value);
0136 }
0137 return result;
0138 }