From d22a5503c16570ee70a3ad9c4a5761db70c3dae2 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Mon, 19 Oct 2020 18:00:16 +0200
Subject: [PATCH] bugfix: race con on spatial xform

---
 .../atlasViewer.constantService.service.ts    |  80 +---
 .../state/viewerState.store.helper.ts         |  16 +-
 src/services/state/viewerState.store.ts       |  10 +-
 src/services/state/viewerState/actions.ts     |  15 +
 src/services/state/viewerState/selectors.ts   |  10 +
 .../viewerStateController/viewerState.base.ts |  14 +-
 .../viewerState.useEffect.spec.ts             | 284 +++++++++++---
 .../viewerState.useEffect.ts                  | 354 ++++++++++++------
 src/util/pureConstant.service.ts              |  80 +++-
 9 files changed, 608 insertions(+), 255 deletions(-)

diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts
index 454a2c933..f435d07be 100644
--- a/src/atlasViewer/atlasViewer.constantService.service.ts
+++ b/src/atlasViewer/atlasViewer.constantService.service.ts
@@ -1,16 +1,12 @@
 import { HttpClient, HttpHeaders } from "@angular/common/http";
 import { Injectable, OnDestroy } from "@angular/core";
 import { select, Store } from "@ngrx/store";
-import { merge, Observable, of, Subscription, throwError, fromEvent, forkJoin } from "rxjs";
-import { catchError, map, shareReplay, switchMap, tap, filter, take } from "rxjs/operators";
-import { LoggingService } from "src/logging";
+import { Observable, Subscription } from "rxjs";
+import { map, shareReplay } from "rxjs/operators";
 import { SNACKBAR_MESSAGE } from "src/services/state/uiState.store";
 import { IavRootStoreInterface } from "../services/stateStore.service";
-import { AtlasWorkerService } from "./atlasViewer.workerService.service";
 import { PureContantService } from "src/util";
 
-const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16)
-
 @Injectable({
   providedIn : 'root',
 })
