diff --git a/backend/app/sane_url.py b/backend/app/sane_url.py index 947466933489b6509e6139df18d032864fbade43..dc4a4e3470896620de381882bc6478c8dc598e91 100644 --- a/backend/app/sane_url.py +++ b/backend/app/sane_url.py @@ -4,7 +4,7 @@ from fastapi.exceptions import HTTPException from authlib.integrations.requests_client import OAuth2Session import requests import json -from typing import Union, Dict, Optional +from typing import Union, Dict, Optional, Any import time from io import StringIO from pydantic import BaseModel @@ -126,11 +126,19 @@ data_proxy_store = SaneUrlDPStore() @router.get("/{short_id:str}") async def get_short(short_id:str, request: Request): try: - existing_json = data_proxy_store.get(short_id) + existing_json: Dict[str, Any] = data_proxy_store.get(short_id) accept = request.headers.get("Accept", "") if "text/html" in accept: hashed_path = existing_json.get("hashPath") - return RedirectResponse(f"{HOST_PATHNAME}/#{hashed_path}") + extra_routes = [] + for key in existing_json: + if key.startswith("x-"): + extra_routes.append(f"{key}:{short_id}") + continue + + extra_routes_str = "" if len(extra_routes) == 0 else ("/" + "/".join(extra_routes)) + + return RedirectResponse(f"{HOST_PATHNAME}/#{hashed_path}{extra_routes_str}") return JSONResponse(existing_json) except DataproxyStore.NotFound as e: raise HTTPException(404, str(e)) diff --git a/docs/releases/v2.13.2.md b/docs/releases/v2.13.2.md new file mode 100644 index 0000000000000000000000000000000000000000..d0fe9ede6a3a32e3400acf75ac3f3ddb41d4db6b --- /dev/null +++ b/docs/releases/v2.13.2.md @@ -0,0 +1,5 @@ +# v2.13.2 + +## Bugfixes + +- fixed displaying annotation in `saneurl` diff --git a/e2e/checklist.md b/e2e/checklist.md index 5e740efa83d8e722ee51853383ce0ece19bb25da..8378541a0f49d69c4ca72bd7a48d9489b2a185cb 100644 --- a/e2e/checklist.md +++ b/e2e/checklist.md @@ -72,10 +72,13 @@ - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/whs4) redirects to waxholm v4 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/allen2017) redirects to allen 2017 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/mebrains) redirects to monkey +- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/stnr) redirects to URL that contains annotations + ## VIP URL - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/human) redirects to human mni152 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/monkey) redirects mebrains - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/rat) redirects to waxholm v4 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/mouse) redirects allen mouse 2017 + ## plugins - [ ] jugex plugin works diff --git a/mkdocs.yml b/mkdocs.yml index da0c7cd795225bb0141a01843c90337e00917ca3..af8c5c51fba201901758acb7b829ef8f572138f5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.13.2: 'releases/v2.13.2.md' - v2.13.1: 'releases/v2.13.1.md' - v2.13.0: 'releases/v2.13.0.md' - v2.12.5: 'releases/v2.12.5.md' diff --git a/package.json b/package.json index 4a5fc8a314db8c6cb5f450c4b3fea5296a457b07..8ad925aa39bb3becb37abef12627dcb34ed9c5ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siibra-explorer", - "version": "2.13.1", + "version": "2.13.2", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "lint": "eslint src --ext .ts", diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index d133671c3682ccf16c5af36271a12b90fbb4cf73..fd19915d2854db0302cff3f636316feacf075fc5 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -2,6 +2,7 @@ import { BehaviorSubject, Observable } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; import { getUuid, waitFor } from "src/util/fn"; import { PeriodicSvc } from "src/util/periodic.service"; +import { NehubaLayerControlService } from "src/viewerModule/nehuba/layerCtrl.service"; export type TNgAnnotationEv = { pickedAnnotationId: string @@ -54,6 +55,10 @@ interface NgAnnotationLayer { registerDisposer(fn: () => void): void } setVisible(flag: boolean): void + layerChanged: { + add(cb: () => void): void + } + visible: boolean } export class AnnotationLayer { @@ -109,11 +114,13 @@ export class AnnotationLayer { this.nglayer.layer.registerDisposer(() => { this.dispose() }) + NehubaLayerControlService.RegisterLayerName(this.name) } setVisible(flag: boolean){ this.nglayer && this.nglayer.setVisible(flag) } dispose() { + NehubaLayerControlService.DeregisterLayerName(this.name) AnnotationLayer.Map.delete(this.name) this._onHover.complete() while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 5fa7acf9251d3ced3358c2284d670b05ddaca300..f1e2e268977dfaaa31818a1d0fbea40b7034d8b3 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -2,12 +2,12 @@ import { Injectable, OnDestroy, Type } from "@angular/core"; import { ARIA_LABELS } from 'common/constants' import { Inject, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; +import { BehaviorSubject, combineLatest, from, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; import {map, switchMap, filter, shareReplay, pairwise, withLatestFrom } from "rxjs/operators"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TCallback, DESC_TYPE } from "./type"; -import { getExportNehuba, switchMapWaitFor } from "src/util/fn"; +import { getExportNehuba, switchMapWaitFor, retry } from "src/util/fn"; import { Polygon } from "./poly"; import { Line } from "./line"; import { Point } from "./point"; @@ -455,14 +455,13 @@ export class ModularUserAnnotationToolService implements OnDestroy{ store.pipe( select(atlasSelection.selectors.viewerMode), withLatestFrom(this.#voxelSize), - ).subscribe(([viewerMode, voxelSize]) => { - this.currMode = viewerMode - if (viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) { - if (this.annotationLayer) this.annotationLayer.setVisible(true) - else { - if (!voxelSize) throw new Error(`voxelSize of ${this.selectedTmpl.id} cannot be found!`) + switchMap(([viewerMode, voxelSize]) => from( + retry(() => { if (this.annotationLayer) { - this.annotationLayer.dispose() + return this.annotationLayer + } + if (!voxelSize) { + throw new Error(`voxelSize of ${this.selectedTmpl.id} cannot be found!`) } this.annotationLayer = new AnnotationLayer( ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME, @@ -479,15 +478,22 @@ export class ModularUserAnnotationToolService implements OnDestroy{ : null }) }) - /** - * on template changes, the layer gets lost - * force redraw annotations if layer needs to be recreated - */ - this.forcedAnnotationRefresh$.next(null) - } - } else { - if (this.annotationLayer) this.annotationLayer.setVisible(false) - } + + return this.annotationLayer + }) + ).pipe( + map(annotationLayer => ({viewerMode, voxelSize, annotationLayer})) + ) + ) + ).subscribe(({viewerMode, voxelSize, annotationLayer}) => { + this.currMode = viewerMode + + /** + * on template changes, the layer gets lost + * force redraw annotations if layer needs to be recreated + */ + this.forcedAnnotationRefresh$.next(null) + annotationLayer.setVisible(viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) }) ) diff --git a/src/util/fn.ts b/src/util/fn.ts index 45a4f4be56e38b52994c0d40319fa98ceef0ae2b..1508dc6303509477d45e744e28f23d5e1ae9d1eb 100644 --- a/src/util/fn.ts +++ b/src/util/fn.ts @@ -5,6 +5,21 @@ import { filter, mapTo, take } from 'rxjs/operators' // eslint-disable-next-line @typescript-eslint/no-empty-function export function noop(){} +export async function retry<T>(fn: () => T, config={timeout: 1000, retries:3}){ + let retryNo = 0 + const { retries, timeout } = config + while (retryNo < retries) { + retryNo ++ + try { + return await fn() + } catch (e) { + console.warn(`fn failed, retry after ${timeout} milliseconds`) + await (() => new Promise(rs => setTimeout(rs, timeout)))() + } + } + throw new Error(`fn failed ${retries} times, aborting`) +} + export async function getExportNehuba() { // eslint-disable-next-line no-constant-condition while (true) { diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index be87e8eeec2a8fad91596feadac14a1dde29da3f..dffa37da9a7f919967e83b9a3af061b1d3ce72a9 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -397,4 +397,22 @@ export class NehubaLayerControlService implements OnDestroy{ ]).pipe( map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName, ...AnnotationLayer.Map.keys()]) ) + + + static ExternalLayerNames = new Set<string>() + + /** + * @description Occationally, a layer can be managed by external components. Register the name of such layers so it will be ignored. + * @param layername + */ + static RegisterLayerName(layername: string) { + NehubaLayerControlService.ExternalLayerNames.add(layername) + } + /** + * @description Once external component is done with the layer, return control back to the service + * @param layername + */ + static DeregisterLayerName(layername: string) { + NehubaLayerControlService.ExternalLayerNames.delete(layername) + } } diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts index 1e18596e98fe06959628aa9186d2e2dd08981f98..d0e9069d3a17e0520ba2a4202ac3c20b132d01cb 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts @@ -61,12 +61,19 @@ export type TNgLayerCtrl<T extends keyof INgLayerCtrl> = { payload: INgLayerCtrl[T] } +export interface IExternalLayerCtl { + RegisterLayerName(layername: string): void + DeregisterLayerName(layername: string): void + readonly ExternalLayerNames: Set<string> +} + export const SET_COLORMAP_OBS = new InjectionToken<Observable<IColorMap>>('SET_COLORMAP_OBS') export const SET_LAYER_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_LAYER_VISIBILITY') export const SET_SEGMENT_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_SEGMENT_VISIBILITY') export const NG_LAYER_CONTROL = new InjectionToken<TNgLayerCtrl<keyof INgLayerCtrl>>('NG_LAYER_CONTROL') export const Z_TRAVERSAL_MULTIPLIER = new InjectionToken<Observable<number>>('Z_TRAVERSAL_MULTIPLIER') export const CURRENT_TEMPLATE_DIM_INFO = new InjectionToken<Observable<TemplateInfo>>('CURRENT_TEMPLATE_DIM_INFO') +export const EXTERNAL_LAYER_CONTROL = new InjectionToken<IExternalLayerCtl>("EXTERNAL_LAYER_CONTROL") export type TemplateInfo = { transform: number[][] diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 2a06e3f2d32b1427342a14e2f7f9480085cc506a..3bede3ea12c10cb8c7695cbef9a3ecfe1e966b0d 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -11,7 +11,7 @@ import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl. /** * import of nehuba js files moved to angular.json */ -import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util"; +import { EXTERNAL_LAYER_CONTROL, IExternalLayerCtl, INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util"; import { NgCoordinateSpace, Unit } from "../types"; import { PeriodicSvc } from "src/util/periodic.service"; import { ViewerInternalStateSvc, AUTO_ROTATE } from "src/viewerModule/viewerInternalState.service"; @@ -131,6 +131,7 @@ export class NehubaViewerUnit implements OnDestroy { @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>, @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>, @Optional() @Inject(Z_TRAVERSAL_MULTIPLIER) multiplier$: Observable<number>, + @Optional() @Inject(EXTERNAL_LAYER_CONTROL) private externalLayerCtrl: IExternalLayerCtl, @Optional() intViewerStateSvc: ViewerInternalStateSvc, ) { if (multiplier$) { @@ -261,7 +262,12 @@ export class NehubaViewerUnit implements OnDestroy { * on switch from freesurfer -> volumetric viewer, race con results in managed layer not necessarily setting layer visible correctly */ const managedLayers = this.nehubaViewer.ngviewer.layerManager.managedLayers - managedLayers.forEach(layer => layer.setVisible(false)) + managedLayers.forEach(layer => { + if (this.externalLayerCtrl && this.externalLayerCtrl.ExternalLayerNames.has(layer.name)) { + return + } + layer.setVisible(false) + }) for (const layerName of layerNames) { const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName) diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index c3bc899bd568f5242f37f4a90f2cf1f57f78579b..1f47b5680c88ac1c21faeca6a89104d4f3e54ce7 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -5,7 +5,7 @@ import { distinctUntilChanged } from "rxjs/operators"; import { IViewer, TViewerEvent } from "../../viewer.interface"; import { NehubaMeshService } from "../mesh.service"; import { NehubaLayerControlService, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"; -import { NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util"; +import { EXTERNAL_LAYER_CONTROL, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util"; import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; import { NehubaConfig } from "../config.service"; import { SET_MESHES_TO_LOAD } from "../constants"; @@ -25,6 +25,10 @@ import { atlasSelection, userInteraction } from "src/state"; useFactory: (meshService: NehubaMeshService) => meshService.loadMeshes$, deps: [ NehubaMeshService ] }, + { + provide: EXTERNAL_LAYER_CONTROL, + useValue: NehubaLayerControlService + }, NehubaMeshService, { provide: SET_COLORMAP_OBS,