Skip to content
Snippets Groups Projects
nehubaViewerGlue.component.ts 28.4 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";

Xiao Gui's avatar
Xiao Gui committed
import { getNgIds, getMultiNgIdsRegionsLabelIndexMap } from "../constants";
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";
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'
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> = []

  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)
        : {}
      
      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 })
Xiao Gui's avatar
Xiao Gui committed
      this.onDestroyCb.push(() => deregister(selOnhoverRegion)) 
    }

    /**
     * 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
       */
      if (!viewPanels.every(v => !!v)) { 
        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)
      
      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`)
  
          // 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 ')
          }
  
          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`)
  
          // 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)
  
            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 }
      })
    )
  }

Xiao Gui's avatar
Xiao Gui committed
}