Skip to content
Snippets Groups Projects
nehubaViewerGlue.component.ts 9.37 KiB
Newer Older
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
import { AfterContentChecked, AfterViewInit, ChangeDetectionStrategy, 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 { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
import { distinctUntilChanged, filter, map } from "rxjs/operators";
import { ARIA_LABELS, CONST } from 'common/constants'
import { IViewer, TViewerEvent } from "../../viewer.interface";
import { NehubaMeshService } from "../mesh.service";
import { NehubaLayerControlService, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
import { getExportNehuba, getUuid } from "src/util/fn";
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 } from "../config.service";
Xiao Gui's avatar
Xiao Gui committed
import { SET_MESHES_TO_LOAD } from "../constants";
import { atlasAppearance, atlasSelection, userInteraction } from "src/state";
import { linearTransform, TVALID_LINEAR_XFORM_DST, TVALID_LINEAR_XFORM_SRC } from "src/atlasComponents/sapi/core/space/interspaceLinearXform";
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
import { CustomLayer, NgLayerCustomLayer } from "src/state/atlasAppearance";
import { arrayEqual } from "src/util/array";
export const INVALID_FILE_INPUT = `Exactly one (1) 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
})

export class NehubaGlueCmp implements IViewer<'nehuba'>, AfterViewInit, OnDestroy {
  @ViewChild('layerCtrlTmpl', { static: true })
  layerCtrlTmpl: TemplateRef<any>

Xiao Gui's avatar
Xiao Gui committed
  public ARIA_LABELS = ARIA_LABELS
  public CONST = CONST
Xiao Gui's avatar
Xiao Gui committed

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

Xiao Gui's avatar
Xiao Gui committed
  public nehubaConfig: NehubaConfig
Xiao Gui's avatar
Xiao Gui committed
  ngOnDestroy(): void {
Xiao Gui's avatar
Xiao Gui committed
    while (this.onDestroyCb.length) this.onDestroyCb.pop()()
  }

  @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
    @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
Xiao Gui's avatar
Xiao Gui committed
  ){
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
    }

    /**
     * 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())
Xiao Gui's avatar
Xiao Gui committed
  }

  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]
  ngAfterViewInit(): void {
    const customLayer = this.store$.pipe(
      select(atlasAppearance.selectors.customLayers),
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
      distinctUntilChanged(arrayEqual((o: NgLayerCustomLayer, n) => o.id === n.id)),
      map(cl => {
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
        const customLayers = cl.filter(l => l.clType === "customlayer/nglayer/controller" && l.controllable)
        return customLayers
      }),
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
      filter(r => !!r),
      distinctUntilChanged(arrayEqual((o: NgLayerCustomLayer, n) => o.source === n.source)),
    ).subscribe((l: NgLayerCustomLayer[]) => {
Daviti Gogshelidze's avatar
Daviti Gogshelidze committed
      console.log(l)
      if (l && l.length === 1) {
        this.openLayerController({layerName: l[0].id, fileName: l[0].source.split(',').pop()})
      }
    })
    this.onDestroyCb.push(() => customLayer.unsubscribe())
  }

  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)
    }
  }
Xiao Gui's avatar
Xiao Gui committed
  public async handleFileDrop(files: File[]): Promise<void> {
    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()
Xiao Gui's avatar
Xiao Gui committed

    if (/\.swc$/i.test(file.name)) {
      let message = `The swc rendering is experimental. Please contact us on any feedbacks. `
      const swcText = await file.text()
      let src: TVALID_LINEAR_XFORM_SRC
Xiao Gui's avatar
Xiao Gui committed
      const dst: TVALID_LINEAR_XFORM_DST = "NEHUBA"
      if (/ccf/i.test(swcText)) {
        src = "CCF"
        message += `CCF detected, applying known transformation.`
      }
      if (!src) {
        message += `no known space detected. Applying default transformation.`
      }

      const xform = await linearTransform(src, dst)
      
Xiao Gui's avatar
Xiao Gui committed
      const url = URL.createObjectURL(file)
      this.droppedLayerNames.push({
        layerName: randomUuid,
        resourceUrl: url
      })
      this.store$.dispatch(
        atlasAppearance.actions.addCustomLayer({
          customLayer: {
            id: randomUuid,
            source: `swc://${url}`,
            segments: ["1"],
            transform: xform,
Xiao Gui's avatar
Xiao Gui committed
            clType: 'customlayer/nglayer' as const
          }
        })
      )
      this.snackbar.open(message, "Dismiss", {
        duration: 10000
      })
    // 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 {
Xiao Gui's avatar
Xiao Gui committed
      const { result } = 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.openLayerController({layerName: randomUuid,
        fileName: file.name,
        min: meta.min || 0,
        max: meta.max || 1,
        warning: meta.warning || []})
      
    } catch (e) {
      console.error(e)
      this.snackbar.open(`Error loading nifti: ${e.toString()}`, 'Dismiss', {
        duration: 5000
      })
    }

  openLayerController(meta: {layerName: string, fileName: string, min?: number, max?: number, warning?: any[]}) {
    this.dialog.open(
      this.layerCtrlTmpl,
      {
        data: {
          layerName: meta.layerName,
          filename: meta.fileName,
          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()
    )
  }