Skip to content
Snippets Groups Projects
layerCtrl.service.ts 13.2 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";
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";
import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes";
import { AnnotationLayer } from "src/atlasComponents/annotations";
import { PMAP_LAYER_NAME } from "../constants"
Xiao Gui's avatar
Xiao Gui committed
import { getShader } from "src/util/fn";
Xiao Gui's avatar
Xiao Gui committed
import { BaseService } from "../base.service/base.service";
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
  public selectedATPR$ = this.baseService.selectedATPR$
Xiao Gui's avatar
Xiao Gui committed

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
  
  public completeNgIdLabelRegionMap$ = this.baseService.completeNgIdLabelRegionMap$

Xiao Gui's avatar
Xiao Gui committed
  private activeColorMap$ = combineLatest([
Xiao Gui's avatar
Xiao Gui committed
    combineLatest([
Xiao Gui's avatar
Xiao Gui committed
      this.completeNgIdLabelRegionMap$,
Xiao Gui's avatar
Xiao Gui committed
      this.customLayers$,
      this.selectedRegion$,
Xiao Gui's avatar
Xiao Gui committed
    ]).pipe(
      map(([record, layers, selectedRegions]) => {
        const returnVal: IColorMap = {}
Xiao Gui's avatar
Xiao Gui committed

Xiao Gui's avatar
Xiao Gui committed
        const cmCustomLayers = layers.filter(l => l.clType === "customlayer/colormap") as atlasAppearance.const.ColorMapCustomLayer[]
        const cmBaseLayers = layers.filter(l => l.clType === "baselayer/colormap") as atlasAppearance.const.ColorMapCustomLayer[]
Xiao Gui's avatar
Xiao Gui committed
        
        const usingCustomCM = cmCustomLayers.length > 0

Xiao Gui's avatar
Xiao Gui committed
        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`)
            },
Xiao Gui's avatar
Xiao Gui committed
            get: (r: SxplrRegion) => r.color
Xiao Gui's avatar
Xiao Gui committed
          }
        })()

        const selectedRegionNameSet = new Set(selectedRegions.map(v => v.name))
Xiao Gui's avatar
Xiao Gui committed
        
Xiao Gui's avatar
Xiao Gui committed
        for (const [ngId, labelRecord] of Object.entries(record)) {
          for (const [label, region] of Object.entries(labelRecord)) {
            if (!region.color) continue
            /**
             * if custom color map is used, do *not* selectively paint selected region
             * custom color map can choose to subscribe to selected regions, and update the color map accordingly, 
             * if they wish to respect the selected regions
             */
            const [ red, green, blue ] = usingCustomCM || selectedRegionNameSet.size === 0 || selectedRegionNameSet.has(region.name)
              ? useCm.get(region) || [200, 200, 200]
              : [255, 255, 255]
Xiao Gui's avatar
Xiao Gui committed
            if (!returnVal[ngId]) {
              returnVal[ngId] = {}
            }
            returnVal[ngId][label] = { 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
    private baseService: BaseService,
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),
Xiao Gui's avatar
Xiao Gui committed
          map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer" && typeof l.source === "string" && /^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
   */
  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
    switchMap(( [ selectedRegions, customMapExists, nonmixableLayerExists ] ) => this.completeNgIdLabelRegionMap$.pipe(
      map(completeNgIdLabelRegion => {
        /**
Xiao Gui's avatar
Xiao Gui committed
       * 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
       */
Xiao Gui's avatar
Xiao Gui committed
      const selectedRegionNameSet = new Set(selectedRegions.map(r => r.name))
      const roiIndexSet = new Set<string>()
      for (const ngId in completeNgIdLabelRegion) {
        for (const label in completeNgIdLabelRegion[ngId]) {
          const val = completeNgIdLabelRegion[ngId][label]
          if (!customMapExists && !selectedRegionNameSet.has(val.name)) {
            continue
          }
          roiIndexSet.add(serializeSegment(ngId, label))
        } 
      }
Xiao Gui's avatar
Xiao Gui committed
      if (roiIndexSet.size > 0) {
        return [...roiIndexSet]
Xiao Gui's avatar
Xiao Gui committed
      }),
    )),
Xiao Gui's avatar
Xiao Gui committed
  private ngLayersRegister: atlasAppearance.const.NgLayerCustomLayer[] = []
Xiao Gui's avatar
Xiao Gui committed
  private getUpdatedCustomLayer(isSameLayer: (o: atlasAppearance.const.NgLayerCustomLayer, n: atlasAppearance.const.NgLayerCustomLayer) => boolean){
    return this.store$.pipe(
      select(atlasAppearance.selectors.customLayers),
Xiao Gui's avatar
Xiao Gui committed
      map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.const.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(
Xiao Gui's avatar
Xiao Gui committed
    map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.const.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.const.NgLayerCustomLayer[]),
Xiao Gui's avatar
Xiao Gui committed
      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 otherLayerNames = cl.filter(l => l.clType === "customlayer/nglayer").map(l => l.id)
        return otherColormapExist
          ? []
          : otherLayerNames
    map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName, ...AnnotationLayer.Map.keys()])
Xiao Gui's avatar
Xiao Gui committed


  static ExternalLayerNames = new Set<string>()

  /**
   * @description Occationally, a layer can be managed by external components. Register the name of such layers so it will be ignored.
   * @param layername 
   */
  static RegisterLayerName(layername: string) {
    NehubaLayerControlService.ExternalLayerNames.add(layername)
  }
  /**
   * @description Once external component is done with the layer, return control back to the service
   * @param layername 
   */
  static DeregisterLayerName(layername: string) {
    NehubaLayerControlService.ExternalLayerNames.delete(layername)
  }