From 2329ea90357704b61f72e2e0698418e49d71f974 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 18 Mar 2022 15:47:24 +0100 Subject: [PATCH] feat: use http interceptor to priortise refactor: reworked route state conversion --- .../sapi/core/sapiParcellation.ts | 41 +-- src/atlasComponents/sapi/core/sapiSpace.ts | 17 +- src/atlasComponents/sapi/sapi.service.ts | 40 +-- src/atlasComponents/sapi/type.ts | 4 + src/routerModule/module.ts | 4 +- .../routeStateTransform.service.ts | 269 ++++++++++++++++++ src/routerModule/router.service.ts | 49 ++-- src/routerModule/type.ts | 5 - src/routerModule/util.ts | 239 +--------------- src/state/atlasSelection/effects.ts | 10 +- src/state/index.ts | 14 +- src/util/priority.ts | 42 ++- src/util/pureConstant.service.ts | 165 ----------- src/viewerModule/nehuba/store/util.ts | 9 +- 14 files changed, 418 insertions(+), 490 deletions(-) create mode 100644 src/routerModule/routeStateTransform.service.ts diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts index 7add33499..00c030313 100644 --- a/src/atlasComponents/sapi/core/sapiParcellation.ts +++ b/src/atlasComponents/sapi/core/sapiParcellation.ts @@ -1,24 +1,30 @@ +import { Observable } from "rxjs" import { SapiVolumeModel } from ".." import { SAPI } from "../sapi.service" -import { SapiParcellationFeatureModel, SapiParcellationModel, SapiRegionModel } from "../type" +import { SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryParam, SapiRegionModel } from "../type" + +type PaginationQuery = {} export class SAPIParcellation{ constructor(private sapi: SAPI, public atlasId: string, public id: string){ } - getDetail(): Promise<SapiParcellationModel>{ - return this.sapi.cachedGet<SapiParcellationModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}` + + getDetail(queryParam?: SapiQueryParam): Observable<SapiParcellationModel>{ + return this.sapi.httpGet<SapiParcellationModel>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`, + null, + queryParam ) } - getRegions(spaceId: string): Promise<SapiRegionModel[]> { - return this.sapi.cachedGet<SapiRegionModel[]>( + + getRegions(spaceId: string, queryParam?: SapiQueryParam): Observable<SapiRegionModel[]> { + return this.sapi.httpGet<SapiRegionModel[]>( `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, { - params: { - space_id: spaceId - } - } + space_id: spaceId + }, + queryParam ) } getVolumes(): Promise<SapiVolumeModel[]>{ @@ -27,16 +33,15 @@ export class SAPIParcellation{ ) } - getFeatures(): Promise<SapiParcellationFeatureModel[]> { - return this.sapi.http.get<SapiParcellationFeatureModel[]>( + getFeatures(param?: PaginationQuery, queryParam?: SapiQueryParam): Observable<SapiParcellationFeatureModel[]> { + return this.sapi.httpGet<SapiParcellationFeatureModel[]>( `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`, { - params: { - per_page: 5, - page: 0, - } - } - ).toPromise() + per_page: '5', + page: '0', + }, + queryParam + ) } getFeatureInstance(instanceId: string): Promise<SapiParcellationFeatureModel> { diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts index 1b3ef810f..33c3c88b7 100644 --- a/src/atlasComponents/sapi/core/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -2,7 +2,8 @@ import { Observable } from "rxjs" import { SAPI } from '../sapi.service' import { camelToSnake } from 'common/util' import { IVolumeTypeDetail } from "src/util/siibraApiConstants/types" -import { SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel } from "../type" +import { SapiQueryParam, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel } from "../type" +import { tap } from "rxjs/operators" type FeatureResponse = { features: { @@ -37,9 +38,11 @@ export class SAPISpace{ constructor(private sapi: SAPI, public atlasId: string, public id: string){} - getModalities(): Observable<FeatureResponse> { - return this.sapi.http.get<FeatureResponse>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features` + getModalities(param?: SapiQueryParam): Observable<FeatureResponse> { + return this.sapi.httpGet<FeatureResponse>( + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, + null, + param ) } @@ -56,9 +59,11 @@ export class SAPISpace{ ) } - getDetail(): Promise<SapiSpaceModel>{ - return this.sapi.cachedGet<SapiSpaceModel>( + getDetail(param?: SapiQueryParam): Observable<SapiSpaceModel>{ + return this.sapi.httpGet<SapiSpaceModel>( `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`, + null, + param ) } diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 083bfcf68..be6a9a608 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -3,13 +3,15 @@ import { HttpClient } from '@angular/common/http'; import { BS_ENDPOINT } from 'src/util/constants'; import { map, shareReplay, take, tap } from "rxjs/operators"; import { SAPIAtlas, SAPISpace } from './core' -import { SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel, SpyNpArrayDataModel } from "./type"; +import { SapiAtlasModel, SapiParcellationModel, SapiQueryParam, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel, SpyNpArrayDataModel } from "./type"; import { CachedFunction, getExportNehuba } from "src/util/fn"; import { SAPIParcellation } from "./core/sapiParcellation"; import { SAPIRegion } from "./core/sapiRegion" import { MatSnackBar } from "@angular/material/snack-bar"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { EnumColorMapName } from "src/util/colorMaps"; +import { PRIORITY_HEADER } from "src/util/priority"; +import { Observable } from "rxjs"; export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' export const SIIBRA_API_VERSION = '0.2.0' @@ -56,26 +58,17 @@ export class SAPI{ return new SAPIRegion(this, atlasId, parcId, regionId) } - @CachedFunction({ - serialization: (atlasId, spaceId, ...args) => `sapi::getSpaceDetail::${atlasId}::${spaceId}` - }) - getSpaceDetail(atlasId: string, spaceId: string, priority = 0): Promise<SapiSpaceModel> { - return this.getSpace(atlasId, spaceId).getDetail() + getSpaceDetail(atlasId: string, spaceId: string, param?: SapiQueryParam): Observable<SapiSpaceModel> { + return this.getSpace(atlasId, spaceId).getDetail(param) } - @CachedFunction({ - serialization: (atlasId, parcId, ...args) => `sapi::getParcDetail::${atlasId}::${parcId}` - }) - getParcDetail(atlasId: string, parcId: string, priority = 0): Promise<SapiParcellationModel> { - return this.getParcellation(atlasId, parcId).getDetail() + getParcDetail(atlasId: string, parcId: string, param?: SapiQueryParam): Observable<SapiParcellationModel> { + return this.getParcellation(atlasId, parcId).getDetail(param) } - @CachedFunction({ - serialization: (atlasId, parcId, spaceId, ...args) => `sapi::getRegions::${atlasId}::${parcId}::${spaceId}` - }) - getParcRegions(atlasId: string, parcId: string, spaceId: string, priority = 0): Promise<SapiRegionModel[]> { + getParcRegions(atlasId: string, parcId: string, spaceId: string, queryParam?: SapiQueryParam): Observable<SapiRegionModel[]> { const parc = this.getParcellation(atlasId, parcId) - return parc.getRegions(spaceId) + return parc.getRegions(spaceId, queryParam) } @CachedFunction({ @@ -94,6 +87,21 @@ export class SAPI{ return this.http.get<T>(url, option).toPromise() } + httpGet<T>(url: string, params?: Record<string, string>, sapiParam?: SapiQueryParam){ + return this.http.get<T>( + url, + { + params: { + ...( params || {} ), + ...( + sapiParam?.priority + ? ({ [PRIORITY_HEADER]: sapiParam.priority }) + : {} + ) + } + } + ) + } public atlases$ = this.http.get<SapiAtlasModel[]>( `${this.bsEndpoint}/atlases`, diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts index 919649125..77a94d21f 100644 --- a/src/atlasComponents/sapi/type.ts +++ b/src/atlasComponents/sapi/type.ts @@ -76,3 +76,7 @@ export function guardPipe< }) ) } + +export type SapiQueryParam = { + priority: number +} diff --git a/src/routerModule/module.ts b/src/routerModule/module.ts index 1053deeb9..c5e484500 100644 --- a/src/routerModule/module.ts +++ b/src/routerModule/module.ts @@ -2,6 +2,7 @@ import { APP_BASE_HREF } from "@angular/common"; import { NgModule } from "@angular/core"; import { RouterModule } from '@angular/router' import { RouterService } from "./router.service"; +import { RouteStateTransformSvc } from "./routeStateTransform.service"; import { routes } from "./util"; @@ -16,7 +17,8 @@ import { routes } from "./util"; provide: APP_BASE_HREF, useValue: '/' }, - RouterService + RouterService, + RouteStateTransformSvc, ], exports:[ RouterModule diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts new file mode 100644 index 000000000..0efa659ce --- /dev/null +++ b/src/routerModule/routeStateTransform.service.ts @@ -0,0 +1,269 @@ +import { Injectable } from "@angular/core"; +import { UrlSegment, UrlTree } from "@angular/router"; +import { map } from "rxjs/operators"; +import { SAPI } from "src/atlasComponents/sapi"; +import { atlasSelection, defaultState, MainState, plugins } from "src/state"; +import { getParcNgId, getRegionLabelIndex } from "src/viewerModule/nehuba/config.service"; +import { decodeToNumber, encodeNumber, encodeURIFull, separator } from "./cipher"; +import { TUrlAtlas, TUrlPathObj, TUrlStandaloneVolume } from "./type"; +import { decodePath, encodeId, endcodePath } from "./util"; + +@Injectable() +export class RouteStateTransformSvc { + + static GetOneAndOnlyOne<T>(arr: T[]): T{ + if (!arr || arr.length === 0) return null + if (arr.length > 1) throw new Error(`expecting exactly 1 item, got ${arr.length}`) + return arr[0] + } + + constructor(private sapi: SAPI){} + + private async getATPR(obj: TUrlPathObj<string[], TUrlAtlas<string[]>>){ + const selectedAtlasId = RouteStateTransformSvc.GetOneAndOnlyOne(obj.a) + const selectedTemplateId = RouteStateTransformSvc.GetOneAndOnlyOne(obj.t) + const selectedParcellationId = RouteStateTransformSvc.GetOneAndOnlyOne(obj.p) + const selectedRegionIds = obj.r + + const [ + selectedAtlas, + selectedTemplate, + selectedParcellation, + allParcellationRegions = [] + ] = await Promise.all([ + this.sapi.atlases$.pipe( + map(atlases => atlases.find(atlas => atlas["@id"] === selectedAtlasId)) + ).toPromise(), + this.sapi.getSpaceDetail(selectedAtlasId, selectedTemplateId, { priority: 10 }).toPromise(), + this.sapi.getParcDetail(selectedAtlasId, selectedParcellationId, { priority: 10 }).toPromise(), + this.sapi.getParcRegions(selectedAtlasId, selectedParcellationId, selectedTemplateId, { priority: 10 }).toPromise(), + ]) + + const latNgIdMap = ["left hemisphere", "right hemisphere", "whole brain"].map(lat => { + let regex: RegExp = /./ + if (lat === "left hemisphere") regex = /left/i + if (lat === "right hemisphere") regex = /right/i + return { + lat, + regex, + ngId: getParcNgId(selectedAtlas, selectedTemplate, selectedParcellation, lat) + } + }) + + const selectedRegions = (() => { + if (!selectedRegionIds) return [] + /** + * assuming only 1 selected region + * if this assumption changes, iterate over array of selectedRegionIds + */ + const json = { [selectedRegionIds[0]]: selectedRegionIds[1] } + + for (const ngId in json) { + const matchingMap = latNgIdMap.find(ngIdMap => ngIdMap.ngId === ngId) + if (!matchingMap) { + console.error(`could not find matching map for ${ngId}`) + continue + } + + 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) { + /** + * currently, only 1 region is expected to be selected at max + * this method probably out performs creating a map + * but with 2+ regions, creating a map would consistently be faster + */ + for (const r of allParcellationRegions) { + const idx = getRegionLabelIndex(selectedAtlas, selectedTemplate, selectedParcellation, r) + if (idx === labelIndex) { + return [ r ] + } + } + } + } + return [] + })() + + return { + selectedAtlas, + selectedTemplate, + selectedParcellation, + selectedRegions, + } + } + + async cvtRouteToState(fullPath: UrlTree) { + + const returnState: MainState = defaultState + const pathFragments: UrlSegment[] = fullPath.root.hasChildren() + ? fullPath.root.children['primary'].segments + : [] + + const returnObj: Partial<TUrlPathObj<string[], unknown>> = {} + for (const f of pathFragments) { + const { key, val } = decodePath(f.path) || {} + if (!key || !val) continue + returnObj[key] = val + } + + // nav obj is almost always defined, regardless if standaloneVolume or not + const cViewerState = returnObj['@'] && returnObj['@'][0] + let parsedNavObj: MainState['[state.atlasSelection]']['navigation'] + 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) + parsedNavObj = { + orientation: o, + perspectiveOrientation: po, + perspectiveZoom: pz, + position: p, + zoom: z, + } + } catch (e) { + /** + * TODO Poisoned encoded char + * send error message + */ + console.error(`cannot parse navigation state`, e) + } + } + + // pluginState should always be defined, regardless if standalone volume or not + const pluginStates = fullPath.queryParams['pl'] + if (pluginStates) { + try { + const arrPluginStates = JSON.parse(pluginStates) + returnState["[state.plugins]"].initManifests = arrPluginStates.map(url => [plugins.INIT_MANIFEST_SRC, url] as [string, string]) + } catch (e) { + /** + * parsing plugin error + */ + console.error(`parse plugin states error`, e, pluginStates) + } + } + + // 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 + const standaloneVolumes = fullPath.queryParams['standaloneVolumes'] + try { + const parsedArr = JSON.parse(standaloneVolumes) + if (!Array.isArray(parsedArr)) throw new Error(`Parsed standalone volumes not of type array`) + + returnState["[state.atlasSelection]"].standAloneVolumes = parsedArr + returnState["[state.atlasSelection]"].navigation = parsedNavObj + return returnState + } catch (e) { + // if any error occurs, parse rest per normal + console.error(`parse standalone volume error`, e) + } + + + try { + const { selectedAtlas, selectedParcellation, selectedRegions, selectedTemplate } = await this.getATPR(returnObj as TUrlPathObj<string[], TUrlAtlas<string[]>>) + returnState["[state.atlasSelection]"].selectedAtlas = selectedAtlas + returnState["[state.atlasSelection]"].selectedParcellation = selectedParcellation + returnState["[state.atlasSelection]"].selectedTemplate = selectedTemplate + returnState["[state.atlasSelection]"].selectedRegions = selectedRegions + + } catch (e) { + // if error, show error on UI? + console.error(`parse template, parc, region error`, e) + } + return returnState + } + + cvtStateToRoute(state: MainState) { + const { + atlas: selectedAtlas, + parcellation: selectedParcellation, + template: selectedTemplate, + } = atlasSelection.selectors.selectedATP(state) + + const selectedRegions = atlasSelection.selectors.selectedRegions(state) + const standaloneVolumes = atlasSelection.selectors.standaloneVolumes(state) + const navigation = atlasSelection.selectors.navigation(state) + + let dsPrvString: string + const searchParam = new URLSearchParams() + + let cNavString: string + if (navigation) { + const { orientation, perspectiveOrientation, perspectiveZoom, position, zoom } = navigation + if (orientation && perspectiveOrientation && perspectiveZoom && position && zoom) { + cNavString = [ + orientation.map((n: number) => 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}`) + } + } + + // encoding selected regions + let selectedRegionsString: string + if (selectedRegions.length === 1) { + const region = selectedRegions[0] + const labelIndex = getRegionLabelIndex(selectedAtlas, selectedTemplate, selectedParcellation, region) + + const ngId = getParcNgId(selectedAtlas, selectedTemplate, selectedParcellation, region) + selectedRegionsString = `${ngId}::${encodeNumber(labelIndex, { float: false })}` + } + let routes: any + + routes= { + // for atlas + a: selectedAtlas && encodeId(selectedAtlas['@id']), + // for template + t: selectedTemplate && encodeId(selectedTemplate['@id'] || selectedTemplate['fullId']), + // for parcellation + p: selectedParcellation && encodeId(selectedParcellation['@id'] || selectedParcellation['fullId']), + // for regions + r: selectedRegionsString && encodeURIFull(selectedRegionsString), + // nav + ['@']: cNavString, + // dataset file preview + dsp: dsPrvString && encodeURI(dsPrvString), + } as TUrlPathObj<string, TUrlAtlas<string>> + + /** + * if any params needs to overwrite previosu routes, put them here + */ + if (standaloneVolumes && Array.isArray(standaloneVolumes) && standaloneVolumes.length > 0) { + searchParam.set('standaloneVolumes', JSON.stringify(standaloneVolumes)) + routes = { + // nav + ['@']: cNavString, + } as TUrlPathObj<string|string[], TUrlStandaloneVolume<string[]>> + } + + const routesArr: string[] = [] + for (const key in routes) { + if (!!routes[key]) { + const segStr = endcodePath(key, routes[key]) + routesArr.push(segStr) + } + } + + return searchParam.toString() === '' + ? routesArr.join('/') + : `${routesArr.join('/')}?${searchParam.toString()}` + } +} diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index 28a8c696b..ea1724ef4 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -4,11 +4,10 @@ import { Inject } from "@angular/core"; import { NavigationEnd, Router } from '@angular/router' import { Store } from "@ngrx/store"; import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators"; -import { PureContantService } from "src/util"; -import { cvtStateToHashedRoutes, cvtFullRouteToState, encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; +import { encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; import { BehaviorSubject, combineLatest, merge, NEVER, Observable, of } from 'rxjs' import { scan } from 'rxjs/operators' -import { generalActions } from "src/state" +import { RouteStateTransformSvc } from "./routeStateTransform.service"; @Injectable({ providedIn: 'root' @@ -37,7 +36,7 @@ export class RouterService { constructor( router: Router, - pureConstantService: PureContantService, + routeToStateTransformSvc: RouteStateTransformSvc, store$: Store<any>, @Inject(APP_BASE_HREF) baseHref: string ){ @@ -106,28 +105,28 @@ export class RouterService { ).subscribe(arg => { const [ev, state, customRoutes] = arg - const fullPath = ev.urlAfterRedirects - const stateFromRoute = cvtFullRouteToState(router.parseUrl(fullPath), state, this.logError) - let routeFromState: string - try { - routeFromState = cvtStateToHashedRoutes(state) - } catch (_e) { - routeFromState = `` - } + // const fullPath = ev.urlAfterRedirects + // const stateFromRoute = cvtFullRouteToState(router.parseUrl(fullPath), state, this.logError) + // let routeFromState: string + // try { + // routeFromState = cvtStateToHashedRoutes(state) + // } catch (_e) { + // routeFromState = `` + // } - for (const key in customRoutes) { - const customStatePath = encodeCustomState(key, customRoutes[key]) - if (!customStatePath) continue - routeFromState += `/${customStatePath}` - } + // for (const key in customRoutes) { + // const customStatePath = encodeCustomState(key, customRoutes[key]) + // if (!customStatePath) continue + // routeFromState += `/${customStatePath}` + // } - if ( fullPath !== `/${routeFromState}`) { - store$.dispatch( - generalActions.generalApplyState({ - state: stateFromRoute - }) - ) - } + // if ( fullPath !== `/${routeFromState}`) { + // store$.dispatch( + // generalActions.generalApplyState({ + // state: stateFromRoute + // }) + // ) + // } }) // TODO this may still be a bit finiky. @@ -140,7 +139,7 @@ export class RouterService { debounceTime(160), map(state => { try { - return cvtStateToHashedRoutes(state) + return `` //cvtStateToHashedRoutes(state) } catch (e) { this.logError(e) return `` diff --git a/src/routerModule/type.ts b/src/routerModule/type.ts index fa8855ad8..10f7e0e76 100644 --- a/src/routerModule/type.ts +++ b/src/routerModule/type.ts @@ -9,10 +9,6 @@ export type TUrlAtlas<T> = { r?: T // region selected } -export type TUrlPreviewDs<T> = { - dsp: T // dataset preview -} - export type TUrlPlugin<T> = { pl: T // pluginState } @@ -22,7 +18,6 @@ export type TUrlNav<T> = { } export type TConditional<T> = Partial< - TUrlPreviewDs<T> & TUrlPlugin<T> & TUrlNav<T> > diff --git a/src/routerModule/util.ts b/src/routerModule/util.ts index 90a8f8ed5..ee8f181f0 100644 --- a/src/routerModule/util.ts +++ b/src/routerModule/util.ts @@ -1,28 +1,17 @@ -import { encodeNumber, decodeToNumber, separator, encodeURIFull } from './cipher' import { UrlSegment, UrlTree } from "@angular/router" import { Component } from "@angular/core" -import { atlasSelection, plugins } from "src/state" -import { - TUrlStandaloneVolume, - TUrlAtlas, - TUrlPathObj, -} from './type' +export const encodeId = (id: string) => id && id.replace(/\//g, ':') +export const decodeId = (codedId: string) => codedId && codedId.replace(/:/g, '/') -import { - parseSearchParamForTemplateParcellationRegion, - encodeId, -} from './parseRouteToTmplParcReg' -import { spaceMiscInfoMap } from "src/util/pureConstant.service" -import { getRegionLabelIndex, getParcNgId } from "src/viewerModule/nehuba/config.service" - -const endcodePath = (key: string, val: string|string[]) => +export const endcodePath = (key: string, val: string|string[]) => key[0] === '?' ? `?${key}=${val}` : `${key}:${Array.isArray(val) ? val.map(v => encodeURI(v)).join('::') : encodeURI(val)}` -const decodePath = (path: string) => { + +export const decodePath = (path: string) => { const re = /^(.*?):(.*?)$/.exec(path) if (!re) return null return { @@ -42,224 +31,6 @@ export const DEFAULT_NAV = { position: [0, 0, 0], } -export const cvtFullRouteToState = (fullPath: UrlTree, state: any, _warnCb?: (arg: string) => void) => { - - const warnCb = _warnCb || ((...e: any[]) => console.warn(...e)) - const pathFragments: UrlSegment[] = fullPath.root.hasChildren() - ? fullPath.root.children['primary'].segments - : [] - - const returnState = JSON.parse(JSON.stringify(state)) - - const returnObj: Partial<TUrlPathObj<string[], unknown>> = {} - for (const f of pathFragments) { - const { key, val } = decodePath(f.path) || {} - if (!key || !val) continue - returnObj[key] = val - } - - // logical assignment. Use instead of above after typescript > v4.0.0 - // returnState['viewerState'] ||= {} - if (!returnState['viewerState']) { - returnState['viewerState'] = {} - } - // -- end fix logical assignment - - // nav obj is almost always defined, regardless if standaloneVolume or not - const cViewerState = returnObj['@'] && returnObj['@'][0] - let parsedNavObj: any - 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) - parsedNavObj = { - 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 - */ - warnCb(`parse nav error`, e) - } - } - - // pluginState should always be defined, regardless if standalone volume or not - const pluginStates = fullPath.queryParams['pl'] - const { pluginState } = returnState - if (pluginStates) { - try { - const arrPluginStates = JSON.parse(pluginStates) - pluginState.initManifests = arrPluginStates.map(url => [plugins.INIT_MANIFEST_SRC, url] as [string, string]) - } catch (e) { - /** - * parsing plugin error - */ - warnCb(`parse plugin states error`, e, pluginStates) - } - } - - // preview dataset can and should be displayed regardless of standalone volume or not - - try { - 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, - filename - } - }) - } - } catch (e) { - // parsing previewingDatasetFiles - warnCb(`parse dsp error`, e) - } - - // 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 - const standaloneVolumes = fullPath.queryParams['standaloneVolumes'] - if (!!standaloneVolumes) { - try { - const parsedArr = JSON.parse(standaloneVolumes) - if (!Array.isArray(parsedArr)) throw new Error(`Parsed standalone volumes not of type array`) - - returnState['viewerState']['standaloneVolumes'] = parsedArr - returnState['viewerState']['navigation'] = parsedNavObj - return returnState - } catch (e) { - // if any error occurs, parse rest per normal - warnCb(`parse standalone volume error`, e) - } - } else { - returnState['viewerState']['standaloneVolumes'] = [] - } - - try { - 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 - - if (templateSelected) { - const { scale } = spaceMiscInfoMap.get(templateSelected.id) || { scale: 1 } - returnState['viewerState']['navigation'] = parsedNavObj || ({ - ...DEFAULT_NAV, - zoom: 350000 * scale, - perspectiveZoom: 1922235.5293810747 * scale - }) - } - } catch (e) { - // if error, show error on UI? - warnCb(`parse template, parc, region error`, e) - } - - /** - * parsing template to get atlasId - */ - // TODO - return returnState -} - -export const cvtStateToHashedRoutes = (state): string => { - // TODO check if this causes memleak - const { - atlas: selectedAtlas, - parcellation: selectedParcellation, - template: selectedTemplate, - } = atlasSelection.selectors.selectedATP(state) - - const selectedRegions = atlasSelection.selectors.selectedRegions(state) - const standaloneVolumes = atlasSelection.selectors.standaloneVolumes(state) - const navigation = atlasSelection.selectors.navigation(state) - - let dsPrvString: string - const searchParam = new URLSearchParams() - - let cNavString: string - if (navigation) { - const { orientation, perspectiveOrientation, perspectiveZoom, position, zoom } = navigation - if (orientation && perspectiveOrientation && perspectiveZoom && position && zoom) { - cNavString = [ - orientation.map((n: number) => 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}`) - } - } - - // encoding selected regions - let selectedRegionsString: string - if (selectedRegions.length === 1) { - const region = selectedRegions[0] - const labelIndex = getRegionLabelIndex(selectedAtlas, selectedTemplate, selectedParcellation, region) - - const ngId = getParcNgId(selectedAtlas, selectedTemplate, selectedParcellation, region) - selectedRegionsString = `${ngId}::${encodeNumber(labelIndex, { float: false })}` - } - let routes: any - - routes= { - // for atlas - a: selectedAtlas && encodeId(selectedAtlas['@id']), - // for template - t: selectedTemplate && encodeId(selectedTemplate['@id'] || selectedTemplate['fullId']), - // for parcellation - p: selectedParcellation && encodeId(selectedParcellation['@id'] || selectedParcellation['fullId']), - // for regions - r: selectedRegionsString && encodeURIFull(selectedRegionsString), - // nav - ['@']: cNavString, - // dataset file preview - dsp: dsPrvString && encodeURI(dsPrvString), - } as TUrlPathObj<string, TUrlAtlas<string>> - - /** - * if any params needs to overwrite previosu routes, put them here - */ - if (standaloneVolumes && Array.isArray(standaloneVolumes) && standaloneVolumes.length > 0) { - searchParam.set('standaloneVolumes', JSON.stringify(standaloneVolumes)) - routes = { - // nav - ['@']: cNavString, - dsp: dsPrvString && encodeURI(dsPrvString) - } as TUrlPathObj<string|string[], TUrlStandaloneVolume<string[]>> - } - - const routesArr: string[] = [] - for (const key in routes) { - if (!!routes[key]) { - const segStr = endcodePath(key, routes[key]) - routesArr.push(segStr) - } - } - - return searchParam.toString() === '' - ? routesArr.join('/') - : `${routesArr.join('/')}?${searchParam.toString()}` -} - export const verifyCustomState = (key: string) => { return /^x-/.test(key) } diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 7620569b1..6454613ea 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -19,10 +19,12 @@ export class Effect { filter(action => !!action.atlas), switchMap(({ atlas }) => { const selectedParc = atlas.parcellations.find(p => /290/.test(p["@id"])) || atlas.parcellations[0] - return this.sapiSvc.getParcDetail(atlas["@id"], selectedParc["@id"], 100).then( - parcellation => ({ - parcellation, - atlas + return this.sapiSvc.getParcDetail(atlas["@id"], selectedParc["@id"], { priority: 10 }).pipe( + map(parcellation => { + return { + parcellation, + atlas + } }) ) }), diff --git a/src/state/index.ts b/src/state/index.ts index 18f568ae7..034486928 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -60,4 +60,16 @@ export function getStoreEffects() { ] } -export { MainState } from "./const" +import { MainState } from "./const" + +export { MainState } + +export const defaultState: MainState = { + [userPreference.nameSpace]: userPreference.defaultState, + [atlasSelection.nameSpace]: atlasSelection.defaultState, + [userInterface.nameSpace]: userInterface.defaultState, + [userInteraction.nameSpace]: userInteraction.defaultState, + [annotation.nameSpace]: annotation.defaultState, + [plugins.nameSpace]: plugins.defaultState, + [atlasAppearance.nameSpace]: atlasAppearance.defaultState, +} diff --git a/src/util/priority.ts b/src/util/priority.ts index 441ed5e0a..34673b81e 100644 --- a/src/util/priority.ts +++ b/src/util/priority.ts @@ -1,7 +1,7 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http" import { Injectable } from "@angular/core" import { interval, merge, Observable, Subject, timer } from "rxjs" -import { filter, finalize, switchMap, switchMapTo, take } from "rxjs/operators" +import { filter, finalize, switchMap, switchMapTo, take, takeUntil, takeWhile } from "rxjs/operators" export const PRIORITY_HEADER = 'x-sxplr-http-priority' @@ -12,10 +12,13 @@ type PriorityReq = { next: HttpHandler } -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class PriorityHttpInterceptor implements HttpInterceptor{ private priorityQueue: PriorityReq[] = [] + private currentJob: Set<string> = new Set() private priority$: Subject<PriorityReq> = new Subject() @@ -24,10 +27,6 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ private counter = 0 private max = 6 - private shouldRun(){ - return this.counter <= this.max - } - constructor(){ this.forceCheck$.pipe( switchMapTo( @@ -35,17 +34,24 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ timer(0), interval(16) ).pipe( - filter(() => this.shouldRun()) + filter(() => { + return this.counter <= this.max + }), + takeWhile(() => this.priorityQueue.length > 0) ) ) ).subscribe(() => { - this.priority$.next( - this.priorityQueue.pop() - ) + const job = this.priorityQueue.pop() + if (!job) return + this.currentJob.add(job.urlWithParams) + this.priority$.next(job) }) } updatePriority(urlWithParams: string, newPriority: number) { + + if (this.currentJob.has(urlWithParams)) return + const foundIdx = this.priorityQueue.findIndex(v => v.urlWithParams === urlWithParams) if (foundIdx < 0) return false const [ item ] = this.priorityQueue.splice(foundIdx, 1) @@ -57,7 +63,17 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ } private insert(obj: PriorityReq) { - const { priority } = obj + const { priority, urlWithParams } = obj + + if (this.currentJob.has(urlWithParams)) return + + const existing = this.priorityQueue.find(q => q.urlWithParams === urlWithParams) + if (existing) { + if (existing.priority < priority) { + this.updatePriority(urlWithParams, priority) + } + return + } const foundIdx = this.priorityQueue.findIndex(q => q.priority <= priority) const useIndex = foundIdx >= 0 ? foundIdx : this.priorityQueue.length this.priorityQueue.splice(useIndex, 0, obj) @@ -73,6 +89,8 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ next, urlWithParams } + return next.handle(req) + this.insert(objToInsert) this.forceCheck$.next(true) @@ -81,10 +99,12 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ filter(v => v.urlWithParams === urlWithParams), take(1), switchMap(({ next, req }) => { + console.log("handle!!") this.counter ++ return next.handle(req) }), finalize(() => { + console.log('finalize??') this.counter -- }), ) diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts index 68360a593..477a7c7a6 100644 --- a/src/util/pureConstant.service.ts +++ b/src/util/pureConstant.service.ts @@ -125,171 +125,6 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}" ) } - private patchRegions$ = forkJoin( - patchRegions.map(patch => from(patch)) - ).pipe( - shareReplay(1) - ) - - private getRegions(atlasId: string, parcId: string, spaceId: string){ - return this.http.get<TRegionSummary[]>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}/regions`, - { - params: { - 'space_id': spaceId - }, - responseType: 'json' - } - ).pipe( - switchMap(regions => this.patchRegions$.pipe( - map(patchRegions => { - for (const p of patchRegions) { - if ( - p.targetParcellation !== '*' - && Array.isArray(p.targetParcellation) - && p.targetParcellation.every(p => p["@id"] !== parcId) - ) { - continue - } - if ( - p.targetSpace !== '*' - && Array.isArray(p.targetSpace) - && p.targetSpace.every(sp => sp['@id'] !== spaceId) - ) { - continue - } - - recursiveMutate( - regions, - r => r.children || [], - region => { - - if (p["@type"] === 'julich/siibra/append-region/v0.0.1') { - if (p.parent['name'] === region.name) { - if (!region.children) region.children = [] - region.children.push( - p.payload as TRegionSummary - ) - } - } - if (p['@type'] === 'julich/siibra/patch-region/v0.0.1') { - if (p.target['name'] === region.name) { - mutateDeepMerge( - region, - p.payload - ) - } - } - }, - true - ) - } - return regions - }) - )) - ) - } - - private getParcs(atlasId: string){ - return this.http.get<TParc[]>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations`, - { responseType: 'json' } - ) - } - - private httpCallCache = new Map<string, Observable<any>>() - - private getParcDetail(atlasId: string, parcId: string) { - return this.http.get<TParc>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/parcellations/${encodeURIComponent(parcId)}`, - { responseType: 'json' } - ) - } - - private getSpaces(atlasId: string){ - return this.http.get<TSpaceSummary[]>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces`, - { responseType: 'json' } - ) - } - - private getSpaceDetail(atlasId: string, spaceId: string) { - return this.http.get<TSpaceFull>( - `${this.bsEndpoint}/atlases/${encodeURIComponent(atlasId)}/spaces/${encodeURIComponent(spaceId)}`, - { responseType: 'json' } - ) - } - - private getSpacesAndParc(atlasId: string): Observable<{ templateSpaces: TSpaceFull[], parcellations: TParc[] }> { - const cacheKey = `getSpacesAndParc::${atlasId}` - if (this.httpCallCache.has(cacheKey)) return this.httpCallCache.get(cacheKey) - - const spaces$ = this.getSpaces(atlasId).pipe( - switchMap(spaces => spaces.length > 0 - ? forkJoin( - spaces.map(space => this.getSpaceDetail(atlasId, parseId(space.id))) - ) - : of([])) - ) - const parcs$ = this.getParcs(atlasId).pipe( - // need not to get full parc data. first level gets all data - // switchMap(parcs => forkJoin( - // parcs.map(parc => this.getParcDetail(atlasId, parseId(parc.id))) - // )) - ) - const returnObs = forkJoin([ - spaces$, - parcs$, - ]).pipe( - map(([ templateSpaces, parcellations ]) => { - /** - * select only parcellations that contain renderable volume(s) - */ - const filteredParcellations = parcellations.filter(p => { - return p._dataset_specs.some(spec => spec["@type"] === 'fzj/tmp/volume_type/v0.0.1' && validVolumeType.has(spec.volume_type)) - }) - - /** - * remove parcellation versions that are marked as deprecated - * and assign prev/next id accordingly - */ - for (const p of filteredParcellations) { - if (!p.version) continue - if (p.version.deprecated) { - const prevId = p.version.prev - const nextId = p.version.next - - const prev = prevId && filteredParcellations.find(p => parseId(p.id) === prevId) - const next = nextId && filteredParcellations.find(p => parseId(p.id) === nextId) - - const newPrevId = prev && parseId(prev.id) - const newNextId = next && parseId(next.id) - - if (!!prev.version) { - prev.version.next = newNextId - } - - if (!!next.version) { - next.version.prev = newPrevId - } - } - } - const removeDeprecatedParc = filteredParcellations.filter(p => { - if (!p.version) return true - return !(p.version.deprecated) - }) - - return { - templateSpaces, - parcellations: removeDeprecatedParc - } - }), - shareReplay(1) - ) - this.httpCallCache.set(cacheKey, returnObs) - return returnObs - } - constructor( private store: Store<any>, private http: HttpClient, diff --git a/src/viewerModule/nehuba/store/util.ts b/src/viewerModule/nehuba/store/util.ts index 8fefc1aea..fdc923b8f 100644 --- a/src/viewerModule/nehuba/store/util.ts +++ b/src/viewerModule/nehuba/store/util.ts @@ -43,9 +43,9 @@ export const fromRootStore = { filter(({ parcellation }) => !!parcellation), switchMap(({ atlas, template, parcellation }) => { return forkJoin([ - sapi.registry.get<SAPIParcellation>(parcellation["@id"]) - .getRegions(template["@id"]) - .then(regions => { + sapi.registry.get<SAPIParcellation>(parcellation["@id"]).getRegions(template["@id"]).pipe( + map(regions => { + const returnArr: ParcVolumeSpec[] = [] for (const r of regions) { const source = r?.hasAnnotation?.visualizedIn?.["@id"] @@ -71,7 +71,8 @@ export const fromRootStore = { }) } return returnArr - }), + }) + ), sapi.registry.get<SAPIParcellation>(parcellation["@id"]).getVolumes() ]).pipe( map(([ volumeSrcs, volumes ]) => { -- GitLab