From 7fdbff0f0baf73965bff9df3f075a76b84991b54 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 6 May 2022 09:27:37 +0200 Subject: [PATCH] bugfix: jba29 in bigbrain --- src/atlasComponents/sapi/constants.ts | 9 ++ src/atlasComponents/sapi/core/sapiRegion.ts | 16 ++- src/atlasComponents/sapi/index.ts | 4 + src/atlasComponents/sapi/schema.ts | 100 +++++++++++--- src/atlasComponents/sapi/type.ts | 6 + .../features/voi/voiQuery.directive.ts | 8 +- .../util/parcellationSupportedInSpace.pipe.ts | 7 +- .../routeStateTransform.service.ts | 47 +++---- .../nehuba/config.service/type.ts | 1 - .../nehuba/config.service/util.ts | 69 ++++++---- .../layerCtrl.service/layerCtrl.effects.ts | 128 +++++++++++++----- .../nehubaViewerGlue.component.ts | 9 +- src/viewerModule/nehuba/store/util.ts | 8 +- 13 files changed, 297 insertions(+), 115 deletions(-) create mode 100644 src/atlasComponents/sapi/constants.ts diff --git a/src/atlasComponents/sapi/constants.ts b/src/atlasComponents/sapi/constants.ts new file mode 100644 index 000000000..40011a86a --- /dev/null +++ b/src/atlasComponents/sapi/constants.ts @@ -0,0 +1,9 @@ +export const IDS = { + TEMPLATES: { + BIG_BRAIN: "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588", + MNI152: "minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2" + }, + PARCELLATION: { + JBA29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290" + } +} diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index afadc4929..1787145e8 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -1,5 +1,5 @@ import { SAPI } from ".."; -import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset } from "../type"; +import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type"; import { strToRgb, hexToRgb } from 'common/util' import { forkJoin, Observable, of } from "rxjs"; import { catchError, map } from "rxjs/operators"; @@ -80,6 +80,20 @@ export class SAPIRegion{ return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}` } + getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{ + const url = `${this.prefix}/volumes` + return this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>( + url + ) + } + + getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> { + const url = `${this.prefix}/volumes/${encodeURIComponent(volumeId)}` + return this.sapi.httpGet<SapiVolumeModel>( + url + ) + } + getDetail(spaceId: string): Observable<SapiRegionModel> { const url = `${this.prefix}/${encodeURIComponent(this.id)}` return this.sapi.httpGet<SapiRegionModel>( diff --git a/src/atlasComponents/sapi/index.ts b/src/atlasComponents/sapi/index.ts index 2c223648d..e5d8afd20 100644 --- a/src/atlasComponents/sapi/index.ts +++ b/src/atlasComponents/sapi/index.ts @@ -24,3 +24,7 @@ export { SAPIParcellation, SAPIRegion } from "./core" + +export { + IDS +} from "./constants" \ No newline at end of file diff --git a/src/atlasComponents/sapi/schema.ts b/src/atlasComponents/sapi/schema.ts index 3eb4599d5..a54de84de 100644 --- a/src/atlasComponents/sapi/schema.ts +++ b/src/atlasComponents/sapi/schema.ts @@ -24,6 +24,9 @@ export interface paths { /** Returns a regional map for given region name. */ get: operations["get_regional_map_file_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__regional_map_map_get"] } + "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/volumes": { + get: operations["get_regional_volumes_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__volumes_get"] + } "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}": { get: operations["get_single_region_detail_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__get"] } @@ -104,7 +107,7 @@ export interface paths { * :param space_id: * :param parcellation_id: * :param region_id: - * :return: UnionRegionalFeatureModels + * :return: FeatureModels */ get: operations["get_feature_details_features__feature_id__get"] } @@ -235,11 +238,24 @@ export interface components { /** Urls */ urls: components["schemas"]["Url"][] /** Cells */ - cells: string + cells?: components["schemas"]["CorticalCellModel"][] /** Section */ - section: string + section?: string /** Patch */ - patch: string + patch?: string + } + /** CorticalCellModel */ + CorticalCellModel: { + /** X */ + x: number + /** Y */ + y: number + /** Area */ + area: number + /** Layer */ + layer: number + /** Instance Label */ + "instance label": number } /** DatasetJsonModel */ DatasetJsonModel: { @@ -420,6 +436,29 @@ export interface components { /** Content */ content: string } + /** Page[Union[siibra.features.connectivity.ConnectivityMatrixDataModel, app.models.SerializationErrorModel]] */ + "Page_Union_siibra.features.connectivity.ConnectivityMatrixDataModel__app.models.SerializationErrorModel__": { + /** Items */ + items: (Partial<components["schemas"]["ConnectivityMatrixDataModel"]> & + Partial<components["schemas"]["SerializationErrorModel"]>)[] + /** Total */ + total: number + /** Page */ + page: number + /** Size */ + size: number + } + /** Page[VolumeModel] */ + Page_VolumeModel_: { + /** Items */ + items: components["schemas"]["VolumeModel"][] + /** Total */ + total: number + /** Page */ + page: number + /** Size */ + size: number + } /** ProfileDataModel */ ProfileDataModel: { density: components["schemas"]["NpArrayDataModel"] @@ -787,7 +826,7 @@ export interface components { /** ValidationError */ ValidationError: { /** Location */ - loc: string[] + loc: (Partial<string> & Partial<number>)[] /** Message */ msg: string /** Error Type */ @@ -822,11 +861,8 @@ export interface components { VolumeModel: { /** @Id */ "@id": string - /** - * @Type - * @constant - */ - "@type"?: "https://openminds.ebrains.eu/core/DatasetVersion" + /** @Type */ + "@type": string metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"] /** Urls */ urls: components["schemas"]["Url"][] @@ -1491,6 +1527,35 @@ export interface operations { } } } + get_regional_volumes_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__volumes_get: { + parameters: { + path: { + atlas_id: string + parcellation_id: string + region_id: string + } + query: { + space_id?: string + type?: string + page?: number + size?: number + } + } + responses: { + /** Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Page_VolumeModel_"] + } + } + /** Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } get_single_region_detail_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__get: { parameters: { path: { @@ -1575,18 +1640,15 @@ export interface operations { } query: { type?: string - per_page?: number page?: number + size?: number } } responses: { /** Successful Response */ 200: { content: { - "application/json": (Partial< - components["schemas"]["ConnectivityMatrixDataModel"] - > & - Partial<components["schemas"]["SerializationErrorModel"]>)[] + "application/json": components["schemas"]["Page_Union_siibra.features.connectivity.ConnectivityMatrixDataModel__app.models.SerializationErrorModel__"] } } /** Validation Error */ @@ -1889,7 +1951,7 @@ export interface operations { * :param space_id: * :param parcellation_id: * :param region_id: - * :return: UnionRegionalFeatureModels + * :return: FeatureModels */ get_feature_details_features__feature_id__get: { parameters: { @@ -1911,7 +1973,11 @@ export interface operations { components["schemas"]["ReceptorDatasetModel"] > & Partial<components["schemas"]["BaseDatasetJsonModel"]> & - Partial<components["schemas"]["CorticalCellDistributionModel"]> + Partial<components["schemas"]["CorticalCellDistributionModel"]> & + Partial<components["schemas"]["IEEGSessionModel"]> & + Partial<components["schemas"]["VOIDataModel"]> & + Partial<components["schemas"]["ConnectivityMatrixDataModel"]> & + Partial<components["schemas"]["SerializationErrorModel"]> } } /** Validation Error */ diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts index 1796c54d0..3eb47a670 100644 --- a/src/atlasComponents/sapi/type.ts +++ b/src/atlasComponents/sapi/type.ts @@ -34,6 +34,12 @@ export type SapiIeegSessionModel = components["schemas"]["IEEGSessionModel"] * utility types */ type PathReturn<T extends keyof paths> = Required<paths[T]["get"]["responses"][200]["content"]["application/json"]> +export type PaginatedResponse<T> = { + items: T[] + total: number + page: number + size: number +} /** * serialization error type diff --git a/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts b/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts index e70d1ab45..623011d89 100644 --- a/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts +++ b/src/atlasComponents/sapiViews/features/voi/voiQuery.directive.ts @@ -1,10 +1,11 @@ import { Directive, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges } from "@angular/core"; import { interval, merge, Observable, of, Subject, Subscription } from "rxjs"; -import { debounce, debounceTime, filter, pairwise, shareReplay, startWith, switchMap, take, tap } from "rxjs/operators"; +import { debounce, debounceTime, distinctUntilChanged, filter, pairwise, shareReplay, startWith, switchMap, take, tap } from "rxjs/operators"; import { AnnotationLayer, TNgAnnotationPoint, TNgAnnotationAABBox } from "src/atlasComponents/annotations"; import { SAPI } from "src/atlasComponents/sapi/sapi.service"; import { BoundingBoxConcept, SapiAtlasModel, SapiSpaceModel, SapiVOIDataResponse, OpenMINDSCoordinatePoint } from "src/atlasComponents/sapi/type"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; +import { arrayEqual } from "src/util/array"; @Directive({ selector: '[sxplr-sapiviews-features-voi-query]', @@ -130,14 +131,15 @@ export class SapiViewsFeaturesVoiQuery implements OnChanges, OnDestroy{ clickInterceptor.register(handle) this.subscription.push( this.features$.pipe( + startWith([] as SapiVOIDataResponse[]), + distinctUntilChanged(arrayEqual((o, n) => o["@id"] === n["@id"])), + pairwise(), debounce(() => interval(16).pipe( filter(() => !!this.voiBBoxSvc), take(1), ) ), - startWith([] as SapiVOIDataResponse[]), - pairwise() ).subscribe(([ prev, curr ]) => { for (const v of prev) { const box = this.pointsToAABB(v.location.maxpoint, v.location.minpoint) diff --git a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts index aaafaf90f..f16e6a0a8 100644 --- a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts +++ b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, of } from "rxjs"; import { map } from "rxjs/operators"; import { SAPIParcellation } from "src/atlasComponents/sapi/core"; import { SAPI } from "src/atlasComponents/sapi/sapi.service"; @@ -35,6 +35,11 @@ export class ParcellationSupportedInSpacePipe implements PipeTransform{ const tmplId = typeof tmpl === "string" ? tmpl : tmpl["@id"] + for (const key in knownExceptions.supported) { + if (key === parcId && knownExceptions.supported[key].indexOf(tmplId) >= 0) { + return of(true) + } + } return this.sapi.registry.get<SAPIParcellation>(parcId).getVolumes().pipe( map(volumes => volumes.some(v => v.data.space["@id"] === tmplId)) ) diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts index d7deecf51..10f003638 100644 --- a/src/routerModule/routeStateTransform.service.ts +++ b/src/routerModule/routeStateTransform.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { UrlSegment, UrlTree } from "@angular/router"; import { map } from "rxjs/operators"; -import { SAPI } from "src/atlasComponents/sapi"; +import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi"; import { atlasSelection, defaultState, MainState, plugins, userInteraction } from "src/state"; import { getParcNgId, getRegionLabelIndex } from "src/viewerModule/nehuba/config.service"; import { decodeToNumber, encodeNumber, encodeURIFull, separator } from "./cipher"; @@ -43,16 +43,21 @@ export class RouteStateTransformSvc { this.sapi.getParcRegions(selectedAtlasId, selectedParcellationId, selectedTemplateId, { priority: 10 }).toPromise(), ]) - const latNgIdMap = ["left hemisphere", "right hemisphere", "whole brain"].map(lat => { - let regex: RegExp = /./ - if (lat === "left hemisphere") regex = /left/i - if (lat === "right hemisphere") regex = /right/i - return { - lat, - regex, - ngId: getParcNgId(selectedAtlas, selectedTemplate, selectedParcellation, lat) + const ngIdToRegionMap: Map<string, Map<number, SapiRegionModel[]>> = new Map() + + for (const region of allParcellationRegions) { + const ngId = getParcNgId(selectedAtlas, selectedTemplate, selectedParcellation, region) + if (!ngIdToRegionMap.has(ngId)) { + ngIdToRegionMap.set(ngId, new Map()) } - }) + const map = ngIdToRegionMap.get(ngId) + + const idx = getRegionLabelIndex(selectedAtlas, selectedTemplate, selectedParcellation, region) + if (!map.has(idx)) { + map.set(idx, []) + } + map.get(idx).push(region) + } const selectedRegions = (() => { if (!selectedRegionIds) return [] @@ -63,12 +68,12 @@ export class RouteStateTransformSvc { const json = { [selectedRegionIds[0]]: selectedRegionIds[1] } for (const ngId in json) { - const matchingMap = latNgIdMap.find(ngIdMap => ngIdMap.ngId === ngId) - if (!matchingMap) { + if (!ngIdToRegionMap.has(ngId)) { console.error(`could not find matching map for ${ngId}`) continue } - const filteredRegions = allParcellationRegions.filter(r => matchingMap.regex.test(r.name)) + + const map = ngIdToRegionMap.get(ngId) const val = json[ngId] const labelIndicies = val.split(separator).map(n => { @@ -81,20 +86,8 @@ export class RouteStateTransformSvc { return null } }).filter(v => !!v) - - for (const labelIndex of labelIndicies) { - /** - * currently, only 1 region is expected to be selected at max - * this method probably out performs creating a map - * but with 2+ regions, creating a map would consistently be faster - */ - for (const r of filteredRegions) { - const idx = getRegionLabelIndex(selectedAtlas, selectedTemplate, selectedParcellation, r) - if (idx === labelIndex) { - return [ r ] - } - } - } + + return labelIndicies.map(idx => map.get(idx) || []).flatMap(v => v) } return [] })() diff --git a/src/viewerModule/nehuba/config.service/type.ts b/src/viewerModule/nehuba/config.service/type.ts index c0e963ecb..c8154dd44 100644 --- a/src/viewerModule/nehuba/config.service/type.ts +++ b/src/viewerModule/nehuba/config.service/type.ts @@ -112,5 +112,4 @@ export type NgPrecompMeshSpec = { export type NgSegLayerSpec = { labelIndicies: number[] - laterality: 'left hemisphere' | 'right hemisphere' | 'whole brain' } & NgLayerSpec diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts index a89599f60..6cd2b0c35 100644 --- a/src/viewerModule/nehuba/config.service/util.ts +++ b/src/viewerModule/nehuba/config.service/util.ts @@ -1,5 +1,5 @@ import { SapiParcellationModel, SapiSpaceModel, SapiAtlasModel, SapiRegionModel } from 'src/atlasComponents/sapi' -import { SapiVolumeModel } from 'src/atlasComponents/sapi/type' +import { SapiVolumeModel, IDS } from 'src/atlasComponents/sapi' import { atlasSelection } from 'src/state' import { MultiDimMap } from 'src/util/fn' import { ParcVolumeSpec } from "../store/util" @@ -14,7 +14,7 @@ import { // fsaverage uses threesurfer, which, whilst do not use ngId, uses 'left' and 'right' as keys const fsAverageKeyVal = { - "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290": { + [IDS.PARCELLATION.JBA29]: { "left hemisphere": "left", "right hemisphere": "right" } @@ -29,7 +29,7 @@ const BACKCOMAP_KEY_DICT = { // human multi level 'juelich/iav/atlas/v1.0.0/1': { // icbm152 - 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': { + [IDS.TEMPLATES.MNI152]: { // julich brain v2.6 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-26': { 'left hemisphere': 'MNI152_V25_LEFT_NG_SPLIT_HEMISPHERE', @@ -186,15 +186,23 @@ export function getTmplAuxNgLayer(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, s } } -export function getParcNgId(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, _laterality: string | SapiRegionModel): string { - let laterality: string - if (typeof _laterality === "string") { - laterality = _laterality - } else { - laterality = "whole brain" - if (_laterality.name.indexOf("left") >= 0) laterality = "left hemisphere" - if (_laterality.name.indexOf("right") >= 0) laterality = "right hemisphere" +export function getParcNgId(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, region: SapiRegionModel): string { + + let laterality: string = "whole brain" + if (region.name.indexOf("left") >= 0) laterality = "left hemisphere" + if (region.name.indexOf("right") >= 0) laterality = "right hemisphere" + + /** + * for JBA29 in big brain, there exist several volumes. (e.g. v1, v2, v5, interpolated, etc) + */ + if (tmpl['@id'] === IDS.TEMPLATES.BIG_BRAIN && parc['@id'] === IDS.PARCELLATION.JBA29) { + laterality = region.hasAnnotation.visualizedIn?.['@id'] } + + if (!laterality) { + return null + } + let ngId = BACKCOMAP_KEY_DICT[atlas["@id"]]?.[tmpl["@id"]]?.[parc["@id"]]?.[laterality] if (!ngId) { ngId = `_${MultiDimMap.GetKey(atlas["@id"], tmpl["@id"], parc["@id"], laterality)}` @@ -202,18 +210,37 @@ export function getParcNgId(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: S return ngId } +const labelIdxRegex = /siibra_python_ng_precomputed_labelindex:\/\/(.*?)#([0-9]+)$/ + +export function getRegionLabelIndex(_atlas: SapiAtlasModel, _tmpl: SapiSpaceModel, _parc: SapiParcellationModel, region: SapiRegionModel): number { + const overwriteLabelIndex = region.hasAnnotation.inspiredBy.map(({ "@id": id }) => labelIdxRegex.exec(id)).filter(v => !!v) + if (overwriteLabelIndex.length > 0) { + const match = overwriteLabelIndex[0] + const volumeId = match[1] + const labelIndex = match[2] + const _labelIndex = Number(labelIndex) + if (!isNaN(_labelIndex)) return _labelIndex + } + const lblIdx = Number(region?.hasAnnotation?.internalIdentifier) + if (isNaN(lblIdx)) return null + return lblIdx +} + export function getParcNgLayers(atlas: SapiAtlasModel, tmpl: SapiSpaceModel, parc: SapiParcellationModel, parcVolumes: { volume: SapiVolumeModel, volumeMetadata: ParcVolumeSpec }[]): Record<string, NgSegLayerSpec>{ const returnVal: Record<string, NgSegLayerSpec> = {} for (const parcVol of parcVolumes) { const { volume, volumeMetadata } = parcVol - const { laterality, labelIndicies } = volumeMetadata - const ngId = getParcNgId(atlas, tmpl, parc, laterality) + const { regions } = volumeMetadata + if (regions.length === 0) { + console.warn(`parc volume with no associated region`) + continue + } + const ngId = getParcNgId(atlas, tmpl, parc, regions[0].region) returnVal[ngId] = { source: `precomputed://${volume.data.url.replace(/^precomputed:\/\//, '')}`, - labelIndicies, - laterality, - transform: (volume.data.detail["neuroglancer/precomputed"] as any).transform + transform: (volume.data.detail["neuroglancer/precomputed"] as any).transform, + labelIndicies: regions.map(v => v.labelIndex) } } return returnVal @@ -240,12 +267,6 @@ export const getNgLayersFromVolumesATP = (volumes: CongregatedVolume, ATP: { atl } } -export function getRegionLabelIndex(_atlas: SapiAtlasModel, _tmpl: SapiSpaceModel, _parc: SapiParcellationModel, region: SapiRegionModel): number { - const lblIdx = Number(region?.hasAnnotation?.internalIdentifier) - if (isNaN(lblIdx)) return null - return lblIdx -} - export const defaultNehubaConfig: NehubaConfig = { "configName": "", "globals": { @@ -318,11 +339,11 @@ export const defaultNehubaConfig: NehubaConfig = { } export const spaceMiscInfoMap = new Map([ - ['minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588', { + [IDS.TEMPLATES.BIG_BRAIN, { name: 'bigbrain', scale: 1, }], - ['minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2', { + [IDS.TEMPLATES.MNI152, { name: 'icbm2009c', scale: 1, }], diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index 314049276..6f6d70887 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -3,8 +3,8 @@ import { createEffect } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import { forkJoin, of } from "rxjs"; import { mapTo, switchMap, withLatestFrom, filter, catchError, map, debounceTime, shareReplay, distinctUntilChanged, startWith, pairwise } from "rxjs/operators"; -import { SAPI, SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi"; -import { SapiVOIDataResponse } from "src/atlasComponents/sapi/type"; +import { SAPI, SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel, SapiRegionModel } from "src/atlasComponents/sapi"; +import { SapiVOIDataResponse, SapiVolumeModel } from "src/atlasComponents/sapi/type"; import { atlasAppearance, atlasSelection, userInteraction } from "src/state"; import { arrayEqual } from "src/util/array"; import { EnumColorMapName } from "src/util/colorMaps"; @@ -146,51 +146,115 @@ export class LayerCtrlEffects { throw new Error(`parcellation defined, but template not defined!`) } + /** + * some labelled maps (such as julich brain in big brain) do not have the volumes defined on the parcellation level. + * While we have the URLs of these volumes (the method we use is also kind of hacky), and in theory, we could construct a volume object directly + * It is probably better to fetch the correct volume object to begin with + */ const parcVolumes$ = !parcellation - ? of([]) + ? of([] as {volume: SapiVolumeModel, volumeMetadata: ParcVolumeSpec}[]) : forkJoin([ this.sapi.getParcellation(atlas["@id"], parcellation["@id"]).getRegions(template["@id"]).pipe( map(regions => { - const returnArr: ParcVolumeSpec[] = [] + const volumeIdToRegionMap = new Map<string, { + labelIndex: number + region: SapiRegionModel + }[]>() + for (const r of regions) { - const source = r?.hasAnnotation?.visualizedIn?.["@id"] - if (!source) continue - if (source.indexOf("precomputed://") < 0) continue + const volumeId = r?.hasAnnotation?.visualizedIn?.["@id"] + if (!volumeId) continue + const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) if (!labelIndex) continue - - const found = returnArr.find(v => v.volumeSrc === source) - if (found) { - found.labelIndicies.push(labelIndex) - continue - } - let laterality: "left hemisphere" | "right hemisphere" | "whole brain" = "whole brain" - if (r.name.indexOf("left") >= 0) laterality = "left hemisphere" - if (r.name.indexOf("right") >= 0) laterality = "right hemisphere" - returnArr.push({ - volumeSrc: source, - labelIndicies: [labelIndex], - parcellation, - laterality, + if (!volumeIdToRegionMap.has(volumeId)) { + volumeIdToRegionMap.set(volumeId, []) + } + volumeIdToRegionMap.get(volumeId).push({ + labelIndex, + region: r }) } - return returnArr + return volumeIdToRegionMap }) ), this.sapi.getParcellation(atlas["@id"], parcellation["@id"]).getVolumes() ]).pipe( - map(([ volumeSrcs, volumes ]) => { - return volumes.map( - v => { - const found = volumeSrcs.find(volSrc => volSrc.volumeSrc.indexOf(v.data.url) >= 0) - return { - volume: v, - volumeMetadata: found, + switchMap(([ volumeIdToRegionMap, volumes ]) => { + const missingVolumeIds = Array.from(volumeIdToRegionMap.keys()).filter(id => volumes.map(v => v["@id"]).indexOf(id) < 0) + + const volumesFromParc: {volume: SapiVolumeModel, volumeMetadata: ParcVolumeSpec}[] = volumes.map( + volume => { + const found = volumeIdToRegionMap.get(volume["@id"]) + if (!found) return null + + try { + + const volumeMetadata: ParcVolumeSpec = { + regions: found, + parcellation, + volumeSrc: volume.data.url + } + return { + volume, + volumeMetadata, + } + } catch (e) { + console.error(e) + return null } - }).filter( - v => !!v.volumeMetadata?.labelIndicies + } + ).filter(v => v?.volumeMetadata?.regions) + + if (missingVolumeIds.length === 0) return of([...volumesFromParc]) + return forkJoin( + missingVolumeIds.map(missingId => { + if (!volumeIdToRegionMap.has(missingId)) { + console.warn(`volumeIdToRegionMap does not have volume with id ${missingId}`) + return of(null as SapiVolumeModel) + } + const { region } = volumeIdToRegionMap.get(missingId)[0] + return this.sapi.getRegion(atlas["@id"], parcellation["@id"], region.name).getVolumeInstance(missingId).pipe( + catchError((err, obs) => of(null as SapiVolumeModel)) + ) + }) + ).pipe( + map((missingVolumes: SapiVolumeModel[]) => { + + const volumesFromRegion: { volume: SapiVolumeModel, volumeMetadata: ParcVolumeSpec }[] = missingVolumes.map( + volume => { + if (!volume || !volumeIdToRegionMap.has(volume['@id'])) { + return null + } + + try { + + const found = volumeIdToRegionMap.get(volume['@id']) + const volumeMetadata: ParcVolumeSpec = { + regions: found, + parcellation, + volumeSrc: volume.data.url + } + return { + volume, + volumeMetadata + } + } catch (e) { + console.error(`volume from region error`, e) + return null + } + } + ).filter( + v => !!v + ) + + return [ + ...volumesFromParc, + ...volumesFromRegion + ] + }) ) }) ) @@ -199,7 +263,7 @@ export class LayerCtrlEffects { ? this.sapi.getSpace(atlas["@id"], template["@id"]).getVolumes().pipe( shareReplay(1), ) - : of([]) + : of([] as SapiVolumeModel[]) return forkJoin({ tmplVolumes: spaceVols$.pipe( diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index c719c98ce..0c7b084eb 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -187,11 +187,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni private layerCtrlService: NehubaLayerControlService, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, ){ - /** - * This **massively** improve the performance of the viewer - * TODO investigate why, and perhaps eventually remove the cdr.detach() - */ - // this.cdr.detach() /** * define onclick behaviour @@ -215,10 +210,12 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni this.multiNgIdsRegionsLabelIndexMap.clear() for (const r of regions) { const ngId = getParcNgId(atlas, template, parcellation, r) - const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) + if (!ngId) continue if (!this.multiNgIdsRegionsLabelIndexMap.has(ngId)) { this.multiNgIdsRegionsLabelIndexMap.set(ngId, new Map()) } + const labelIndex = getRegionLabelIndex(atlas, template, parcellation, r) + if (!labelIndex) continue this.multiNgIdsRegionsLabelIndexMap.get(ngId).set(labelIndex, r) } }) diff --git a/src/viewerModule/nehuba/store/util.ts b/src/viewerModule/nehuba/store/util.ts index cc59b9f00..14a59dc1d 100644 --- a/src/viewerModule/nehuba/store/util.ts +++ b/src/viewerModule/nehuba/store/util.ts @@ -1,8 +1,10 @@ -import { SapiParcellationModel } from "src/atlasComponents/sapi"; +import { SapiParcellationModel, SapiRegionModel } from "src/atlasComponents/sapi"; export type ParcVolumeSpec = { volumeSrc: string - labelIndicies: number[] parcellation: SapiParcellationModel - laterality: 'left hemisphere' | 'right hemisphere' | 'whole brain' + regions: { + labelIndex: number + region: SapiRegionModel + }[] } -- GitLab