diff --git a/README.md b/README.md index f7dc29d9c9d6986afcb4f6313b3e8727763932cf..8deba5e6a1d2e2bb2b9e38b325e75c2338bf8d69 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ It is recommended to manage your environments with `.env` file. | `CSP_REPORT_URI` | report uri for csp violations | `/report-violation` | | `NODE_ENV` | set to `production` to disable [`reportOnly`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) | `null` | | `SCRIPT_SRC` | `JSON.stringify`'ed array of allowed scriptSrc | `[]` | -| `DATA_SRC` | `JSON.stringify`'ed array of allowed dataSrc | `[]` | +| `CSP_CONNECT_SRC` | `JSON.stringify`'ed array of allowed dataSrc | `[]` | | `WHITE_LIST_SRC` | `JSON.stringify`'ed array of allowed src | `[]` | | `PROXY_HOSTNAME_WHITELIST` | diff --git a/common/constants.js b/common/constants.js index 8c0565a7d88280f824b152e9920f1c207613e30c..69ffc91ca8e0e1ce6e4914b1e17d719a3edca3aa 100644 --- a/common/constants.js +++ b/common/constants.js @@ -62,6 +62,10 @@ exports.CONST = { MULTI_REGION_SELECTION: `Multi region selection`, REGIONAL_FEATURES: 'Regional features', - NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.` + NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.`, + + ATLAS_NOT_FOUND: `Atlas not found. Maybe it is still loading. Try again in a few seconds?`, + TEMPLATE_NOT_FOUND: `Template not found. Maybe it is still loading. Try again in a few seconds?`, + PARC_NOT_FOUND: `` } })(typeof exports === 'undefined' ? module.exports : exports) diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 5c11e142c406619f9f08df70220e568c69f4b9ac..1b702ad034f2d9dde5adab17bea2584011e98e3f 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -2,7 +2,7 @@ const csp = require('helmet-csp') const bodyParser = require('body-parser') const crypto = require('crypto') -let WHITE_LIST_SRC, DATA_SRC, SCRIPT_SRC +let WHITE_LIST_SRC, CSP_CONNECT_SRC, SCRIPT_SRC // TODO bandaid solution // OKD/nginx reverse proxy seems to strip csp header @@ -26,10 +26,10 @@ try { } try { - DATA_SRC = JSON.parse(process.env.DATA_SRC || '[]') + CSP_CONNECT_SRC = JSON.parse(process.env.CSP_CONNECT_SRC || '[]') } catch (e) { - console.warn(`parsing DATA_SRC error ${process.env.DATA_SRC}`, e) - DATA_SRC = [] + console.warn(`parsing CSP_CONNECT_SRC error ${process.env.CSP_CONNECT_SRC}`, e) + CSP_CONNECT_SRC = [] } const defaultAllowedSites = [ @@ -38,14 +38,15 @@ const defaultAllowedSites = [ 'stats-dev.humanbrainproject.eu' ] -const dataSource = [ +const connectSrc = [ "'self'", "blob:", 'neuroglancer.humanbrainproject.org', 'neuroglancer.humanbrainproject.eu', 'connectivity-query-v1-1-connectivity.apps-dev.hbp.eu', 'object.cscs.ch', - ...DATA_SRC + 'hbp-kg-dataset-previewer.apps.hbp.eu/v2/', // required for dataset previews + ...CSP_CONNECT_SRC ] module.exports = (app) => { @@ -74,9 +75,13 @@ module.exports = (app) => { ], connectSrc: [ ...defaultAllowedSites, - ...dataSource, + ...connectSrc, ...WHITE_LIST_SRC ], + imgSrc: [ + "'self'", + "hbp-kg-dataset-previewer.apps.hbp.eu/v2/" + ], scriptSrc:[ "'self'", 'code.jquery.com', // plugin load external library -> jquery v2 and v3 @@ -85,7 +90,7 @@ module.exports = (app) => { 'cdn.jsdelivr.net/npm/vue@2.5.16/', // plugin load external lib -> vue 2 'cdn.jsdelivr.net/npm/preact@8.4.2/', // plugin load external lib -> preact 'unpkg.com/react@16/umd/', // plugin load external lib -> react - 'unpkg.com/kg-dataset-previewer@1.1.4/', // preview component + 'unpkg.com/kg-dataset-previewer@1.1.5/', // preview component 'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, diff --git a/deploy/datasets/query.js b/deploy/datasets/query.js index 2219f2da80014c7cf62d98b7ccb842e8c8ba748c..e89cc3d082abbebb3067aa363fbd53c08d2374e4 100644 --- a/deploy/datasets/query.js +++ b/deploy/datasets/query.js @@ -114,10 +114,22 @@ const getDs = ({ user }) => (user : getPublicDs() ).then(async datasets => { + /** + * populate the manually inserted dataset first + * this allows inserted dataset to overwrite the KG dataset (if needed) + */ return [ - ...datasets, ...(await returnAdditionalDatasets()), + ...datasets, ] + .reduce((acc, curr) => { + /** + * remove duplicates + */ + const currSet = new Set(acc.map(v => v['fullId'])) + if (currSet.has(curr['fullId'])) return [...acc] + else return acc.concat(curr) + }, []) }) const getExternalSchemaDatasets = (kgId, kgSchema) => { diff --git a/deploy/regionalFeatures/index.js b/deploy/regionalFeatures/index.js index ac779fc9872799e707b0cac31d78caf7b729feee..f642c86afa4b8e1695fde72b9bd600f4c5efbfbc 100644 --- a/deploy/regionalFeatures/index.js +++ b/deploy/regionalFeatures/index.js @@ -107,7 +107,7 @@ Promise.all( for (const [ datasetId, arrRegionIds ] of map.entries()) { additionalDatasets = additionalDatasets.concat({ - fullId: datasetId, + fullId: `https://nexus.humanbrainproject.org/v0/data/${datasetId}`, parcellationRegion: arrRegionIds.map(id => ({ fullId: id })), species: [], kgReference: [ diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 7277a3ebb7422137b55ea234b7d1b3db591baeab..d4872bb8221e35bd018b7d6e906c54979f788a25 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -5,6 +5,7 @@ import { Observable, Subject, Subscription, from, race, of, } from "rxjs"; import { distinctUntilChanged, map, filter, startWith, switchMap, catchError, mapTo, take } from "rxjs/operators"; import { DialogService } from "src/services/dialogService.service"; import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"; +import { viewerStateFetchedTemplatesSelector } from "src/services/state/viewerState/selectors"; import { getLabelIndexMap, getMultiNgIdsRegionsLabelIndexMap, @@ -197,9 +198,7 @@ export class AtlasViewerAPIServices implements OnDestroy{ } this.loadedTemplates$ = this.store.pipe( - select('viewerState'), - safeFilter('fetchedTemplates'), - map(state => state.fetchedTemplates), + select(viewerStateFetchedTemplatesSelector) ) this.selectParcellation$ = this.store.pipe( diff --git a/src/atlasViewer/atlasViewer.history.service.ts b/src/atlasViewer/atlasViewer.history.service.ts index c58aa5a5f32d4081e5e922cc0d1fa31188aa915b..6df36758a8d45a192a1d8f2ea79e7ab728075285 100644 --- a/src/atlasViewer/atlasViewer.history.service.ts +++ b/src/atlasViewer/atlasViewer.history.service.ts @@ -40,7 +40,7 @@ export class AtlasViewerHistoryUseEffect implements OnDestroy { )), ).pipe( withLatestFrom(this.store$), - map(([searchUrl, storeState]: [string, IavRootStoreInterface] ) => { + map(([searchUrl, storeState]: [string, any] ) => { const search = new URLSearchParams(searchUrl) try { if (Array.from(search.keys()).length === 0) { @@ -75,6 +75,10 @@ export class AtlasViewerHistoryUseEffect implements OnDestroy { ...defaultRootState.viewerState, fetchedTemplates: storeState.viewerState.fetchedTemplates, }, + viewerStateHelper: { + ...defaultRootState.viewerStateHelper, + fetchedAtlases: storeState.viewerStateHelper.fetchedAtlases + } }, } } diff --git a/src/atlasViewer/pluginUnit/pluginFactory.directive.ts b/src/atlasViewer/pluginUnit/pluginFactory.directive.ts index 28e0de95a724b0a0d06452d91bdf6f529b05dbf8..fcd47654120dcf3754471b1fc6e7eac2342cb57e 100644 --- a/src/atlasViewer/pluginUnit/pluginFactory.directive.ts +++ b/src/atlasViewer/pluginUnit/pluginFactory.directive.ts @@ -22,7 +22,8 @@ export const SUPPORT_LIBRARY_MAP: Map<string, Map<string, string>> = new Map([ ['8.4.2', 'https://cdn.jsdelivr.net/npm/preact@8.4.2/dist/preact.min.js'] ])], ['d3', new Map([ - ['5.7.0', 'https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js'] + ['5.7.0', 'https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js'], + ['6.2.0', 'https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js'] ])], ['mathjax', new Map([ ['3.1.2', 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.1.2/es5/tex-svg.js'] diff --git a/src/index.html b/src/index.html index 3fce358392e951472c13d4b67c4edd4d58ffb887..8b800ab83f784074bd34125e3bf21a7841258f95 100644 --- a/src/index.html +++ b/src/index.html @@ -12,7 +12,7 @@ <link rel="stylesheet" href="theme.css"> <link rel="stylesheet" href="version.css"> - <script src="https://unpkg.com/kg-dataset-previewer@1.1.4/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer> + <script src="https://unpkg.com/kg-dataset-previewer@1.1.5/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer> </script> <title>Interactive Atlas Viewer</title> diff --git a/src/main.module.ts b/src/main.module.ts index d559316fd2faf5641f35e91357f7bb96653ecd7e..3cc29399b3f96f5f9a7784197c66c8f62e436886 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -1,8 +1,8 @@ import { DragDropModule } from '@angular/cdk/drag-drop' import { CommonModule } from "@angular/common"; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule, InjectionToken } from "@angular/core"; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; -import { StoreModule, Store, ActionReducer } from "@ngrx/store"; +import { StoreModule, ActionReducer } from "@ngrx/store"; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' import { AtlasViewer } from "./atlasViewer/atlasViewer.component"; import { ComponentsModule } from "./components/components.module"; @@ -54,7 +54,7 @@ import 'src/res/css/extra_styles.css' import 'src/res/css/version.css' import 'src/theme.scss' import { DatasetPreviewGlue, datasetPreviewMetaReducer, IDatasetPreviewGlue, GlueEffects, ClickInterceptorService } from './glue'; -import { viewerStateHelperReducer, viewerStateFleshOutDetail, viewerStateMetaReducers, ViewerStateHelperEffect } from './services/state/viewerState.store.helper'; +import { viewerStateHelperReducer, viewerStateMetaReducers, ViewerStateHelperEffect } from './services/state/viewerState.store.helper'; import { TOS_OBS_INJECTION_TOKEN } from './ui/kgtos/kgtos.component'; import { UiEffects } from './services/state/uiState/ui.effects'; import { MesssagingModule } from './messaging/module'; @@ -115,7 +115,6 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { // debug, ...viewerStateMetaReducers, datasetPreviewMetaReducer, - viewerStateFleshOutDetail ] }), HttpClientModule, diff --git a/src/services/state/uiState/ui.effects.ts b/src/services/state/uiState/ui.effects.ts index 2f9287652937e0d517e59c1ebfc939f61b56e211..128655ad53c9ec87a3655dc4da19f336ab4f918a 100644 --- a/src/services/state/uiState/ui.effects.ts +++ b/src/services/state/uiState/ui.effects.ts @@ -1,4 +1,5 @@ import { Injectable, OnDestroy } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; import { Actions, ofType } from "@ngrx/effects"; import { Subscription } from "rxjs"; import { generalActionError } from "src/services/stateStore.helper"; @@ -11,11 +12,17 @@ export class UiEffects implements OnDestroy{ private subscriptions: Subscription[] = [] - constructor(private actions$: Actions){ + constructor( + private actions$: Actions, + snackBar: MatSnackBar + ){ this.subscriptions.push( this.actions$.pipe( ofType(generalActionError.type) - ).subscribe(console.log) + ).subscribe((payload: any) => { + if (!payload.message) console.log(payload) + snackBar.open(payload.message || `Error: cannot complete your action.`, 'Dismiss', { duration: 5000 }) + }) ) } diff --git a/src/services/state/viewerState.store.helper.ts b/src/services/state/viewerState.store.helper.ts index 307ea047fd31479f533094505faa96a56c3a64a4..8bfe2d676df861a97eb62a58259b877f4a6bd4d9 100644 --- a/src/services/state/viewerState.store.helper.ts +++ b/src/services/state/viewerState.store.helper.ts @@ -153,16 +153,6 @@ export const viewerStateHelperReducer = createReducer( export const viewerStateHelperStoreName = 'viewerStateHelper' -export function viewerStateFleshOutDetail(reducer: ActionReducer<any>): ActionReducer<any> { - return (state, action) => { - if (action.type === viewerStateSelectAtlas.type) { - const reconstitutedAtlas = state[viewerStateHelperStoreName].fetchedAtlases.find(a => a['@id'] === (action as any).atlas['@id']) - return reducer(state, { type: action.type, atlas: reconstitutedAtlas } as any) - } - return reducer(state, action) - } -} - export const defaultState = initialState interface IVersion{ diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index b28168b9e76ef0ed84a12aec010de6b5354c9b1d..36435f358d7f101f7881b97170d0b38fc8260432 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -87,32 +87,6 @@ export const defaultState: StateInterface = { export const getStateStore = ({ state = defaultState } = {}) => (prevState: Partial<StateInterface> = state, action: ActionInterface) => { switch (action.type) { - /** - * glue code. in future, viewerStateSelectAtlas should load templates and parcellations by it self - */ - case viewerStateSelectAtlas.type: { - const { fetchedTemplates } = prevState - /** - * selecting atlas means selecting the first available templateSpace - */ - const atlas = (action as any).atlas - const templateTobeSelected = atlas.templateSpaces[0] - const templateSpaceId = templateTobeSelected['@id'] - - const parcellationId = ( - templateTobeSelected.availableIn.find(p => !!p.baseLayer) || - templateTobeSelected.availableIn[0] - )['@id'] - - const templateSelected = fetchedTemplates.find(t => templateSpaceId === t['@id']) - const parcellationSelected = templateSelected.parcellations.find(p => p['@id'] === parcellationId) - - return { - ...prevState, - templateSelected, - parcellationSelected - } - } /** * TODO may be obsolete. test when nifti become available */ diff --git a/src/services/state/viewerState/selectors.ts b/src/services/state/viewerState/selectors.ts index 934f6e003573d0b0955a3ac613d7da3e1261fd15..bb2bd592e782891df9f03398a7bf3171314270bc 100644 --- a/src/services/state/viewerState/selectors.ts +++ b/src/services/state/viewerState/selectors.ts @@ -40,6 +40,7 @@ export const viewerStateSelectedTemplatePureSelector = createSelector( viewerStateFetchedTemplatesSelector, viewerStateSelectedTemplateSelector, (fetchedTemplates, selectedTemplate) => { + if (!selectedTemplate) return null return fetchedTemplates.find(t => t['@id'] === selectedTemplate['@id']) } ) @@ -98,12 +99,17 @@ export const viewerStateGetOverlayingAdditionalParcellations = createSelector( } ) +export const viewerStateFetchedAtlasesSelector = createSelector( + state => state[viewerStateHelperStoreName], + helperState => helperState['fetchedAtlases'] +) export const viewerStateGetSelectedAtlas = createSelector( state => state[viewerStateHelperStoreName], helperState => { if (!helperState) return null const { selectedAtlasId, fetchedAtlases } = helperState + if (!selectedAtlasId) return null return selectedAtlasId && fetchedAtlases.find(a => a['@id'] === selectedAtlasId) } ) @@ -132,7 +138,7 @@ export const viewerStateAtlasParcellationSelector = createSelector( export const viewerStateAtlasLatestParcellationSelector = createSelector( viewerStateAtlasParcellationSelector, - parcs => parcs.filter( p => !p['@version'] || !p['@version']['@next']) + parcs => (parcs && parcs.filter( p => !p['@version'] || !p['@version']['@next']) || []) ) export const viewerStateParcVersionSelector = createSelector( @@ -178,12 +184,16 @@ export const viewerStateParcVersionSelector = createSelector( export const viewerStateSelectedTemplateFullInfoSelector = createSelector( viewerStateGetSelectedAtlas, viewerStateFetchedTemplatesSelector, - ({ templateSpaces }, fetchedTemplates) => templateSpaces.map(templateSpace => { - const fullTemplateInfo = fetchedTemplates.find(t => t['@id'] === templateSpace['@id']) - return { - ...templateSpace, - ...(fullTemplateInfo || {}), - darktheme: (fullTemplateInfo || {}).useTheme === 'dark' - } - }) + (selectedAtlas, fetchedTemplates) => { + if (!selectedAtlas) return null + const { templateSpaces } = selectedAtlas + return templateSpaces.map(templateSpace => { + const fullTemplateInfo = fetchedTemplates.find(t => t['@id'] === templateSpace['@id']) + return { + ...templateSpace, + ...(fullTemplateInfo || {}), + darktheme: (fullTemplateInfo || {}).useTheme === 'dark' + } + }) + } ) diff --git a/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts b/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts index 7e8fcac094bf7a344ee63ca0b9bcd8d52c5c0551..585083536b64287853ba3166840b32bb07021007 100644 --- a/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts +++ b/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts @@ -82,7 +82,7 @@ export class SplashScreen implements AfterViewInit { */ this.subscriptions.push( fromEvent(this.parentContainer.nativeElement, 'mousedown').pipe( - filter((ev: MouseEvent) => ev.which === 1), + filter((ev: MouseEvent) => ev.button === 0), switchMap(() => fromEvent(this.parentContainer.nativeElement, 'mouseup').pipe( bufferTime(200), take(1), diff --git a/src/ui/regionalFeatures/regionalFeature.service.ts b/src/ui/regionalFeatures/regionalFeature.service.ts index 3151f329660b047def9fe630e4a294da0768dd1b..07507f9ebeb1f8812325d3ec9afdb23e032d48fc 100644 --- a/src/ui/regionalFeatures/regionalFeature.service.ts +++ b/src/ui/regionalFeatures/regionalFeature.service.ts @@ -3,7 +3,7 @@ import { Inject, Injectable, OnDestroy, Optional } from "@angular/core"; import { PureContantService } from "src/util"; import { getIdFromFullId, getRegionHemisphere, getStringIdsFromRegion, flattenReducer } from 'common/util' import { forkJoin, from, Observable, of, Subject, Subscription } from "rxjs"; -import { catchError, map, mapTo, shareReplay, switchMap, tap } from "rxjs/operators"; +import { catchError, map, mapTo, shareReplay, switchMap } from "rxjs/operators"; import { IHasId } from "src/util/interfaces"; import { select, Store } from "@ngrx/store"; import { viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; @@ -12,7 +12,7 @@ import { uiStateMouseoverUserLandmark } from "src/services/state/uiState/selecto import { APPEND_SCRIPT_TOKEN } from "src/util/constants"; const libraries = [ - 'https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.1.2/es5/tex-svg.js' ] diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css index 7b0d7bbed20b91de0a5e85f7516b4c23af593ceb..66b93261b6b81769c5bf5e84d0404d91822fc86e 100644 --- a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.style.css @@ -3,3 +3,14 @@ kg-dataset-previewer display: block; height: 20em; } + +kg-ds-prv-regional-feature-view +{ + display: block; + min-height: 20em; +} + +kg-ds-prv-regional-feature-view >>> div +{ + min-height: 20em; +} diff --git a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html index 90ac2e367b2e7112948ab4c613cab2a0481cd89c..474bb7df28823b10240fc748fa8725e7384d1742 100644 --- a/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html +++ b/src/ui/regionalFeatures/singleFeatures/receptorDensity/receptorDensity/receptorDensity.template.html @@ -12,7 +12,7 @@ </ng-container> </ng-container> -<mat-form-field class="mt-2"> +<mat-form-field class="mt-2 w-100"> <mat-label> Select a receptor </mat-label> diff --git a/src/ui/viewerStateController/viewerState.useEffect.spec.ts b/src/ui/viewerStateController/viewerState.useEffect.spec.ts index d720dfa2c33a0b095aa5a69332d6cf155209501f..b38a7db90370f82ebe1c53b4811cefb053e0555a 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.spec.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.spec.ts @@ -3,7 +3,7 @@ import { Observable, of } from 'rxjs' import { TestBed, async } from '@angular/core/testing' import { provideMockActions } from '@ngrx/effects/testing' import { MockStore, provideMockStore } from '@ngrx/store/testing' -import { defaultRootState, generalActionError, NEWVIEWER } from 'src/services/stateStore.service' +import { defaultRootState, generalActionError } from 'src/services/stateStore.service' import { Injectable } from '@angular/core' import { TemplateCoordinatesTransformation, ITemplateCoordXformResp } from 'src/services/templateCoordinatesTransformation.service' import { hot } from 'jasmine-marbles' @@ -11,7 +11,9 @@ import { AngularMaterialModule } from '../sharedModules/angularMaterial.module' import { HttpClientModule } from '@angular/common/http' import { WidgetModule } from 'src/widget' import { PluginModule } from 'src/atlasViewer/pluginUnit/plugin.module' -import { viewerStateNavigateToRegion, viewerStateNavigationStateSelector, viewerStateNewViewer, viewerStateSelectTemplateWithName } from 'src/services/state/viewerState.store.helper' +import { viewerStateFetchedTemplatesSelector, viewerStateNavigateToRegion, viewerStateNavigationStateSelector, viewerStateNewViewer, viewerStateSelectAtlas, viewerStateSelectTemplateWithName } from 'src/services/state/viewerState.store.helper' +import { viewerStateFetchedAtlasesSelector } from 'src/services/state/viewerState/selectors' +import { CONST } from 'common/constants' const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json') const bigBrainNehubaConfig = require('!json-loader!src/res/ext/bigbrainNehubaConfig.json') @@ -346,6 +348,121 @@ describe('> viewerState.useEffect.ts', () => { }) }) }) + + describe('> onSelectAtlasSelectTmplParc$', () => { + let mockStore: MockStore + beforeEach(() => { + mockStore = TestBed.inject(MockStore) + }) + + it('> if atlas not found, return general error', () => { + mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, []) + mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, []) + actions$ = hot('a', { + a: viewerStateSelectAtlas({ + atlas: { + ['@id']: 'foo-bar', + } + }) + }) + + const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) + expect( + viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ + ).toBeObservable( + hot('a', { + a: generalActionError({ + message: CONST.ATLAS_NOT_FOUND + }) + }) + ) + }) + + describe('> if atlas found, will try to find id of first template', () => { + const mockParc1 = { + ['@id']: 'parc-1', + availableIn: [{ + ['@id']: 'test-1' + }] + } + const mockParc0 = { + ['@id']: 'parc-0', + availableIn: [{ + ['@id']: 'hello world' + }] + } + const mockTmplSpc = { + ['@id']: 'hello world', + availableIn: [ mockParc0 ] + } + const mockTmplSpc1 = { + ['@id']: 'test-1', + availableIn: [ mockParc1 ] + } + it('> if fails, will return general error', () => { + + mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, [ + mockTmplSpc1 + ]) + mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ + ['@id']: 'foo-bar', + templateSpaces: [ mockTmplSpc ] + }]) + actions$ = hot('a', { + a: viewerStateSelectAtlas({ + atlas: { + ['@id']: 'foo-bar', + } + }) + }) + + const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) + expect( + viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ + ).toBeObservable( + hot('a', { + a: generalActionError({ + message: CONST.TEMPLATE_NOT_FOUND + }) + }) + ) + }) + + it('> if succeeds, will dispatch new viewer', () => { + const completeMocktmpl = { + ...mockTmplSpc1, + parcellations: [ mockParc1 ] + } + mockStore.overrideSelector(viewerStateFetchedTemplatesSelector, [ + completeMocktmpl + ]) + mockStore.overrideSelector(viewerStateFetchedAtlasesSelector, [{ + ['@id']: 'foo-bar', + templateSpaces: [ mockTmplSpc1 ] + }]) + actions$ = hot('a', { + a: viewerStateSelectAtlas({ + atlas: { + ['@id']: 'foo-bar', + } + }) + }) + + const viewerSTateCtrlEffect = TestBed.inject(ViewerStateControllerUseEffect) + expect( + viewerSTateCtrlEffect.onSelectAtlasSelectTmplParc$ + ).toBeObservable( + hot('a', { + a: viewerStateNewViewer({ + selectTemplate: completeMocktmpl, + selectParcellation: mockParc1 + }) + }) + ) + }) + }) + }) + }) describe('> cvtNehubaConfigToNavigationObj', () => { diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index f5ed55c0457129b8f615d8f59fc693ceccf2b852..18eef3feafbaebc9d4bdf05b60447a6619ce870c 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -7,12 +7,14 @@ import { CHANGE_NAVIGATION, FETCHED_TEMPLATE, IavRootStoreInterface, SELECT_PARC import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "./viewerState.base"; import { TemplateCoordinatesTransformation } from "src/services/templateCoordinatesTransformation.service"; import { CLEAR_STANDALONE_VOLUMES } from "src/services/state/viewerState.store"; -import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState.store.helper"; +import { viewerStateToggleRegionSelect, viewerStateHelperSelectParcellationWithId, viewerStateSelectTemplateWithId, viewerStateNavigateToRegion, viewerStateSelectedTemplateSelector, viewerStateFetchedTemplatesSelector, viewerStateNewViewer, viewerStateSelectedParcellationSelector, viewerStateNavigationStateSelector, viewerStateSelectTemplateWithName, viewerStateSelectedRegionsSelector, viewerStateSelectAtlas } from "src/services/state/viewerState.store.helper"; import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors"; import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions"; import { PureContantService } from "src/util"; import { verifyPositionArg } from 'common/util' +import { CONST } from 'common/constants' import { uiActionHideAllDatasets } from "src/services/state/uiState/actions"; +import { viewerStateFetchedAtlasesSelector } from "src/services/state/viewerState/selectors"; const defaultPerspectiveZoom = 1e6 const defaultZoom = 1e6 @@ -115,6 +117,52 @@ export class ViewerStateControllerUseEffect implements OnDestroy { }), ) + @Effect() + public onSelectAtlasSelectTmplParc$ = this.actions$.pipe( + ofType(viewerStateSelectAtlas.type), + withLatestFrom( + this.store$.pipe( + select(viewerStateFetchedTemplatesSelector), + startWith([]) + ), + this.store$.pipe( + select(viewerStateFetchedAtlasesSelector), + startWith([]) + ) + ), + map(([action, fetchedTemplates, fetchedAtlases ])=> { + + const atlas = fetchedAtlases.find(a => a['@id'] === (action as any).atlas['@id']) + if (!atlas) { + return generalActionError({ + message: CONST.ATLAS_NOT_FOUND + }) + } + /** + * selecting atlas means selecting the first available templateSpace + */ + const templateTobeSelected = atlas.templateSpaces[0] + const templateSpaceId = templateTobeSelected['@id'] + + const parcellationId = ( + templateTobeSelected.availableIn.find(p => !!p.baseLayer) || + templateTobeSelected.availableIn[0] + )['@id'] + + const templateSelected = fetchedTemplates.find(t => templateSpaceId === t['@id']) + if (!templateSelected) { + return generalActionError({ + message: CONST.TEMPLATE_NOT_FOUND + }) + } + const parcellationSelected = templateSelected.parcellations.find(p => p['@id'] === parcellationId) + return viewerStateNewViewer({ + selectTemplate: templateSelected, + selectParcellation: parcellationSelected + }) + }) + ) + /** * on region selected change (clear, select, or change selection), clear selected dataset ids */