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 { PluginInitManifestInterface } 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 { ToastService } from "src/services/toastService.service"; import { SELECT_REGIONS_WITH_ID } from "src/services/state/viewerState.store"; declare var window @Injectable({ providedIn : 'root' }) export class AtlasViewerURLService{ private changeQueryObservable$ : Observable<any> private additionalNgLayers$ : Observable<any> private pluginState$ : Observable<PluginInitManifestInterface> constructor( private store : Store<ViewerStateInterface>, private pluginService : PluginServices, private constantService:AtlasViewerConstantsServices, private toastService: ToastService ){ 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( map(state => state.templateSelected) ), 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'), filter(state=>isDefined(state)&&isDefined(state.fetchedTemplates)), map(state=>state.fetchedTemplates), 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) /* first, check if any template and parcellations are to be loaded */ const searchedTemplatename = searchparams.get('templateSelected') const searchedParcellationName = searchparams.get('parcellationSelected') 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.toastService.showToast( this.constantService.incorrectTemplateNameSearchParam(searchedTemplatename), { timeout: 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.toastService.showToast( this.constantService.incorrectParcellationNameSearchParam(searchedParcellationName), { timeout: 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 */ 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(decodeToNumber) 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 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).then(res => res.json()).then(json => this.pluginService.launchNewWidget(json)).catch(console.error)) } }) /* pushing state to url */ combineLatest( this.changeQueryObservable$.pipe( map(state=>{ 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) ){ _[key] = [ state[key].orientation.join('_'), state[key].perspectiveOrientation.join('_'), state[key].perspectiveZoom, state[key].position.join('_'), state[key].zoom ].join('__') } 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(encodeNumber).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$, 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.map(layer => layer.name).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()) }) } }