Warning, /firebird/packages/firebird-ng/src/lib/builtin-loaders.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Built-in data loaders, registered through the same GEOMETRY_LOADERS /
0003 * EVENT_LOADERS tokens that user extensions get (built-ins are first
0004 * consumers of the extension API). The display asks each loader `canLoad()`
0005 * in registration order; the first taker loads the source (see tokens.ts for
0006 * the rule). A loader returns the data; EventDisplayService shows it.
0007 *
0008 * A `.root` file is ambiguous by name - it holds detector geometry OR events -
0009 * so the ROOT loaders also implement `canLoadContent()`, which the routing
0010 * control uses after probing a picked or dropped file. Each loader keeps the
0011 * knowledge of what its own format looks like; the control only carries the
0012 * probe around.
0013 *
0014 * Bundle note: these classes are referenced from app.config (the initial
0015 * bundle), so the services that do the work (GeometryService, RootFileService,
0016 * DataModelService) come from the display entry through a DYNAMIC import at
0017 * load time. A static import would drag three.js and the whole display stack
0018 * into the initial bundle.
0019 */
0020
0021 import { Injectable, Injector, inject } from '@angular/core';
0022 // The /loaders subpath (initial-bundle file): the core root would pull the
0023 // painter modules and with them three.js.
0024 import { matchesFileExtensions, sourceName } from '@dexvis/firebird-core/loaders';
0025 import type { Object3D } from 'three';
0026 import type {
0027 DataExchange,
0028 DataLoaderMeta,
0029 DataSource,
0030 EventDataLoader,
0031 FileContentProbe,
0032 GeometryDataLoader,
0033 LoaderContext,
0034 } from '@dexvis/firebird-core';
0035 import {
0036 ConfigService,
0037 ROOT_COLLECTIONS_CONFIG,
0038 ROOT_EVENT_RANGE_CONFIG,
0039 TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG,
0040 } from '@dexvis/firebird-ng/api';
0041
0042 /** Sources the browser can byte-range on its own, with no server helping. */
0043 function isDirectlyReadable(source: DataSource): boolean {
0044 if (typeof source !== 'string') return true; // a picked/dropped file
0045 const lower = source.toLowerCase();
0046 return lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('asset://');
0047 }
0048
0049 /**
0050 * Parses a comma-separated config list ('tracker_hits,mc_particles') into its
0051 * trimmed names. An empty list gives undefined: the converter's default.
0052 */
0053 function parseListConfig(value: string): string[] | undefined {
0054 const names = value.split(',').map(name => name.trim()).filter(Boolean);
0055 return names.length ? names : undefined;
0056 }
0057
0058 /** Firebird DEX event files (.firebird.json / .firebird.zip), by URL or picked/dropped. */
0059 @Injectable()
0060 export class DexEventLoader implements EventDataLoader {
0061 readonly meta: DataLoaderMeta = {
0062 id: 'firebird-dex',
0063 label: 'Firebird DEX events (json/zip)',
0064 fileExtensions: ['.firebird.json', '.firebird.zip', '.firebird.json.zip', '.json', '.zip'],
0065 };
0066
0067 private injector = inject(Injector);
0068
0069 canLoad(source: DataSource): boolean {
0070 return matchesFileExtensions(source, this.meta);
0071 }
0072
0073 async loadEvents(source: DataSource, context: LoaderContext): Promise<DataExchange> {
0074 const { DataModelService } = await import('@dexvis/firebird-ng/display');
0075 const dataModel = this.injector.get(DataModelService);
0076 // A picked/dropped file is read in place, never uploaded
0077 if (typeof source !== 'string') return dataModel.readDexFile(source);
0078 return dataModel.fetchDex(context.resolveUrl(source), { name: source, signal: context.signal });
0079 }
0080 }
0081
0082 /**
0083 * EDM4eic / EDM4hep ROOT files converted IN THE BROWSER by @dexvis/root2dex.
0084 *
0085 * Claims what the browser can byte-range itself: http(s) and asset URLs, and
0086 * files the user picked or dropped. Everything else with a `.root` name -
0087 * `root://` XRootD URLs above all, and paths that pyrobird serves from its work
0088 * directory - falls through to Edm4eicEventLoader, which keeps going through
0089 * pyrobird's convert endpoint. Registration order encodes that: this loader is
0090 * asked first, the server one remains the fallback.
0091 *
0092 * Installations that would rather always convert server-side can set
0093 * `events.rootConverter` to 'server'.
0094 */
0095 @Injectable()
0096 export class Root2DexEventLoader implements EventDataLoader {
0097 readonly meta: DataLoaderMeta = {
0098 id: 'root2dex',
0099 label: 'EDM4eic/EDM4hep ROOT file (in-browser conversion)',
0100 fileExtensions: ['.root'],
0101 // The open-event panel can report the event count and let the user pick
0102 // a range before converting
0103 offersEventPicker: true,
0104 };
0105
0106 private injector = inject(Injector);
0107 private config = inject(ConfigService);
0108
0109 canLoad(source: DataSource): boolean {
0110 if (this.config.getConfigOrCreate<string>('events.rootConverter', 'browser').value === 'server') {
0111 return false;
0112 }
0113 return matchesFileExtensions(source, this.meta) && isDirectlyReadable(source);
0114 }
0115
0116 /** A podio event file has an 'events' TTree at the top level. */
0117 canLoadContent(probe: FileContentProbe): boolean {
0118 return probe.entries.some(entry => entry.name === 'events' && entry.className === 'TTree');
0119 }
0120
0121 async loadEvents(source: DataSource, context: LoaderContext): Promise<DataExchange> {
0122 const entries = this.config.declare(ROOT_EVENT_RANGE_CONFIG).value || ROOT_EVENT_RANGE_CONFIG.default;
0123 const collections = parseListConfig(this.config.declare(ROOT_COLLECTIONS_CONFIG).value || '');
0124 const trajectoryExcludedCollections = parseListConfig(this.config.declare(TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG).value || '');
0125 const { DataModelService, RootFileService } = await import('@dexvis/firebird-ng/display');
0126 context.signal.throwIfAborted();
0127
0128 // A handle of its own: the data selector's picker keeps its file open on
0129 // another handle, and an overlapping load opens its own. Closing it
0130 // rejects the conversion in flight, which is how a newer load stops it.
0131 const rootFile = this.injector.get(RootFileService).createHandle();
0132 let closed = false;
0133 const close = () => {
0134 if (closed) return;
0135 closed = true;
0136 rootFile.close();
0137 };
0138 context.signal.addEventListener('abort', close, { once: true });
0139 try {
0140 await rootFile.open(source);
0141 const converted = await rootFile.convert(entries, { collections, trajectoryExcludedCollections });
0142 for (const warning of converted.warnings) {
0143 console.warn(`[root2dex] ${warning}`);
0144 }
0145 return this.injector.get(DataModelService).parseDex(converted.dex, sourceName(source));
0146 } catch (error) {
0147 context.signal.throwIfAborted();
0148 throw error;
0149 } finally {
0150 context.signal.removeEventListener('abort', close);
0151 close();
0152 }
0153 }
0154 }
0155
0156 /**
0157 * EDM4eic / EDM4hep ROOT files converted server-side through the pyrobird
0158 * convert endpoint, which detects the data model itself. This is the path for
0159 * XRootD (`root://`) sources: pyrobird opens them remotely and converts.
0160 */
0161 @Injectable()
0162 export class Edm4eicEventLoader implements EventDataLoader {
0163 readonly meta: DataLoaderMeta = {
0164 id: 'edm4eic-root',
0165 label: 'EDM4eic/EDM4hep ROOT file (server conversion)',
0166 fileExtensions: ['.root'],
0167 urlSchemes: ['root://'],
0168 };
0169
0170 private injector = inject(Injector);
0171 private config = inject(ConfigService);
0172
0173 canLoad(source: DataSource): boolean {
0174 // The endpoint takes a URL/path the server can reach; a local file has none
0175 return typeof source === 'string' && matchesFileExtensions(source, this.meta);
0176 }
0177
0178 async loadEvents(source: DataSource, context: LoaderContext): Promise<DataExchange> {
0179 const eventRange = this.config.declare(ROOT_EVENT_RANGE_CONFIG).value || ROOT_EVENT_RANGE_CONFIG.default;
0180 const collections = parseListConfig(this.config.declare(ROOT_COLLECTIONS_CONFIG).value || '');
0181 const { DataModelService } = await import('@dexvis/firebird-ng/display');
0182 // The convert endpoint resolves the source on the server: no resolveUrl here
0183 return this.injector.get(DataModelService).fetchRootConversion(source as string, eventRange, collections, context.signal);
0184 }
0185 }
0186
0187 /** ROOT TGeo detector geometry, loaded via jsroot in the geometry worker. */
0188 @Injectable()
0189 export class RootGeometryLoader implements GeometryDataLoader {
0190 readonly meta: DataLoaderMeta = {
0191 id: 'root-geometry',
0192 label: 'ROOT TGeo geometry',
0193 fileExtensions: ['.root'],
0194 };
0195
0196 /** TGeo lengths are centimeters. */
0197 readonly millimetersPerUnit = 10;
0198
0199 private injector = inject(Injector);
0200
0201 canLoad(source: DataSource): boolean {
0202 return matchesFileExtensions(source, this.meta);
0203 }
0204
0205 /** A geometry file has a TGeoManager at the top level (usually 'Default'). */
0206 canLoadContent(probe: FileContentProbe): boolean {
0207 return probe.entries.some(entry => entry.className === 'TGeoManager');
0208 }
0209
0210 async load(source: DataSource, context: LoaderContext): Promise<Object3D> {
0211 const { GeometryService } = await import('@dexvis/firebird-ng/display');
0212 return this.injector.get(GeometryService).loadGeometry(source, { signal: context.signal });
0213 }
0214 }