Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-core/src/loaders.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * Loader contracts for the extension system.
0003  *
0004  * A loader teaches Firebird to open a file format or URL scheme. Loaders are
0005  * core citizens (plain TS, worker-safe interfaces); the Angular layer collects
0006  * implementations through the `GEOMETRY_LOADERS` / `EVENT_LOADERS` DI tokens
0007  * (`withGeometryLoader()` / `withEventLoader()`).
0008  *
0009  * A loader returns data and never touches the display: the display picks the
0010  * first registered loader whose `canLoad()` claims a source, hands it a
0011  * `LoaderContext`, and puts what the loader returns on screen. Failures are
0012  * rejections that carry the reason (HTTP status, unsupported version, ...):
0013  * the display shows that message to the user.
0014  *
0015  * `meta.fileExtensions` and `meta.urlSchemes` are static declarations that
0016  * drive UI (open-dialog filters, drop zones) without the loader writing UI code.
0017  *
0018  * Two-stage claiming: `canLoad()` decides from the source alone (extension,
0019  * scheme) and is enough for formats whose name says what they are. When the
0020  * name is ambiguous - a `.root` file holds detector geometry OR events, and
0021  * nothing outside tells you which - the control that opened the file probes
0022  * its contents and asks `canLoadContent()`. Each loader keeps the knowledge of
0023  * what its own format looks like inside; the control only carries facts around.
0024  */
0025 
0026 import type { Object3D } from "three";
0027 import type { DataExchange } from "./model/data-exchange";
0028 
0029 /**
0030  * What a loader can be pointed at: a URL or path, or a file the user picked or
0031  * dropped (which is never uploaded - loaders read it in place).
0032  */
0033 export type DataSource = string | File;
0034 
0035 /** Static, declarative description of what a loader can open. */
0036 export interface DataLoaderMeta {
0037   /** Unique id, e.g. 'root-geometry' or 'firebird-dex'. */
0038   id: string;
0039   /** Human-readable label for menus and error messages. */
0040   label: string;
0041   /** Extensions this loader claims, with dots: ['.root', '.firebird.json']. */
0042   fileExtensions: string[];
0043   /**
0044    * Optional URL schemes this loader claims, e.g. ['root://']. A scheme
0045    * claims only a source whose file name has no extension or one listed in
0046    * `fileExtensions`: `root://host//run.root` and `root://host//run` match
0047    * ['root://'], `root://host//run.firebird.zip` does not. Another loader's
0048    * format therefore never lands here because of its scheme.
0049    */
0050   urlSchemes?: string[];
0051   /**
0052    * True when the loader's format supports the interactive open flow: report
0053    * the event count first, let the user pick a range, convert on demand (the
0054    * ROOT converter does this). Loaders without it load the whole source in
0055    * one `loadEvents` call — the open-event panel shows the result directly
0056    * instead of offering the picker.
0057    */
0058   offersEventPicker?: boolean;
0059 }
0060 
0061 /** One top-level object inside a container file, with the class it holds. */
0062 export interface ContentEntry {
0063   name: string;
0064   className: string;
0065 }
0066 
0067 /**
0068  * Neutral facts about what a container file holds - no interpretation. For a
0069  * ROOT file these are its top-level keys, so a detector geometry shows up as
0070  * `{name: 'Default', className: 'TGeoManager'}` and event data as
0071  * `{name: 'events', className: 'TTree'}`.
0072  */
0073 export interface FileContentProbe {
0074   /** File name or URL the probe was taken from. */
0075   name: string;
0076   entries: ContentEntry[];
0077 }
0078 
0079 /** What the display hands a loader with each load. */
0080 export interface LoaderContext {
0081   /**
0082    * Turns a URL alias (`asset://`, a pack's alias such as `exp://`) or a path
0083    * the server serves into a URL `fetch()` can read. Absolute http(s) URLs
0084    * come back unchanged.
0085    */
0086   resolveUrl(url: string): string;
0087   /**
0088    * Aborted when a newer load of the same kind (geometry, or events)
0089    * replaces this one. Pass it to `fetch()`, stop work when it fires, and
0090    * reject with `signal.reason`; the display drops the result either way.
0091    */
0092   readonly signal: AbortSignal;
0093 }
0094 
0095 /** What every loader declares, whatever it loads. */
0096 interface DataLoaderBase {
0097   readonly meta: DataLoaderMeta;
0098   /** Claims a source by its name alone (extension, scheme). */
0099   canLoad(source: DataSource): boolean;
0100   /**
0101    * Optional: claims a source by what is INSIDE it. Asked only when the name
0102    * was ambiguous. A loader that does not implement it is never selected by
0103    * content.
0104    */
0105   canLoadContent?(probe: FileContentProbe): boolean;
0106 }
0107 
0108 /**
0109  * Opens detector geometry from a URL, path, or local file.
0110  * The first registered loader whose `canLoad()` returns true loads it.
0111  */
0112 export interface GeometryDataLoader extends DataLoaderBase {
0113   /**
0114    * Millimeters per length unit of the geometry `load()` returns: 10 for
0115    * ROOT TGeo (centimeters). The display scales the geometry container by
0116    * it, so detector and event data (millimeters) line up. Default 1.
0117    */
0118   readonly millimetersPerUnit?: number;
0119   /**
0120    * Loads the geometry and resolves to its root object; the display adds it
0121    * to the scene, replacing the previous geometry. Rejects with the reason
0122    * when the source cannot be loaded.
0123    */
0124   load(source: DataSource, context: LoaderContext): Promise<Object3D>;
0125 }
0126 
0127 /**
0128  * Opens event data from a URL, path, or local file.
0129  * The first registered loader whose `canLoad()` returns true loads it.
0130  */
0131 export interface EventDataLoader extends DataLoaderBase {
0132   /**
0133    * Loads the events and resolves to the parsed DEX container
0134    * (`DataExchange.fromDexObj()`); the display shows its first event.
0135    * Rejects with the reason (HTTP status, unsupported DEX version, entries
0136    * outside the file) when the source cannot be loaded.
0137    */
0138   loadEvents(source: DataSource, context: LoaderContext): Promise<DataExchange>;
0139 }
0140 
0141 /** The name to match against: the URL itself, or a picked file's name. */
0142 export function sourceName(source: DataSource): string {
0143   return typeof source === 'string' ? source : source.name;
0144 }
0145 
0146 /**
0147  * True when the last path segment of a URL or path carries a file extension.
0148  * The host of a URL is not a path segment: `root://host.org` has none.
0149  */
0150 function hasFileExtension(path: string): boolean {
0151   const schemeEnd = path.indexOf('://');
0152   const afterScheme = schemeEnd >= 0 ? path.slice(schemeEnd + 3) : path;
0153   const pathStart = schemeEnd >= 0 ? afterScheme.indexOf('/') : 0;
0154   if (pathStart < 0) return false;
0155   const lastSegment = afterScheme.slice(pathStart).split('/').pop() ?? '';
0156   return lastSegment.lastIndexOf('.') > 0;
0157 }
0158 
0159 /**
0160  * Shared helper: does the loader claim the source by its name? A source
0161  * matches when its path ends with one of `meta.fileExtensions`, when it
0162  * starts with one of `meta.urlSchemes` and has no file extension, or when a
0163  * query parameter value (download-style URLs) ends with one of the
0164  * extensions.
0165  */
0166 export function matchesFileExtensions(source: DataSource, meta: DataLoaderMeta): boolean {
0167   const name = sourceName(source);
0168   const matchesPath = (path: string) =>
0169     meta.fileExtensions.some(ext => path.toLowerCase().endsWith(ext.toLowerCase()));
0170 
0171   const [path, query] = name.split('?');
0172   if (matchesPath(path)) return true;
0173   // A scheme never overrides an extension: 'asset://data/x.root' belongs to
0174   // a .root loader even when another loader claims every asset:// URL
0175   const matchesScheme = (meta.urlSchemes ?? []).some(scheme => path.toLowerCase().startsWith(scheme.toLowerCase()));
0176   if (matchesScheme && !hasFileExtension(path)) {
0177     return true;
0178   }
0179 
0180   // Download-style URLs carry the file name in a query parameter
0181   // (pyrobird: /api/v1/download?f=events.firebird.zip) — match those values too
0182   if (query) {
0183     for (const pair of query.split('&')) {
0184       const value = pair.split('=')[1];
0185       if (!value) continue;
0186       try {
0187         if (matchesPath(decodeURIComponent(value))) return true;
0188       } catch {
0189         // Malformed percent-encoding: not a file name worth matching
0190       }
0191     }
0192   }
0193   return false;
0194 }