Skip to content
Snippets Groups Projects
layerCtrl.service.ts 12.6 KiB
Newer Older
Xiao Gui's avatar
Xiao Gui committed
import { Injectable, OnDestroy } from "@angular/core";
import { select, Store } from "@ngrx/store";
Xiao Gui's avatar
Xiao Gui committed
import { combineLatest, merge, Observable, Subject, Subscription } from "rxjs";
import { debounceTime, distinctUntilChanged, filter, map, pairwise, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
import { IColorMap, INgLayerCtrl, TNgLayerCtrl } from "./layerCtrl.util";
import { SAPIRegion } from "src/atlasComponents/sapi/core";
Xiao Gui's avatar
Xiao Gui committed
import { getParcNgId } from "../config.service"
Xiao Gui's avatar
Xiao Gui committed
import { getRegionLabelIndex } from "../config.service/util";
Xiao Gui's avatar
Xiao Gui committed
import { annotation, atlasAppearance, atlasSelection } from "src/state";
Xiao Gui's avatar
Xiao Gui committed
import { serializeSegment } from "../util";
Xiao Gui's avatar
Xiao Gui committed
import { LayerCtrlEffects } from "./layerCtrl.effects";
import { arrayEqual } from "src/util/array";
Xiao Gui's avatar
Xiao Gui committed
import { ColorMapCustomLayer } from "src/state/atlasAppearance";
import { SapiRegionModel } from "src/atlasComponents/sapi";
import { AnnotationLayer } from "src/atlasComponents/annotations";
import { PMAP_LAYER_NAME } from "../constants"
import { EnumColorMapName, mapKeyColorMap } from "src/util/colorMaps";
import { getShader } from "src/util/constants";
export const BACKUP_COLOR = {
  red: 255,
  green: 255,
  blue: 255
}

Xiao Gui's avatar
Xiao Gui committed
@Injectable({
  providedIn: 'root'
})
export class NehubaLayerControlService implements OnDestroy{

  private selectedRegion$ = this.store$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    select(atlasSelection.selectors.selectedRegions),
Xiao Gui's avatar
Xiao Gui committed
  private defaultNgLayers$ = this.layerEffects.onATPDebounceNgLayers$
Xiao Gui's avatar
Xiao Gui committed
  private selectedATP$ = this.store$.pipe(
Xiao Gui's avatar
Xiao Gui committed
    atlasSelection.fromRootStore.distinctATP(),
    shareReplay(1),
  )
Xiao Gui's avatar
Xiao Gui committed

  public selectedATPR$ = this.selectedATP$.pipe(
    switchMap(({ atlas, template, parcellation }) => 
      this.store$.pipe(
        select(atlasSelection.selectors.selectedParcAllRegions),
Xiao Gui's avatar
Xiao Gui committed
        map(regions => ({
          atlas, template, parcellation, regions
        })),
        shareReplay(1)
      )
    )
  )

Xiao Gui's avatar
Xiao Gui committed
  private customLayers$ = this.store$.pipe(
    select(atlasAppearance.selectors.customLayers),
    distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)),
    shareReplay(1)
  )
