Newer
Older
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, OnDestroy, Optional, Output, TemplateRef, ViewChild } from "@angular/core";
import { Subject, Subscription } from "rxjs";
import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
import { debounceTime, distinctUntilChanged, filter, map, startWith, switchMap, take } from "rxjs/operators";
import { ARIA_LABELS } from 'common/constants'
import { EnumViewerEvt, IViewer, TViewerEvent } from "../../viewer.interface";
import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component";
import { NehubaViewerContainerDirective, TMouseoverEvent } from "../nehubaViewerInterface/nehubaViewerInterface.directive";
import { cvtNavigationObjToNehubaConfig } from "../util";
import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service";
import { NehubaMeshService } from "../mesh.service";
import { NehubaLayerControlService, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
import { getExportNehuba, getUuid } from "src/util/fn";
import { INavObj } from "../navigation.service";
import { NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util";
import { MatSnackBar } from "@angular/material/snack-bar";
import { getShader } from "src/util/constants";
import { EnumColorMapName } from "src/util/colorMaps";
import { MatDialog } from "@angular/material/dialog";
import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi";
import { NehubaConfig, getNehubaConfig, getParcNgId, getRegionLabelIndex } from "../config.service";
import { annotation, atlasAppearance, atlasSelection, userInteraction } from "src/state";
import { NgLayerCustomLayer } from "src/state/atlasAppearance";
import { arrayEqual } from "src/util/array";
import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects";
export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!`
@Component({
selector: 'iav-cmp-viewer-nehuba-glue',
templateUrl: './nehubaViewerGlue.template.html',
styleUrls: [
'./nehubaViewerGlue.style.css'
exportAs: 'iavCmpViewerNehubaGlue',
providers: [
{
provide: SET_MESHES_TO_LOAD,
useFactory: (meshService: NehubaMeshService) => meshService.loadMeshes$,
deps: [ NehubaMeshService ]
},
NehubaMeshService,
{
provide: SET_COLORMAP_OBS,
useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.setColorMap$,
deps: [ NehubaLayerControlService ]
},
{
provide: SET_LAYER_VISIBILITY,
useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.visibleLayer$,
deps: [ NehubaLayerControlService ]
},
{
provide: SET_SEGMENT_VISIBILITY,
useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.segmentVis$,
deps: [ NehubaLayerControlService ]
},
{
provide: NG_LAYER_CONTROL,
useFactory: (layerCtrl: NehubaLayerControlService) => layerCtrl.ngLayersController$,
deps: [ NehubaLayerControlService ]
},
NehubaLayerControlService,
NehubaMeshService,
],
changeDetection: ChangeDetectionStrategy.OnPush
export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewInit {
@ViewChild('layerCtrlTmpl', { read: TemplateRef }) layerCtrlTmpl: TemplateRef<any>
@ViewChild(NehubaViewerContainerDirective, { static: true })
public nehubaContainerDirective: NehubaViewerContainerDirective
private multiNgIdsRegionsLabelIndexMap = new Map<string, Map<number, SapiRegionModel>>()
public customLandmarks$ = this.store$.pipe(
select(annotation.selectors.annotations),
public filterCustomLandmark(lm: any){
return !!lm['showInSliceView']
}
private setupNehubaEvRelay() {
while (this.nehubaContainerSub.length > 0) this.nehubaContainerSub.pop().unsubscribe()
if (!this.nehubaContainerDirective) return
const {
mouseOverSegments,
navigationEmitter,
mousePosEmitter,
} = this.nehubaContainerDirective
mouseOverSegments.pipe(
startWith(null as TMouseoverEvent[])
).subscribe(seg => {
this.viewerEvent.emit({
type: EnumViewerEvt.VIEWER_CTX,
data: {
viewerType: 'nehuba',
payload: {
nehuba: seg && seg.map(v => {
return {
layerName: v.layer.name,
labelIndices: [ Number(v.segmentId) ],
regions: (() => {
const map = this.multiNgIdsRegionsLabelIndexMap.get(v.layer.name)
if (!map) return []
return [map.get(Number(v.segmentId))]
})()
}
})
}
}
})
}),
navigationEmitter.pipe(
startWith(null as INavObj)
).subscribe(nav => {
this.viewerEvent.emit({
type: EnumViewerEvt.VIEWER_CTX,
data: {
viewerType: 'nehuba',
payload: {
nav
}
}
})
}),
mousePosEmitter.pipe(
startWith(null as {
).subscribe(mouse => {
this.viewerEvent.emit({
type: EnumViewerEvt.VIEWER_CTX,
data: {
viewerType: 'nehuba',
payload: {
mouse
}
() => {
if (this.nehubaContainerSub) {
while(this.nehubaContainerSub.length > 0) this.nehubaContainerSub.pop().unsubscribe()
this.nehubaContainerSub = null
}
}
ngAfterViewInit(){
this.setupNehubaEvRelay()
}
ngOnDestroy() {
while (this.onDestroyCb.length) this.onDestroyCb.pop()()
}
this.viewerUnit = null
this.nehubaContainerDirective && this.nehubaContainerDirective.clear()
private async loadNewViewer(ATP: { atlas: SapiAtlasModel, parcellation: SapiParcellationModel, template: SapiSpaceModel }, baseLayers: NgLayerCustomLayer[]) {
const config = getNehubaConfig(ATP.template)
for (const baseLayer of baseLayers) {
config.dataset.initialNgState.layers[baseLayer.id] = baseLayer
? cvtNavigationObjToNehubaConfig(this.navigation, config.dataset.initialNgState)
: {}
...config.dataset.initialNgState,
...overwritingInitState,
}
await this.nehubaContainerDirective.createNehubaInstance(config)
this.viewerUnit = this.nehubaContainerDirective.nehubaViewerInstance
public viewerEvent = new EventEmitter<TViewerEvent<'nehuba'>>()
private store$: Store<any>,
private log: LoggingService,
@Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
@Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle,
@Optional() private layerCtrlService: NehubaLayerControlService,
/**
* This **massively** improve the performance of the viewer
* TODO investigate why, and perhaps eventually remove the cdr.detach()
*/
this.cdr.detach()
/**
* define onclick behaviour
*/
if (clickInterceptor) {
const { deregister, register } = clickInterceptor
const selOnhoverRegion = this.selectHoveredRegion.bind(this)
this.onDestroyCb.push(() => deregister(selOnhoverRegion))
).subscribe(this.disposeViewer.bind(this))
this.onDestroyCb.push(() => onATPClear.unsubscribe())
const onATPDebounceNgBaseLayers = this.store$.pipe(
switchMap((ATP: { atlas: SapiAtlasModel, parcellation: SapiParcellationModel, template: SapiSpaceModel }) => this.store$.pipe(
select(atlasAppearance.selectors.customLayers),
debounceTime(16),
map(cl => cl.filter(l => l.clType === "baselayer/nglayer") as NgLayerCustomLayer[]),
distinctUntilChanged(arrayEqual((oi, ni) => oi.id === ni.id)),
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
filter(layers => layers.length > 0),
map(ngBaseLayers => {
return {
ATP,
ngBaseLayers
}
})
))
).subscribe(async ({ ATP, ngBaseLayers }) => {
await this.loadNewViewer(ATP, ngBaseLayers)
/**
* TODO this part is a little awkward. needs refactor
*/
const {
parcNgLayers,
tmplAuxNgLayers,
} = await this.effect.onATPDebounceNgLayers$.pipe(
take(1)
).toPromise()
const ngIdSegmentsMap: Record<string, number[]> = {}
for (const key in parcNgLayers) {
ngIdSegmentsMap[key] = parcNgLayers[key].labelIndicies
}
this.viewerUnit.ngIdSegmentsMap = ngIdSegmentsMap
})
this.onDestroyCb.push(() => onATPDebounceNgBaseLayers.unsubscribe())
/**
* subscribe to ngIdtolblIdxToRegion
*/
const ngIdSub = this.layerCtrlService.selectedATPR$.subscribe(({ atlas, parcellation, template, regions }) => {
this.multiNgIdsRegionsLabelIndexMap.clear()
for (const r of regions) {
const ngId = getParcNgId(atlas, template, parcellation, r)
const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r)
if (!this.multiNgIdsRegionsLabelIndexMap.has(ngId)) {
this.multiNgIdsRegionsLabelIndexMap.set(ngId, new Map())
}
this.multiNgIdsRegionsLabelIndexMap.get(ngId).set(labelIndex, r)
}
})
this.onDestroyCb.push(() => ngIdSub.unsubscribe())
/**
* on hover segment
*/
const onhovSegSub = this.store$.pipe(
})
this.onDestroyCb.push(() => onhovSegSub.unsubscribe())
if (setViewerHandle) {
console.warn(`NYI viewer handle is deprecated`)
}
// listen to navigation change from store
const navSub = this.store$.pipe(
select(atlasSelection.selectors.navigation)
).subscribe(nav => {
this.navigation = nav
})
this.onDestroyCb.push(() => navSub.unsubscribe())
type: EnumViewerEvt.VIEWERLOADED,
private selectHoveredRegion(_ev: any): boolean{
/**
* If label indicies are not defined by the ontology, it will be a string in the format of `{ngId}#{labelIndex}`
*/
const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object')
if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true
atlasSelection.actions.selectRegion({
region: trueOnhoverSegments[0]
private droppedLayerNames: {
layerName: string
resourceUrl: string
}[] = []
private dismissAllAddedLayers(){
while (this.droppedLayerNames.length) {
const { resourceUrl, layerName } = this.droppedLayerNames.pop()
this.store$.dispatch(
atlasAppearance.actions.removeCustomLayer({
id: layerName
})
)
URL.revokeObjectURL(resourceUrl)
}
}
if (files.length !== 1) {
this.snackbar.open(INVALID_FILE_INPUT, 'Dismiss', {
duration: 5000
})
return
}
const randomUuid = getUuid()
const file = files[0]
/**
* TODO check extension?
*/
// Get file, try to inflate, if files, use original array buffer
const buf = await file.arrayBuffer()
let outbuf
try {
outbuf = getExportNehuba().pako.inflate(buf).buffer
} catch (e) {
console.log('unpack error', e)
outbuf = buf
}
try {
const { result, ...other } = await this.worker.sendMessage({
method: 'PROCESS_NIFTI',
param: {
nifti: outbuf
transfers: [ outbuf ]
})
const { meta, buffer } = result
const url = URL.createObjectURL(new Blob([ buffer ]))
this.droppedLayerNames.push({
layerName: randomUuid,
resourceUrl: url
})
this.store$.dispatch(
atlasAppearance.actions.addCustomLayer({
customLayer: {
id: randomUuid,
source: `nifti://${url}`,
shader: getShader({
colormap: EnumColorMapName.MAGMA,
lowThreshold: meta.min || 0,
highThreshold: meta.max || 1
}),
clType: 'customlayer/nglayer'
}
})
)
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
this.dialog.open(
this.layerCtrlTmpl,
{
data: {
layerName: randomUuid,
filename: file.name,
moreInfoFlag: false,
min: meta.min || 0,
max: meta.max || 1,
warning: meta.warning || []
},
hasBackdrop: false,
disableClose: true,
position: {
top: '0em'
},
autoFocus: false,
panelClass: [
'no-padding-dialog',
'w-100'
]
}
).afterClosed().subscribe(
() => this.dismissAllAddedLayers()
)
} catch (e) {
console.error(e)
this.snackbar.open(`Error loading nifti: ${e.toString()}`, 'Dismiss', {
duration: 5000
})
}