Back to home page

EIC code displayed by LXR

 
 

    


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

0001 import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core';
0002 import { DataCatalogEntry, DataCatalogFacet, facetValues, matchEntries } from '@dexvis/firebird-core';
0003 import { DataCatalogService, DataSelectionService } from '@dexvis/firebird-ng/api';
0004 
0005 interface FacetChip {
0006   value: string;
0007   label: string;
0008   selected: boolean;
0009   /** False when no entry has this value together with the other selected values. Still clickable. */
0010   available: boolean;
0011 }
0012 
0013 interface FacetRow {
0014   facet: DataCatalogFacet;
0015   chips: FacetChip[];
0016 }
0017 
0018 /**
0019  * Physics tab: choose a dataset by its tags — one chip per facet value
0020  * (process, beam, ...). Chips that lead to no dataset with the other choices
0021  * are dimmed but stay clickable; a selection that matches nothing says so.
0022  * Several matches list under the chips, the first one pre-chosen.
0023  */
0024 @Component({
0025   selector: 'firebird-physics-tab',
0026   changeDetection: ChangeDetectionStrategy.OnPush,
0027   imports: [],
0028   template: `
0029     <div class="tab-block">
0030       @for (row of rows(); track row.facet.key) {
0031         <div class="facet">
0032           <span class="facet-label">{{ row.facet.label }}</span>
0033           <div class="chip-row">
0034             @for (chip of row.chips; track chip.value) {
0035               <button type="button"
0036                       class="chip"
0037                       [class.selected]="chip.selected"
0038                       [class.unavailable]="!chip.available"
0039                       (click)="toggle(row.facet.key, chip.value)">
0040                 {{ chip.label }}
0041               </button>
0042             }
0043           </div>
0044         </div>
0045       }
0046 
0047       @if (infos().length) {
0048         <div class="description">
0049           @for (info of infos(); track info.key) {
0050             <p>
0051               <strong>{{ info.label }}</strong> {{ info.description }}
0052               @if (info.link) {
0053                 <a [href]="info.link" target="_blank" rel="noopener">More</a>
0054               }
0055             </p>
0056           }
0057         </div>
0058       }
0059 
0060       @if (nothingSelected()) {
0061         <div class="hint">Choose a process or a beam to see the datasets.</div>
0062       } @else if (matches().length === 0) {
0063         <div class="hint">No dataset has this combination. Change a choice, or open the Manual tab to load your own file.</div>
0064       } @else {
0065         <div class="matches">
0066           @for (entry of matches(); track entry.name) {
0067             <label class="match" [class.chosen]="entry === chosen()">
0068               <input type="radio" name="physics-match" [checked]="entry === chosen()" (change)="choose(entry)"/>
0069               <span class="match-name">{{ entry.name }}</span>
0070             </label>
0071           }
0072         </div>
0073         @if (chosen()?.description; as description) {
0074           <div class="description">{{ description }}</div>
0075         }
0076       }
0077     </div>
0078   `,
0079   styles: `
0080     @use './shared';
0081 
0082     .tab-block {
0083       @include shared.tab-block;
0084     }
0085 
0086     .facet {
0087       display: flex;
0088       flex-direction: column;
0089       gap: 4px;
0090     }
0091 
0092     .facet-label {
0093       @include shared.section-label;
0094     }
0095 
0096     .chip-row {
0097       @include shared.chip-row;
0098     }
0099 
0100     .chip {
0101       @include shared.chip;
0102     }
0103 
0104     .description {
0105       @include shared.description;
0106 
0107       p {
0108         margin: 0 0 4px;
0109       }
0110     }
0111 
0112     .hint {
0113       font-size: 0.8rem;
0114       opacity: 0.8;
0115     }
0116 
0117     .matches {
0118       display: flex;
0119       flex-direction: column;
0120       gap: 4px;
0121     }
0122 
0123     .match {
0124       display: flex;
0125       align-items: center;
0126       gap: 6px;
0127       cursor: pointer;
0128       font-size: 0.82rem;
0129 
0130       input {
0131         accent-color: var(--mat-sys-primary, #4bac84);
0132       }
0133 
0134       &.chosen {
0135         font-weight: 500;
0136       }
0137     }
0138   `,
0139 })
0140 export class PhysicsTabComponent {
0141   private readonly catalog = inject(DataCatalogService);
0142   private readonly selection = inject(DataSelectionService);
0143 
0144   /** Only tagged entries take part; untagged presets are for the Presets tab. */
0145   readonly entries = computed(() => this.catalog.entries().filter(entry => entry.tags && Object.keys(entry.tags).length > 0));
0146 
0147   /** Starts from the tags of the loaded preset, when there is one. */
0148   readonly selectedTags = signal<Record<string, string>>({ ...(this.selection.activeEntry()?.tags ?? {}) });
0149   readonly nothingSelected = computed(() => Object.keys(this.selectedTags()).length === 0);
0150 
0151   readonly rows = computed<FacetRow[]>(() => {
0152     const entries = this.entries();
0153     const selected = this.selectedTags();
0154     return this.catalog.facets()
0155       .map(facet => ({
0156         facet,
0157         chips: facetValues(entries, facet).map(value => ({
0158           value,
0159           label: facet.values?.[value]?.label ?? value,
0160           selected: selected[facet.key] === value,
0161           available: matchEntries(entries, { ...selected, [facet.key]: value }).length > 0,
0162         })),
0163       }))
0164       .filter(row => row.chips.length > 0);
0165   });
0166 
0167   /** Descriptions of the selected values that have one. */
0168   readonly infos = computed(() => {
0169     const selected = this.selectedTags();
0170     return this.catalog.facets().flatMap(facet => {
0171       const value = selected[facet.key];
0172       const info = value !== undefined ? facet.values?.[value] : undefined;
0173       if (!info?.description) return [];
0174       return [{ key: facet.key, label: info.label ?? value, description: info.description, link: info.link }];
0175     });
0176   });
0177 
0178   readonly matches = computed<DataCatalogEntry[]>(() =>
0179     this.nothingSelected() ? [] : matchEntries(this.entries(), this.selectedTags()));
0180 
0181   readonly chosen = signal<DataCatalogEntry | null>(null);
0182 
0183   constructor() {
0184     // The first match is pre-chosen whenever the match list changes
0185     effect(() => {
0186       const matches = this.matches();
0187       untracked(() => this.choose(matches[0] ?? null));
0188     });
0189   }
0190 
0191   toggle(key: string, value: string): void {
0192     this.selectedTags.update(selected => {
0193       const next = { ...selected };
0194       if (next[key] === value) {
0195         delete next[key];
0196       } else {
0197         next[key] = value;
0198       }
0199       return next;
0200     });
0201   }
0202 
0203   choose(entry: DataCatalogEntry | null): void {
0204     this.chosen.set(entry);
0205     if (entry) {
0206       this.selection.updateDraft(this.selection.selectionForEntry(entry));
0207     } else {
0208       this.selection.clearDraft();
0209     }
0210   }
0211 }