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