File

src/app/core/store/spatial-search-ui/spatial-search-ui.state.ts

Index

Properties

Properties

defaultValue
defaultValue: number
Type : number
max
max: number
Type : number
min
min: number
Type : number
import { Injectable } from '@angular/core';
import { Matrix4 } from '@math.gl/core';
import { Action, Actions, ofActionDispatched, Selector, State, StateContext, Store } from '@ngxs/store';
import {
  Filter,
  getOriginScene,
  SpatialEntity,
  SpatialSceneNode,
  SpatialSearch,
  TissueBlockResult,
} from 'ccf-database';
import { DataSourceService, OrganInfo } from 'ccf-shared';
import { GoogleAnalyticsService } from 'ngx-google-analytics';
import { forkJoin, Observable } from 'rxjs';
import { debounceTime, mergeMap, take, tap } from 'rxjs/operators';

import { Sex } from '../../../shared/components/spatial-search-config/spatial-search-config.component';
import { UpdateFilter } from '../data/data.actions';
import { DataStateSelectors } from '../data/data.selectors';
import { SceneState } from '../scene/scene.state';
import { AddSearch } from '../spatial-search-filter/spatial-search-filter.actions';
import { SpatialSearchFilterSelectors } from '../spatial-search-filter/spatial-search-filter.selectors';
import {
  GenerateSpatialSearch,
  MoveToNode,
  ResetPosition,
  ResetRadius,
  SetExecuteSearchOnGenerate,
  SetOrgan,
  SetPosition,
  SetRadius,
  SetSex,
  StartSpatialSearchFlow,
  UpdateSpatialSearch,
} from './spatial-search-ui.actions';

export interface Position {
  x: number;
  y: number;
  z: number;
}

export interface RadiusSettings {
  min: number;
  max: number;
  defaultValue: number;
}

export interface TermResult {
  '@id': string;
  label: string;
  count: number;
}

export interface SpatialSearchUiModel {
  sex: Sex;
  organId?: string;
  position?: Position;
  radius?: number;

  defaultPosition?: Position;
  radiusSettings?: RadiusSettings;

  referenceOrgans?: OrganInfo[];
  organScene?: SpatialSceneNode[];

  spatialSearchScene?: SpatialSceneNode[];
  tissueBlocks?: TissueBlockResult[];
  anatomicalStructures?: Record<string, number>;
  cellTypes?: Record<string, number>;

  executeSearchOnGeneration: boolean;
}

class ReallyUpdateSpatialSearch {
  static readonly type = '[SpatialSearchUi] Really update spatial search data';
}

@State<SpatialSearchUiModel>({
  name: 'spatialSearchUi',
  defaults: {
    sex: 'female',
    executeSearchOnGeneration: true,
  },
})
@Injectable()
export class SpatialSearchUiState {
  @Selector([SpatialSearchUiState, SceneState.referenceOrganEntities])
  static organEntity(state: SpatialSearchUiModel, organs: SpatialEntity[]): SpatialEntity | undefined {
    const { organId, sex } = state;
    return organs.find((o) => o.representation_of === organId && o.sex?.toLowerCase() === sex);
  }

  constructor(
    private readonly dataSource: DataSourceService,
    private readonly store: Store,
    actions$: Actions,
    private readonly ga: GoogleAnalyticsService,
  ) {
    actions$
      .pipe(
        ofActionDispatched(UpdateSpatialSearch),
        debounceTime(500),
        tap(() => store.dispatch(ReallyUpdateSpatialSearch)),
      )
      .subscribe();
  }

  @Action(StartSpatialSearchFlow)
  startSpatialSearchFlow(ctx: StateContext<SpatialSearchUiModel>): Observable<unknown> {
    const { sex, organId } = ctx.getState();
    const shortOrgan = organId?.split('/').slice(-1)[0];
    this.ga.event('set_organ', 'spatial_search_ui', `${sex}_${shortOrgan}`);

    return ctx.dispatch(new SetSex(sex));
  }

