Skip to content
Snippets Groups Projects
nehubaViewer.component.ts 31.1 KiB
Newer Older
import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optional } from "@angular/core";
Xiao Gui's avatar
Xiao Gui committed
import { Subscription, BehaviorSubject, Observable, Subject, of, interval, combineLatest } from 'rxjs'
import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce, map } from "rxjs/operators";
import { LoggingService } from "src/logging";
import { bufferUntil, getExportNehuba, getUuid, switchMapWaitFor } from "src/util/fn";
import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util";
Xiao Gui's avatar
Xiao Gui committed
import { arrayOrderedEql, rgbToHex } from 'common/util'
import { IMeshesToLoad, SET_MESHES_TO_LOAD, PERSPECTIVE_ZOOM_FUDGE_FACTOR } from "../constants";
import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
Xiao Gui's avatar
Xiao Gui committed
/**
 * import of nehuba js files moved to angular.json
 */
Xiao Gui's avatar
Xiao Gui committed
import { EXTERNAL_LAYER_CONTROL, IExternalLayerCtl, INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util";
Xiao Gui's avatar
Xiao Gui committed
import { NgCoordinateSpace, Unit } from "../types";
import { PeriodicSvc } from "src/util/periodic.service";
import { ViewerInternalStateSvc, AUTO_ROTATE } from "src/viewerModule/viewerInternalState.service";
Xiao Gui's avatar
Xiao Gui committed

function translateUnit(unit: Unit) {
  if (unit === "m") {
    return 1e9
  }

  throw new Error(`Cannot translate unit: ${unit}`)
}
export const IMPORT_NEHUBA_INJECT_TOKEN = `IMPORT_NEHUBA_INJECT_TOKEN`

interface LayerLabelIndex {
Xiao Gui's avatar
Xiao Gui committed
  layer: {
Xiao Gui's avatar
Xiao Gui committed
    name: string
  }

  labelIndicies: number[]
}

export const scanFn = (acc: LayerLabelIndex[], curr: LayerLabelIndex) => {
  const found = acc.find(layerLabelIndex => {
    return layerLabelIndex.layer.name === curr.layer.name
  })
  if (!found) {
    return [ ...acc, curr ]
Xiao Gui's avatar
Xiao Gui committed
  }
  return acc.map(layerLabelIndex => {
    return layerLabelIndex.layer.name === curr.layer.name
      ? curr
      : layerLabelIndex
  })
/**
 * no selector is needed, as currently, nehubaviewer is created dynamically
 */
Xiao Gui's avatar
Xiao Gui committed
@Component({
  templateUrl : './nehubaViewer.template.html',
  styleUrls : [
Xiao Gui's avatar
Xiao Gui committed
    './nehubaViewer.style.css',
  ],
Xiao Gui's avatar
Xiao Gui committed
})

export class NehubaViewerUnit implements OnDestroy {
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
  #translateVoxelToReal: (voxels: number[]) => number[]
Xiao Gui's avatar
Xiao Gui committed

  public ngIdSegmentsMap: Record<string, number[]> = {}

Xiao Gui's avatar
Xiao Gui committed
  public viewerPosInVoxel$ = new BehaviorSubject<number[]>(null)
  public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null)
  public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null)
  public mousePosInReal$ = new BehaviorSubject(null)

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

  private subscriptions: Subscription[] = []
Xiao Gui's avatar
Xiao Gui committed

  private _nehubaReady = false
Xiao Gui's avatar
Xiao Gui committed
  @Output() public nehubaReady: EventEmitter<null> = new EventEmitter()
  @Output() public layersChanged: EventEmitter<null> = new EventEmitter()
  private layersChangedHandler: any
Xiao Gui's avatar
Xiao Gui committed
  @Output() public viewerPositionChange: EventEmitter<{ orientation: number[], perspectiveOrientation: number[], perspectiveZoom: number, zoom: number, position: number[], positionReal?: boolean }> = new EventEmitter()
Xiao Gui's avatar
Xiao Gui committed
  @Output() public mouseoverSegmentEmitter:
    EventEmitter<{
Xiao Gui's avatar
Xiao Gui committed
      segmentId: number | null
Xiao Gui's avatar
Xiao Gui committed
      layer: {
Xiao Gui's avatar
Xiao Gui committed
        name?: string
        url?: string
      }
    }> = new EventEmitter()
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
  @Output() public regionSelectionEmitter: EventEmitter<{
    segment: number
Xiao Gui's avatar
Xiao Gui committed
    layer: {
      name?: string
Xiao Gui's avatar
Xiao Gui committed
      url?: string
  }}> = new EventEmitter()
Xiao Gui's avatar
Xiao Gui committed
  @Output() public errorEmitter: EventEmitter<any> = new EventEmitter()
Xiao Gui's avatar
Xiao Gui committed
  @Output() public totalMeshesToLoad = new EventEmitter<number>()
  /* only used to set initial navigation state */
Xiao Gui's avatar
Xiao Gui committed
  public initNav: any
Xiao Gui's avatar
Xiao Gui committed
  public config: any
  public nehubaViewer: any
  private _dim: [number, number, number]
Xiao Gui's avatar
Xiao Gui committed
  get dim() {
    return this._dim
      ? this._dim
      : [1.5e9, 1.5e9, 1.5e9]
  }
Xiao Gui's avatar
Xiao Gui committed
  #newViewerSubs: { unsubscribe: () => void }[] = []
Xiao Gui's avatar
Xiao Gui committed
  public ondestroySubscriptions: Subscription[] = []
  public nehubaLoaded: boolean = false

Xiao Gui's avatar
Xiao Gui committed
  #triggerMeshLoad$ = new BehaviorSubject(null)
Xiao Gui's avatar
Xiao Gui committed
  multplier = new Float32Array(1)

Xiao Gui's avatar
Xiao Gui committed
    public elementRef: ElementRef,
    private log: LoggingService,
    private periodicSvc: PeriodicSvc,
Xiao Gui's avatar
Xiao Gui committed
    @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise<any>,
    @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject<NehubaViewerUnit>,
    @Optional() @Inject(SET_MESHES_TO_LOAD) private injSetMeshesToLoad$: Observable<IMeshesToLoad>,
    @Optional() @Inject(SET_COLORMAP_OBS) private setColormap$: Observable<IColorMap>,
    @Optional() @Inject(SET_LAYER_VISIBILITY) private layerVis$: Observable<string[]>,
    @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>,
    @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>,
Xiao Gui's avatar
Xiao Gui committed
    @Optional() @Inject(Z_TRAVERSAL_MULTIPLIER) multiplier$: Observable<number>,
Xiao Gui's avatar
Xiao Gui committed
    @Optional() @Inject(EXTERNAL_LAYER_CONTROL) private externalLayerCtrl: IExternalLayerCtl,
    @Optional() intViewerStateSvc: ViewerInternalStateSvc,
Xiao Gui's avatar
Xiao Gui committed
  ) {
Xiao Gui's avatar
Xiao Gui committed
    if (multiplier$) {
      this.ondestroySubscriptions.push(
        multiplier$.subscribe(val => this.multplier[0] = val)
      )
    } else {
      this.multplier[0] = 1
    }
    if (intViewerStateSvc) {
      let raqRef: number
      const {
        done,
        next,
      } = intViewerStateSvc.registerEmitter({
        "@type": "TViewerInternalStateEmitter",
        viewerType: "nehuba",
        applyState: arg => {
          
          if (arg.viewerType === AUTO_ROTATE) {
            if (raqRef) {
              cancelAnimationFrame(raqRef)
            }
            const autoPlayFlag = (arg.payload as any).play
            const reverseFlag = (arg.payload as any).reverse
            const autoplaySpeed = (arg.payload as any).speed
            
            if (!autoPlayFlag) {
              return
            }
            const deg = (reverseFlag ? 1 : -1) * autoplaySpeed * 1e-3
            const animate = () => {
              raqRef = requestAnimationFrame(() => {
                animate()
              })
              const perspectivePose = this.nehubaViewer?.ngviewer?.perspectiveNavigationState?.pose
              if (!perspectivePose) {
                return
              }
              perspectivePose.rotateAbsolute([0, 0, 1], deg, [0, 0, 0])
            }

            animate()
            return
          }
        }
      })

      this.onDestroyCb.push(() => done())
      next({
        "@id": getUuid(),
        '@type': "TViewerInternalStateEmitterEvent",
        viewerType: "nehuba",
        payload: {}
      })
    }

Xiao Gui's avatar
Xiao Gui committed
    if (this.nehubaViewer$) {
      this.nehubaViewer$.next(this)
    }
Xiao Gui's avatar
Xiao Gui committed

    getImportNehubaPr()
Xiao Gui's avatar
Xiao Gui committed
      .then(() => getExportNehuba())
      .then(exportNehuba => {
        this.nehubaLoaded = true
Xiao Gui's avatar
Xiao Gui committed
        this.exportNehuba = exportNehuba
Xiao Gui's avatar
Xiao Gui committed
        const fixedZoomPerspectiveSlices = this.config && this.config.layout && this.config.layout.useNehubaPerspective && this.config.layout.useNehubaPerspective.fixedZoomPerspectiveSlices
        if (fixedZoomPerspectiveSlices) {
          const { sliceZoom, sliceViewportWidth, sliceViewportHeight } = fixedZoomPerspectiveSlices
          const dim = Math.min(sliceZoom * sliceViewportWidth, sliceZoom * sliceViewportHeight)
          this._dim = [dim, dim, dim]
        }
        this.patchNG()
        this.loadNehuba()
        const viewer = this.nehubaViewer.ngviewer
Xiao Gui's avatar
Xiao Gui committed

        this.layersChangedHandler = viewer.layerManager.readyStateChanged.add(() => {
          this.layersChanged.emit(null)
Xiao Gui's avatar
Xiao Gui committed
          const readiedLayerNames: string[] = viewer.layerManager.managedLayers.filter(l => l.isReady()).map(l => l.name)
          for (const layerName in this.ngIdSegmentsMap) {
            if (!readiedLayerNames.includes(layerName)) {
              return
            }
          }
          this._nehubaReady = true
          this.nehubaReady.emit(null)
        })
        viewer.registerDisposer(this.layersChangedHandler)
      })
      .catch(e => this.errorEmitter.emit(e))
    if (this.setColormap$) {
      this.ondestroySubscriptions.push(
        this.setColormap$.pipe(
Xiao Gui's avatar
Xiao Gui committed
          switchMap(switchMapWaitFor({
            condition: () => this._nehubaReady
Xiao Gui's avatar
Xiao Gui committed
          })),
          debounceTime(160),
        ).subscribe(v => {
          const map = new Map()
          for (const key in v) {
            const m = new Map()
            map.set(key, m)
            for (const lblIdx in v[key]) {
              m.set(lblIdx, v[key][lblIdx])
            }
          }
          this.setColorMap(map)
        })
      )
    } else {
      this.log.error(`SET_COLORMAP_OBS not provided`)
    }

    if (this.layerVis$) {
      this.ondestroySubscriptions.push(
        this.layerVis$.pipe(
          switchMap(switchMapWaitFor({
            condition: () => this._nehubaReady
          })),
          distinctUntilChanged(arrayOrderedEql),
          debounceTime(160),
        ).subscribe((layerNames: string[]) => {
          /**
           * debounce 160ms to set layer invisible etc
           * on switch from freesurfer -> volumetric viewer, race con results in managed layer not necessarily setting layer visible correctly
           */
          const managedLayers = this.nehubaViewer.ngviewer.layerManager.managedLayers
Xiao Gui's avatar
Xiao Gui committed
          managedLayers.forEach(layer => {
            if (this.externalLayerCtrl && this.externalLayerCtrl.ExternalLayerNames.has(layer.name)) {
              return
            }
            layer.setVisible(false)
          })
          for (const layerName of layerNames) {
            const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
            if (layer) {
              layer.setVisible(true)
            } else {
              this.log.log('layer unavailable', layerName)
            }
          }
        })
      )
    } else {
      this.log.error(`SET_LAYER_VISIBILITY not provided`)
    }

    if (this.segVis$) {
      this.ondestroySubscriptions.push(
Xiao Gui's avatar
Xiao Gui committed
        this.segVis$.pipe(
          switchMap(
            switchMapWaitFor({
              condition: () => this._nehubaReady,
Xiao Gui's avatar
Xiao Gui committed
              leading: true,
            })
          )
        ).subscribe(val => {
          // null === hide all seg
          if (val === null) {
            this.hideAllSeg()
            return
          }
          // empty array === show all seg
          if (val.length === 0) {
            this.showAllSeg()
            return
          }
          // show limited seg
          this.showSegs(val)
        })
      )
    } else {
      this.log.error(`SET_SEGMENT_VISIBILITY not provided`)
    }

    if (this.layerCtrl$) {
      this.ondestroySubscriptions.push(
        this.layerCtrl$.pipe(
          bufferUntil(({
            condition: () => this._nehubaReady
          }))
        ).subscribe(messages => {
          for (const message of messages) {
            if (message.type === 'add') {
              const p = message as TNgLayerCtrl<'add'>
              this.loadLayer(p.payload)
            }
            if (message.type === 'remove') {
              const p = message as TNgLayerCtrl<'remove'>
              for (const name of p.payload.names){
                this.removeLayer({ name })
              }
            }
            if (message.type === 'update') {
              const p = message as TNgLayerCtrl<'update'>
              this.updateLayer(p.payload)
            }
Xiao Gui's avatar
Xiao Gui committed
            if (message.type === 'setLayerTransparency') {
              const p = message as TNgLayerCtrl<'setLayerTransparency'>
              for (const key in p.payload) {
                this.setLayerTransparency(key, p.payload[key])
              }
            }
            if (message.type === "updateShader") {
              const p = message as TNgLayerCtrl<'updateShader'>
              for (const key in p.payload) {
                this.setLayerShader(key, p.payload[key])
              }
            }
          }
        })
      )
    } else {
      this.log.error(`NG_LAYER_CONTROL not provided`)
    }
    if (this.injSetMeshesToLoad$) {
      this.subscriptions.push(
Xiao Gui's avatar
Xiao Gui committed
        combineLatest([
          this.#triggerMeshLoad$,
          this.injSetMeshesToLoad$.pipe(
            scan(scanFn, []),
          ),
        ]).pipe(
          map(([_, val]) => val),
          debounce(() => this._nehubaReady
            ? of(true)
            : interval(160).pipe(
Xiao Gui's avatar
Xiao Gui committed
              filter(() => this._nehubaReady),
              take(1),
            )
          ),
        ).subscribe(layersLabelIndex => {
          let totalMeshes = 0
          for (const layerLayerIndex of layersLabelIndex) {
            const { layer, labelIndicies } = layerLayerIndex
            totalMeshes += labelIndicies.length
            this.nehubaViewer.setMeshesToLoad(labelIndicies, layer)
          }
Xiao Gui's avatar
Xiao Gui committed
          this.totalMeshesToLoad.emit(totalMeshes)
        }),
      )
    } else {
      this.log.error(`SET_MESHES_TO_LOAD not provided`)
    }
  }
