File

src/app/core/store/reference-data/reference-data.state.ts

Description

Data for the main 3d model display

Extends

NgxsImmutableDataRepository

Index

Methods

Constructor

constructor(globals: GlobalsService, globalConfig: GlobalConfigState<GlobalConfig>)
Parameters :
Name Type Optional
globals GlobalsService No
globalConfig GlobalConfigState<GlobalConfig> No

Methods

getOrganData
getOrganData(iri: string)

Looks up organ information from an IRI

Parameters :
Name Type Optional Description
iri string No

The IRI

A populated organ data if the IRI is valid, otherwise undefined

getReferenceOrganIri
getReferenceOrganIri(organ: string, sex?: "Male" | "Female" | string, side?: "Left" | "Right" | string, organInfo?: OrganInfo)

Looks up an IRI for a potential reference organ.

Parameters :
Name Type Optional Description
organ string No

the organ

sex "Male" | "Female" | string Yes

the sex: male, female, or undefined

side "Left" | "Right" | string Yes

the side: left, right, or undefined

organInfo OrganInfo Yes
Returns : string | undefined

An IRI if there is a reference organ for this state, otherwise undefined

ngxsOnInit
ngxsOnInit()

Initializes this state service.

Returns : void
normalizePlacement
normalizePlacement(place: SpatialPlacementJsonLd)
Parameters :
Name Type Optional
place SpatialPlacementJsonLd No
Returns : SpatialPlacementJsonLd
import { Immutable } from '@angular-ru/common/typings';
import { StateRepository } from '@angular-ru/ngxs/decorators';
import { NgxsImmutableDataRepository } from '@angular-ru/ngxs/repositories';
import { Injectable } from '@angular/core';
import { Matrix4, toRadians } from '@math.gl/core';
import { State } from '@ngxs/store';
import { SpatialPlacementJsonLd, SpatialSceneNode } from 'ccf-body-ui';
import { ExtractionSet, SpatialEntity } from 'ccf-database';
import { ALL_ORGANS, GlobalConfigState, GlobalsService, OrganInfo } from 'ccf-shared';
import { EMPTY, Observable, from } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';

import { environment } from '../../../../environments/environment';
import { GlobalConfig } from '../../services/config/config';
import { XYZTriplet } from '../model/model.state';

export function applySpatialPlacement(tx: Matrix4, placement: Immutable<SpatialPlacementJsonLd>): Matrix4 {
  const p = placement;
  let factor: number;
  switch (p.translation_units) {
    case 'centimeter':
      factor = 1 / 100;
      break;
    case 'millimeter':
      factor = 1 / 1000;
      break;
    case 'meter':
    default:
      factor = 1;
      break;
  }
  const T = [p.x_translation, p.y_translation, p.z_translation].map((t) => t * factor);
  const R = [p.x_rotation, p.y_rotation, p.z_rotation].map<number>(toRadians) as [number, number, number];
  const S = [p.x_scaling, p.y_scaling, p.z_scaling];

  return tx.translate(T).rotateXYZ(R).scale(S);
}

export interface ReferenceDataStateModel {
  organIRILookup: { [lookup: string]: string };
  organSpatialEntities: { [iri: string]: SpatialEntity };
  anatomicalStructures: { [iri: string]: SpatialEntity[] };
  extractionSets: { [iri: string]: ExtractionSet[] };
  sceneNodeLookup: { [iri: string]: SpatialSceneNode };
  simpleSceneNodeLookup: { [iri: string]: SpatialSceneNode };
  placementPatches: { [iri: string]: SpatialPlacementJsonLd };
}

export interface OrganData {
  organ: OrganInfo;
  sex?: 'male' | 'female';
  side?: 'left' | 'right';
}

/**
 * Data for the main 3d model display
 */