Xiao Gui's avatar
Xiao Gui committed
  private activeColorMap$ = combineLatest([
Xiao Gui's avatar
Xiao Gui committed
    combineLatest([
      this.selectedATPR$,
      this.customLayers$,
    ]).pipe(
      map(([{ atlas, parcellation, regions, template }, layers]) => {
        const returnVal: IColorMap = {}
Xiao Gui's avatar
Xiao Gui committed

        const cmCustomLayers = layers.filter(l => l.clType === "customlayer/colormap") as ColorMapCustomLayer[]
        const cmBaseLayers = layers.filter(l => l.clType === "baselayer/colormap") as ColorMapCustomLayer[]
        
        const useCm = (() => {
          /**
           * if custom layer exist, use the last custom layer
           */
          if (cmCustomLayers.length > 0) return cmCustomLayers[cmCustomLayers.length - 1].colormap
          /**
           * otherwise, use last baselayer
           */
          if (cmBaseLayers.length > 0) return cmBaseLayers[cmBaseLayers.length - 1].colormap
          /**
           * fallback color map
           */
          return {
            set: () => {
              throw new Error(`cannot set`)
            },
            get: (r: SapiRegionModel) => SAPIRegion.GetDisplayColor(r)
          }
        })()
        
Xiao Gui's avatar
Xiao Gui committed
        for (const r of regions) {
Xiao Gui's avatar
Xiao Gui committed
          if (!r.hasAnnotation) continue
          if (!r.hasAnnotation.visualizedIn) continue

          const ngId = getParcNgId(atlas, template, parcellation, r)
          const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r)
          if (!labelIndex) continue

Xiao Gui's avatar
Xiao Gui committed
          const [ red, green, blue ] = useCm.get(r)

Xiao Gui's avatar
Xiao Gui committed
          if (!returnVal[ngId]) {
            returnVal[ngId] = {}
Xiao Gui's avatar
Xiao Gui committed
          returnVal[ngId][labelIndex] = { red, green, blue }
        }
        return returnVal
      })
    ),
Xiao Gui's avatar
Xiao Gui committed
    this.defaultNgLayers$.pipe(
      map(({ tmplAuxNgLayers }) => {
        const returnVal: IColorMap = {}
        for (const ngId in tmplAuxNgLayers) {
          returnVal[ngId] = {}
          const { auxMeshes } = tmplAuxNgLayers[ngId]
          for (const auxMesh of auxMeshes) {
            const { labelIndicies } = auxMesh
            for (const lblIdx of labelIndicies) {
              returnVal[ngId][lblIdx] = BACKUP_COLOR
Xiao Gui's avatar
Xiao Gui committed
        return returnVal
      })
    )
  ]).pipe(
Xiao Gui's avatar
Xiao Gui committed
    map(([cmParc, cmAux]) => ({
      ...cmParc,
      ...cmAux
    }))
  )
  private sub: Subscription[] = []

Xiao Gui's avatar
Xiao Gui committed
  ngOnDestroy(): void{
    while (this.sub.length > 0) this.sub.pop().unsubscribe()
  }

  constructor(
    private store$: Store<any>,
Xiao Gui's avatar
Xiao Gui committed
    private layerEffects: LayerCtrlEffects,
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
    this.sub.push(
Xiao Gui's avatar
Xiao Gui committed

      /**
       * on store showdelin
       * toggle parcnglayers visibility
       */
      this.store$.pipe(
        select(atlasAppearance.selectors.showDelineation),
        withLatestFrom(this.defaultNgLayers$)
      ).subscribe(([flag, { parcNgLayers }]) => {
        const layerObj = {}
        for (const key in parcNgLayers) {
          layerObj[key] = {
            visible: flag
          }
        }

        this.manualNgLayersControl$.next({
          type: 'update',
          payload: layerObj
        })
      }),
Xiao Gui's avatar
Xiao Gui committed
    )
      this.ngLayers$.subscribe(({ customLayers }) => {
        this.ngLayersRegister = customLayers
Xiao Gui's avatar
Xiao Gui committed
    /**
     * on custom landmarks loaded, set mesh transparency
     */
    this.sub.push(
      merge(
        this.store$.pipe(
          select(annotation.selectors.annotations),
          map(landmarks => landmarks.length > 0),
        ),
        this.store$.pipe(
          select(atlasAppearance.selectors.customLayers),
          map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer" && /^swc:\/\//.test(l.source)).length > 0),
        )
      ).pipe(
        startWith(false),
        withLatestFrom(this.defaultNgLayers$)
      ).subscribe(([flag, { tmplAuxNgLayers }]) => {
Xiao Gui's avatar
Xiao Gui committed
        const payload: {
          [key: string]: number
        } = {}
        const alpha = flag
Xiao Gui's avatar
Xiao Gui committed
          ? 0.2
          : 1.0
        for (const ngId in tmplAuxNgLayers) {
          payload[ngId] = alpha
Xiao Gui's avatar
Xiao Gui committed
        }
        
        this.manualNgLayersControl$.next({
          type: 'setLayerTransparency',
          payload
        })
      })
    )
Xiao Gui's avatar
Xiao Gui committed
  public setColorMap$: Observable<IColorMap> = this.activeColorMap$.pipe(
    debounceTime(16),
  ).pipe(
    shareReplay(1)
Xiao Gui's avatar
Xiao Gui committed
  public expectedLayerNames$ = this.defaultNgLayers$.pipe(
    map(({ parcNgLayers, tmplAuxNgLayers, tmplNgLayers }) => {
      return [
        ...Object.keys(parcNgLayers),
        ...Object.keys(tmplAuxNgLayers),
        ...Object.keys(tmplNgLayers),
      ]

  /**
   * define when shown segments should be updated
   */
Xiao Gui's avatar
Xiao Gui committed
  public _segmentVis$: Observable<string[]> = combineLatest([
    this.selectedATP$,
    this.selectedRegion$
  ]).pipe(
    map(() => [''])
  )

  public segmentVis$: Observable<string[]> = combineLatest([
    /**
     * selectedRegions
     */
    this.selectedRegion$,
Xiao Gui's avatar
Xiao Gui committed
    this.customLayers$.pipe(
      map(layers => layers.filter(l => l.clType === "customlayer/colormap").length > 0),
    ),
    /**
     * if layer contains non mixable layer
     */
Xiao Gui's avatar
Xiao Gui committed
    this.customLayers$.pipe(
      map(layers => layers.filter(l => l.clType === "customlayer/nglayer").length > 0),
Xiao Gui's avatar
Xiao Gui committed
    withLatestFrom(this.selectedATPR$),
    map(([[ selectedRegions, customMapExists, nonmixableLayerExists ], { atlas, parcellation, template, regions }]) => {
      /**
       * if non mixable layer exist (e.g. pmap)
       * and no custom color map exist
       * hide all segmentations
       */
      if (!customMapExists && nonmixableLayerExists) {
Xiao Gui's avatar
Xiao Gui committed
      /**
       * if custom map exists, roi is all regions
       * otherwise, roi is only selectedRegions
       */
      const roi = customMapExists ? regions : selectedRegions

      const roiIndexSet = new Set<string>(
        roi.map(r => {
Xiao Gui's avatar
Xiao Gui committed
          const ngId = getParcNgId(atlas, template, parcellation, r)
          const label = getRegionLabelIndex(atlas, template, parcellation, r)
Xiao Gui's avatar
Xiao Gui committed
          return ngId && label && serializeSegment(ngId, label)
        }).filter(v => !!v)
Xiao Gui's avatar
Xiao Gui committed
      )
Xiao Gui's avatar
Xiao Gui committed
      if (roiIndexSet.size > 0) {
        return [...roiIndexSet]
      } else {
        return []
      }
    })
  )

  /**
   * ngLayers controller
   */

  private ngLayersRegister: atlasAppearance.NgLayerCustomLayer[] = []

  private getUpdatedCustomLayer(isSameLayer: (o: atlasAppearance.NgLayerCustomLayer, n: atlasAppearance.NgLayerCustomLayer) => boolean){
    return this.store$.pipe(
      select(atlasAppearance.selectors.customLayers),
      map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
      pairwise(),
      map(([ oldCustomLayers, newCustomLayers ]) => {
        return newCustomLayers.filter(n => oldCustomLayers.some(o => o.id === n.id && !isSameLayer(o, n)))
      }),
      filter(arr => arr.length > 0),
    )
  }

  private updateCustomLayerTransparency$ = this.getUpdatedCustomLayer((o, n) => o.opacity === n.opacity)
  private updateCustomLayerColorMap$ = this.getUpdatedCustomLayer((o, n) => o.shader === n.shader)
Xiao Gui's avatar
Xiao Gui committed
  private ngLayers$ = this.customLayers$.pipe(
    map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
    distinctUntilChanged(
      arrayEqual((o, n) => o.id === n.id)
    ),
    map(customLayers => {
      const newLayers = customLayers.filter(l => {
        const registeredLayerNames = this.ngLayersRegister.map(l => l.id)
        return !registeredLayerNames.includes(l.id)
      const removeLayers = this.ngLayersRegister.filter(l => {
        const stateLayerNames = customLayers.map(l => l.id)
        return !stateLayerNames.includes(l.id)
      return { newLayers, removeLayers, customLayers }
    }),
    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(({ id, source, ...rest }) => newLayersObj[id] = {
          ...rest,
          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.id)
        return {
          type: 'remove',
          payload: { names: removeLayerNames }
        } as TNgLayerCtrl<'remove'>
      })
    ),
    this.updateCustomLayerTransparency$.pipe(
      map(layers => {
        const payload: Record<string, number> = {}
        for (const layer of layers) {
          const opacity = layer.opacity ?? 0.8
          payload[layer.id] = opacity
        }
        return {
          type: 'setLayerTransparency',
          payload
        } as TNgLayerCtrl<'setLayerTransparency'>
      })
    ),
    this.updateCustomLayerColorMap$.pipe(
      map(layers => {
        const payload: Record<string, string> = {}
        for (const layer of layers) {
          const shader = layer.shader ?? getShader()
          payload[layer.id] = shader
        }
        return {
          type: 'updateShader',
          payload
        } as TNgLayerCtrl<'updateShader'>
      })
    ),
    this.manualNgLayersControl$,
  ).pipe(
  )

  public visibleLayer$: Observable<string[]> = combineLatest([
    this.expectedLayerNames$.pipe(
      map(expectedLayerNames => {
        const ngIdSet = new Set<string>([...expectedLayerNames])
        return Array.from(ngIdSet)
      })
    ),
    this.ngLayers$.pipe(
      map(({ customLayers }) => customLayers),
Xiao Gui's avatar
Xiao Gui committed
      startWith([] as atlasAppearance.NgLayerCustomLayer[]),
      map(customLayers => {
        /**
         * pmap control has its own visibility controller
         */
        return customLayers
          .map(l => l.id)
          .filter(name => name !== PMAP_LAYER_NAME)
Xiao Gui's avatar
Xiao Gui committed
      })
Xiao Gui's avatar
Xiao Gui committed
    this.customLayers$.pipe(
      map(cl => {
        const otherColormapExist = cl.filter(l => l.clType === "customlayer/colormap").length > 0
        const pmapExist = cl.filter(l => l.clType === "customlayer/nglayer").length > 0
        return pmapExist && !otherColormapExist
      }),
      distinctUntilChanged(),
      map(flag => flag
        ? [ PMAP_LAYER_NAME ]
    map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName, ...AnnotationLayer.Map.keys()])