From 899132a9523ce90d14d2d5104ff302d525b80d16 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Tue, 4 May 2021 14:36:39 +0200
Subject: [PATCH] exmpt MVP use siibra-api for template/parc/region

Co-authored-by: Xiao Gui <xgui3783@gmail.com>
Co-authored-by: Daviti Gogshelidze <daviti1@mail.com>
---
 .../regionalFeatures/bsFeatures/constants.ts  |   3 +-
 .../regionalFeatures/bsFeatures/module.ts     |   5 -
 .../splashScreen/splashScreen.component.ts    |   5 -
 .../atlasViewer.constantService.service.ts    |  11 +-
 src/main.module.ts                            |   7 +-
 src/util/constants.ts                         |   1 +
 src/util/fn.ts                                |  37 ++
 src/util/pureConstant.service.ts              | 528 +++++++++++++++---
 src/util/siibraApiConstants/types.ts          | 140 +++++
 .../layerCtrl.service/layerCtrl.service.ts    |   2 +-
 10 files changed, 631 insertions(+), 108 deletions(-)
 create mode 100644 src/util/siibraApiConstants/types.ts

diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts b/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts
index 4bbc8df0f..1d2f42a5c 100644
--- a/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts
+++ b/src/atlasComponents/regionalFeatures/bsFeatures/constants.ts
@@ -2,8 +2,7 @@ import { InjectionToken } from "@angular/core";
 import { Observable } from "rxjs";
 import { IHasId } from "src/util/interfaces";
 import { TBSDetail, TBSSummary } from "./receptor/type";
-
-export const BS_ENDPOINT = new InjectionToken<string>('BS_ENDPOINT')
+export { BS_ENDPOINT } from 'src/util/constants'
 
 export type TRegion = {
   name: string
diff --git a/src/atlasComponents/regionalFeatures/bsFeatures/module.ts b/src/atlasComponents/regionalFeatures/bsFeatures/module.ts
index f4f16b8fa..185a3387c 100644
--- a/src/atlasComponents/regionalFeatures/bsFeatures/module.ts
+++ b/src/atlasComponents/regionalFeatures/bsFeatures/module.ts
@@ -1,6 +1,5 @@
 import { CommonModule } from "@angular/common";
 import { NgModule } from "@angular/core";
-import { BS_ENDPOINT } from "./constants";
 import { BSFeatureReceptorModule } from "./receptor";
 import { BsFeatureService } from "./service";
 
@@ -10,10 +9,6 @@ import { BsFeatureService } from "./service";
     BSFeatureReceptorModule,
   ],
   providers: [
-    {
-      provide: BS_ENDPOINT,
-      useValue: BS_REST_URL || `https://brainscapes.apps-dev.hbp.eu`
-    },
     BsFeatureService
   ],
   exports: [
diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts
index d70b2ba32..b734cf054 100644
--- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts
+++ b/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts
@@ -30,7 +30,6 @@ export class SplashScreen implements AfterViewInit {
 
   constructor(
     private store: Store<IavRootStoreInterface>,
-    private constanceService: AtlasViewerConstantsServices,
     private pureConstantService: PureContantService
   ) {
     this.loadedTemplate$ = this.store.pipe(
@@ -91,10 +90,6 @@ export class SplashScreen implements AfterViewInit {
       })
     )
   }
-
-  get totalTemplates() {
-    return this.constanceService.templateUrls.length
-  }
 }
 
 @Pipe({
diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts
index b9484fd0f..bf9b870c4 100644
--- a/src/atlasViewer/atlasViewer.constantService.service.ts
+++ b/src/atlasViewer/atlasViewer.constantService.service.ts
@@ -26,12 +26,6 @@ export class AtlasViewerConstantsServices implements OnDestroy {
   // instead of using window.location.href, which includes query param etc
   public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}`
 
-  public totalTemplates = null
-
-  public getTemplateEndpoint$ = this.http.get(`${this.backendUrl}templates`, { responseType: 'json' }).pipe(
-    shareReplay(1)
-  )
-
   public templateUrls = Array(100)
 
   /* to be provided by KG in future */
@@ -217,10 +211,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}"
           this.dissmissUserLayerSnackbarMessage = this.dissmissUserLayerSnackbarMessageDesktop
         }
       }),
-    ),
-    this.pureConstantService.getTemplateEndpoint$.subscribe(arr => {
-      this.totalTemplates = arr.length
-    })
+    )
   }
 
   private subscriptions: Subscription[] = []
diff --git a/src/main.module.ts b/src/main.module.ts
index f4a8fcaa0..887448d74 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -62,6 +62,7 @@ import { KgTosModule } from './ui/kgtos/module';
 import { MouseoverModule } from './mouseoverModule/mouseover.module';
 import { AtlasViewerRouterModule } from './routerModule';
 import { MessagingGlue } from './messagingGlue';
+import { BS_ENDPOINT } from './util/constants';
 
 export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
   return function(state, action) {
@@ -254,7 +255,11 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
     {
       provide: WINDOW_MESSAGING_HANDLER_TOKEN,
       useClass: MessagingGlue
-    }
+    },
+    {
+      provide: BS_ENDPOINT,
+      useValue: (BS_REST_URL || `https://brainscapes.apps-dev.hbp.eu`).replace(/\/$/, '')
+    },
   ],
   bootstrap : [
     AtlasViewer,
diff --git a/src/util/constants.ts b/src/util/constants.ts
index 0236f4e41..773b99b8b 100644
--- a/src/util/constants.ts
+++ b/src/util/constants.ts
@@ -115,3 +115,4 @@ export const compareLandmarksChanged: (prevLandmarks: any[], newLandmarks: any[]
 }
 
 export const CYCLE_PANEL_MESSAGE = `[spacebar] to cycle through views`
+export const BS_ENDPOINT = new InjectionToken<string>('BS_ENDPOINT')
diff --git a/src/util/fn.ts b/src/util/fn.ts
index 4f673a08e..96369e7dd 100644
--- a/src/util/fn.ts
+++ b/src/util/fn.ts
@@ -83,3 +83,40 @@ export function switchMapWaitFor(opts: ISwitchMapWaitFor){
     mapTo(arg)
   )
 }