  /**
   * Updates sex in the SpatialSearchUI and sets selected organ to undefined if not valid for the sex
   */
  @Action(SetSex)
  setSex(ctx: StateContext<SpatialSearchUiModel>, { sex }: SetSex): Observable<unknown> | void {
    let { organId } = ctx.getState();
    ctx.patchState({ sex });
    this.ga.event('set_sex', 'spatial_search_ui', sex);

    if (organId !== undefined && !this.organValidForSex(organId, sex)) {
      organId = undefined;
    }

    const filter = {
      ...this.store.selectSnapshot(DataStateSelectors.filter),
      spatialSearches: [],
    };
    const referenceOrgans = this.store.selectSnapshot(SceneState.referenceOrgans);

    return this.dataSource.getOntologyTermOccurences(filter).pipe(
      take(1),
      tap((counts: Record<string, number>) => {
        ctx.patchState({
          referenceOrgans: referenceOrgans.filter((o) => o.id && !o.disabled && counts[o.id] > 0),
        });
        ctx.dispatch(new SetOrgan(organId));
      }),
    );
  }

  /**
   * Updates organId in the SpatialSearchUI
   */
  @Action(SetOrgan)
  setOrgan(ctx: StateContext<SpatialSearchUiModel>, { organId }: SetOrgan): Observable<unknown> | void {
    const { sex } = ctx.getState();
    ctx.patchState({ sex, organId });
    const shortOrgan = organId?.split('/').slice(-1)[0];
    this.ga.event('set_organ', 'spatial_search_ui', shortOrgan);

    const organ = this.store.selectSnapshot(SpatialSearchUiState.organEntity);
    if (organ && organId && organ.sex) {
      const { x_dimension: width, y_dimension: height, z_dimension: depth } = organ;
      const position = { x: Math.round(width / 2), y: Math.round(height / 2), z: Math.round(depth / 2) };
      const defaultRadius = Math.round(Math.max(width, height, depth) * 0.07);
      const globalFilter = this.store.selectSnapshot(DataStateSelectors.filter);
      const filter = {
        ...globalFilter,
        sex: organ.sex,
        ontologyTerms: [organId],
        spatialSearches: [],
      };

      return this.dataSource.getReferenceOrganScene(organId, filter).pipe(
        take(1),
        tap((organScene: SpatialSceneNode[]) => {
          ctx.patchState({
            position,
            radius: defaultRadius,
            defaultPosition: position,
            radiusSettings: {
              min: Math.min(defaultRadius, 5),
              max: Math.floor(Math.max(width, height, depth) / 1.5),
              defaultValue: defaultRadius,
            },
            organScene: getOriginScene(organ).concat(organScene),
          });
        }),
        mergeMap(() => ctx.dispatch(new UpdateSpatialSearch())),
      );
    }
  }

  /**
   * Updates position in the SpatialSearchUI
   */
  @Action(SetPosition)
  setPosition(ctx: StateContext<SpatialSearchUiModel>, { position }: SetPosition): void {
    ctx.patchState({ position });
    ctx.dispatch(new UpdateSpatialSearch());

    const { x, y, z } = position;
    this.ga.event('set_position', 'spatial_search_ui', `${x}_${y}_${z}`);
  }

  @Action(ResetPosition)
  resetPosition(ctx: StateContext<SpatialSearchUiModel>): void {
    const { defaultPosition } = ctx.getState();
    ctx.patchState({ position: defaultPosition });
    ctx.dispatch(new UpdateSpatialSearch());

    const { x, y, z } = defaultPosition ?? { x: 0, y: 0, z: 0 };
    this.ga.event('reset_position', 'spatial_search_ui', `${x}_${y}_${z}`);
  }

  @Action(MoveToNode)
  moveToNode(ctx: StateContext<SpatialSearchUiModel>, { node }: MoveToNode): Observable<unknown> | void {
    const matrix = new Matrix4(node.transformMatrix);
    const [x, y, z] = matrix.getTranslation().map((n) => Math.round(n * 1000));
    const position: Position = { x, y, z };

    return ctx.dispatch(new SetPosition(position));
  }

