File

src/app/shared/components/spatial-search-ui/spatial-search-ui.component.ts

Description

Main Spatial Search UI component

Metadata

Index

Properties
Inputs
Outputs
HostBindings

Inputs

anatomicalStructures
Type : TermResult[]

Anatomical structures within the sphere radius

cellTypes
Type : TermResult[]

Cell types within the sphere radius

defaultPosition
Type : Position

Starting position of sphere

position
Type : Position

Current position of sphere

radius
Type : number

Current sphere radius setting

radiusSettings
Type : RadiusSettings

Maximum, minimum, and default sphere radius values

referenceOrgan
Type : OrganInfo

Current selected organ

scene
Type : SpatialSceneNode[]

Nodes in the scene

sceneBounds
Type : Position

Bounds of the scene

sceneTarget
Type : [number, number, number]

Scene target

sex
Type : string

Current selected sex

tissueBlocks
Type : TissueBlockResult[]

Tissue blocks within the sphere radius

Outputs

addSpatialSearch
Type : EventEmitter

Emits when run spatial search button clicked

closeSpatialSearch
Type : EventEmitter

Emits when close button clicked

editReferenceOrganClicked
Type : EventEmitter

Emits when the edit organ link is clicked

infoClicked
Type : EventEmitter

Emits when info button in header is clicked

nodeClicked
Type : EventEmitter

Emits when a node in the scene is clicked

positionChange
Type : EventEmitter

Emits when the sphere position changes

radiusChange
Type : EventEmitter

Emits when the radius changes

resetPosition
Type : EventEmitter

Emits when reset probing sphere button clicked

resetSphere
Type : EventEmitter

Emits when reset camera button clicked

HostBindings

class
Type : "ccf-spatial-search-ui"
Default value : 'ccf-spatial-search-ui'

HTML Class

Properties

Readonly className
Type : string
Default value : 'ccf-spatial-search-ui'
Decorators :
@HostBinding('class')

HTML Class

import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
import { SpatialSceneNode } from 'ccf-body-ui';
import { TissueBlockResult } from 'ccf-database';
import { OrganInfo } from 'ccf-shared';

import { Position, RadiusSettings, TermResult } from '../../../core/store/spatial-search-ui/spatial-search-ui.state';

/**
 * Main Spatial Search UI component
 */