+
+export class MultiDimMap{
+  
+  private map = new Map()
+
+  static GetKey(...arg: any[]){
+    let mapKey = ``
+    for (let i = 0; i < arg.length; i++) {
+      mapKey += arg[i]
+    }
+    return mapKey
+  }
+
+  set(...arg: any[]) {
+    const mapKey = MultiDimMap.GetKey(...(arg.slice(0, -1)))
+    this.map.set(mapKey, arg[arg.length - 1])
+  }
+  get(...arg: any[]) {
+    const mapKey = MultiDimMap.GetKey(...arg)
+    return this.map.get(mapKey)
+  }
+  delete(...arg: any[]) {
+    const mapKey = MultiDimMap.GetKey(...arg)
+    return this.map.delete(mapKey)
+  }
+}
+
+export function recursiveMutate<T>(arr: T[], getChildren: (obj: T) => T[], mutateFn: (obj: T) => void){
+  for (const obj of arr) {
+    mutateFn(obj)
+    recursiveMutate(
+      getChildren(obj),
+      getChildren,
+      mutateFn
+    )
+  }
+}
diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts
index 6bd40e68d..7224fd7f9 100644
--- a/src/util/pureConstant.service.ts
+++ b/src/util/pureConstant.service.ts
@@ -1,15 +1,142 @@
-import { Injectable, OnDestroy } from "@angular/core";
+import { Inject, Injectable, OnDestroy } from "@angular/core";
 import { Store, select } from "@ngrx/store";
 import { Observable, merge, Subscription, of, forkJoin, fromEvent, combineLatest, timer } from "rxjs";
 import { viewerConfigSelectorUseMobileUi } from "src/services/state/viewerConfig.store.helper";
