File

src/app/app.component.ts

Index

Properties

Properties

asctbUrl
asctbUrl: string
Type : string
Optional
donorLabel
donorLabel: string
Type : string
Optional
euiUrl
euiUrl: string
Type : string
Optional
highlightProviders
highlightProviders: string[]
Type : string[]
Optional
hraPortalUrl
hraPortalUrl: string
Type : string
Optional
onlineCourseUrl
onlineCourseUrl: string
Type : string
Optional
organIri
organIri: string
Type : string
Optional
paperUrl
paperUrl: string
Type : string
Optional
ruiUrl
ruiUrl: string
Type : string
Optional
sex
sex: "Both" | "Male" | "Female"
Type : "Both" | "Male" | "Female"
Optional
side
side: string
Type : string
Optional
import { Immutable } from '@angular-ru/common/typings';
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  Output,
  ViewChild,
} from '@angular/core';
import { SpatialSceneNode } from 'ccf-body-ui';
import { AggregateResult, SpatialEntity, TissueBlockResult } from 'ccf-database';
import { GlobalConfigState, OrganInfo } from 'ccf-shared';
import { GoogleAnalyticsService } from 'ngx-google-analytics';
import { Observable, combineLatest, of } from 'rxjs';
import { map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';

import { OrganLookupService } from './core/services/organ-lookup/organ-lookup.service';

interface GlobalConfig {
  organIri?: string;
  side?: string;
  sex?: 'Both' | 'Male' | 'Female';
  highlightProviders?: string[];
  donorLabel?: string;
  ruiUrl?: string;
  euiUrl?: string;
  asctbUrl?: string;
  hraPortalUrl?: string;
  onlineCourseUrl?: string;
  paperUrl?: string;
}

const EMPTY_SCENE = [{ color: [0, 0, 0, 0], opacity: 0.001 }];

@Component({
  selector: 'ccf-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements AfterViewInit {
  @ViewChild('left', { read: ElementRef, static: true }) left!: ElementRef<HTMLElement>;
  @ViewChild('right', { read: ElementRef, static: true }) right!: ElementRef<HTMLElement>;

  @Output() readonly sexChange = new EventEmitter<'Male' | 'Female'>();
  @Output() readonly sideChange = new EventEmitter<'Left' | 'Right'>();
  @Output() readonly nodeClicked = new EventEmitter();
  readonly sex$ = this.configState.getOption('sex');
  readonly side$ = this.configState.getOption('side');
  readonly filter$ = this.configState
    .getOption('highlightProviders')
    .pipe(map((providers) => ({ tmc: providers ?? [] })));
  readonly donorLabel$ = this.configState.getOption('donorLabel');
  readonly ruiUrl$ = this.configState.getOption('ruiUrl');
  readonly euiUrl$ = this.configState.getOption('euiUrl');
  readonly asctbUrl$ = this.configState.getOption('asctbUrl');
  readonly hraPortalUrl$ = this.configState.getOption('hraPortalUrl');
  readonly onlineCourseUrl$ = this.configState.getOption('onlineCourseUrl');
  readonly paperUrl$ = this.configState.getOption('paperUrl');

  readonly organInfo$: Observable<OrganInfo | undefined>;
  readonly organ$: Observable<SpatialEntity | undefined>;
  readonly scene$: Observable<SpatialSceneNode[]>;
  readonly stats$: Observable<AggregateResult[]>;
  readonly statsLabel$: Observable<string>;
  readonly blocks$: Observable<TissueBlockResult[]>;

  stats: AggregateResult[] = [];

  private latestConfig: Immutable<GlobalConfig> = {};
  private latestOrganInfo?: OrganInfo;

  constructor(
    lookup: OrganLookupService,
    private readonly ga: GoogleAnalyticsService,
    private readonly configState: GlobalConfigState<GlobalConfig>,
  ) {
    this.organInfo$ = configState.config$.pipe(
      tap((config) => (this.latestConfig = config)),
      switchMap((config) =>
        lookup.getOrganInfo(config.organIri ?? '', config.side?.toLowerCase?.() as OrganInfo['side'], config.sex),
      ),
      tap((info) => this.logOrganLookup(info)),
      tap((info) => (this.latestOrganInfo = info)),
      shareReplay(1),
    );

    this.organ$ = this.organInfo$.pipe(
      switchMap((info) =>
        info ? lookup.getOrgan(info, info.hasSex ? this.latestConfig.sex : undefined) : of(undefined),
      ),
      tap((organ) => {
        if (organ && this.latestOrganInfo) {
          const newSex = this.latestOrganInfo?.hasSex ? organ.sex : undefined;
          if (newSex !== this.latestConfig.sex) {
            this.updateInput('sex', newSex);
          }
          if (organ.side !== this.latestConfig.side) {
            this.updateInput('side', organ.side);
          }
        }
      }),
      shareReplay(1),
    );

    this.scene$ = this.organ$.pipe(
      switchMap((organ) =>
        organ && this.latestOrganInfo
          ? lookup.getOrganScene(this.latestOrganInfo, organ.sex)
          : of(EMPTY_SCENE as SpatialSceneNode[]),
      ),
    );

    this.stats$ = combineLatest([this.organ$, this.donorLabel$]).pipe(
      switchMap(([organ, donorLabel]) =>
        organ && this.latestOrganInfo
          ? lookup
              .getOrganStats(this.latestOrganInfo, organ.sex)
              .pipe(
                map((agg) =>
                  agg.map((result) =>
                    donorLabel && result.label === 'Donors' ? { ...result, label: donorLabel } : result,
                  ),
                ),
              )
          : of([]),
      ),
    );

    this.statsLabel$ = this.organ$.pipe(
      map((organ) => this.makeStatsLabel(this.latestOrganInfo, organ?.sex)),
      startWith('Loading...'),
    );

    this.blocks$ = this.organ$.pipe(
      switchMap((organ) =>
        organ && this.latestOrganInfo ? lookup.getBlocks(this.latestOrganInfo, organ.sex) : of([]),
      ),
    );
  }

  ngAfterViewInit(): void {
    const { left, right } = this;
    const rightHeight = right.nativeElement.offsetHeight;
    left.nativeElement.style.height = `${rightHeight}px`;
  }

  updateInput(key: string, value: unknown): void {
    this.configState.patchConfig({ [key]: value });
  }

  private makeStatsLabel(info: OrganInfo | undefined, sex?: string): string {
    let parts: (string | undefined)[] = [`Unknown IRI: ${this.latestConfig.organIri}`];
    if (info) {
      // Use title cased side for a cleaner display
      const side = info.side ? info.side.charAt(0).toUpperCase() + info.side.slice(1) : undefined;
      parts = [sex, info.organ, side];
    }
    return parts.filter((seg) => !!seg).join(', ');
  }

  private logOrganLookup(info: OrganInfo | undefined): void {
    const event = info ? 'organ_lookup_success' : 'organ_lookup_failure';
    const inputs = `Iri: ${this.latestConfig.organIri} - Sex: ${this.latestConfig.sex} - Side: ${this.latestConfig.side}`;
    this.ga.event(event, 'organ', inputs);
  }
}

results matching ""

    No results matching ""