src/app/shared/components/spatial-search-ui/spatial-search-ui.component.ts
Main Spatial Search UI component
changeDetection | ChangeDetectionStrategy.OnPush |
selector | ccf-spatial-search-ui |
styleUrls | ./spatial-search-ui.component.scss |
templateUrl | ./spatial-search-ui.component.html |
Properties |
|
Inputs |
Outputs |
HostBindings |
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 |
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 |
class |
Type : "ccf-spatial-search-ui"
|
Default value : 'ccf-spatial-search-ui'
|
HTML Class |
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;
}
}
}
}
}