  /**
   * Updates radius in the SpatialSearchUI
   */
  @Action(SetRadius)
  setRadius(ctx: StateContext<SpatialSearchUiModel>, { radius }: SetRadius): void {
    ctx.patchState({ radius });
    ctx.dispatch(new UpdateSpatialSearch());

    this.ga.event('set_radius', 'spatial_search_ui', radius.toFixed(1));
  }

  @Action(ResetRadius)
  resetRadius(ctx: StateContext<SpatialSearchUiModel>): void {
    const { radiusSettings } = ctx.getState();
    const radius = radiusSettings?.defaultValue ?? 0;
    ctx.patchState({ radius });
    ctx.dispatch(new UpdateSpatialSearch());

    this.ga.event('reset_radius', 'spatial_search_ui', radius.toFixed(1));
  }

  /**
   * Updates the spatial search data as the organ, position, and radius changes
   */
  @Action(ReallyUpdateSpatialSearch)
  updateSpatialSearch(ctx: StateContext<SpatialSearchUiModel>): Observable<unknown> | void {
    const { position, radius } = ctx.getState();
    const organ = this.store.selectSnapshot(SpatialSearchUiState.organEntity);
    if (organ && position && radius && organ.representation_of) {
      const db = this.dataSource;
      const organId = organ.representation_of;
      const globalFilter = this.store.selectSnapshot(DataStateSelectors.filter);
      const filter: Filter = {
        ...globalFilter,
        sex: organ.sex as 'Male' | 'Female',
        ontologyTerms: [organId],
        spatialSearches: [
          {
            ...position,
            radius: radius,
            target: organ['@id'],
          },
        ],
      };

      return forkJoin({
        spatialSearchScene: db.getReferenceOrganScene(organId, filter).pipe(take(1)),
        tissueBlocks: db.getTissueBlockResults(filter).pipe(take(1)),
        anatomicalStructures: db.getOntologyTermOccurences(filter).pipe(take(1)),
        cellTypes: db.getCellTypeTermOccurences(filter).pipe(take(1)),
      }).pipe(tap((data: Partial<SpatialSearchUiModel>) => ctx.patchState(data)));
    }
  }

  /**
   * Generates and adds a new spatial search then resets the ui state
   */
  @Action(GenerateSpatialSearch)
  generateSpatialSearch(ctx: StateContext<SpatialSearchUiModel>): Observable<unknown> | void {
    const { position, radius, sex, organId, referenceOrgans = [], executeSearchOnGeneration } = ctx.getState();
    const organ = this.store.selectSnapshot(SpatialSearchUiState.organEntity);
    const info = referenceOrgans.find((item) => item.id === organId);

    if (position && radius && organ?.representation_of && info) {
      const search: SpatialSearch = {
        ...position,
        radius,
        target: organ['@id'],
      };
      const actions: unknown[] = [new AddSearch(sex, info.name, search)];

      if (executeSearchOnGeneration) {
        const searches = this.store.selectSnapshot(SpatialSearchFilterSelectors.selectedSearches);
        actions.push(
          new UpdateFilter({
            spatialSearches: searches.concat(search),
          }),
        );
      }

      this.ga.event('generate_search', 'spatial_search_ui');
      return ctx.dispatch(actions).pipe(
        tap(() =>
          ctx.patchState({
            sex: 'female',
            organId: undefined,
          }),
        ),
      );
    }
  }

  @Action(SetExecuteSearchOnGenerate)
  setExecuteSearchOnGenerate(ctx: StateContext<SpatialSearchUiModel>, { execute }: SetExecuteSearchOnGenerate): void {
    ctx.patchState({
      executeSearchOnGeneration: execute,
    });
  }

  /**
   * Used to determine if an organ should be listed if a certain sex is selected
   */
  private organValidForSex(organId: string, sex: Sex): boolean {
    const organs = this.store.selectSnapshot(SceneState.referenceOrgans);
    const organ = organs.find((o) => o.id === organId);
    return organ?.hasSex || organ?.sex === sex;
  }
}

results matching ""

    No results matching ""