diff --git a/src/glue.spec.ts b/src/glue.spec.ts index f1fc2ade86faed6b023c7218c7b105951a4c6778..1ddec928262742c199fcb195781f4aa3a1296fb3 100644 --- a/src/glue.spec.ts +++ b/src/glue.spec.ts @@ -11,6 +11,9 @@ import { getIdObj } from 'common/util' import { DS_PREVIEW_URL } from 'src/util/constants' import { NgLayersService } from "./ui/layerbrowser/ngLayerService.service" import { EnumColorMapName } from "./util/colorMaps" +import { ngViewerSelectorClearView } from "./services/state/ngViewerState/selectors" +import { tap, ignoreElements } from "rxjs/operators" +import { merge, of } from "rxjs" const mockActionOnSpyReturnVal0 = { id: getRandomHex(), @@ -53,6 +56,24 @@ const chart = { referenceSpaces: [] } +const region0 = { + name: 'region0', + originDatasets: [{ + kgId: getRandomHex(), + kgSchema: 'minds/core/dataset/v1.0.0', + filename: getRandomHex() + }] +} + +const region1 = { + name: 'name', + originDatasets: [{ + kgId: getRandomHex(), + kgSchema: 'minds/core/dataset/v1.0.0', + filename: getRandomHex() + }] +} + const file1 = { datasetId: getRandomHex(), filename: getRandomHex() @@ -74,6 +95,15 @@ const dataset1 = { describe('> glue.ts', () => { describe('> DatasetPreviewGlue', () => { + + const initialState = { + uiState: { + previewingDatasetFiles: [] + }, + viewerState: { + regionsSelected: [] + } + } beforeEach(() => { actionOnWidgetSpy = jasmine.createSpy('actionOnWidget').and.returnValues( mockActionOnSpyReturnVal0, @@ -87,11 +117,7 @@ describe('> glue.ts', () => { providers: [ DatasetPreviewGlue, provideMockStore({ - initialState: { - uiState: { - previewingDatasetFiles: [] - } - } + initialState }), { provide: ACTION_TO_WIDGET_TOKEN, @@ -452,6 +478,315 @@ describe('> glue.ts', () => { })) }) + + describe('> selectedRegionPreview$', () => { + it('> when one region with origindataset is selected, emits correctly', fakeAsync(() => { + + const store = TestBed.inject(MockStore) + const glue = TestBed.inject(DatasetPreviewGlue) + const ctrl = TestBed.inject(HttpTestingController) + store.overrideSelector(ngViewerSelectorClearView, false) + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [region1] + } + }) + + const { kgSchema, kgId, filename } = region1.originDatasets[0] + const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) + req.flush(nifti) + tick(200) + expect(glue.selectedRegionPreview$).toBeObservable( + hot('a', { + a: region1.originDatasets + }) + ) + })) + + it('> when regions are selected without originDatasets, emits empty array', () => { + + const store = TestBed.inject(MockStore) + const glue = TestBed.inject(DatasetPreviewGlue) + store.overrideSelector(ngViewerSelectorClearView, false) + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [{ + ...region0, + originDatasets: [] + }, { + ...region1, + originDatasets: [] + }] + } + }) + + expect(glue.selectedRegionPreview$).toBeObservable( + hot('a', { + a: [] + }) + ) + }) + + it('> if multiple region, each with origin datasets are selected, emit array', fakeAsync(() => { + + const store = TestBed.inject(MockStore) + const glue = TestBed.inject(DatasetPreviewGlue) + const ctrl = TestBed.inject(HttpTestingController) + store.overrideSelector(ngViewerSelectorClearView, false) + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [region0, region1] + } + }) + + const expectedOriginDatasets = [ + ...region0.originDatasets, + ...region1.originDatasets, + ] + + for (const { kgSchema, kgId, filename } of expectedOriginDatasets) { + const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) + req.flush(nifti) + } + tick(200) + expect(glue.selectedRegionPreview$).toBeObservable( + hot('a', { + a: expectedOriginDatasets + }) + ) + })) + + it('> if regions with multiple originDatasets are selected, emit array containing all origindatasets', fakeAsync(() => { + + const store = TestBed.inject(MockStore) + const glue = TestBed.inject(DatasetPreviewGlue) + const ctrl = TestBed.inject(HttpTestingController) + store.overrideSelector(ngViewerSelectorClearView, false) + const originDatasets0 = [ + ...region0.originDatasets, + { + kgId: getRandomHex(), + kgSchema: 'minds/core/dataset/v1.0.0', + filename: getRandomHex() + } + ] + const origindataset1 = [ + ...region1.originDatasets, + { + kgSchema: 'minds/core/dataset/v1.0.0', + kgId: getRandomHex(), + filename: getRandomHex() + } + ] + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [{ + ...region0, + originDatasets: originDatasets0 + }, { + ...region1, + originDatasets: origindataset1 + }] + } + }) + + const expectedOriginDatasets = [ + ...originDatasets0, + ...origindataset1, + ] + + for (const { kgSchema, kgId, filename } of expectedOriginDatasets) { + const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) + req.flush(nifti) + } + tick(200) + expect(glue.selectedRegionPreview$).toBeObservable( + hot('a', { + a: expectedOriginDatasets + }) + ) + })) + }) + + describe('> onRegionSelectChangeShowPreview$', () => { + it('> calls getDatasetPreviewFromId for each of the selectedRegion', fakeAsync(() => { + + /** + * Testing Store observable + * https://stackoverflow.com/a/61871144/6059235 + */ + const store = TestBed.inject(MockStore) + const glue = TestBed.inject(DatasetPreviewGlue) + const ctrl = TestBed.inject(HttpTestingController) + store.overrideSelector(ngViewerSelectorClearView, false) + + const getDatasetPreviewFromIdSpy = spyOn(glue, 'getDatasetPreviewFromId').and.callThrough() + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [region1] + } + }) + + const { kgSchema, kgId, filename } = region1.originDatasets[0] + const req = ctrl.expectOne(`${DS_PREVIEW_URL}/${encodeURIComponent(kgSchema)}/${kgId}/${encodeURIComponent(filename)}`) + req.flush(nifti) + tick(200) + + for (const { kgId, kgSchema, filename } of region1.originDatasets) { + expect(getDatasetPreviewFromIdSpy).toHaveBeenCalledWith({ + datasetId: kgId, + datasetSchema: kgSchema, + filename + }) + } + + expect(glue.onRegionSelectChangeShowPreview$).toBeObservable( + hot('a', { + a: [ { + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + } ] + }) + ) + })) + }) + + describe('> onRegionDeselectRemovePreview$', () => { + it('> on region selected [ region ] > [], emits', fakeAsync(() => { + + const store = TestBed.inject(MockStore) + store.overrideSelector(ngViewerSelectorClearView, false) + const glue = TestBed.inject(DatasetPreviewGlue) + + const regionsSelected$ = hot('bab', { + a: [region1], + b: [] + }) + + const spy = spyOn(glue, 'getDatasetPreviewFromId') + spy.and.returnValue(of({ + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + })) + + const src$ = merge( + regionsSelected$.pipe( + tap(regionsSelected => store.setState({ + ...initialState, + viewerState: { + regionsSelected + } + })), + ignoreElements() + ), + glue.onRegionDeselectRemovePreview$ + ) + + src$.subscribe() + + expect(glue.onRegionDeselectRemovePreview$).toBeObservable( + hot('bba', { + a: [{ + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + }], + b: [] + }) + ) + + tick(200) + })) + }) + + describe('> onClearviewRemovePreview$', () => { + it('> on regions selected [ region ] > clear view selector returns true, emits ', fakeAsync(() => { + const store = TestBed.inject(MockStore) + store.overrideSelector(ngViewerSelectorClearView, true) + + const glue = TestBed.inject(DatasetPreviewGlue) + + const spy = spyOn(glue, 'getDatasetPreviewFromId') + spy.and.returnValue(of({ + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + })) + + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [region1] + } + }) + + expect(glue.onClearviewRemovePreview$).toBeObservable( + hot('a', { + a: [{ + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + }], + b: [] + }) + ) + + tick(200) + })) + }) + + describe('> onClearviewAddPreview$', () => { + it('> on region selected [ region ] > clear view selector returns false, emits', fakeAsync(() => { + const store = TestBed.inject(MockStore) + const overridenSelector = store.overrideSelector(ngViewerSelectorClearView, true) + + const overridenSelector$ = hot('ab', { + a: true, + b: false + }) + + const glue = TestBed.inject(DatasetPreviewGlue) + + const spy = spyOn(glue, 'getDatasetPreviewFromId') + spy.and.returnValue(of({ + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + })) + + store.setState({ + ...initialState, + viewerState: { + regionsSelected: [region1] + } + }) + + overridenSelector$.subscribe(flag => { + overridenSelector.setResult(flag) + store.refreshState() + }) + + expect(glue.onClearviewAddPreview$).toBeObservable( + hot('-a', { + a: [{ + ...nifti, + filename: region1.originDatasets[0].filename, + datasetId: region1.originDatasets[0].kgId, + }], + b: [] + }) + ) + + tick(200) + })) + }) }) diff --git a/src/glue.ts b/src/glue.ts index bee292f7e1cfe4c55391f645555ba42936c39685..baacec2a566a6b1fd04189869a00e06b606123dc 100644 --- a/src/glue.ts +++ b/src/glue.ts @@ -15,6 +15,7 @@ import { NgLayersService } from "src/ui/layerbrowser/ngLayerService.service" import { EnumColorMapName } from "./util/colorMaps" import { Effect } from "@ngrx/effects" import { viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector, viewerStateSelectedParcellationSelector } from "./services/state/viewerState/selectors" +import { ngViewerSelectorClearView } from "./services/state/ngViewerState/selectors" const PREVIEW_FILE_TYPES_NO_UI = [ EnumPreviewFileTypes.NIFTI, @@ -120,12 +121,12 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ } static GetDatasetPreviewId(data: IDatasetPreviewData ){ - const { datasetSchema = 'untitled', datasetId, filename } = data + const { datasetSchema = 'minds/core/dataset/v1.0.0', datasetId, filename } = data return `${datasetSchema}/${datasetId}:${filename}` } static GetDatasetPreviewFromId(id: string): IDatasetPreviewData{ - const re = /^([a-f0-9-]+):(.+)$/.exec(id) + const re = /([a-f0-9-]+):(.+)$/.exec(id) if (!re) throw new Error(`id cannot be decoded: ${id}`) return { datasetId: re[1], filename: re[2] } } @@ -195,13 +196,62 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ ) } - private fetchedDatasetPreviewCache: Map<string, any> = new Map() + public selectedRegionPreview$ = this.store$.pipe( + select(state => state?.viewerState?.regionsSelected), + filter(regions => !!regions), + map(regions => /** effectively flatMap */ regions.reduce((acc, curr) => acc.concat( + curr.originDatasets && Array.isArray(curr.originDatasets) && curr.originDatasets.length > 0 + ? curr.originDatasets + : [] + ), [])), + ) + + public onRegionSelectChangeShowPreview$ = this.selectedRegionPreview$.pipe( + switchMap(arr => arr.length > 0 + ? forkJoin(...arr.map(({ kgId, kgSchema, filename }) => this.getDatasetPreviewFromId({ datasetId: kgId, datasetSchema: kgSchema, filename }))) + : of([]) + ), + shareReplay(1), + ) + + public onRegionDeselectRemovePreview$ = this.onRegionSelectChangeShowPreview$.pipe( + pairwise(), + map(([oArr, nArr]) => oArr.filter(item => { + return !nArr + .map(DatasetPreviewGlue.GetDatasetPreviewId) + .includes( + DatasetPreviewGlue.GetDatasetPreviewId(item) + ) + })), + ) + + public onClearviewRemovePreview$ = this.onRegionSelectChangeShowPreview$.pipe( + filter(arr => arr.length > 0), + switchMap(arr => this.store$.pipe( + select(ngViewerSelectorClearView), + distinctUntilChanged(), + filter(val => val), + mapTo(arr) + )), + ) + + public onClearviewAddPreview$ = this.onRegionSelectChangeShowPreview$.pipe( + filter(arr => arr.length > 0), + switchMap(arr => this.store$.pipe( + select(ngViewerSelectorClearView), + distinctUntilChanged(), + filter(val => !val), + mapTo(arr) + )) + ) + + private fetchedDatasetPreviewCache: Map<string, Observable<any>> = new Map() public getDatasetPreviewFromId({ datasetSchema = 'minds/core/dataset/v1.0.0', datasetId, filename }: IDatasetPreviewData){ const dsPrvId = DatasetPreviewGlue.GetDatasetPreviewId({ datasetSchema, datasetId, filename }) - const cachedPrv = this.fetchedDatasetPreviewCache.get(dsPrvId) + const cachedPrv$ = this.fetchedDatasetPreviewCache.get(dsPrvId) const filteredDsId = /[a-f0-9-]+$/.exec(datasetId) - if (cachedPrv) return of(cachedPrv) - return this.http.get(`${DS_PREVIEW_URL}/${encodeURIComponent(datasetSchema)}/${filteredDsId}/${encodeURIComponent(filename)}`, { responseType: 'json' }).pipe( + if (cachedPrv$) return cachedPrv$ + const filedetail$ = this.http.get(`${DS_PREVIEW_URL}/${encodeURIComponent(datasetSchema)}/${filteredDsId}/${encodeURIComponent(filename)}`, { responseType: 'json' }).pipe( map(json => { return { ...json, @@ -209,7 +259,10 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ datasetId } }), - tap(val => this.fetchedDatasetPreviewCache.set(dsPrvId, val)) + ) + this.fetchedDatasetPreviewCache.set(dsPrvId, filedetail$) + return filedetail$.pipe( + tap(val => this.fetchedDatasetPreviewCache.set(dsPrvId, of(val))) ) } @@ -268,9 +321,24 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ // managing niftiVolumes // monitors previewDatasetFile obs to add/remove ng layer + this.subscriptions.push( - this.getDiffDatasetFilesPreviews( - dsPrv => determinePreviewFileType(dsPrv) === EnumPreviewFileTypes.NIFTI + merge( + this.getDiffDatasetFilesPreviews( + dsPrv => determinePreviewFileType(dsPrv) === EnumPreviewFileTypes.NIFTI + ), + this.onRegionSelectChangeShowPreview$.pipe( + map(prvToShow => ({ prvToShow, prvToDismiss: [] })) + ), + this.onRegionDeselectRemovePreview$.pipe( + map(prvToDismiss => ({ prvToShow: [], prvToDismiss })) + ), + this.onClearviewRemovePreview$.pipe( + map(prvToDismiss => ({ prvToDismiss, prvToShow: [] })) + ), + this.onClearviewAddPreview$.pipe( + map(prvToShow => ({ prvToDismiss: [], prvToShow })) + ) ).pipe( withLatestFrom(this.store$.pipe( select(state => state?.viewerState?.templateSelected || null), @@ -365,7 +433,6 @@ export class DatasetPreviewGlue implements IDatasetPreviewGlue, OnDestroy{ } }) ) - } private closeDatasetPreviewWidget(data: IDatasetPreviewData){ diff --git a/src/services/state/ngViewerState.store.helper.ts b/src/services/state/ngViewerState.store.helper.ts index 1d1e5a5421f8c84f4dd000fb86548637fcd05a32..834c5d5d53d4168a08a65c348ffbcf5e51eb5fa0 100644 --- a/src/services/state/ngViewerState.store.helper.ts +++ b/src/services/state/ngViewerState.store.helper.ts @@ -9,6 +9,8 @@ import { ngViewerActionSetPerspOctantRemoval, ngViewerActionToggleMax, ngViewerActionClearView, + ngViewerActionSetPanelOrder, + ngViewerActionForceShowSegment, } from './ngViewerState/actions' export { @@ -17,4 +19,6 @@ export { ngViewerActionSetPerspOctantRemoval, ngViewerActionToggleMax, ngViewerActionClearView, + ngViewerActionSetPanelOrder, + ngViewerActionForceShowSegment, } diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index ca436b3eefed79704e05afaf423db86e974c9800..f7a1418a36bb5f089dda09dac0ae89a6ba8ec7c3 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -4,13 +4,14 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { withLatestFrom, map, distinctUntilChanged, scan, shareReplay, filter, mapTo, debounceTime, catchError, skip, throttleTime } from 'rxjs/operators'; import { SNACKBAR_MESSAGE } from './uiState.store'; import { getNgIds, IavRootStoreInterface, GENERAL_ACTION_TYPES } from '../stateStore.service'; -import { Action, select, Store } from '@ngrx/store' +import { Action, select, Store, createReducer, on } from '@ngrx/store' import { BACKENDURL, CYCLE_PANEL_MESSAGE } from 'src/util/constants'; import { HttpClient } from '@angular/common/http'; import { INgLayerInterface, ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionSetPerspOctantRemoval } from './ngViewerState.store.helper' import { PureContantService } from 'src/util'; import { PANELS } from './ngViewerState.store.helper' -import { ngViewerActionToggleMax } from './ngViewerState/actions'; +import { ngViewerActionToggleMax, ngViewerActionClearView, ngViewerActionSetPanelOrder, ngViewerActionSwitchPanelMode, ngViewerActionForceShowSegment, ngViewerActionNehubaReady } from './ngViewerState/actions'; +import { generalApplyState } from '../stateStore.helper'; export function mixNgLayers(oldLayers: INgLayerInterface[], newLayers: INgLayerInterface|INgLayerInterface[]): INgLayerInterface[] { if (newLayers instanceof Array) { @@ -18,9 +19,6 @@ export function mixNgLayers(oldLayers: INgLayerInterface[], newLayers: INgLayerI } else { return oldLayers.concat({ ...newLayers, - ...( newLayers.mixability === 'nonmixable' && oldLayers.findIndex(l => l.mixability === 'nonmixable') >= 0 - ? {visible: false} - : {}), }) } } @@ -35,6 +33,10 @@ export interface StateInterface { octantRemoval: boolean showSubstrate: boolean showZoomlevel: boolean + + clearViewQueue: { + [key: string]: boolean + } } export interface ActionInterface extends Action { @@ -55,91 +57,82 @@ export const defaultState: StateInterface = { octantRemoval: true, showSubstrate: null, showZoomlevel: null, + + clearViewQueue: {} } -export const getStateStore = ({ state = defaultState } = {}) => (prevState: StateInterface = state, action: ActionInterface): StateInterface => { - switch (action.type) { - case ngViewerActionSetPerspOctantRemoval.type:{ - const { octantRemovalFlag } = action as any - return { - ...prevState, - octantRemoval: octantRemovalFlag +export const ngViewerStateReducer = createReducer( + defaultState, + on(ngViewerActionClearView, (state, { payload }) => { + const { clearViewQueue } = state + for (const key in payload) { + clearViewQueue[key] = payload[key] } - } - case ACTION_TYPES.SET_PANEL_ORDER: { - const { payload } = action - const { panelOrder } = payload - return { - ...prevState, - panelOrder, + ...state, + clearViewQueue: { + ...clearViewQueue + } } - } - case ACTION_TYPES.SWITCH_PANEL_MODE: { - const { payload } = action - const { panelMode } = payload - if (SUPPORTED_PANEL_MODES.indexOf(panelMode) < 0) { return prevState } + }), + on(ngViewerActionSetPerspOctantRemoval, (state, { octantRemovalFlag }) => { return { - ...prevState, - panelMode, + ...state, + octantRemoval: octantRemovalFlag } - } - case ngViewerActionAddNgLayer.type: - case ADD_NG_LAYER: + }), + on(ngViewerActionAddNgLayer, (state, { layer }) => { return { - ...prevState, - layers : mixNgLayers(prevState.layers, action.layer), + ...state, + layers: mixNgLayers(state.layers, layer) } - case ngViewerActionRemoveNgLayer.type: - case REMOVE_NG_LAYER: { - if (Array.isArray(action.layer)) { - const { layer } = action - const layerNameSet = new Set(layer.map(l => l.name)) - return { - ...prevState, - layers: prevState.layers.filter(l => !layerNameSet.has(l.name)), - } - } else { - return { - ...prevState, - layers : prevState.layers.filter(l => l.name !== action.layer.name), - } + }), + on(ngViewerActionSetPanelOrder, (state, { payload }) => { + const { panelOrder } = payload + return { + ...state, + panelOrder } - } - case SHOW_NG_LAYER: + }), + on(ngViewerActionSwitchPanelMode, (state, { payload }) => { + const { panelMode } = payload + if (SUPPORTED_PANEL_MODES.indexOf(panelMode as any) < 0) { return state } return { - ...prevState, - layers : prevState.layers.map(l => l.name === action.layer.name - ? { ...l, visible: true } - : l), + ...state, + panelMode } - case HIDE_NG_LAYER: + }), + on(ngViewerActionRemoveNgLayer, (state, { layer }) => { + + const newLayers = Array.isArray(layer) + ? (() => { + const layerNameSet = new Set(layer.map(l => l.name)) + return state.layers.filter(l => !layerNameSet.has(l.name)) + })() + : state.layers.filter(l => l.name !== layer.name) return { - ...prevState, - - layers : prevState.layers.map(l => l.name === action.layer.name - ? { ...l, visible: false } - : l), + ...state, + layers: newLayers } - case FORCE_SHOW_SEGMENT: + }), + on(ngViewerActionForceShowSegment, (state, { forceShowSegment }) => { return { - ...prevState, - forceShowSegment : action.forceShowSegment, + ...state, + forceShowSegment } - case NEHUBA_READY: { - const { nehubaReady } = action + }), + on(ngViewerActionNehubaReady, (state, { nehubaReady }) => { return { - ...prevState, + ...state, nehubaReady } - } - case GENERAL_ACTION_TYPES.APPLY_STATE: { - const { ngViewerState } = (action as any).state + }), + on(generalApplyState, (_, { state }) => { + const { ngViewerState } = state return ngViewerState - } - default: return prevState - } -} + }) +) + // must export a named function for aot compilation // see https://github.com/angular/angular/issues/15587 @@ -148,10 +141,8 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Stat // // angular function expressions are not supported in decorators -const defaultStateStore = getStateStore() - export function stateStore(state, action) { - return defaultStateStore(state, action) + return ngViewerStateReducer(state, action) } @Injectable({ @@ -258,12 +249,11 @@ export class NgViewerUseEffect implements OnDestroy { ofType(ACTION_TYPES.CYCLE_VIEWS), withLatestFrom(this.panelOrder$), map(([_, panelOrder]) => { - return { - type: ACTION_TYPES.SET_PANEL_ORDER, + return ngViewerActionSetPanelOrder({ payload: { panelOrder: [...panelOrder.slice(1), ...panelOrder.slice(0, 1)].join(''), - }, - } + } + }) }), ) @@ -280,12 +270,9 @@ export class NgViewerUseEffect implements OnDestroy { const { index = 0 } = payload const panelOrder = [...oldPanelOrder.slice(index), ...oldPanelOrder.slice(0, index)].join('') - return { - type: ACTION_TYPES.SET_PANEL_ORDER, - payload: { - panelOrder, - }, - } + return ngViewerActionSetPanelOrder({ + payload: { panelOrder }, + }) }), ) @@ -320,12 +307,9 @@ export class NgViewerUseEffect implements OnDestroy { const panelOrder = panelOrdersPrev || [...panelOrders.slice(index), ...panelOrders.slice(0, index)].join('') - return { - type: ACTION_TYPES.SET_PANEL_ORDER, - payload: { - panelOrder, - }, - } + return ngViewerActionSetPanelOrder({ + payload: { panelOrder } + }) }), ) @@ -336,14 +320,13 @@ export class NgViewerUseEffect implements OnDestroy { scan(scanFn, []), )), map(([ _, panelModes ]) => { - return { - type: ACTION_TYPES.SWITCH_PANEL_MODE, + return ngViewerActionSwitchPanelMode({ payload: { panelMode: panelModes[0] === PANELS.SINGLE_PANEL ? (panelModes[1] || PANELS.FOUR_PANEL) : PANELS.SINGLE_PANEL, }, - } + }) }), ) @@ -419,10 +402,9 @@ export class NgViewerUseEffect implements OnDestroy { return loadedNgLayers.filter(l => !baseNameSet.has(l.name)) }), map(layer => { - return { - type: REMOVE_NG_LAYER, - layer, - } + return ngViewerActionRemoveNgLayer({ + layer + }) }), ) } @@ -434,19 +416,9 @@ export class NgViewerUseEffect implements OnDestroy { } } -export const ADD_NG_LAYER = 'ADD_NG_LAYER' -export const REMOVE_NG_LAYER = 'REMOVE_NG_LAYER' -export const SHOW_NG_LAYER = 'SHOW_NG_LAYER' -export const HIDE_NG_LAYER = 'HIDE_NG_LAYER' -export const FORCE_SHOW_SEGMENT = `FORCE_SHOW_SEGMENT` -export const NEHUBA_READY = `NEHUBA_READY` - export { INgLayerInterface } const ACTION_TYPES = { - SWITCH_PANEL_MODE: 'SWITCH_PANEL_MODE', - SET_PANEL_ORDER: 'SET_PANEL_ORDER', - CYCLE_VIEWS: 'CYCLE_VIEWS', REMOVE_ALL_NONBASE_LAYERS: `REMOVE_ALL_NONBASE_LAYERS`, diff --git a/src/services/state/ngViewerState/actions.ts b/src/services/state/ngViewerState/actions.ts index 1280bd3f3d3c3287c0b49d490d58e2eaea7560e4..7f864defac09d58f556d97d276bd44255a435bc3 100644 --- a/src/services/state/ngViewerState/actions.ts +++ b/src/services/state/ngViewerState/actions.ts @@ -1,4 +1,4 @@ -import { createAction, props } from "@ngrx/store" +import { createAction, props, createReducer } from "@ngrx/store" import { INgLayerInterface } from './constants' export const ngViewerActionAddNgLayer = createAction( @@ -21,6 +21,26 @@ export const ngViewerActionToggleMax = createAction( props<{ payload: { index: number } }>() ) +export const ngViewerActionSetPanelOrder = createAction( + `[ngViewerAction] setPanelOrder`, + props<{ payload: { panelOrder: string } }>() +) + +export const ngViewerActionSwitchPanelMode = createAction( + `[ngViewerAction] switchPanelMode`, + props<{ payload: { panelMode: string } }>() +) + +export const ngViewerActionForceShowSegment = createAction( + `[ngViewerAction] forceShowSegment`, + props<{ forceShowSegment: boolean }>() +) + +export const ngViewerActionNehubaReady = createAction( + `[ngViewerAction] nehubaReady`, + props<{ nehubaReady: boolean }>() +) + /** * Clear viewer view from additional layers such as PMap or connectivity * To request view to be cleared, call @@ -44,5 +64,5 @@ export const ngViewerActionToggleMax = createAction( */ export const ngViewerActionClearView = createAction( `[ngViewerAction] clearView`, - props<{ payload: { [key:string]: boolean }}>() + props<{ payload: { [key: string]: boolean }}>() ) diff --git a/src/services/state/ngViewerState/selectors.spec.ts b/src/services/state/ngViewerState/selectors.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ad0df3afbb2a740f7eb82250c9c2b6426243bb0 --- /dev/null +++ b/src/services/state/ngViewerState/selectors.spec.ts @@ -0,0 +1,37 @@ +import { ngViewerSelectorClearView } from './selectors' + +let clearViewQueue = {} + +describe('> ngViewerState/selectors.ts', () => { + describe('> ngViewerSelectorClearView', () => { + beforeEach(() => { + clearViewQueue = {} + }) + describe('> when prop is not provided', () => { + it('> if clearViewQueue is empty (on startup)', () => { + const result = ngViewerSelectorClearView.projector(clearViewQueue) + expect(result).toEqual(false) + }) + it('> if clearViewQueue is non empty, but falsy, should return false', () => { + clearViewQueue['hello - world'] = null + clearViewQueue['oo bar'] = false + const result = ngViewerSelectorClearView.projector(clearViewQueue) + expect(result).toEqual(false) + }) + it('> if clearViewQueue is non empty and truthy, should return true', () => { + clearViewQueue['hello - world'] = 1 + const result = ngViewerSelectorClearView.projector(clearViewQueue) + expect(result).toEqual(true) + }) + }) + describe('> when prop is provided', () => { + it('> only provides clear view status of the required', () => { + clearViewQueue['hello - world'] = 1 + const result0 = ngViewerSelectorClearView.projector(clearViewQueue, { id: 'hello - world' }) + const result1 = ngViewerSelectorClearView.projector(clearViewQueue, { id: 'foo bar' }) + expect(result0).toEqual(true) + expect(result1).toEqual(false) + }) + }) + }) +}) \ No newline at end of file diff --git a/src/services/state/ngViewerState/selectors.ts b/src/services/state/ngViewerState/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..34b0305f1a88ac7055be0e9220d3a2fd860d4406 --- /dev/null +++ b/src/services/state/ngViewerState/selectors.ts @@ -0,0 +1,16 @@ +import { createSelector } from "@ngrx/store"; + +export const ngViewerSelectorClearView = createSelector( + (state: any) => state?.ngViewerState?.clearViewQueue, + (clearViewQueue, props) => { + + if (!!props && !!props.id) { + for (const key in clearViewQueue) { + if (key === props.id) return !!clearViewQueue[key] + } + return false + } else { + return Object.keys(clearViewQueue).some(key => !!clearViewQueue[key]) + } + } +) diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index 298032f2b960735737d4c552642e0dcbb8de07d5..32d4f68bca8b24d53bed3ed9d7e23c155866fa65 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -49,7 +49,6 @@ export { ViewerStateInterface, ViewerActionInterface, viewerState } export { UIStateInterface, UIActionInterface, uiState } export { userConfigState, USER_CONFIG_ACTION_TYPES} -export { ADD_NG_LAYER, FORCE_SHOW_SEGMENT, HIDE_NG_LAYER, REMOVE_NG_LAYER, SHOW_NG_LAYER } from './state/ngViewerState.store' export { CHANGE_NAVIGATION, DESELECT_LANDMARKS, FETCHED_TEMPLATE, NEWVIEWER, SELECT_LANDMARKS, SELECT_PARCELLATION, SELECT_REGIONS, USER_LANDMARKS } from './state/viewerState.store' export { IDataEntry, IParcellationRegion, FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, ILandmark, IOtherLandmarkGeometry, IPlaneLandmarkGeometry, IPointLandmarkGeometry, IProperty, IPublication, IReferenceSpace, IFile, IFileSupplementData } from './state/dataStore.store' export { CLOSE_SIDE_PANEL, MOUSE_OVER_LANDMARK, MOUSE_OVER_SEGMENT, OPEN_SIDE_PANEL, COLLAPSE_SIDE_PANEL_CURRENT_VIEW, EXPAND_SIDE_PANEL_CURRENT_VIEW } from './state/uiState.store' diff --git a/src/ui/config/config.component.ts b/src/ui/config/config.component.ts index 78526889780b6a5be9ec9905cd8dc10ffdf71b94..756c15d1a6e80fd4f49a58dc1282642b331bc07f 100644 --- a/src/ui/config/config.component.ts +++ b/src/ui/config/config.component.ts @@ -3,12 +3,14 @@ import { select, Store } from '@ngrx/store'; import { combineLatest, Observable, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { NG_VIEWER_ACTION_TYPES, SUPPORTED_PANEL_MODES } from 'src/services/state/ngViewerState.store'; +import { ngViewerActionSetPanelOrder } from 'src/services/state/ngViewerState.store.helper'; import { VIEWER_CONFIG_ACTION_TYPES, StateInterface as ViewerConfiguration } from 'src/services/state/viewerConfig.store' import { IavRootStoreInterface } from 'src/services/stateStore.service'; import { isIdentityQuat } from '../nehubaContainer/util'; import {MatSlideToggleChange} from "@angular/material/slide-toggle"; import {MatSliderChange} from "@angular/material/slider"; import { PureContantService } from 'src/util'; +import { ngViewerActionSwitchPanelMode } from 'src/services/state/ngViewerState/actions'; const GPU_TOOLTIP = `Higher GPU usage can cause crashes on lower end machines` const ANIMATION_TOOLTIP = `Animation can cause slowdowns in lower end machines` @@ -141,10 +143,11 @@ export class ConfigComponent implements OnInit, OnDestroy { }) } public usePanelMode(panelMode: string) { - this.store.dispatch({ - type: NG_VIEWER_ACTION_TYPES.SWITCH_PANEL_MODE, - payload: { panelMode }, - }) + this.store.dispatch( + ngViewerActionSwitchPanelMode({ + payload: { panelMode } + }) + ) } public handleDrop(event: DragEvent) { @@ -157,10 +160,11 @@ export class ConfigComponent implements OnInit, OnDestroy { const arr = this.panelOrder.split(''); [arr[idx1], arr[idx2]] = [arr[idx2], arr[idx1]] - this.store.dispatch({ - type: NG_VIEWER_ACTION_TYPES.SET_PANEL_ORDER, - payload: { panelOrder: arr.join('') }, - }) + this.store.dispatch( + ngViewerActionSetPanelOrder({ + payload: { panelOrder: arr.join('') } + }) + ) } public handleDragOver(event: DragEvent) { event.preventDefault() diff --git a/src/ui/databrowserModule/kgSingleDatasetService.service.ts b/src/ui/databrowserModule/kgSingleDatasetService.service.ts index 985c4f6df17d53aad0c2038357ce1d32cae1a88e..4f00a6f0ce282c6a78d95bb0ac01c787ce64b9d9 100644 --- a/src/ui/databrowserModule/kgSingleDatasetService.service.ts +++ b/src/ui/databrowserModule/kgSingleDatasetService.service.ts @@ -4,10 +4,10 @@ import { select, Store } from "@ngrx/store"; import { Subscription } from "rxjs"; import { filter } from "rxjs/operators"; import { IDataEntry, ViewerPreviewFile, DATASETS_ACTIONS_TYPES } from "src/services/state/dataStore.store"; -import { SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; -import { IavRootStoreInterface, REMOVE_NG_LAYER } from "src/services/stateStore.service"; +import { IavRootStoreInterface } from "src/services/stateStore.service"; import { BACKENDURL } from "src/util/constants"; import { uiStateShowBottomSheet } from "src/services/state/uiState.store.helper"; +import { ngViewerActionRemoveNgLayer } from "src/services/state/ngViewerState/actions"; @Injectable({ providedIn: 'root' }) export class KgSingleDatasetService implements OnDestroy { @@ -74,12 +74,11 @@ export class KgSingleDatasetService implements OnDestroy { } public removeNgLayer({ url }) { - this.store$.dispatch({ - type : REMOVE_NG_LAYER, - layer : { - name : url, - }, - }) + this.store$.dispatch( + ngViewerActionRemoveNgLayer({ + layer: { name: url } + }) + ) } } diff --git a/src/ui/databrowserModule/preview/preview.base.ts b/src/ui/databrowserModule/preview/preview.base.ts index b1191fdd39a7352979e31b4766a918b53c9d0c55..1a9e4f7b143df53acca71eea87c548acfea302ec 100644 --- a/src/ui/databrowserModule/preview/preview.base.ts +++ b/src/ui/databrowserModule/preview/preview.base.ts @@ -17,7 +17,7 @@ export class PreviewBase implements OnChanges{ datasetId: string @Input() - datasetSchema: string = 'minds/core/dataste/v1.0.0' + datasetSchema: string = 'minds/core/dataset/v1.0.0' previewtype: EnumPreviewFileTypes diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts index 8c70e75bd16c65c68c325e81459304b5d3408416..4641e4ca4f72f187ddfd27771a50d74a4f5d06df 100644 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts @@ -6,7 +6,7 @@ import { MatSliderChange } from "@angular/material/slider"; import { getViewer } from "src/util/fn"; import { PureContantService } from "src/util"; -import { ngViewerActionRemoveNgLayer } from "src/services/state/ngViewerState/actions"; +import { ngViewerActionRemoveNgLayer, ngViewerActionForceShowSegment } from "src/services/state/ngViewerState/actions"; import { getNgIds } from 'src/util/fn' import { LoggingService } from "src/logging"; import { ARIA_LABELS } from 'common/constants' @@ -173,14 +173,15 @@ export class LayerBrowser implements OnInit, OnDestroy { /** * TODO perhaps useEffects ? */ - this.store.dispatch({ - type : 'FORCE_SHOW_SEGMENT', - forceShowSegment : this.forceShowSegmentCurrentState === null - ? true - : this.forceShowSegmentCurrentState === true - ? false - : null, - }) + this.store.dispatch( + ngViewerActionForceShowSegment({ + forceShowSegment : this.forceShowSegmentCurrentState === null + ? true + : this.forceShowSegmentCurrentState === true + ? false + : null, + }) + ) } public removeLayer(layer: any) { diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index a225812a97008d5ad340ab4088c16575664abd7f..69df9fb8629c5af432f0b31d8de8d0705cc55c20 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -14,7 +14,7 @@ import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { compareLandmarksChanged } from "src/util/constants"; import { PureContantService } from "src/util"; import { ARIA_LABELS, IDS } from 'common/constants' -import { ngViewerActionSetPerspOctantRemoval, PANELS, ngViewerActionToggleMax, ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "src/services/state/ngViewerState.store.helper"; +import { ngViewerActionSetPerspOctantRemoval, PANELS, ngViewerActionToggleMax, ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionClearView } from "src/services/state/ngViewerState.store.helper"; import { viewerStateSelectRegionWithIdDeprecated, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks } from 'src/services/state/viewerState.store.helper' import { SwitchDirective } from "src/util/directives/switch.directive"; import { @@ -133,6 +133,14 @@ const { export class NehubaContainer implements OnInit, OnChanges, OnDestroy { + public test(flag: boolean) { + this.store.dispatch( + ngViewerActionClearView({ payload: { + ['id-me']: flag + } }) + ) + } + public ARIA_LABEL_ZOOM_IN = ZOOM_IN public ARIA_LABEL_ZOOM_OUT = ZOOM_OUT public ARIA_LABEL_TOGGLE_SIDE_PANEL = TOGGLE_SIDE_PANEL diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index ccce423806523cf6a229100601d8f22fc4a4fe7b..0b6c3e5b6da24a4f319e086cd04bd338312663bf 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -296,6 +296,8 @@ let-content="content"> <mat-expansion-panel class="mt-1 mb-1" hideToggle + (afterExpand)="test(true)" + (afterCollapse)="test(false)" *ngIf="iavNgIf"> <mat-expansion-panel-header> diff --git a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts index 886009dbd779a6cf37b88e7a5ab0bcee3dfeb832..7965bedeecb757feb7e33e859ee10d9d1666c5cd 100644 --- a/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts +++ b/src/ui/nehubaContainer/nehubaViewerInterface/nehubaViewerInterface.directive.ts @@ -7,10 +7,10 @@ import { distinctUntilChanged, filter, debounceTime, shareReplay, scan, map, thr import { StateInterface as ViewerConfigStateInterface } from "src/services/state/viewerConfig.store"; import { getNavigationStateFromConfig } from "../util"; import { NEHUBA_LAYER_CHANGED, CHANGE_NAVIGATION, VIEWERSTATE_ACTION_TYPES } from "src/services/state/viewerState.store"; -import { NEHUBA_READY } from "src/services/state/ngViewerState.store"; import { timedValues } from "src/util/generator"; import { MOUSE_OVER_SEGMENTS, MOUSE_OVER_LANDMARK } from "src/services/state/uiState.store"; import { takeOnePipe } from "../nehubaContainer.component"; +import { ngViewerActionNehubaReady } from "src/services/state/ngViewerState/actions"; const defaultNehubaConfig = { "configName": "", @@ -425,10 +425,11 @@ export class NehubaViewerContainerDirective implements OnInit, OnDestroy{ /** * TODO when user selects new template, window.viewer */ - this.store$.dispatch({ - type: NEHUBA_READY, - nehubaReady: true, - }) + this.store$.dispatch( + ngViewerActionNehubaReady({ + nehubaReady: true, + }) + ) }), this.nehubaViewerInstance.mouseoverSegmentEmitter.pipe( diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index 9cde285d0cc6a81a7ae21880b153dba8e157cd36..9c99eef72e25df206a061bb3ca41b202d77151a9 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -19,29 +19,16 @@ Brain region </span> - <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> - - <!-- origin datas --> - <button mat-icon-button - [color]="previewDirective.active ? 'primary' : 'basic'" - *ngFor="let originDataset of (region.originDatasets || []); let index = index" - iav-dataset-preview-dataset-file - [iav-dataset-preview-dataset-file-kgschema]="originDataset.kgSchema" - [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" - [iav-dataset-preview-dataset-file-filename]="originDataset.filename" - #previewDirective="iavDatasetPreviewDatasetFile" - iv-custom-comp - [attr.primary]="previewDirective.active || null" - role="switch" - [attr.aria-checked]="previewDirective.active" - [matTooltip]="SHOW_ORIGIN_DATASET" - [attr.aria-label]="SHOW_ORIGIN_DATASET"> - <mat-icon fontSet="fas" fontIcon="fa-eye"></mat-icon> - <!-- <span> - View {{ regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} - </span> --> - </button> + <!-- origin datas format --> + <div *ngFor="let originDataset of (region.originDatasets || []); let index = index" + class="ml-2"> + <i>·</i> + <span> + {{ regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} + </span> + </div> + <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> <!-- position --> <button mat-icon-button *ngIf="region?.position" diff --git a/webpack.staticassets.js b/webpack.staticassets.js index d88a1ff8b7d973dc8f78b5af0f738dc56f1ba60e..2b23b8b46f6546c6bad8a1c148e0c135780ed1b6 100644 --- a/webpack.staticassets.js +++ b/webpack.staticassets.js @@ -66,7 +66,7 @@ module.exports = { : JSON.stringify('unspecificied hash'), PRODUCTION: !!process.env.PRODUCTION, BACKEND_URL: (process.env.BACKEND_URL && JSON.stringify(process.env.BACKEND_URL)) || 'null', - DATASET_PREVIEW_URL: JSON.stringify(process.env.DATASET_PREVIEW_URL || 'https://hbp-kg-dataset-previewer.apps.hbp.eu/datasetPreview'), + DATASET_PREVIEW_URL: JSON.stringify(process.env.DATASET_PREVIEW_URL || 'https://hbp-kg-dataset-previewer.apps.hbp.eu/v2'), SPATIAL_TRANSFORM_BACKEND: JSON.stringify(process.env.SPATIAL_TRANSFORM_BACKEND || 'https://hbp-spatial-backend.apps.hbp.eu'), MATOMO_URL: JSON.stringify(process.env.MATOMO_URL || null), MATOMO_ID: JSON.stringify(process.env.MATOMO_ID || null),