From b15998aebc3f9339d7eb9b0d13bf5b8146793a3b Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Mon, 18 Sep 2023 10:34:09 +0200
Subject: [PATCH] fix: annotation in saneurl

---
 backend/app/sane_url.py                       | 14 +++++--
 docs/releases/v2.13.2.md                      |  5 +++
 e2e/checklist.md                              |  3 ++
 mkdocs.yml                                    |  1 +
 package.json                                  |  2 +-
 .../annotations/annotation.service.ts         |  7 ++++
 .../userAnnotations/tools/service.ts          | 42 +++++++++++--------
 src/util/fn.ts                                | 15 +++++++
 .../layerCtrl.service/layerCtrl.service.ts    | 18 ++++++++
 .../layerCtrl.service/layerCtrl.util.ts       |  7 ++++
 .../nehubaViewer/nehubaViewer.component.ts    | 10 ++++-
 .../nehubaViewerGlue.component.ts             |  6 ++-
 12 files changed, 105 insertions(+), 25 deletions(-)
 create mode 100644 docs/releases/v2.13.2.md

diff --git a/backend/app/sane_url.py b/backend/app/sane_url.py
index 947466933..dc4a4e347 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 000000000..d0fe9ede6
--- /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 5e740efa8..8378541a0 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 da0c7cd79..af8c5c51f 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 4a5fc8a31..8ad925aa3 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 d133671c3..fd19915d2 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 5fa7acf92..f1e2e2689 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 45a4f4be5..1508dc630 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 be87e8eee..dffa37da9 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 1e18596e9..d0e9069d3 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 2a06e3f2d..3bede3ea1 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 c3bc899bd..1f47b5680 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,
-- 
GitLab