Skip to content
Snippets Groups Projects
nehubaViewerGlue.component.ts 11.4 KiB
Newer Older
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, OnDestroy, Optional, Output, TemplateRef, ViewChild } from "@angular/core";
Xiao Gui's avatar
Xiao Gui committed
import { select, Store } from "@ngrx/store";
import { Subject, Subscription } from "rxjs";
Xiao Gui's avatar
Xiao Gui committed
import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
import { distinctUntilChanged, startWith } from "rxjs/operators";
import { ARIA_LABELS } from 'common/constants'
Xiao Gui's avatar
Xiao Gui committed
import { LoggingService } from "src/logging";
import { EnumViewerEvt, IViewer, TViewerEvent } from "../../viewer.interface";
import { NehubaViewerContainerDirective, TMouseoverEvent } from "../nehubaViewerInterface/nehubaViewerInterface.directive";
Xiao Gui's avatar
Xiao Gui committed
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 { SapiRegionModel } from "src/atlasComponents/sapi";
import { NehubaConfig, getParcNgId, getRegionLabelIndex } from "../config.service";
Xiao Gui's avatar
Xiao Gui committed
import { SET_MESHES_TO_LOAD } from "../constants";
import { annotation, atlasAppearance, atlasSelection, userInteraction } from "src/state";