-import { shareReplay, tap, scan, catchError, filter, switchMap, map, take, distinctUntilChanged } from "rxjs/operators";
+import { shareReplay, tap, scan, catchError, filter, switchMap, map, take, distinctUntilChanged, mapTo } from "rxjs/operators";
 import { HttpClient } from "@angular/common/http";
 import { viewerStateFetchedTemplatesSelector, viewerStateSetFetchedAtlases } from "src/services/state/viewerState.store.helper";
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
 import { LoggingService } from "src/logging";
 import { viewerStateFetchedAtlasesSelector } from "src/services/state/viewerState/selectors";
+import { BS_ENDPOINT } from "src/util/constants";
+import { flattenReducer } from 'common/util'
+import { TAtlas, TId, TParc, TRegion, TSpaceFull, TSpaceSummary } from "./siibraApiConstants/types";
+import { MultiDimMap, recursiveMutate } from "./fn";
 
-const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16)
+function parseId(id: TId){
+  if (typeof id === 'string') return id
+  return `${id.kg.kgSchema}/${id.kg.kgId}`
+}
+
+type THasId = {
+  ['@id']: string
+  name: string
+}
+
+type TIAVAtlas = {
+  templateSpaces: ({ availableIn: THasId[] } & THasId)[]
+  parcellations: ({
+    availableIn: THasId[]
+    baseLayer: boolean
+    '@version': {
+      name: string
+      '@next': string
+      '@previous': string
+      '@this': string
+    }
+  } & THasId)[]
+} & THasId
+
+function getNehubaConfig(darkTheme: boolean) {
+  const backgrd = darkTheme
+    ? [0,0,0,1]
+    : [1,1,1,1]
+
+  const rmPsp = darkTheme
+    ? {"mode":"<","color":[0.1,0.1,0.1,1]}
+    :{"color":[1,1,1,1],"mode":"=="}
+  const drawSubstrates = darkTheme
+    ? {"color":[0.5,0.5,1,0.2]}
+    : {"color":[0,0,0.5,0.15]}
+  const drawZoomLevels = darkTheme
+    ? {"cutOff":150000}
+    : {"cutOff":200000,"color":[0.5,0,0,0.15] }
+
+  return {
+    "configName": "",
+    "globals": {
+      "hideNullImageValues": true,
+      "useNehubaLayout": {
+        "keepDefaultLayouts": false
+      },
+      "useNehubaMeshLayer": true,
+      "rightClickWithCtrlGlobal": false,
+      "zoomWithoutCtrlGlobal": false,
+      "useCustomSegmentColors": true
+    },
+    "zoomWithoutCtrl": true,
+    "hideNeuroglancerUI": true,
+    "rightClickWithCtrl": true,
+    "rotateAtViewCentre": true,
+    "enableMeshLoadingControl": true,
+    "zoomAtViewCentre": true,
+    "restrictUserNavigation": true,
+    "disableSegmentSelection": false,
+    "dataset": {
+      "imageBackground": backgrd,
+      "initialNgState": {
+        "showDefaultAnnotations": false,
+        "layers": {},
+        // "navigation": {
+        //   "pose": {
+        //     "position": {
+        //       "voxelSize": [
+        //         21166.666015625,
+        //         20000,
+        //         21166.666015625
+        //       ],
+        //       "voxelCoordinates": [
+        //         -21.8844051361084,
+        //         16.288618087768555,
+        //         28.418994903564453
+        //       ]
+        //     }
+        //   },
+        //   "zoomFactor": 350000
+        // },
+        "perspectiveOrientation": [
+          0.3140767216682434,
+          -0.7418519854545593,
+          0.4988985061645508,
+          -0.3195493221282959
+        ],
+        "perspectiveZoom": 1922235.5293810747
+      }
+    },
+    "layout": {
+      "views": "hbp-neuro",
+      "planarSlicesBackground": backgrd,
+      "useNehubaPerspective": {
+        "enableShiftDrag": false,
+        "doNotRestrictUserNavigation": false,
+        "perspectiveSlicesBackground": backgrd,
+        "removePerspectiveSlicesBackground": rmPsp,
+        "perspectiveBackground": backgrd,
+        "fixedZoomPerspectiveSlices": {
+          "sliceViewportWidth": 300,
+          "sliceViewportHeight": 300,
+          "sliceZoom": 563818.3562426177,
+          "sliceViewportSizeMultiplier": 2
+        },
+        "mesh": {
+          "backFaceColor": backgrd,
+          "removeBasedOnNavigation": true,
+          "flipRemovedOctant": true
+        },
+        "centerToOrigin": true,
+        "drawSubstrates": drawSubstrates,
+        "drawZoomLevels": drawZoomLevels,
+        "hideImages": false,
+        "waitForMesh": true,
+        "restrictZoomLevel": {
+          "minZoom": 1200000,
+          "maxZoom": 3500000
+        }
+      }
+    }
+  }
+  
+}
 
 @Injectable({
   providedIn: 'root'
@@ -17,96 +144,103 @@ const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16)
 
 export class PureContantService implements OnDestroy{
   
+  private subscriptions: Subscription[] = []
   public useTouchUI$: Observable<boolean>
-  public fetchedAtlases$: Observable<any>
   public darktheme$: Observable<boolean>
 
   public totalAtlasesLength: number
 
   public allFetchingReady$: Observable<boolean>
 
-  public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}`
+  private atlasParcSpcRegionMap = new MultiDimMap()
+
+  private _backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}`
+  get backendUrl() {
+    console.warn(`something is using backendUrl`)
+    return this._backendUrl
+  }
 
+  /**
+   * TODO remove
+   * when removing, also remove relevant worker code
+   */
   private workerUpdateParcellation$ = fromEvent(this.workerService.worker, 'message').pipe(
     filter((message: MessageEvent) => message && message.data && message.data.type === 'UPDATE_PARCELLATION_REGIONS'),
     map(({ data }) => data)
   )
 
-  private fetchTemplate = (templateUrl) => this.http.get(`${this.backendUrl}${templateUrl}`, { responseType: 'json' }).pipe(
-    switchMap((template: any) => {
-      if (template.nehubaConfig) {
-        return of(template)
-      }
-      if (template.nehubaConfigURL) {
-        return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe(
-          map(nehubaConfig => {
-            return {
-              ...template,
-              nehubaConfig,
-            }
-          }),
-        )
+  private getRegions(atlasId: string, parcId: string, spaceId: string){
+    return this.http.get<TRegion[]>(
+      `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}/regions`,
+      {
+        params: {
+          space_id: spaceId
+        },
+        responseType: 'json'
       }
-      return of(template)
-    }),
-  )
+    )
+  }
 
-  private processTemplate = template => forkJoin(
-    template.parcellations.map(parcellation => {
+  private getParcs(atlasId: string){
+    return this.http.get<TParc[]>(
+      `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations`,
+      { responseType: 'json' }
+    )
+  }
 
-      const id = getUniqueId()
+  private getParcDetail(atlasId: string, parcId: string) {
+    return this.http.get<TParc>(
+      `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}`,
+      { responseType: 'json' }
+    )
+  }
 
-      this.workerService.worker.postMessage({
-        type: 'PROPAGATE_PARC_REGION_ATTR',
-        parcellation,
-        inheritAttrsOpts: {
-          ngId: (parcellation as any ).ngId,
-          relatedAreas: [],
-          fullId: null
-        },
-        id
-      })
+  private getSpaces(atlasId: string){
+    return this.http.get<TSpaceSummary[]>(
+      `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces`,
+      { responseType: 'json' }
+    )
+  }
 
-      return this.workerUpdateParcellation$.pipe(
-        filter(({ id: returnedId }) => id === returnedId),
-        take(1),
-        map(({ parcellation }) => parcellation)
-      )
-    })
-  )
+  private getSpaceDetail(atlasId: string, spaceId: string) {
+    return this.http.get<TSpaceFull>(
+      `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces/${encodeURIComponent(spaceId)}`,
+      { responseType: 'json' }
+    )
+  }
 
-  public getTemplateEndpoint$ = this.http.get<any[]>(`${this.backendUrl}templates`, { responseType: 'json' }).pipe(
-    catchError(() => {
-      this.log.warn(`fetching root /tempaltes error`)
-      return of([])
-    }),
-    shareReplay(),
-  )
+  private getSpacesAndParc(atlasId: string) {
+    const spaces$ = this.getSpaces(atlasId).pipe(
+      switchMap(spaces => forkJoin(
+        spaces.map(space => this.getSpaceDetail(atlasId, parseId(space.id)))
+      ))
+    )
+    const parcs$ = this.getParcs(atlasId).pipe(
+      // need not to get full parc data. first level gets all data
+      // switchMap(parcs => forkJoin(
+      //   parcs.map(parc => this.getParcDetail(atlasId, parseId(parc.id)))
+      // ))
+    )
+    return forkJoin([
+      spaces$,
+      parcs$,
+    ]).pipe(
+      map(([ templateSpaces, parcellations ]) => {
+        return { templateSpaces, parcellations }
+      })
+    )
+  }
 
-  public initFetchTemplate$ = this.getTemplateEndpoint$.pipe(
-    switchMap((templates: string[]) => merge(
-      ...templates.map(templateName => this.fetchTemplate(templateName).pipe(
-        switchMap(template => this.processTemplate(template).pipe(
-          map(parcellations => {
-            return {
-              ...template,
-              parcellations
-            }
-          })
-        ))
-      )),
-    )),
-    catchError((err) => {
-      this.log.warn(`fetching templates error`, err)
-      return of(null)
-    }),
-  )
+  private getFullRegions(atlasId: string, parcId: string, spaceId: string) {
+
+  }
 
   constructor(
     private store: Store<any>,
     private http: HttpClient,
     private log: LoggingService,
     private workerService: AtlasWorkerService,
+    @Inject(BS_ENDPOINT) private bsEndpoint: string,
   ){
     this.darktheme$ = this.store.pipe(
       select(state => state?.viewerState?.templateSelected?.useTheme === 'dark')
@@ -117,21 +251,6 @@ export class PureContantService implements OnDestroy{
       shareReplay(1)
     )
 
-    this.fetchedAtlases$ = this.http.get(`${this.backendUrl.replace(/\/$/, '')}/atlases/`, { responseType: 'json' }).pipe(
-      catchError((err, obs) => of(null)),
-      filter(v => !!v),
-      tap((arr: any[]) => this.totalAtlasesLength = arr.length),
-      switchMap(atlases => merge(
-        ...atlases.map(({ url }) => this.http.get(
-          /^http/.test(url)
-            ? url
-            : `${this.backendUrl.replace(/\/$/, '')}/${url}`,
-          { responseType: 'json' }))
-      )),
-      scan((acc, curr) => acc.concat(curr).sort((a, b) => (a.order || 1000) - (b.order || 1001)), []),
-      shareReplay(1)
-    )
-
     this.subscriptions.push(
       this.fetchedAtlases$.subscribe(fetchedAtlases => 
         this.store.dispatch(
@@ -141,7 +260,8 @@ export class PureContantService implements OnDestroy{
     )
 
     this.allFetchingReady$ = combineLatest([
-      this.getTemplateEndpoint$.pipe(
+      this.initFetchTemplate$.pipe(
+        filter(v => !!v),
         map(arr => arr.length),
       ),
       this.store.pipe(
@@ -161,7 +281,247 @@ export class PureContantService implements OnDestroy{
     )
   }
 
-  private subscriptions: Subscription[] = []
+  private getAtlases$ = this.http.get<TAtlas[]>(
+    `${this.bsEndpoint}/atlases`,
+    {
+      responseType: 'json'
+    }
+  ).pipe(
+    shareReplay(1)
+  )
+
+  public fetchedAtlases$: Observable<TIAVAtlas[]> = this.getAtlases$.pipe(
+    switchMap(atlases => {
+      return forkJoin(
+        atlases.map(
+          atlas => this.getSpacesAndParc(atlas.id).pipe(
+            map(({ templateSpaces, parcellations }) => {
+              return {
+                '@id': atlas.id,
+                name: atlas.name,
+                templateSpaces: templateSpaces.map(tmpl => {
+                  return {
+                    '@id': tmpl.id,
+                    name: tmpl.name,
+                    availableIn: tmpl.availableParcellations.map(parc => {
+                      return {
+                        '@id': parc.id,
+                        name: parc.name
+                      }
+                    })
+                  }
+                }),
+                parcellations: parcellations.map(parc => {
+                  return {
+                    '@id': parseId(parc.id),
+                    name: parc.name,
+                    baseLayer: parc.modality === 'cytoarchitecture',
+                    '@version': {
+                      '@next': parc.version?.next,
+                      '@previous': parc.version?.prev,
+                      'name': parc.version?.name,
+                      '@this': parseId(parc.id)
+                    },
+                    availableIn: parc.availableSpaces.map(space => {
+                      return {
+                        '@id': space.id,
+                        name: space.name,
+                        /**
+                         * TODO need original data format
+                         */
+                        // originalDatasetFormats: [{
+                        //   name: "probability map"
+                        // }]
+                      }
+                    })
+                  }
+                })
+              }
+            })
+          )
+        )
+      )
+    }),
+    catchError((err, obs) => of([])),
+    tap((arr: any[]) => this.totalAtlasesLength = arr.length),
+    scan((acc, curr) => acc.concat(curr).sort((a, b) => (a.order || 1000) - (b.order || 1001)), []),
+    shareReplay(1)
+  )
+
+  public initFetchTemplate$ = this.fetchedAtlases$.pipe(
+    switchMap(atlases => {
+      return forkJoin(
+        atlases.map(atlas => this.getSpacesAndParc(atlas['@id']).pipe(
+          switchMap(({ templateSpaces, parcellations }) => {
+            const ngLayerObj = {}
+            return forkJoin(
+              templateSpaces.map(
+                tmpl => {
+                  ngLayerObj[tmpl.id] = {}
+                  return tmpl.availableParcellations.map(
+                    parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe(
+                      tap(regions => {
+                        recursiveMutate(
+                          regions,
+                          region => region.children,
+                          region => {
+                            /**
+                             * individual map(s)
+                             * this should work for both fully mapped and interpolated
+                             * in the case of interpolated, it sucks that the ngLayerObj will be set multiple times
+                             */
+                            if (
+                              tmpl.id in region.volumeSrc
+                              && 'collect' in region.volumeSrc[tmpl.id]
+                            ) {
+                              const dedicatedMap = region.volumeSrc[tmpl.id]['collect'].filter(v => v.volume_type === 'neuroglancer/precomputed')
+                              if (dedicatedMap.length === 1) {
+                                const ngId = MultiDimMap.GetKey(atlas['@id'], tmpl.id, parc.id, dedicatedMap[0].id)
+                                region['ngId'] = ngId
+                                region['labelIndex'] = dedicatedMap[0].detail['neuroglancer/precomputed'].labelIndex
+                                ngLayerObj[tmpl.id][ngId] = {
+                                  source: `precomputed://${dedicatedMap[0].url}`,
+                                  type: "segmentation",
+                                  transform: dedicatedMap[0].detail['neuroglancer/precomputed'].transform
+                                }
+                              }
+                            }
+  
+                            /**
+                             * if label index is defined
+                             */
+                            if (!!region.labelIndex) {
+                              const hemisphereKey = /left hemisphere/.test(region.name)
+                                // these two keys are, unfortunately, more or less hardcoded
+                                // which is less than ideal
+                                ? 'left hemisphere'
+                                : /right hemisphere/.test(region.name)
+                                  ? 'right hemisphere'
+                                  : 'whole brain'
+
+                              /**
+                               * TODO fix in siibra-api
+                               */
+                              if (
+                                tmpl.id !== 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588'
+                                && parc.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25'
+                                && hemisphereKey === 'whole brain'
+                              ) {
+                                region.labelIndex = null
+                                return
+                              }
+
+                              const hemispheredNgId = MultiDimMap.GetKey(atlas['@id'], tmpl.id, parc.id, hemisphereKey)
+                              region['ngId'] = hemispheredNgId
+                            }
+                          }  
+                        )
+                        this.atlasParcSpcRegionMap.set(
+                          atlas['@id'], tmpl.id, parc.id, regions
+                        )
+  
+                        /**
+                         * populate maps for parc
+                         */
+                        for (const parc of parcellations) {
+                          if (tmpl.id in (parc.volumeSrc || {})) {
+                            // key: 'left hemisphere' | 'right hemisphere' | 'whole brain'
+                            for (const key in parc.volumeSrc[tmpl.id]) {
+                              for (const vol of parc.volumeSrc[tmpl.id][key]) {
+                                if (vol.volume_type === 'neuroglancer/precomputed') {
+                                  const ngIdKey = MultiDimMap.GetKey(atlas['@id'], tmpl.id, parseId(parc.id), key)
+                                  ngLayerObj[tmpl.id][ngIdKey] = {
+                                    source: `precomputed://${vol.url}`,
+                                    type: "segmentation",
+                                    transform: vol.detail['neuroglancer/precomputed'].transform
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        }
+                      })
+                    )
+                  )
+                }
+              ).reduce(flattenReducer, [])
+            ).pipe(
+              mapTo({ templateSpaces, parcellations, ngLayerObj })
+            )
+          }),
+          map(({ templateSpaces, ngLayerObj }) => {
+            return templateSpaces.map(tmpl => {
+              const darkTheme = tmpl.src_volume_type === 'mri'
+              const nehubaConfig = getNehubaConfig(darkTheme)
+              const initialLayers = nehubaConfig.dataset.initialNgState.layers
+              
+              const tmplNgId = tmpl.name
+              const tmplAuxMesh = `${tmpl.name} auxmesh`
+
+              const precomputed = tmpl.volume_src.find(src => src.volume_type === 'neuroglancer/precomputed')
+              if (precomputed) {
+                initialLayers[tmplNgId] = {
+                  type: "image",
+                  source: `precomputed://${precomputed.url}`,
+                  transform: precomputed.detail['neuroglancer/precomputed'].transform
+                }
+              }
+
+              const precompmesh = tmpl.volume_src.find(src => src.volume_type === 'neuroglancer/precompmesh')
+              const auxMeshes = []
+              if (precompmesh){
+                initialLayers[tmplAuxMesh] = {
+                  source: `precompmesh://${precompmesh.url}`,
+                  type: "segmentation",
+                  transform: precompmesh.detail['neuroglancer/precompmesh'].transform
+                }
+                for (const auxMesh of precompmesh.detail['neuroglancer/precompmesh'].auxMeshes) {
+
+                  auxMeshes.push({
+                    ...auxMesh,
+                    ngId: tmplAuxMesh,
+                    '@id': `${tmplAuxMesh} ${auxMesh.name}`,
+                    visible: true
+                  })
+                }
+              }
+
+              for (const key in (ngLayerObj[tmpl.id] || {})) {
+                initialLayers[key] = ngLayerObj[tmpl.id][key]
+              }
+
+              return {
+                name: tmpl.name,
+                '@id': tmpl.id,
+                fullId: tmpl.id,
+                useTheme: darkTheme ? 'dark' : 'light',
+                ngId: tmplNgId,
+                nehubaConfig,
+                auxMeshes,
+                parcellations: tmpl.availableParcellations.map(parc => {
+                  const regions = this.atlasParcSpcRegionMap.get(atlas['@id'], tmpl.id, parc.id) || []
+                  return {
+                    fullId: parc.id,
+                    '@id': parc.id,
+                    name: parc.name,
+                    regions
+                  }
+                })
+              }
+            })
+          })
+        ))
+      )
+    }),
+    map(arr => {
+      return arr.reduce(flattenReducer, [])
+    }),
+    catchError((err) => {
+      this.log.warn(`fetching templates error`, err)
+      return of(null)
+    }),
+    shareReplay(1),
+  )
 
   ngOnDestroy(){
     while(this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe()
diff --git a/src/util/siibraApiConstants/types.ts b/src/util/siibraApiConstants/types.ts
new file mode 100644
index 000000000..4510e27ee
--- /dev/null
+++ b/src/util/siibraApiConstants/types.ts
@@ -0,0 +1,140 @@
+type THref = {
+  href: string
+}
+
+type TSpaceType = 'mri' | 'histology'
+
+type TNgTransform = number[][]
+
+type TVolumeType = 'nii' | 'neuroglancer/precomputed' | 'neuroglancer/precompmesh' | 'detailed maps'
+type TParcModality = 'cytoarchitecture'
+
+type TAuxMesh = {
+  name: string
+  labelIndicies: number[]
+}
+
+interface IVolumeTypeDetail {
+  'nii': null
+  'neuroglancer/precomputed': {
+    'neuroglancer/precomputed': {
+      'transform': TNgTransform
+    }
+  }
+  'neuroglancer/precompmesh': {
+    'neuroglancer/precompmesh': {
+      'auxMeshes': TAuxMesh[]
+      'transform': TNgTransform
+    }
+  }
+  'detailed maps': null
+}
+
+type TVolumeSrc<VolumeType extends keyof IVolumeTypeDetail> = {
+  id: string
+  name: string
+  url: string
+  volume_type: TVolumeType
+  detail: IVolumeTypeDetail[VolumeType]
+}
+
+type TKgIdentifier = {
+  kgSchema: string
+  kgId: string
+}
+
+type TVersion = {
+  name: string
+  prev: string | null
+  next: string | null
+}
+
+export type TId = string | { kg: TKgIdentifier }
+
+export type TAtlas = {
+  id: string
+  name: string
+  links: {
+    parcellations: THref
+    spaces: THref
+  }
+}
+
+export type TSpaceSummary = {
+  id: {
+    kg: TKgIdentifier
+  },
+  name: string
+  links: {
+    self: THref
+  }
+}
+
+export type TParcSummary = {
+  id: string
+  name: string
+}
+
+export type TSpaceFull = {
+  id: string
+  name: string
+  key: string //???
+  type: string //???
+  url: string //???
+  ziptarget: string //???
+  src_volume_type: TSpaceType
+  volume_src: TVolumeSrc<keyof IVolumeTypeDetail>[]
+  availableParcellations: TParcSummary[]
+  links: {
+    templates: THref
+    parcellation_maps: THref
+  }
+}
+
+export type TParc = {
+  id: {
+    kg: TKgIdentifier
+  }
+  name: string
+  availableSpaces: {
+    id: string
+    name: string
+  }[]
+  links: {
+    self: THref
+  }
+  regions: THref
+  modality: TParcModality
+  version: TVersion
+  volumeSrc: {
+    [key: string]: {
+      [key: string]: TVolumeSrc<keyof IVolumeTypeDetail>[]
+    }
+  }
+}
+
+export type TRegion = {
+  name: string
+  children: TRegion[]
+  volumeSrc: {
+    [key: string]: {
+      [key: string]: TVolumeSrc<keyof IVolumeTypeDetail>[]
+    }
+  }
+
+  labelIndex?: number
+  rgb?: number[]
+  id?: {
+    kg: TKgIdentifier
+  }
+
+  /**
+   * missing 
+   */
+
+  originDatasets?: ({
+    filename: string
+  } & TKgIdentifier) []
+
+  position?: number[]
+}
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index 7a338c72e..194681b36 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -10,7 +10,7 @@ import { IAuxMesh } from '../store'
 export function getAuxMeshesAndReturnIColor(auxMeshes: IAuxMesh[]): IColorMap{
   const returnVal: IColorMap = {}
   for (const auxMesh of auxMeshes as IAuxMesh[]) {
-    const { ngId, labelIndicies, rgb } = auxMesh
+    const { ngId, labelIndicies, rgb = [255, 255, 255] } = auxMesh
     const auxMeshColorMap = returnVal[ngId] || {}
     for (const lblIdx of labelIndicies) {
       auxMeshColorMap[lblIdx as number] = {
-- 
GitLab