diff --git a/deploy/bkwdCompat/urlState.js b/deploy/bkwdCompat/urlState.js index 7f527c684c872df9f8237e201c414eff89030abb..cacb88ae35262848e9ee654efac0a3be837fd478 100644 --- a/deploy/bkwdCompat/urlState.js +++ b/deploy/bkwdCompat/urlState.js @@ -1,6 +1,6 @@ // this module is suppose to rewrite state stored in query param // and convert it to path based url - +const separator = '.' const waxolmObj = { aId: 'minds/core/parcellationatlas/v1.0.0/522b368e-49a3-49fa-88d3-0870a307974a', id: 'minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', @@ -133,14 +133,66 @@ module.exports = query => { // to avoid potentially issues (e.g. url containing __, which is very possible) const plugins = pluginStates && pluginStates.split('__') + const searchParam = new URLSearchParams() + + + // common search param & path + let nav, dsp, r + if (cNavigation) nav = `/@:${encodeURI(cNavigation)}` + if (previewingDatasetFiles) { + try { + const parsedDsp = JSON.parse(previewingDatasetFiles) + if (Array.isArray(parsedDsp)) { + if (parsedDsp.length === 1) { + const { datasetId, filename } = parsedDsp[0] + dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}` + } else { + searchParam.set(`previewingDatasetFiles`, previewingDatasetFiles) + } + } + } catch (_e) { + // parsing preview dataset error + // ignore preview dataset query param + } + } + if (plugins && plugins.length > 0) { + searchParam.set(`pl`, JSON.stringify(plugins)) + } + if (niftiLayers) searchParam.set(`niftiLayers`, niftiLayers) + if (cRegionsSelected) { + try { + (() => { + const parsedRS = JSON.parse(cRegionsSelected) + if (Object.keys(parsedRS).length > 1) { + searchParam.set('cRegionsSelected', cRegionsSelected) + return + } + for (const ngId in parsedRS) { + const encodedIdArr = parsedRS[ngId] + const encodedRArr = encodedIdArr.split(separator) + if (encodedRArr.length > 0) { + if (encodedRArr.length > 1) { + searchParam.set('cRegionsSelected', cRegionsSelected) + return + } + if (!!encodedRArr[0]) { + r = `/r:${encodeURI(ngId)}::${encodeURI(encodedRArr[0])}` + } + } + } + })() + } catch (e) { + // parsing cregions selected error + // ignore region selected and move on + } + } + let redirectUrl = '/#' if (standaloneVolumes) { - redirectUrl += `/sv:${encodeURI(standaloneVolumes)}` - if (cNavigation) redirectUrl += `/@:${encodeURI(cNavigation)}` - if (previewingDatasetFiles) redirectUrl += `/dsp:${encodeURI(previewingDatasetFiles)}` - if (plugins && plugins.length > 0) redirectUrl += `/pl:${encodeURI(JSON.stringify(plugins))}` - - if (niftiLayers) redirectUrl += `?niftiLayers=${encodeURI(niftiLayers)}` + redirectUrl += `/sv:${encodeURIComponent(standaloneVolumes)}` + if (nav) redirectUrl += nav + if (dsp) redirectUrl += dsp + if (Array.from(searchParam.keys()).length > 0) redirectUrl += `?${searchParam.toString()}` return redirectUrl } @@ -149,12 +201,12 @@ module.exports = query => { redirectUrl += `/a:${encodeId(a)}/t:${encodeId(t)}` const { id: p } = parc[parcellationSelected] || {} if (p) redirectUrl += `/p:${encodeId(p)}` - if (cRegionsSelected) redirectUrl += `/r:${encodeURI(cRegionsSelected)}` - if (cNavigation) redirectUrl += `/@:${encodeURI(cNavigation)}` - if (previewingDatasetFiles) redirectUrl += `/dsp:${encodeURI(previewingDatasetFiles)}` - if (plugins && plugins.length > 0) redirectUrl += `/pl:${encodeURI(JSON.stringify(plugins))}` + if (r) redirectUrl += r + if (nav) redirectUrl += nav + if (dsp) redirectUrl += dsp + + if (Array.from(searchParam.keys()).length > 0) redirectUrl += `?${searchParam.toString()}` - if (niftiLayers) redirectUrl += `?niftiLayers=${encodeURI(niftiLayers)}` return redirectUrl } return null diff --git a/src/plugin/atlasViewer.pluginService.service.ts b/src/plugin/atlasViewer.pluginService.service.ts index 52b100cb55d7c60e0a7b55eeb706d66329eab86f..1e35d24ec50d34909a8766d79ad13a0d218d1b17 100644 --- a/src/plugin/atlasViewer.pluginService.service.ts +++ b/src/plugin/atlasViewer.pluginService.service.ts @@ -60,13 +60,6 @@ export class PluginServices { @Inject(REMOVE_SCRIPT_TOKEN) private removeSrc: (src: HTMLScriptElement) => void, ) { - // TODO implement - this.store.pipe( - select('pluginState'), - select('initManifests'), - filter(v => !!v), - ) - this.pluginUnitFactory = this.cfr.resolveComponentFactory( PluginUnit ) /** diff --git a/src/routerModule/util.ts b/src/routerModule/util.ts index 1a4c91beca9e39f1543edeafb2c797deb8fa54f2..b6ef6673de52fedfcdceb9fcdabf77841d6cf9fe 100644 --- a/src/routerModule/util.ts +++ b/src/routerModule/util.ts @@ -20,55 +20,56 @@ const encodeId = (id: string) => id && id.replace(/\//g, ':') const decodeId = (codedId: string) => codedId && codedId.replace(/:/g, '/') const endcodePath = (key: string, val: string) => `${key}:${encodeURI(val)}` const decodePath = (path: string) => { - const re = /^(.*?):(.+)$/.exec(path) + const re = /^(.*?):(.*?)$/.exec(path) if (!re) return null return { key: re[1], - val: decodeURI(re[2]) + val: re[2].split('::').map(v => decodeURI(v)) } } -type TUrlStandaloneVolume = { - sv: string // standalone volume +type TUrlStandaloneVolume<T> = { + sv: T // standalone volume } -type TUrlAtlas = { - a: string // atlas - t: string // template - p: string // parcellation - r?: string // region selected +type TUrlAtlas<T> = { + a: T // atlas + t: T // template + p: T // parcellation + r?: T // region selected } -type TUrlPreviewDs = { - dsp: string // dataset preview +type TUrlPreviewDs<T> = { + dsp: T // dataset preview } -type TUrlPlugin = { - pl: string // pluginState +type TUrlPlugin<T> = { + pl: T // pluginState } -type TUrlNav = { - ['@']: string // string +type TUrlNav<T> = { + ['@']: T // navstring } -type TConditional = Partial< - TUrlPreviewDs & - TUrlPlugin & - TUrlNav +type TConditional<T> = Partial< + TUrlPreviewDs<T> & + TUrlPlugin<T> & + TUrlNav<T> > -type TUrlPathObj = (TUrlAtlas | TUrlStandaloneVolume) & TConditional +type TUrlPathObj<T, V> = (V extends TUrlStandaloneVolume<T> ? TUrlStandaloneVolume<T> : TUrlAtlas<T>) & TConditional<T> -function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj, state: any, warnCb: Function) { +function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj<string[], TUrlAtlas<string[]>>, fullPath: UrlTree, state: any, warnCb: Function) { /** * TODO if search param of either template or parcellation is incorrect, wrong things are searched */ + const templateSelected = (() => { const { fetchedTemplates } = state.viewerState - const searchedId = decodeId(obj['t']) + const searchedId = obj.t && decodeId(obj.t[0]) if (!searchedId) return null const templateToLoad = fetchedTemplates.find(template => (template['@id'] || template['fullId']) === searchedId) @@ -78,7 +79,7 @@ function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj, state: const parcellationSelected = (() => { if (!templateSelected) return null - const searchedId = decodeId(obj['p']) + const searchedId = obj.p && decodeId(obj.p[0]) const parcellationToLoad = templateSelected.parcellations.find(parcellation => (parcellation['@id'] || parcellation['fullId']) === searchedId) if (!parcellationToLoad) { @@ -100,45 +101,44 @@ function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj, state: * either or both parcellationToLoad and .regions maybe empty */ - const cRegionsSelectedParam = obj['r'] - if (cRegionsSelectedParam) { - try { - const json = JSON.parse(cRegionsSelectedParam) - - const selectRegionIds = [] - - for (const 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 - */ - return null - } - }).filter(v => !!v) - for (const labelIndex of labelIndicies) { - selectRegionIds.push( serialiseParcellationRegion({ ngId, labelIndex }) ) + try { + const json = obj.r + ? { [obj.r[0]] : obj.r[1] } + : JSON.parse(fullPath.queryParams['cRegionsSelected'] || '{}') + + const selectRegionIds = [] + + for (const 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 + */ + return null } + }).filter(v => !!v) + for (const labelIndex of labelIndicies) { + selectRegionIds.push( serialiseParcellationRegion({ ngId, labelIndex }) ) } - return selectRegionIds - .map(labelIndexId => { - const region = getRegionFromlabelIndexId({ labelIndexId }) - if (!region) { - // cb && cb({ type: ID_ERROR, message: `region with id ${labelIndexId} not found, and will be ignored.` }) - } - return region - }) - .filter(r => !!r) - - } catch (e) { - /** - * parsing cRegionSelected error - */ - // cb && cb({ type: DECODE_CIPHER_ERROR, message: `parsing cRegionSelected error ${e.toString()}` }) } + return selectRegionIds + .map(labelIndexId => { + const region = getRegionFromlabelIndexId({ labelIndexId }) + if (!region) { + // cb && cb({ type: ID_ERROR, message: `region with id ${labelIndexId} not found, and will be ignored.` }) + } + return region + }) + .filter(r => !!r) + + } catch (e) { + /** + * parsing cRegionSelected error + */ + // cb && cb({ type: DECODE_CIPHER_ERROR, message: `parsing cRegionSelected error ${e.toString()}` }) } return [] })() @@ -159,7 +159,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun const returnState = JSON.parse(JSON.stringify(state)) - const returnObj: Partial<TUrlPathObj> = {} + const returnObj: Partial<TUrlPathObj<string[], unknown>> = {} for (const f of pathFragments) { const { key, val } = decodePath(f.path) || {} if (!key || !val) continue @@ -193,7 +193,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun // -- end fix logical assignment // nav obj is almost always defined, regardless if standaloneVolume or not - const cViewerState = returnObj['@'] + const cViewerState = returnObj['@'] && returnObj['@'][0] let parsedNavObj = {} if (cViewerState) { try { @@ -223,7 +223,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun } // pluginState should always be defined, regardless if standalone volume or not - const pluginStates = returnObj['pl'] + const pluginStates = fullPath.queryParams['pl'] const { pluginState } = returnState if (pluginStates) { try { @@ -233,16 +233,21 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun /** * parsing plugin error */ - warnCb(`parse plugin states error`, e) + warnCb(`parse plugin states error`, e, pluginStates) } } // preview dataset can and should be displayed regardless of standalone volume or not - const { uiState } = returnState - const stringSearchParam = returnObj['dsp'] + try { - if (stringSearchParam) { - const arr = JSON.parse(stringSearchParam) as Array<{datasetId: string, filename: string}> + const { uiState } = returnState + const arr = returnObj.dsp + ? [{ + datasetId: returnObj.dsp[0], + filename: returnObj.dsp[1] + }] + : fullPath.queryParams['previewingDatasetFiles'] && JSON.parse(fullPath.queryParams['previewingDatasetFiles']) + if (arr) { uiState.previewingDatasetFiles = arr.map(({ datasetId, filename }) => { return { datasetId, @@ -258,6 +263,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun // If sv (standaloneVolume is defined) // only load sv in state // ignore all other params + // /#/sv:%5B%22precomputed%3A%2F%2Fhttps%3A%2F%2Fobject.cscs.ch%2Fv1%2FAUTH_08c08f9f119744cbbf77e216988da3eb%2Fimgsvc-46d9d64f-bdac-418e-a41b-b7f805068c64%22%5D if (!!returnObj['sv']) { try { const parsedArr = JSON.parse(returnObj['sv']) @@ -275,7 +281,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun } try { - const { parcellationSelected, regionsSelected, templateSelected } = parseSearchParamForTemplateParcellationRegion(returnObj as TUrlPathObj, state, warnCb) + const { parcellationSelected, regionsSelected, templateSelected } = parseSearchParamForTemplateParcellationRegion(returnObj as TUrlPathObj<string[], TUrlAtlas<string[]>>, fullPath, state, warnCb) returnState['viewerState']['parcellationSelected'] = parcellationSelected returnState['viewerState']['regionsSelected'] = regionsSelected returnState['viewerState']['templateSelected'] = templateSelected @@ -308,7 +314,7 @@ export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: Fun return returnState } -export const cvtStateToHashedRoutes = state => { +export const cvtStateToHashedRoutes = (state): string[] => { // TODO check if this causes memleak const selectedAtlas = viewerStateGetSelectedAtlas(state) const selectedTemplate = viewerStateSelectedTemplateSelector(state) @@ -326,7 +332,9 @@ export const cvtStateToHashedRoutes = state => { dsPrvArr.push(preview) } - if (dsPrvArr.length > 0) dsPrvString = JSON.stringify(dsPrvArr) + if (dsPrvArr.length === 1) { + dsPrvString = `${dsPrvArr[0].datasetId}::${dsPrvArr[0].filename}` + } } let cNavString: string @@ -344,20 +352,15 @@ export const cvtStateToHashedRoutes = state => { } // encoding selected regions - const accumulatorMap = new Map<string, number[]>() - for (const region of selectedRegions) { + let selectedRegionsString + if (selectedRegions.length === 1) { + const region = selectedRegions[0] const { ngId, labelIndex } = 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) + selectedRegionsString = `${ngId}::${labelIndex}` } - - let routes: TUrlPathObj - - routes = { + let routes: any + + routes= { // for atlas a: selectedAtlas && encodeId(selectedAtlas['@id']), // for template @@ -365,12 +368,12 @@ export const cvtStateToHashedRoutes = state => { // for parcellation p: selectedParcellation && encodeId(selectedParcellation['@id'] || selectedParcellation['fullId']), // for regions - r: Object.keys(cRegionObj).length > 0 && JSON.stringify(cRegionObj), + r: selectedRegionsString && encodeURI(selectedRegionsString), // nav ['@']: cNavString, // dataset file preview - dsp: dsPrvString, - } + dsp: dsPrvString && encodeURI(dsPrvString), + } as TUrlPathObj<string, TUrlAtlas<string>> /** * if any params needs to overwrite previosu routes, put them here @@ -378,11 +381,11 @@ export const cvtStateToHashedRoutes = state => { if (standaloneVolumes && Array.isArray(standaloneVolumes) && standaloneVolumes.length > 0) { routes = { // standalone volumes - sv: JSON.stringify(standaloneVolumes), + sv: encodeURIComponent(JSON.stringify(standaloneVolumes)), // nav ['@']: cNavString, - dsp: dsPrvString - } + dsp: dsPrvString && encodeURI(dsPrvString) + } as TUrlPathObj<string, TUrlStandaloneVolume<string>> } const returnRoutes = [] diff --git a/src/services/effect/pluginUseEffect.ts b/src/services/effect/pluginUseEffect.ts index 098d2fd6c02ead4545ff47aa1dcda9825150656c..e9263e7b7c5f6cbad29cda9c8ac50a0d1f8edbf3 100644 --- a/src/services/effect/pluginUseEffect.ts +++ b/src/services/effect/pluginUseEffect.ts @@ -6,7 +6,7 @@ import { filter, map, startWith, switchMap } from "rxjs/operators" import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service" import { PluginServices } from "src/plugin/atlasViewer.pluginService.service" import { PLUGINSTORE_CONSTANTS } from 'src/services/state/pluginState.store' -import { PLUGINSTORE_ACTION_TYPES } from 'src/services/state/pluginState.helper' +import { PLUGINSTORE_ACTION_TYPES, pluginStateSelectorInitManifests } from 'src/services/state/pluginState.helper' import { IavRootStoreInterface } from "../stateStore.service" import { HttpClient } from "@angular/common/http" @@ -26,9 +26,7 @@ export class PluginServiceUseEffect { http: HttpClient ) { this.initManifests$ = store$.pipe( - select('pluginState'), - select('initManifests'), - filter(v => !!v), + select(pluginStateSelectorInitManifests), startWith([]), map(arr => { // only launch plugins that has init manifest src label on it @@ -36,7 +34,7 @@ export class PluginServiceUseEffect { }), filter(arr => arr.length > 0), switchMap(arr => forkJoin( - ...arr.map(([_source, url]) => + arr.map(([_source, url]) => http.get(url, { headers: constantService.getHttpHeader(), responseType: 'json' diff --git a/src/services/state/pluginState.helper.ts b/src/services/state/pluginState.helper.ts index 2c6494747b0d51354a836fa4af7854735d48bf8a..d620e3f40b148f06c33b08fed673392cdf9f124a 100644 --- a/src/services/state/pluginState.helper.ts +++ b/src/services/state/pluginState.helper.ts @@ -1,5 +1,11 @@ +import { createSelector } from "@ngrx/store" export const PLUGINSTORE_ACTION_TYPES = { SET_INIT_PLUGIN: `SET_INIT_PLUGIN`, CLEAR_INIT_PLUGIN: 'CLEAR_INIT_PLUGIN', -} \ No newline at end of file +} + +export const pluginStateSelectorInitManifests = createSelector( + state => state['pluginState'], + pluginState => pluginState.initManifests +) \ No newline at end of file