Xiao Gui's avatar
Xiao Gui committed
  public applyGpuLimit(gpuLimit: number) {
    if (gpuLimit && this.nehubaViewer) {
      const limit = this.nehubaViewer.ngviewer.state.children.get('gpuMemoryLimit')
      if (limit && limit.restoreState) {
        limit.restoreState(gpuLimit)
      }
    }
  }

Xiao Gui's avatar
Xiao Gui committed
  private _multiNgIdColorMap: Map<string, Map<number, {red: number, green: number, blue: number}>>
  get multiNgIdColorMap() {
    return this._multiNgIdColorMap
  }

  set multiNgIdColorMap(val) {
    this._multiNgIdColorMap = val
  }

  public mouseOverSegment: number | null
Xiao Gui's avatar
Xiao Gui committed
  public mouseOverLayer: {name: string, url: string}| null
Xiao Gui's avatar
Xiao Gui committed
  public getNgHash: () => string = () => this.exportNehuba
    ? this.exportNehuba.getNgHash()
Xiao Gui's avatar
Xiao Gui committed
  public loadNehuba() {
Xiao Gui's avatar
Xiao Gui committed
    const { createNehubaViewer } = this.exportNehuba

    this.nehubaViewer = createNehubaViewer(this.config, (err: string) => {
      /* print in debug mode */
Xiao Gui's avatar
Xiao Gui committed
    });

    (window as any).nehubaViewer = this.nehubaViewer
    const viewer = window['viewer']

    /**
     * Hide all layers except the base layer (template)
     * Then show the layers referenced in multiNgIdLabelIndexMap
     */
Xiao Gui's avatar
Xiao Gui committed
    const patchSliceview = async () => {
      
      viewer.inputEventBindings.sliceView.set("at:wheel", "proxy-wheel")
      viewer.inputEventBindings.sliceView.set("at:control+shift+wheel", "proxy-wheel-alt")
      await (async () => {
        let lenPanels = 0

        while (lenPanels === 0) {
          lenPanels = viewer.display.panels.size
          /* creation of the layout is done on next frame, hence the settimeout */
Xiao Gui's avatar
Xiao Gui committed
          await new Promise(rs => setTimeout(rs, 150))
        }
      })()
      viewer.inputEventBindings.sliceView.set("at:wheel", "proxy-wheel-1")
      viewer.inputEventBindings.sliceView.set("at:keyp", "proxy-wheel-1")
      viewer.inputEventBindings.sliceView.set("at:keyn", "proxy-wheel-1")
Xiao Gui's avatar
Xiao Gui committed
      viewer.inputEventBindings.sliceView.set("at:control+shift+wheel", "proxy-wheel-10")
      viewer.display.panels.forEach(sliceView => patchSliceViewPanel(sliceView, this.exportNehuba, this.multplier))
    }

    viewer.inputEventBindings.sliceView.set("at:touchhold1", { action: "noop", stopPropagation: false })
    viewer.inputEventBindings.perspectiveView.set("at:touchhold1", { action: "noop", stopPropagation: false })
Xiao Gui's avatar
Xiao Gui committed
    patchSliceview()
Xiao Gui's avatar
Xiao Gui committed

    this.newViewerInit()
Xiao Gui's avatar
Xiao Gui committed
    window['nehubaViewer'] = this.nehubaViewer
Xiao Gui's avatar
Xiao Gui committed
    this.onDestroyCb.push(() => {
      window['nehubaViewer'] = null
    })
Xiao Gui's avatar
Xiao Gui committed
  public ngOnDestroy() {
Xiao Gui's avatar
Xiao Gui committed
    if (this.nehubaViewer$) {
      this.nehubaViewer$.next(null)
    }
Xiao Gui's avatar
Xiao Gui committed
    while (this.subscriptions.length > 0) {
      this.subscriptions.pop().unsubscribe()
    }
Xiao Gui's avatar
Xiao Gui committed
    while (this.#newViewerSubs.length > 0) {
      this.#newViewerSubs.pop().unsubscribe()
    }
    
    this.ondestroySubscriptions.forEach(s => s.unsubscribe())
Xiao Gui's avatar
Xiao Gui committed
    while (this.onDestroyCb.length > 0) {
      this.onDestroyCb.pop()()
    }
    this.nehubaViewer && this.nehubaViewer.dispose()
Xiao Gui's avatar
Xiao Gui committed
  private onDestroyCb: Array<() => void> = []

  private patchNG() {
Xiao Gui's avatar
Xiao Gui committed
    const { LayerManager, UrlHashBinding } = this.exportNehuba.getNgPatchableObj()
xgui3783's avatar
xgui3783 committed

    UrlHashBinding.prototype.setUrlHash = () => {
Xiao Gui's avatar
Xiao Gui committed
      // this.log.log('seturl hash')
      // this.log.log('setting url hash')
xgui3783's avatar
xgui3783 committed
    }

    UrlHashBinding.prototype.updateFromUrlHash = () => {
Xiao Gui's avatar
Xiao Gui committed
      // this.log.log('update hash binding')
xgui3783's avatar
xgui3783 committed
    }
Xiao Gui's avatar
Xiao Gui committed

xgui3783's avatar
xgui3783 committed
    /* TODO find a more permanent fix to disable double click */
    LayerManager.prototype.invokeAction = (arg) => {

      /**
       * The emitted value does not affect the region selection
       * the region selection is taken care of in nehubaContainer
       */
Xiao Gui's avatar
Xiao Gui committed
      
      if (arg === 'select') {
Xiao Gui's avatar
Xiao Gui committed
        this.regionSelectionEmitter.emit({
          segment: this.mouseOverSegment,
          layer: this.mouseOverLayer
        })
xgui3783's avatar
xgui3783 committed
      }
    }
Xiao Gui's avatar
Xiao Gui committed
    /* eslint-disable-next-line @typescript-eslint/no-empty-function */
    this.onDestroyCb.push(() => LayerManager.prototype.invokeAction = (_arg) => { /** in default neuroglancer, this function is invoked when selection occurs */ })
xgui3783's avatar
xgui3783 committed
  }

Xiao Gui's avatar
Xiao Gui committed
  private filterLayers(l: any, layerObj: any): boolean {
    /**
     * if selector is an empty object, select all layers
     */
Xiao Gui's avatar
Xiao Gui committed
    return layerObj instanceof Object && Object.keys(layerObj).every(key =>
      /**
       * the property described by the selector must exist and ...
       */
Xiao Gui's avatar
Xiao Gui committed
      !!l[key] &&
        /**
         * if the selector is regex, test layer property
         */
        ( layerObj[key] instanceof RegExp
          ? layerObj[key].test(l[key])
          /**
           * if selector is string, test for strict equality
           */
          : typeof layerObj[key] === 'string'
            ? layerObj[key] === l[key]
            /**
             * otherwise do not filter
             */
Xiao Gui's avatar
Xiao Gui committed
            : false
        ),
Xiao Gui's avatar
Xiao Gui committed
    )
Xiao Gui's avatar
Xiao Gui committed
  public setLayerVisibility(condition: {name: string|RegExp}, visible: boolean) {
    if (!this.nehubaViewer) {
Xiao Gui's avatar
Xiao Gui committed
    }
Xiao Gui's avatar
Xiao Gui committed
    const viewer = this.nehubaViewer.ngviewer
    viewer.layerManager.managedLayers
Xiao Gui's avatar
Xiao Gui committed
      .filter(l => this.filterLayers(l, condition))
      .map(layer => layer.setVisible(visible))
Xiao Gui's avatar
Xiao Gui committed
  public removeLayer(layerObj: any) {
    if (!this.nehubaViewer) {
Xiao Gui's avatar
Xiao Gui committed
    }
    const viewer = this.nehubaViewer.ngviewer
Xiao Gui's avatar
Xiao Gui committed
    const removeLayer = (i) => (viewer.layerManager.removeManagedLayer(i), i.name)

    return viewer.layerManager.managedLayers
Xiao Gui's avatar
Xiao Gui committed
      .filter(l => this.filterLayers(l, layerObj))
      .map(removeLayer)
  }

Xiao Gui's avatar
Xiao Gui committed
  public loadLayer(layerObj: any) {
    const viewer = this.nehubaViewer.ngviewer
    return Object.keys(layerObj)
Xiao Gui's avatar
Xiao Gui committed
      .filter(key =>
        /* if the layer exists, it will not be loaded */
        !viewer.layerManager.getLayerByName(key))
Xiao Gui's avatar
Xiao Gui committed
      .map(key => {
        /**
         * new implementation of neuroglancer treats swc as a mesh layer of segmentation layer
         * But it cannot *directly* be accessed by nehuba's setMeshesToLoad, since it filters by 
         * UserSegmentationLayer.
         * 
         * The below monkey patch sets the mesh to load, allow the SWC to be shown
         */
Xiao Gui's avatar
Xiao Gui committed
        let url = layerObj[key]['source']
        if (typeof url !== "string") {
          url = url['url']
        }
        const isSwc = url.includes("swc://")
        const hasSegment = (layerObj[key]["segments"] || []).length > 0
        if (isSwc && hasSegment) {
          this.periodicSvc.addToQueue(
            () => {
              const layer = viewer.layerManager.getLayerByName(key)
              if (!(layer?.layer)) {
                return false
              }
              layer.layer.displayState.visibleSegments.setMeshesToLoad([1])
              return true
            }
          )
        }
Xiao Gui's avatar
Xiao Gui committed
        const { transform=null, ...rest } = layerObj[key]

        const combined = {
          type: 'image',
Xiao Gui's avatar
Xiao Gui committed
          opacity: 1,
Xiao Gui's avatar
Xiao Gui committed
          ...rest,
          ...(transform ? { transform } : {})
        }
        viewer.layerManager.addManagedLayer(
Xiao Gui's avatar
Xiao Gui committed
          viewer.layerSpecification.getLayer(key, combined), 1)

        return layerObj[key]
      })
  }

  public updateLayer(layerObj: INgLayerCtrl['update']) {

    const viewer = this.nehubaViewer.ngviewer

    for (const layerName in layerObj) {
      const layer = viewer.layerManager.getLayerByName(layerName)
      if (!layer) continue
      const { visible } = layerObj[layerName]
      layer.setVisible(!!visible)
    }
  }

Xiao Gui's avatar
Xiao Gui committed
  public hideAllSeg() {
Xiao Gui's avatar
Xiao Gui committed
    if (!this.nehubaViewer) return
    for (const ngId in this.ngIdSegmentsMap) {
      for (const idx of this.ngIdSegmentsMap[ngId]) {
        this.nehubaViewer.hideSegment(idx, {
Xiao Gui's avatar
Xiao Gui committed
          name: ngId,
Xiao Gui's avatar
Xiao Gui committed
      }
      this.nehubaViewer.showSegment(0, {
Xiao Gui's avatar
Xiao Gui committed
        name: ngId,
Xiao Gui's avatar
Xiao Gui committed
    }
Xiao Gui's avatar
Xiao Gui committed
  public showAllSeg() {
    if (!this.nehubaViewer) { return }
Xiao Gui's avatar
Xiao Gui committed
    for (const ngId in this.ngIdSegmentsMap) {
      for (const idx of this.ngIdSegmentsMap[ngId]) {
        this.nehubaViewer.showSegment(idx, {
          name: ngId,
        })
      }
Xiao Gui's avatar
Xiao Gui committed
      this.nehubaViewer.hideSegment(0, {
        name: ngId,
Xiao Gui's avatar
Xiao Gui committed
    }
Xiao Gui's avatar
Xiao Gui committed
  public showSegs(array: (number|string)[]) {
Xiao Gui's avatar
Xiao Gui committed
    if (!this.nehubaViewer) { return }
Xiao Gui's avatar
Xiao Gui committed
    if (array.length === 0) { return }
    /**
     * TODO tobe deprecated
     */

    if (typeof array[0] === 'number') {
Xiao Gui's avatar
Xiao Gui committed
      this.log.warn(`show seg with number indices has been deprecated`)
Xiao Gui's avatar
Xiao Gui committed
    }
Xiao Gui's avatar
Xiao Gui committed
    const reduceFn: (acc: Map<string, number[]>, curr: string) => Map<string, number[]> = (acc, curr) => {

      const newMap = new Map(acc)
Xiao Gui's avatar
Xiao Gui committed
      const { ngId, label: labelIndex } = deserializeSegment(curr)
      const exist = newMap.get(ngId)
Xiao Gui's avatar
Xiao Gui committed
      if (!exist) {
        newMap.set(ngId, [Number(labelIndex)])
      } else {
        newMap.set(ngId, [...exist, Number(labelIndex)])
      }
Xiao Gui's avatar
Xiao Gui committed

    const newMap: Map<string, number[]> = array.reduce(reduceFn, new Map())

    /**
     * TODO
     * ugh, ugly code. cleanify
     */
Xiao Gui's avatar
Xiao Gui committed
     * TODO
     * sometimes, ngId still happends to be undefined
     */
    newMap.forEach((segs, ngId) => {
      this.nehubaViewer.hideSegment(0, {
Xiao Gui's avatar
Xiao Gui committed
        name: ngId,
      })
      segs.forEach(seg => {
        this.nehubaViewer.showSegment(seg, {
Xiao Gui's avatar
Xiao Gui committed
          name: ngId,
Xiao Gui's avatar
Xiao Gui committed
  private vec3(pos: number[]) {
Xiao Gui's avatar
Xiao Gui committed
    return this.exportNehuba.vec3.fromValues(...pos)
Xiao Gui's avatar
Xiao Gui committed
  public setNavigationState(newViewerState: Partial<ViewerState>) {
Xiao Gui's avatar
Xiao Gui committed
    if (!this.nehubaViewer) {
      this.log.warn('setNavigationState > this.nehubaViewer is not yet defined')
    const {
      orientation,
      perspectiveOrientation,
      perspectiveZoom,
      position,
      positionReal,
Xiao Gui's avatar
Xiao Gui committed
      zoom,
    } = newViewerState || {}
Xiao Gui's avatar
Xiao Gui committed
    if ( perspectiveZoom ) {
Xiao Gui's avatar
Xiao Gui committed
      this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom * PERSPECTIVE_ZOOM_FUDGE_FACTOR)
Xiao Gui's avatar
Xiao Gui committed
    }
    if ( zoom ) {
      this.nehubaViewer.ngviewer.navigationState.zoomFactor.restoreState(zoom)
Xiao Gui's avatar
Xiao Gui committed
    }
    if ( perspectiveOrientation ) {
      this.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.restoreState( perspectiveOrientation )
Xiao Gui's avatar
Xiao Gui committed
    }
    if ( orientation ) {
      this.nehubaViewer.ngviewer.navigationState.pose.orientation.restoreState( orientation )
Xiao Gui's avatar
Xiao Gui committed
    }
    if ( position ) {
      this.nehubaViewer.setPosition( this.vec3(position) , positionReal ? true : false )
Xiao Gui's avatar
Xiao Gui committed
    }
  public toggleOctantRemoval(flag?: boolean) {
    const ctrl = this.nehubaViewer?.ngviewer?.showPerspectiveSliceViews
    if (!ctrl) {
      this.log.error(`toggleOctantRemoval failed. this.nehubaViewer.ngviewer?.showPerspectiveSliceViews returns falsy`)
      return
    }
    const newVal = typeof flag === 'undefined'
      ? !ctrl.value
      : flag
    ctrl.restoreState(newVal)
  }

Xiao Gui's avatar
Xiao Gui committed
  private setLayerTransparency(layerName: string, alpha: number) {
    const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
    if (!(layer?.layer)) return

    /**
     * for segmentation layer
     */
    if (layer.layer.displayState) layer.layer.displayState.objectAlpha.restoreState(alpha)
    /**
     * for image layer
     */
    if (layer.layer.opacity) layer.layer.opacity.restoreState(alpha)
  private setLayerShader(layerName: string, shader: string) {
    const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
    if (layer?.layer?.fragmentMain) layer.layer.fragmentMain.restoreState(shader)
  }

  public setMeshTransparency(flag: boolean){

    /**
     * remove transparency from meshes in current layer(s)
     */
Xiao Gui's avatar
Xiao Gui committed
    for (const layerKey in this.ngIdSegmentsMap) {
      const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerKey)
      if (layer) {
        layer.layer.displayState.objectAlpha.restoreState(flag ? 0.2 : 1.0)
      }
    }
  }

  public redraw(){
    this.nehubaViewer.redraw()
  }

Xiao Gui's avatar
Xiao Gui committed
  private newViewerInit() {
Xiao Gui's avatar
Xiao Gui committed
    
    while (this.#newViewerSubs.length > 0) {
      this.#newViewerSubs.pop().unsubscribe()
    }
Xiao Gui's avatar
Xiao Gui committed
    this.#newViewerSubs.push(

      /* isn't this layer specific? */
      /* TODO this is layer specific. need a way to distinguish between different segmentation layers */
      this.nehubaViewer.mouseOver.segment.subscribe(({ segment, layer }) => {
        this.mouseOverSegment = segment
        this.mouseOverLayer = { ...layer }
Xiao Gui's avatar
Xiao Gui committed
      }),
Xiao Gui's avatar
Xiao Gui committed
      this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => {
        this.mouseoverSegmentEmitter.emit({
          layer,
          segmentId,
        })
      }),
Xiao Gui's avatar
Xiao Gui committed
      // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer
      this.nehubaViewer.navigationState.all
      .distinctUntilChanged((a, b) => {
        const {
          orientation: o1,
          perspectiveOrientation: po1,
          perspectiveZoom: pz1,
          position: p1,
Xiao Gui's avatar
Xiao Gui committed
          zoom: z1,
        } = a
        const {
          orientation: o2,
          perspectiveOrientation: po2,
          perspectiveZoom: pz2,
          position: p2,
Xiao Gui's avatar
Xiao Gui committed
          zoom: z2,
Xiao Gui's avatar
Xiao Gui committed
        return [0, 1, 2, 3].every(idx => o1[idx] === o2[idx]) &&
          [0, 1, 2, 3].every(idx => po1[idx] === po2[idx]) &&
Xiao Gui's avatar
Xiao Gui committed
          [0, 1, 2].every(idx => p1[idx] === p2[idx]) &&
      /**
       * somewhat another fudge factor
       * navigationState.all occassionally emits slice zoom and perspective zoom that maeks no sense
       * filter those out
       * 
       * TODO find out why, and perhaps inform pavel about this
       */
      .filter(val => !this.initNav && val?.perspectiveZoom > 10)
Xiao Gui's avatar
Xiao Gui committed
      .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => {

Xiao Gui's avatar
Xiao Gui committed
        this.viewerPositionChange.emit({
          orientation : Array.from(orientation),
          perspectiveOrientation : Array.from(perspectiveOrientation),
Xiao Gui's avatar
Xiao Gui committed
          perspectiveZoom: perspectiveZoom / PERSPECTIVE_ZOOM_FUDGE_FACTOR,
          position: Array.from(position),
Xiao Gui's avatar
Xiao Gui committed
          positionReal : true,
Xiao Gui's avatar
Xiao Gui committed
      }),

      this.nehubaViewer.navigationState.position.inVoxels
        .filter(v => typeof v !== 'undefined' && v !== null)
        .subscribe((v: Float32Array) => {
          const coordInVoxel = Array.from(v)
          this.viewerPosInVoxel$.next(coordInVoxel)
          if (this.#translateVoxelToReal) {
            
            const coordInReal = this.#translateVoxelToReal(coordInVoxel)
            this.viewerPosInReal$.next(coordInReal as [number, number, number])
          }
        }),
Xiao Gui's avatar
Xiao Gui committed
      this.nehubaViewer.mousePosition.inVoxels
        .filter((v: Float32Array) => typeof v !== 'undefined' && v !== null)
        .subscribe((v: Float32Array) => {
          const coordInVoxel = Array.from(v) as [number, number, number]
          this.mousePosInVoxel$.next( coordInVoxel )
          if (this.#translateVoxelToReal) {
            
            const coordInReal = this.#translateVoxelToReal(coordInVoxel)
            this.mousePosInReal$.next( coordInReal )
          }
        }),
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
    const coordSpListener = this.nehubaViewer.ngviewer.coordinateSpace.changed.add(() => {
      const coordSp = this.nehubaViewer.ngviewer.coordinateSpace.value as NgCoordinateSpace
      if (coordSp.valid) {
        this.#translateVoxelToReal = (coordInVoxel: number[]) => {
          return coordInVoxel.map((voxel, idx) => (
            translateUnit(coordSp.units[idx])
            * coordSp.scales[idx]
            * voxel
          ))
        }
      }
Xiao Gui's avatar
Xiao Gui committed
    this.nehubaViewer.ngviewer.registerDisposer(coordSpListener)

    if (this.initNav) {
      this.setNavigationState(this.initNav)
      this.initNav = null
    }
    
  private setColorMap(map: Map<string, Map<number, {red: number, green: number, blue: number}>>) {
    this.multiNgIdColorMap = map
Xiao Gui's avatar
Xiao Gui committed
    const mainDict: Record<string, Record<number, string>> = {}
    for (const [ ngId, cMap ] of map.entries()) {
Xiao Gui's avatar
Xiao Gui committed
      const nRecord: Record<number, string> = {}
      for (const [ key, cm ] of cMap.entries()) {
Xiao Gui's avatar
Xiao Gui committed
        nRecord[key] = rgbToHex([cm.red, cm.green, cm.blue])
      }
      mainDict[ngId] = nRecord

      /**
       * n.b.
       * cannot restoreState on each individual layer
       * it seems to create duplicated datasources, which eats memory, and wrecks opacity
       */
    }

    /**
     * n.b. 2
     * updating layer colormap seems to also mess up the position ()
     */

Xiao Gui's avatar
Xiao Gui committed
    const layersManager = this.nehubaViewer.ngviewer.state.children.get("layers")
    const position = this.nehubaViewer.ngviewer.state.children.get("position")
    const prevPos = position.toJSON()
Xiao Gui's avatar
Xiao Gui committed
    const layerJson = layersManager.toJSON()
    for (const layer of layerJson) {
      if (layer.name in mainDict) {
        layer['segmentColors'] = mainDict[layer.name]
Xiao Gui's avatar
Xiao Gui committed
    layersManager.restoreState(layerJson)
    position.restoreState(prevPos)
Xiao Gui's avatar
Xiao Gui committed
    this.#triggerMeshLoad$.next(null)
Xiao Gui's avatar
Xiao Gui committed
  }
Xiao Gui's avatar
Xiao Gui committed
const noop = (_event: MouseEvent) => {
  // TODO either emit contextmenu
  // or capture longtouch on higher level as contextmenu
  // at the moment, this is required to override default behavior (move to cursur location)
}

Xiao Gui's avatar
Xiao Gui committed
const patchSliceViewPanel = (sliceViewPanel: any, exportNehuba: any, mulitplier: Float32Array) => {

  // patch draw calls to dispatch viewerportToData
Xiao Gui's avatar
Xiao Gui committed
  const originalDraw = sliceViewPanel.draw
Xiao Gui's avatar
Xiao Gui committed
  sliceViewPanel.draw = function(this) {

    if (this.sliceView) {
Xiao Gui's avatar
Xiao Gui committed
      const viewportToDataEv = new CustomEvent('viewportToData', {
        bubbles: true,
        detail: {
          viewportToData : this.sliceView.invViewMatrix,
Xiao Gui's avatar
Xiao Gui committed
        },
Xiao Gui's avatar
Xiao Gui committed
      })
      this.element.dispatchEvent(viewportToDataEv)
    }

    originalDraw.call(this)
  }
Xiao Gui's avatar
Xiao Gui committed

  // patch ctrl+wheel & shift+wheel
  const { navigationState } = sliceViewPanel
  const { registerActionListener, vec3 } = exportNehuba
  const tempVec3 = vec3.create()

  for (const val of [1, 10]) {
    registerActionListener(sliceViewPanel.element, `proxy-wheel-${val}`, event => {
      const e = event.detail

      let keyDelta = null
      if (e.key === "p") {
        keyDelta = -1
      }
      if (e.key === "n") {
        keyDelta = 1
      }
Xiao Gui's avatar
Xiao Gui committed
      const offset = tempVec3
      const wheelDelta = e.deltaY !== 0 ? e.deltaY : e.deltaX
      
      const delta = keyDelta ?? wheelDelta

Xiao Gui's avatar
Xiao Gui committed
      offset[0] = 0
      offset[1] = 0
      offset[2] = (delta > 0 ? -1 : 1) * mulitplier[0] * val
      navigationState.pose.translateVoxelsRelative(offset)
    })
  }

  registerActionListener(sliceViewPanel.element, `noop`, noop)
Xiao Gui's avatar
Xiao Gui committed
export interface ViewerState {
Xiao Gui's avatar
Xiao Gui committed
  orientation: number[]
  perspectiveOrientation: number[]
Xiao Gui's avatar
Xiao Gui committed
  perspectiveZoom: number
Xiao Gui's avatar
Xiao Gui committed
  position: number[]
Xiao Gui's avatar
Xiao Gui committed
  positionReal: boolean
  zoom: number