diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f084ac2010fe12bb85d640c634fdda21594c2c04..0ee38faacdd75f02e4d4c8567b72065c7d9ef719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - run: | if [[ "$GITHUB_REF" = *hotfix* ]] || [[ "$GITHUB_REF" = refs/heads/staging ]] then - export SIIBRA_API_ENDPOINTS=https://siibra-api-rc.apps.hbp.eu/v2_0 + export SIIBRA_API_ENDPOINTS=https://siibra-api-rc.apps.hbp.eu/v2_0,https://siibra-api-rc.apps.jsc.hbp.eu/v2_0 node src/environments/parseEnv.js ./environment.ts fi npm run test-ci diff --git a/docs/releases/v2.7.4.md b/docs/releases/v2.7.4.md new file mode 100644 index 0000000000000000000000000000000000000000..d214b2f110f3848759ec885f36821072f051b987 --- /dev/null +++ b/docs/releases/v2.7.4.md @@ -0,0 +1,6 @@ +# v2.7.4 + +## Bugfix + +- Properly use fallback when detecting fault +- Minor wording/cosmetic change diff --git a/mkdocs.yml b/mkdocs.yml index 645c2483972a50115c13db5bfeecb40e4489fc6d..7fce47cfb860fd0fa5cd67382445bdd098b2bde4 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.7.4: 'releases/v2.7.4.md' - v2.7.3: 'releases/v2.7.3.md' - v2.7.2: 'releases/v2.7.2.md' - v2.7.1: 'releases/v2.7.1.md' diff --git a/package.json b/package.json index dd637a495824c81008750313065fd1866e01dadf..0890653e938f23546b5a2173c2ee93c3e8ab75d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.7.3", + "version": "2.7.4", "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/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts index 4767cc2897ab0ed6c6567f3ee727ad2f73fece04..085c5a82b8136901136593aa0b2933d992efb22e 100644 --- a/src/atlasComponents/sapi/core/sapiParcellation.ts +++ b/src/atlasComponents/sapi/core/sapiParcellation.ts @@ -1,4 +1,5 @@ import { Observable } from "rxjs" +import { switchMap } from "rxjs/operators" import { SapiVolumeModel } from ".." import { SAPI } from "../sapi.service" import {SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionModel} from "../type" @@ -20,43 +21,53 @@ export class SAPIParcellation{ } getDetail(queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationModel>{ - return this.sapi.httpGet<SapiParcellationModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`, - null, - queryParam + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiParcellationModel>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`, + null, + queryParam + )) ) } getRegions(spaceId: string, queryParam?: SapiQueryPriorityArg): Observable<SapiRegionModel[]> { - return this.sapi.httpGet<SapiRegionModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, - { - space_id: spaceId - }, - queryParam + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiRegionModel[]>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, + { + space_id: spaceId + }, + queryParam + )) ) } getVolumes(): Observable<SapiVolumeModel[]>{ - return this.sapi.httpGet<SapiVolumeModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes` - ) + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiVolumeModel[]>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes` + )) + ) } getFeatures(parcPagination?: ParcellationPaginationQuery, queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationFeatureModel[]> { - return this.sapi.httpGet<SapiParcellationFeatureModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`, - { - type: parcPagination?.type, - size: parcPagination?.size?.toString() || '5', - page: parcPagination?.page.toString() || '0', - }, - queryParam + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiParcellationFeatureModel[]>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`, + { + type: parcPagination?.type, + size: parcPagination?.size?.toString() || '5', + page: parcPagination?.page.toString() || '0', + }, + queryParam + )) ) } getFeatureInstance(instanceId: string): Observable<SapiParcellationFeatureModel> { - return this.sapi.http.get<SapiParcellationFeatureModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.http.get<SapiParcellationFeatureModel>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, + )) ) } } diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index d7b82c7d918d3e8209ead4d8033a6a4f12965605..d9263cea03f0d7045e3a1f5043f8a4edc2209929 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -2,7 +2,7 @@ import { SAPI } from ".."; import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type"; import { strToRgb, hexToRgb } from 'common/util' import { merge, Observable, of } from "rxjs"; -import { catchError, map, scan } from "rxjs/operators"; +import { catchError, map, scan, switchMap } from "rxjs/operators"; export class SAPIRegion{ @@ -16,7 +16,7 @@ export class SAPIRegion{ return strToRgb(JSON.stringify(region)) } - private prefix: string + private prefix$: Observable<string> constructor( private sapi: SAPI, @@ -24,20 +24,26 @@ export class SAPIRegion{ public parcId: string, public id: string, ){ - this.prefix = `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}` + this.prefix$ = SAPI.BsEndpoint$.pipe( + map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}`) + ) } getFeatures(spaceId: string): Observable<(SapiRegionalFeatureModel | CleanedIeegDataset)[]> { return merge( - this.sapi.httpGet<SapiRegionalFeatureModel[]>( - `${this.prefix}/features`, - { - space_id: spaceId - } - ).pipe( - catchError((err, obs) => { - return of([]) - }) + this.prefix$.pipe( + switchMap(prefix => + this.sapi.httpGet<SapiRegionalFeatureModel[]>( + `${prefix}/features`, + { + space_id: spaceId + } + ).pipe( + catchError((err, obs) => { + return of([]) + }) + ) + ) ), spaceId ? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe( @@ -56,50 +62,59 @@ export class SAPIRegion{ } getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> { - return this.sapi.httpGet<SapiRegionalFeatureModel>( - `${this.prefix}/features/${encodeURIComponent(instanceId)}`, - { - space_id: spaceId - } + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiRegionalFeatureModel>( + `${prefix}/features/${encodeURIComponent(instanceId)}`, + { + space_id: spaceId + } + )) ) } getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> { - return this.sapi.http.get<SapiRegionMapInfoModel>( - `${this.prefix}/regional_map/info`, - { - params: { - space_id: spaceId + return this.prefix$.pipe( + switchMap(prefix => this.sapi.http.get<SapiRegionMapInfoModel>( + `${prefix}/regional_map/info`, + { + params: { + space_id: spaceId + } } - } + )) ) } - getMapUrl(spaceId: string): string { - return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}` + getMapUrl(spaceId: string): Observable<string> { + return this.prefix$.pipe( + map(prefix => `${prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`) + ) } getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{ - const url = `${this.prefix}/volumes` - return this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>( - url + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>( + `${prefix}/volumes` + )) ) } getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> { - const url = `${this.prefix}/volumes/${encodeURIComponent(volumeId)}` - return this.sapi.httpGet<SapiVolumeModel>( - url + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiVolumeModel>( + `${prefix}/volumes/${encodeURIComponent(volumeId)}` + )) ) } getDetail(spaceId: string): Observable<SapiRegionModel> { - const url = `${this.prefix}` - return this.sapi.httpGet<SapiRegionModel>( - url, - { - space_id: spaceId - } + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiRegionModel>( + prefix, + { + space_id: spaceId + } + )) ) } } diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts index 3effd6f16345aa82eebd3cf94b9f83118fb9546e..5f61ae6f68240437d2a9510b4744d64d16e54977 100644 --- a/src/atlasComponents/sapi/core/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -2,6 +2,7 @@ import { Observable } from "rxjs" import { SAPI } from '../sapi.service' import { camelToSnake } from 'common/util' import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type" +import { map, switchMap } from "rxjs/operators" type FeatureResponse = { features: { @@ -22,13 +23,21 @@ type SpatialFeatureOpts = RegionalSpatialFeatureOpts | BBoxSpatialFEatureOpts export class SAPISpace{ - constructor(private sapi: SAPI, public atlasId: string, public id: string){} + constructor(private sapi: SAPI, public atlasId: string, public id: string){ + this.prefix$ = SAPI.BsEndpoint$.pipe( + map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`) + ) + } + + private prefix$: Observable<string> getModalities(param?: SapiQueryPriorityArg): Observable<FeatureResponse> { - return this.sapi.httpGet<FeatureResponse>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, - null, - param + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<FeatureResponse>( + `${prefix}/features`, + null, + param + )) ) } @@ -37,9 +46,11 @@ export class SAPISpace{ for (const [key, value] of Object.entries(opts)) { query[camelToSnake(key)] = value } - return this.sapi.httpGet<SapiSpatialFeatureModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, - query + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel[]>( + `${prefix}/features`, + query + )) ) } @@ -48,23 +59,29 @@ export class SAPISpace{ for (const [key, value] of Object.entries(opts)) { query[camelToSnake(key)] = value } - return this.sapi.httpGet<SapiSpatialFeatureModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, - query + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel>( + `${prefix}/features/${encodeURIComponent(instanceId)}`, + query + )) ) } getDetail(param?: SapiQueryPriorityArg): Observable<SapiSpaceModel>{ - return this.sapi.httpGet<SapiSpaceModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`, - null, - param + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiSpaceModel>( + `${prefix}`, + null, + param + )) ) } getVolumes(): Observable<SapiVolumeModel[]>{ - return this.sapi.httpGet<SapiVolumeModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/volumes`, + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiVolumeModel[]>( + `${prefix}/volumes`, + )) ) } } diff --git a/src/atlasComponents/sapi/features/sapiFeature.ts b/src/atlasComponents/sapi/features/sapiFeature.ts index 8290da0cad9b4c8e057845258980ff5091579272..f2f341acce3aca72ba481f3eda6c68083320cfe1 100644 --- a/src/atlasComponents/sapi/features/sapiFeature.ts +++ b/src/atlasComponents/sapi/features/sapiFeature.ts @@ -1,3 +1,4 @@ +import { switchMap } from "rxjs/operators"; import { SAPI } from "../sapi.service"; import { SapiFeatureModel } from "../type"; @@ -6,8 +7,10 @@ export class SAPIFeature { } - public detail$ = this.sapi.httpGet<SapiFeatureModel>( - `${SAPI.BsEndpoint}/features/${this.id}`, - this.opts + public detail$ = SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiFeatureModel>( + `${endpt}/features/${this.id}`, + this.opts + )) ) } diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts index 6d0757bb5e771b5bb18e9009bc9032f399fa0c38..9b9efaf0ee92240c0b27b57b5067acf02dcbd972 100644 --- a/src/atlasComponents/sapi/module.ts +++ b/src/atlasComponents/sapi/module.ts @@ -1,5 +1,4 @@ -import { APP_INITIALIZER, NgModule } from "@angular/core"; -import { SAPI } from "./sapi.service"; +import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { PriorityHttpInterceptor } from "src/util/priority"; @@ -16,16 +15,10 @@ import { MatSnackBarModule } from "@angular/material/snack-bar"; exports: [ ], providers: [ - SAPI, { provide: HTTP_INTERCEPTORS, useClass: PriorityHttpInterceptor, multi: true - }, - { - provide: APP_INITIALIZER, - useValue: () => SAPI.SetBsEndPoint(), - multi: true } ] }) diff --git a/src/atlasComponents/sapi/sapi.service.spec.ts b/src/atlasComponents/sapi/sapi.service.spec.ts index 0448de0ccf6c7c2fcb3d5cd587e996db861a7bf2..20233eca63ce2485db0136ee3702cace46659e96 100644 --- a/src/atlasComponents/sapi/sapi.service.spec.ts +++ b/src/atlasComponents/sapi/sapi.service.spec.ts @@ -1,10 +1,10 @@ -import { NEVER } from "rxjs" +import { finalize } from "rxjs/operators" import * as env from "src/environments/environment" import { SAPI } from "./sapi.service" describe("> sapi.service.ts", () => { describe("> SAPI", () => { - describe("#SetBsEndPoint", () => { + describe("#BsEndpoint$", () => { let fetchSpy: jasmine.Spy let environmentSpy: jasmine.Spy @@ -14,16 +14,10 @@ describe("> sapi.service.ts", () => { const atlas1 = 'foo' const atlas2 = 'bar' - let originalBsEndpoint: string - beforeAll(() => { - originalBsEndpoint = SAPI.BsEndpoint - }) + let subscribedVal: string - afterAll(() => { - SAPI.BsEndpoint = originalBsEndpoint - }) - beforeEach(() => { + SAPI.ClearBsEndPoint() fetchSpy = spyOn(window, 'fetch') fetchSpy.and.callThrough() @@ -33,43 +27,70 @@ describe("> sapi.service.ts", () => { }) }) + afterEach(() => { + SAPI.ClearBsEndPoint() fetchSpy.calls.reset() environmentSpy.calls.reset() + subscribedVal = null }) describe("> first passes", () => { - beforeEach(() => { + beforeEach(done => { const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 }) - fetchSpy.and.resolveTo(resp) + fetchSpy.and.callFake(async url => { + if (url === `${endpt1}/atlases`) { + return resp + } + throw new Error("controlled throw") + }) + SAPI.BsEndpoint$.pipe( + finalize(() => done()) + ).subscribe(val => { + subscribedVal = val + }) }) - it("> should call fetch once", async () => { - await SAPI.SetBsEndPoint() - expect(fetchSpy).toHaveBeenCalledTimes(1) - expect(fetchSpy).toHaveBeenCalledOnceWith(`${endpt1}/atlases`) + it("> should call fetch twice", async () => { + expect(fetchSpy).toHaveBeenCalledTimes(2) + + const allArgs = fetchSpy.calls.allArgs() + expect(allArgs.length).toEqual(2) + expect(allArgs[0]).toEqual([`${endpt1}/atlases`]) + expect(allArgs[1]).toEqual([`${endpt2}/atlases`]) }) it("> endpoint should be set", async () => { - await SAPI.SetBsEndPoint() - expect(SAPI.BsEndpoint).toBe(endpt1) + expect(subscribedVal).toBe(endpt1) + }) + + it("> additional calls should return cached observable", () => { + + expect(fetchSpy).toHaveBeenCalledTimes(2) + SAPI.BsEndpoint$.subscribe() + SAPI.BsEndpoint$.subscribe() + + expect(fetchSpy).toHaveBeenCalledTimes(2) }) }) describe("> first fails", () => { - beforeEach(() => { - let counter = 0 - fetchSpy.and.callFake(async () => { - if (counter === 0) { - counter ++ + beforeEach(done => { + fetchSpy.and.callFake(async url => { + if (url === `${endpt1}/atlases`) { throw new Error(`bla`) } const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 }) return resp }) + + SAPI.BsEndpoint$.pipe( + finalize(() => done()) + ).subscribe(val => { + subscribedVal = val + }) }) it("> should call twice", async () => { - await SAPI.SetBsEndPoint() expect(fetchSpy).toHaveBeenCalledTimes(2) expect(fetchSpy.calls.allArgs()).toEqual([ [`${endpt1}/atlases`], @@ -78,18 +99,7 @@ describe("> sapi.service.ts", () => { }) it('> should set endpt2', async () => { - await SAPI.SetBsEndPoint() - expect(SAPI.BsEndpoint).toBe(endpt2) - }) - - it("> instances bsendpoint should be the updated version", async () => { - await SAPI.SetBsEndPoint() - const mockHttpClient = { - get: jasmine.createSpy() - } - mockHttpClient.get.and.returnValue(NEVER) - const sapi = new SAPI(mockHttpClient as any, null, null) - expect(sapi.bsEndpoint).toBe(endpt2) + expect(subscribedVal).toBe(endpt2) }) }) }) diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 394acc033c64d0b3567024c4f65f1ddc53dfc3c1..ef3dc46e1e4dd37e1b50206f5b070bc562a270e5 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { HttpClient } from '@angular/common/http'; -import { map, shareReplay } from "rxjs/operators"; +import { catchError, filter, map, shareReplay, switchMap, take, tap } from "rxjs/operators"; import { SAPIAtlas, SAPISpace } from './core' import { SapiAtlasModel, @@ -20,7 +20,7 @@ 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"; +import { concat, EMPTY, from, merge, Observable, of } from "rxjs"; import { SAPIFeature } from "./features"; import { environment } from "src/environments/environment" @@ -29,34 +29,56 @@ export const SIIBRA_API_VERSION = '0.2.2' type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation -@Injectable() +let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null + +@Injectable({ + providedIn: 'root' +}) export class SAPI{ - static async SetBsEndPoint() { - let idx = 0 - const siibraApiEndpts = environment.SIIBRA_API_ENDPOINTS.split(',') - while (idx < siibraApiEndpts.length) { - const url = siibraApiEndpts[idx] - try { - const resp = await fetch(`${url}/atlases`) - const atlases = await resp.json() - if (atlases.length > 0) { - SAPI.BsEndpoint = url - return - } - } catch (e) { - idx ++ - } - } - SAPI.ErrorMessage = `It appears all of our mirrors are not working. The viewer may not be working properly...` + /** + * Used to clear BsEndPoint, so the next static get BsEndpoints$ will + * fetch again. Only used for unit test of BsEndpoint$ + */ + static ClearBsEndPoint(){ + BS_ENDPOINT_CACHED_VALUE = null } - static ErrorMessage = null - static BsEndpoint = `https://siibra-api-rc.apps.hbp.eu/v2_0` - - get bsEndpoint() { - return SAPI.BsEndpoint + /** + * BsEndpoint$ is designed as a static getter mainly for unit testing purposes. + * see usage of BsEndpoint$ and ClearBsEndPoint in sapi.service.spec.ts + */ + static get BsEndpoint$(): Observable<string> { + if (!!BS_ENDPOINT_CACHED_VALUE) return BS_ENDPOINT_CACHED_VALUE + BS_ENDPOINT_CACHED_VALUE = concat( + merge( + ...environment.SIIBRA_API_ENDPOINTS.split(',').map(url => { + return from((async () => { + const resp = await fetch(`${url}/atlases`) + const atlases = await resp.json() + if (atlases.length == 0) { + throw new Error(`atlas length == 0`) + } + return url + })()).pipe( + catchError(() => EMPTY) + ) + }) + ), + of(null).pipe( + tap(() => { + SAPI.ErrorMessage = `It appears all of our mirrors are not working. The viewer may not be working properly...` + }), + filter(() => false) + ) + ).pipe( + take(1), + shareReplay(1), + ) + return BS_ENDPOINT_CACHED_VALUE } + + static ErrorMessage = null registry = { _map: {} as Record<string, { @@ -116,7 +138,9 @@ export class SAPI{ } getModalities(): Observable<SapiModalityModel[]> { - return this.http.get<SapiModalityModel[]>(`${SAPI.BsEndpoint}/modalities`) + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.http.get<SapiModalityModel[]>(`${endpt}/modalities`)) + ) } httpGet<T>(url: string, params?: Record<string, string>, sapiParam?: SapiQueryPriorityArg){ @@ -133,12 +157,13 @@ export class SAPI{ ) } - public atlases$ = this.http.get<SapiAtlasModel[]>( - `${this.bsEndpoint}/atlases`, - { - observe: "response" - } - ).pipe( + public atlases$ = SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.http.get<SapiAtlasModel[]>( + `${endpt}/atlases`, + { + observe: "response" + } + )), map(resp => { const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY) if (respVersion !== SIIBRA_API_VERSION) { diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts index 1fa58bef89b562d5be3ad8ef5383c302bb335649..57e28f20da83acf96a247117f70fb921998c3bf0 100644 --- a/src/atlasComponents/sapi/stories.base.ts +++ b/src/atlasComponents/sapi/stories.base.ts @@ -67,22 +67,27 @@ export const parcId = { } export async function getAtlases(): Promise<SapiAtlasModel[]> { - return await (await fetch(`${SAPI.BsEndpoint}/atlases`)).json() as SapiAtlasModel[] + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases`)).json() as SapiAtlasModel[] } export async function getAtlas(id: string): Promise<SapiAtlasModel>{ - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${id}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${id}`)).json() } export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{ - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId}/parcellations/${id}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json() } export async function getParcRegions(atlasId: string, id: string, spaceId: string): Promise<SapiRegionModel[]>{ - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> { - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId}/spaces/${id}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json() } export async function getHumanAtlas(): Promise<SapiAtlasModel> { @@ -90,7 +95,8 @@ export async function getHumanAtlas(): Promise<SapiAtlasModel> { } export async function getMni152(): Promise<SapiSpaceModel> { - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json() } export async function getJba29(): Promise<SapiParcellationModel> { @@ -103,33 +109,41 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> { export async function getHoc1Right(spaceId=null): Promise<SapiRegionModel> { if (!spaceId) { - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json() } - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function get44Left(spaceId=null): Promise<SapiRegionModel> { if (!spaceId) { - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json() } - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function getHoc1RightSpatialFeatures(): Promise<SxplrCleanedFeatureModel[]> { - const json: SapiSpatialFeatureModel[] = await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9®ion=hoc1%20right`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + const json: SapiSpatialFeatureModel[] = await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9®ion=hoc1%20right`)).json() return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession")) } export async function getHoc1RightFeatures(): Promise<SapiRegionalFeatureModel[]> { - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json() } export async function getHoc1RightFeatureDetail(featId: string): Promise<SapiRegionalFeatureModel>{ - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json() } export async function getJba29Features(): Promise<SapiParcellationFeatureModel[]> { - return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json() } export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureModel[]>{ @@ -137,14 +151,16 @@ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureMo [-1000, -1000, -1000], [1000, 1000, 1000] ] - const url = new URL(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`) + const endPt = await SAPI.BsEndpoint$.toPromise() + const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`) url.searchParams.set(`bbox`, JSON.stringify(bbox)) return await (await fetch(url.toString())).json() } export async function getMni152SpatialFeatureHoc1Right(): Promise<SapiSpatialFeatureModel[]>{ - const url = new URL(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`) + const endPt = await SAPI.BsEndpoint$.toPromise() + const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`) url.searchParams.set(`parcellation_id`, parcId.human.jba29) url.searchParams.set("region", 'hoc1 right') return await (await fetch(url.toString())).json() diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts index 972e411795a3ac9e3a862a3cb61bc7d403f1a105..ed2edbf53711f0c84c1b024c24b46b2e30934f63 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts @@ -3,11 +3,12 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service" import { SapiParcellationModel } from "src/atlasComponents/sapi/type" import { getTraverseFunctions } from "./parcellationVersion.pipe" -describe(`parcellationVersion.pipe.ts (endpoint at ${SAPI.BsEndpoint})`, () => { +describe(`parcellationVersion.pipe.ts`, () => { describe("getTraverseFunctions", () => { let julichBrainParcellations: SapiParcellationModel[] = [] beforeAll(async () => { - const res = await fetch(`${SAPI.BsEndpoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`) + const bsEndPoint = await SAPI.BsEndpoint$.toPromise() + const res = await fetch(`${bsEndPoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`) const arr: SapiParcellationModel[] = await res.json() julichBrainParcellations = arr.filter(it => /Julich-Brain Cytoarchitectonic Maps/.test(it.name)) }) diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html index c15b1d11c2b503b4023ec2b46df3d163bb5f7dab..05899973a89bf3d63282cb4e63fdc2f077318085 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html @@ -126,7 +126,7 @@ color="primary"> <div class="fas fa-external-link-alt"></div> <span> - Explore + Dataset Detail </span> </a> diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index 579f7fb79ab3e65547cc420391f2097587eeff18..6886634b6e6c02a6556dcaa8a18c3f4729b7b71a 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -37,17 +37,20 @@ export class LayerCtrlEffects { ), switchMap(([ regions, { atlas, parcellation, template } ]) => { const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name) - return sapiRegion.getMapInfo(template["@id"]).pipe( - map(val => + return forkJoin([ + sapiRegion.getMapInfo(template["@id"]), + sapiRegion.getMapUrl(template["@id"]) + ]).pipe( + map(([mapInfo, mapUrl]) => atlasAppearance.actions.addCustomLayer({ customLayer: { clType: "customlayer/nglayer", id: PMAP_LAYER_NAME, - source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`, + source: `nifti://${mapUrl}`, shader: getShader({ colormap: EnumColorMapName.VIRIDIS, - highThreshold: val.max, - lowThreshold: val.min, + highThreshold: mapInfo.max, + lowThreshold: mapInfo.min, removeBg: true, }) }