@@ -30,77 +26,12 @@ 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}`
 
-  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,
-          }
-        }),
-      )
-      }
-      throwError('neither nehubaConfig nor nehubaConfigURL defined')
-    }),
-  )
-
   public totalTemplates = null
 
-  private workerUpdateParcellation$ = fromEvent(this.workerService.worker, 'message').pipe(
-    filter((message: MessageEvent) => message && message.data && message.data.type === 'UPDATE_PARCELLATION_REGIONS'),
-    map(({ data }) => data)
-  )
-
-  private processTemplate = template => forkJoin(
-    ...template.parcellations.map(parcellation => {
-
-      const id = getUniqueId()
-
-      this.workerService.worker.postMessage({
-        type: 'PROPAGATE_PARC_REGION_ATTR',
-        parcellation,
-        inheritAttrsOpts: {
-          ngId: (parcellation as any ).ngId,
-          relatedAreas: [],
-          fullId: null
-        },
-        id
-      })
-
-      return this.workerUpdateParcellation$.pipe(
-        filter(({ id: returnedId }) => id === returnedId),
-        take(1),
-        map(({ parcellation }) => parcellation)
-      )
-    })
-  )
-
   public getTemplateEndpoint$ = this.http.get(`${this.backendUrl}templates`, { responseType: 'json' }).pipe(
     shareReplay(1)
   )
 
-  public initFetchTemplate$ = this.getTemplateEndpoint$.pipe(
-    tap((arr: any[]) => this.totalTemplates = arr.length),
-    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)
-    }),
-  )
-
   public templateUrls = Array(100)
 
   /* to be provided by KG in future */
@@ -258,8 +189,6 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}"
   constructor(
     private store$: Store<IavRootStoreInterface>,
     private http: HttpClient,
-    private log: LoggingService,
-    private workerService: AtlasWorkerService,
     private pureConstantService: PureContantService
   ) {
 
@@ -291,7 +220,10 @@ 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/services/state/viewerState.store.helper.ts b/src/services/state/viewerState.store.helper.ts
index 6405a55b3..de45f8a07 100644
--- a/src/services/state/viewerState.store.helper.ts
+++ b/src/services/state/viewerState.store.helper.ts
@@ -7,6 +7,7 @@ import { withLatestFrom, map } from "rxjs/operators";
 import { Injectable } from "@angular/core";
 
 import {
+  viewerStateNewViewer,
   viewerStateHelperSelectParcellationWithId,
   viewerStateNavigateToRegion,
   viewerStateRemoveAdditionalLayer,
@@ -22,10 +23,14 @@ import {
   viewerStateSelectRegionWithIdDeprecated,
   viewerStateDblClickOnViewer,
   viewerStateAddUserLandmarks,
-  viewreStateRemoveUserLandmarks
+  viewreStateRemoveUserLandmarks,
+  viewerStateMouseOverCustomLandmark,
+  viewerStateMouseOverCustomLandmarkInPerspectiveView,
+  viewerStateSelectTemplateWithName,
 } from './viewerState/actions'
 
 export {
+  viewerStateNewViewer,
   viewerStateHelperSelectParcellationWithId,
   viewerStateNavigateToRegion,
   viewerStateRemoveAdditionalLayer,
@@ -41,7 +46,10 @@ export {
   viewerStateSelectRegionWithIdDeprecated,
   viewerStateDblClickOnViewer,
   viewerStateAddUserLandmarks,
-  viewreStateRemoveUserLandmarks
+  viewreStateRemoveUserLandmarks,
+  viewerStateMouseOverCustomLandmark,
+  viewerStateMouseOverCustomLandmarkInPerspectiveView,
+  viewerStateSelectTemplateWithName,
 }
 
 import {
@@ -50,6 +58,8 @@ import {
   viewerStateSelectedParcellationSelector,
   viewerStateGetSelectedAtlas,
   viewerStateCustomLandmarkSelector,
+  viewerStateFetchedTemplatesSelector,
+  viewerStateNavigationStateSelector,
 } from './viewerState/selectors'
 
 export {
@@ -57,6 +67,8 @@ export {
   viewerStateSelectedTemplateSelector,
   viewerStateSelectedParcellationSelector,
   viewerStateCustomLandmarkSelector,
+  viewerStateFetchedTemplatesSelector,
+  viewerStateNavigationStateSelector,
 }
 
 interface IViewerStateHelperStore{
diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts
index 0060ef7b3..fa8c727a6 100644
--- a/src/services/state/viewerState.store.ts
+++ b/src/services/state/viewerState.store.ts
@@ -17,10 +17,14 @@ import {
   viewerStateSelectParcellation,
   viewerStateSelectRegionWithIdDeprecated,
   viewerStateCustomLandmarkSelector,
+  viewerStateDblClickOnViewer,
+  viewerStateAddUserLandmarks,
+  viewreStateRemoveUserLandmarks,
+  viewerStateMouseOverCustomLandmark,
+  viewerStateMouseOverCustomLandmarkInPerspectiveView,
+  viewerStateNewViewer
 } from './viewerState.store.helper';
 
-import { viewerStateDblClickOnViewer, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, viewerStateMouseOverCustomLandmark, viewerStateMouseOverCustomLandmarkInPerspectiveView } from './viewerState/actions';
-
 export interface StateInterface {
   fetchedTemplates: any[]
 
@@ -258,7 +262,7 @@ export function stateStore(state, action) {
 export const LOAD_DEDICATED_LAYER = 'LOAD_DEDICATED_LAYER'
 export const UNLOAD_DEDICATED_LAYER = 'UNLOAD_DEDICATED_LAYER'
 
-export const NEWVIEWER = 'NEWVIEWER'
+export const NEWVIEWER = viewerStateNewViewer.type
 
 export const FETCHED_TEMPLATE = 'FETCHED_TEMPLATE'
 export const CHANGE_NAVIGATION = 'CHANGE_NAVIGATION'
diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts
index 3b19de4b1..7cea8db48 100644
--- a/src/services/state/viewerState/actions.ts
+++ b/src/services/state/viewerState/actions.ts
@@ -1,6 +1,15 @@
 import { createAction, props } from "@ngrx/store"
 import { IRegion } from './constants'
 
+export const viewerStateNewViewer = createAction(
+  `[viewerState] newViewer`,
+  props<{ 
+    selectTemplate: any
+    selectParcellation: any
+    navigation: any
+  }>()
+)
+
 export const viewerStateSetSelectedRegionsWithIds = createAction(
   `[viewerState] setSelectedRegionsWithIds`,
   props<{ selectRegionIds: string[] }>()
@@ -46,6 +55,11 @@ export const viewerStateSelectParcellation = createAction(
   props<{ selectParcellation: any }>()
 )
 
+export const viewerStateSelectTemplateWithName = createAction(
+  `[viewerState] selectTemplateWithName`, 
+  props<{ payload: { name: string } }>()
+)
+
 export const viewerStateSelectTemplateWithId = createAction(
   `[viewerState] selectTemplateWithId`,
   props<{ payload: { ['@id']: string }, config?: { selectParcellation: { ['@id']: string } } }>()
@@ -90,3 +104,4 @@ export const viewerStateMouseOverCustomLandmarkInPerspectiveView = createAction(
   `[viewerState] mouseOverCustomLandmarkInPerspectiveView`,
   props<{ payload: { label: string } }>()
 )
+
diff --git a/src/services/state/viewerState/selectors.ts b/src/services/state/viewerState/selectors.ts
index b7a091bdc..d41d1e0aa 100644
--- a/src/services/state/viewerState/selectors.ts
+++ b/src/services/state/viewerState/selectors.ts
@@ -22,6 +22,11 @@ const flattenFetchedTemplatesIntoParcellationsReducer = (acc, curr) => {
   return acc.concat( parcelations )
 }
 
+export const viewerStateFetchedTemplatesSelector = createSelector(
+  state => state['viewerState'],
+  viewerState => viewerState['fetchedTemplates']
+)
+
 export const viewerStateSelectedTemplateSelector = createSelector(
   state => state['viewerState'],
   viewerState => viewerState['templateSelected']
@@ -32,6 +37,11 @@ export const viewerStateSelectedParcellationSelector = createSelector(
   viewerState => viewerState['parcellationSelected']
 )
 
+export const viewerStateNavigationStateSelector = createSelector(
+  state => state['viewerState'],
+  viewerState => viewerState['navigation']
+)
+
 export const viewerStateAllRegionsFlattenedRegionSelector = createSelector(
   viewerStateSelectedParcellationSelector,
   parc => {
diff --git a/src/ui/viewerStateController/viewerState.base.ts b/src/ui/viewerStateController/viewerState.base.ts
index ee206f5bd..70a67c226 100644
--- a/src/ui/viewerStateController/viewerState.base.ts
+++ b/src/ui/viewerStateController/viewerState.base.ts
@@ -7,9 +7,9 @@ import { RegionSelection } from "src/services/state/userConfigState.store";
 import { IavRootStoreInterface, SELECT_REGIONS, USER_CONFIG_ACTION_TYPES } from "src/services/stateStore.service";
 import { MatSelectChange } from "@angular/material/select";
 import { MatBottomSheet, MatBottomSheetRef } from "@angular/material/bottom-sheet";
+import { viewerStateSelectTemplateWithName } from "src/services/state/viewerState/actions";
 
 const ACTION_TYPES = {
-  SELECT_TEMPLATE_WITH_NAME: 'SELECT_TEMPLATE_WITH_NAME',
   SELECT_PARCELLATION_WITH_NAME: 'SELECT_PARCELLATION_WITH_NAME',
 
 }
@@ -94,13 +94,11 @@ export class ViewerStateBase implements OnInit {
   }
 
   public handleTemplateChange(event: MatSelectChange) {
-
-    this.store$.dispatch({
-      type: ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME,
-      payload: {
-        name: event.value,
-      },
-    })
+    this.store$.dispatch(
+      viewerStateSelectTemplateWithName({
+        payload: { name: event.value }
+      })
+    )
   }
 
   public handleParcellationChange(event: MatSelectChange) {
diff --git a/src/ui/viewerStateController/viewerState.useEffect.spec.ts b/src/ui/viewerStateController/viewerState.useEffect.spec.ts
index 513854f67..e15a18fb8 100644
--- a/src/ui/viewerStateController/viewerState.useEffect.spec.ts
+++ b/src/ui/viewerStateController/viewerState.useEffect.spec.ts
@@ -1,19 +1,20 @@
-import { ViewerStateControllerUseEffect } from './viewerState.useEffect'
+import { cvtNavigationObjToNehubaConfig, cvtNehubaConfigToNavigationObj, ViewerStateControllerUseEffect, defaultNavigationObject, defaultNehubaConfigObject } from './viewerState.useEffect'
 import { Observable, of } from 'rxjs'
 import { TestBed, async } from '@angular/core/testing'
 import { provideMockActions } from '@ngrx/effects/testing'
-import { provideMockStore } from '@ngrx/store/testing'
+import { MockStore, provideMockStore } from '@ngrx/store/testing'
 import { defaultRootState, NEWVIEWER } from 'src/services/stateStore.service'
 import { Injectable } from '@angular/core'
 import { TemplateCoordinatesTransformation, ITemplateCoordXformResp } from 'src/services/templateCoordinatesTransformation.service'
 import { hot } from 'jasmine-marbles'
-import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from './viewerState.base'
 import { AngularMaterialModule } from '../sharedModules/angularMaterial.module'
 import { HttpClientModule } from '@angular/common/http'
 import { WidgetModule } from 'src/widget'
 import { PluginModule } from 'src/atlasViewer/pluginUnit/plugin.module'
+import { viewerStateNavigationStateSelector, viewerStateNewViewer, viewerStateSelectTemplateWithName } from 'src/services/state/viewerState.store.helper'
 
 const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json')
+const bigBrainNehubaConfig = require('!json-loader!src/res/ext/bigbrainNehubaConfig.json')
 const colinJson = require('!json-loader!src/res/ext/colin.json')
 const colinJsonNehubaConfig = require('!json-loader!src/res/ext/colinNehubaConfig.json')
 const reconstitutedColin = JSON.parse(JSON.stringify(
@@ -22,6 +23,12 @@ const reconstitutedColin = JSON.parse(JSON.stringify(
     nehubaConfig: colinJsonNehubaConfig
   }
 ))
+const reconstitutedBigBrain = JSON.parse(JSON.stringify(
+  {
+    ...bigbrainJson,
+    nehubaConfig: bigBrainNehubaConfig
+  }
+))
 let returnPosition = null
 @Injectable()
 class MockCoordXformService{
@@ -34,7 +41,7 @@ class MockCoordXformService{
 
 const initialState = JSON.parse(JSON.stringify( defaultRootState ))
 initialState.viewerState.fetchedTemplates = [
-  bigbrainJson,
+  reconstitutedBigBrain,
   reconstitutedColin
 ]
 initialState.viewerState.templateSelected = initialState.viewerState.fetchedTemplates[0]
@@ -47,8 +54,8 @@ const currentNavigation = {
 }
 initialState.viewerState.navigation = currentNavigation
 
-describe('viewerState.useEffect.ts', () => {
-  describe('ViewerStateControllerUseEffect', () => {
+describe('> viewerState.useEffect.ts', () => {
+  describe('> ViewerStateControllerUseEffect', () => {
     let actions$: Observable<any>
     let spy: any
     beforeEach(async(() => {
@@ -59,10 +66,7 @@ describe('viewerState.useEffect.ts', () => {
       actions$ = hot(
         'a',
         {
-          a: { 
-            type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME,
-            payload: reconstitutedColin
-          }
+          a: viewerStateSelectTemplateWithName({ payload: reconstitutedColin })
         }
       )
 
@@ -85,27 +89,97 @@ describe('viewerState.useEffect.ts', () => {
       }).compileComponents()
     }))
 
-    describe('selectTemplate$', () => {
+    describe('> selectTemplate$', () => {
+      describe('> when transiting from template A to template B', () => {
+        describe('> if the current navigation is correctly formed', () => {
+          it('> uses current navigation param', () => {
 
-      it('if coordXform returns error', () => {
-        const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect)
-        expect(
-          viewerStateCtrlEffect.selectTemplate$
-        ).toBeObservable(
-          hot(
-            'a',
-            {
-              a: {
-                type: NEWVIEWER,
-                selectTemplate: reconstitutedColin,
-                selectParcellation: reconstitutedColin.parcellations[0]
-              }
-            }
-          )
-        )
+            const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect)
+            expect(
+              viewerStateCtrlEffect.selectTemplate$
+            ).toBeObservable(
+              hot(
+                'a',
+                {
+                  a: viewerStateNewViewer({
+                      selectTemplate: reconstitutedColin,
+                      selectParcellation: reconstitutedColin.parcellations[0],
+                      navigation: {}
+                    })
+                }
+              )
+            )
+            expect(spy).toHaveBeenCalledWith(
+              reconstitutedBigBrain.name,
+              reconstitutedColin.name,
+              initialState.viewerState.navigation.position
+            )
+          })
+        })
+
+        describe('> if current navigation is malformed', () => {
+          it('> if current navigation is undefined, use nehubaConfig of last template', () => {
+
+            const mockStore = TestBed.inject(MockStore)
+            mockStore.overrideSelector(viewerStateNavigationStateSelector, null)
+            const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect)
+
+            expect(
+              viewerStateCtrlEffect.selectTemplate$
+            ).toBeObservable(
+              hot(
+                'a',
+                {
+                  a: viewerStateNewViewer({
+                      selectTemplate: reconstitutedColin,
+                      selectParcellation: reconstitutedColin.parcellations[0],
+                      navigation: {}
+                    })
+                }
+              )
+            )
+            const { position } = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain.nehubaConfig.dataset.initialNgState)
+
+            expect(spy).toHaveBeenCalledWith(
+              reconstitutedBigBrain.name,
+              reconstitutedColin.name,
+              position
+            )
+          })
+  
+          it('> if current navigation is empty object, use nehubaConfig of last template', () => {
+
+            const mockStore = TestBed.inject(MockStore)
+            mockStore.overrideSelector(viewerStateNavigationStateSelector, {})
+            const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect)
+
+            expect(
+              viewerStateCtrlEffect.selectTemplate$
+            ).toBeObservable(
+              hot(
+                'a',
+                {
+                  a: viewerStateNewViewer({
+                      selectTemplate: reconstitutedColin,
+                      selectParcellation: reconstitutedColin.parcellations[0],
+                      navigation: {}
+                    })
+                }
+              )
+            )
+            const { position } = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain.nehubaConfig.dataset.initialNgState)
+
+            expect(spy).toHaveBeenCalledWith(
+              reconstitutedBigBrain.name,
+              reconstitutedColin.name,
+              position
+            )
+          })
+        })
+  
       })
 
-      it('calls with correct param', () => {
+      it('> if coordXform returns error', () => {
         const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect)
         expect(
           viewerStateCtrlEffect.selectTemplate$
@@ -113,26 +187,22 @@ describe('viewerState.useEffect.ts', () => {
           hot(
             'a',
             {
-              a: {
-                type: NEWVIEWER,
-                selectTemplate: reconstitutedColin,
-                selectParcellation: reconstitutedColin.parcellations[0]
-              }
+              a: viewerStateNewViewer({
+                  selectTemplate: reconstitutedColin,
+                  selectParcellation: reconstitutedColin.parcellations[0],
+                  navigation: {}
+                })
             }
           )
         )
-        expect(spy).toHaveBeenCalledWith(
-          bigbrainJson.name,
-          reconstitutedColin.name,
-          initialState.viewerState.navigation.position
-        )
       })
 
-      it('if coordXform returns complete', () => {
+      it('> if coordXform complete', () => {
         returnPosition = [ 1.11e6, 2.22e6, 3.33e6 ]
 
         const viewerStateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect)
         const updatedColin = JSON.parse( JSON.stringify( reconstitutedColin ) )
+        const initialNgState = updatedColin.nehubaConfig.dataset.initialNgState
         const updatedColinNavigation = updatedColin.nehubaConfig.dataset.initialNgState.navigation
 
         const { zoom, orientation, perspectiveOrientation, position, perspectiveZoom } = currentNavigation
@@ -142,6 +212,8 @@ describe('viewerState.useEffect.ts', () => {
         }
         updatedColinNavigation.zoomFactor = zoom
         updatedColinNavigation.pose.orientation = orientation
+        initialNgState.perspectiveOrientation = perspectiveOrientation
+        initialNgState.perspectiveZoom = perspectiveZoom
         
         expect(
           viewerStateCtrlEffect.selectTemplate$
@@ -149,15 +221,137 @@ describe('viewerState.useEffect.ts', () => {
           hot(
             'a',
             {
-              a: {
-                type: NEWVIEWER,
-                selectTemplate: updatedColin,
-                selectParcellation: updatedColin.parcellations[0]
-              }
+              a: viewerStateNewViewer({
+                  selectTemplate: updatedColin,
+                  selectParcellation: updatedColin.parcellations[0],
+                  navigation: {}
+                })
             }
           )
         )
       })
+
+    })
+  })
+
+  describe('> cvtNehubaConfigToNavigationObj', () => {
+    describe('> returns default obj when input is malformed', () => {
+      it('> if no arg is provided', () => {
+
+        const obj = cvtNehubaConfigToNavigationObj()
+        expect(obj).toEqual({
+          orientation: [0, 0, 0, 1],
+          perspectiveOrientation: [0 , 0, 0, 1],
+          perspectiveZoom: 1e6,
+          zoom: 1e6,
+          position: [0, 0, 0],
+          positionReal: true
+        })
+      })
+      it('> if null or undefined is provided', () => {
+
+        const obj = cvtNehubaConfigToNavigationObj(null)
+        expect(obj).toEqual(defaultNavigationObject)
+
+        const obj2 = cvtNehubaConfigToNavigationObj(undefined)
+        expect(obj2).toEqual(defaultNavigationObject)
+      })
+      it('> if malformed', () => {
+        
+        const obj = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain)
+        expect(obj).toEqual(defaultNavigationObject)
+
+        const obj2 = cvtNehubaConfigToNavigationObj({})
+        expect(obj2).toEqual(defaultNavigationObject)
+      })
+    })
+    it('> converts nehubaConfig object to navigation object', () => {
+
+      const obj = cvtNehubaConfigToNavigationObj(reconstitutedBigBrain.nehubaConfig.dataset.initialNgState)
+      expect(obj).toEqual({
+        orientation: [0, 0, 0, 1],
+        perspectiveOrientation: [
+          0.3140767216682434,
+          -0.7418519854545593,
+          0.4988985061645508,
+          -0.3195493221282959
+        ],
+        perspectiveZoom: 1922235.5293810747,
+        zoom: 350000,
+        position: [ -463219.89446663484, 325772.3617553711, 601535.3736234978 ],
+        positionReal: true
+      })
+    })
+  })
+  describe('> cvtNavigationObjToNehubaConfig', () => {
+    const validNehubaConfigObj = reconstitutedBigBrain.nehubaConfig.dataset.initialNgState
+    const validNavigationObj = currentNavigation
+    describe('> if inputs are malformed', () => {
+      describe('> if navigation object is malformed, uses navigation default object', () => {
+        it('> if navigation object is null', () => {
+          const v1 = cvtNavigationObjToNehubaConfig(null, validNehubaConfigObj)
+          const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj)
+          expect(v1).toEqual(v2)
+        })
+        it('> if navigation object is undefined', () => {
+          const v1 = cvtNavigationObjToNehubaConfig(undefined, validNehubaConfigObj)
+          const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj)
+          expect(v1).toEqual(v2)
+        })
+
+        it('> if navigation object is otherwise malformed', () => {
+          const v1 = cvtNavigationObjToNehubaConfig(reconstitutedBigBrain, validNehubaConfigObj)
+          const v2 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj)
+          expect(v1).toEqual(v2)
+
+          const v3 = cvtNavigationObjToNehubaConfig({}, validNehubaConfigObj)
+          const v4 = cvtNavigationObjToNehubaConfig(defaultNavigationObject, validNehubaConfigObj)
+          expect(v3).toEqual(v4)
+        })
+      })
+
+      describe('> if nehubaConfig object is malformed, use default nehubaConfig obj', () => {
+        it('> if nehubaConfig is null', () => {
+          const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, null)
+          const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject)
+          expect(v1).toEqual(v2)
+        })
+
+        it('> if nehubaConfig is undefined', () => {
+          const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, undefined)
+          const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject)
+          expect(v1).toEqual(v2)
+        })
+
+        it('> if nehubaConfig is otherwise malformed', () => {
+          const v1 = cvtNavigationObjToNehubaConfig(validNavigationObj, {})
+          const v2 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject)
+          expect(v1).toEqual(v2)
+
+          const v3 = cvtNavigationObjToNehubaConfig(validNavigationObj, reconstitutedBigBrain)
+          const v4 = cvtNavigationObjToNehubaConfig(validNavigationObj, defaultNehubaConfigObject)
+          expect(v3).toEqual(v4)
+        })
+      })
+    })
+    it('> converts navigation object and reference nehuba config object to navigation object', () => {
+      const convertedVal = cvtNavigationObjToNehubaConfig(validNavigationObj, validNehubaConfigObj)
+      const { perspectiveOrientation, orientation, zoom, perspectiveZoom, position } = validNavigationObj
+      
+      expect(convertedVal).toEqual({
+        navigation: {
+          pose: {
+            position: {
+              voxelSize: validNehubaConfigObj.navigation.pose.position.voxelSize,
+              voxelCoordinates: [0, 1, 2].map(idx => position[idx] / validNehubaConfigObj.navigation.pose.position.voxelSize[idx])
+            },
+            orientation
+          },
+          zoomFactor: zoom
+        },
+        perspectiveOrientation: perspectiveOrientation,
+        perspectiveZoom: perspectiveZoom
+      })
     })
   })
-})
\ No newline at end of file
+})
diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts
index 8edba98ef..12ac1882e 100644
--- a/src/ui/viewerStateController/viewerState.useEffect.ts
+++ b/src/ui/viewerStateController/viewerState.useEffect.ts
@@ -2,15 +2,96 @@ import { Injectable, OnDestroy } from "@angular/core";
 import { Actions, Effect, ofType } from "@ngrx/effects";
 import { Action, select, Store } from "@ngrx/store";
 import { Observable, Subscription, of, merge } from "rxjs";
-import { distinctUntilChanged, filter, map, shareReplay, withLatestFrom, switchMap, mapTo } from "rxjs/operators";
-import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
+import { distinctUntilChanged, filter, map, shareReplay, withLatestFrom, switchMap, mapTo, startWith } from "rxjs/operators";
 import { CHANGE_NAVIGATION, FETCHED_TEMPLATE, IavRootStoreInterface, NEWVIEWER, SELECT_PARCELLATION, SELECT_REGIONS, generalActionError } from "src/services/stateStore.service";
 import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "./viewerState.base";
 import { TemplateCoordinatesTransformation } from "src/services/templateCoordinatesTransformation.service";
 import { CLEAR_STANDALONE_VOLUMES } from "src/services/state/viewerState.store";
-import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector } from "src/services/state/viewerState.store.helper";
+import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName } from "src/services/state/viewerState.store.helper";
 import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors";
 import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions";
+import { PureContantService } from "src/util";
+
+const defaultPerspectiveZoom = 1e6
+const defaultZoom = 1e6
+
+export const defaultNavigationObject = {
+  orientation: [0, 0, 0, 1],
+  perspectiveOrientation: [0 , 0, 0, 1],
+  perspectiveZoom: defaultPerspectiveZoom,
+  zoom: defaultZoom,
+  position: [0, 0, 0],
+  positionReal: true
+}
+
+export const defaultNehubaConfigObject = {
+  perspectiveOrientation: [0, 0, 0, 1],
+  perspectiveZoom: 1e6,
+  navigation: {
+    pose: {
+      position: {
+        voxelCoordinates: [0, 0, 0],
+        voxelSize: [1,1,1]
+      },
+      orientation: [0, 0, 0, 1],
+    },
+    zoomFactor: defaultZoom
+  }
+}
+
+export function cvtNehubaConfigToNavigationObj(nehubaConfig?){
+  const { navigation, perspectiveOrientation = [0, 0, 0, 1], perspectiveZoom = 1e6 } = nehubaConfig || {}
+  const { pose, zoomFactor = 1e6 } = navigation || {}
+  const { position, orientation = [0, 0, 0, 1] } = pose || {}
+  const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position || {}
+
+  return {
+    orientation,
+    perspectiveOrientation: perspectiveOrientation,
+    perspectiveZoom: perspectiveZoom,
+    zoom: zoomFactor,
+    position: [0, 1, 2].map(idx => voxelSize[idx] * voxelCoordinates[idx]),
+    positionReal: true
+  }
+}
+
+export function cvtNavigationObjToNehubaConfig(navigationObj, nehubaConfigObj){
+  const {
+    orientation = [0, 0, 0, 1],
+    perspectiveOrientation = [0, 0, 0, 1],
+    perspectiveZoom = 1e6,
+    zoom = 1e6,
+    position = [0, 0, 0],
+    positionReal = true,
+  } = navigationObj || {}
+
+  const voxelSize = (() => {
+    const {
+      navigation = {}
+    } = nehubaConfigObj || {}
+    const { pose = {}, zoomFactor = 1e6 } = navigation
+    const { position = {}, orientation = [0, 0, 0, 1] } = pose
+    const { voxelSize = [1, 1, 1], voxelCoordinates = [0, 0, 0] } = position
+    return voxelSize
+  })()
+
+  return {
+    perspectiveOrientation,
+    perspectiveZoom,
+    navigation: {
+      pose: {
+        position: {
+          voxelCoordinates: positionReal
+            ? [0, 1, 2].map(idx => position[idx] / voxelSize[idx])
+            : position,
+          voxelSize
+        },
+        orientation,
+      },
+      zoomFactor: zoom
+    }
+  }
+}
 
 @Injectable({
   providedIn: 'root',
@@ -23,7 +104,7 @@ export class ViewerStateControllerUseEffect implements OnDestroy {
   private selectedRegions$: Observable<any[]>
 
   @Effect()
-  public init$ = this.constantSerivce.initFetchTemplate$.pipe(
+  public init$ = this.pureService.initFetchTemplate$.pipe(
     map(fetchedTemplate => {
       return {
         type: FETCHED_TEMPLATE,
@@ -35,8 +116,154 @@ export class ViewerStateControllerUseEffect implements OnDestroy {
   @Effect()
   public selectParcellation$: Observable<any>
 
+  private selectTemplateIntent$: Observable<any> = merge(
+    this.actions$.pipe(
+      ofType(viewerStateSelectTemplateWithId.type),
+      map(({ payload, config }) => {
+        return {
+          templateId: payload['@id'],
+          parcellationId: config && config['selectParcellation'] && config['selectParcellation']['@id']
+        }
+      })
+    ),
+    this.actions$.pipe(
+      ofType(viewerStateSelectTemplateWithName),
+      withLatestFrom(this.store$.pipe(
+        select(viewerStateFetchedTemplatesSelector)
+      )),
+      map(([ action, fetchedTemplates ]) => {
+        const templateName = (action as any).payload.name
+        const foundTemplate = fetchedTemplates.find(t => t.name === templateName)
+        return foundTemplate && foundTemplate['@id']
+      }),
+      filter(v => !!v),
+      map(templateId => {
+        return { templateId, parcellationId: null }
+      })
+    )
+  )
+
   @Effect()
-  public selectTemplate$: Observable<any>
+  public selectTemplate$: Observable<any> = this.selectTemplateIntent$.pipe(
+    withLatestFrom(
+      this.store$.pipe(
+        select(viewerStateFetchedTemplatesSelector)
+      ),
+      this.store$.pipe(
+        select(viewerStateSelectedParcellationSelector)
+      )
+    ),
+    map(([ { templateId, parcellationId }, fetchedTemplates, parcellationSelected ]) => {
+      /**
+       * find the correct template & parcellation from their IDs
+       */
+
+      /**
+       * for template, just look for the new id in fetched templates
+       */
+      const newTemplateTobeSelected = fetchedTemplates.find(t => t['@id'] === templateId)
+      if (!newTemplateTobeSelected) {
+        return {
+          selectTemplate: null,
+          selectParcellation: null,
+          errorMessage: `Selected templateId ${templateId} not found.`
+        }
+      }
+
+      /**
+       * for parcellation,
+       * if new parc id is defined, try to find the corresponding parcellation in the new template
+       * if above fails, try to find the corresponding parcellation of the currently selected parcellation
+       * if the above fails, select the first parcellation in the new template
+       */
+      const selectParcellationWithTemplate = (parcellationId && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationId))
+        || (parcellationSelected && parcellationSelected['@id'] && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationSelected['@id']))
+        || newTemplateTobeSelected.parcellations[0]
+
+      return {
+        selectTemplate: newTemplateTobeSelected,
+        selectParcellation: selectParcellationWithTemplate
+      }
+    }),
+    withLatestFrom(
+      this.store$.pipe(
+        select(viewerStateSelectedTemplateSelector),
+        startWith(<any>null),
+      ),
+      this.store$.pipe(
+        select(viewerStateNavigationStateSelector),
+        startWith(<any>null),
+      )
+    ),
+    switchMap(([{ selectTemplate, selectParcellation, errorMessage }, lastSelectedTemplate, navigation]) => {
+      /**
+       * if selectTemplate is undefined (cannot find template with id)
+       */
+      if (errorMessage) {
+        return of(generalActionError({
+          message: errorMessage || 'Switching template error.',
+        }))
+      }
+      /**
+       * if there were no template selected last
+       * simply return selectTemplate object
+       */
+      if (!lastSelectedTemplate) {
+        return of(viewerStateNewViewer({
+          navigation: {},
+          selectParcellation,
+          selectTemplate
+        }))
+      }
+
+      /**
+       * if there were template selected last, extract navigation info
+       */
+      const previousNavigation = (navigation && Object.keys(navigation).length > 0 && navigation) || cvtNehubaConfigToNavigationObj(lastSelectedTemplate.nehubaConfig?.dataset?.initialNgState)
+      return this.coordinatesTransformation.getPointCoordinatesForTemplate(lastSelectedTemplate.name, selectTemplate.name, previousNavigation.position).pipe(
+        map(({ status, result }) => {
+
+          /**
+           * if getPointCoordinatesForTemplate returns error, simply load the temp/parc
+           */
+          if (status === 'error') {
+            return viewerStateNewViewer({
+              navigation: {},
+              selectParcellation,
+              selectTemplate
+            })
+          }
+
+          /**
+           * otherwise, copy the nav state to templateSelected
+           * deepclone of json object is required, or it will mutate the fetchedTemplate
+           * setting navigation sometimes creates a race con, as creating nehubaViewer is not sync
+           */
+          const deepCopiedState = JSON.parse(JSON.stringify(selectTemplate))
+          const initialNgState = deepCopiedState.nehubaConfig.dataset.initialNgState
+
+          const newInitialNgState = cvtNavigationObjToNehubaConfig({
+            ...previousNavigation,
+            position: result
+          }, initialNgState)
+
+          /**
+           * mutation of initialNgState is expected here
+           */
+          deepCopiedState.nehubaConfig.dataset.initialNgState = {
+            ...initialNgState,
+            ...newInitialNgState
+          }
+
+          return viewerStateNewViewer({
+            selectTemplate: deepCopiedState,
+            selectParcellation,
+            navigation: {}
+          })
+        })
+      )
+    })
+  )
 
   @Effect()
   public toggleRegionSelection$: Observable<any>
@@ -67,7 +294,7 @@ export class ViewerStateControllerUseEffect implements OnDestroy {
   constructor(
     private actions$: Actions,
     private store$: Store<IavRootStoreInterface>,
-    private constantSerivce: AtlasViewerConstantsServices,
+    private pureService: PureContantService,
     private coordinatesTransformation: TemplateCoordinatesTransformation
   ) {
     const viewerState$ = this.store$.pipe(
@@ -135,121 +362,6 @@ export class ViewerStateControllerUseEffect implements OnDestroy {
       })
     )
 
-    /**
-     * merge all sources into single stream consisting of template id's
-     */
-    this.selectTemplate$ = merge(
-      this.actions$.pipe(
-        ofType(viewerStateSelectTemplateWithId.type),
-        map(({ payload, config }) => {
-          return {
-            templateId: payload['@id'],
-            parcellationId: config && config['selectParcellation'] && config['selectParcellation']['@id']
-          }
-        })
-      ),
-      this.actions$.pipe(
-        ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.SELECT_TEMPLATE_WITH_NAME),
-        withLatestFrom(viewerState$.pipe(
-          select('fetchedTemplates')
-        )),
-        map(([ action, fetchedTemplates ]) => {
-          const templateName = (action as any).payload.name
-          const foundTemplate = fetchedTemplates.find(t => t.name === templateName)
-          return foundTemplate && foundTemplate['@id']
-        }),
-        filter(v => !!v),
-        map(templateId => {
-          return { templateId, parcellationId: null }
-        })
-      )
-    ).pipe(
-
-      withLatestFrom(
-        viewerState$
-      ),
-      switchMap(([{ templateId: newTemplateId, parcellationId: newParcellationId }, { templateSelected, fetchedTemplates, navigation, parcellationSelected }]) => {
-        if (!templateSelected) {
-          return of({
-            newTemplateId,
-            templateSelected: templateSelected,
-            fetchedTemplates,
-            translatedCoordinate: null,
-            navigation,
-            newParcellationId,
-            parcellationSelected
-          })
-        }
-        const position = (navigation && navigation.position) || [0, 0, 0]
-        if (newTemplateId === templateSelected['@id']) return of(null)
-
-        const newTemplateName = fetchedTemplates.find(t => t['@id'] === newTemplateId).name
-
-        return this.coordinatesTransformation.getPointCoordinatesForTemplate(templateSelected.name, newTemplateName, position).pipe(
-          map(({ status, statusText, result }) => {
-            if (status === 'error') {
-              return {
-                newTemplateId,
-                templateSelected: templateSelected,
-                fetchedTemplates,
-                translatedCoordinate: null,
-                navigation,
-                newParcellationId,
-                parcellationSelected
-              }
-            }
-            return {
-              newTemplateId,
-              templateSelected: templateSelected,
-              fetchedTemplates,
-              translatedCoordinate: result,
-              navigation,
-              newParcellationId,
-              parcellationSelected
-            }
-          })
-        )
-      }),
-      filter(v => !!v),
-      map(({ newTemplateId, templateSelected, newParcellationId, fetchedTemplates, translatedCoordinate, navigation, parcellationSelected }) => {
-        const newTemplateTobeSelected = fetchedTemplates.find(t => t['@id'] === newTemplateId)
-        if (!newTemplateTobeSelected) {
-          return generalActionError({
-            message: 'Selected template not found.'
-          })
-        }
-
-        const selectParcellationWithTemplate = (newParcellationId && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === newParcellationId))
-          || (parcellationSelected && parcellationSelected['@id'] && newTemplateTobeSelected['parcellations'].find(p => p['@id'] === parcellationSelected['@id']))
-          || newTemplateTobeSelected.parcellations[0]
-
-        if (!translatedCoordinate) {
-          return {
-            type: NEWVIEWER,
-            selectTemplate: newTemplateTobeSelected,
-            selectParcellation: selectParcellationWithTemplate,
-          }
-        }
-        const deepCopiedState = JSON.parse(JSON.stringify(newTemplateTobeSelected))
-        const initNavigation = deepCopiedState.nehubaConfig.dataset.initialNgState.navigation
-
-        const { zoom = null, orientation = null } = navigation || {}
-        if (zoom) initNavigation.zoomFactor = zoom
-        if (orientation) initNavigation.pose.orientation = orientation
-
-        for (const idx of [0, 1, 2]) {
-          initNavigation.pose.position.voxelCoordinates[idx] = translatedCoordinate[idx] / initNavigation.pose.position.voxelSize[idx]
-        }
-
-        return {
-          type: NEWVIEWER,
-          selectTemplate: deepCopiedState,
-          selectParcellation: selectParcellationWithTemplate,
-        }
-      }),
-    )
-
-
     this.navigateToRegion$ = this.actions$.pipe(
       ofType(viewerStateNavigateToRegion),
       map(action => {
diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts
index 5cdd0b0cf..2dc6b8aa9 100644
--- a/src/util/pureConstant.service.ts
+++ b/src/util/pureConstant.service.ts
@@ -1,11 +1,15 @@
 import { Injectable, OnDestroy } from "@angular/core";
 import { Store, createSelector, select } from "@ngrx/store";
-import { Observable, merge, Subscription, of } from "rxjs";
+import { Observable, merge, Subscription, of, throwError, forkJoin, fromEvent } from "rxjs";
 import { VIEWER_CONFIG_FEATURE_KEY, IViewerConfigState } from "src/services/state/viewerConfig.store.helper";
-import { shareReplay, tap, scan, catchError, filter, mergeMap, switchMapTo, switchMap } from "rxjs/operators";
+import { shareReplay, tap, scan, catchError, filter, mergeMap, switchMapTo, switchMap, map, take } from "rxjs/operators";
 import { HttpClient } from "@angular/common/http";
 import { BACKENDURL } from './constants'
 import { viewerStateSetFetchedAtlases } from "src/services/state/viewerState.store.helper";
+import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
+import { LoggingService } from "src/logging";
+
+const getUniqueId = () => Math.round(Math.random() * 1e16).toString(16)
 
 @Injectable({
   providedIn: 'root'
@@ -24,9 +28,81 @@ export class PureContantService implements OnDestroy{
     (state: IViewerConfigState) => state.useMobileUI
   )
 
+  public backendUrl = (BACKEND_URL && `${BACKEND_URL}/`.replace(/\/\/$/, '/')) || `${window.location.origin}${window.location.pathname}`
+
+  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,
+          }
+        }),
+      )
+      }
+      throwError('neither nehubaConfig nor nehubaConfigURL defined')
+    }),
+  )
+
+  private processTemplate = template => forkJoin(
+    template.parcellations.map(parcellation => {
+
+      const id = getUniqueId()
+
+      this.workerService.worker.postMessage({
+        type: 'PROPAGATE_PARC_REGION_ATTR',
+        parcellation,
+        inheritAttrsOpts: {
+          ngId: (parcellation as any ).ngId,
+          relatedAreas: [],
+          fullId: null
+        },
+        id
+      })
+
+      return this.workerUpdateParcellation$.pipe(
+        filter(({ id: returnedId }) => id === returnedId),
+        take(1),
+        map(({ parcellation }) => parcellation)
+      )
+    })
+  )
+
+  public getTemplateEndpoint$ = this.http.get<any[]>(`${this.backendUrl}templates`, { responseType: 'json' }).pipe(
+    shareReplay(1)
+  )
+
+  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)
+    }),
+  )
+
   constructor(
     private store: Store<any>,
     private http: HttpClient,
+    private log: LoggingService,
+    private workerService: AtlasWorkerService,
   ){
     this.darktheme$ = this.store.pipe(
       select(state => state?.viewerState?.templateSelected?.useTheme === 'dark')
-- 
GitLab