@StateRepository()
@State<ReferenceDataStateModel>({
  name: 'reference',
  defaults: {
    organIRILookup: {},
    organSpatialEntities: {},
    anatomicalStructures: {},
    extractionSets: {},
    sceneNodeLookup: {},
    simpleSceneNodeLookup: {},
    placementPatches: {},
  },
})
@Injectable()
export class ReferenceDataState extends NgxsImmutableDataRepository<ReferenceDataStateModel> {
  constructor(
    private readonly globals: GlobalsService,
    private readonly globalConfig: GlobalConfigState<GlobalConfig>,
  ) {
    super();
  }

  /**
   * Initializes this state service.
   */
  override ngxsOnInit(): void {
    super.ngxsOnInit();

    this.getSourceDB().subscribe((db) => {
      this.setState(db);

      // In development, make the db globally accessible
      if (!environment.production) {
        this.globals.set('db', db);
      }
    });
  }

  private getSourceDB(): Observable<ReferenceDataStateModel> {
    return this.globalConfig.getOption<string>('referenceData').pipe(
      switchMap((url) =>
        from(fetch(url)).pipe(
          switchMap((data) => data.json()),
          catchError(() => EMPTY),
        ),
      ),
    );
  }

  normalizePlacement(place: SpatialPlacementJsonLd): SpatialPlacementJsonLd {
    const db = this.snapshot;
    const patchPlacement = db.placementPatches[place?.target];
    if (patchPlacement) {
      const matrix = applySpatialPlacement(new Matrix4(Matrix4.IDENTITY), patchPlacement);
      const position: XYZTriplet = { x: place.x_translation, y: place.y_translation, z: place.z_translation };
      const [x, y, z] = matrix.transformAsPoint([position.x, position.y, position.z], []);
      const newPlacement = { ...place, target: patchPlacement.target };
      newPlacement.x_translation = x;
      newPlacement.y_translation = y;
      newPlacement.z_translation = z;
      return newPlacement;
    } else {
      return place;
    }
  }

  /**
   * Looks up an IRI for a potential reference organ.
   *
   * @param organ the organ
   * @param sex the sex: male, female, or undefined
   * @param side the side: left, right, or undefined
   * @returns An IRI if there is a reference organ for this state, otherwise undefined
   */
  getReferenceOrganIri(
    organ: string,
    sex?: 'Male' | 'Female' | string,
    side?: 'Left' | 'Right' | string,
    organInfo?: OrganInfo,
  ): string | undefined {
    const db = this.snapshot;
    if (organ.toUpperCase() !== 'KIDNEY') {
      side = '';
    }
    if (organInfo?.sex) {
      sex = organInfo.sex;
    }
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
    const lookup = [organ, sex, side || organInfo?.side].join('|').toUpperCase();
    const key = Object.keys(db.organIRILookup).find((code) => code.toUpperCase().endsWith(lookup));
    return this.getLatestIri(key ? db.organIRILookup[key] : undefined);
  }

  /**
   * Looks up organ information from an IRI
   *
   * @param iri The IRI
   * @returns A populated organ data if the IRI is valid, otherwise undefined
   */
  getOrganData(iri: string): OrganData | undefined {
    const updatedIri = this.getLatestIri(iri);
    const state = this.snapshot;
    const entity = state.organSpatialEntities[updatedIri];
    if (!entity) {
      return undefined;
    }

    const name = entity.label ?? '';
    const organ = ALL_ORGANS.find(
      (info) => name.endsWith(info.organ) && (!entity.side || entity.side.toLowerCase() === info.side),
    );
    if (!organ) {
      return undefined;
    }

    return {
      organ,
      sex: entity.sex?.toLowerCase() as 'male' | 'female',
      side: entity.side?.toLowerCase() as 'left' | 'right',
    };
  }

  private getLatestIri(organ?: string): string {
    if (!organ) {
      return '';
    }
    const organEntry = this.snapshot.placementPatches[organ];
    return organEntry ? this.getLatestIri(organEntry.target) : organ;
  }
}

results matching ""

    No results matching ""