export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!`
Xiao Gui's avatar
Xiao Gui committed

@Component({
  selector: 'iav-cmp-viewer-nehuba-glue',
  templateUrl: './nehubaViewerGlue.template.html',
  styleUrls: [
    './nehubaViewerGlue.style.css'
Xiao Gui's avatar
Xiao Gui committed
  ],
  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
Xiao Gui's avatar
Xiao Gui committed
})

Xiao Gui's avatar
Xiao Gui committed
export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewInit {
  @ViewChild('layerCtrlTmpl', { read: TemplateRef }) layerCtrlTmpl: TemplateRef<any>

Xiao Gui's avatar
Xiao Gui committed
  public ARIA_LABELS = ARIA_LABELS
Xiao Gui's avatar
Xiao Gui committed
  @ViewChild(NehubaViewerContainerDirective, { static: true })
  public nehubaContainerDirective: NehubaViewerContainerDirective

Xiao Gui's avatar
Xiao Gui committed
  private onhoverSegments: SapiRegionModel[] = []
Xiao Gui's avatar
Xiao Gui committed
  private onDestroyCb: (() => void)[] = []
Xiao Gui's avatar
Xiao Gui committed
  private multiNgIdsRegionsLabelIndexMap = new Map<string, Map<number, SapiRegionModel>>()
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
  public nehubaConfig: NehubaConfig
Xiao Gui's avatar
Xiao Gui committed
  public customLandmarks$ = this.store$.pipe(
    select(annotation.selectors.annotations),
Xiao Gui's avatar
Xiao Gui committed
  public filterCustomLandmark(lm: any){
    return !!lm['showInSliceView']
  }

Xiao Gui's avatar
Xiao Gui committed
  private nehubaContainerSub: Subscription[] = []
  private setupNehubaEvRelay() {
Xiao Gui's avatar
Xiao Gui committed
    while (this.nehubaContainerSub.length > 0) this.nehubaContainerSub.pop().unsubscribe()
    
    if (!this.nehubaContainerDirective) return
    const {
      mouseOverSegments,
      navigationEmitter,
      mousePosEmitter,
    } = this.nehubaContainerDirective
Xiao Gui's avatar
Xiao Gui committed

    this.nehubaContainerSub.push(

      mouseOverSegments.pipe(
        startWith(null as TMouseoverEvent[])
Xiao Gui's avatar
Xiao Gui committed
      ).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)
Xiao Gui's avatar
Xiao Gui committed
      ).subscribe(nav => {
        this.viewerEvent.emit({
          type: EnumViewerEvt.VIEWER_CTX,
          data: {
            viewerType: 'nehuba',
            payload: {
              nav
            }
          }
        })
      }),

      mousePosEmitter.pipe(
        startWith(null as {
Xiao Gui's avatar
Xiao Gui committed
          voxel: number[]
          real: number[]
Xiao Gui's avatar
Xiao Gui committed
      ).subscribe(mouse => {
        this.viewerEvent.emit({
          type: EnumViewerEvt.VIEWER_CTX,
          data: {
            viewerType: 'nehuba',
            payload: {
              mouse
            }
Xiao Gui's avatar
Xiao Gui committed
        })
    this.onDestroyCb.push(
      () => {
        if (this.nehubaContainerSub) {
Xiao Gui's avatar
Xiao Gui committed
          while(this.nehubaContainerSub.length > 0) this.nehubaContainerSub.pop().unsubscribe()
  ngAfterViewInit(){
    this.setupNehubaEvRelay()
  }

Xiao Gui's avatar
Xiao Gui committed
  ngOnDestroy() {
    while (this.onDestroyCb.length) this.onDestroyCb.pop()()
  }

Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
  private disposeViewer() {
Xiao Gui's avatar
Xiao Gui committed
    /**
     * clear existing container
     */
    this.nehubaContainerDirective && this.nehubaContainerDirective.clear()
  @Output()
  public viewerEvent = new EventEmitter<TViewerEvent<'nehuba'>>()
Xiao Gui's avatar
Xiao Gui committed
  constructor(
Xiao Gui's avatar
Xiao Gui committed
    private store$: Store<any>,
    private snackbar: MatSnackBar,
    private dialog: MatDialog,
    private worker: AtlasWorkerService,
Xiao Gui's avatar
Xiao Gui committed
    private layerCtrlService: NehubaLayerControlService,
Xiao Gui's avatar
Xiao Gui committed
    @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
    @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle,
Xiao Gui's avatar
Xiao Gui committed
  ){
    /**
     * This **massively** improve the performance of the viewer
     * TODO investigate why, and perhaps eventually remove the cdr.detach()
     */
    // this.cdr.detach()
Xiao Gui's avatar
Xiao Gui committed
    /**
     * define onclick behaviour
     */
    if (clickInterceptor) {
      const { deregister, register } = clickInterceptor
      const selOnhoverRegion = this.selectHoveredRegion.bind(this)
      register(selOnhoverRegion, { last: true })
fsdavid's avatar
fsdavid committed
      this.onDestroyCb.push(() => deregister(selOnhoverRegion))
Xiao Gui's avatar
Xiao Gui committed
    const onATPClear = this.store$.pipe(
Xiao Gui's avatar
Xiao Gui committed
      atlasSelection.fromRootStore.distinctATP()
Xiao Gui's avatar
Xiao Gui committed
    ).subscribe(this.disposeViewer.bind(this))
    this.onDestroyCb.push(() => onATPClear.unsubscribe())
    
Xiao Gui's avatar
Xiao Gui committed
    /**
     * 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())

Xiao Gui's avatar
Xiao Gui committed
    /**
     * on hover segment
     */
    const onhovSegSub = this.store$.pipe(
Xiao Gui's avatar
Xiao Gui committed
      select(userInteraction.selectors.mousingOverRegions),
Xiao Gui's avatar
Xiao Gui committed
      distinctUntilChanged(),
    ).subscribe(arr => {
Xiao Gui's avatar
Xiao Gui committed
      this.onhoverSegments = arr
Xiao Gui's avatar
Xiao Gui committed
    })
    this.onDestroyCb.push(() => onhovSegSub.unsubscribe())

    if (setViewerHandle) {
      console.warn(`NYI viewer handle is deprecated`)
    }
Xiao Gui's avatar
Xiao Gui committed
  }

Xiao Gui's avatar
Xiao Gui committed
  handleViewerLoadedEvent(flag: boolean) {
    this.viewerEvent.emit({
      type: EnumViewerEvt.VIEWERLOADED,
Xiao Gui's avatar
Xiao Gui committed
      data: flag
    })
  }

  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
Xiao Gui's avatar
Xiao Gui committed
    this.store$.dispatch(
Xiao Gui's avatar
Xiao Gui committed
      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)
    }
  }
  public async handleFileDrop(files: File[]){
    if (files.length !== 1) {
      this.snackbar.open(INVALID_FILE_INPUT, 'Dismiss', {
        duration: 5000
      })
      return
    }
    const randomUuid = getUuid()
    const file = files[0]

    /**
     * TODO check extension?
     */
    this.dismissAllAddedLayers()
    
    // 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'
          }
        })
      )
      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
      })
    }