Skip to content
Snippets Groups Projects
Unverified Commit a58f279b authored by xgui3783's avatar xgui3783 Committed by GitHub
Browse files

Merge pull request #1224 from FZJ-INM1-BDA/hotfix_sapiFallback

hotfix: sapi endpoint fallback
parents e28e34fe a15b314b
No related branches found
No related tags found
No related merge requests found
Showing
with 283 additions and 182 deletions
...@@ -39,7 +39,7 @@ jobs: ...@@ -39,7 +39,7 @@ jobs:
- run: | - run: |
if [[ "$GITHUB_REF" = *hotfix* ]] || [[ "$GITHUB_REF" = refs/heads/staging ]] if [[ "$GITHUB_REF" = *hotfix* ]] || [[ "$GITHUB_REF" = refs/heads/staging ]]
then 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 node src/environments/parseEnv.js ./environment.ts
fi fi
npm run test-ci npm run test-ci
......
# v2.7.4
## Bugfix
- Properly use fallback when detecting fault
- Minor wording/cosmetic change
...@@ -33,6 +33,7 @@ nav: ...@@ -33,6 +33,7 @@ nav:
- Fetching datasets: 'advanced/datasets.md' - Fetching datasets: 'advanced/datasets.md'
- Display non-atlas volumes: 'advanced/otherVolumes.md' - Display non-atlas volumes: 'advanced/otherVolumes.md'
- Release notes: - Release notes:
- v2.7.4: 'releases/v2.7.4.md'
- v2.7.3: 'releases/v2.7.3.md' - v2.7.3: 'releases/v2.7.3.md'
- v2.7.2: 'releases/v2.7.2.md' - v2.7.2: 'releases/v2.7.2.md'
- v2.7.1: 'releases/v2.7.1.md' - v2.7.1: 'releases/v2.7.1.md'
......
{ {
"name": "interactive-viewer", "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", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular",
"scripts": { "scripts": {
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
......
import { Observable } from "rxjs" import { Observable } from "rxjs"
import { switchMap } from "rxjs/operators"
import { SapiVolumeModel } from ".." import { SapiVolumeModel } from ".."
import { SAPI } from "../sapi.service" import { SAPI } from "../sapi.service"
import {SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionModel} from "../type" import {SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionModel} from "../type"
...@@ -20,43 +21,53 @@ export class SAPIParcellation{ ...@@ -20,43 +21,53 @@ export class SAPIParcellation{
} }
getDetail(queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationModel>{ getDetail(queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationModel>{
return this.sapi.httpGet<SapiParcellationModel>( return SAPI.BsEndpoint$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`, switchMap(endpt => this.sapi.httpGet<SapiParcellationModel>(
null, `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`,
queryParam null,
queryParam
))
) )
} }
getRegions(spaceId: string, queryParam?: SapiQueryPriorityArg): Observable<SapiRegionModel[]> { getRegions(spaceId: string, queryParam?: SapiQueryPriorityArg): Observable<SapiRegionModel[]> {
return this.sapi.httpGet<SapiRegionModel[]>( return SAPI.BsEndpoint$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, switchMap(endpt => this.sapi.httpGet<SapiRegionModel[]>(
{ `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`,
space_id: spaceId {
}, space_id: spaceId
queryParam },
queryParam
))
) )
} }
getVolumes(): Observable<SapiVolumeModel[]>{ getVolumes(): Observable<SapiVolumeModel[]>{
return this.sapi.httpGet<SapiVolumeModel[]>( return SAPI.BsEndpoint$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes` switchMap(endpt => this.sapi.httpGet<SapiVolumeModel[]>(
) `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes`
))
)
} }
getFeatures(parcPagination?: ParcellationPaginationQuery, queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationFeatureModel[]> { getFeatures(parcPagination?: ParcellationPaginationQuery, queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationFeatureModel[]> {
return this.sapi.httpGet<SapiParcellationFeatureModel[]>( return SAPI.BsEndpoint$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`, switchMap(endpt => this.sapi.httpGet<SapiParcellationFeatureModel[]>(
{ `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`,
type: parcPagination?.type, {
size: parcPagination?.size?.toString() || '5', type: parcPagination?.type,
page: parcPagination?.page.toString() || '0', size: parcPagination?.size?.toString() || '5',
}, page: parcPagination?.page.toString() || '0',
queryParam },
queryParam
))
) )
} }
getFeatureInstance(instanceId: string): Observable<SapiParcellationFeatureModel> { getFeatureInstance(instanceId: string): Observable<SapiParcellationFeatureModel> {
return this.sapi.http.get<SapiParcellationFeatureModel>( return SAPI.BsEndpoint$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, switchMap(endpt => this.sapi.http.get<SapiParcellationFeatureModel>(
`${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
))
) )
} }
} }
...@@ -2,7 +2,7 @@ import { SAPI } from ".."; ...@@ -2,7 +2,7 @@ import { SAPI } from "..";
import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type"; import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type";
import { strToRgb, hexToRgb } from 'common/util' import { strToRgb, hexToRgb } from 'common/util'
import { merge, Observable, of } from "rxjs"; import { merge, Observable, of } from "rxjs";
import { catchError, map, scan } from "rxjs/operators"; import { catchError, map, scan, switchMap } from "rxjs/operators";
export class SAPIRegion{ export class SAPIRegion{
...@@ -16,7 +16,7 @@ export class SAPIRegion{ ...@@ -16,7 +16,7 @@ export class SAPIRegion{
return strToRgb(JSON.stringify(region)) return strToRgb(JSON.stringify(region))
} }
private prefix: string private prefix$: Observable<string>
constructor( constructor(
private sapi: SAPI, private sapi: SAPI,
...@@ -24,20 +24,26 @@ export class SAPIRegion{ ...@@ -24,20 +24,26 @@ export class SAPIRegion{
public parcId: string, public parcId: string,
public id: 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)[]> { getFeatures(spaceId: string): Observable<(SapiRegionalFeatureModel | CleanedIeegDataset)[]> {
return merge( return merge(
this.sapi.httpGet<SapiRegionalFeatureModel[]>( this.prefix$.pipe(
`${this.prefix}/features`, switchMap(prefix =>
{ this.sapi.httpGet<SapiRegionalFeatureModel[]>(
space_id: spaceId `${prefix}/features`,
} {
).pipe( space_id: spaceId
catchError((err, obs) => { }
return of([]) ).pipe(
}) catchError((err, obs) => {
return of([])
})
)
)
), ),
spaceId spaceId
? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe( ? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe(
...@@ -56,50 +62,59 @@ export class SAPIRegion{ ...@@ -56,50 +62,59 @@ export class SAPIRegion{
} }
getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> { getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> {
return this.sapi.httpGet<SapiRegionalFeatureModel>( return this.prefix$.pipe(
`${this.prefix}/features/${encodeURIComponent(instanceId)}`, switchMap(prefix => this.sapi.httpGet<SapiRegionalFeatureModel>(
{ `${prefix}/features/${encodeURIComponent(instanceId)}`,
space_id: spaceId {
} space_id: spaceId
}
))
) )
} }
getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> { getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> {
return this.sapi.http.get<SapiRegionMapInfoModel>( return this.prefix$.pipe(
`${this.prefix}/regional_map/info`, switchMap(prefix => this.sapi.http.get<SapiRegionMapInfoModel>(
{ `${prefix}/regional_map/info`,
params: { {
space_id: spaceId params: {
space_id: spaceId
}
} }
} ))
) )
} }
getMapUrl(spaceId: string): string { getMapUrl(spaceId: string): Observable<string> {
return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}` return this.prefix$.pipe(
map(prefix => `${prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`)
)
} }
getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{ getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{
const url = `${this.prefix}/volumes` return this.prefix$.pipe(
return this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>( switchMap(prefix => this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>(
url `${prefix}/volumes`
))
) )
} }
getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> { getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> {
const url = `${this.prefix}/volumes/${encodeURIComponent(volumeId)}` return this.prefix$.pipe(
return this.sapi.httpGet<SapiVolumeModel>( switchMap(prefix => this.sapi.httpGet<SapiVolumeModel>(
url `${prefix}/volumes/${encodeURIComponent(volumeId)}`
))
) )
} }
getDetail(spaceId: string): Observable<SapiRegionModel> { getDetail(spaceId: string): Observable<SapiRegionModel> {
const url = `${this.prefix}` return this.prefix$.pipe(
return this.sapi.httpGet<SapiRegionModel>( switchMap(prefix => this.sapi.httpGet<SapiRegionModel>(
url, prefix,
{ {
space_id: spaceId space_id: spaceId
} }
))
) )
} }
} }
...@@ -2,6 +2,7 @@ import { Observable } from "rxjs" ...@@ -2,6 +2,7 @@ import { Observable } from "rxjs"
import { SAPI } from '../sapi.service' import { SAPI } from '../sapi.service'
import { camelToSnake } from 'common/util' import { camelToSnake } from 'common/util'
import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type" import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type"
import { map, switchMap } from "rxjs/operators"
type FeatureResponse = { type FeatureResponse = {
features: { features: {
...@@ -22,13 +23,21 @@ type SpatialFeatureOpts = RegionalSpatialFeatureOpts | BBoxSpatialFEatureOpts ...@@ -22,13 +23,21 @@ type SpatialFeatureOpts = RegionalSpatialFeatureOpts | BBoxSpatialFEatureOpts
export class SAPISpace{ 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> { getModalities(param?: SapiQueryPriorityArg): Observable<FeatureResponse> {
return this.sapi.httpGet<FeatureResponse>( return this.prefix$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, switchMap(prefix => this.sapi.httpGet<FeatureResponse>(
null, `${prefix}/features`,
param null,
param
))
) )
} }
...@@ -37,9 +46,11 @@ export class SAPISpace{ ...@@ -37,9 +46,11 @@ export class SAPISpace{
for (const [key, value] of Object.entries(opts)) { for (const [key, value] of Object.entries(opts)) {
query[camelToSnake(key)] = value query[camelToSnake(key)] = value
} }
return this.sapi.httpGet<SapiSpatialFeatureModel[]>( return this.prefix$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel[]>(
query `${prefix}/features`,
query
))
) )
} }
...@@ -48,23 +59,29 @@ export class SAPISpace{ ...@@ -48,23 +59,29 @@ export class SAPISpace{
for (const [key, value] of Object.entries(opts)) { for (const [key, value] of Object.entries(opts)) {
query[camelToSnake(key)] = value query[camelToSnake(key)] = value
} }
return this.sapi.httpGet<SapiSpatialFeatureModel>( return this.prefix$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel>(
query `${prefix}/features/${encodeURIComponent(instanceId)}`,
query
))
) )
} }
getDetail(param?: SapiQueryPriorityArg): Observable<SapiSpaceModel>{ getDetail(param?: SapiQueryPriorityArg): Observable<SapiSpaceModel>{
return this.sapi.httpGet<SapiSpaceModel>( return this.prefix$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`, switchMap(prefix => this.sapi.httpGet<SapiSpaceModel>(
null, `${prefix}`,
param null,
param
))
) )
} }
getVolumes(): Observable<SapiVolumeModel[]>{ getVolumes(): Observable<SapiVolumeModel[]>{
return this.sapi.httpGet<SapiVolumeModel[]>( return this.prefix$.pipe(
`${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/volumes`, switchMap(prefix => this.sapi.httpGet<SapiVolumeModel[]>(
`${prefix}/volumes`,
))
) )
} }
} }
import { switchMap } from "rxjs/operators";
import { SAPI } from "../sapi.service"; import { SAPI } from "../sapi.service";
import { SapiFeatureModel } from "../type"; import { SapiFeatureModel } from "../type";
...@@ -6,8 +7,10 @@ export class SAPIFeature { ...@@ -6,8 +7,10 @@ export class SAPIFeature {
} }
public detail$ = this.sapi.httpGet<SapiFeatureModel>( public detail$ = SAPI.BsEndpoint$.pipe(
`${SAPI.BsEndpoint}/features/${this.id}`, switchMap(endpt => this.sapi.httpGet<SapiFeatureModel>(
this.opts `${endpt}/features/${this.id}`,
this.opts
))
) )
} }
import { APP_INITIALIZER, NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { SAPI } from "./sapi.service";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { PriorityHttpInterceptor } from "src/util/priority"; import { PriorityHttpInterceptor } from "src/util/priority";
...@@ -16,16 +15,10 @@ import { MatSnackBarModule } from "@angular/material/snack-bar"; ...@@ -16,16 +15,10 @@ import { MatSnackBarModule } from "@angular/material/snack-bar";
exports: [ exports: [
], ],
providers: [ providers: [
SAPI,
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: PriorityHttpInterceptor, useClass: PriorityHttpInterceptor,
multi: true multi: true
},
{
provide: APP_INITIALIZER,
useValue: () => SAPI.SetBsEndPoint(),
multi: true
} }
] ]
}) })
......
import { NEVER } from "rxjs" import { finalize } from "rxjs/operators"
import * as env from "src/environments/environment" import * as env from "src/environments/environment"
import { SAPI } from "./sapi.service" import { SAPI } from "./sapi.service"
describe("> sapi.service.ts", () => { describe("> sapi.service.ts", () => {
describe("> SAPI", () => { describe("> SAPI", () => {
describe("#SetBsEndPoint", () => { describe("#BsEndpoint$", () => {
let fetchSpy: jasmine.Spy let fetchSpy: jasmine.Spy
let environmentSpy: jasmine.Spy let environmentSpy: jasmine.Spy
...@@ -14,16 +14,10 @@ describe("> sapi.service.ts", () => { ...@@ -14,16 +14,10 @@ describe("> sapi.service.ts", () => {
const atlas1 = 'foo' const atlas1 = 'foo'
const atlas2 = 'bar' const atlas2 = 'bar'
let originalBsEndpoint: string let subscribedVal: string
beforeAll(() => {
originalBsEndpoint = SAPI.BsEndpoint
})
afterAll(() => {
SAPI.BsEndpoint = originalBsEndpoint
})
beforeEach(() => { beforeEach(() => {
SAPI.ClearBsEndPoint()
fetchSpy = spyOn(window, 'fetch') fetchSpy = spyOn(window, 'fetch')
fetchSpy.and.callThrough() fetchSpy.and.callThrough()
...@@ -33,43 +27,70 @@ describe("> sapi.service.ts", () => { ...@@ -33,43 +27,70 @@ describe("> sapi.service.ts", () => {
}) })
}) })
afterEach(() => { afterEach(() => {
SAPI.ClearBsEndPoint()
fetchSpy.calls.reset() fetchSpy.calls.reset()
environmentSpy.calls.reset() environmentSpy.calls.reset()
subscribedVal = null
}) })
describe("> first passes", () => { describe("> first passes", () => {
beforeEach(() => { beforeEach(done => {
const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 }) 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 () => { it("> should call fetch twice", async () => {
await SAPI.SetBsEndPoint() expect(fetchSpy).toHaveBeenCalledTimes(2)
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledOnceWith(`${endpt1}/atlases`) 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 () => { it("> endpoint should be set", async () => {
await SAPI.SetBsEndPoint() expect(subscribedVal).toBe(endpt1)
expect(SAPI.BsEndpoint).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", () => { describe("> first fails", () => {
beforeEach(() => { beforeEach(done => {
let counter = 0 fetchSpy.and.callFake(async url => {
fetchSpy.and.callFake(async () => { if (url === `${endpt1}/atlases`) {
if (counter === 0) {
counter ++
throw new Error(`bla`) throw new Error(`bla`)
} }
const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 }) const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 })
return resp return resp
}) })
SAPI.BsEndpoint$.pipe(
finalize(() => done())
).subscribe(val => {
subscribedVal = val
})
}) })
it("> should call twice", async () => { it("> should call twice", async () => {
await SAPI.SetBsEndPoint()
expect(fetchSpy).toHaveBeenCalledTimes(2) expect(fetchSpy).toHaveBeenCalledTimes(2)
expect(fetchSpy.calls.allArgs()).toEqual([ expect(fetchSpy.calls.allArgs()).toEqual([
[`${endpt1}/atlases`], [`${endpt1}/atlases`],
...@@ -78,18 +99,7 @@ describe("> sapi.service.ts", () => { ...@@ -78,18 +99,7 @@ describe("> sapi.service.ts", () => {
}) })
it('> should set endpt2', async () => { it('> should set endpt2', async () => {
await SAPI.SetBsEndPoint() expect(subscribedVal).toBe(endpt2)
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)
}) })
}) })
}) })
......
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { HttpClient } from '@angular/common/http'; 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 { SAPIAtlas, SAPISpace } from './core'
import { import {
SapiAtlasModel, SapiAtlasModel,
...@@ -20,7 +20,7 @@ import { MatSnackBar } from "@angular/material/snack-bar"; ...@@ -20,7 +20,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
import { EnumColorMapName } from "src/util/colorMaps"; import { EnumColorMapName } from "src/util/colorMaps";
import { PRIORITY_HEADER } from "src/util/priority"; 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 { SAPIFeature } from "./features";
import { environment } from "src/environments/environment" import { environment } from "src/environments/environment"
...@@ -29,34 +29,56 @@ export const SIIBRA_API_VERSION = '0.2.2' ...@@ -29,34 +29,56 @@ export const SIIBRA_API_VERSION = '0.2.2'
type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation
@Injectable() let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null
@Injectable({
providedIn: 'root'
})
export class SAPI{ export class SAPI{
static async SetBsEndPoint() { /**
let idx = 0 * Used to clear BsEndPoint, so the next static get BsEndpoints$ will
const siibraApiEndpts = environment.SIIBRA_API_ENDPOINTS.split(',') * fetch again. Only used for unit test of BsEndpoint$
while (idx < siibraApiEndpts.length) { */
const url = siibraApiEndpts[idx] static ClearBsEndPoint(){
try { BS_ENDPOINT_CACHED_VALUE = null
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...`
} }
static ErrorMessage = null /**
static BsEndpoint = `https://siibra-api-rc.apps.hbp.eu/v2_0` * BsEndpoint$ is designed as a static getter mainly for unit testing purposes.
* see usage of BsEndpoint$ and ClearBsEndPoint in sapi.service.spec.ts
get bsEndpoint() { */
return SAPI.BsEndpoint 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 = { registry = {
_map: {} as Record<string, { _map: {} as Record<string, {
...@@ -116,7 +138,9 @@ export class SAPI{ ...@@ -116,7 +138,9 @@ export class SAPI{
} }
getModalities(): Observable<SapiModalityModel[]> { 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){ httpGet<T>(url: string, params?: Record<string, string>, sapiParam?: SapiQueryPriorityArg){
...@@ -133,12 +157,13 @@ export class SAPI{ ...@@ -133,12 +157,13 @@ export class SAPI{
) )
} }
public atlases$ = this.http.get<SapiAtlasModel[]>( public atlases$ = SAPI.BsEndpoint$.pipe(
`${this.bsEndpoint}/atlases`, switchMap(endpt => this.http.get<SapiAtlasModel[]>(
{ `${endpt}/atlases`,
observe: "response" {
} observe: "response"
).pipe( }
)),
map(resp => { map(resp => {
const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY) const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY)
if (respVersion !== SIIBRA_API_VERSION) { if (respVersion !== SIIBRA_API_VERSION) {
......
...@@ -67,22 +67,27 @@ export const parcId = { ...@@ -67,22 +67,27 @@ export const parcId = {
} }
export async function getAtlases(): Promise<SapiAtlasModel[]> { 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>{ 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>{ 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[]>{ 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> { 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> { export async function getHumanAtlas(): Promise<SapiAtlasModel> {
...@@ -90,7 +95,8 @@ export async function getHumanAtlas(): Promise<SapiAtlasModel> { ...@@ -90,7 +95,8 @@ export async function getHumanAtlas(): Promise<SapiAtlasModel> {
} }
export async function getMni152(): Promise<SapiSpaceModel> { 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> { export async function getJba29(): Promise<SapiParcellationModel> {
...@@ -103,33 +109,41 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> { ...@@ -103,33 +109,41 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> {
export async function getHoc1Right(spaceId=null): Promise<SapiRegionModel> { export async function getHoc1Right(spaceId=null): Promise<SapiRegionModel> {
if (!spaceId) { 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> { export async function get44Left(spaceId=null): Promise<SapiRegionModel> {
if (!spaceId) { 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[]> { 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&region=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&region=hoc1%20right`)).json()
return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession")) return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession"))
} }
export async function getHoc1RightFeatures(): Promise<SapiRegionalFeatureModel[]> { 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>{ 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[]> { 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[]>{ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureModel[]>{
...@@ -137,14 +151,16 @@ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureMo ...@@ -137,14 +151,16 @@ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureMo
[-1000, -1000, -1000], [-1000, -1000, -1000],
[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)) url.searchParams.set(`bbox`, JSON.stringify(bbox))
return await (await fetch(url.toString())).json() return await (await fetch(url.toString())).json()
} }
export async function getMni152SpatialFeatureHoc1Right(): Promise<SapiSpatialFeatureModel[]>{ 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(`parcellation_id`, parcId.human.jba29)
url.searchParams.set("region", 'hoc1 right') url.searchParams.set("region", 'hoc1 right')
return await (await fetch(url.toString())).json() return await (await fetch(url.toString())).json()
......
...@@ -3,11 +3,12 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service" ...@@ -3,11 +3,12 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service"
import { SapiParcellationModel } from "src/atlasComponents/sapi/type" import { SapiParcellationModel } from "src/atlasComponents/sapi/type"
import { getTraverseFunctions } from "./parcellationVersion.pipe" import { getTraverseFunctions } from "./parcellationVersion.pipe"
describe(`parcellationVersion.pipe.ts (endpoint at ${SAPI.BsEndpoint})`, () => { describe(`parcellationVersion.pipe.ts`, () => {
describe("getTraverseFunctions", () => { describe("getTraverseFunctions", () => {
let julichBrainParcellations: SapiParcellationModel[] = [] let julichBrainParcellations: SapiParcellationModel[] = []
beforeAll(async () => { 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() const arr: SapiParcellationModel[] = await res.json()
julichBrainParcellations = arr.filter(it => /Julich-Brain Cytoarchitectonic Maps/.test(it.name)) julichBrainParcellations = arr.filter(it => /Julich-Brain Cytoarchitectonic Maps/.test(it.name))
}) })
......
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
color="primary"> color="primary">
<div class="fas fa-external-link-alt"></div> <div class="fas fa-external-link-alt"></div>
<span> <span>
Explore Dataset Detail
</span> </span>
</a> </a>
......
...@@ -37,17 +37,20 @@ export class LayerCtrlEffects { ...@@ -37,17 +37,20 @@ export class LayerCtrlEffects {
), ),
switchMap(([ regions, { atlas, parcellation, template } ]) => { switchMap(([ regions, { atlas, parcellation, template } ]) => {
const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name) const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name)
return sapiRegion.getMapInfo(template["@id"]).pipe( return forkJoin([
map(val => sapiRegion.getMapInfo(template["@id"]),
sapiRegion.getMapUrl(template["@id"])
]).pipe(
map(([mapInfo, mapUrl]) =>
atlasAppearance.actions.addCustomLayer({ atlasAppearance.actions.addCustomLayer({
customLayer: { customLayer: {
clType: "customlayer/nglayer", clType: "customlayer/nglayer",
id: PMAP_LAYER_NAME, id: PMAP_LAYER_NAME,
source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`, source: `nifti://${mapUrl}`,
shader: getShader({ shader: getShader({
colormap: EnumColorMapName.VIRIDIS, colormap: EnumColorMapName.VIRIDIS,
highThreshold: val.max, highThreshold: mapInfo.max,
lowThreshold: val.min, lowThreshold: mapInfo.min,
removeBg: true, removeBg: true,
}) })
} }
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment