diff --git a/package.json b/package.json index 8b3f3a58a9110fbcb592137eda0ce04a153dc267..a0dd920987c7769ec25db9f5d9258fb416948396 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "jasmine": "^3.1.0", "jasmine-core": "^3.5.0", "jasmine-spec-reporter": "^4.2.1", + "json-loader": "^0.5.7", "karma": "^4.1.0", "karma-chrome-launcher": "^2.2.0", "karma-cli": "^2.0.0", diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 536a1760af1617ed1b9e1ff69b15c4706bc86d42..bbffae56c49aff6519c9724364975e0eb63e377e 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -24,11 +24,9 @@ import { concatMap, withLatestFrom, } from "rxjs/operators"; -import { AtlasViewerDataService } from "./atlasViewer.dataService.service"; import { WidgetServices } from "./widgetUnit/widgetService.service"; import { LayoutMainSide } from "../layouts/mainside/mainside.component"; import { AtlasViewerConstantsServices, UNSUPPORTED_PREVIEW, UNSUPPORTED_INTERVAL } from "./atlasViewer.constantService.service"; -import { AtlasViewerURLService } from "./atlasViewer.urlService.service"; import { AtlasViewerAPIServices } from "./atlasViewer.apiService.service"; import { NehubaContainer } from "../ui/nehubaContainer/nehubaContainer.component"; @@ -38,6 +36,7 @@ import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS, SHOW_BOTTOM_SHEET } from "src/ import { TabsetComponent } from "ngx-bootstrap/tabs"; import { LocalFileService } from "src/services/localFile.service"; import { MatDialog, MatDialogRef, MatSnackBar, MatSnackBarRef, MatBottomSheet, MatBottomSheetRef } from "@angular/material"; +import { isSame } from "src/util/fn"; /** @@ -120,10 +119,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { constructor( private store: Store<IavRootStoreInterface>, - public dataService: AtlasViewerDataService, private widgetServices: WidgetServices, private constantsService: AtlasViewerConstantsServices, - public urlService: AtlasViewerURLService, public apiService: AtlasViewerAPIServices, private matDialog: MatDialog, private dispatcher$: ActionsSubject, @@ -188,9 +185,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.newViewer$ = this.store.pipe( select('viewerState'), - filter(state => isDefined(state) && isDefined(state.templateSelected)), - map(state => state.templateSelected), - distinctUntilChanged((t1, t2) => t1.name === t2.name) + select('templateSelected'), + distinctUntilChanged(isSame) ) this.dedicatedView$ = this.store.pipe( @@ -345,19 +341,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ) this.subscriptions.push( - this.newViewer$.subscribe(template => { - this.darktheme = this.meetsRequirement ? - template.useTheme === 'dark' : - false - - this.constantsService.darktheme = this.darktheme - - /* new viewer should reset the spatial data search */ - this.store.dispatch({ - type : FETCHED_SPATIAL_DATA, - fetchedDataEntries : [] - }) - + this.newViewer$.subscribe(() => { this.widgetServices.clearAllWidgets() }) ) diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts index cb6da036d05d4961dbccd1769c7ef5ca5a8c1576..bbe7dc29051b75331cbb9abd8c2ca0557e058303 100644 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ b/src/atlasViewer/atlasViewer.constantService.service.ts @@ -1,12 +1,14 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { IavRootStoreInterface } from "../services/stateStore.service"; -import { Observable, Subscription } from "rxjs"; -import { map, shareReplay, filter } from "rxjs/operators"; +import { Observable, Subscription, throwError, of, merge } from "rxjs"; +import { map, shareReplay, filter, switchMap, catchError, tap } from "rxjs/operators"; import { SNACKBAR_MESSAGE } from "src/services/state/uiState.store"; +import { HttpClient } from "@angular/common/http"; export const CM_THRESHOLD = `0.05` export const CM_MATLAB_JET = `float r;if( x < 0.7 ){r = 4.0 * x - 1.5;} else {r = -4.0 * x + 4.5;}float g;if (x < 0.5) {g = 4.0 * x - 0.5;} else {g = -4.0 * x + 3.5;}float b;if (x < 0.3) {b = 4.0 * x + 0.5;} else {b = -4.0 * x + 2.5;}float a = 1.0;` +export const GLSL_COLORMAP_JET = `void main(){float x = toNormalized(getDataValue());${CM_MATLAB_JET}if(x>${CM_THRESHOLD}){emitRGB(vec3(r,g,b));}else{emitTransparent();}}` @Injectable({ providedIn : 'root' @@ -20,8 +22,6 @@ export class AtlasViewerConstantsServices implements OnDestroy { public useMobileUI$: Observable<boolean> public loadExportNehubaPromise : Promise<boolean> - public getActiveColorMapFragmentMain = ():string=>`void main(){float x = toNormalized(getDataValue());${CM_MATLAB_JET}if(x>${CM_THRESHOLD}){emitRGB(vec3(r,g,b));}else{emitTransparent();}}` - public ngLandmarkLayerName = 'spatial landmark layer' public ngUserLandmarkLayerName = 'user landmark layer' @@ -40,15 +40,6 @@ export class AtlasViewerConstantsServices implements OnDestroy { */ private TIMEOUT = 16000 - /** - * raceFetch - */ - public raceFetch = (url) => Promise.race([ - fetch(url, this.getFetchOption()), - new Promise((_, reject) => setTimeout(() => { - reject(`fetch did not resolve under ${this.TIMEOUT} ms`) - }, this.TIMEOUT)) as Promise<Response> - ]) /* TODO to be replaced by @id: Landmark/UNIQUE_ID in KG in the future */ public testLandmarksChanged : (prevLandmarks : any[], newLandmarks : any[]) => boolean = (prevLandmarks:any[], newLandmarks:any[]) => { @@ -60,6 +51,34 @@ export class AtlasViewerConstantsServices implements OnDestroy { // instead of using window.location.href, which includes query param etc public backendUrl = BACKEND_URL || `${window.location.origin}${window.location.pathname}` + private fetchTemplate = (templateUrl) => this.http.get(`${this.backendUrl}${templateUrl}`, { responseType: 'json' }).pipe( + switchMap((template:any) => { + if (template.nehubaConfig) return of(template) + if (template.nehubaConfigURL) return this.http.get(`${this.backendUrl}${template.nehubaConfigURL}`, { responseType: 'json' }).pipe( + map(nehubaConfig => { + return { + ...template, + nehubaConfig + } + }) + ) + throwError('neither nehubaConfig nor nehubaConfigURL defined') + }) + ) + + public totalTemplates = null + + public initFetchTemplate$ = this.http.get(`${this.backendUrl}templates`, { responseType: 'json' }).pipe( + tap((arr:any[]) => this.totalTemplates = arr.length), + switchMap((templates: string[]) => merge( + ...templates.map(this.fetchTemplate) + )), + catchError((err) => { + console.warn(`fetching templates error`, err) + return of(null) + }) + ) + /* to be provided by KG in future */ public templateUrlsPr : Promise<string[]> = new Promise((resolve, reject) => { fetch(`${this.backendUrl}templates`, this.getFetchOption()) @@ -233,14 +252,17 @@ Send us an email: <a target = "_blank" href = "mailto:${this.supportEmailAddress private repoUrl = `https://github.com/HumanBrainProject/interactive-viewer` constructor( - private store$: Store<IavRootStoreInterface> + private store$: Store<IavRootStoreInterface>, + private http: HttpClient, ){ this.darktheme$ = this.store$.pipe( select('viewerState'), select('templateSelected'), - filter(v => !!v), - map(({useTheme}) => useTheme === 'dark'), + map(template => { + if (!template) return false + return template.useTheme === 'dark' + }), shareReplay(1) ) @@ -250,6 +272,10 @@ Send us an email: <a target = "_blank" href = "mailto:${this.supportEmailAddress shareReplay(1) ) + this.subscriptions.push( + this.darktheme$.subscribe(flag => this.darktheme = flag) + ) + this.subscriptions.push( this.useMobileUI$.subscribe(bool => { if (bool) { diff --git a/src/atlasViewer/atlasViewer.dataService.service.ts b/src/atlasViewer/atlasViewer.dataService.service.ts deleted file mode 100644 index 31d9248911486947a0c7987e4d5e953ac8d132b4..0000000000000000000000000000000000000000 --- a/src/atlasViewer/atlasViewer.dataService.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Injectable, OnDestroy } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { FETCHED_TEMPLATE, IavRootStoreInterface } from "../services/stateStore.service"; -import { Subscription } from "rxjs"; -import { AtlasViewerConstantsServices } from "./atlasViewer.constantService.service"; - -/** - * TODO move constructor into else where and deprecate ASAP - */ - -@Injectable({ - providedIn : 'root' -}) -export class AtlasViewerDataService implements OnDestroy{ - - private subscriptions : Subscription[] = [] - - constructor( - private store: Store<IavRootStoreInterface>, - private constantService : AtlasViewerConstantsServices - ){ - this.constantService.templateUrlsPr - .then(urls => - urls.map(url => - this.constantService.raceFetch(`${this.constantService.backendUrl}${url}`) - .then(res => res.json()) - .then(json => new Promise((resolve, reject) => { - if(json.nehubaConfig) - resolve(json) - else if(json.nehubaConfigURL) - this.constantService.raceFetch(`${this.constantService.backendUrl}${json.nehubaConfigURL}`) - .then(res => res.json()) - .then(json2 => resolve({ - ...json, - nehubaConfig: json2 - })) - .catch(reject) - else - reject('neither nehubaConfig nor nehubaConfigURL defined') - })) - .then(json => this.store.dispatch({ - type: FETCHED_TEMPLATE, - fetchedTemplate: json - })) - .catch(e => { - console.warn('fetching template url failed', e) - this.store.dispatch({ - type: FETCHED_TEMPLATE, - fetchedTemplate: null - }) - }) - )) - } - - public searchDataset(){ - - } - - ngOnDestroy(){ - this.subscriptions.forEach(s=>s.unsubscribe()) - } -} \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.history.service.ts b/src/atlasViewer/atlasViewer.history.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa676870381f32cb57feac78cdf54be0c8cf19ce --- /dev/null +++ b/src/atlasViewer/atlasViewer.history.service.ts @@ -0,0 +1,126 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { Effect, Actions, ofType } from '@ngrx/effects' +import { IavRootStoreInterface, GENERAL_ACTION_TYPES, defaultRootState } from "src/services/stateStore.service"; +import { debounceTime, distinctUntilChanged, map, startWith, filter, withLatestFrom, switchMap, take, switchMapTo } from "rxjs/operators"; +import { Subscription, fromEvent, merge, of } from "rxjs"; +import { cvtStateToSearchParam, cvtSearchParamToState } from "./atlasViewer.urlUtil"; +import { AtlasViewerConstantsServices } from "src/ui/databrowserModule/singleDataset/singleDataset.base"; + +const getSearchParamStringFromState = state => { + try { + return cvtStateToSearchParam(state).toString() + } catch (e) { + console.warn(`cvt state to search param error`, e) + return null + } +} + +@Injectable({ + providedIn: 'root' +}) + +export class AtlasViewerHistoryUseEffect implements OnDestroy{ + + // ensure that fetchedTemplates are all populated + @Effect() + parsingSearchUrlToState$ = this.store$.pipe( + filter(state => state.viewerState.fetchedTemplates.length === this.constantService.totalTemplates), + take(1), + switchMapTo(merge( + // parsing state can occur via 2 ways: + // either pop state event or on launch + fromEvent(window, 'popstate').pipe( + map(({ state } : PopStateEvent) => state) + ), + of(new URLSearchParams(window.location.search).toString()) + )) + ).pipe( + withLatestFrom(this.store$), + map(([searchUrl, storeState]: [string, IavRootStoreInterface] ) => { + const search = new URLSearchParams(searchUrl) + try { + if (Array.from(search.keys()).length === 0) { + // if empty searchParam + return { + type: GENERAL_ACTION_TYPES.APPLY_STATE, + state: { + ...defaultRootState, + viewerState: { + ...defaultRootState.viewerState, + fetchedTemplates: storeState.viewerState.fetchedTemplates + } + } + } + } else { + // if non empty search param + const newState = cvtSearchParamToState(search, storeState) + return { + type: GENERAL_ACTION_TYPES.APPLY_STATE, + state: newState + } + } + } catch (e) { + // usually parsing error + // TODO should log + return { + type: GENERAL_ACTION_TYPES.APPLY_STATE, + state: { + ...defaultRootState, + viewerState: { + ...defaultRootState.viewerState, + fetchedTemplates: storeState.viewerState.fetchedTemplates + } + } + } + } + }) + ) + + private subscriptions: Subscription[] = [] + + private currentStateSearchParam$ = this.store$.pipe( + map(getSearchParamStringFromState), + filter(v => v !== null) + ) + + constructor( + private store$: Store<IavRootStoreInterface>, + private actions$: Actions, + private constantService: AtlasViewerConstantsServices + ){ + this.subscriptions.push( + + // GENERAL_ACTION_TYPES.APPLY_STATE is triggered by pop state or initial + // conventiently, the action has a state property + this.actions$.pipe( + ofType(GENERAL_ACTION_TYPES.APPLY_STATE), + // subscribe to inner obs on init + startWith({}), + switchMap(({ state } :any) => + this.currentStateSearchParam$.pipe( + distinctUntilChanged(), + debounceTime(100), + + // compares the searchParam triggerd by change of state with the searchParam generated by GENERAL_ACTION_TYPES.APPLY_STATE + // if the same, the change is induced by GENERAL_ACTION_TYPES.APPLY_STATE, and should NOT be pushed to history + filter((newSearchParam, index) => { + const oldSearchParam = (state && getSearchParamStringFromState(state)) || '' + + // in the unlikely event that user returns to the exact same state without use forward/back button + return index > 0 || newSearchParam !== oldSearchParam + }) + ) + ) + ).subscribe(newSearchString => { + const url = new URL(window.location.toString()) + url.search = newSearchString + window.history.pushState(newSearchString, '', url.toString()) + }) + ) + } + + ngOnDestroy(){ + while(this.subscriptions.length > 0) this.subscriptions.pop().unsubscribe() + } +} \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.pluginService.service.ts b/src/atlasViewer/atlasViewer.pluginService.service.ts index 3165a3f118de88a2352c0258213f75047de65fea..7fb7a085f0daae921386cb1a60d4825a26a0f7a7 100644 --- a/src/atlasViewer/atlasViewer.pluginService.service.ts +++ b/src/atlasViewer/atlasViewer.pluginService.service.ts @@ -8,10 +8,12 @@ import { WidgetServices } from "./widgetUnit/widgetService.service"; import '../res/css/plugin_styles.css' import { BehaviorSubject, Observable, merge, of } from "rxjs"; -import { map, shareReplay } from "rxjs/operators"; -import { Store } from "@ngrx/store"; +import { map, shareReplay, filter, startWith } from "rxjs/operators"; +import { Store, select } from "@ngrx/store"; import { WidgetUnit } from "./widgetUnit/widgetUnit.component"; import { AtlasViewerConstantsServices } from "./atlasViewer.constantService.service"; +import { ACTION_TYPES as PLUGINSTORE_ACTION_TYPES, CONSTANTS as PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' +import { Effect } from "@ngrx/effects"; @Injectable({ providedIn : 'root' @@ -40,6 +42,13 @@ export class PluginServices{ private http: HttpClient ){ + // TODO implement + this.store.pipe( + select('pluginState'), + select('initManifests'), + filter(v => !!v) + ) + this.pluginUnitFactory = this.cfr.resolveComponentFactory( PluginUnit ) this.apiService.interactiveViewer.uiHandle.launchNewWidget = this.launchNewWidget.bind(this) @@ -282,6 +291,49 @@ export class PluginServices{ } } +@Injectable({ + providedIn: 'root' +}) + +export class PluginServiceuseEffect{ + + @Effect() + public initManifests$: Observable<any> + + constructor( + store$: Store<IavRootStoreInterface>, + constantService: AtlasViewerConstantsServices, + pluginService: PluginServices + ){ + this.initManifests$ = store$.pipe( + select('pluginState'), + select('initManifests'), + filter(v => !!v), + startWith([]), + map(arr => { + // only launch plugins that has init manifest src label on it + return arr.filter(([ source ]) => source === PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC) + }), + filter(arr => arr.length > 0), + map((arr: [string, string|null][]) => { + + for (const [source, url] of arr){ + fetch(url, constantService.getFetchOption()) + .then(res => res.json()) + .then(json => pluginService.launchNewWidget(json)) + .catch(console.error) + } + + // clear init manifest + return { + type: PLUGINSTORE_ACTION_TYPES.CLEAR_INIT_PLUGIN + } + }) + ) + } +} + + export class PluginHandler{ onShutdown : (callback:()=>void)=>void = (_) => {} blink : (sec?:number)=>void = (_) => {} diff --git a/src/atlasViewer/atlasViewer.urlService.service.ts b/src/atlasViewer/atlasViewer.urlService.service.ts deleted file mode 100644 index f0ca872ceead6e378668d7ae422c95f5322eb05e..0000000000000000000000000000000000000000 --- a/src/atlasViewer/atlasViewer.urlService.service.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Store, select } from "@ngrx/store"; -import { ViewerStateInterface, isDefined, NEWVIEWER, CHANGE_NAVIGATION, ADD_NG_LAYER } from "../services/stateStore.service"; -import { StateInterface as PluginStateInterface } from 'src/services/state/pluginState.store' -import { Observable,combineLatest } from "rxjs"; -import { filter, map, scan, distinctUntilChanged, skipWhile, take } from "rxjs/operators"; -import { PluginServices } from "./atlasViewer.pluginService.service"; -import { AtlasViewerConstantsServices, encodeNumber, separator, decodeToNumber } from "./atlasViewer.constantService.service"; -import { SELECT_REGIONS_WITH_ID } from "src/services/state/viewerState.store"; -import { UIService } from "src/services/uiService.service"; - -declare var window - -const parseQueryString = (searchparams: URLSearchParams) => { - -} - -@Injectable({ - providedIn : 'root' -}) - -export class AtlasViewerURLService{ - private changeQueryObservable$ : Observable<any> - private additionalNgLayers$ : Observable<any> - private pluginState$ : Observable<PluginStateInterface> - - constructor( - private store : Store<ViewerStateInterface>, - private pluginService : PluginServices, - private constantService:AtlasViewerConstantsServices, - private uiService:UIService - ){ - - this.pluginState$ = this.store.pipe( - select('pluginState'), - distinctUntilChanged() - ) - - this.changeQueryObservable$ = this.store.pipe( - select('viewerState'), - filter(state=> - isDefined(state) && - (isDefined(state.templateSelected) || - isDefined(state.regionsSelected) || - isDefined(state.navigation) || - isDefined(state.parcellationSelected))), - - /* map so that only a selection are serialized */ - map(({templateSelected,regionsSelected,navigation,parcellationSelected})=>({ - templateSelected, - regionsSelected, - navigation, - parcellationSelected - })) - ).pipe( - scan((acc,val)=>Object.assign({},acc,val),{}) - ) - - /** - * TODO change additionalNgLayer to id, querying node backend for actual urls - */ - this.additionalNgLayers$ = combineLatest( - this.changeQueryObservable$.pipe( - select('templateSelected'), - filter(v => !!v) - ), - /** - * TODO duplicated with viewerState.loadedNgLayers ? - */ - this.store.pipe( - select('ngViewerState'), - select('layers') - ) - ).pipe( - map(([templateSelected, layers])=>{ - const state = templateSelected.nehubaConfig.dataset.initialNgState - /* TODO currently only parameterise nifti layer */ - return layers.filter(layer => /^nifti\:\/\//.test(layer.source) && Object.keys(state.layers).findIndex(layerName => layerName === layer.name) < 0) - }) - ) - - /* services has no ngOnInit lifecycle */ - this.subscriptions() - } - - private subscriptions(){ - - /* parse search url to state */ - this.store.pipe( - select('viewerState'), - select('fetchedTemplates'), - filter(_=> !!_), - skipWhile(fetchedTemplates => fetchedTemplates.length !== this.constantService.templateUrls.length), - take(1), - map(ft => ft.filter(t => t !== null)) - ).subscribe(fetchedTemplates=>{ - - /** - * TODO - * consider what to do when we have ill formed search params - * param validation? - */ - const searchparams = new URLSearchParams(window.location.search) - - /** - * TODO - * triage: change of template and parcellation names is breaking old links - * change back when camilla/oli updated the links to new versions - */ - - /* first, check if any template and parcellations are to be loaded */ - const searchedTemplatename = (() => { - const param = searchparams.get('templateSelected') - if (param === 'Allen Mouse') return `Allen adult mouse brain reference atlas V3` - if (param === 'Waxholm Rat V2.0') return 'Waxholm Space rat brain atlas v.2.0' - return param - })() - const searchedParcellationName = (() => { - const param = searchparams.get('parcellationSelected') - if (param === 'Allen Mouse Brain Atlas') return 'Allen adult mouse brain reference atlas V3 Brain Atlas' - if (param === 'Whole Brain (v2.0)') return 'Waxholm Space rat brain atlas v.2.0' - return param - })() - - if (!searchedTemplatename) { - const urlString = window.location.href - /** - * TODO think of better way of doing this - */ - history.replaceState(null, '', urlString.split('?')[0]) - return - } - - const templateToLoad = fetchedTemplates.find(template=>template.name === searchedTemplatename) - if (!templateToLoad) { - this.uiService.showMessage( - this.constantService.incorrectTemplateNameSearchParam(searchedTemplatename), - null, - { duration: 5000 } - ) - const urlString = window.location.href - /** - * TODO think of better way of doing this... maybe pushstate? - */ - history.replaceState(null, '', urlString.split('?')[0]) - return - } - - /** - * TODO if search param of either template or parcellation is incorrect, wrong things are searched - */ - const parcellationToLoad = templateToLoad.parcellations.find(parcellation=>parcellation.name === searchedParcellationName) - - if (!parcellationToLoad) { - this.uiService.showMessage( - this.constantService.incorrectParcellationNameSearchParam(searchedParcellationName), - null, - { duration: 5000 } - ) - } - - this.store.dispatch({ - type : NEWVIEWER, - selectTemplate : templateToLoad, - selectParcellation : parcellationToLoad || templateToLoad.parcellations[0] - }) - - /* selected regions */ - if (parcellationToLoad && parcellationToLoad.regions) { - /** - * either or both parcellationToLoad and .regions maybe empty - */ - /** - * backwards compatibility - */ - const selectedRegionsParam = searchparams.get('regionsSelected') - if(selectedRegionsParam){ - const ids = selectedRegionsParam.split('_') - - this.store.dispatch({ - type : SELECT_REGIONS_WITH_ID, - selectRegionIds: ids - }) - } - - const cRegionsSelectedParam = searchparams.get('cRegionsSelected') - if (cRegionsSelectedParam) { - try { - const json = JSON.parse(cRegionsSelectedParam) - - const selectRegionIds = [] - - for (let ngId in json) { - const val = json[ngId] - const labelIndicies = val.split(separator).map(n =>{ - try{ - return decodeToNumber(n) - } catch (e) { - /** - * TODO poisonsed encoded char, send error message - */ - this.uiService.showMessage(`cRegionSelectionParam is malformed: cannot decode ${n}`) - return null - } - }).filter(v => !!v) - for (let labelIndex of labelIndicies) { - selectRegionIds.push(`${ngId}#${labelIndex}`) - } - } - - this.store.dispatch({ - type: SELECT_REGIONS_WITH_ID, - selectRegionIds - }) - - } catch (e) { - /** - * parsing cRegionSelected error - */ - console.log('parsing cRegionSelected error', e) - } - } - } - - /* now that the parcellation is loaded, load the navigation state */ - const viewerState = searchparams.get('navigation') - if(viewerState){ - const [o,po,pz,p,z] = viewerState.split('__') - this.store.dispatch({ - type : CHANGE_NAVIGATION, - navigation : { - orientation : o.split('_').map(n=>Number(n)), - perspectiveOrientation : po.split('_').map(n=>Number(n)), - perspectiveZoom : Number(pz), - position : p.split('_').map(n=>Number(n)), - zoom : Number(z) - } - }) - } - - const cViewerState = searchparams.get('cNavigation') - if (cViewerState) { - try { - const [ cO, cPO, cPZ, cP, cZ ] = cViewerState.split(`${separator}${separator}`) - const o = cO.split(separator).map(s => decodeToNumber(s, {float: true})) - const po = cPO.split(separator).map(s => decodeToNumber(s, {float: true})) - const pz = decodeToNumber(cPZ) - const p = cP.split(separator).map(s => decodeToNumber(s)) - const z = decodeToNumber(cZ) - this.store.dispatch({ - type : CHANGE_NAVIGATION, - navigation : { - orientation: o, - perspectiveOrientation: po, - perspectiveZoom: pz, - position: p, - zoom: z - } - }) - } catch (e) { - /** - * TODO Poisoned encoded char - * send error message - */ - } - } - - const niftiLayers = searchparams.get('niftiLayers') - if(niftiLayers){ - const layers = niftiLayers.split('__') - - layers.forEach(layer => this.store.dispatch({ - type : ADD_NG_LAYER, - layer : { - name : layer, - source : `nifti://${layer}`, - mixability : 'nonmixable', - shader : this.constantService.getActiveColorMapFragmentMain() - } - })) - } - - const pluginStates = searchparams.get('pluginStates') - if(pluginStates){ - const arrPluginStates = pluginStates.split('__') - arrPluginStates.forEach(url => fetch(url, this.constantService.getFetchOption()).then(res => res.json()).then(json => this.pluginService.launchNewWidget(json)).catch(console.error)) - } - }) - - /* pushing state to url */ - combineLatest( - combineLatest( - this.changeQueryObservable$, - this.store.pipe( - select('viewerState'), - select('parcellationSelected') - ) - ).pipe( - map(([state, parcellationSelected])=>{ - let _ = {} - for(const key in state){ - if(isDefined(state[key])){ - switch(key){ - case 'navigation': - if( - isDefined(state[key].orientation) && - isDefined(state[key].perspectiveOrientation) && - isDefined(state[key].perspectiveZoom) && - isDefined(state[key].position) && - isDefined(state[key].zoom) - ){ - const { - orientation, - perspectiveOrientation, - perspectiveZoom, - position, - zoom - } = state[key] - - _['cNavigation'] = [ - orientation.map(n => encodeNumber(n, {float: true})).join(separator), - perspectiveOrientation.map(n => encodeNumber(n, {float: true})).join(separator), - encodeNumber(Math.floor(perspectiveZoom)), - Array.from(position).map((v:number) => Math.floor(v)).map(n => encodeNumber(n)).join(separator), - encodeNumber(Math.floor(zoom)) - ].join(`${separator}${separator}`) - - _[key] = null - } - break; - case 'regionsSelected': { - // _[key] = state[key].map(({ ngId, labelIndex })=> generateLabelIndexId({ ngId,labelIndex })).join('_') - const ngIdLabelIndexMap : Map<string, number[]> = state[key].reduce((acc, curr) => { - const returnMap = new Map(acc) - const { ngId, labelIndex } = curr - const existingArr = (returnMap as Map<string, number[]>).get(ngId) - if (existingArr) { - existingArr.push(labelIndex) - } else { - returnMap.set(ngId, [labelIndex]) - } - return returnMap - }, new Map()) - - if (ngIdLabelIndexMap.size === 0) { - _['cRegionsSelected'] = null - _[key] = null - break; - } - - const returnObj = {} - - for (let entry of ngIdLabelIndexMap) { - const [ ngId, labelIndicies ] = entry - returnObj[ngId] = labelIndicies.map(n => encodeNumber(n)).join(separator) - } - - _['cRegionsSelected'] = JSON.stringify(returnObj) - _[key] = null - break; - } - case 'templateSelected': - case 'parcellationSelected': - _[key] = state[key].name - break; - default: - _[key] = state[key] - } - } - } - return _ - }) - ), - this.additionalNgLayers$.pipe( - map(layers => layers - .map(layer => layer.name) - .filter(layername => !/^blob\:/.test(layername))) - ), - this.pluginState$ - ).pipe( - /* TODO fix encoding of nifti path. if path has double underscore, this encoding will fail */ - map(([navigationState, niftiLayers, pluginState]) => { - return { - ...navigationState, - pluginState: Array.from(pluginState.initManifests.values()).filter(v => v !== null).length > 0 - ? Array.from(pluginState.initManifests.values()).filter(v => v !== null).join('__') - : null, - niftiLayers : niftiLayers.length > 0 - ? niftiLayers.join('__') - : null - } - }) - ).subscribe(cleanedState=>{ - const url = new URL(window.location) - const search = new URLSearchParams( window.location.search ) - for (const key in cleanedState) { - if (cleanedState[key]) { - search.set(key, cleanedState[key]) - } else { - search.delete(key) - } - } - - url.search = search.toString() - history.replaceState(null, '', url.toString()) - }) - } -} \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.urlUtil.spec.ts b/src/atlasViewer/atlasViewer.urlUtil.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e36c0c1cb70a7603d1473fac4faba5d039fcaa6d --- /dev/null +++ b/src/atlasViewer/atlasViewer.urlUtil.spec.ts @@ -0,0 +1,69 @@ +import {} from 'jasmine' +import { cvtSearchParamToState, PARSING_SEARCHPARAM_ERROR } from './atlasViewer.urlUtil' +import { defaultRootState } from 'src/services/stateStore.service' + +const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json') +const colin = require('!json-loader!src/res/ext/colin.json') +const mni152 = require('!json-loader!src/res/ext/MNI152.json') +const allen = require('!json-loader!src/res/ext/allenMouse.json') +const waxholm = require('!json-loader!src/res/ext/waxholmRatV2_0.json') + +const { viewerState, ...rest } = defaultRootState +const fetchedTemplateRootState = { + ...rest, + viewerState: { + ...viewerState, + fetchedTemplates: [ bigbrainJson, colin, mni152, allen, waxholm ] + } +} + +describe('atlasViewer.urlService.service.ts', () => { + describe('cvtSearchParamToState', () => { + it('convert empty search param to empty state', () => { + const searchparam = new URLSearchParams() + expect(() => cvtSearchParamToState(searchparam, defaultRootState)).toThrow() + }) + + it('successfully converts with only template defined', () => { + const searchparam = new URLSearchParams('?templateSelected=Big+Brain+%28Histology%29') + + const newState = cvtSearchParamToState(searchparam, fetchedTemplateRootState) + + const { parcellationSelected, templateSelected } = newState.viewerState + expect(templateSelected.name).toEqual(bigbrainJson.name) + expect(parcellationSelected.name).toEqual(bigbrainJson.parcellations[0].name) + }) + + it('successfully converts with template AND parcellation defined', () => { + const searchparam = new URLSearchParams() + searchparam.set('templateSelected', mni152.name) + searchparam.set('parcellationSelected', mni152.parcellations[1].name) + + const newState = cvtSearchParamToState(searchparam, fetchedTemplateRootState) + + const { parcellationSelected, templateSelected } = newState.viewerState + expect(templateSelected.name).toEqual(mni152.name) + expect(parcellationSelected.name).toEqual(mni152.parcellations[1].name) + }) + + it('successfully converts with template, parcellation AND selected regions defined', () => { + + }) + + it('parses cNavigation correctly', () => { + + }) + + it('parses niftiLayers correctly', () => { + + }) + + it('parses pluginStates correctly', () => { + + }) + }) + + describe('cvtStateToSearchParam', () => { + + }) +}) \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.urlUtil.ts b/src/atlasViewer/atlasViewer.urlUtil.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a7c38f1f6b886ba8d43bfe3412cc0eb7dbe46d7 --- /dev/null +++ b/src/atlasViewer/atlasViewer.urlUtil.ts @@ -0,0 +1,238 @@ +import { IavRootStoreInterface, generateLabelIndexId, getNgIdLabelIndexFromRegion } from "../services/stateStore.service"; +import { encodeNumber, separator, decodeToNumber, GLSL_COLORMAP_JET } from "./atlasViewer.constantService.service"; +import { mixNgLayers } from "src/services/state/ngViewerState.store"; +import { getGetRegionFromLabelIndexId } from "src/services/effect/effect"; +import { CONSTANTS as PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' + +export const PARSING_SEARCHPARAM_ERROR = { + TEMPALTE_NOT_SET: 'TEMPALTE_NOT_SET', + TEMPLATE_NOT_FOUND: 'TEMPLATE_NOT_FOUND', + PARCELLATION_NOT_UPDATED: 'PARCELLATION_NOT_UPDATED' +} +const PARSING_SEARCHPARAM_WARNING = { + UNKNOWN_PARCELLATION: 'UNKNOWN_PARCELLATION', + DECODE_CIPHER_ERROR: 'DECODE_CIPHER_ERROR' +} + +export const CVT_STATE_TO_SEARCHPARAM_ERROR = { + TEMPLATE_NOT_SELECTED: 'TEMPLATE_NOT_SELECTED' +} + +export const cvtStateToSearchParam = (state: IavRootStoreInterface): URLSearchParams => { + const searchParam = new URLSearchParams() + + const { viewerState, ngViewerState, pluginState } = state + const { templateSelected, parcellationSelected, navigation, regionsSelected } = viewerState + + if (!templateSelected) throw new Error(CVT_STATE_TO_SEARCHPARAM_ERROR.TEMPLATE_NOT_SELECTED) + + // encoding states + searchParam.set('templateSelected', templateSelected.name) + searchParam.set('parcellationSelected', parcellationSelected.name) + + // encoding selected regions + const accumulatorMap = new Map<string, number[]>() + for (const region of regionsSelected) { + const { ngId, labelIndex } = getNgIdLabelIndexFromRegion({ region }) + const existingEntry = accumulatorMap.get(ngId) + if (existingEntry) existingEntry.push(labelIndex) + else accumulatorMap.set(ngId, [ labelIndex ]) + } + const cRegionObj = {} + for (const [key, arr] of accumulatorMap){ + cRegionObj[key] = arr.map(n => encodeNumber(n)).join(separator) + } + searchParam.set('cRegionsSelected', JSON.stringify(cRegionObj)) + + // encoding navigation + const { orientation, perspectiveOrientation, perspectiveZoom, position, zoom } = navigation + const cNavString = [ + orientation.map(n => encodeNumber(n, {float: true})).join(separator), + perspectiveOrientation.map(n => encodeNumber(n, {float: true})).join(separator), + encodeNumber(Math.floor(perspectiveZoom)), + Array.from(position).map((v:number) => Math.floor(v)).map(n => encodeNumber(n)).join(separator), + encodeNumber(Math.floor(zoom)) + ].join(`${separator}${separator}`) + searchParam.set('cNavigation', cNavString) + + // encode nifti layers + const initialNgState = templateSelected.nehubaConfig.dataset.initialNgState + const { layers } = ngViewerState + const additionalLayers = layers.filter(layer => + /^blob\:/.test(layer.name) && + Object.keys(initialNgState.layers).findIndex(layerName => layerName === layer.name) < 0 + ) + const niftiLayers = additionalLayers.filter(layer => /^nifti\:\/\//.test(layer.source)) + if (niftiLayers.length > 0) searchParam.set('niftiLayers', niftiLayers.join('__')) + + // plugin state + const { initManifests } = pluginState + const pluginStateParam = initManifests + .filter(([ src ]) => src !== PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC) + .map(([ src, url]) => url) + .join('__') + + if (initManifests.length > 0) searchParam.set('pluginState', pluginStateParam) + + return searchParam +} + +export const cvtSearchParamToState = (searchparams: URLSearchParams, state:IavRootStoreInterface, warningCb = (args) => {}): IavRootStoreInterface => { + + const returnState = JSON.parse(JSON.stringify(state)) as IavRootStoreInterface + + const { TEMPLATE_NOT_FOUND, TEMPALTE_NOT_SET, PARCELLATION_NOT_UPDATED } = PARSING_SEARCHPARAM_ERROR + const { UNKNOWN_PARCELLATION, DECODE_CIPHER_ERROR } = PARSING_SEARCHPARAM_WARNING + const { fetchedTemplates } = state.viewerState + + const searchedTemplatename = (() => { + const param = searchparams.get('templateSelected') + if (param === 'Allen Mouse') return `Allen adult mouse brain reference atlas V3` + if (param === 'Waxholm Rat V2.0') return 'Waxholm Space rat brain atlas v.2.0' + return param + })() + const searchedParcellationName = (() => { + const param = searchparams.get('parcellationSelected') + if (param === 'Allen Mouse Brain Atlas') return 'Allen adult mouse brain reference atlas V3 Brain Atlas' + if (param === 'Whole Brain (v2.0)') return 'Waxholm Space rat brain atlas v.2.0' + return param + })() + + if (!searchedTemplatename) throw new Error(TEMPALTE_NOT_SET) + + const templateToLoad = fetchedTemplates.find(template=>template.name === searchedTemplatename) + if (!templateToLoad) throw new Error(TEMPLATE_NOT_FOUND) + + /** + * TODO if search param of either template or parcellation is incorrect, wrong things are searched + */ + const parcellationToLoad = templateToLoad.parcellations.find(parcellation=>parcellation.name === searchedParcellationName) + if (!parcellationToLoad) warningCb({ type: UNKNOWN_PARCELLATION }) + + const { viewerState } = returnState + viewerState.templateSelected = templateToLoad + viewerState.parcellationSelected = parcellationToLoad || templateToLoad.parcellations[0] + + /* selected regions */ + + // TODO deprecate. Fallback (defaultNgId) (should) already exist + // if (!viewerState.parcellationSelected.updated) throw new Error(PARCELLATION_NOT_UPDATED) + + const getRegionFromlabelIndexId = getGetRegionFromLabelIndexId({ parcellation: viewerState.parcellationSelected }) + /** + * either or both parcellationToLoad and .regions maybe empty + */ + /** + * backwards compatibility + */ + const selectedRegionsParam = searchparams.get('regionsSelected') + if(selectedRegionsParam){ + const ids = selectedRegionsParam.split('_') + + viewerState.regionsSelected = ids.map(labelIndexId => getRegionFromlabelIndexId({ labelIndexId })) + } + + const cRegionsSelectedParam = searchparams.get('cRegionsSelected') + if (cRegionsSelectedParam) { + try { + const json = JSON.parse(cRegionsSelectedParam) + + const selectRegionIds = [] + + for (let ngId in json) { + const val = json[ngId] + const labelIndicies = val.split(separator).map(n =>{ + try{ + return decodeToNumber(n) + } catch (e) { + /** + * TODO poisonsed encoded char, send error message + */ + warningCb({ type: DECODE_CIPHER_ERROR, message: `cRegionSelectionParam is malformed: cannot decode ${n}` }) + return null + } + }).filter(v => !!v) + for (let labelIndex of labelIndicies) { + selectRegionIds.push( generateLabelIndexId({ ngId, labelIndex }) ) + } + } + viewerState.regionsSelected = selectRegionIds.map(labelIndexId => getRegionFromlabelIndexId({ labelIndexId })) + + } catch (e) { + /** + * parsing cRegionSelected error + */ + warningCb({ type: DECODE_CIPHER_ERROR, message: `parsing cRegionSelected error ${e.toString()}` }) + } + } + + /* now that the parcellation is loaded, load the navigation state */ + /* what to do with malformed navigation? */ + + // for backwards compatibility + const _viewerState = searchparams.get('navigation') + if(_viewerState){ + const [o,po,pz,p,z] = _viewerState.split('__') + viewerState.navigation = { + orientation : o.split('_').map(n=>Number(n)), + perspectiveOrientation : po.split('_').map(n=>Number(n)), + perspectiveZoom : Number(pz), + position : p.split('_').map(n=>Number(n)), + zoom : Number(z), + + // flag to allow for animation when enabled + animation: {} + } + } + + const cViewerState = searchparams.get('cNavigation') + if (cViewerState) { + try { + const [ cO, cPO, cPZ, cP, cZ ] = cViewerState.split(`${separator}${separator}`) + const o = cO.split(separator).map(s => decodeToNumber(s, {float: true})) + const po = cPO.split(separator).map(s => decodeToNumber(s, {float: true})) + const pz = decodeToNumber(cPZ) + const p = cP.split(separator).map(s => decodeToNumber(s)) + const z = decodeToNumber(cZ) + viewerState.navigation = { + orientation: o, + perspectiveOrientation: po, + perspectiveZoom: pz, + position: p, + zoom: z, + + // flag to allow for animation when enabled + animation: {} + } + } catch (e) { + /** + * TODO Poisoned encoded char + * send error message + */ + } + } + + const niftiLayers = searchparams.get('niftiLayers') + if(niftiLayers){ + const layers = niftiLayers + .split('__') + .map(layer => { + return { + name : layer, + source : `nifti://${layer}`, + mixability : 'nonmixable', + shader : GLSL_COLORMAP_JET + } as any + }) + const { ngViewerState } = returnState + ngViewerState.layers = mixNgLayers(ngViewerState.layers, layers) + } + + const { pluginState } = returnState + const pluginStates = searchparams.get('pluginStates') + if(pluginStates){ + const arrPluginStates = pluginStates.split('__') + pluginState.initManifests = arrPluginStates.map(url => [PLUGINSTORE_CONSTANTS.INIT_MANIFEST_SRC, url] as [string, string]) + } + return returnState +} diff --git a/src/main.module.ts b/src/main.module.ts index 076716937a6a011e39384792744bb50d042b95d2..352b25fa853b788a8e7e1a1f3a74ebe5a6c25cbb 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -12,14 +12,13 @@ import { GetNamePipe } from "./util/pipes/getName.pipe"; import { FormsModule } from "@angular/forms"; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' -import { AtlasViewerDataService } from "./atlasViewer/atlasViewer.dataService.service"; import { WidgetUnit } from "./atlasViewer/widgetUnit/widgetUnit.component"; import { WidgetServices } from './atlasViewer/widgetUnit/widgetService.service' +// TODO deprecate import { fasTooltipScreenshotDirective,fasTooltipInfoSignDirective,fasTooltipLogInDirective,fasTooltipNewWindowDirective,fasTooltipQuestionSignDirective,fasTooltipRemoveDirective,fasTooltipRemoveSignDirective } from "./util/directives/glyphiconTooltip.directive"; import { TooltipModule } from "ngx-bootstrap/tooltip"; import { TabsModule } from 'ngx-bootstrap/tabs' import { ModalUnit } from "./atlasViewer/modalUnit/modalUnit.component"; -import { AtlasViewerURLService } from "./atlasViewer/atlasViewer.urlService.service"; import { ToastComponent } from "./components/toast/toast.component"; import { AtlasViewerAPIServices } from "./atlasViewer/atlasViewer.apiService.service"; import { PluginUnit } from "./atlasViewer/pluginUnit/pluginUnit.component"; @@ -47,13 +46,15 @@ import { NgViewerUseEffect } from "./services/state/ngViewerState.store"; import { DatabrowserModule } from "./ui/databrowserModule/databrowser.module"; import { UIService } from "./services/uiService.service"; import { UtilModule } from "./util/util.module"; +import {CaptureClickListenerDirective} from "src/util/directives/captureClickListener.directive"; +import { PluginServiceuseEffect } from "./atlasViewer/atlasViewer.pluginService.service"; import 'hammerjs' import 'src/res/css/version.css' import 'src/theme.scss' import 'src/res/css/extra_styles.css' -import {CaptureClickListenerDirective} from "src/util/directives/captureClickListener.directive"; +import { AtlasViewerHistoryUseEffect } from "./atlasViewer/atlasViewer.history.service"; @NgModule({ imports : [ @@ -75,7 +76,9 @@ import {CaptureClickListenerDirective} from "src/util/directives/captureClickLis UserConfigStateUseEffect, ViewerStateControllerUseEffect, ViewerStateUseEffect, - NgViewerUseEffect + NgViewerUseEffect, + PluginServiceuseEffect, + AtlasViewerHistoryUseEffect ]), StoreModule.forRoot({ pluginState, @@ -124,9 +127,7 @@ import {CaptureClickListenerDirective} from "src/util/directives/captureClickLis ConfirmDialogComponent, ], providers : [ - AtlasViewerDataService, WidgetServices, - AtlasViewerURLService, AtlasViewerAPIServices, AtlasWorkerService, AuthService, diff --git a/src/res/ext/bigbrainNehubaConfig.json b/src/res/ext/bigbrainNehubaConfig.json index 185ac5077ab33ef3004edf4e326602aefa446020..32046653e7a602f2f3a0d4c94e21b57554dfaecc 100644 --- a/src/res/ext/bigbrainNehubaConfig.json +++ b/src/res/ext/bigbrainNehubaConfig.json @@ -1 +1,334 @@ -{"configName":"","globals":{"hideNullImageValues":true,"useNehubaLayout":{"keepDefaultLayouts":false},"useNehubaMeshLayer":true,"rightClickWithCtrlGlobal":false,"zoomWithoutCtrlGlobal":false,"useCustomSegmentColors":true},"zoomWithoutCtrl":true,"hideNeuroglancerUI":true,"rightClickWithCtrl":true,"rotateAtViewCentre":true,"enableMeshLoadingControl":true,"zoomAtViewCentre":true,"restrictUserNavigation":true,"disableSegmentSelection":false,"dataset":{"imageBackground":[1,1,1,1],"initialNgState":{"showDefaultAnnotations":false,"layers":{" grey value: ":{"type":"image","source":"precomputed://https://neuroglancer.humanbrainproject.org/precomputed/BigBrainRelease.2015/8bit","transform":[[1,0,0,-70677184],[0,1,0,-70010000],[0,0,1,-58788284],[0,0,0,1]]}," tissue type: ":{"type":"segmentation","source":"precomputed://https://neuroglancer.humanbrainproject.org/precomputed/BigBrainRelease.2015/classif","segments":["0"],"selectedAlpha":0,"notSelectedAlpha":0,"transform":[[1,0,0,-70666600],[0,1,0,-72910000],[0,0,1,-58777700],[0,0,0,1]]},"v1":{"type":"segmentation","source":"precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_01_v1","segments":["0"],"selectedAlpha":0.45,"notSelectedAlpha":0,"transform":[[1,0,0,-70677184],[0,1,0,-69390000],[0,0,1,-58788284],[0,0,0,1]]},"v2":{"type":"segmentation","source":"precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_01_v2","segments":["0"],"selectedAlpha":0.45,"notSelectedAlpha":0,"transform":[[1,0,0,-70677184],[0,1,0,-69870000],[0,0,1,-58788284],[0,0,0,1]]},"interpolated":{"type":"segmentation","source":"precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_22_interpolated_areas","segments":["0"],"selectedAlpha":0.45,"notSelectedAlpha":0,"transform":[[1,0,0,-70677184],[0,1,0,-51990000],[0,0,1,-58788284],[0,0,0,1]]},"cortical layers":{"type":"segmentation","source":"precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_27_cortical_layers","selectedAlpha":0.5,"notSelectedAlpha":0,"transform":[[1,0,0,-70677184],[0,1,0,-70010000],[0,0,1,-58788284],[0,0,0,1]]}},"navigation":{"pose":{"position":{"voxelSize":[21166.666015625,20000,21166.666015625],"voxelCoordinates":[-21.8844051361084,16.288618087768555,28.418994903564453]}},"zoomFactor":350000},"perspectiveOrientation":[0.3140767216682434,-0.7418519854545593,0.4988985061645508,-0.3195493221282959],"perspectiveZoom":1922235.5293810747}},"layout":{"views":"hbp-neuro","planarSlicesBackground":[1,1,1,1],"useNehubaPerspective":{"enableShiftDrag":false,"doNotRestrictUserNavigation":false,"perspectiveSlicesBackground":[1,1,1,1],"removePerspectiveSlicesBackground":{"color":[1,1,1,1],"mode":"=="},"perspectiveBackground":[1,1,1,1],"fixedZoomPerspectiveSlices":{"sliceViewportWidth":300,"sliceViewportHeight":300,"sliceZoom":563818.3562426177,"sliceViewportSizeMultiplier":2},"mesh":{"backFaceColor":[1,1,1,1],"removeBasedOnNavigation":true,"flipRemovedOctant":true},"centerToOrigin":true,"drawSubstrates":{"color":[0,0,0.5,0.15]},"drawZoomLevels":{"cutOff":200000,"color":[0.5,0,0,0.15]},"hideImages":false,"waitForMesh":true,"restrictZoomLevel":{"minZoom":1200000,"maxZoom":3500000}}}} \ No newline at end of file +{ + "configName": "", + "globals": { + "hideNullImageValues": true, + "useNehubaLayout": { + "keepDefaultLayouts": false + }, + "useNehubaMeshLayer": true, + "rightClickWithCtrlGlobal": false, + "zoomWithoutCtrlGlobal": false, + "useCustomSegmentColors": true + }, + "zoomWithoutCtrl": true, + "hideNeuroglancerUI": true, + "rightClickWithCtrl": true, + "rotateAtViewCentre": true, + "enableMeshLoadingControl": true, + "zoomAtViewCentre": true, + "restrictUserNavigation": true, + "disableSegmentSelection": false, + "dataset": { + "imageBackground": [ + 1, + 1, + 1, + 1 + ], + "initialNgState": { + "showDefaultAnnotations": false, + "layers": { + " grey value: ": { + "type": "image", + "source": "precomputed://https://neuroglancer.humanbrainproject.org/precomputed/BigBrainRelease.2015/8bit", + "transform": [ + [ + 1, + 0, + 0, + -70677184 + ], + [ + 0, + 1, + 0, + -70010000 + ], + [ + 0, + 0, + 1, + -58788284 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + " tissue type: ": { + "type": "segmentation", + "source": "precomputed://https://neuroglancer.humanbrainproject.org/precomputed/BigBrainRelease.2015/classif", + "segments": [ + "0" + ], + "selectedAlpha": 0, + "notSelectedAlpha": 0, + "transform": [ + [ + 1, + 0, + 0, + -70666600 + ], + [ + 0, + 1, + 0, + -72910000 + ], + [ + 0, + 0, + 1, + -58777700 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "v1": { + "type": "segmentation", + "source": "precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_01_v1", + "segments": [ + "0" + ], + "selectedAlpha": 0.45, + "notSelectedAlpha": 0, + "transform": [ + [ + 1, + 0, + 0, + -70677184 + ], + [ + 0, + 1, + 0, + -69390000 + ], + [ + 0, + 0, + 1, + -58788284 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "v2": { + "type": "segmentation", + "source": "precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_01_v2", + "segments": [ + "0" + ], + "selectedAlpha": 0.45, + "notSelectedAlpha": 0, + "transform": [ + [ + 1, + 0, + 0, + -70677184 + ], + [ + 0, + 1, + 0, + -69870000 + ], + [ + 0, + 0, + 1, + -58788284 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "interpolated": { + "type": "segmentation", + "source": "precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_22_interpolated_areas", + "segments": [ + "0" + ], + "selectedAlpha": 0.45, + "notSelectedAlpha": 0, + "transform": [ + [ + 1, + 0, + 0, + -70677184 + ], + [ + 0, + 1, + 0, + -51990000 + ], + [ + 0, + 0, + 1, + -58788284 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "cortical layers": { + "type": "segmentation", + "source": "precomputed://https://neuroglancer-dev.humanbrainproject.org/precomputed/BigBrainRelease.2015/2019_05_27_cortical_layers", + "selectedAlpha": 0.5, + "notSelectedAlpha": 0, + "transform": [ + [ + 1, + 0, + 0, + -70677184 + ], + [ + 0, + 1, + 0, + -70010000 + ], + [ + 0, + 0, + 1, + -58788284 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "navigation": { + "pose": { + "position": { + "voxelSize": [ + 21166.666015625, + 20000, + 21166.666015625 + ], + "voxelCoordinates": [ + -21.8844051361084, + 16.288618087768555, + 28.418994903564453 + ] + } + }, + "zoomFactor": 350000 + }, + "perspectiveOrientation": [ + 0.3140767216682434, + -0.7418519854545593, + 0.4988985061645508, + -0.3195493221282959 + ], + "perspectiveZoom": 1922235.5293810747 + } + }, + "layout": { + "views": "hbp-neuro", + "planarSlicesBackground": [ + 1, + 1, + 1, + 1 + ], + "useNehubaPerspective": { + "enableShiftDrag": false, + "doNotRestrictUserNavigation": false, + "perspectiveSlicesBackground": [ + 1, + 1, + 1, + 1 + ], + "removePerspectiveSlicesBackground": { + "color": [ + 1, + 1, + 1, + 1 + ], + "mode": "==" + }, + "perspectiveBackground": [ + 1, + 1, + 1, + 1 + ], + "fixedZoomPerspectiveSlices": { + "sliceViewportWidth": 300, + "sliceViewportHeight": 300, + "sliceZoom": 563818.3562426177, + "sliceViewportSizeMultiplier": 2 + }, + "mesh": { + "backFaceColor": [ + 1, + 1, + 1, + 1 + ], + "removeBasedOnNavigation": true, + "flipRemovedOctant": true + }, + "centerToOrigin": true, + "drawSubstrates": { + "color": [ + 0, + 0, + 0.5, + 0.15 + ] + }, + "drawZoomLevels": { + "cutOff": 200000, + "color": [ + 0.5, + 0, + 0, + 0.15 + ] + }, + "hideImages": false, + "waitForMesh": true, + "restrictZoomLevel": { + "minZoom": 1200000, + "maxZoom": 3500000 + } + } + } +} \ No newline at end of file diff --git a/src/services/effect/effect.spec.ts b/src/services/effect/effect.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..622639671d92cb8fe9c98c4395f4994a2fa30961 --- /dev/null +++ b/src/services/effect/effect.spec.ts @@ -0,0 +1,37 @@ +import {} from 'jasmine' +import { getGetRegionFromLabelIndexId } from './effect' +const colinsJson = require('!json-loader!../../res/ext/colin.json') + +const hoc1 = { + "name": "Area hOc1 (V1, 17, CalcS) - left hemisphere", + "rgb": [ + 190, + 132, + 147 + ], + "labelIndex": 8, + "ngId": "jubrain colin v18 left", + "children": [], + "status": "publicP", + "position": [ + -8533787, + -84646549, + 1855106 + ] +} + +describe('effect.ts', () => { + describe('getGetRegionFromLabelIndexId', () => { + it('translateds hoc1 from labelIndex to region', () => { + + const getRegionFromlabelIndexId = getGetRegionFromLabelIndexId({ + parcellation: { + ...colinsJson.parcellations[0], + updated: true + } + }) + const fetchedRegion = getRegionFromlabelIndexId({ labelIndexId: 'jubrain colin v18 left#8' }) + expect(fetchedRegion).toEqual(hoc1) + }) + }) +}) \ No newline at end of file diff --git a/src/services/effect/effect.ts b/src/services/effect/effect.ts index 4354421dea75ce55508feed5b8d380903c9510c5..87cd072181691fe8e4a7a535964c097ea14c52e7 100644 --- a/src/services/effect/effect.ts +++ b/src/services/effect/effect.ts @@ -228,6 +228,12 @@ export class UseEffects implements OnDestroy{ ) } +export const getGetRegionFromLabelIndexId = ({ parcellation }) => { + const { ngId: defaultNgId, regions } = parcellation + // if (!updated) throw new Error(`parcellation not yet updated`) + return ({ labelIndexId }) => recursiveFindRegionWithLabelIndexId({ regions, labelIndexId, inheritedNgId: defaultNgId }) +} + export const compareRegions: (r1: any,r2: any) => boolean = (r1, r2) => { if (!r1) return !r2 if (!r2) return !r1 diff --git a/src/services/state/dataStore.store.ts b/src/services/state/dataStore.store.ts index b4a13defd1476c80838a409e12c8b3aade9cfc0d..5ebec4c8326ff95c886d722cdebf7c6d800fdf9e 100644 --- a/src/services/state/dataStore.store.ts +++ b/src/services/state/dataStore.store.ts @@ -10,37 +10,40 @@ export interface StateInterface{ fetchedSpatialData: DataEntry[] } -const defaultState = { +export const defaultState = { fetchedDataEntries: [], favDataEntries: [], fetchedSpatialData: [] } -export function stateStore(state:StateInterface = defaultState, action:Partial<ActionInterface>){ +export const getStateStore = ({ state: state = defaultState } = {}) => (prevState:StateInterface = state, action:Partial<ActionInterface>) => { + switch (action.type){ case FETCHED_DATAENTRIES: { return { - ...state, + ...prevState, fetchedDataEntries : action.fetchedDataEntries } } case FETCHED_SPATIAL_DATA :{ return { - ...state, + ...prevState, fetchedSpatialData : action.fetchedDataEntries } } case ACTION_TYPES.UPDATE_FAV_DATASETS: { const { favDataEntries = [] } = action return { - ...state, + ...prevState, favDataEntries } } - default: return state + default: return prevState } } +export const stateStore = getStateStore() + export interface ActionInterface extends Action{ favDataEntries: DataEntry[] fetchedDataEntries : DataEntry[] diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index 2af420f1cd115744fe62bfbb0c698bf6f04c04a5..507c6cdc5dd9bf27064e3cfa69be97b6988c6c54 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -12,6 +12,19 @@ export const V_ONE_THREE = 'V_ONE_THREE' export const H_ONE_THREE = 'H_ONE_THREE' export const SINGLE_PANEL = 'SINGLE_PANEL' +export function mixNgLayers(oldLayers:NgLayerInterface[], newLayers:NgLayerInterface|NgLayerInterface[]): NgLayerInterface[]{ + if (newLayers instanceof Array) { + return oldLayers.concat(newLayers) + } else { + return oldLayers.concat({ + ...newLayers, + ...( newLayers.mixability === 'nonmixable' && oldLayers.findIndex(l => l.mixability === 'nonmixable') >= 0 + ? {visible: false} + : {}) + }) + } +} + export interface StateInterface{ layers : NgLayerInterface[] forceShowSegment : boolean | null @@ -31,7 +44,7 @@ export interface ActionInterface extends Action{ payload: any } -const defaultState:StateInterface = { +export const defaultState:StateInterface = { layers:[], forceShowSegment:null, nehubaReady: false, @@ -42,7 +55,7 @@ const defaultState:StateInterface = { showZoomlevel: null } -export function stateStore(prevState:StateInterface = defaultState, action:ActionInterface):StateInterface{ +export const getStateStore = ({ state = defaultState } = {}) => (prevState:StateInterface = state, action:ActionInterface):StateInterface => { switch(action.type){ case ACTION_TYPES.SET_PANEL_ORDER: { const { payload } = action @@ -75,14 +88,16 @@ export function stateStore(prevState:StateInterface = defaultState, action:Actio /* this configuration allows the addition of multiple non mixables */ // layers : prevState.layers.map(l => mapLayer(l, action.layer)).concat(action.layer) - layers : action.layer.constructor === Array - ? prevState.layers.concat(action.layer) - : prevState.layers.concat({ - ...action.layer, - ...( action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 - ? {visible: false} - : {}) - }) + layers : mixNgLayers(prevState.layers, action.layer) + + // action.layer.constructor === Array + // ? prevState.layers.concat(action.layer) + // : prevState.layers.concat({ + // ...action.layer, + // ...( action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 + // ? {visible: false} + // : {}) + // }) } case REMOVE_NG_LAYERS: const { layers } = action @@ -126,6 +141,8 @@ export function stateStore(prevState:StateInterface = defaultState, action:Actio } } +export const stateStore = getStateStore() + @Injectable({ providedIn: 'root' }) @@ -369,11 +386,11 @@ export const HIDE_NG_LAYER = 'HIDE_NG_LAYER' export const FORCE_SHOW_SEGMENT = `FORCE_SHOW_SEGMENT` export const NEHUBA_READY = `NEHUBA_READY` -interface NgLayerInterface{ +export interface NgLayerInterface{ name : string source : string mixability : string // base | mixable | nonmixable - visible : boolean + visible? : boolean shader? : string transform? : any } diff --git a/src/services/state/pluginState.store.ts b/src/services/state/pluginState.store.ts index f9e3cea64b79a6013cc296215ce1c968520ae626..3170e228468c565ffa6a93b3b45f9d5320e37b29 100644 --- a/src/services/state/pluginState.store.ts +++ b/src/services/state/pluginState.store.ts @@ -1,8 +1,11 @@ import { Action } from '@ngrx/store' +export const defaultState: StateInterface = { + initManifests: [] +} export interface StateInterface{ - initManifests : Map<string,string|null> + initManifests : [ string, string|null ][] } export interface ActionInterface extends Action{ @@ -13,18 +16,35 @@ export interface ActionInterface extends Action{ } export const ACTION_TYPES = { - SET_INIT_PLUGIN: `SET_INIT_PLUGIN` + SET_INIT_PLUGIN: `SET_INIT_PLUGIN`, + CLEAR_INIT_PLUGIN: 'CLEAR_INIT_PLUGIN' +} + +export const CONSTANTS = { + INIT_MANIFEST_SRC: 'INIT_MANIFEST_SRC' } -export function stateStore(prevState:StateInterface = {initManifests : new Map()}, action:ActionInterface):StateInterface{ +export const getStateStore = ({ state = defaultState } = {}) => (prevState:StateInterface = state, action:ActionInterface):StateInterface => { switch(action.type){ case ACTION_TYPES.SET_INIT_PLUGIN: const newMap = new Map(prevState.initManifests ) + + // reserved source label for init manifest + if (action.manifest.name !== CONSTANTS.INIT_MANIFEST_SRC) newMap.set(action.manifest.name, action.manifest.initManifestUrl) return { ...prevState, - initManifests: newMap.set(action.manifest.name, action.manifest.initManifestUrl) + initManifests: Array.from(newMap) + } + case ACTION_TYPES.CLEAR_INIT_PLUGIN: + const { initManifests } = prevState + const newManifests = initManifests.filter(([source]) => source !== CONSTANTS.INIT_MANIFEST_SRC) + return { + ...prevState, + initManifests: newManifests } default: return prevState } } + +export const stateStore = getStateStore() diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index ecf896187c16ea6c3b21239d5f3a6240848d813f..e0a0b4202ddfe80b527880997d300d25e93f086a 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -2,8 +2,9 @@ import { Action } from '@ngrx/store' import { TemplateRef } from '@angular/core'; import { LOCAL_STORAGE_CONST, COOKIE_VERSION, KG_TOS_VERSION } from 'src/util/constants' +import { GENERAL_ACTION_TYPES } from '../stateStore.service' -const defaultState: StateInterface = { +export const defaultState: StateInterface = { mouseOverSegments: [], mouseOverSegment: null, @@ -24,29 +25,29 @@ const defaultState: StateInterface = { agreedKgTos: localStorage.getItem(LOCAL_STORAGE_CONST.AGREE_KG_TOS) === KG_TOS_VERSION } -export function stateStore(state:StateInterface = defaultState,action:ActionInterface){ +export const getStateStore = ({ state = defaultState } = {}) => (prevState:StateInterface = state,action:ActionInterface) => { switch(action.type){ case MOUSE_OVER_SEGMENTS: const { segments } = action return { - ...state, + ...prevState, mouseOverSegments: segments } case MOUSE_OVER_SEGMENT: return { - ...state, + ...prevState, mouseOverSegment : action.segment } case MOUSEOVER_USER_LANDMARK: const { payload = {} } = action const { userLandmark: mouseOverUserLandmark = null } = payload return { - ...state, + ...prevState, mouseOverUserLandmark } case MOUSE_OVER_LANDMARK: return { - ...state, + ...prevState, mouseOverLandmark : action.landmark } case SNACKBAR_MESSAGE: @@ -55,7 +56,7 @@ export function stateStore(state:StateInterface = defaultState,action:ActionInte * Need to use symbol here, or repeated snackbarMessage will not trigger new event */ return { - ...state, + ...prevState, snackbarMessage: Symbol(snackbarMessage) } /** @@ -64,17 +65,17 @@ export function stateStore(state:StateInterface = defaultState,action:ActionInte */ case TOGGLE_SIDE_PANEL: return { - ...state, - sidePanelOpen: !state.sidePanelOpen + ...prevState, + sidePanelOpen: !prevState.sidePanelOpen } case OPEN_SIDE_PANEL: return { - ...state, + ...prevState, sidePanelOpen: true } case CLOSE_SIDE_PANEL: return { - ...state, + ...prevState, sidePanelOpen: false } case AGREE_COOKIE: @@ -83,7 +84,7 @@ export function stateStore(state:StateInterface = defaultState,action:ActionInte */ localStorage.setItem(LOCAL_STORAGE_CONST.AGREE_COOKIE, COOKIE_VERSION) return { - ...state, + ...prevState, agreedCookies: true } case AGREE_KG_TOS: @@ -92,20 +93,22 @@ export function stateStore(state:StateInterface = defaultState,action:ActionInte */ localStorage.setItem(LOCAL_STORAGE_CONST.AGREE_KG_TOS, KG_TOS_VERSION) return { - ...state, + ...prevState, agreedKgTos: true } case SHOW_BOTTOM_SHEET: const { bottomSheetTemplate } = action return { - ...state, + ...prevState, bottomSheetTemplate } default: - return state + return prevState } } +export const stateStore = getStateStore() + export interface StateInterface{ mouseOverSegments: { layer: { diff --git a/src/services/state/userConfigState.store.ts b/src/services/state/userConfigState.store.ts index bd5f817f9ab46ced325b63c33804ddb0ddb68e89..d9304886915002991cca6540b53a2612d795a1db 100644 --- a/src/services/state/userConfigState.store.ts +++ b/src/services/state/userConfigState.store.ts @@ -42,7 +42,7 @@ interface UserConfigAction extends Action{ payload?: any } -const defaultUserConfigState: StateInterface = { +export const defaultState: StateInterface = { savedRegionsSelection: [] } @@ -55,8 +55,7 @@ export const ACTION_TYPES = { LOAD_REGIONS_SELECTION: 'LOAD_REGIONS_SELECTION' } - -export function stateStore(prevState: StateInterface = defaultUserConfigState, action: UserConfigAction) { +export const getStateStore = ({ state = defaultState } = {}) => (prevState: StateInterface = state, action: UserConfigAction) => { switch(action.type) { case ACTION_TYPES.UPDATE_REGIONS_SELECTIONS: const { config = {} } = action @@ -72,6 +71,9 @@ export function stateStore(prevState: StateInterface = defaultUserConfigState, a } } +export const stateStore = getStateStore() + + @Injectable({ providedIn: 'root' }) diff --git a/src/services/state/viewerConfig.store.ts b/src/services/state/viewerConfig.store.ts index 598f7d37298b6fb8345f5f7d444a1d2ae1cd41d5..7a3899744f7f69bd355f2044eda5adf8cf7ca85f 100644 --- a/src/services/state/viewerConfig.store.ts +++ b/src/services/state/viewerConfig.store.ts @@ -53,15 +53,15 @@ const getIsMobile = () => { /* https://stackoverflow.com/a/25394023/6059235 */ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(ua) } -const useMobileUIStroageValue = window.localStorage.getItem(LOCAL_STORAGE_CONST.MOBILE_UI) +const useMobileUIStroageValue = window && window.localStorage && window.localStorage.getItem(LOCAL_STORAGE_CONST.MOBILE_UI) -const onLoadViewerconfig: StateInterface = { +export const defaultState: StateInterface = { animation, gpuLimit, useMobileUI: (useMobileUIStroageValue && useMobileUIStroageValue === 'true') || getIsMobile() } -export function stateStore(prevState:StateInterface = onLoadViewerconfig, action:ViewerConfigurationAction) { +export const getStateStore = ({ state = defaultState } = {}) => (prevState:StateInterface = state, action:ViewerConfigurationAction) => { switch (action.type) { case ACTION_TYPES.SET_MOBILE_UI: const { payload } = action @@ -89,3 +89,5 @@ export function stateStore(prevState:StateInterface = onLoadViewerconfig, action default: return prevState } } + +export const stateStore = getStateStore() diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index 2b08a8b0c17a8dad9b6b6dd08287843edebd289b..b9be474d9b0f08174ff1b9938eeed8efce67fb32 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -7,6 +7,7 @@ import { withLatestFrom, map, shareReplay, startWith, filter, distinctUntilChang import { Observable } from 'rxjs'; import { MOUSEOVER_USER_LANDMARK } from './uiState.store'; import { generateLabelIndexId, IavRootStoreInterface } from '../stateStore.service'; +import { GENERAL_ACTION_TYPES } from '../stateStore.service' export interface StateInterface{ fetchedTemplates : any[] @@ -44,33 +45,37 @@ export interface ActionInterface extends Action{ payload: any } -export function stateStore( - state:Partial<StateInterface> = { - landmarksSelected : [], - fetchedTemplates : [], - loadedNgLayers: [], - regionsSelected: [], - userLandmarks: [] - }, - action:ActionInterface -){ +export const defaultState:StateInterface = { + + landmarksSelected : [], + fetchedTemplates : [], + loadedNgLayers: [], + regionsSelected: [], + userLandmarks: [], + dedicatedView: null, + navigation: null, + parcellationSelected: null, + templateSelected: null +} + +export const getStateStore = ({ state = defaultState } = {}) => (prevState:Partial<StateInterface> = state, action:ActionInterface) => { switch(action.type){ /** * TODO may be obsolete. test when nifti become available */ case LOAD_DEDICATED_LAYER: - const dedicatedView = state.dedicatedView - ? state.dedicatedView.concat(action.dedicatedView) + const dedicatedView = prevState.dedicatedView + ? prevState.dedicatedView.concat(action.dedicatedView) : [action.dedicatedView] return { - ...state, + ...prevState, dedicatedView } case UNLOAD_DEDICATED_LAYER: return { - ...state, - dedicatedView : state.dedicatedView - ? state.dedicatedView.filter(dv => dv !== action.dedicatedView) + ...prevState, + dedicatedView : prevState.dedicatedView + ? prevState.dedicatedView.filter(dv => dv !== action.dedicatedView) : [] } case NEWVIEWER: @@ -78,7 +83,7 @@ export function stateStore( // const parcellation = propagateNgId( selectParcellation ): parcellation const { regions, ...parcellationWORegions } = parcellation return { - ...state, + ...prevState, templateSelected : action.selectTemplate, parcellationSelected : { ...parcellationWORegions, @@ -92,13 +97,13 @@ export function stateStore( } case FETCHED_TEMPLATE : { return { - ...state, - fetchedTemplates: state.fetchedTemplates.concat(action.fetchedTemplate) + ...prevState, + fetchedTemplates: prevState.fetchedTemplates.concat(action.fetchedTemplate) } } case CHANGE_NAVIGATION : { return { - ...state, + ...prevState, navigation : action.navigation } } @@ -106,7 +111,7 @@ export function stateStore( const { selectParcellation:parcellation } = action const { regions, ...parcellationWORegions } = parcellation return { - ...state, + ...prevState, parcellationSelected: parcellationWORegions, // taken care of by effect.ts // regionsSelected: [] @@ -115,7 +120,7 @@ export function stateStore( case UPDATE_PARCELLATION: { const { updatedParcellation } = action return { - ...state, + ...prevState, parcellationSelected: { ...updatedParcellation, updated: true @@ -125,24 +130,24 @@ export function stateStore( case SELECT_REGIONS: const { selectRegions } = action return { - ...state, + ...prevState, regionsSelected: selectRegions } case DESELECT_LANDMARKS : { return { - ...state, - landmarksSelected : state.landmarksSelected.filter(lm => action.deselectLandmarks.findIndex(dLm => dLm.name === lm.name) < 0) + ...prevState, + landmarksSelected : prevState.landmarksSelected.filter(lm => action.deselectLandmarks.findIndex(dLm => dLm.name === lm.name) < 0) } } case SELECT_LANDMARKS : { return { - ...state, + ...prevState, landmarksSelected : action.landmarks } } case USER_LANDMARKS : { return { - ...state, + ...prevState, userLandmarks: action.landmarks } } @@ -153,12 +158,12 @@ export function stateStore( case NEHUBA_LAYER_CHANGED: { if (!window['viewer']) { return { - ...state, + ...prevState, loadedNgLayers: [] } } else { return { - ...state, + ...prevState, loadedNgLayers: (window['viewer'].layerManager.managedLayers as any[]).map(obj => ({ name : obj.name, type : obj.initialSpecification.type, @@ -168,11 +173,16 @@ export function stateStore( } } } + case GENERAL_ACTION_TYPES.APPLY_STATE: + const { viewerState } = (action as any).state + return viewerState default : - return state + return prevState } } +export const stateStore = getStateStore() + export const LOAD_DEDICATED_LAYER = 'LOAD_DEDICATED_LAYER' export const UNLOAD_DEDICATED_LAYER = 'UNLOAD_DEDICATED_LAYER' diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index c43fe0d6710f259a6a0c50eb37f6bccff25b95b0..9ce8ea1bec76f1f0972888942b56f12c11e74196 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -2,37 +2,52 @@ import { filter } from 'rxjs/operators'; import { StateInterface as PluginStateInterface, - stateStore as pluginState + stateStore as pluginState, + defaultState as pluginDefaultState, + getStateStore as pluginGetStateStore } from './state/pluginState.store' import { StateInterface as ViewerConfigStateInterface, - stateStore as viewerConfigState + stateStore as viewerConfigState, + defaultState as viewerConfigDefaultState, + getStateStore as getViewerConfigStateStore } from './state/viewerConfig.store' import { StateInterface as NgViewerStateInterface, ActionInterface as NgViewerActionInterface, - stateStore as ngViewerState + stateStore as ngViewerState, + defaultState as ngViewerDefaultState, + getStateStore as getNgViewerStateStore } from './state/ngViewerState.store' import { StateInterface as ViewerStateInterface, ActionInterface as ViewerActionInterface, - stateStore as viewerState + stateStore as viewerState, + defaultState as viewerDefaultState, + getStateStore as getViewerStateStore } from './state/viewerState.store' import { StateInterface as DataStateInterface, ActionInterface as DatasetAction, - stateStore as dataStore + stateStore as dataStore, + defaultState as dataStoreDefaultState, + getStateStore as getDatasetStateStore } from './state/dataStore.store' import { StateInterface as UIStateInterface, ActionInterface as UIActionInterface, - stateStore as uiState + stateStore as uiState, + defaultState as uiDefaultState, + getStateStore as getUiStateStore } from './state/uiState.store' import{ stateStore as userConfigState, ACTION_TYPES as USER_CONFIG_ACTION_TYPES, - StateInterface as UserConfigStateInterface + StateInterface as UserConfigStateInterface, + defaultState as userConfigDefaultState, + getStateStore as getuserConfigStateStore } from './state/userConfigState.store' +import { cvtSearchParamToState } from 'src/atlasViewer/atlasViewer.urlUtil'; export { pluginState } export { viewerConfigState } @@ -49,7 +64,8 @@ export { CLOSE_SIDE_PANEL, MOUSE_OVER_LANDMARK, MOUSE_OVER_SEGMENT, OPEN_SIDE_PA export { UserConfigStateUseEffect } from './state/userConfigState.store' export const GENERAL_ACTION_TYPES = { - ERROR: 'ERROR' + ERROR: 'ERROR', + APPLY_STATE: 'APPLY_STATE' } // TODO deprecate @@ -75,6 +91,12 @@ const inheritNgId = (region:any) => { } } +export function getNgIdLabelIndexFromRegion({ region }){ + const { ngId, labelIndex } = region + if (ngId && labelIndex) return { ngId, labelIndex } + throw new Error(`ngId: ${ngId} or labelIndex: ${labelIndex} not defined`) +} + export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}):Map<string,Map<number, any>>{ const map:Map<string,Map<number, any>> = new Map() const { ngId = 'root'} = parcellation @@ -199,4 +221,20 @@ export interface IavRootStoreInterface{ dataStore: DataStateInterface uiState: UIStateInterface userConfigState: UserConfigStateInterface -} \ No newline at end of file +} + +export const defaultRootState: IavRootStoreInterface = { + pluginState: pluginDefaultState, + dataStore: dataStoreDefaultState, + ngViewerState: ngViewerDefaultState, + uiState: uiDefaultState, + userConfigState: userConfigDefaultState, + viewerConfigState: viewerConfigDefaultState, + viewerState: viewerDefaultState +} + + +// export const getInitialState = (): IavRootStoreInterface => { +// const search = new URLSearchParams( window.location.search ) +// cvtSearchParamToState(search, defaultRootState) +// } \ No newline at end of file diff --git a/src/ui/databrowserModule/kgSingleDatasetService.service.ts b/src/ui/databrowserModule/kgSingleDatasetService.service.ts index 5a758272d1b9e16017c8efd51e63a4949f6da022..a636fc77ac88102fa2d2823c38835d4c8cf55fa4 100644 --- a/src/ui/databrowserModule/kgSingleDatasetService.service.ts +++ b/src/ui/databrowserModule/kgSingleDatasetService.service.ts @@ -1,5 +1,5 @@ import { Injectable, TemplateRef, OnDestroy } from "@angular/core"; -import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service" +import { AtlasViewerConstantsServices, GLSL_COLORMAP_JET } from "src/atlasViewer/atlasViewer.constantService.service" import { Store, select } from "@ngrx/store"; import { SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; import { ViewerPreviewFile, DataEntry } from "src/services/state/dataStore.store"; @@ -130,7 +130,7 @@ export class KgSingleDatasetService implements OnDestroy{ name : url, source : `nifti://${url}`, mixability : 'nonmixable', - shader : this.constantService.getActiveColorMapFragmentMain() + shader : GLSL_COLORMAP_JET } this.store$.dispatch({ type: ADD_NG_LAYER, diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index fbbc509659c1cc4b6c9f8729283f1554e8393972..5b312c5794d474e5e3da488c500564a292b122ad 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -3,7 +3,7 @@ import { NehubaViewerUnit, computeDistance } from "./nehubaViewer/nehubaViewer.c import { Store, select } from "@ngrx/store"; import { ViewerStateInterface, safeFilter, CHANGE_NAVIGATION, isDefined, ADD_NG_LAYER, REMOVE_NG_LAYER, NgViewerStateInterface, MOUSE_OVER_LANDMARK, Landmark, PointLandmarkGeometry, PlaneLandmarkGeometry, OtherLandmarkGeometry, getNgIds, getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId, DataEntry } from "src/services/stateStore.service"; import { Observable, Subscription, fromEvent, combineLatest, merge, of } from "rxjs"; -import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, buffer, tap, switchMapTo, shareReplay, mapTo, takeUntil, throttleTime } from "rxjs/operators"; +import { filter,map, take, scan, debounceTime, distinctUntilChanged, switchMap, skip, buffer, tap, switchMapTo, shareReplay, mapTo, takeUntil, throttleTime, withLatestFrom, startWith } from "rxjs/operators"; import { AtlasViewerAPIServices, UserLandmark } from "../../atlasViewer/atlasViewer.apiService.service"; import { timedValues } from "../../util/generator"; import { AtlasViewerConstantsServices } from "../../atlasViewer/atlasViewer.constantService.service"; @@ -13,6 +13,7 @@ import { NEHUBA_READY, H_ONE_THREE, V_ONE_THREE, FOUR_PANEL, SINGLE_PANEL, NG_VI import { MOUSE_OVER_SEGMENTS } from "src/services/state/uiState.store"; import { getHorizontalOneThree, getVerticalOneThree, getFourPanel, getSinglePanel } from "./util"; import { SELECT_REGIONS_WITH_ID, NEHUBA_LAYER_CHANGED, VIEWERSTATE_ACTION_TYPES } from "src/services/state/viewerState.store"; +import { isSame } from "src/util/fn"; const getProxyUrl = (ngUrl) => `nifti://${BACKEND_URL}preview/file?fileUrl=${encodeURIComponent(ngUrl.replace(/^nifti:\/\//,''))}` const getProxyOther = ({source}) => /AUTH_227176556f3c4bb38df9feea4b91200c/.test(source) @@ -99,6 +100,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ public sliceViewLoading2$: Observable<boolean> public perspectiveViewLoading$: Observable<string|null> + private templateSelected$: Observable<any> private newViewer$ : Observable<any> private selectedParcellation$ : Observable<any> private selectedRegions$ : Observable<any[]> @@ -205,12 +207,14 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ this.nehubaViewerFactory = this.csf.resolveComponentFactory(NehubaViewerUnit) - this.newViewer$ = this.store.pipe( + this.templateSelected$ = this.store.pipe( select('viewerState'), - filter(state=>isDefined(state) && isDefined(state.templateSelected)), - filter(state=> - !isDefined(this.selectedTemplate) || - state.templateSelected.name !== this.selectedTemplate.name) + select('templateSelected'), + distinctUntilChanged(isSame) + ) + + this.newViewer$ = this.templateSelected$.pipe( + filter(v => !!v) ) this.selectedParcellation$ = this.store.pipe( @@ -592,33 +596,40 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ }) ) + this.subscriptions.push( + this.templateSelected$.subscribe(() => this.destroynehuba()) + ) + /* order of subscription will determine the order of execution */ this.subscriptions.push( this.newViewer$.pipe( - map(state => { - const deepCopiedState = JSON.parse(JSON.stringify(state)) - const navigation = deepCopiedState.templateSelected.nehubaConfig.dataset.initialNgState.navigation + map(templateSelected => { + const deepCopiedState = JSON.parse(JSON.stringify(templateSelected)) + const navigation = deepCopiedState.nehubaConfig.dataset.initialNgState.navigation if (!navigation) { return deepCopiedState } navigation.zoomFactor = calculateSliceZoomFactor(navigation.zoomFactor) - deepCopiedState.templateSelected.nehubaConfig.dataset.initialNgState.navigation = navigation + deepCopiedState.nehubaConfig.dataset.initialNgState.navigation = navigation return deepCopiedState - }) - ).subscribe((state)=>{ + }), + withLatestFrom(this.selectedParcellation$.pipe( + startWith(null) + )) + ).subscribe(([templateSelected, parcellationSelected])=>{ this.store.dispatch({ type: NEHUBA_READY, nehubaReady: false }) this.nehubaViewerSubscriptions.forEach(s=>s.unsubscribe()) - this.selectedTemplate = state.templateSelected - this.createNewNehuba(state.templateSelected) - const foundParcellation = state.templateSelected.parcellations.find(parcellation=> - state.parcellationSelected.name === parcellation.name) - this.handleParcellation(foundParcellation ? foundParcellation : state.templateSelected.parcellations[0]) + this.selectedTemplate = templateSelected + this.createNewNehuba(templateSelected) + const foundParcellation = parcellationSelected + && templateSelected.parcellations.find(parcellation=> parcellationSelected.name === parcellation.name) + this.handleParcellation(foundParcellation || templateSelected.parcellations[0]) - const nehubaConfig = state.templateSelected.nehubaConfig + const nehubaConfig = templateSelected.nehubaConfig const initialSpec = nehubaConfig.dataset.initialNgState const {layers} = initialSpec @@ -725,6 +736,8 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ combineLatest( this.navigationChanges$, this.selectedRegions$, + ).pipe( + filter(() => !!this.nehubaViewer) ).subscribe(([navigation,regions])=>{ this.nehubaViewer.initNav = { ...navigation, @@ -966,18 +979,22 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ oldNavigation : any = {} spatialSearchPagination : number = 0 - private createNewNehuba(template:any){ - + private destroynehuba(){ /** * TODO if plugin subscribes to viewerHandle, and then new template is selected, changes willl not be be sent * could be considered as a bug. */ this.apiService.interactiveViewer.viewerHandle = null + if( this.cr ) this.cr.destroy() + this.container.clear() + + this.viewerLoaded = false + this.nehubaViewer = null + } + + private createNewNehuba(template:any){ this.viewerLoaded = true - if( this.cr ) - this.cr.destroy() - this.container.clear() this.cr = this.container.createComponent(this.nehubaViewerFactory) this.nehubaViewer = this.cr.instance @@ -986,6 +1003,21 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ */ const { gpuLimit = null } = this.viewerConfig const { nehubaConfig } = template + const { navigation = {}, perspectiveOrientation = [0, 0, 0, 1], perspectiveZoom = 1e7 } = nehubaConfig.dataset.initialNgState || {} + const { zoomFactor = 3e5, pose = {} } = navigation || {} + const { voxelSize = [1e6, 1e6, 1e6], voxelCoordinates = [0, 0, 0] } = (pose && pose.position) || {} + + const initNavigation = { + orientation: [0, 0, 0, 1], + perspectiveOrientation, + perspectiveZoom, + position: [0, 1, 2].map(idx => voxelSize[idx] * voxelCoordinates[idx]), + zoom: zoomFactor + } + + this.handleEmittedNavigationChange(initNavigation) + + this.oldNavigation = initNavigation if (gpuLimit) { const initialNgState = nehubaConfig && nehubaConfig.dataset && nehubaConfig.dataset.initialNgState @@ -1210,15 +1242,14 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{ As the viewer updates the dynamically changed navigation, it will emit the navigation state. The emitted navigation state should be identical to this.oldnavigation */ - const navigationChangedActively : boolean = Object.keys(this.oldNavigation).length === 0 || !Object.keys(this.oldNavigation).every(key=>{ + const navigationChangedActively: boolean = Object.keys(this.oldNavigation).length === 0 || !Object.keys(this.oldNavigation).every(key=>{ return this.oldNavigation[key].constructor === Number || this.oldNavigation[key].constructor === Boolean ? this.oldNavigation[key] === navigation[key] : this.oldNavigation[key].every((_,idx)=>this.oldNavigation[key][idx] === navigation[key][idx]) }) /* if navigation is changed dynamically (ie not actively), the state would have been propagated to the store already. Hence return */ - if( !navigationChangedActively ) - return + if( !navigationChangedActively ) return /* navigation changed actively (by user interaction with the viewer) probagate the changes to the store */ diff --git a/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts b/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts index 9f7e287a841bae5eeb7822d2ab632b38008d114f..2b4fe6c2e0bd3573deb7cddaf942513ae265bfe6 100644 --- a/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts +++ b/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts @@ -1,9 +1,9 @@ import { Component, Pipe, PipeTransform, ElementRef, ViewChild, AfterViewInit } from "@angular/core"; import { Observable, fromEvent, Subscription, Subject } from "rxjs"; import { Store, select } from "@ngrx/store"; -import { switchMap, bufferTime, take, filter, withLatestFrom, map, tap } from 'rxjs/operators' -import { ViewerStateInterface, NEWVIEWER } from "../../../services/stateStore.service"; -import { AtlasViewerConstantsServices } from "../../../atlasViewer/atlasViewer.constantService.service"; +import { switchMap, bufferTime, take, filter, withLatestFrom, map } from 'rxjs/operators' +import { ViewerStateInterface, NEWVIEWER } from "src/services/stateStore.service"; +import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; @Component({ diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index 305da063db1bb578166affba4d3ef81ac7ba58ad..80a4cdb8b596d52f8daef7b20b8cdf06aefc5972 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Output, ViewChild, ElementRef, TemplateRef, Input, ChangeDetectionStrategy } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { Observable, combineLatest } from "rxjs"; -import { map, distinctUntilChanged, startWith, debounceTime, shareReplay, take, tap } from "rxjs/operators"; +import { map, distinctUntilChanged, startWith, debounceTime, shareReplay, take, tap, filter } from "rxjs/operators"; import { getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId, IavRootStoreInterface } from "src/services/stateStore.service"; import { FormControl } from "@angular/forms"; import { MatAutocompleteSelectedEvent, MatDialog } from "@angular/material"; @@ -52,6 +52,7 @@ export class RegionTextSearchAutocomplete{ this.regionsWithLabelIndex$ = viewerState$.pipe( select('parcellationSelected'), distinctUntilChanged(), + filter(p => !!p), map(parcellationSelected => { const returnArray = [] const ngIdMap = getMultiNgIdsRegionsLabelIndexMap(parcellationSelected) diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index 405d22b47bd47f7be70c84fbf0597d2760744207..5e7a3fc7fd6698ef1e517f3c74d7525570c749c0 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -4,9 +4,10 @@ import { Actions, ofType, Effect } from "@ngrx/effects"; import { Store, select, Action } from "@ngrx/store"; import { shareReplay, distinctUntilChanged, map, withLatestFrom, filter } from "rxjs/operators"; import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "./viewerState.base"; -import { CHANGE_NAVIGATION, SELECT_REGIONS, NEWVIEWER, GENERAL_ACTION_TYPES, SELECT_PARCELLATION, isDefined, IavRootStoreInterface } from "src/services/stateStore.service"; +import { CHANGE_NAVIGATION, SELECT_REGIONS, NEWVIEWER, GENERAL_ACTION_TYPES, SELECT_PARCELLATION, isDefined, IavRootStoreInterface, FETCHED_TEMPLATE } from "src/services/stateStore.service"; import { regionFlattener } from "src/util/regionFlattener"; import { UIService } from "src/services/uiService.service"; +import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; @Injectable({ providedIn: 'root' @@ -19,6 +20,16 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ private selectedRegions$: Observable<any[]> + @Effect() + init$ = this.constantSerivce.initFetchTemplate$.pipe( + map(fetchedTemplate => { + return { + type: FETCHED_TEMPLATE, + fetchedTemplate + } + }) + ) + @Effect() selectTemplateWithName$: Observable<any> @@ -46,7 +57,8 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ constructor( private actions$: Actions, private store$: Store<IavRootStoreInterface>, - private uiService: UIService + private uiService: UIService, + private constantSerivce: AtlasViewerConstantsServices ){ const viewerState$ = this.store$.pipe( select('viewerState'), diff --git a/src/util/fn.spec.ts b/src/util/fn.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1161a9fbaf2ec88409dd2b506b0977fbfca76410 --- /dev/null +++ b/src/util/fn.spec.ts @@ -0,0 +1,31 @@ +import {} from 'jasmine' +import { isSame } from './fn' + +describe(`util/fn.ts`, () => { + describe(`#isSame`, () => { + it('should return true with null, null', () => { + expect(isSame(null, null)).toBe(true) + }) + + it('should return true with string', () => { + expect(isSame('test', 'test')).toBe(true) + }) + + it(`should return true with numbers`, () => { + expect(isSame(12, 12)).toBe(true) + }) + + it('should return true with obj with name attribute', () => { + + const obj = { + name: 'hello' + } + const obj2 = { + name: 'hello', + world: 'world' + } + expect(isSame(obj, obj2)).toBe(true) + expect(obj).not.toEqual(obj2) + }) + }) +}) \ No newline at end of file diff --git a/src/util/fn.ts b/src/util/fn.ts new file mode 100644 index 0000000000000000000000000000000000000000..efc9974787f07a51989bf59fc7c86d686562f37a --- /dev/null +++ b/src/util/fn.ts @@ -0,0 +1,4 @@ +export function isSame (o, n) { + if (!o) return !n + return o === n || (o && n && o.name === n.name) +} \ No newline at end of file