From b203e1e2ce15ff65c4798287a1b17481834df9f1 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Mon, 15 Mar 2021 18:35:45 +0100
Subject: [PATCH] chore: fix standalone volumes chore: fix e2e

---
 deploy/bkwdCompat/urlState.js                 |   4 +-
 src/routerModule/router.service.spec.ts       |  84 +++++++++++--
 src/routerModule/router.service.ts            |  28 +++--
 src/routerModule/type.ts                      |   6 +-
 src/routerModule/util.spec.ts                 |  51 +++++++-
 src/routerModule/util.ts                      |  33 +++--
 .../effects/viewerState.useEffect.spec.ts     |  72 +----------
 src/state/effects/viewerState.useEffect.ts    |  39 +-----
 src/state/index.ts                            |   1 -
 src/util/fn.ts                                |   7 ++
 .../nehubaViewerGlue.component.spec.ts        |   3 +-
 .../nehubaViewerGlue.component.ts             |  26 +++-
 .../nehubaViewerInterface.directive.ts        |   3 +-
 src/viewerModule/nehuba/util.spec.ts          | 119 ++++++++++++++++++
 src/viewerModule/nehuba/util.ts               |  40 +++++-
 .../viewerCmp/viewerCmp.component.ts          |  18 +--
 16 files changed, 373 insertions(+), 161 deletions(-)
 create mode 100644 src/viewerModule/nehuba/util.spec.ts

