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 }