Skip to content
Snippets Groups Projects
layerCtrl.service.ts 12.8 KiB
Newer Older
import { Inject, Injectable, OnDestroy, Optional } from "@angular/core";
import { select, Store } from "@ngrx/store";
import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject, Subscription } from "rxjs";
import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, withLatestFrom } from "rxjs/operators";
Xiao Gui's avatar
Xiao Gui committed
import { viewerStateCustomLandmarkSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors";
import { getRgb, IColorMap, INgLayerCtrl, INgLayerInterface, TNgLayerCtrl } from "./layerCtrl.util";
import { getMultiNgIdsRegionsLabelIndexMap } from "../constants";
import { IAuxMesh } from '../store'
import { REGION_OF_INTEREST } from "src/util/interfaces";
import { TRegionDetail } from "src/util/siibraApiConstants/types";
import { EnumColorMapName } from "src/util/colorMaps";
import { getShader, PMAP_DEFAULT_CONFIG } from "src/util/constants";
import { ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerSelectorClearView, ngViewerSelectorLayers } from "src/services/state/ngViewerState.store.helper";
import { serialiseParcellationRegion } from 'common/util'
export const BACKUP_COLOR = {
  red: 255,
  green: 255,
  blue: 255
}

export function getAuxMeshesAndReturnIColor(auxMeshes: IAuxMesh[]): IColorMap{
  const returnVal: IColorMap = {}
  for (const auxMesh of auxMeshes as IAuxMesh[]) {
    const { ngId, labelIndicies, rgb = [255, 255, 255] } = auxMesh
    const auxMeshColorMap = returnVal[ngId] || {}
    for (const lblIdx of labelIndicies) {
      auxMeshColorMap[lblIdx as number] = {
        red: rgb[0] as number,
        green: rgb[1] as number,
        blue: rgb[2] as number,
      }
    }
    returnVal[ngId] = auxMeshColorMap
  }
  return returnVal
}

@Injectable()
export class NehubaLayerControlService implements OnDestroy{

  static PMAP_LAYER_NAME = 'regional-pmap'
  private selectedRegion$ = this.store$.pipe(
    select(viewerStateSelectedRegionsSelector),
    shareReplay(1),
  )

  private selectedParcellation$ = this.store$.pipe(
    select(viewerStateSelectedParcellationSelector)
  )

  private selectedTemplateSelector$ = this.store$.pipe(
    select(viewerStateSelectedTemplateSelector)
  )

  private selParcNgIdMap$ = this.selectedParcellation$.pipe(
    map(parc => getMultiNgIdsRegionsLabelIndexMap(parc)),
    shareReplay(1),
  )
  
  private activeColorMap$: Observable<IColorMap> = combineLatest([
    this.selParcNgIdMap$.pipe(
      map(map => {
        const returnVal: IColorMap = {}
        for (const [ key, val ] of map.entries()) {
          returnVal[key] = {}
          for (const [ lblIdx, region ] of val.entries()) {
            const rgb = getRgb(lblIdx, region)
            returnVal[key][lblIdx] = rgb
          }
        }
        return returnVal
      })
    ),
    this.selectedRegion$,
    this.selectedTemplateSelector$.pipe(
      map(template => {
Xiao Gui's avatar
Xiao Gui committed
        const { auxMeshes = [] } = template || {}
        return getAuxMeshesAndReturnIColor(auxMeshes)
      })
    ),
    this.selectedParcellation$.pipe(
      map(parc => {
Xiao Gui's avatar
Xiao Gui committed
        const { auxMeshes = [] } = parc || {}
        return getAuxMeshesAndReturnIColor(auxMeshes)
      })
    ),
  ]).pipe(
    map(([ regions, selReg, ...auxMeshesArr ]) => {
      
      const returnVal: IColorMap = {}
      if (selReg.length === 0) {
        for (const key in regions) {
          returnVal[key] = regions[key]
        }
      } else {
        /**
         * if selected regions are non empty
         * set the selected regions to show color,
         * but the rest to show white 
         */
        for (const key in regions) {
          const colorMap = {}
          returnVal[key] = colorMap
          for (const lblIdx in regions[key]) {
            if (selReg.some(r => r.ngId === key && r.labelIndex === Number(lblIdx))) {
              colorMap[lblIdx] = regions[key][lblIdx]
            } else {
              colorMap[lblIdx] = BACKUP_COLOR
            }
          }
        }
      }

      for (const auxMeshes of auxMeshesArr) {
        for (const key in auxMeshes) {
          const existingObj = returnVal[key] || {}
          returnVal[key] = {
            ...existingObj,
            ...auxMeshes[key],
          }
        }
      }
      this.activeColorMap = returnVal
      return returnVal
    })
  )

  private auxMeshes$: Observable<IAuxMesh[]> = combineLatest([
    this.selectedTemplateSelector$,
    this.selectedParcellation$,
  ]).pipe(
    map(([ tmpl, parc ]) => {
Xiao Gui's avatar
Xiao Gui committed
      const { auxMeshes: tmplAuxMeshes = [] as IAuxMesh[] } = tmpl || {}
      const { auxMeshes: parclAuxMeshes = [] as IAuxMesh[] } = parc || {}
      return [...tmplAuxMeshes, ...parclAuxMeshes]
    })
  )

  private sub: Subscription[] = []

  ngOnDestroy(){
    while (this.sub.length > 0) this.sub.pop().unsubscribe()
  }

  constructor(
    private store$: Store<any>,
    @Optional() @Inject(REGION_OF_INTEREST) roi$: Observable<TRegionDetail>
    if (roi$) {

      this.sub.push(
        roi$.pipe(
          switchMap(roi => {
            if (!roi || !roi.hasRegionalMap) {
              // clear pmap
              return of(null)
            }
            
            const { links } = roi
            const { regional_map: regionalMapUrl, regional_map_info: regionalMapInfoUrl } = links
            return from(fetch(regionalMapInfoUrl).then(res => res.json())).pipe(
              map(regionalMapInfo => {
                return {
                  roi,
                  regionalMapUrl,
                  regionalMapInfo
                }
              })
            )
          })
        ).subscribe(processedRoi => {
          if (!processedRoi) {
            this.store$.dispatch(
              ngViewerActionRemoveNgLayer({
                layer: {
                  name: NehubaLayerControlService.PMAP_LAYER_NAME
                }
              })
            )
            return
          }
          const { 
            roi,
            regionalMapUrl,
            regionalMapInfo
          } = processedRoi
          const { min, max, colormap = EnumColorMapName.VIRIDIS } = regionalMapInfo || {} as any
          const shaderObj = {
            ...PMAP_DEFAULT_CONFIG,
            ...{ colormap },
            ...( typeof min !== 'undefined' ? { lowThreshold: min } : {} ),
            ...( max ? { highThreshold: max } : { highThreshold: 1 } )
          }

          const layer = {
            name: NehubaLayerControlService.PMAP_LAYER_NAME,
            source : `nifti://${regionalMapUrl}`,
            mixability : 'nonmixable',
            shader : getShader(shaderObj),
          }

          this.store$.dispatch(
            ngViewerActionAddNgLayer({ layer })
          )

          // this.layersService.highThresholdMap.set(layerName, highThreshold)
          // this.layersService.lowThresholdMap.set(layerName, lowThreshold)
          // this.layersService.colorMapMap.set(layerName, cmap)
          // this.layersService.removeBgMap.set(layerName, removeBg)
        })
      )
    }

    this.sub.push(
      this.ngLayers$.subscribe(({ ngLayers }) => {
        this.ngLayersRegister.layers = ngLayers
      })
    )

    this.sub.push(
      this.store$.pipe(
        select(ngViewerSelectorClearView),
        distinctUntilChanged()
      ).subscribe(flag => {
        const pmapLayer = this.ngLayersRegister.layers.find(l => l.name === NehubaLayerControlService.PMAP_LAYER_NAME)
        if (!pmapLayer) return
        const payload = {
          type: 'update',
          payload: {
            [NehubaLayerControlService.PMAP_LAYER_NAME]: {
              visible: !flag
            }
          }
        } as TNgLayerCtrl<'update'>
        this.manualNgLayersControl$.next(payload)
      })
    )
Xiao Gui's avatar
Xiao Gui committed

    /**
     * on custom landmarks loaded, set mesh transparency
     */
    this.sub.push(
      this.store$.pipe(
        select(viewerStateCustomLandmarkSelector),
        withLatestFrom(this.auxMeshes$)
      ).subscribe(([landmarks, auxMeshes]) => {
        
        const payload: {
          [key: string]: number
        } = {}
        const alpha = landmarks.length > 0
          ? 0.2
          : 1.0
        for (const auxMesh of auxMeshes) {
          payload[auxMesh.ngId] = alpha
        }
        
        this.manualNgLayersControl$.next({
          type: 'setLayerTransparency',
          payload
        })
      })
    )
  }

  public activeColorMap: IColorMap

  public overwriteColorMap$ = new BehaviorSubject<IColorMap>(null)

  public setColorMap$: Observable<IColorMap> = merge(
    this.activeColorMap$.pipe(
      // TODO this is a dirty fix
      // it seems, sometimes, overwritecolormap and activecolormap can emit at the same time
      // (e.g. when reg selection changes)
      // this ensures that the activecolormap emits later, and thus take effect over overwrite colormap
      debounceTime(16),
    ),
    this.overwriteColorMap$.pipe(
      filter(v => !!v),
  ).pipe(
    shareReplay(1)
  )

  public visibleLayer$: Observable<string[]> = combineLatest([
    this.selectedTemplateSelector$,
    this.auxMeshes$,
    this.selParcNgIdMap$
  ]).pipe(
    map(([ tmpl, auxMeshes, parcNgIdMap ]) => {
      const ngIdSet = new Set<string>()
      const { ngId } = tmpl
      ngIdSet.add(ngId)
      for (const auxMesh of auxMeshes) {
        const { ngId } = auxMesh
        ngIdSet.add(ngId as string)
      }
      for (const ngId of parcNgIdMap.keys()) {
        ngIdSet.add(ngId)
      }
      return Array.from(ngIdSet)
    })
  )

  /**
   * define when shown segments should be updated
   */
  public segmentVis$: Observable<string[]> = combineLatest([
    /**
     * selectedRegions
     */
    this.selectedRegion$,
    /**
     * 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(
    withLatestFrom(this.selectedParcellation$),
    map(([[ regions, nonmixableLayerExists, clearViewFlag ], selParc]) => {
      if (nonmixableLayerExists && !clearViewFlag) {
        return null
      }
      const { ngId: defaultNgId } = selParc || {}
  
      /* 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) {
        return [...selectedRegionIndexSet]
      } else {
        return []
      }
    })
  )

  /**
   * ngLayers controller
   */

  private ngLayersRegister: {layers: INgLayerInterface[]} = {
    layers: []
  }
  public removeNgLayers(layerNames: string[]) {
    this.ngLayersRegister.layers
      .filter(layer => layerNames?.findIndex(l => l === layer.name) >= 0)
      .map(l => l.name)
      .forEach(layerName => {
        this.store$.dispatch(ngViewerActionRemoveNgLayer({
          layer: {
            name: layerName
          }
        }))
      })
  }
  public addNgLayer(layers: INgLayerInterface[]){
    this.store$.dispatch(ngViewerActionAddNgLayer({
      layer: layers
    }))
  }
  private ngLayers$ = this.store$.pipe(
    select(ngViewerSelectorLayers),
    map((ngLayers: INgLayerInterface[]) => {
      const newLayers = ngLayers.filter(l => {
        const registeredLayerNames = this.ngLayersRegister.layers.map(l => l.name)
        return !registeredLayerNames.includes(l.name)
      })
      const removeLayers = this.ngLayersRegister.layers.filter(l => {
        const stateLayerNames = ngLayers.map(l => l.name)
        return !stateLayerNames.includes(l.name)
      })
      return { newLayers, removeLayers, ngLayers }
    }),
    shareReplay(1)
  )
  private manualNgLayersControl$ = new Subject<TNgLayerCtrl<keyof INgLayerCtrl>>()
  ngLayersController$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>> = merge(
    this.ngLayers$.pipe(
      map(({ newLayers }) => newLayers),
      filter(layers => layers.length > 0),
      map(newLayers => {

        const newLayersObj: any = {}
        newLayers.forEach(({ name, source, ...rest }) => newLayersObj[name] = {
          ...rest,
          source,
          // source: getProxyUrl(source),
          // ...getProxyOther({source})
        })
  
        return {
          type: 'add',
          payload: newLayersObj
        } as TNgLayerCtrl<'add'>
      })
    ),
    this.ngLayers$.pipe(
      map(({ removeLayers }) => removeLayers),
      filter(layers => layers.length > 0),
      map(removeLayers => {
        const removeLayerNames = removeLayers.map(v => v.name)
        return {
          type: 'remove',
          payload: { names: removeLayerNames }
        } as TNgLayerCtrl<'remove'>
      })
    ),
    this.manualNgLayersControl$,
  ).pipe(
  )