Skip to content
Snippets Groups Projects
nehubaViewerGlue.component.ts 29.8 KiB
Newer Older
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core";
Xiao Gui's avatar
Xiao Gui committed
import { select, Store } from "@ngrx/store";
import { asyncScheduler, combineLatest, fromEvent, interval, merge, Observable, of, Subject, timer } from "rxjs";
import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionSetPerspOctantRemoval, ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions";
import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors";
import { debounceTime, distinctUntilChanged, filter, map, mapTo, scan, shareReplay, startWith, switchMap, switchMapTo, take, tap, throttleTime, withLatestFrom } from "rxjs/operators";
import { viewerStateAddUserLandmarks, viewerStateChangeNavigation, viewerStateMouseOverCustomLandmark, viewerStateSelectRegionWithIdDeprecated, viewerStateSetSelectedRegions, viewreStateRemoveUserLandmarks } from "src/services/state/viewerState/actions";
import { ngViewerSelectorLayers, ngViewerSelectorClearView, ngViewerSelectorPanelOrder, ngViewerSelectorOctantRemoval, ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors";
Xiao Gui's avatar
Xiao Gui committed
import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors";
Xiao Gui's avatar
Xiao Gui committed
import { serialiseParcellationRegion } from 'common/util'
import { ARIA_LABELS, IDS } from 'common/constants'
import { PANELS } from "src/services/state/ngViewerState/constants";
import { LoggingService } from "src/logging";

import { getNgIds, getMultiNgIdsRegionsLabelIndexMap, SET_MESHES_TO_LOAD } from "../constants";
Xiao Gui's avatar
Xiao Gui committed
import { IViewer, TViewerEvent } from "../../viewer.interface";
import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component";
import { NehubaViewerContainerDirective } from "../nehubaViewerInterface/nehubaViewerInterface.directive";
Xiao Gui's avatar
Xiao Gui committed
import { cvtNavigationObjToNehubaConfig, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn, takeOnePipe } from "../util";
Xiao Gui's avatar
Xiao Gui committed
import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service";
Xiao Gui's avatar
Xiao Gui committed
import { MouseHoverDirective } from "src/mouseoverModule";
import { NehubaMeshService } from "../mesh.service";
Xiao Gui's avatar
Xiao Gui committed

interface INgLayerInterface {
  name: string // displayName
  source: string
  mixability: string // base | mixable | nonmixable
  annotation?: string //
  id?: string // unique identifier
  visible?: boolean
  shader?: string
  transform?: any
}

@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
  ]
Xiao Gui's avatar
Xiao Gui committed
})

