diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b17f49748a407f3cbb7356a1875d37c65a82a82..30f7c157bab9534f4e1754e072de57338e18dbd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,16 +58,15 @@ jobs: backend: if: always() runs-on: ubuntu-latest - - env: - NODE_ENV: test - steps: - uses: actions/checkout@v3 - - name: Use Node.js 16.x - uses: actions/setup-node@v1 + - uses: actions/setup-python@v4 with: - node-version: 16.x + python-version: '3.10' - run: | - echo "busybody" - + cd backend + echo "hello world" >> index.html + export PATH_TO_PUBLIC=$(pwd) + pip install -r requirements.txt + pip install pytest + pytest diff --git a/backend/app/sane_url.py b/backend/app/sane_url.py index 947466933489b6509e6139df18d032864fbade43..29f14078e7aca259f2dc17c7815a74e9364c902e 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 @@ -27,6 +27,7 @@ vip_routes = [ class SaneUrlDPStore(DataproxyStore): class AlreadyExists(Exception): ... + class NotWritable(IOError): ... @staticmethod def GetTimeMs() -> int: @@ -36,8 +37,12 @@ class SaneUrlDPStore(DataproxyStore): def TransformKeyToObjName(key: str): return f"saneUrl/{key}.json" + writable = False + def __init__(self, expiry_s=3 * 24 * 60 * 60): - + if not (SXPLR_EBRAINS_IAM_SA_CLIENT_ID and SXPLR_EBRAINS_IAM_SA_CLIENT_SECRET): + super().__init__(None, SXPLR_BUCKET_NAME) + return resp = requests.get(f"{EBRAINS_IAM_DISCOVERY_URL}/.well-known/openid-configuration") resp.raise_for_status() resp_json = resp.json() @@ -54,6 +59,7 @@ class SaneUrlDPStore(DataproxyStore): self._refresh_token() super().__init__(self.token, SXPLR_BUCKET_NAME) + self.writable = True def _refresh_token(self): token_dict = self.session.fetch_token(self._token_endpoint, grant_type="client_credentials") @@ -94,7 +100,8 @@ class SaneUrlDPStore(DataproxyStore): raise SaneUrlDPStore.GenericException(str(e)) from e def set(self, key: str, value: Union[str, Dict], request: Optional[Request]=None): - + if not self.writable: + raise SaneUrlDPStore.NotWritable object_name = SaneUrlDPStore.TransformKeyToObjName(key) try: super().get(object_name) @@ -126,11 +133,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/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..bf8e4ff582ba9bad8361a33d53a8dcbd74aad89b --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +pythonpath = . +testpaths = + test_app diff --git a/backend/test_app/test_sane_url.py b/backend/test_app/test_sane_url.py new file mode 100644 index 0000000000000000000000000000000000000000..49ab5ec495c797a1cff592c6f5dd1850034f3232 --- /dev/null +++ b/backend/test_app/test_sane_url.py @@ -0,0 +1,13 @@ +from app.app import app +from fastapi.testclient import TestClient + +client = TestClient(app) + +def test_annotation_redirect(): + resp = client.get("/go/stnr", headers={ + "Accept": "text/html" + }, follow_redirects=False) + loc = resp.headers.get("Location") + assert loc, "Expected location header to be present, but was not" + assert "x-user-anntn:stnr" in loc, f"Expected the string 'x-user-anntn:stnr' in {loc!r}, but was not" + diff --git a/docs/releases/v2.13.2.md b/docs/releases/v2.13.2.md new file mode 100644 index 0000000000000000000000000000000000000000..0f4731c3be36ebba0e4e174bab3e3539055f52b0 --- /dev/null +++ b/docs/releases/v2.13.2.md @@ -0,0 +1,11 @@ +# v2.13.2 + +## Features + +- added visual display of errors relating to siibra-api + +## Bugfixes + +- fixed displaying annotation in `saneurl` +- fixed feature fetching spinner +- fixed feature fetching logic 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/sapi/sapi.service.spec.ts b/src/atlasComponents/sapi/sapi.service.spec.ts index 7232406894823ca0f75994ac0fbb2536e26b08e4..ba9a8bfca5540076b45c795c9a4156ece347a79a 100644 --- a/src/atlasComponents/sapi/sapi.service.spec.ts +++ b/src/atlasComponents/sapi/sapi.service.spec.ts @@ -53,12 +53,11 @@ describe("> sapi.service.ts", () => { }) }) it("> should call fetch twice", async () => { - expect(fetchSpy).toHaveBeenCalledTimes(2) + expect(fetchSpy).toHaveBeenCalledTimes(1) const allArgs = fetchSpy.calls.allArgs() - expect(allArgs.length).toEqual(2) + expect(allArgs.length).toEqual(1) expect(atlasEndpts).toContain(allArgs[0][0]) - expect(atlasEndpts).toContain(allArgs[1][0]) }) it("> endpoint should be set", async () => { @@ -67,11 +66,11 @@ describe("> sapi.service.ts", () => { it("> additional calls should return cached observable", () => { - expect(fetchSpy).toHaveBeenCalledTimes(2) + expect(fetchSpy).toHaveBeenCalledTimes(1) SAPI.BsEndpoint$.subscribe() SAPI.BsEndpoint$.subscribe() - expect(fetchSpy).toHaveBeenCalledTimes(2) + expect(fetchSpy).toHaveBeenCalledTimes(1) }) }) diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 7cd9209fb6299e521aebf324cc1e55265efbbfda..c3543e08c2ed155d20de261fc0eb74321dcf9bfd 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -5,13 +5,13 @@ import { getExportNehuba, noop } from "src/util/fn"; import { MatSnackBar } from "@angular/material/snack-bar"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { EnumColorMapName } from "src/util/colorMaps"; -import { forkJoin, from, NEVER, Observable, of, throwError } from "rxjs"; +import { BehaviorSubject, forkJoin, from, NEVER, Observable, of, throwError } from "rxjs"; import { environment } from "src/environments/environment" import { translateV3Entities } from "./translateV3" import { FeatureType, PathReturn, RouteParam, SapiRoute } from "./typeV3"; -import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature, Feature } from "./sxplrTypes"; +import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature } from "./sxplrTypes"; import { parcBanList, speciesOrder } from "src/util/constants"; import { CONST } from "common/constants" @@ -110,15 +110,6 @@ export class SAPI{ BS_ENDPOINT_CACHED_VALUE = new Observable<string>(obs => { (async () => { - const backupPr = new Promise<string>(rs => { - for (const endpt of backupEndpoints) { - SAPI.VerifyEndpoint(endpt) - .then(flag => { - if (flag) rs(endpt) - }) - .catch(noop) - } - }) try { const url = await Promise.race([ SAPI.VerifyEndpoint(mainEndpoint), @@ -129,12 +120,23 @@ export class SAPI{ try { const url = await Promise.race([ - backupPr, + /** + * find the first endpoint of the backup endpoints + */ + new Promise<string>(rs => { + for (const endpt of backupEndpoints) { + SAPI.VerifyEndpoint(endpt) + .then(flag => { + if (flag) rs(endpt) + }) + .catch(noop) + } + }), new Promise<string>((_, rj) => setTimeout(() => rj(`5s timeout`), 5000)) ]) obs.next(url) } catch (e) { - SAPI.ErrorMessage = `No usabe mirror found` + SAPI.ErrorMessage = `No usabe siibra-api endpoints found. Tried: ${mainEndpoint}, ${backupEndpoints.join(",")}` } } finally { obs.complete() @@ -147,7 +149,10 @@ export class SAPI{ return BS_ENDPOINT_CACHED_VALUE } - static ErrorMessage = null + static ErrorMessage$ = new BehaviorSubject<string>(null) + static set ErrorMessage(val: string){ + this.ErrorMessage$.next(val) + } getParcRegions(parcId: string) { const param = { @@ -187,40 +192,6 @@ export class SAPI{ if (!!resp.total || resp.total === 0) return true return false } - getV3Features<T extends FeatureType>(featureType: T, sapiParam: RouteParam<`/feature/${T}`>): Observable<Feature[]> { - const query = structuredClone(sapiParam) - return this.v3Get<`/feature/${T}`>(`/feature/${featureType}`, { - ...query - }).pipe( - switchMap(resp => { - if (!this.#isPaged(resp)) return throwError(`endpoint not returning paginated response`) - return this.iteratePages( - resp, - page => { - const query = structuredClone(sapiParam) - query.query.page = page - return this.v3Get(`/feature/${featureType}`, { - ...query, - }).pipe( - map(val => { - if (this.#isPaged(val)) return val - return { items: [], total: 0, page: 0, size: 0 } - }) - ) - } - ) - }), - switchMap(features => features.length === 0 - ? of([]) - : forkJoin( - features.map(feat => translateV3Entities.translateFeature(feat) ) - ) - ), - catchError((err) => { - console.error("Error fetching features", err) - return of([])}), - ) - } getV3FeatureDetail<T extends FeatureType>(featureType: T, sapiParam: RouteParam<`/feature/${T}/{feature_id}`>): Observable<PathReturn<`/feature/${T}/{feature_id}`>> { return this.v3Get<`/feature/${T}/{feature_id}`>(`/feature/${featureType}/{feature_id}`, { @@ -248,12 +219,6 @@ export class SAPI{ ) } - getModalities() { - return this.v3Get("/feature/_types", { query: {} }).pipe( - map(v => v.items) - ) - } - v3GetRoute<T extends SapiRoute>(route: T, sapiParam: RouteParam<T>) { const params: Record<string, string|number> = "query" in sapiParam ? sapiParam["query"] : {} const _path: Record<string, string|number> = "path" in sapiParam ? sapiParam["path"] : {} @@ -593,9 +558,6 @@ export class SAPI{ private snackbar: MatSnackBar, private workerSvc: AtlasWorkerService, ){ - if (SAPI.ErrorMessage) { - this.snackbar.open(SAPI.ErrorMessage, 'Dismiss', { duration: 5000 }) - } } /** diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 5fa7acf9251d3ced3358c2284d670b05ddaca300..bb0b092d7a025a191f6cced5c506b70a6a9d8143 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, annotationLayer})) + ) + ) + ).subscribe(({viewerMode, 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/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index c127379a8ee112b36de6e104eaa8da3c62eb04b2..1fc4e3a606a4ae87698701fd2e0e60fb38f6ae75 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -27,6 +27,7 @@ import { DOCUMENT } from "@angular/common"; import { userPreference } from "src/state" import { DARKTHEME } from "src/util/injectionTokens"; import { EnumQuickTourSeverity } from "src/ui/quickTour/constrants"; +import { SAPI } from "src/atlasComponents/sapi"; @Component({ @@ -44,6 +45,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public CONST = CONST + sapiError$ = SAPI.ErrorMessage$ + @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef<any> @ViewChild(MouseHoverDirective) private mouseOverNehuba: MouseHoverDirective diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 864976bc6901450056408aecc096df0c0254e7ce..cade469e7db2cd56594c21a6b6aa06f069722f48 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -1,7 +1,10 @@ <ng-container *ngIf="meetsRequirement; else doesNotMeetReqTemplate"> - <ng-container *ngTemplateOutlet="viewerBody"> - </ng-container> + <ng-template [ngIf]="sapiError$ | async" let-data [ngIfElse]="viewerBody"> + <ng-template [ngTemplateOutlet]="errorTmpl" [ngTemplateOutletContext]="{ message: data }"> + </ng-template> + </ng-template> + </ng-container> <!-- cookie --> @@ -56,3 +59,8 @@ <ng-template #emptyArrowTmpl> </ng-template> + +<ng-template #errorTmpl let-message="message"> + <not-supported-component [errorString]="message"> + </not-supported-component> +</ng-template> diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts index 73156e7af17e6cf4e39e97fca101655eb02b07bb..a6f34d27d9b68532408780eb774ecb707718fcdf 100644 --- a/src/features/category-acc.directive.ts +++ b/src/features/category-acc.directive.ts @@ -144,6 +144,12 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { ).subscribe(async ({ total, current, ds }) => { if (total > current && current < 50) { try { + /** + * TODO Interaction between ParentDataSource and ListDatadirective, which both pulls seems + * to weirdly interact with each other. + * For now, pulling twice seems to solve the issue + */ + await ds.pull() await ds.pull() } catch (e) { // if already pulling, ignore diff --git a/src/notSupportedCmp/notSupported.component.ts b/src/notSupportedCmp/notSupported.component.ts index 6562a46a650f2c2c8b01caa6b1fc174d7251157f..f17b70e3fa288c434c047c76efc2f201e621105b 100644 --- a/src/notSupportedCmp/notSupported.component.ts +++ b/src/notSupportedCmp/notSupported.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { interval, merge, of } from "rxjs"; import { map } from "rxjs/operators"; import { UNSUPPORTED_INTERVAL, UNSUPPORTED_PREVIEW } from "src/util/constants"; @@ -13,6 +13,14 @@ import { MIN_REQ_EXPLAINER } from 'src/util/constants' }) export class NotSupportedCmp{ + + /** + * default error is webgl + * custom error message can be inputed to override default message + */ + @Input() + errorString="webgl" + public unsupportedPreviews: any[] = UNSUPPORTED_PREVIEW public unsupportedPreviewIdx: number = 0 public MIN_REQ_EXPLAINER = MIN_REQ_EXPLAINER diff --git a/src/notSupportedCmp/notSupported.template.html b/src/notSupportedCmp/notSupported.template.html index db0d8020b24ace07d3e798df155ba118074aaecb..c46e667a4c2d8b8a40496cccc1a36f2e22afdad1 100644 --- a/src/notSupportedCmp/notSupported.template.html +++ b/src/notSupportedCmp/notSupported.template.html @@ -1,5 +1,5 @@ <div class="jumbotron bg-light text-center mb-0"> - <div> + <div *ngIf="errorString === 'webgl' else otherErrorTmpl"> <h1 class="mb-3"> <i class="fas fa-exclamation-triangle"></i> Unsupported browser detected </h1> @@ -21,6 +21,12 @@ </div> </div> + + <ng-template #otherErrorTmpl> + <h1 class="mb-3"> + <i class="fas fa-exclamation-triangle"></i> {{ errorString }} + </h1> + </ng-template> </div> <ng-container *ngFor="let preview of unsupportedPreviews; let idx = index"> <div [hidden]="idx !== unsupportedPreviewIdx" class="text-center mb-3 image-container" @@ -31,4 +37,4 @@ </div> </div> </div> -</ng-container> \ No newline at end of file +</ng-container> 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/util/pullable.ts b/src/util/pullable.ts index 6a5d87e96701b3c9c41553ce66c0f611054c0afa..02bf6dc4481a62c9189e90601781c885f1451623 100644 --- a/src/util/pullable.ts +++ b/src/util/pullable.ts @@ -70,8 +70,14 @@ export class PulledDataSource<T> extends DataSource<T> { return [] } this.isPulling = true - const newResults = await this.#pull() - this.isPulling = false + let newResults = [] + try { + newResults = await this.#pull() + } catch (e) { + console.error("Pulling failed", e) + } finally { + this.isPulling = false + } if (newResults.length === 0) { this.complete() } 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,