Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/services/data-model.service.ts is written in an unsupported language. File is not indexed.

0001 import {Injectable, linkedSignal, signal} from "@angular/core";
0002 import { Event, DataExchange } from "@dexvis/firebird-core";
0003 import { HttpClient } from "@angular/common/http";
0004 import { UrlService } from "@dexvis/firebird-ng/api";
0005 import { fetchTextFile, loadJSONFileEvents, loadZipFileEvents, readDexFile } from "../utils/data-fetching.utils";
0006 
0007 /**
0008  * Service for loading and managing event/entry data in Firebird.
0009  *
0010  * This service fetches ROOT events converted by the server and Firebird DEX
0011  * data from JSON or ZIP, and stores the list of entries and the currently
0012  * selected entry as Angular signals.
0013  *
0014  * Fetching and parsing never change the stored entries: `adoptEvents()` does,
0015  * and EventDisplayService calls it only for the latest requested load, so a
0016  * slow earlier load cannot replace a newer one.
0017  */
0018 @Injectable({
0019   providedIn: 'root'
0020 })
0021 export class DataModelService {
0022 
0023   /**
0024    * Signal holding the list of loaded entries (events).
0025    * Each Entry corresponds to one event's data in Firebird Dex format.
0026    */
0027   public entries = signal<Event[]>([]);
0028 
0029   /**
0030    * Signal holding the currently selected entry (event).
0031    */
0032   public currentEntry = linkedSignal(() => {
0033     if(this.entries().length > 0 ) {
0034       return this.entries()[0]
0035     }
0036     return null;
0037   });
0038 
0039   /**
0040    * Constructor that injects services needed for resolving URLs and making HTTP requests.
0041    * @param urlService - Service used for building/transforming URLs
0042    * @param http - Angular HttpClient for making network requests (currently not used directly here)
0043    */
0044   constructor(
0045     private urlService: UrlService,
0046     private http: HttpClient
0047   ) {}
0048 
0049   /**
0050    * Checks if an unknown object is valid "Firebird DEX" format by
0051    * verifying if it has a `"type": "firebird-dex-json"` property.
0052    *
0053    * @param obj - The object to inspect.
0054    * @returns True if the object has a `"type"` property equal to `"firebird-dex-json"`, otherwise false.
0055    */
0056   public isFirebirdDex(obj: unknown): boolean {
0057     return (
0058       typeof obj === "object" &&
0059       obj !== null &&
0060       "type" in obj &&
0061       (obj as any)["type"] === "firebird-dex-json"
0062     );
0063   }
0064 
0065   /**
0066    * Converts ROOT events through the pyrobird convert endpoint and parses the
0067    * result. The server detects the data model (EDM4eic or EDM4hep) itself.
0068    * Does not change the loaded entries; see `adoptEvents()`.
0069    *
0070    * @param url - Location of the ROOT file: a `root://` URL, an http(s) URL,
0071    *   or a path the server serves.
0072    * @param entryNames - Entry numbers as typed: '0', '0-4', '1,3'. The server
0073    *   converts the entries the file holds and rejects a range with none.
0074    * @param collections - Collection groups to convert (server `collections`
0075    *   query parameter, same names as `pyrobird convert --collections`).
0076    *   Empty means all.
0077    * @param signal - Aborts the request.
0078    * @returns The parsed DEX document.
0079    * @throws Error with the reason: no backend, HTTP status and server message,
0080    *   not DEX, unsupported DEX version.
0081    */
0082   async fetchRootConversion(url: string, entryNames: string = "0", collections?: string[], signal?: AbortSignal): Promise<DataExchange> {
0083     const finalUrl = this.urlService.resolveConvertUrl(url, "auto", entryNames, collections);
0084     console.log(`[DataModelService.fetchRootConversion] Fetching: ${finalUrl}`);
0085     const text = await fetchTextFile(finalUrl, signal);
0086     let dexData: unknown;
0087     try {
0088       dexData = JSON.parse(text);
0089     } catch (error) {
0090       throw new Error(`The server's conversion of '${url}' is not JSON: ${error instanceof Error ? error.message : error}`);
0091     }
0092     return this.parseDex(dexData, url);
0093   }
0094 
0095   /**
0096    * Fetches a Firebird DEX JSON or ZIP file and parses it. Does not change
0097    * the loaded entries; see `adoptEvents()`.
0098    *
0099    * @param url - A URL `fetch()` reads: resolve aliases and server paths
0100    *   first (`LoaderContext.resolveUrl`).
0101    * @param options.name - The source as the user named it, for messages.
0102    * @param options.signal - Aborts the download.
0103    * @returns The parsed DEX document.
0104    * @throws Error with the reason: HTTP status, unreadable zip, not DEX,
0105    *   unsupported DEX version (with the upgrade command).
0106    */
0107   async fetchDex(url: string, options: { name?: string; signal?: AbortSignal } = {}): Promise<DataExchange> {
0108     console.log(`[DataModelService.fetchDex] Loading: ${url}`);
0109     // The name decides the format: a resolved download URL carries the file
0110     // name in its query (api/v1/download?f=run.firebird.zip)
0111     const name = options.name ?? url;
0112     const dexData = name.endsWith("zip") || url.endsWith("zip")
0113       ? await loadZipFileEvents(url, options.signal)
0114       : await loadJSONFileEvents(url, options.signal);
0115     return this.parseDex(dexData, name);
0116   }
0117 
0118   /**
0119    * Reads a DEX JSON or ZIP file the user picked or dropped, in place (never
0120    * uploaded), and parses it.
0121    *
0122    * @throws Error with the reason: unreadable zip, not JSON, not DEX,
0123    *   unsupported DEX version.
0124    */
0125   async readDexFile(file: File): Promise<DataExchange> {
0126     return this.parseDex(await readDexFile(file), file.name);
0127   }
0128 
0129   /**
0130    * Parses a DEX document that is already in memory.
0131    *
0132    * @param dexData - The parsed JSON object.
0133    * @param sourceName - File name or URL, for the error message.
0134    * @throws Error when the object is not a Firebird DEX document or has an
0135    *   unsupported version.
0136    */
0137   parseDex(dexData: unknown, sourceName = 'the document'): DataExchange {
0138     if (!this.isFirebirdDex(dexData)) {
0139       throw new Error(`'${sourceName}' is not a Firebird DEX document (it lacks "type": "firebird-dex-json")`);
0140     }
0141     return DataExchange.fromDexObj(dexData);
0142   }
0143 
0144   /** Publishes loaded events to the signals and selects the first one. */
0145   adoptEvents(data: DataExchange): void {
0146     this.entries.set(data.events);
0147     if (this.entries().length > 0) {
0148       // Explicitly the first entry, not setNextEntry(): `currentEntry` is a
0149       // linkedSignal that has already recomputed to entries[0], so "next"
0150       // would step to entries[1]. With a single event that wrapped back around
0151       // and looked right; loading several events showed the second one while
0152       // the painter drew the first.
0153       this.setCurrentEntry(this.entries()[0]);
0154     }
0155   }
0156 
0157   /**
0158    * Parses and adopts a DEX document that is already in memory.
0159    *
0160    * @param dexData - A parsed Firebird DEX document.
0161    * @returns The DataExchange, or null when the object is not DEX.
0162    */
0163   loadDexObject(dexData: unknown): DataExchange | null {
0164     if (!this.isFirebirdDex(dexData)) {
0165       console.error("[DataModelService.loadDexObject] The object does not conform to Firebird DEX JSON format.");
0166       return null;
0167     }
0168     const data = this.parseDex(dexData);
0169     this.adoptEvents(data);
0170     return data;
0171   }
0172 
0173   /**
0174    * Sets the currently selected entry (event) to the provided `Entry`.
0175    *
0176    * @param entry - The Entry object to be marked as current.
0177    */
0178   setCurrentEntry(entry: Event): void {
0179     console.log(`[DataModelService.setCurrentEntry] Setting event: ${entry.id}`);
0180     this.currentEntry.set(entry);
0181   }
0182 
0183   /**
0184    * Finds and sets the current entry by its name (i.e. `entry.id`).
0185    *
0186    * @param name - The string name or ID of the entry to set as current.
0187    */
0188   setCurrentEntryByName(name: string): void {
0189     // Look up the first Entry whose 'id' matches the provided name
0190     const found = this.entries().find((entry) => entry.id === name);
0191 
0192     // If found, update currentEntry; otherwise log a warning.
0193     if (found) {
0194       this.setCurrentEntry(found);
0195     } else {
0196       console.warn(`[DataModelService] setCurrentEntryByName: Entry with id='${name}' not found.`);
0197     }
0198   }
0199 
0200   setNextEntry() {
0201     const allEntries = this.entries();
0202     const current = this.currentEntry();
0203 
0204     // If no entries available, return
0205     if (allEntries.length === 0) {
0206       console.warn('[DataModelService.setNextEntry] No entries available');
0207       return;
0208     }
0209 
0210     // If no current entry, set the first one
0211     if (!current) {
0212       this.setCurrentEntry(allEntries[0]);
0213       return;
0214     }
0215 
0216     // Find the current entry index
0217     const currentIndex = allEntries.findIndex(entry => entry === current);
0218 
0219     // If current entry not found in the list (shouldn't happen), set first entry
0220     if (currentIndex === -1) {
0221       console.warn('[DataModelService.setNextEntry] Current entry not found in entries list');
0222       this.setCurrentEntry(allEntries[0]);
0223       return;
0224     }
0225 
0226     // Calculate the next index with wrapping
0227     const nextIndex = (currentIndex + 1) % allEntries.length;
0228 
0229     // Set the next entry
0230     this.setCurrentEntry(allEntries[nextIndex]);
0231     console.log(`[DataModelService.setNextEntry] Switching from entry ${currentIndex} to ${nextIndex} (${allEntries[nextIndex].id})`);
0232   }
0233 }