diff --git a/deploy/bkwdCompat/urlState.js b/deploy/bkwdCompat/urlState.js
index 3b2a885ce..e63fcd235 100644
--- a/deploy/bkwdCompat/urlState.js
+++ b/deploy/bkwdCompat/urlState.js
@@ -208,10 +208,10 @@ module.exports = (query, _warningCb) => {
 
   let redirectUrl = '/#'
   if (standaloneVolumes) {
-    redirectUrl += `/sv:${encodeURIComponent(standaloneVolumes)}`
+    searchParam.set('standaloneVolumes', standaloneVolumes)
     if (nav) redirectUrl += nav
     if (dsp) redirectUrl += dsp
-    if (Array.from(searchParam.keys()).length > 0) redirectUrl += `?${searchParam.toString()}`
+    if (Array.from(searchParam.keys()).length > 0) redirectUrl += `/?${searchParam.toString()}`
     return redirectUrl
   }
 
diff --git a/src/routerModule/router.service.spec.ts b/src/routerModule/router.service.spec.ts
index 6a6b198a4..5507f4a5b 100644
--- a/src/routerModule/router.service.spec.ts
+++ b/src/routerModule/router.service.spec.ts
@@ -21,8 +21,8 @@ let router: Router
 describe('> router.service.ts', () => {
   describe('> RouterService', () => {
     beforeEach(() => {
-      cvtStateToHashedRoutesSpy= jasmine.createSpy('cvtStateToHashedRoutesSpy')
-      cvtFullRouteToStateSpy= jasmine.createSpy('cvtFullRouteToState')
+      cvtStateToHashedRoutesSpy = jasmine.createSpy('cvtStateToHashedRoutesSpy')
+      cvtFullRouteToStateSpy = jasmine.createSpy('cvtFullRouteToState')
 
       spyOnProperty(util, 'cvtStateToHashedRoutes').and.returnValue(cvtStateToHashedRoutesSpy)
       spyOnProperty(util, 'cvtFullRouteToState').and.returnValue(cvtFullRouteToStateSpy)
@@ -57,7 +57,7 @@ describe('> router.service.ts', () => {
     describe('> on state set', () => {
 
       it('> should call cvtStateToHashedRoutes', fakeAsync(() => {
-        cvtStateToHashedRoutesSpy.and.callFake(() => [])
+        cvtStateToHashedRoutesSpy.and.callFake(() => ``)
         const service = TestBed.inject(RouterService)
         const store = TestBed.inject(MockStore)
         const fakeState = {
@@ -86,7 +86,7 @@ describe('> router.service.ts', () => {
       }))
       it('> if cvtStateToHashedRoutes returns, should navigate to expected location', fakeAsync(() => {
         cvtStateToHashedRoutesSpy.and.callFake(() => {
-          return ['foo', 'bar']
+          return `foo/bar`
         })
         const service = TestBed.inject(RouterService)
         const store = TestBed.inject(MockStore)
@@ -100,11 +100,76 @@ describe('> router.service.ts', () => {
           location.path()
         ).toBe('/foo/bar')
       }))
+    
+      describe('> does not excessively call navigateByUrl', () => {
+        let navigateSpy: jasmine.Spy
+        let navigateByUrlSpy: jasmine.Spy
+        beforeEach(() => {
+          const router = TestBed.inject(Router)
+          navigateSpy = spyOn(router, 'navigate').and.callThrough()
+          navigateByUrlSpy = spyOn(router, 'navigateByUrl').and.callThrough()
+        })
+        afterEach(() => {
+          navigateSpy.calls.reset()
+          navigateByUrlSpy.calls.reset()
+        })
+
+        it('> navigate calls navigateByUrl', fakeAsync(() => {
+          cvtStateToHashedRoutesSpy.and.callFake(() => {
+            return `foo/bar`
+          })
+          TestBed.inject(RouterService)
+          const store = TestBed.inject(MockStore)
+          store.setState({
+            'hello': 'world'
+          })
+          tick(320)
+          expect(cvtStateToHashedRoutesSpy).toHaveBeenCalledTimes(1 + 1)
+          expect(navigateByUrlSpy).toHaveBeenCalledTimes(1)
+        }))
+
+        it('> same state should not navigate', fakeAsync(() => {
+          cvtStateToHashedRoutesSpy.and.callFake(() => {
+            return `foo/bar`
+          })
+          
+          TestBed.inject(RouterService)
+          const router = TestBed.inject(Router)
+          router.navigate(['foo', 'bar'])
+          const store = TestBed.inject(MockStore)
+          store.setState({
+            'hello': 'world'
+          })
+          tick(320)
+          expect(cvtStateToHashedRoutesSpy).toHaveBeenCalledTimes(1 + 1)
+          expect(navigateByUrlSpy).toHaveBeenCalledTimes(1)
+        }))
+
+        it('> should handle queryParam gracefully', fakeAsync(() => {
+          const searchParam = new URLSearchParams()
+          const sv = '["precomputed://https://object.cscs.ch/v1/AUTH_08c08f9f119744cbbf77e216988da3eb/imgsvc-46d9d64f-bdac-418e-a41b-b7f805068c64"]'
+          searchParam.set('standaloneVolumes', sv)
+          cvtStateToHashedRoutesSpy.and.callFake(() => {
+            return `foo/bar?${searchParam.toString()}`
+          })
+          TestBed.inject(RouterService)
+          const store = TestBed.inject(MockStore)
+
+          TestBed.inject(RouterService)
+          const router = TestBed.inject(Router)
+          router.navigate(['foo', `bar`], { queryParams: { standaloneVolumes: sv }})
+          store.setState({
+            'hello': 'world'
+          })
+          tick(320)
+          expect(cvtStateToHashedRoutesSpy).toHaveBeenCalledTimes(1 + 1)
+          expect(navigateByUrlSpy).toHaveBeenCalledTimes(1)
+        }))
+      })
     })
   
     describe('> on route change', () => {
 
-
       describe('> compares new state and previous state', () => {
 
         it('> calls cvtFullRouteToState', fakeAsync(() => {
@@ -185,7 +250,7 @@ describe('> router.service.ts', () => {
             }
             cvtFullRouteToStateSpy.and.callFake(() => fakeParsedState)
             cvtStateToHashedRoutesSpy.and.callFake(() => {
-              return ['fizz', 'buzz']
+              return `fizz/buzz`
             })
             router = TestBed.inject(Router)
             router.navigate(['foo', 'bar'])
@@ -194,7 +259,7 @@ describe('> router.service.ts', () => {
             const store = TestBed.inject(MockStore)
             const dispatchSpy = spyOn(store, 'dispatch')
             
-            tick()
+            tick(320)
 
             expect(dispatchSpy).toHaveBeenCalled()
     
@@ -207,17 +272,16 @@ describe('> router.service.ts', () => {
             }
             cvtFullRouteToStateSpy.and.callFake(() => fakeParsedState)
             cvtStateToHashedRoutesSpy.and.callFake(() => {
-              return ['foo', 'bar']
+              return `foo/bar`
             })
             router = TestBed.inject(Router)
             router.navigate(['foo', 'bar'])
     
             const service = TestBed.inject(RouterService)
-            service['firstRenderFlag'] = false
             const store = TestBed.inject(MockStore)
             const dispatchSpy = spyOn(store, 'dispatch')
             
-            tick()
+            tick(320)
 
             expect(dispatchSpy).not.toHaveBeenCalled()
     
diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts
index 9f3871aab..ff625f4d4 100644
--- a/src/routerModule/router.service.ts
+++ b/src/routerModule/router.service.ts
@@ -49,14 +49,15 @@ export class RouterService {
     ).subscribe(([ev, state]: [NavigationEnd, any]) => {
       const fullPath = ev.urlAfterRedirects
       const stateFromRoute = cvtFullRouteToState(router.parseUrl(fullPath), state, this.logError)
-      let routeFromState: string[]
+      let routeFromState: string
       try {
         routeFromState = cvtStateToHashedRoutes(state)
       } catch (_e) {
-        routeFromState = []
+        routeFromState = ``
       }
 
-      if ( fullPath !== `/${routeFromState.join('/')}`) {
+      if ( fullPath !== `/${routeFromState}`) {
+        console.log(`apply state`, stateFromRoute)
         store$.dispatch(
           generalApplyState({
             state: stateFromRoute
@@ -76,18 +77,27 @@ export class RouterService {
             try {
               return cvtStateToHashedRoutes(state)
             } catch (e) {
-              return []
+              this.logError(e)
+              return ``
             }
           })
         )
       )
-    ).subscribe(routes => {
-      if (routes.length === 0) {
+    ).subscribe(routePath => {
+      if (routePath === '') {
         router.navigate([ baseHref ])
       } else {
-        const currUrl = router.routerState.snapshot.url
-        const joinedRoutes = `/${routes.join('/')}`
-        if (currUrl !== joinedRoutes) {
+
+        // this needs to be done, because, for some silly reasons
+        // router decodes encoded ':' character
+        // this means, if url is compared with url, it will always be falsy
+        // if a non encoded ':' exists
+        const currUrlUrlTree = router.parseUrl(router.url)
+        const joinedRoutes = `/${routePath}`
+        const newUrlUrlTree = router.parseUrl(joinedRoutes)
+        
+        if (currUrlUrlTree.toString() !== newUrlUrlTree.toString()) {
+          console.log(`navigate\n${currUrlUrlTree.toString()}\n${newUrlUrlTree.toString()}`)
           router.navigateByUrl(joinedRoutes)
         }
       }
diff --git a/src/routerModule/type.ts b/src/routerModule/type.ts
index d50301fad..fa8855ad8 100644
--- a/src/routerModule/type.ts
+++ b/src/routerModule/type.ts
@@ -27,4 +27,8 @@ export type TConditional<T> = Partial<
   TUrlNav<T>
 >
 
-export type TUrlPathObj<T, V> =  (V extends TUrlStandaloneVolume<T> ? TUrlStandaloneVolume<T> : TUrlAtlas<T>) & TConditional<T>
+export type TUrlPathObj<T, V> = 
+  (V extends TUrlStandaloneVolume<T>
+      ? TUrlStandaloneVolume<T>
+      : TUrlAtlas<T>)
+  & TConditional<T>
diff --git a/src/routerModule/util.spec.ts b/src/routerModule/util.spec.ts
index a97938a0e..928defbec 100644
--- a/src/routerModule/util.spec.ts
+++ b/src/routerModule/util.spec.ts
@@ -2,16 +2,65 @@ import { TestBed } from '@angular/core/testing'
 import { MockStore, provideMockStore } from '@ngrx/store/testing'
 import { uiStatePreviewingDatasetFilesSelector } from 'src/services/state/uiState/selectors'
 import { viewerStateGetSelectedAtlas, viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector, viewerStateSelectorNavigation, viewerStateSelectorStandaloneVolumes } from 'src/services/state/viewerState/selectors'
-import { cvtStateToHashedRoutes } from './util'
+import { cvtFullRouteToState, cvtStateToHashedRoutes, DummyCmp, routes } from './util'
 import { encodeNumber } from './cipher'
+import { Router } from '@angular/router'
+import { RouterTestingModule } from '@angular/router/testing'
 
 describe('> util.ts', () => {
   describe('> cvtFullRouteToState', () => {
+
+    beforeEach(() => {
+      TestBed.configureTestingModule({
+        imports: [
+          RouterTestingModule.withRoutes(routes, {
+            useHash: true
+          })
+        ],
+        declarations: [
+          DummyCmp
+        ]
+      })
+    })
     beforeEach(() => {
     })
     it('> should be able to decode region properly', () => {
 
     })
+
+    describe('> decode sv', () => {
+      let sv: any
+      beforeEach(() => {
+        const searchParam = new URLSearchParams()
+        searchParam.set('standaloneVolumes', '["precomputed://https://object.cscs.ch/v1/AUTH_08c08f9f119744cbbf77e216988da3eb/imgsvc-46d9d64f-bdac-418e-a41b-b7f805068c64"]')
+        const svRoute = `/?${searchParam.toString()}`
+        const router = TestBed.inject(Router)
+        const parsedUrl = router.parseUrl(svRoute)
+        const returnState = cvtFullRouteToState(parsedUrl, {})
+        sv = returnState?.viewerState?.standaloneVolumes
+      })
+
+      it('> sv should be truthy', () => {
+        expect(sv).toBeTruthy()
+      })
+
+      it('> sv should be array', () => {
+        expect(
+          Array.isArray(sv)
+        ).toBeTrue()
+      })
+
+      it('> sv should have length 1', () => {
+        expect(sv.length).toEqual(1)
+      })
+
+      it('> sv[0] should be expected value', () => {
+        expect(sv[0]).toEqual(
+          'precomputed://https://object.cscs.ch/v1/AUTH_08c08f9f119744cbbf77e216988da3eb/imgsvc-46d9d64f-bdac-418e-a41b-b7f805068c64'
+        )
+      })
+    })
+
   })
 
   describe('> cvtStateToHashedRoutes', () => {
diff --git a/src/routerModule/util.ts b/src/routerModule/util.ts
index f0fa9f281..6f93f0da7 100644
--- a/src/routerModule/util.ts
+++ b/src/routerModule/util.ts
@@ -19,7 +19,12 @@ import {
   encodeId,
 } from './parseRouteToTmplParcReg'
 
-const endcodePath = (key: string, val: string) => `${key}:${encodeURI(val)}`
+const endcodePath = (key: string, val: string|string[]) =>
+  key[0] === '?'
+    ? `?${key}=${val}`
+    : `${key}:${Array.isArray(val)
+      ? val.map(v => encodeURI(v)).join('::')
+      : encodeURI(val)}`
 const decodePath = (path: string) => {
   const re = /^(.*?):(.*?)$/.exec(path)
   if (!re) return null
@@ -143,9 +148,10 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun
   // only load sv in state
   // ignore all other params
   // /#/sv:%5B%22precomputed%3A%2F%2Fhttps%3A%2F%2Fobject.cscs.ch%2Fv1%2FAUTH_08c08f9f119744cbbf77e216988da3eb%2Fimgsvc-46d9d64f-bdac-418e-a41b-b7f805068c64%22%5D
-  if (!!returnObj['sv']) {
+  const standaloneVolumes = fullPath.queryParams['standaloneVolumes']
+  if (!!standaloneVolumes) {
     try {
-      const parsedArr = JSON.parse(returnObj['sv'])
+      const parsedArr = JSON.parse(standaloneVolumes)
       if (!Array.isArray(parsedArr)) throw new Error(`Parsed standalone volumes not of type array`)
 
       returnState['viewerState']['standaloneVolumes'] = parsedArr
@@ -193,7 +199,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun
   return returnState
 }
 
-export const cvtStateToHashedRoutes = (state): string[] => {
+export const cvtStateToHashedRoutes = (state): string => {
   // TODO check if this causes memleak
   const selectedAtlas = viewerStateGetSelectedAtlas(state)
   const selectedTemplate = viewerStateSelectedTemplateSelector(state)
@@ -204,6 +210,8 @@ export const cvtStateToHashedRoutes = (state): string[] => {
 
   const previewingDatasetFiles = uiStatePreviewingDatasetFilesSelector(state)
   let dsPrvString: string
+  const searchParam = new URLSearchParams()
+
   if (previewingDatasetFiles && Array.isArray(previewingDatasetFiles)) {
     const dsPrvArr = []
     const datasetPreviews = (previewingDatasetFiles as {datasetId: string, filename: string}[])
@@ -258,24 +266,25 @@ export const cvtStateToHashedRoutes = (state): string[] => {
    * if any params needs to overwrite previosu routes, put them here
    */
   if (standaloneVolumes && Array.isArray(standaloneVolumes) && standaloneVolumes.length > 0) {
+    searchParam.set('standaloneVolumes', JSON.stringify(standaloneVolumes))
     routes = {
-      // standalone volumes
-      sv: encodeURIComponent(JSON.stringify(standaloneVolumes)),
       // nav
       ['@']: cNavString,
       dsp: dsPrvString && encodeURI(dsPrvString)
-    } as TUrlPathObj<string, TUrlStandaloneVolume<string>>
+    } as TUrlPathObj<string|string[], TUrlStandaloneVolume<string[]>>
   }
 
-  const returnRoutes = []
+  const routesArr: string[] = []
   for (const key in routes) {
     if (!!routes[key]) {
-      returnRoutes.push(
-        endcodePath(key, routes[key])
-      )
+      const segStr = endcodePath(key, routes[key])
+      routesArr.push(segStr)
     }
   }
-  return returnRoutes
+
+  return searchParam.toString() === '' 
+    ? routesArr.join('/')
+    : `${routesArr.join('/')}?${searchParam.toString()}`
 }
 
 @Component({
diff --git a/src/state/effects/viewerState.useEffect.spec.ts b/src/state/effects/viewerState.useEffect.spec.ts
index b6c0340d1..05adfe3a6 100644
--- a/src/state/effects/viewerState.useEffect.spec.ts
+++ b/src/state/effects/viewerState.useEffect.spec.ts
@@ -1,4 +1,4 @@
-import { cvtNavigationObjToNehubaConfig, cvtNehubaConfigToNavigationObj, ViewerStateControllerUseEffect, defaultNavigationObject, defaultNehubaConfigObject } from './viewerState.useEffect'
+import { cvtNehubaConfigToNavigationObj, ViewerStateControllerUseEffect, defaultNavigationObject } from './viewerState.useEffect'
 import { Observable, of } from 'rxjs'
 import { TestBed, async } from '@angular/core/testing'
 import { provideMockActions } from '@ngrx/effects/testing'
@@ -604,75 +604,5 @@ describe('> viewerState.useEffect.ts', () => {
       })
     })
   })
-  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
-      })
-    })
-  })
 })
diff --git a/src/state/effects/viewerState.useEffect.ts b/src/state/effects/viewerState.useEffect.ts
index f865242c5..814e32355 100644
--- a/src/state/effects/viewerState.useEffect.ts
+++ b/src/state/effects/viewerState.useEffect.ts
@@ -15,6 +15,7 @@ import { CONST } from 'common/constants'
 import { uiActionHideAllDatasets } from "src/services/state/uiState/actions";
 import { viewerStateFetchedAtlasesSelector } from "src/services/state/viewerState/selectors";
 import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions";
+import { cvtNavigationObjToNehubaConfig } from 'src/viewerModule/nehuba/util'
 
 const defaultPerspectiveZoom = 1e6
 const defaultZoom = 1e6
@@ -59,44 +60,6 @@ export function cvtNehubaConfigToNavigationObj(nehubaConfig?){
   }
 }
 
-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',
 })
diff --git a/src/state/index.ts b/src/state/index.ts
index 755a48414..778709a1c 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -1,6 +1,5 @@
 export { StateModule } from "./state.module"
 export {
   ViewerStateControllerUseEffect,
-  cvtNavigationObjToNehubaConfig,
   cvtNehubaConfigToNavigationObj,
 } from "./effects/viewerState.useEffect"
\ No newline at end of file
diff --git a/src/util/fn.ts b/src/util/fn.ts
index a62dd546b..f8f65694e 100644
--- a/src/util/fn.ts
+++ b/src/util/fn.ts
@@ -62,3 +62,10 @@ export const getGetRegionFromLabelIndexId = ({ parcellation }) => {
   return ({ labelIndexId }) =>
     recursiveFindRegionWithLabelIndexId({ regions, labelIndexId, inheritedNgId: defaultNgId })
 }
+
+type TPrimitive = string | number
+
+const include = <T extends TPrimitive>(el: T, arr: T[]) => arr.indexOf(el) >= 0
+export const arrayOfPrimitiveEqual = <T extends TPrimitive>(o: T[], n: T[]) =>
+  o.every(el => include(el, n))
+  && n.every(el => include(el, o))
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts
index 808e4e2e4..9865869b2 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts
@@ -6,7 +6,7 @@ import { PANELS } from "src/services/state/ngViewerState/constants"
 import { ngViewerSelectorOctantRemoval, ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from "src/services/state/ngViewerState/selectors"
 import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"
 import { viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"
-import { viewerStateCustomLandmarkSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors"
+import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors"
 import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"
 import { NehubaGlueCmp } from "./nehubaViewerGlue.component"
 
@@ -48,6 +48,7 @@ describe('> nehubaViewerGlue.component.ts', () => {
     mockStore.overrideSelector(viewerStateCustomLandmarkSelector, [])
     mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [])
     mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [])
+    mockStore.overrideSelector(viewerStateNavigationStateSelector, null)
   })
 
   it('> can be init', () => {
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
index baaf1b2ad..8085a4513 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
@@ -7,7 +7,7 @@ import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/sel
 import { debounceTime, distinctUntilChanged, filter, map, mapTo, scan, shareReplay, startWith, switchMap, switchMapTo, take, tap, throttleTime, withLatestFrom } from "rxjs/operators";
 import { viewerStateAddUserLandmarks, viewerStateChangeNavigation, viewerStateMouseOverCustomLandmark, viewerStateSelectRegionWithIdDeprecated, viewerStateSetSelectedRegions, viewreStateRemoveUserLandmarks } from "src/services/state/viewerState/actions";
 import { ngViewerSelectorLayers, ngViewerSelectorClearView, ngViewerSelectorPanelOrder, ngViewerSelectorOctantRemoval, ngViewerSelectorPanelMode } from "src/services/state/ngViewerState/selectors";
-import { viewerStateCustomLandmarkSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors";
+import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors";
 import { serialiseParcellationRegion } from 'common/util'
 import { ARIA_LABELS, IDS } from 'common/constants'
 import { PANELS } from "src/services/state/ngViewerState/constants";
@@ -17,7 +17,7 @@ import { getNgIds, getMultiNgIdsRegionsLabelIndexMap } from "../constants";
 import { IViewer, TViewerEvent } from "../../viewer.interface";
 import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component";
 import { NehubaViewerContainerDirective } from "../nehubaViewerInterface/nehubaViewerInterface.directive";
-import { calculateSliceZoomFactor, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn, takeOnePipe } from "../util";
+import { cvtNavigationObjToNehubaConfig, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree, NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn, takeOnePipe } from "../util";
 import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service";
 import { MouseHoverDirective } from "src/mouseoverModule";
 
@@ -68,6 +68,8 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{
   @Input()
   public selectedTemplate: any
 
+  private navigation: any
+
   private newViewer$ = new Subject()
 
   public showPerpsectiveScreen$: Observable<string>
@@ -200,12 +202,19 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{
     const template = (() => {
 
       const deepCopiedState = JSON.parse(JSON.stringify(_template))
-      const navigation = deepCopiedState.nehubaConfig.dataset.initialNgState.navigation
-      if (!navigation) {
+      const initialNgState = deepCopiedState.nehubaConfig.dataset.initialNgState
+
+      if (!initialNgState || !this.navigation) {
         return deepCopiedState
       }
-      navigation.zoomFactor = calculateSliceZoomFactor(navigation.zoomFactor)
-      deepCopiedState.nehubaConfig.dataset.initialNgState.navigation = navigation
+      const overwritingInitState = this.navigation
+        ? cvtNavigationObjToNehubaConfig(this.navigation, initialNgState)
+        : {}
+      
+      deepCopiedState.nehubaConfig.dataset.initialNgState = {
+        ...initialNgState,
+        ...overwritingInitState,
+      }
       return deepCopiedState
     })()
 
@@ -692,6 +701,11 @@ export class NehubaGlueCmp implements IViewer, OnChanges, OnDestroy{
     })
     this.onDestroyCb.push(() => setupViewerApiSub.unsubscribe())
   
+    // listen to navigation change from store
+    const navSub = this.store$.pipe(
+      select(viewerStateNavigationStateSelector)
+    ).subscribe(nav => this.navigation = nav)
+    this.onDestroyCb.push(() => navSub.unsubscribe())
   }
 
   handleViewerLoadedEvent(flag: boolean) {
diff --git a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts
index d997f740b..a4954ea99 100644
--- a/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts
+++ b/src/viewerModule/nehuba/nehubaViewerInterface/nehubaViewerInterface.directive.ts
@@ -12,6 +12,7 @@ import { ngViewerSelectorOctantRemoval } from "src/services/state/ngViewerState/
 import { LoggingService } from "src/logging";
 import { uiActionMouseoverLandmark, uiActionMouseoverSegments } from "src/services/state/uiState/actions";
 import { IViewerConfigState } from "src/services/state/viewerConfig.store.helper";
+import { arrayOfPrimitiveEqual } from 'src/util/fn'
 
 const defaultNehubaConfig = {
   "configName": "",
@@ -319,7 +320,7 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{
       this.store$.pipe(
         select(viewerStateStandAloneVolumes),
         filter(v => v && Array.isArray(v) && v.length > 0),
-        distinctUntilChanged()
+        distinctUntilChanged(arrayOfPrimitiveEqual)
       ).subscribe(async volumes => {
         const copiedNehubaConfig = JSON.parse(JSON.stringify(defaultNehubaConfig))
 
diff --git a/src/viewerModule/nehuba/util.spec.ts b/src/viewerModule/nehuba/util.spec.ts
new file mode 100644
index 000000000..5b23c2d66
--- /dev/null
+++ b/src/viewerModule/nehuba/util.spec.ts
@@ -0,0 +1,119 @@
+import { cvtNavigationObjToNehubaConfig } from './util'
+const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json')
+const bigBrainNehubaConfig = require('!json-loader!src/res/ext/bigbrainNehubaConfig.json')
+const reconstitutedBigBrain = JSON.parse(JSON.stringify(
+  {
+    ...bigbrainJson,
+    nehubaConfig: bigBrainNehubaConfig
+  }
+))
+const currentNavigation = {
+  position: [4, 5, 6],
+  orientation: [0, 0, 0, 1],
+  perspectiveOrientation: [ 0, 0, 0, 1],
+  perspectiveZoom: 2e5,
+  zoom: 1e5
+}
+
+const defaultPerspectiveZoom = 1e6
+const defaultZoom = 1e6
+
+const defaultNavigationObject = {
+  orientation: [0, 0, 0, 1],
+  perspectiveOrientation: [0 , 0, 0, 1],
+  perspectiveZoom: defaultPerspectiveZoom,
+  zoom: defaultZoom,
+  position: [0, 0, 0],
+  positionReal: true
+}
+
+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
+  }
+}
+
+describe('> util.ts', () => {
+  
+  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
+      })
+    })
+  })
+
+})
diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts
index 3849713a6..3da17749f 100644
--- a/src/viewerModule/nehuba/util.ts
+++ b/src/viewerModule/nehuba/util.ts
@@ -288,4 +288,42 @@ export const takeOnePipe = () => {
   )
 }
 
-export const NEHUBA_INSTANCE_INJTKN = new InjectionToken<Observable<NehubaViewerUnit>>('NEHUBA_INSTANCE_INJTKN')
\ No newline at end of file
+export const NEHUBA_INSTANCE_INJTKN = new InjectionToken<Observable<NehubaViewerUnit>>('NEHUBA_INSTANCE_INJTKN')
+
+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
+    }
+  }
+}
diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts
index 3cc2a4d6a..5c075f984 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.component.ts
+++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts
@@ -96,8 +96,17 @@ export class ViewerCmp implements OnDestroy {
     distinctUntilChanged(),
   )
 
-  public useViewer$: Observable<TSupportedViewer> = this.templateSelected$.pipe(
-    map(t => {
+  public isStandaloneVolumes$ = this.store$.pipe(
+    select(viewerStateStandAloneVolumes),
+    map(v => v.length > 0)
+  )
+
+  public useViewer$: Observable<TSupportedViewer> = combineLatest([
+    this.templateSelected$,
+    this.isStandaloneVolumes$,
+  ]).pipe(
+    map(([t, isSv]) => {
+      if (isSv) return 'nehuba'
       if (!t) return null
       if (!!t['nehubaConfigURL'] || !!t['nehubaConfig']) return 'nehuba'
       if (!!t['three-surfer']) return 'threeSurfer'
@@ -105,11 +114,6 @@ export class ViewerCmp implements OnDestroy {
     })
   )
 
-  public isStandaloneVolumes$ = this.store$.pipe(
-    select(viewerStateStandAloneVolumes),
-    map(v => v.length > 0)
-  )
-
   public selectedLayerVersions$ = this.store$.pipe(
     select(viewerStateParcVersionSelector),
     map(arr => arr.map(item => {
-- 
GitLab