File

src/app/app.component.ts

Extends

CCFDatabaseOptions

Index

Properties

Properties

baseHref
baseHref: string
Type : string
Optional
filter
filter: Partial<Filter>
Type : Partial<Filter>
Optional
header
header: boolean
Type : boolean
Optional
homeUrl
homeUrl: string
Type : string
Optional
loginDisabled
loginDisabled: boolean
Type : boolean
Optional
loginEnabled
loginEnabled: boolean
Type : boolean
Optional
logoTooltip
logoTooltip: string
Type : string
Optional
selectedOrgans
selectedOrgans: string[]
Type : string[]
Optional
theme
theme: string
Type : string
Optional
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ElementRef,
  Injector,
  OnInit,
  ViewChild,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Dispatch } from '@ngxs-labs/dispatch-decorator';
import { Select } from '@ngxs/store';
import { CCFDatabaseOptions, Filter, OntologyTreeModel } from 'ccf-database';
import { BodyUiComponent, DataSourceService, GlobalConfigState, OrganInfo, TrackingPopupComponent } from 'ccf-shared';
import { ConsentService } from 'ccf-shared/analytics';
import { Observable, ReplaySubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

import { Immutable } from '@angular-ru/common/typings';
import { environment } from '../environments/environment';
import { OntologySelection } from './core/models/ontology-selection';
import { AppRootOverlayContainer } from './core/services/app-root-overlay/app-root-overlay.service';
import { ThemingService } from './core/services/theming/theming.service';
import { actionAsFn } from './core/store/action-as-fn';
import { DataStateSelectors } from './core/store/data/data.selectors';
import { DataState } from './core/store/data/data.state';
import { ListResultsState } from './core/store/list-results/list-results.state';
import { SceneState } from './core/store/scene/scene.state';
import { RemoveSearch, SetSelectedSearches } from './core/store/spatial-search-filter/spatial-search-filter.actions';
import { SpatialSearchFilterSelectors } from './core/store/spatial-search-filter/spatial-search-filter.selectors';
import { SpatialSearchFilterItem } from './core/store/spatial-search-filter/spatial-search-filter.state';
import { FiltersPopoverComponent } from './modules/filters/filters-popover/filters-popover.component';
import { DrawerComponent } from './shared/components/drawer/drawer/drawer.component';

interface AppOptions extends CCFDatabaseOptions {
  theme?: string;
  header?: boolean;
  homeUrl?: string;
  logoTooltip?: string;
  selectedOrgans?: string[];
  loginEnabled?: boolean;
  baseHref?: string;
  filter?: Partial<Filter>;
  loginDisabled?: boolean;
}

/**
 * This is the main angular component that all the other components branch off from.
 * It is in charge of the header and drawer components who have many sub-components.
 */
@Component({
  selector: 'ccf-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
  @ViewChild('bodyUI', { static: false }) bodyUI!: BodyUiComponent;

  @Select(DataStateSelectors.cellTypesTreeModel)
  readonly cellTypeTreeModel$!: Observable<OntologyTreeModel>;

  @Select(DataStateSelectors.anatomicalStructuresTreeModel)
  readonly ontologyTreeModel$!: Observable<OntologyTreeModel>;

  @Select(DataStateSelectors.biomarkersTreeModel)
  readonly biomarkersTreeModel$!: Observable<OntologyTreeModel>;

  @Select(SpatialSearchFilterSelectors.items)
  readonly selectableSearches$!: Observable<SpatialSearchFilterItem>;

  @Dispatch()
  readonly setSelectedSearches = actionAsFn(SetSelectedSearches);

  @Dispatch()
  readonly removeSpatialSearch = actionAsFn(RemoveSearch);

  menuOptions: string[] = ['AS', 'CT', 'B'];
  tooltips: string[] = ['Anatomical Structures', 'Cell Types', 'Biomarkers'];
  /**
   * Used to keep track of the ontology label to be passed down to the
   * results-browser component.
   */
  ontologySelectionLabel = 'body';

  cellTypeSelectionLabel = 'cell';

  biomarkerSelectionLabel = 'biomarker';

  selectionLabel = 'body | cell | biomarker';

  selectedtoggleOptions: string[] = [];

  /**
   * Whether or not organ carousel is open
   */
  organListVisible = true;

  /**
   * Emitted url object from the results browser item
   */
  url = '';

  /**
   * Acceptable viewer domains (others will open in new window)
   */
  acceptableViewerDomains: string[] = environment.acceptableViewerDomains || [];

  /**
   * Variable to keep track of whether the viewer is open
   * or not
   */
  viewerOpen = false;

  get isLightTheme(): boolean {
    return this.theming.getTheme().endsWith('light');
  }

  get isFirefox(): boolean {
    return navigator.userAgent.indexOf('Firefox') !== -1;
  }

  /** Emits true whenever the overlay spinner should activate. */
  readonly spinnerActive$ = this.data.state$.pipe(map((state) => state?.status !== 'Ready'));

  readonly loadingMessage$ = this.data.state$.pipe(map((x) => x?.statusMessage));

  readonly ontologyTerms$: Observable<readonly string[]>;
  readonly cellTypeTerms$: Observable<readonly string[]>;
  readonly biomarkerTerms$: Observable<readonly string[]>;

  readonly theme$ = this.globalConfig.getOption('theme');
  readonly themeMode$ = new ReplaySubject<'light' | 'dark'>(1);

  readonly header$ = this.globalConfig.getOption('header');
  readonly homeUrl$ = this.globalConfig.getOption('homeUrl');
  readonly logoTooltip$ = this.globalConfig.getOption('logoTooltip');
  readonly loginDisabled$ = this.globalConfig.getOption('loginDisabled');
  readonly filter$ = this.globalConfig.getOption('filter');
  readonly selectedOrgans$ = this.globalConfig.getOption('selectedOrgans');
  readonly baseHref$ = this.globalConfig.getOption('baseHref');

  /**
   * Creates an instance of app component.
   *
   * @param data The data state.
   */
  constructor(
    el: ElementRef<HTMLElement>,
    injector: Injector,
    readonly data: DataState,
    readonly theming: ThemingService,
    readonly scene: SceneState,
    readonly listResultsState: ListResultsState,
    readonly consentService: ConsentService,
    readonly snackbar: MatSnackBar,
    overlay: AppRootOverlayContainer,
    readonly dataSource: DataSourceService,
    private readonly globalConfig: GlobalConfigState<AppOptions>,
    cdr: ChangeDetectorRef,
  ) {
    theming.initialize(el, injector);
    overlay.setRootElement(el);
    data.tissueBlockData$.subscribe();
    data.aggregateData$.subscribe();
    data.ontologyTermOccurencesData$.subscribe();
    data.cellTypeTermOccurencesData$.subscribe();
    data.biomarkerTermOccurencesData$.subscribe();
    data.sceneData$.subscribe();
    data.filter$.subscribe();
    data.technologyFilterData$.subscribe();
    data.providerFilterData$.subscribe();
    this.ontologyTerms$ = data.filter$.pipe(map((x) => x?.ontologyTerms));
    this.cellTypeTerms$ = data.filter$.pipe(map((x) => x?.cellTypeTerms));
    this.biomarkerTerms$ = data.filter$.pipe(map((x) => x?.biomarkerTerms));
    this.filter$.subscribe((filter = {}) => data.updateFilter(filter));
    this.baseHref$.subscribe((ref) => this.globalConfig.patchState({ baseHref: ref ?? '' }));

    combineLatest([scene.referenceOrgans$, this.selectedOrgans$]).subscribe(([refOrgans, selected]) => {
      scene.setSelectedReferenceOrgansWithDefaults(refOrgans as OrganInfo[], selected ?? []);
    });
    combineLatest([this.theme$, this.themeMode$]).subscribe(([theme, mode]) => {
      this.theming.setTheme(`${theme}-theme-${mode}`);
      cdr.markForCheck();
    });
    this.selectedtoggleOptions = this.menuOptions;
  }

  ngOnInit(): void {
    const snackBar = this.snackbar.openFromComponent(TrackingPopupComponent, {
      data: {
        preClose: () => {
          snackBar.dismiss();
        },
      },
      duration: this.consentService.consent === 'not-set' ? Infinity : 3000,
    });

    if (window.matchMedia) {
      // Sets initial theme according to user theme preference
      if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
        this.themeMode$.next('dark');
      } else {
        this.themeMode$.next('light');
      }

      // Listens for changes in user theme preference
      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
        this.themeMode$.next(e.matches ? 'dark' : 'light');
      });
    } else {
      this.themeMode$.next('light');
    }
  }

  /**
   * Resets the drawers and filter components to their default state.
   *
   * @param left The left drawer component gets passed in so we can call it's methods to control it's state
   * @param right The right drawer component gets passed in so we can call it's methods to control it's state
   * @param filterbox The filter's popover component gets passed in so we can control it's popover's state
   */
  reset(left: DrawerComponent, right: DrawerComponent, filterbox: FiltersPopoverComponent): void {
    left.open();
    left.closeExpanded();
    right.open();
    right.closeExpanded();
    filterbox.removeBox();
    this.resetView();
  }

  resetView(): void {
    this.bodyUI.target = [0, 0, 0];
    this.bodyUI.rotation = 0;
    this.bodyUI.rotationX = 0;
    this.bodyUI.bounds = { x: 2.2, y: 2, z: 0.4 };
  }

  /**
   * Toggles scheme between light and dark mode
   */
  toggleScheme(): void {
    this.themeMode$.next(this.isLightTheme ? 'dark' : 'light');
  }

  /**
   * Captures changes in the ontologySelection and uses them to update the results-browser label
   * and the filter object in the data store.
   *
   * @param ontologySelection the list of currently selected organ nodes
   */
  ontologySelected(
    ontologySelection: OntologySelection[] | undefined,
    type: 'anatomical-structures' | 'cell-type' | 'biomarkers',
  ): void {
    if (ontologySelection) {
      if (type === 'anatomical-structures') {
        this.data.updateFilter({ ontologyTerms: ontologySelection.map((selection) => selection.id) });
        this.ontologySelectionLabel = this.createSelectionLabel(ontologySelection);
      } else if (type === 'cell-type') {
        this.data.updateFilter({ cellTypeTerms: ontologySelection.map((selection) => selection.id) });
        this.cellTypeSelectionLabel = this.createSelectionLabel(ontologySelection);
      } else if (type === 'biomarkers') {
        this.data.updateFilter({ biomarkerTerms: ontologySelection.map((selection) => selection.id) });
        this.biomarkerSelectionLabel = this.createSelectionLabel(ontologySelection);
      }

      this.selectionLabel = [
        this.ontologySelectionLabel || 'body',
        this.cellTypeSelectionLabel || 'cell',
        this.biomarkerSelectionLabel || 'biomarker',
      ].join(' | ');

      if (ontologySelection[0] && ontologySelection[0].label === 'body') {
        this.resetView();
      }
      return;
    }

    this.data.updateFilter({ ontologyTerms: [], cellTypeTerms: [], biomarkerTerms: [] });
    this.ontologySelectionLabel = '';
    this.cellTypeSelectionLabel = '';
  }

  /**
   * Creates selection label for the results-browser to display based on an
   * array of selected ontology nodes.
   */
  createSelectionLabel(ontologySelection: OntologySelection[]): string {
    if (ontologySelection.length === 0) {
      return '';
    }

    if (ontologySelection.length === 1) {
      return ontologySelection[0].label;
    }

    let selectionString = '';
    ontologySelection.forEach((selection, index) => {
      selectionString += selection.label;

      // Don't add a comma if it's the last item in the array.
      if (index < ontologySelection.length - 1) {
        selectionString += ', ';
      }
    });
    return selectionString;
  }

  /**
   * Opens the iframe viewer with an url
   *
   * @param url The url
   */
  openiFrameViewer(url: string): void {
    const isWhitelisted = this.acceptableViewerDomains.some((domain) => url?.startsWith(domain));
    if (isWhitelisted) {
      this.url = url;
      this.viewerOpen = !!url;
    } else {
      // Open link in new tab
      window.open(url, '_blank');
      this.closeiFrameViewer();
    }
  }

  /**
   * Function to easily close the iFrame viewer.
   */
  closeiFrameViewer(): void {
    this.viewerOpen = false;
  }

  /**
   * Gets login token
   */
  get loggedIn(): boolean {
    const token = this.globalConfig.snapshot.hubmapToken ?? '';
    return token.length > 0;
  }

  isItemSelected(item: string) {
    return this.selectedtoggleOptions.includes(item);
  }

  toggleSelection(value: string[]) {
    this.selectedtoggleOptions = value;
  }

  asMutable<T>(value: Immutable<T>): T {
    return value as T;
  }
}

results matching ""

    No results matching ""