Back to home page

EIC code displayed by LXR

 
 

    


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 }