Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/components/data-selector/root-event-picker.component.ts is written in an unsupported language. File is not indexed.

0001 import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, untracked } from '@angular/core';
0002 import { MatIcon } from '@angular/material/icon';
0003 import { MatProgressSpinner } from '@angular/material/progress-spinner';
0004 import type { DataSource } from '@dexvis/firebird-core';
0005 import { injectEventLoaders } from '@dexvis/firebird-ng/api';
0006 import { RootFileService } from '../../services/root-file.service';
0007 
0008 /** Checkbox captions of the conversion collection groups. */
0009 const GROUP_LABELS: Record<string, string> = {
0010   tracker_hits: 'Tracker hits',
0011   tracks: 'Tracks',
0012   mc_trajectories: 'MC hit trajectories',
0013   mc_particles: 'MC particles',
0014 };
0015 
0016 /**
0017  * Event numbers and collection groups to convert from a ROOT event source.
0018  * Opens the file in the converter worker to show its data model and event
0019  * count (only the header is read; nothing is uploaded), when a registered
0020  * loader with an event picker claims the source. The picker opens it under
0021  * its own worker handle, so it never disturbs a conversion the display runs.
0022  * When the source changes while a file is opening, only the latest source's
0023  * result is shown. Sources only pyrobird can
0024  * convert (`root://`) get the range and group inputs without the count.
0025  *
0026  * The choice is emitted as the `events.rootEventRange` and
0027  * `events.rootCollections` values ('' = all groups), the same knobs deep
0028  * links and `pyrobird convert --collections` use.
0029  */
0030 @Component({
0031   selector: 'firebird-root-event-picker',
0032   changeDetection: ChangeDetectionStrategy.OnPush,
0033   imports: [MatIcon, MatProgressSpinner],
0034   template: `
0035     @if (openedFile(); as file) {
0036       <div class="file-meta">
0037         <span class="badge">{{ file.model }}</span>
0038         <span>{{ file.entryCount }} events</span>
0039       </div>
0040     } @else if (busy()) {
0041       <div class="status busy"><mat-spinner diameter="18"></mat-spinner><span>Reading ROOT file header&hellip;</span></div>
0042     } @else if (error(); as message) {
0043       <div class="status error"><mat-icon>error_outline</mat-icon><span>{{ message }}</span></div>
0044     } @else if (!pickerAvailable()) {
0045       <div class="status note">Converted by the server when shown; the event count is not known in advance.</div>
0046     }
0047 
0048     <div class="event-row">
0049       <label for="root-event-range">Events</label>
0050       <input id="root-event-range"
0051              class="event-range-input"
0052              type="text"
0053              [value]="eventRange()"
0054              (input)="onRangeInput($any($event.target).value)"/>
0055       <span class="range-hint">single, list or range{{ maxEventHint() }} (e.g. <code>0,2,4-5</code>)</span>
0056     </div>
0057 
0058     <div class="collections-row">
0059       <span class="collections-label">Convert</span>
0060       @for (group of groups(); track group) {
0061         <label class="collection-checkbox">
0062           <input type="checkbox" [checked]="isGroupSelected(group)" (change)="toggleGroup(group)"/>
0063           <span>{{ groupLabel(group) }}</span>
0064         </label>
0065       }
0066     </div>
0067   `,
0068   styles: `
0069     @use './shared';
0070 
0071     :host {
0072       display: flex;
0073       flex-direction: column;
0074       gap: 8px;
0075       padding: 8px 10px;
0076       border-left: 2px solid var(--mat-sys-outline-variant, rgba(128, 128, 128, 0.3));
0077       font-size: 0.82rem;
0078     }
0079 
0080     .file-meta {
0081       display: flex;
0082       align-items: center;
0083       gap: 10px;
0084       font-size: 0.78rem;
0085       opacity: 0.85;
0086     }
0087 
0088     .badge {
0089       padding: 1px 8px;
0090       border-radius: 10px;
0091       background: color-mix(in srgb, #bec2ff 25%, transparent);
0092       font-family: monospace;
0093     }
0094 
0095     .event-row {
0096       display: flex;
0097       align-items: center;
0098       flex-wrap: wrap;
0099       gap: 8px;
0100     }
0101 
0102     .event-range-input {
0103       width: 7rem;
0104       @include shared.text-input;
0105     }
0106 
0107     .range-hint {
0108       opacity: 0.7;
0109       font-size: 0.72rem;
0110     }
0111 
0112     .collections-row {
0113       display: flex;
0114       align-items: center;
0115       flex-wrap: wrap;
0116       gap: 6px 12px;
0117     }
0118 
0119     .collections-label {
0120       opacity: 0.8;
0121     }
0122 
0123     .collection-checkbox {
0124       display: inline-flex;
0125       align-items: center;
0126       gap: 4px;
0127       cursor: pointer;
0128       white-space: nowrap;
0129 
0130       input {
0131         accent-color: var(--mat-sys-primary, #4bac84);
0132         cursor: pointer;
0133       }
0134     }
0135 
0136     .status {
0137       @include shared.status;
0138       align-items: center;
0139     }
0140   `,
0141 })
0142 export class RootEventPickerComponent {
0143   /** The ROOT event source (URL or picked file). */
0144   readonly source = input.required<DataSource>();
0145   /** Event numbers as typed: '0', '0,2,4-5'. */
0146   readonly eventRange = input('0');
0147   /** Comma list of collection groups to convert, '' = all. */
0148   readonly collections = input('');
0149   readonly eventRangeChange = output<string>();
0150   readonly collectionsChange = output<string>();
0151 
0152   private readonly rootFile = inject(RootFileService).createHandle();
0153   private readonly loaders = injectEventLoaders();
0154   /** Bumped per source; a result for an older source is not shown. */
0155   private openToken = 0;
0156 
0157   readonly openedFile = this.rootFile.openedFile;
0158   readonly busy = this.rootFile.busy;
0159   readonly error = signal<string | null>(null);
0160 
0161   /** True when a loader that can report the event count claims the source. */
0162   readonly pickerAvailable = computed(() => {
0163     const source = this.source();
0164     // untracked: a loader's canLoad() may declare a config key, which
0165     // applies pending layer values (signal writes) — forbidden inside computed()
0166     return untracked(() => this.loaders.find(loader => loader.canLoad(source))?.meta.offersEventPicker === true);
0167   });
0168 
0169   /** Groups of the open file; the known groups when the file could not be opened. */
0170   readonly groups = computed(() => this.openedFile()?.collectionGroups ?? Object.keys(GROUP_LABELS));
0171 
0172   readonly maxEventHint = computed(() => {
0173     const file = this.openedFile();
0174     return file ? ` within 0–${Math.max(0, file.entryCount - 1)}` : '';
0175   });
0176 
0177   constructor() {
0178     effect(() => {
0179       const source = this.source();
0180       const available = this.pickerAvailable();
0181       untracked(() => void this.openForCount(source, available));
0182     });
0183     inject(DestroyRef).onDestroy(() => this.rootFile.close());
0184   }
0185 
0186   private async openForCount(source: DataSource, available: boolean): Promise<void> {
0187     const token = ++this.openToken;
0188     this.error.set(null);
0189     if (!available) {
0190       this.rootFile.close();
0191       return;
0192     }
0193     try {
0194       await this.rootFile.open(source);
0195     } catch (error) {
0196       if (token === this.openToken) {
0197         this.error.set(error instanceof Error ? error.message : String(error));
0198       }
0199     }
0200   }
0201 
0202   onRangeInput(value: string): void {
0203     this.eventRangeChange.emit(value);
0204   }
0205 
0206   groupLabel(group: string): string {
0207     return GROUP_LABELS[group] ?? group;
0208   }
0209 
0210   private selectedGroups(): string[] {
0211     const configured = this.collections().split(',').map(group => group.trim()).filter(Boolean);
0212     return configured.length ? configured : this.groups();
0213   }
0214 
0215   isGroupSelected(group: string): boolean {
0216     return this.selectedGroups().includes(group);
0217   }
0218 
0219   toggleGroup(group: string): void {
0220     const selected = new Set(this.selectedGroups());
0221     if (selected.has(group)) {
0222       selected.delete(group);
0223     } else {
0224       selected.add(group);
0225     }
0226     // All groups selected collapses back to '' (= all), so groups added in
0227     // later versions stay included by default
0228     const all = this.groups();
0229     const value = all.every(candidate => selected.has(candidate)) ? '' : all.filter(candidate => selected.has(candidate)).join(',');
0230     this.collectionsChange.emit(value);
0231   }
0232 }