@Component({
  selector: 'ccf-spatial-search-ui',
  templateUrl: './spatial-search-ui.component.html',
  styleUrls: ['./spatial-search-ui.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SpatialSearchUiComponent {
  /** HTML Class */
  @HostBinding('class') readonly className = 'ccf-spatial-search-ui';

  /** Nodes in the scene */
  @Input() scene!: SpatialSceneNode[];

  /** Bounds of the scene */
  @Input() sceneBounds!: Position;

  /** Scene target */
  @Input() sceneTarget!: [number, number, number];

  /** Current selected sex */
  @Input() sex!: string;

  /** Current selected organ */
  @Input() referenceOrgan!: OrganInfo;

  /** Current sphere radius setting */
  @Input() radius!: number;

  /** Maximum, minimum, and default sphere radius values */
  @Input() radiusSettings!: RadiusSettings;

  /** Starting position of sphere */
  @Input() defaultPosition!: Position;

  /** Current position of sphere */
  @Input() position!: Position;

  /** Tissue blocks within the sphere radius */
  @Input() tissueBlocks!: TissueBlockResult[];

  /** Anatomical structures within the sphere radius */
  @Input() anatomicalStructures!: TermResult[];

  /** Cell types within the sphere radius */
  @Input() cellTypes!: TermResult[];

  /** Emits when run spatial search button clicked */
  @Output() readonly addSpatialSearch = new EventEmitter();

  /** Emits when reset probing sphere button clicked */
  @Output() readonly resetPosition = new EventEmitter();

  /** Emits when reset camera button clicked */
  @Output() readonly resetSphere = new EventEmitter();

  /** Emits when close button clicked */
  @Output() readonly closeSpatialSearch = new EventEmitter();

  /** Emits when the radius changes */
  @Output() readonly radiusChange = new EventEmitter<number>();

  /** Emits when the sphere position changes */
  @Output() readonly positionChange = new EventEmitter<Position>();

  /** Emits when the edit organ link is clicked */
  @Output() readonly editReferenceOrganClicked = new EventEmitter();

  /** Emits when info button in header is clicked */
  @Output() readonly infoClicked = new EventEmitter();

  /** Emits when a node in the scene is clicked */
  @Output() readonly nodeClicked = new EventEmitter<SpatialSceneNode>();
}
<div class="header">
  <div class="title">Configure Spatial Search</div>
  <button class="info" mat-icon-button (click)="infoClicked.emit()">
    <mat-icon>info</mat-icon>
  </button>
  <button class="close" mat-icon-button (click)="closeSpatialSearch.emit()">
    <mat-icon>close</mat-icon>
  </button>
</div>

<div class="content">
  <div class="info-panel">
    <div class="organ-sex-selection">
      <div class="sex">
        <div class="label">Donor Sex:</div>
        <div class="current-sex">{{ sex.charAt(0).toUpperCase() + sex.slice(1) }}</div>
      </div>
      <div class="organ">
        <div class="label">Organ:</div>
        <div class="current-organ">{{ referenceOrgan.name }}</div>
      </div>
      <div class="edit" (click)="editReferenceOrganClicked.emit()">Edit</div>
    </div>
    <mat-divider></mat-divider>
    <div class="radius-slider">
      <div class="title">Probing Sphere Radius</div>
      <div class="slider-container">
        <mat-slider class="slider" [max]="radiusSettings.max" [min]="radiusSettings.min" [step]="1">
          <input matSliderThumb [value]="radius" (input)="radiusChange.emit(+slider.value)" #slider />
        </mat-slider>
        <span class="text value">{{ radius }} mm</span>
      </div>
      <div class="reset-buttons">
        <button
          class="reset-sphere button"
          [class.disabled]="radius === radiusSettings.defaultValue && position === defaultPosition"
          mat-button
          (click)="resetSphere.emit(); resetPosition.emit()"
        >
          Reset Probing Sphere
        </button>
        <button
          class="reset-camera button"
          mat-button
          (click)="
            primary.rotation = primary.rotationX = minimap.rotation = minimap.rotationX = 0;
            primary.target = minimap.target = sceneTarget;
            primary.bounds = minimap.bounds = sceneBounds
          "
        >
          Reset Camera View
        </button>
      </div>
    </div>
    <mat-divider></mat-divider>
    <div class="results">
      <ccf-tissue-block-list class="tissue-block list" [tissueBlocks]="tissueBlocks"></ccf-tissue-block-list>
      <ccf-term-occurrence-list
        class="anatomical-structures list"
        [termList]="anatomicalStructures"
        title="Anatomical Structures"
        toolTipText="Total quantity of predicted anatomical structures detected by the Probing Sphere"
      >
      </ccf-term-occurrence-list>
      <ccf-term-occurrence-list
        class="cell-type list"
        [termList]="cellTypes"
        title="Predicted Cell Types from ASCT+B Tables"
        toolTipText="Total quantity of predicted cell types detected by the Probing Sphere"
      ></ccf-term-occurrence-list>
    </div>
    <button
      class="run-spatial-search button"
      [class.disabled]="tissueBlocks?.length === 0"
      mat-button
      (click)="addSpatialSearch.emit()"
    >
      Run Spatial Search
    </button>
  </div>
  <div class="spatial-search-scene">
    <div class="primary-scene-wrapper">
      <div class="body-ui-hint">Use the keyboard or click a Tissue Block to move the Probing Sphere</div>
      <ccf-body-ui
        #primary
        class="primary-scene"
        [scene]="scene"
        [bounds]="sceneBounds"
        [target]="sceneTarget"
        (nodeClick)="nodeClicked.emit($event?.node)"
        (rotationChange)="minimap.rotation = $event[0]; minimap.rotationX = $event[1]"
      ></ccf-body-ui>
    </div>
    <div class="sidebar">
      <ccf-body-ui
        #minimap
        class="minimap-scene"
        [interactive]="false"
        [scene]="scene"
        [bounds]="sceneBounds"
        [target]="sceneTarget"
        (nodeClick)="nodeClicked.emit($event?.node)"
      ></ccf-body-ui>
      <ccf-spatial-search-keyboard-ui-behavior
        [delta]="1"
        [shiftDelta]="2"
        [position]="position"
        (changePosition)="positionChange.emit($event)"
      ></ccf-spatial-search-keyboard-ui-behavior>
      <ccf-xyz-position [x]="position.x" [y]="position.y" [z]="position.z"></ccf-xyz-position>
    </div>
  </div>
</div>

./spatial-search-ui.component.scss

:host {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 2rem;
  gap: 1rem;
  height: 95vh;
  width: 78vw;
  border-radius: 0.25rem;
  min-height: 45rem;
  min-width: 60rem;

  .header {
    display: flex;
    width: 100%;
    align-items: center;

    .info,
    .close {
      padding: 0;
      background: none;
      border: none;
      cursor: pointer;
      outline: none;
      border-radius: 0.25rem;
      transition: 0.6s;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .title {
      display: flex;
      align-items: center;
      margin-right: 1rem;
    }

    .close {
      margin-left: auto;
    }
  }

  .content {
    display: flex;
    width: 100%;
    height: calc(100% - 3.5rem);

    mat-divider {
      border-top-width: 1px;
    }

    .button {
      border-width: 1px;
      border-style: solid;
      border-radius: 0.25rem;
      font-size: 0.875rem;
      height: 2rem;
      line-height: 2rem;
      transition: 0.5s;

      &.disabled {
        opacity: 0.5;
        pointer-events: none;
      }
    }

    .info-panel {
      display: flex;
      flex-direction: column;
      margin-right: 2rem;
      grid-gap: 1rem;
      gap: 1rem;
      width: 25rem;

      .organ-sex-selection {
        display: flex;
        font-size: 1rem;
        justify-content: space-between;

        .sex,
        .organ {
          display: flex;

          .label {
            font-weight: 300;
            margin-right: 0.5rem;
          }

          .current-sex,
          .current-organ {
            font-weight: 600;
          }
        }

        .edit {
          cursor: pointer;
        }
      }

      .radius-slider {
        display: flex;
        flex-direction: column;

        .title {
          font-weight: 600;
          font-size: 1rem;
        }

        .slider-container {
          display: flex;
          justify-content: space-between;

          .slider {
            width: 19rem;
          }

          .value {
            display: flex;
            align-items: center;
            font-size: 1rem;
          }
        }

        .reset-buttons {
          display: flex;
          justify-content: space-between;

          button {
            width: 11.5rem;
          }
        }
      }

      .results {
        height: calc(100% - 15rem - 2px);
        .list {
          height: 33%;
          display: flex;
          flex-direction: column;
        }
      }
    }

    .spatial-search-scene {
      display: flex;
      width: calc(100% - 25rem);
      background-color: black;
      border-bottom-left-radius: 0.5rem;
      border-bottom-right-radius: 0.5rem;
      border-top-right-radius: 0.5rem;
      border-top-left-radius: 0.5rem;

      .primary-scene-wrapper {
        display: flex;
        flex-direction: column;
        width: 100%;
        height: 100%;

        .primary-scene {
          flex: auto;
          overflow: hidden;
        }

        .body-ui-hint {
          color: white;
          font-size: 1rem;
          margin: 1rem;
        }
      }

      .sidebar {
        .minimap-scene {
          margin: 1.5rem;
          width: 12.75rem;
          height: 11rem;
          ::ng-deep .body-ui {
            background-color: #232f3a;
          }
        }

        ccf-spatial-search-keyboard-ui-behavior {
          margin: 1.5rem;
          display: flex;
          justify-content: center;
        }

        ccf-xyz-position {
          margin: 1.5rem;
          padding-left: 5rem;
        }
      }
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""