Newer
Older
import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optional } from "@angular/core";
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, switchMapWaitFor } from "src/util/fn";
import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util";
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";
import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl } from "../layerCtrl.service/layerCtrl.util";
import { NgCoordinateSpace, Unit } from "../types";
import { PeriodicSvc } from "src/util/periodic.service";
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`
}
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 ]
return acc.map(layerLabelIndex => {
return layerLabelIndex.layer.name === curr.layer.name
? curr
: layerLabelIndex
})
/**
* no selector is needed, as currently, nehubaviewer is created dynamically
*/
@Component({
templateUrl : './nehubaViewer.template.html',
styleUrls : [
export class NehubaViewerUnit implements OnDestroy {
#translateVoxelToReal: (voxels: number[]) => number[]
public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null)
public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null)
public mousePosInReal$ = new BehaviorSubject(null)
private subscriptions: Subscription[] = []
@Output() public nehubaReady: EventEmitter<null> = new EventEmitter()
@Output() public layersChanged: EventEmitter<null> = new EventEmitter()
@Output() public viewerPositionChange: EventEmitter<{ orientation: number[], perspectiveOrientation: number[], perspectiveZoom: number, zoom: number, position: number[], positionReal?: boolean }> = new EventEmitter()
@Output() public errorEmitter: EventEmitter<any> = new EventEmitter()
/* only used to set initial navigation state */
private _dim: [number, number, number]
return this._dim
? this._dim
: [1.5e9, 1.5e9, 1.5e9]
}
#newViewerSubs: { unsubscribe: () => void }[] = []
Xiao Gui
committed
public nehubaLoaded: boolean = false
constructor(
@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>>,
if (this.nehubaViewer$) {
this.nehubaViewer$.next(this)
}
.then(() => getExportNehuba())
.then(exportNehuba => {
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
this.layersChangedHandler = viewer.layerManager.readyStateChanged.add(() => {
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(
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
managedLayers.forEach(layer => 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(
// 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(({
}))
).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)
}
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(
combineLatest([
this.#triggerMeshLoad$,
this.injSetMeshesToLoad$.pipe(
scan(scanFn, []),
),
]).pipe(
map(([_, val]) => val),
debounce(() => this._nehubaReady
? of(true)
: interval(160).pipe(
),
).subscribe(layersLabelIndex => {
let totalMeshes = 0
for (const layerLayerIndex of layersLabelIndex) {
const { layer, labelIndicies } = layerLayerIndex
totalMeshes += labelIndicies.length
this.nehubaViewer.setMeshesToLoad(labelIndicies, layer)
}
}),
)
} else {
this.log.error(`SET_MESHES_TO_LOAD not provided`)
}
}
if (gpuLimit && this.nehubaViewer) {
const limit = this.nehubaViewer.ngviewer.state.children.get('gpuMemoryLimit')
if (limit && limit.restoreState) {
limit.restoreState(gpuLimit)
}
}
}
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
public getNgHash: () => string = () => this.exportNehuba
? this.exportNehuba.getNgHash()
this.nehubaViewer = this.exportNehuba.createNehubaViewer(this.config, (err: string) => {
/* print in debug mode */
this.log.error(err)
/**
* Hide all layers except the base layer (template)
* Then show the layers referenced in multiNgIdLabelIndexMap
*/
/* creation of the layout is done on next frame, hence the settimeout */
setTimeout(() => {
window['viewer'].display.panels.forEach(patchSliceViewPanel)
this.onDestroyCb.push(() => {
window['nehubaViewer'] = null
})
if (this.nehubaViewer$) {
this.nehubaViewer$.next(null)
}
this.subscriptions.pop().unsubscribe()
}
while (this.#newViewerSubs.length > 0) {
this.#newViewerSubs.pop().unsubscribe()
}
this.ondestroySubscriptions.forEach(s => s.unsubscribe())
this.onDestroyCb.pop()()
}
this.nehubaViewer && this.nehubaViewer.dispose()
private onDestroyCb: Array<() => void> = []
private patchNG() {
const { LayerManager, UrlHashBinding } = this.exportNehuba.getNgPatchableObj()
// this.log.log('seturl hash')
// this.log.log('setting url hash')
}
UrlHashBinding.prototype.updateFromUrlHash = () => {
/* 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
*/
this.regionSelectionEmitter.emit({
segment: this.mouseOverSegment,
layer: this.mouseOverLayer
})
/* 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 */ })
/**
* if selector is an empty object, select all layers
*/
return layerObj instanceof Object && Object.keys(layerObj).every(key =>
/**
* the property described by the selector must exist and ...
*/
/**
* 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
*/
public setLayerVisibility(condition: {name: string|RegExp}, visible: boolean) {
if (!this.nehubaViewer) {
return false
const viewer = this.nehubaViewer.ngviewer
viewer.layerManager.managedLayers
.filter(l => this.filterLayers(l, condition))
.map(layer => layer.setVisible(visible))
return false
const removeLayer = (i) => (viewer.layerManager.removeManagedLayer(i), i.name)
return viewer.layerManager.managedLayers
const viewer = this.nehubaViewer.ngviewer
return Object.keys(layerObj)
/* if the layer exists, it will not be loaded */
!viewer.layerManager.getLayerByName(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
*/
const isSwc = layerObj[key]['source'].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
}
)
}
const { transform=null, ...rest } = layerObj[key]
const combined = {
type: 'image',
...rest,
...(transform ? { transform } : {})
}
console.log(combined)
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)
}
}
if (!this.nehubaViewer) return
for (const ngId in this.ngIdSegmentsMap) {
for (const idx of this.ngIdSegmentsMap[ngId]) {
this.nehubaViewer.hideSegment(idx, {
this.nehubaViewer.showSegment(0, {
for (const ngId in this.ngIdSegmentsMap) {
for (const idx of this.ngIdSegmentsMap[ngId]) {
this.nehubaViewer.showSegment(idx, {
name: ngId,
})
}
Xiao Gui
committed
this.hideAllSeg()
Xiao Gui
committed
/**
* TODO tobe deprecated
*/
if (typeof array[0] === 'number') {
const reduceFn: (acc: Map<string, number[]>, curr: string) => Map<string, number[]> = (acc, curr) => {
if (!exist) {
newMap.set(ngId, [Number(labelIndex)])
} else {
newMap.set(ngId, [...exist, Number(labelIndex)])
}
const newMap: Map<string, number[]> = array.reduce(reduceFn, new Map())
/**
* TODO
* ugh, ugly code. cleanify
*/
* sometimes, ngId still happends to be undefined
*/
newMap.forEach((segs, ngId) => {
this.nehubaViewer.hideSegment(0, {
})
segs.forEach(seg => {
this.nehubaViewer.showSegment(seg, {
this.log.warn('setNavigationState > this.nehubaViewer is not yet defined')
Xiao Gui
committed
const {
orientation,
perspectiveOrientation,
perspectiveZoom,
position,
positionReal,
Xiao Gui
committed
this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom * PERSPECTIVE_ZOOM_FUDGE_FACTOR)
Xiao Gui
committed
this.nehubaViewer.ngviewer.navigationState.zoomFactor.restoreState(zoom)
Xiao Gui
committed
this.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.restoreState( perspectiveOrientation )
Xiao Gui
committed
this.nehubaViewer.ngviewer.navigationState.pose.orientation.restoreState( orientation )
this.nehubaViewer.setPosition( this.vec3(position) , positionReal ? true : false )
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)
}
private setLayerTransparency(layerName: string, alpha: number) {
const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
/**
* 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)
*/
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()
}
while (this.#newViewerSubs.length > 0) {
this.#newViewerSubs.pop().unsubscribe()
}
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 }
this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => {
this.mouseoverSegmentEmitter.emit({
layer,
segmentId,
})
}),
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,
} = a
const {
orientation: o2,
perspectiveOrientation: po2,
perspectiveZoom: pz2,
position: p2,
} = b
return [0, 1, 2, 3].every(idx => o1[idx] === o2[idx]) &&
[0, 1, 2, 3].every(idx => po1[idx] === po2[idx]) &&
pz1 === pz2 &&
z1 === z2
})
/**
* 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)
.subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => {
orientation : Array.from(orientation),
perspectiveOrientation : Array.from(perspectiveOrientation),
}),
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
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 )
}
}),
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
))
}
}
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}>>) {
for (const [ ngId, cMap ] of map.entries()) {
for (const [ key, cm ] of cMap.entries()) {
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
*/
}
const layersManager = this.nehubaViewer.ngviewer.state.children.get("layers")
const layerJson = layersManager.toJSON()
for (const layer of layerJson) {
if (layer.name in mainDict) {
layer['segmentColors'] = mainDict[layer.name]
layersManager.restoreState(layerJson)
this.#triggerMeshLoad$.next(null)
Xiao Gui
committed
}
const patchSliceViewPanel = (sliceViewPanel: any) => {
const originalDraw = sliceViewPanel.draw
sliceViewPanel.draw = function(this) {
if (this.sliceView) {
const viewportToDataEv = new CustomEvent('viewportToData', {
bubbles: true,
detail: {
})
this.element.dispatchEvent(viewportToDataEv)
}
originalDraw.call(this)
}
}
Xiao Gui
committed
}