Xiao Gui's avatar
Xiao Gui committed
export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{

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

  @ViewChild(NehubaViewerContainerDirective, { static: true })
  public nehubaContainerDirective: NehubaViewerContainerDirective

Xiao Gui's avatar
Xiao Gui committed
  @ViewChild(MouseHoverDirective, { static: true })
  private mouseoverDirective: MouseHoverDirective

  public viewerLoaded: boolean = false
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
  private onhoverSegments = []
  private onDestroyCb: Function[] = []
Xiao Gui's avatar
Xiao Gui committed
  private viewerUnit: NehubaViewerUnit
  private ngLayersRegister: {layers: INgLayerInterface[]} = {
    layers: []
  }
  private multiNgIdsRegionsLabelIndexMap: Map<string, Map<number, any>>

  @Input()
  public selectedParcellation: any

  @Input()
  public selectedTemplate: any

Xiao Gui's avatar
Xiao Gui committed
  private navigation: any

Xiao Gui's avatar
Xiao Gui committed
  private newViewer$ = new Subject()

  public showPerpsectiveScreen$: Observable<string>
  public sliceViewLoadingMain$: Observable<[boolean, boolean, boolean]>
  private sliceRenderEvent$: Observable<CustomEvent>
  public perspectiveViewLoading$: Observable<string|null>
  public hoveredPanelIndices$: Observable<number>
  private viewPanelWeakMap = new WeakMap<HTMLElement, number>()
Xiao Gui's avatar
Xiao Gui committed
  public viewPanels: [HTMLElement, HTMLElement, HTMLElement, HTMLElement] = [null, null, null, null]
Xiao Gui's avatar
Xiao Gui committed
  private findPanelIndex = (panel: HTMLElement) => this.viewPanelWeakMap.get(panel)
  public nanometersToOffsetPixelsFn: Array<(...arg) => any> = []

fsdavid's avatar
fsdavid committed
  public quickTourSliceViewSlide = {
    order: 1,
    description: 'The planar views allow you to zoom in to full resolution (mouse wheel), pan the view (click+drag), and select oblique sections (shift+click+drag). You can double-click brain regions to select them.',
    position: 'bottom-right',
    overwritePos: of({arrow: 'arrow3', arrowPosition: 'left', arrowMargin: {right: -50, bottom: 130}})
  }

  public quickTour3dViewSlide = {
    order: 2,
    description: 'The 3D view gives an overview of the brain with limited resolution. It can be independently rotated. Click the „eye“ icon on the bottom left to toggle pure surface view.',
    position: 'top-left',
    overwritePos: of({arrow: 'arrow5', arrowPosition: 'right', arrowAlign: 'bottom', arrowMargin: {bottom: -25, left: -10}, arrowTransform: 'rotate(-130deg)'})
  }

  public quickTourIconsSlide = {
    order: 3,
    description: 'Use these icons in any of the views to maximize it and zoom in/out.',
    position: 'bottom',
    align: 'right',
    overwritePos: of({arrow: 'arrow4', arrowPosition: 'top', arrowAlign: 'right', arrowMargin: {right: 50}})
  }

Xiao Gui's avatar
Xiao Gui committed
  public customLandmarks$: Observable<any> = this.store$.pipe(
    select(viewerStateCustomLandmarkSelector),
    map(lms => lms.map(lm => ({
      ...lm,
      geometry: {
        position: lm.position
      }
    }))),
  )

  private forceUI$ = this.customLandmarks$.pipe(
    map(lm => {
      if (lm.length > 0) {
        return {
          target: 'perspective:octantRemoval',
          mode: false,
          message: `octant control disabled: showing landmarks.`
        }
      } else {
        return {
          target: 'perspective:octantRemoval',
          mode: null
        }
      }
    })
  )

  public disableOctantRemovalCtrl$ = this.forceUI$.pipe(
    filter(({ target }) => target === 'perspective:octantRemoval'),
  )

  public nehubaViewerPerspectiveOctantRemoval$ = this.store$.pipe(
    select(ngViewerSelectorOctantRemoval),
  )

  public panelOrder$ = this.store$.pipe(
    select(ngViewerSelectorPanelOrder),
    distinctUntilChanged(),
    shareReplay(1),
  )

Xiao Gui's avatar
Xiao Gui committed
  ngOnChanges(sc: SimpleChanges){
    const {
      selectedParcellation,
      selectedTemplate
    } = sc
    if (selectedTemplate) {
      if (selectedTemplate?.currentValue?.['@id'] !== selectedTemplate?.previousValue?.['@id']) {

        if (selectedTemplate?.previousValue) {
          this.unloadTmpl(selectedTemplate?.previousValue)
        }
        if (selectedTemplate?.currentValue?.['@id']) {
          this.loadTmpl(selectedTemplate.currentValue, selectedParcellation.currentValue)
        }
      }
    }else if (selectedParcellation && selectedParcellation.currentValue !== selectedParcellation.previousValue) {
Xiao Gui's avatar
Xiao Gui committed
      this.loadParc(selectedParcellation.currentValue)
Xiao Gui's avatar
Xiao Gui committed
    }
  }

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

Xiao Gui's avatar
Xiao Gui committed
  private loadParc(parcellation: any) {
    /**
     * parcellaiton may be undefined
     */
    if ( !(parcellation && parcellation.regions)) {
      return
    }

    /**
     * first, get all all the ngIds, including parent id from parcellation (if defined)
     */
    const ngIds = getNgIds(parcellation.regions).concat( parcellation.ngId ? parcellation.ngId : [])

    this.multiNgIdsRegionsLabelIndexMap = getMultiNgIdsRegionsLabelIndexMap(parcellation)

    this.viewerUnit.multiNgIdsLabelIndexMap = this.multiNgIdsRegionsLabelIndexMap
    this.viewerUnit.auxilaryMeshIndices = parcellation.auxillaryMeshIndices || []

    /* TODO replace with proper KG id */
    /**
     * need to set unique array of ngIds, or else workers will be overworked
     */
    this.viewerUnit.ngIds = Array.from(new Set(ngIds))
  }

Xiao Gui's avatar
Xiao Gui committed
  private unloadTmpl(tmpl: any) {
    /**
     * clear existing container
     */
    this.viewerUnit = null
    this.nehubaContainerDirective.clear()

    /* on selecting of new template, remove additional nglayers */
    const baseLayerNames = Object.keys(tmpl.nehubaConfig.dataset.initialNgState.layers)
    this.ngLayersRegister.layers
      .filter(layer => baseLayerNames?.findIndex(l => l === layer.name) >= 0)
      .map(l => l.name)
      .forEach(layerName => {
        this.store$.dispatch(ngViewerActionRemoveNgLayer({
          layer: {
            name: layerName
          }
        }))
      })
  }

  private async loadTmpl(_template: any, parcellation: any) {

    if (!_template) return
    /**
     * recalcuate zoom
     */
    const template = (() => {

      const deepCopiedState = JSON.parse(JSON.stringify(_template))
Xiao Gui's avatar
Xiao Gui committed
      const initialNgState = deepCopiedState.nehubaConfig.dataset.initialNgState

      if (!initialNgState || !this.navigation) {
Xiao Gui's avatar
Xiao Gui committed
        return deepCopiedState
      }
Xiao Gui's avatar
Xiao Gui committed
      const overwritingInitState = this.navigation
        ? cvtNavigationObjToNehubaConfig(this.navigation, initialNgState)
        : {}
Xiao Gui's avatar
Xiao Gui committed
      deepCopiedState.nehubaConfig.dataset.initialNgState = {
        ...initialNgState,
        ...overwritingInitState,
      }
Xiao Gui's avatar
Xiao Gui committed
      return deepCopiedState
    })()

Xiao Gui's avatar
Xiao Gui committed
    this.nehubaContainerDirective.createNehubaInstance(template)
    this.viewerUnit = this.nehubaContainerDirective.nehubaViewerInstance
Xiao Gui's avatar
Xiao Gui committed
    this.sliceRenderEvent$.pipe(
      takeOnePipe()
    ).subscribe(ev => {
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
      for (const idx of [0, 1, 2]) {
        const e = ev[idx] as CustomEvent
        const el = e.target as HTMLElement
        this.viewPanelWeakMap.set(el, idx)
        this.viewPanels[idx] = el
        this.nanometersToOffsetPixelsFn[idx] = e.detail.nanometersToOffsetPixels
      }
    })
Xiao Gui's avatar
Xiao Gui committed
    const foundParcellation = parcellation
      && template?.parcellations?.find(p => parcellation.name === p.name)
    this.loadParc(foundParcellation || template.parcellations[0])

    const nehubaConfig = template.nehubaConfig
    const initialSpec = nehubaConfig.dataset.initialNgState
    const {layers} = initialSpec

    const dispatchLayers = Object.keys(layers).map(key => {
      const layer = {
        name : key,
        source : layers[key].source,
        mixability : layers[key].type === 'image'
          ? 'base'
          : 'mixable',
        visible : typeof layers[key].visible === 'undefined'
          ? true
          : layers[key].visible,
        transform : typeof layers[key].transform === 'undefined'
          ? null
          : layers[key].transform,
      }
      this.ngLayersRegister.layers.push(layer)
      return layer
    })

Xiao Gui's avatar
Xiao Gui committed
    this.newViewer$.next(true)
    this.store$.dispatch(ngViewerActionAddNgLayer({
Xiao Gui's avatar
Xiao Gui committed
      layer: dispatchLayers
    }))
  }

  @Output()
  public viewerEvent = new EventEmitter<TViewerEvent>()

Xiao Gui's avatar
Xiao Gui committed
  constructor(
Xiao Gui's avatar
Xiao Gui committed
    private store$: Store<any>,
    private el: ElementRef,
    private log: LoggingService,
    @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.viewerEvent.emit({
Xiao Gui's avatar
Xiao Gui committed
      type: 'MOUSEOVER_ANNOTATION',
      data: {}
    })
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 layout change
     */
    const redrawLayoutSub = combineLatest([
      this.store$.pipe(
        select(ngViewerSelectorPanelMode),
        distinctUntilChanged(),
        shareReplay(1),
      ),
      this.panelOrder$,
    ]).pipe(
      switchMap(this.waitForNehuba.bind(this))
    ).subscribe(([mode, panelOrder]) => {
      const viewPanels = panelOrder.split('').map(v => Number(v)).map(idx => this.viewPanels[idx]) as [HTMLElement, HTMLElement, HTMLElement, HTMLElement]

      /**
       * TODO smarter with event stream
       */
fsdavid's avatar
fsdavid committed
      if (!viewPanels.every(v => !!v)) {
Xiao Gui's avatar
Xiao Gui committed
        this.log.error(`on relayout, not every view panel is populated. This should not occur!`)
        return
      }

      switch (mode) {
      case PANELS.H_ONE_THREE: {
        const element = this.removeExistingPanels()
        const newEl = getHorizontalOneThree(viewPanels)
        element.appendChild(newEl)
        break;
      }
      case PANELS.V_ONE_THREE: {
        const element = this.removeExistingPanels()
        const newEl = getVerticalOneThree(viewPanels)
        element.appendChild(newEl)
        break;
      }
      case PANELS.FOUR_PANEL: {
        const element = this.removeExistingPanels()
        const newEl = getFourPanel(viewPanels)
        element.appendChild(newEl)
        break;
      }
      case PANELS.SINGLE_PANEL: {
        const element = this.removeExistingPanels()
        const newEl = getSinglePanel(viewPanels)
        element.appendChild(newEl)
        break;
      }
      default:
      }
      for (const panel of viewPanels) {
        (panel as HTMLElement).classList.add('neuroglancer-panel')
      }

      // TODO needed to redraw?
      // see https://trello.com/c/oJOnlc6v/60-enlarge-panel-allow-user-rearrange-panel-position
      // further investigaation required
      this.nehubaContainerDirective.nehubaViewerInstance.redraw()
    })
    this.onDestroyCb.push(() => redrawLayoutSub.unsubscribe())

    /**
     * on hover segment
     */
    const onhovSegSub = this.store$.pipe(
      select(uiStateMouseOverSegmentsSelector),
      distinctUntilChanged(),
    ).subscribe(arr => {
      const segments = arr.map(({ segment }) => segment).filter(v => !!v)
      this.onhoverSegments = segments
    })
    this.onDestroyCb.push(() => onhovSegSub.unsubscribe())

    /**
     * subscribe to when ngLayer gets updated, and add/remove layer as necessary
     */
    const addRemoveAdditionalLayerSub = this.store$.pipe(
      select(ngViewerSelectorLayers),
      switchMap(this.waitForNehuba.bind(this)),
    ).subscribe((ngLayers: INgLayerInterface[]) => {

      const newLayers = ngLayers.filter(l => this.ngLayersRegister.layers?.findIndex(ol => ol.name === l.name) < 0)
      const removeLayers = this.ngLayersRegister.layers.filter(l => ngLayers?.findIndex(nl => nl.name === l.name) < 0)
Xiao Gui's avatar
Xiao Gui committed
      if (newLayers?.length > 0) {
        const newLayersObj: any = {}
        newLayers.forEach(({ name, source, ...rest }) => newLayersObj[name] = {
          ...rest,
          source,
          // source: getProxyUrl(source),
          // ...getProxyOther({source})
        })

        this.nehubaContainerDirective.nehubaViewerInstance.loadLayer(newLayersObj)

        /**
         * previous miplementation... if nehubaViewer has not yet be instantiated, add it to the queue
         * may no longer be necessary
         * or implement proper queue'ing rather than ... this... half assed queue'ing
         */
        // if (!this.nehubaViewer.nehubaViewer || !this.nehubaViewer.nehubaViewer.ngviewer) {
        //   this.nehubaViewer.initNiftiLayers.push(newLayersObj)
        // } else {
        //   this.nehubaViewer.loadLayer(newLayersObj)
        // }
        this.ngLayersRegister.layers = this.ngLayersRegister.layers.concat(newLayers)
      }

      if (removeLayers?.length > 0) {
        removeLayers.forEach(l => {
          if (this.nehubaContainerDirective.nehubaViewerInstance.removeLayer({
            name : l.name,
          })) {
            this.ngLayersRegister.layers = this.ngLayersRegister.layers.filter(rl => rl.name !== l.name)
          }
        })
      }
    })
    this.onDestroyCb.push(() => addRemoveAdditionalLayerSub.unsubscribe())

    /**
     * define when shown segments should be updated
     */
    const regSelectSub = combineLatest([
      /**
       * selectedRegions
       */
      this.store$.pipe(
        select(viewerStateSelectedRegionsSelector)
      ),
      /**
       * if layer contains non mixable layer
       */
      this.store$.pipe(
        select(ngViewerSelectorLayers),
        map(layers => layers.findIndex(l => l.mixability === 'nonmixable') >= 0),
      ),
      /**
       * clearviewqueue, indicating something is controlling colour map
       * show all seg
       */
      this.store$.pipe(
        select(ngViewerSelectorClearView),
        distinctUntilChanged()
      )
    ]).pipe(
      switchMap(this.waitForNehuba.bind(this)),
    ).subscribe(([ regions, nonmixableLayerExists, clearViewFlag ]) => {
      if (nonmixableLayerExists) {
        this.nehubaContainerDirective.nehubaViewerInstance.hideAllSeg()
        return
      }
      const { ngId: defaultNgId } = this.selectedParcellation || {}

      /* selectedregionindexset needs to be updated regardless of forceshowsegment */
      const selectedRegionIndexSet = new Set<string>(regions.map(({ngId = defaultNgId, labelIndex}) => serialiseParcellationRegion({ ngId, labelIndex })))

      if (selectedRegionIndexSet.size > 0 && !clearViewFlag) {
        this.nehubaContainerDirective.nehubaViewerInstance.showSegs([...selectedRegionIndexSet])
      } else {
        this.nehubaContainerDirective.nehubaViewerInstance.showAllSeg()
      }
    })
    this.onDestroyCb.push(() => regSelectSub.unsubscribe())

    const perspectiveRenderEvSub = this.newViewer$.pipe(
      switchMapTo(fromEvent<CustomEvent>(this.el.nativeElement, 'perpspectiveRenderEvent').pipe(
        take(1)
      ))
    ).subscribe(ev => {
      const perspPanel = ev.target as HTMLElement
      this.viewPanels[3] = perspPanel
      this.viewPanelWeakMap.set(perspPanel, 3)
    })
    this.onDestroyCb.push(() => perspectiveRenderEvSub.unsubscribe())

    const perspOctCtrlSub = this.customLandmarks$.pipe(
      withLatestFrom(
        this.nehubaViewerPerspectiveOctantRemoval$
      ),
      switchMap(this.waitForNehuba.bind(this))
    ).subscribe(([ landmarks, flag ]) => {
      this.nehubaContainerDirective.toggleOctantRemoval(
        landmarks.length > 0 ? false : flag
      )
      this.nehubaContainerDirective.nehubaViewerInstance.updateUserLandmarks(landmarks)
    })
    this.onDestroyCb.push(() => perspOctCtrlSub.unsubscribe())

    this.sliceRenderEvent$ = fromEvent<CustomEvent>(this.el.nativeElement, 'sliceRenderEvent')
    this.sliceViewLoadingMain$ = this.sliceRenderEvent$.pipe(
      scan(scanSliceViewRenderFn, [null, null, null]),
      startWith([true, true, true] as [boolean, boolean, boolean]),
      shareReplay(1),
    )

    this.perspectiveViewLoading$ = fromEvent(this.el.nativeElement, 'perpspectiveRenderEvent').pipe(
      filter((event: CustomEvent) => event?.detail?.lastLoadedMeshId ),
      map(event => {

        /**
         * TODO dig into event detail to see if the exact mesh loaded
         */
        const { meshesLoaded, meshFragmentsLoaded, lastLoadedMeshId } = (event as any).detail
        return meshesLoaded >= this.nehubaContainerDirective.nehubaViewerInstance.numMeshesToBeLoaded
          ? null
          : 'Loading meshes ...'
      }),
      distinctUntilChanged()
    )

    this.showPerpsectiveScreen$ = this.newViewer$.pipe(
      switchMapTo(this.sliceRenderEvent$.pipe(
        scan((acc, curr) => {

          /**
           * if at any point, all chunks have been loaded, always return loaded state
           */
          if (acc.every(v => v === 0)) return [0, 0, 0]
          const { detail = {}, target } = curr || {}
          const { missingChunks = -1, missingImageChunks = -1 } = detail
          const idx = this.findPanelIndex(target as HTMLElement)
          const returnAcc = [...acc]
          if (idx >= 0) {
            returnAcc[idx] = missingChunks + missingImageChunks
          }
          return returnAcc
        }, [-1, -1, -1]),
        map(arr => {
          let sum = 0
          let uncertain = false
          for (const num of arr) {
            if (num < 0) {
              uncertain = true
            } else {
              sum += num
            }
          }
          return sum > 0
            ? `Loading ${sum}${uncertain ? '+' : ''} chunks ...`
            : null
        }),
        distinctUntilChanged(),
        startWith('Loading ...'),
        throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
        shareReplay(1),
      ))
    )

    this.hoveredPanelIndices$ = fromEvent(this.el.nativeElement, 'mouseover').pipe(
      switchMap((ev: MouseEvent) => merge(
        of(this.findPanelIndex(ev.target as HTMLElement)),
        fromEvent(this.el.nativeElement, 'mouseout').pipe(
          mapTo(null),
        ),
      )),
      debounceTime(20),
      shareReplay(1),
    )

    const setupViewerApiSub = this.newViewer$.pipe(
      tap(() => {
        setViewerHandle && setViewerHandle(null)
Xiao Gui's avatar
Xiao Gui committed
      }),
      switchMap(this.waitForNehuba.bind(this))
    ).subscribe(() => {
      setViewerHandle && setViewerHandle({
Xiao Gui's avatar
Xiao Gui committed
        setNavigationLoc : (coord, realSpace?) => this.nehubaContainerDirective.nehubaViewerInstance.setNavigationState({
          position : coord,
          positionReal : typeof realSpace !== 'undefined' ? realSpace : true,
        }),
        /* TODO introduce animation */
        moveToNavigationLoc : (coord, realSpace?) => {
          this.store$.dispatch(
            viewerStateChangeNavigation({
              navigation: {
                position: coord,
                animation: {},
              }
            })
          )
        },
        setNavigationOri : (quat) => this.nehubaContainerDirective.nehubaViewerInstance.setNavigationState({
          orientation : quat,
        }),
        /* TODO introduce animation */
        moveToNavigationOri : (quat) => this.nehubaContainerDirective.nehubaViewerInstance.setNavigationState({
          orientation : quat,
        }),
        showSegment : (_labelIndex) => {
          /**
           * TODO reenable with updated select_regions api
           */
          this.log.warn(`showSegment is temporarily disabled`)
Xiao Gui's avatar
Xiao Gui committed
          // if(!this.selectedRegionIndexSet.has(labelIndex))
          //   this.store.dispatch({
          //     type : SELECT_REGIONS,
          //     selectRegions :  [labelIndex, ...this.selectedRegionIndexSet]
          //   })
        },
        add3DLandmarks : landmarks => {
          // TODO check uniqueness of ID
          if (!landmarks.every(l => !!l.id)) {
            throw new Error('every landmarks needs to be identified with the id field')
          }
          if (!landmarks.every(l => !!l.position)) {
            throw new Error('every landmarks needs to have position defined')
          }
          if (!landmarks.every(l => l.position.constructor === Array) || !landmarks.every(l => l.position.every(v => !isNaN(v))) || !landmarks.every(l => l.position.length == 3)) {
            throw new Error('position needs to be a length 3 tuple of numbers ')
          }
Xiao Gui's avatar
Xiao Gui committed
          this.store$.dispatch(viewerStateAddUserLandmarks({
            landmarks
          }))
        },
        remove3DLandmarks : landmarkIds => {
          this.store$.dispatch(viewreStateRemoveUserLandmarks({
            payload: { landmarkIds }
          }))
        },
        hideSegment : (_labelIndex) => {
          /**
           * TODO reenable with updated select_regions api
           */
          this.log.warn(`hideSegment is temporarily disabled`)
Xiao Gui's avatar
Xiao Gui committed
          // if(this.selectedRegionIndexSet.has(labelIndex)){
          //   this.store.dispatch({
          //     type :SELECT_REGIONS,
          //     selectRegions : [...this.selectedRegionIndexSet].filter(num=>num!==labelIndex)
          //   })
          // }
        },
        showAllSegments : () => {
          const selectRegionIds = []
          this.multiNgIdsRegionsLabelIndexMap.forEach((map, ngId) => {
            Array.from(map.keys()).forEach(labelIndex => {
              selectRegionIds.push(serialiseParcellationRegion({ ngId, labelIndex }))
            })
          })
          this.store$.dispatch(viewerStateSelectRegionWithIdDeprecated({
            selectRegionIds
          }))
        },
        hideAllSegments : () => {
          this.store$.dispatch(viewerStateSelectRegionWithIdDeprecated({
            selectRegionIds: []
          }))
        },
        segmentColourMap : new Map(),
        getLayersSegmentColourMap: () => {
          const newMainMap = new Map()
          for (const [key, colormap] of this.nehubaContainerDirective.nehubaViewerInstance.multiNgIdColorMap.entries()) {
            const newColormap = new Map()
            newMainMap.set(key, newColormap)
Xiao Gui's avatar
Xiao Gui committed
            for (const [lableIndex, entry] of colormap.entries()) {
              newColormap.set(lableIndex, JSON.parse(JSON.stringify(entry)))
            }
          }
          return newMainMap
        },
        applyColourMap : (_map) => {
          throw new Error(`apply color map has been deprecated. use applyLayersColourMap instead`)
        },
        applyLayersColourMap: (map) => {
          this.nehubaContainerDirective.nehubaViewerInstance.setColorMap(map)
        },
        loadLayer : (layerObj) => this.nehubaContainerDirective.nehubaViewerInstance.loadLayer(layerObj),
        removeLayer : (condition) => this.nehubaContainerDirective.nehubaViewerInstance.removeLayer(condition),
        setLayerVisibility : (condition, visible) => this.nehubaContainerDirective.nehubaViewerInstance.setLayerVisibility(condition, visible),
        mouseEvent : merge(
          fromEvent(this.el.nativeElement, 'click').pipe(
            map((ev: MouseEvent) => ({eventName : 'click', event: ev})),
          ),
          fromEvent(this.el.nativeElement, 'mousemove').pipe(
            map((ev: MouseEvent) => ({eventName : 'mousemove', event: ev})),
          ),
          /**
           * neuroglancer prevents propagation, so use capture instead
           */
          Observable.create(observer => {
            this.el.nativeElement.addEventListener('mousedown', event => observer.next({eventName: 'mousedown', event}), true)
          }) as Observable<{eventName: string, event: MouseEvent}>,
          fromEvent(this.el.nativeElement, 'mouseup').pipe(
            map((ev: MouseEvent) => ({eventName : 'mouseup', event: ev})),
          ),
        ) ,
        mouseOverNehuba : of(null).pipe(
          tap(() => console.warn('mouseOverNehuba observable is becoming deprecated. use mouseOverNehubaLayers instead.')),
        ),
        mouseOverNehubaLayers: this.mouseoverDirective.currentOnHoverObs$.pipe(
          map(({ segments }) => segments)
        ),
        mouseOverNehubaUI: this.mouseoverDirective.currentOnHoverObs$.pipe(
          map(({ landmark, segments, userLandmark: customLandmark }) => ({ segments, landmark, customLandmark })),
          shareReplay(1),
        ),
        getNgHash : this.nehubaContainerDirective.nehubaViewerInstance.getNgHash,
      })
    })
    this.onDestroyCb.push(() => setupViewerApiSub.unsubscribe())
Xiao Gui's avatar
Xiao Gui committed
    // listen to navigation change from store
    const navSub = this.store$.pipe(
      select(viewerStateNavigationStateSelector)
    ).subscribe(nav => this.navigation = nav)
    this.onDestroyCb.push(() => navSub.unsubscribe())
Xiao Gui's avatar
Xiao Gui committed
  }

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

Xiao Gui's avatar
Xiao Gui committed
  private selectHoveredRegion(ev: any, next: Function){
    /**
     * 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 next()
Xiao Gui's avatar
Xiao Gui committed
    this.store$.dispatch(
      viewerStateSetSelectedRegions({
        selectRegions: trueOnhoverSegments.slice(0, 1)
Xiao Gui's avatar
Xiao Gui committed
  private waitForNehuba(arg: unknown) {
Xiao Gui's avatar
Xiao Gui committed
    return interval(16).pipe(
      filter(() => !!(this.nehubaContainerDirective?.isReady())),
      take(1),
      mapTo(arg),
    )
  }

  public setOctantRemoval(octantRemovalFlag: boolean) {
    this.store$.dispatch(
      ngViewerActionSetPerspOctantRemoval({
        octantRemovalFlag
      })
    )
  }

  public toggleMaximiseMinimise(index: number) {
    this.store$.dispatch(ngViewerActionToggleMax({
      payload: { index }
    }))
  }

  public zoomNgView(panelIndex: number, factor: number) {
    const ngviewer = this.nehubaContainerDirective?.nehubaViewerInstance?.nehubaViewer?.ngviewer
    if (!ngviewer) throw new Error(`ngviewer not defined!`)

    /**
     * panelIndex < 3 === slice view
     */
    if (panelIndex < 3) {
      /**
       * factor > 1 === zoom out
       */
      ngviewer.navigationState.zoomBy(factor)
    } else {
      ngviewer.perspectiveNavigationState.zoomBy(factor)
    }
  }

  private removeExistingPanels() {
    const element = this.nehubaContainerDirective.nehubaViewerInstance.nehubaViewer.ngviewer.layout.container.componentValue.element as HTMLElement
    while (element.childElementCount > 0) {
      element.removeChild(element.firstElementChild)
    }
    return element
  }


  public returnTruePos(quadrant: number, data: any) {
    const pos = quadrant > 2
      ? [0, 0, 0]
      : this.nanometersToOffsetPixelsFn && this.nanometersToOffsetPixelsFn[quadrant]
        ? this.nanometersToOffsetPixelsFn[quadrant](data.geometry.position.map(n => n * 1e6))
        : [0, 0, 0]
    return pos
  }

  public getPositionX(quadrant: number, data: any) {
    return this.returnTruePos(quadrant, data)[0]
  }
  public getPositionY(quadrant: number, data: any) {
    return this.returnTruePos(quadrant, data)[1]
  }
  public getPositionZ(quadrant: number, data: any) {
    return this.returnTruePos(quadrant, data)[2]
  }

  public handleMouseEnterCustomLandmark(lm) {
    this.store$.dispatch(
      viewerStateMouseOverCustomLandmark({
        payload: { userLandmark: lm }
      })
    )
  }

  public handleMouseLeaveCustomLandmark(lm) {
    this.store$.dispatch(
      viewerStateMouseOverCustomLandmark({
        payload: { userLandmark: null }
      })
    )
  }