diff --git a/package.json b/package.json index d4b47cfb3001a96f0e61c254a7c174ee31cb965a..b05fd47fcb2775a0304ecef3bf84318a0891656c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "watch": "ng build --watch --configuration development", "test": "ng test", "test-ci": "ng test --progress false --watch false --browsers=ChromeHeadless", - "sapi-schema": "npx openapi-typescript@5.1.1 http://localhost:5000/v1_0/openapi.json --output ./src/atlasComponents/sapi/schema.ts", + "sapi-schema": "npx openapi-typescript@5.1.1 http://localhost:5000/v1_0/openapi.json --output ./src/atlasComponents/sapi/schema.ts && eslint ./src/atlasComponents/sapi/schema.ts --no-ignore --fix", "docs:json": "compodoc -p ./tsconfig.json -e json -d .", "storybook": "npm run docs:json && start-storybook -p 6006", "build-storybook": "npm run docs:json && build-storybook" diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..471e628bbff4e137d91d4bd5c3fb2633d25a6dd9 --- /dev/null +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -0,0 +1,120 @@ +import { getUuid } from "src/util/fn"; + +export type TNgAnnotationLine = { + type: 'line' + pointA: [number, number, number] + pointB: [number, number, number] + id: string + description?: string +} + +export type TNgAnnotationPoint = { + type: 'point' + point: [number, number, number] + id: string + description?: string +} + +export type AnnotationSpec = TNgAnnotationLine | TNgAnnotationPoint +type _AnnotationSpec = Omit<AnnotationSpec, 'type'> & { type: number } +type AnnotationRef = {} + +interface NgAnnotationLayer { + layer: { + localAnnotations: { + references: { + get(id: string): AnnotationRef + delete(id: string): void + } + update(ref: AnnotationRef, spec: _AnnotationSpec): void + add(spec: _AnnotationSpec): void + delete(spec: AnnotationRef):void + annotationMap: Map<string, _AnnotationSpec> + } + registerDisposer(fn: () => void): void + } + setVisible(flag: boolean): void +} + +export class AnnotationLayer { + private nglayer: NgAnnotationLayer + constructor( + private name: string = getUuid(), + private color="#ffffff" + ){ + const layerSpec = this.viewer.layerSpecification.getLayer( + this.name, + { + type: "annotation", + "annotationColor": this.color, + "annotations": [], + name: this.name, + transform: [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + } + ) + this.nglayer = this.viewer.layerManager.addManagedLayer(layerSpec) + this.nglayer.layer.registerDisposer(() => { + this.nglayer = null + }) + } + setVisible(flag: boolean){ + this.nglayer.setVisible(flag) + } + dispose() { + try { + this.viewer.layerManager.removeManagedLayer(this.nglayer) + } catch (e) { + + } + } + + addAnnotation(spec: AnnotationSpec){ + const localAnnotations = this.nglayer.layer.localAnnotations + const annSpec = this.parseNgSpecType(spec) + localAnnotations.add( + annSpec + ) + } + removeAnnotation(spec: { id: string }) { + const { localAnnotations } = this.nglayer.layer + const ref = localAnnotations.references.get(spec.id) + if (ref) { + localAnnotations.delete(ref) + localAnnotations.references.delete(spec.id) + } + } + updateAnnotation(spec: AnnotationSpec) { + const localAnnotations = this.nglayer.layer.localAnnotations + const ref = localAnnotations.references.get(spec.id) + const _spec = this.parseNgSpecType(spec) + if (ref) { + localAnnotations.update( + ref, + _spec + ) + } else { + localAnnotations.add(_spec) + } + } + + private get viewer() { + if ((window as any).viewer) return (window as any).viewer + throw new Error(`window.viewer not defined`) + } + + private parseNgSpecType(spec: AnnotationSpec): _AnnotationSpec{ + let overwritingType = null + if (spec.type === 'point') overwritingType = 0 + if (spec.type === 'line') overwritingType = 1 + if (overwritingType === null) throw new Error(`overwrite type lookup failed for ${spec.type}`) + return { + ...spec, + type: overwritingType + } + } +} diff --git a/src/atlasComponents/annotations/index.ts b/src/atlasComponents/annotations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..651584158e28cbeb2ea72a5f3cbb954e4e402310 --- /dev/null +++ b/src/atlasComponents/annotations/index.ts @@ -0,0 +1 @@ +export { AnnotationLayer, TNgAnnotationPoint } from "./annotation.service" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.style.css b/src/atlasComponents/annotations/module.ts similarity index 100% rename from src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.style.css rename to src/atlasComponents/annotations/module.ts diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts index e33e20de39edda831adc161e5e2d2acf1b3219d9..5f956620942b6f2edb19a0046eeea8d59a958b20 100644 --- a/src/atlasComponents/sapi/core/sapiParcellation.ts +++ b/src/atlasComponents/sapi/core/sapiParcellation.ts @@ -44,9 +44,9 @@ export class SAPIParcellation{ ) } - getFeatureInstance(instanceId: string): Promise<SapiParcellationFeatureModel> { + 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)}`, - ).toPromise() + ) } } diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index 0b2c4fb68c1646edbc4a01abf2ec34bd39ccd9ba..afadc4929dc48394affa9daa5a246b301b914b1f 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -1,7 +1,8 @@ import { SAPI } from ".."; -import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel } from "../type"; +import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset } from "../type"; import { strToRgb, hexToRgb } from 'common/util' -import { Observable } from "rxjs"; +import { forkJoin, Observable, of } from "rxjs"; +import { catchError, map } from "rxjs/operators"; export class SAPIRegion{ @@ -26,29 +27,45 @@ export class SAPIRegion{ this.prefix = `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}` } - getFeatures(spaceId: string): Promise<SapiRegionalFeatureModel[]> { - return this.sapi.http.get<SapiRegionalFeatureModel[]>( - `${this.prefix}/features`, - { - params: { + getFeatures(spaceId: string): Observable<(SapiRegionalFeatureModel | CleanedIeegDataset)[]> { + return forkJoin({ + regionalFeatures: this.sapi.httpGet<SapiRegionalFeatureModel[]>( + `${this.prefix}/features`, + { space_id: spaceId } - } - ).toPromise() + ).pipe( + catchError((err, obs) => of([])) + ), + spatialFeatures: spaceId + ? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe( + catchError((err, obs) => { + console.log('error caught') + return of([]) + }), + map(feats => { + const ieegSessions: SapiIeegSessionModel[] = feats.filter(feat => feat["@type"] === "siibra/features/ieegSession") + return cleanIeegSessionDatasets(ieegSessions) + }), + ) + : of([] as CleanedIeegDataset[]) + }).pipe( + map(({ regionalFeatures, spatialFeatures }) => { + return [...spatialFeatures, ...regionalFeatures] + }) + ) } - getFeatureInstance(instanceId: string, spaceId: string = null): Promise<SapiRegionalFeatureModel> { - return this.sapi.http.get<SapiRegionalFeatureModel>( + getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> { + return this.sapi.httpGet<SapiRegionalFeatureModel>( `${this.prefix}/features/${encodeURIComponent(instanceId)}`, { - params: { - space_id: spaceId - } + space_id: spaceId } - ).toPromise() + ) } - getMapInfo(spaceId: string): Promise<SapiRegionMapInfoModel> { + getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> { return this.sapi.http.get<SapiRegionMapInfoModel>( `${this.prefix}/regional_map/info`, { @@ -56,7 +73,7 @@ export class SAPIRegion{ space_id: spaceId } } - ).toPromise() + ) } getMapUrl(spaceId: string): string { diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts index bd33b4239601550338b500a49aa77292952e0045..3ed79e8a7c06be812fc56de29d83cd4d543f8fa4 100644 --- a/src/atlasComponents/sapi/core/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -32,13 +32,24 @@ export class SAPISpace{ ) } - getFeatures(modalityId: string, opts: SpatialFeatureOpts): Observable<SapiSpatialFeatureModel[]> { + getFeatures(opts: SpatialFeatureOpts): Observable<SapiSpatialFeatureModel[]> { const query: Record<string, string> = {} 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(modalityId)}`, + `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, + query + ) + } + + getFeatureInstance(instanceId: string, opts: SpatialFeatureOpts): Observable<SapiSpatialFeatureModel> { + const query: Record<string, string> = {} + 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 ) } diff --git a/src/atlasComponents/sapi/index.ts b/src/atlasComponents/sapi/index.ts index b1bbb7c2313d3c88f505f59b4a4947bbb620c99d..7e00877e930cd38bd040751ccd4b5dadd5ee723b 100644 --- a/src/atlasComponents/sapi/index.ts +++ b/src/atlasComponents/sapi/index.ts @@ -12,6 +12,9 @@ export { SapiSpatialFeatureModel, SapiFeatureModel, SapiParcellationFeatureModel, + CleanedIeegDataset, + SxplrCleanedFeatureModel, + CLEANED_IEEG_DATASET_TYPE, } from "./type" export { SAPI } from "./sapi.service" diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts index 452d71f70804eedce57e4c5b051e3c3abdff4be4..32701a9c5e86af9becbcbcbf23261d796f754f4a 100644 --- a/src/atlasComponents/sapi/module.ts +++ b/src/atlasComponents/sapi/module.ts @@ -2,17 +2,15 @@ import { NgModule } from "@angular/core"; import { SAPI } from "./sapi.service"; import { SpatialFeatureBBox } from "./directives/spatialFeatureBBox.directive" import { CommonModule } from "@angular/common"; -import { EffectsModule } from "@ngrx/effects"; -import { SapiEffects } from "./sapi.effects"; -import { HTTP_INTERCEPTORS } from "@angular/common/http"; +import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { PriorityHttpInterceptor } from "src/util/priority"; +import { MatSnackBarModule } from "@angular/material/snack-bar"; @NgModule({ imports: [ CommonModule, - EffectsModule.forFeature([ - SapiEffects - ]) + HttpClientModule, + MatSnackBarModule, ], declarations: [ SpatialFeatureBBox, diff --git a/src/atlasComponents/sapi/sapi.effects.ts b/src/atlasComponents/sapi/sapi.effects.ts deleted file mode 100644 index c1cb06fb33676d803368afaac8f39bbd0c7887ef..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapi/sapi.effects.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from "@angular/core"; -import { SAPI } from "./sapi.service" - -@Injectable() -export class SapiEffects{ - - constructor( - private sapiSvc: SAPI - ){ - - } -} diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 86c05ee11654f9041900de8dba83be00dd26b0cf..de44e86da06479ad232478e98d05b52491303c7c 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -1,10 +1,9 @@ -import { Inject, Injectable } from "@angular/core"; +import { Injectable } from "@angular/core"; import { HttpClient } from '@angular/common/http'; -import { BS_ENDPOINT } from 'src/util/constants'; -import { map, shareReplay, take, tap } from "rxjs/operators"; +import { map, shareReplay, tap } from "rxjs/operators"; import { SAPIAtlas, SAPISpace } from './core' -import { SapiAtlasModel, SapiParcellationModel, SapiQueryParam, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel, SpyNpArrayDataModel } from "./type"; -import { CachedFunction, getExportNehuba } from "src/util/fn"; +import { SapiAtlasModel, SapiParcellationModel, SapiQueryParam, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel, SpyNpArrayDataModel, SxplrCleanedFeatureModel } from "./type"; +import { getExportNehuba } from "src/util/fn"; import { SAPIParcellation } from "./core/sapiParcellation"; import { SAPIRegion } from "./core/sapiRegion" import { MatSnackBar } from "@angular/material/snack-bar"; @@ -71,10 +70,7 @@ export class SAPI{ return parc.getRegions(spaceId, queryParam) } - @CachedFunction({ - serialization: (atlasId, parcId, spaceId, regionId, ...args) => `sapi::getRegions::${atlasId}::${parcId}::${spaceId}::${regionId}` - }) - getRegionFeatures(atlasId: string, parcId: string, spaceId: string, regionId: string, priority = 0): Promise<SapiRegionalFeatureModel[]>{ + getRegionFeatures(atlasId: string, parcId: string, spaceId: string, regionId: string, priority = 0): Observable<(SapiRegionalFeatureModel | SxplrCleanedFeatureModel)[]>{ const reg = this.getRegion(atlasId, parcId, regionId) return reg.getFeatures(spaceId) diff --git a/src/atlasComponents/sapi/schema.ts b/src/atlasComponents/sapi/schema.ts index c6b5bbca49d58c286aa0381ce8752ce59b4071ac..483b4d62789e416bb058b676c31a728a52715b71 100644 --- a/src/atlasComponents/sapi/schema.ts +++ b/src/atlasComponents/sapi/schema.ts @@ -10,11 +10,11 @@ export interface paths { } "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/features": { /** Returns all regional features for a region. */ - get: operations["get_all_features_for_region_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features_get"] + get: operations["get_all_regional_features_for_region_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features_get"] } "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/features/{feature_id}": { /** Returns a feature for a region, as defined by by the modality and feature ID */ - get: operations["get_regional_modality_by_id_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features__feature_id__get"] + get: operations["get_single_detailed_regional_feature_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features__feature_id__get"] } "/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/regional_map/info": { /** Returns information about a regional map for given region name. */ @@ -33,19 +33,19 @@ export interface paths { } "/atlases/{atlas_id}/parcellations/{parcellation_id}/features/{feature_id}": { /** Returns a global feature for a specific modality id. */ - get: operations["get_single_global_feature_detail_atlases__atlas_id__parcellations__parcellation_id__features__feature_id__get"] + get: operations["get_single_detailed_global_feature_atlases__atlas_id__parcellations__parcellation_id__features__feature_id__get"] } "/atlases/{atlas_id}/parcellations/{parcellation_id}/features": { /** Returns all global features for a parcellation. */ - get: operations["get_global_features_names_atlases__atlas_id__parcellations__parcellation_id__features_get"] + get: operations["get_all_global_features_for_parcellation_atlases__atlas_id__parcellations__parcellation_id__features_get"] } "/atlases/{atlas_id}/parcellations/{parcellation_id}/volumes": { /** Returns one parcellation for given id. */ - get: operations["get_volumes_by_id_atlases__atlas_id__parcellations__parcellation_id__volumes_get"] + get: operations["get_volumes_for_parcellation_atlases__atlas_id__parcellations__parcellation_id__volumes_get"] } "/atlases/{atlas_id}/parcellations/{parcellation_id}": { /** Returns one parcellation for given id. */ - get: operations["get_parcellation_by_id_atlases__atlas_id__parcellations__parcellation_id__get"] + get: operations["get_single_parcellation_detail_atlases__atlas_id__parcellations__parcellation_id__get"] } "/atlases/{atlas_id}/spaces": { /** Returns all spaces that are defined in the siibra client. */ @@ -64,18 +64,18 @@ export interface paths { * Get a detailed view on a single spatial feature. * A parcellation id and region id can be provided optional to get more details. */ - get: operations["get_single_spatial_feature_detail_atlases__atlas_id__spaces__space_id__features__feature_id__get"] + get: operations["get_single_detailed_spatial_feature_atlases__atlas_id__spaces__space_id__features__feature_id__get"] } "/atlases/{atlas_id}/spaces/{space_id}/features": { /** Return all possible feature names and links to get more details */ - get: operations["get_spatial_features_from_space_atlases__atlas_id__spaces__space_id__features_get"] + get: operations["get_all_spatial_features_for_space_atlases__atlas_id__spaces__space_id__features_get"] } "/atlases/{atlas_id}/spaces/{space_id}/volumes": { - get: operations["get_one_space_volumes_atlases__atlas_id__spaces__space_id__volumes_get"] + get: operations["get_volumes_for_space_atlases__atlas_id__spaces__space_id__volumes_get"] } "/atlases/{atlas_id}/spaces/{space_id}": { /** Returns one space for given id, with links to further resources */ - get: operations["get_one_space_by_id_atlases__atlas_id__spaces__space_id__get"] + get: operations["get_single_space_detail_atlases__atlas_id__spaces__space_id__get"] } "/atlases": { /** Get all atlases known by siibra. */ @@ -153,10 +153,10 @@ export interface components { /** @Id */ "@id": string /** - * Type + * @Type * @constant */ - type?: "siibra/core/dataset" + "@type"?: "siibra/core/dataset" metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"] /** Urls */ urls: components["schemas"]["Url"][] @@ -181,6 +181,8 @@ export interface components { } /** BoundingBoxModel */ BoundingBoxModel: { + /** @Type */ + "@type": string /** Space */ space: { [key: string]: string } center: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model"] @@ -195,11 +197,8 @@ export interface components { ConnectivityMatrixDataModel: { /** @Id */ "@id": string - /** - * Type - * @constant - */ - type?: "siibra/features/connectivity" + /** @Type */ + "@type": string /** Name */ name: string /** Parcellations */ @@ -213,10 +212,10 @@ export interface components { /** @Id */ "@id": string /** - * Type + * @Type * @constant */ - type?: "siibra/core/dataset" + "@type"?: "siibra/core/dataset" metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"] /** Urls */ urls: components["schemas"]["Url"][] @@ -310,12 +309,16 @@ export interface components { } /** IEEGContactPointModel */ IEEGContactPointModel: { + /** Inroi */ + inRoi?: boolean /** Id */ id: string point: components["schemas"]["siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model"] } /** IEEGElectrodeModel */ IEEGElectrodeModel: { + /** Inroi */ + inRoi?: boolean /** Electrode Id */ electrode_id: string /** Contact Points */ @@ -325,13 +328,15 @@ export interface components { } /** IEEGSessionModel */ IEEGSessionModel: { + /** Inroi */ + inRoi?: boolean /** @Id */ "@id": string /** - * Type + * @Type * @constant */ - type?: "siibra/features/ieegSession" + "@type"?: "siibra/features/ieegSession" dataset: components["schemas"]["DatasetJsonModel"] /** Sub Id */ sub_id: string @@ -459,10 +464,10 @@ export interface components { /** @Id */ "@id": string /** - * Type + * @Type * @constant */ - type?: "siibra/features/receptor" + "@type"?: "siibra/features/receptor" metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"] /** Urls */ urls: components["schemas"]["Url"][] @@ -734,10 +739,10 @@ export interface components { /** @Id */ "@id": string /** - * Type + * @Type * @constant */ - type?: "siibra/features/voi" + "@type"?: "siibra/features/voi" metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"] /** Urls */ urls: components["schemas"]["Url"][] @@ -784,10 +789,10 @@ export interface components { /** @Id */ "@id": string /** - * Type + * @Type * @constant */ - type?: "siibra/core/dataset" + "@type"?: "siibra/core/dataset" metadata: components["schemas"]["siibra__openminds__core__v4__products__datasetVersion__Model"] /** Urls */ urls: components["schemas"]["Url"][] @@ -1333,7 +1338,7 @@ export interface operations { } } /** Returns all regional features for a region. */ - get_all_features_for_region_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features_get: { + get_all_regional_features_for_region_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features_get: { parameters: { path: { atlas_id: string @@ -1363,7 +1368,7 @@ export interface operations { } } /** Returns a feature for a region, as defined by by the modality and feature ID */ - get_regional_modality_by_id_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features__feature_id__get: { + get_single_detailed_regional_feature_atlases__atlas_id__parcellations__parcellation_id__regions__region_id__features__feature_id__get: { parameters: { path: { atlas_id: string @@ -1498,7 +1503,7 @@ export interface operations { } } /** Returns a global feature for a specific modality id. */ - get_single_global_feature_detail_atlases__atlas_id__parcellations__parcellation_id__features__feature_id__get: { + get_single_detailed_global_feature_atlases__atlas_id__parcellations__parcellation_id__features__feature_id__get: { parameters: { path: { atlas_id: string @@ -1525,7 +1530,7 @@ export interface operations { } } /** Returns all global features for a parcellation. */ - get_global_features_names_atlases__atlas_id__parcellations__parcellation_id__features_get: { + get_all_global_features_for_parcellation_atlases__atlas_id__parcellations__parcellation_id__features_get: { parameters: { path: { atlas_id: string @@ -1555,7 +1560,7 @@ export interface operations { } } /** Returns one parcellation for given id. */ - get_volumes_by_id_atlases__atlas_id__parcellations__parcellation_id__volumes_get: { + get_volumes_for_parcellation_atlases__atlas_id__parcellations__parcellation_id__volumes_get: { parameters: { path: { atlas_id: string @@ -1578,7 +1583,7 @@ export interface operations { } } /** Returns one parcellation for given id. */ - get_parcellation_by_id_atlases__atlas_id__parcellations__parcellation_id__get: { + get_single_parcellation_detail_atlases__atlas_id__parcellations__parcellation_id__get: { parameters: { path: { atlas_id: string @@ -1674,7 +1679,7 @@ export interface operations { * Get a detailed view on a single spatial feature. * A parcellation id and region id can be provided optional to get more details. */ - get_single_spatial_feature_detail_atlases__atlas_id__spaces__space_id__features__feature_id__get: { + get_single_detailed_spatial_feature_atlases__atlas_id__spaces__space_id__features__feature_id__get: { parameters: { path: { feature_id: string @@ -1682,8 +1687,8 @@ export interface operations { space_id: string } query: { - parcellation_id: string - region: string + parcellation_id?: string + region?: string bbox?: string } } @@ -1706,7 +1711,7 @@ export interface operations { } } /** Return all possible feature names and links to get more details */ - get_spatial_features_from_space_atlases__atlas_id__spaces__space_id__features_get: { + get_all_spatial_features_for_space_atlases__atlas_id__spaces__space_id__features_get: { parameters: { path: { atlas_id: string @@ -1736,7 +1741,7 @@ export interface operations { } } } - get_one_space_volumes_atlases__atlas_id__spaces__space_id__volumes_get: { + get_volumes_for_space_atlases__atlas_id__spaces__space_id__volumes_get: { parameters: { path: { atlas_id: string @@ -1759,7 +1764,7 @@ export interface operations { } } /** Returns one space for given id, with links to further resources */ - get_one_space_by_id_atlases__atlas_id__spaces__space_id__get: { + get_single_space_detail_atlases__atlas_id__spaces__space_id__get: { parameters: { path: { atlas_id: string diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts index f4162b72d80ec585472dbe40e77423336f0ec630..51fde4f0cd5695f6055398e8bd9ec39ed469c541 100644 --- a/src/atlasComponents/sapi/stories.base.ts +++ b/src/atlasComponents/sapi/stories.base.ts @@ -1,5 +1,5 @@ import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel } from "." -import { SapiParcellationFeatureModel } from "./type" +import { cleanIeegSessionDatasets, SapiParcellationFeatureModel, SapiSpatialFeatureModel, SxplrCleanedFeatureModel } from "./type" import addons from '@storybook/addons'; import { DARKTHEME } from "src/util/injectionTokens"; import { APP_INITIALIZER, NgZone } from "@angular/core"; @@ -101,11 +101,11 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> { return await getParcRegions(atlasId.human, parcId.human.jba29, spaceId.human.mni152) } -export async function getHoc1Left(spaceId=null): 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%20left`)).json() + return await (await fetch(`${SAPI.bsEndpoint}/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%20left?space_id=${encodeURIComponent(spaceId)}`)).json() + return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function get44Left(spaceId=null): Promise<SapiRegionModel> { @@ -115,14 +115,37 @@ export async function get44Left(spaceId=null): Promise<SapiRegionModel> { return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json() } -export async function getHoc1Features(): Promise<SapiRegionalFeatureModel[]> { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20left/features`)).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() + return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession")) } -export async function getHoc1FeatureDetail(featId: string): Promise<SapiRegionalFeatureModel>{ - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20left/features/${encodeURIComponent(featId)}`)).json() +export async function getHoc1RightFeatures(): Promise<SapiRegionalFeatureModel[]> { + return await (await fetch(`${SAPI.bsEndpoint}/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() } export async function getJba29Features(): Promise<SapiParcellationFeatureModel[]> { return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json() } + +export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureModel[]>{ + const bbox = [ + [-1000, -1000, -1000], + [1000, 1000, 1000] + ] + const url = new URL(`${SAPI.bsEndpoint}/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`) + 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/sapi/type.ts b/src/atlasComponents/sapi/type.ts index 77a94d21f0ebc6ad92236fe2324642bf746615a2..97519d6fd6fcf91e68880e7f8e359f508897815b 100644 --- a/src/atlasComponents/sapi/type.ts +++ b/src/atlasComponents/sapi/type.ts @@ -1,6 +1,6 @@ import { OperatorFunction } from "rxjs" import { map } from "rxjs/operators" -import { components } from "./schema" +import { components, operations, paths } from "./schema" export type IdName = { id: string @@ -15,35 +15,17 @@ export type SapiAtlasModel = components["schemas"]["SapiAtlasModel"] export type SapiSpaceModel = components["schemas"]["SapiSpaceModel"] export type SapiParcellationModel = components["schemas"]["SapiParcellationModel"] export type SapiRegionModel = components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model"] -export type SapiRegionMapInfoModel = components["schemas"]["NiiMetadataModel"] -export type SapiSpatialFeatureModel = components["schemas"]["VOIDataModel"] +export type SapiRegionMapInfoModel = components["schemas"]["NiiMetadataModel"] export type SapiVOIDataResponse = components["schemas"]["VOIDataModel"] - export type SapiVolumeModel = components["schemas"]["VolumeModel"] export type SapiDatasetModel = components["schemas"]["DatasetJsonModel"] - export type SpyNpArrayDataModel = components["schemas"]["NpArrayDataModel"] - - -export function FeatureTypeGuard(input: SapiFeatureModel) { - if (input.type === "siibra/core/dataset") { - return input as SapiDatasetModel - } - if (input.type === "siibra/features/connectivity") { - return input as SapiParcellationFeatureMatrixModel - } - if (input.type === "siibra/features/receptor") { - return input as SapiRegionalFeatureReceptorModel - } - if (input.type === "siibra/features/voi") { - return input as SapiVOIDataResponse - } - if (input.type === "spy/serialization-error") { - return input as SapiSerializationErrorModel - } - throw new Error(`cannot parse type: ${input}`) -} +export type SapiIeegSessionModel = components["schemas"]["IEEGSessionModel"] +/** + * utility types + */ +type PathReturn<T extends keyof paths> = Required<paths[T]["get"]["responses"][200]["content"]["application/json"]> /** * serialization error type @@ -51,16 +33,53 @@ export function FeatureTypeGuard(input: SapiFeatureModel) { export type SapiSerializationErrorModel = components["schemas"]["SerializationErrorModel"] /** - * datafeatures + * datafeatures from operations */ -export type SapiRegionalFeatureReceptorModel = components["schemas"]["ReceptorDatasetModel"] -export type SapiRegionalFeatureModel = components["schemas"]["BaseDatasetJsonModel"] | SapiRegionalFeatureReceptorModel -export type SapiParcellationFeatureMatrixModel = components["schemas"]["ConnectivityMatrixDataModel"] -export type SapiParcellationFeatureModel = SapiParcellationFeatureMatrixModel | SapiSerializationErrorModel +export type SapiRegionalFeatureModel = PathReturn<"/atlases/{atlas_id}/parcellations/{parcellation_id}/regions/{region_id}/features/{feature_id}"> +export type SapiParcellationFeatureModel = PathReturn<"/atlases/{atlas_id}/parcellations/{parcellation_id}/features/{feature_id}"> +export type SapiSpatialFeatureModel = PathReturn<"/atlases/{atlas_id}/spaces/{space_id}/features/{feature_id}"> export type SapiFeatureModel = SapiRegionalFeatureModel | SapiSpatialFeatureModel | SapiParcellationFeatureModel +/** + * specific data features + */ + +export type SapiRegionalFeatureReceptorModel = components["schemas"]["ReceptorDatasetModel"] +export type SapiParcellationFeatureMatrixModel = components["schemas"]["ConnectivityMatrixDataModel"] + + +export const CLEANED_IEEG_DATASET_TYPE = 'sxplr/cleanedIeegDataset' +export type CleanedIeegDataset = Required< + Omit<SapiDatasetModel, "@type"> & { + '@type': 'sxplr/cleanedIeegDataset' + sessions: Record<string, Omit<SapiIeegSessionModel, "dataset">> + } +> + +export function cleanIeegSessionDatasets(ieegSessions: SapiIeegSessionModel[]): CleanedIeegDataset[]{ + const returnArr: CleanedIeegDataset[] = [] + for (const sess of ieegSessions) { + const { dataset, ...itemToAppend } = sess + const existing = returnArr.find(it => it["@id"] === dataset["@id"]) + if (!existing) { + returnArr.push({ + ...dataset, + '@type': CLEANED_IEEG_DATASET_TYPE, + sessions: { + [sess.sub_id]: itemToAppend + } + }) + continue + } + existing.sessions[sess.sub_id] = itemToAppend + } + return returnArr +} + +export type SxplrCleanedFeatureModel = CleanedIeegDataset + export function guardPipe< InputType, GuardType extends InputType diff --git a/src/atlasComponents/sapiViews/core/atlas/module.ts b/src/atlasComponents/sapiViews/core/atlas/module.ts index a41cdb25c60ea1255837923285522a32ca85dfcc..691d41303c80c52229423586c9243d27fc419479 100644 --- a/src/atlasComponents/sapiViews/core/atlas/module.ts +++ b/src/atlasComponents/sapiViews/core/atlas/module.ts @@ -7,6 +7,7 @@ import { SapiViewsUtilModule } from "../../util"; import { SapiViewsCoreParcellationModule } from "../parcellation"; import { SapiViewsCoreSpaceModule } from "../space"; import { SapiViewsCoreAtlasAtlasDropdownSelector } from "./dropdownAtlasSelector/dropdownAtlasSelector.component"; +import { SapiViewsCoreAtlasSplashScreen } from "./splashScreen/splashScreen.component"; import { SapiViewsCoreAtlasAtlasTmplParcSelector } from "./tmplParcSelector/tmplParcSelector.component"; @NgModule({ @@ -22,10 +23,12 @@ import { SapiViewsCoreAtlasAtlasTmplParcSelector } from "./tmplParcSelector/tmpl declarations: [ SapiViewsCoreAtlasAtlasDropdownSelector, SapiViewsCoreAtlasAtlasTmplParcSelector, + SapiViewsCoreAtlasSplashScreen, ], exports: [ SapiViewsCoreAtlasAtlasDropdownSelector, SapiViewsCoreAtlasAtlasTmplParcSelector, + SapiViewsCoreAtlasSplashScreen, ] }) diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.component.ts similarity index 95% rename from src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts rename to src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.component.ts index 775b1a486bc18ec333ce8aec1a32ccdcc8c14091..056e3660c4243ee41bff9e5090354bd6e6fb2deb 100644 --- a/src/atlasComponents/splashScreen/splashScreen/splashScreen.component.ts +++ b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.component.ts @@ -14,7 +14,7 @@ import { atlasSelection } from "src/state" changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SplashScreen { +export class SapiViewsCoreAtlasSplashScreen { public finishedLoading: boolean = false diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.style.css b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.style.css similarity index 100% rename from src/atlasComponents/splashScreen/splashScreen/splashScreen.style.css rename to src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.style.css diff --git a/src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.template.html similarity index 100% rename from src/atlasComponents/splashScreen/splashScreen/splashScreen.template.html rename to src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.template.html diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts index ff2a0406288b6db8a48c20e7de29f6b29edcb0a3..acec7a744768e383bdb511d15b4a8d537b9e0633 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.stories.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI } from "src/atlasComponents/sapi" -import { getHoc1FeatureDetail, getHoc1Features, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1RightFeatureDetail, getHoc1RightFeatures, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { SapiViewsCoreDatasetModule } from ".." import { DatasetView } from "./dataset.component" @@ -35,9 +35,9 @@ const Template: Story<DatasetView> = (args: DatasetView, { loaded }) => { } const loadFeat = async () => { - const features = await getHoc1Features() - const receptorfeat = features.find(f => f.type === "siibra/core/dataset") - const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) + const features = await getHoc1RightFeatures() + const receptorfeat = features.find(f => f['@type'] === "siibra/core/dataset") + const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) return { feature } diff --git a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts index 1fc06697565f11e974950f757052ead015238ed6..ec74e25aadf0b5f3d00ef164efd2ff19238259bb 100644 --- a/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts +++ b/src/atlasComponents/sapiViews/core/region/region/chip/region.chip.stories.ts @@ -1,8 +1,8 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" import { Meta, moduleMetadata, Story } from "@storybook/angular" -import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi" -import { atlasId, getAtlas, provideDarkTheme, getParc, getHumanAtlas, getJba29, getMni152, getHoc1Left, get44Left } from "src/atlasComponents/sapi/stories.base" +import { SAPI } from "src/atlasComponents/sapi" +import { provideDarkTheme, getHumanAtlas, getJba29, getMni152, getHoc1Right, get44Left } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { SapiViewsCoreRegionModule } from "../../module" import { SapiViewsCoreRegionRegionChip } from "./region.chip.component" @@ -72,7 +72,7 @@ Default.loaders = [ const atlas = await getHumanAtlas() const parcellation = await getJba29() const template = await getMni152() - const region = await getHoc1Left() + const region = await getHoc1Right() return { atlas, diff --git a/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.stories.ts b/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.stories.ts index 900bd2e0a0f01a7ec5797aec22c19bb539aee4a7..abc0bef7a287e6f8e8d2f284d52d0bf0b99f9258 100644 --- a/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.stories.ts +++ b/src/atlasComponents/sapiViews/core/region/region/listItem/region.listItem.stories.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI } from "src/atlasComponents/sapi" -import { getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { SapiViewsCoreRegionModule } from "../../module" import { SapiViewsCoreRegionRegionListItem } from "./region.listItem.component" @@ -55,7 +55,7 @@ const loadRegions = async () => { const human = await getHumanAtlas() const mni152 = await getMni152() const jba29 = await getJba29() - const hoc1left = await getHoc1Left(mni152["@id"]) + const hoc1left = await getHoc1Right(mni152["@id"]) return { human, diff --git a/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts index d542fc05dd988f4f599b292633b20445402f8dea..9c3cad17f525199cc4f91b21a890dff98c822235 100644 --- a/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts +++ b/src/atlasComponents/sapiViews/core/region/region/region.features.directive.ts @@ -2,6 +2,7 @@ import { Directive, OnChanges, SimpleChanges } from "@angular/core"; import { BehaviorSubject, merge, Observable } from "rxjs"; import { switchMap, filter, startWith, shareReplay, mapTo, delay, tap } from "rxjs/operators"; import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type"; import { SapiViewsCoreRegionRegionBase } from "./region.base.directive"; @Directive({ @@ -27,7 +28,7 @@ export class SapiViewsCoreRegionRegionalFeatureDirective extends SapiViewsCoreRe super(sapi) } - private features$: Observable<SapiRegionalFeatureModel[]> = this.ATPR$.pipe( + private features$: Observable<(SapiRegionalFeatureModel|SxplrCleanedFeatureModel)[]> = this.ATPR$.pipe( filter(arg => { if (!arg) return false const { atlas, parcellation, region, template } = arg @@ -36,7 +37,7 @@ export class SapiViewsCoreRegionRegionalFeatureDirective extends SapiViewsCoreRe switchMap(({ atlas, parcellation, region, template }) => this.sapi.getRegionFeatures(atlas["@id"], parcellation["@id"], template["@id"], region.name)), ) - public listOfFeatures$: Observable<SapiRegionalFeatureModel[]> = this.features$.pipe( + public listOfFeatures$: Observable<(SapiRegionalFeatureModel|SxplrCleanedFeatureModel)[]> = this.features$.pipe( startWith([]), shareReplay(1), ) diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts index 551ed5a081fb5536fc91ff2e2d4c2196aed3740f..6e9f8907cd68facc3c60f31b6e28cadbb4873573 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.stories.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common" import { HttpClientModule } from "@angular/common/http" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI } from "src/atlasComponents/sapi" -import { atlasId, getHoc1Left, getHumanAtlas, getJba29, getJba29Regions, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1Right, getHumanAtlas, getJba29, getJba29Regions, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { SapiViewsCoreRegionModule } from "../../module" import { SapiViewsCoreRegionRegionRich } from "./region.rich.component" import { action } from '@storybook/addon-actions'; @@ -61,7 +61,7 @@ const loadRegions = async () => { const human = await getHumanAtlas() const mni152 = await getMni152() const jba29 = await getJba29() - const hoc1left = await getHoc1Left(mni152["@id"]) + const hoc1left = await getHoc1Right(mni152["@id"]) return { human, mni152, diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts index 69cd8b70d89a6d9fc65be10e8448da65ff25ce88..44fa05d6623595e6100e4e86149f52c6fe5b4bf1 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts @@ -31,7 +31,7 @@ export class ConnectivityMatrixView implements OnChanges, AfterViewInit{ private async fetchMatrixData(){ this.matrixData = null - const matrix = await this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]).getFeatureInstance(this.featureId) + const matrix = await this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]).getFeatureInstance(this.featureId).toPromise() if ((matrix as SapiSerializationErrorModel)?.type === "spy/serialization-error") { return } diff --git a/src/atlasComponents/sapiViews/features/entry/entry.component.ts b/src/atlasComponents/sapiViews/features/entry/entry.component.ts index 79ee7640af146039fb2fc14d566c12daf7bef396..53b27fb3087946cb260a1a2087a93e1cacb776bb 100644 --- a/src/atlasComponents/sapiViews/features/entry/entry.component.ts +++ b/src/atlasComponents/sapiViews/features/entry/entry.component.ts @@ -1,5 +1,9 @@ -import { Component, Input } from "@angular/core"; -import { SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { Component, Input, OnDestroy } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { AnnotationLayer, TNgAnnotationPoint } from "src/atlasComponents/annotations"; +import { SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel, CLEANED_IEEG_DATASET_TYPE } from "src/atlasComponents/sapi"; +import { IeegOnFocusEvent, ContactPoint, Electrode, Session, IeegOnDefocusEvent } from "../ieeg"; +import { atlasSelection, annotation, atlasAppearance } from "src/state" @Component({ selector: 'sxplr-sapiviews-features-entry', @@ -9,7 +13,7 @@ import { SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceMode ] }) -export class FeatureEntryCmp{ +export class FeatureEntryCmp implements OnDestroy{ /** * in future, hopefully feature detail can be queried with just id, @@ -31,6 +35,145 @@ export class FeatureEntryCmp{ feature: SapiFeatureModel featureType = { - receptor: "siibra/features/receptor" + receptor: "siibra/features/receptor", + ieeg: CLEANED_IEEG_DATASET_TYPE + } + + static readonly IEEG_ANNOTATION_LAYER_RED = `ieeg-annotation-layer-red` + static readonly IEEG_ANNOTATION_LAYER_WHITE = `ieeg-annotation-layer-white` + private ieegRedAnnLayer: AnnotationLayer + private ieegWhiteAnnLayer: AnnotationLayer + + ieegOnFocus(ev: IeegOnFocusEvent){ + if (ev.contactPoint) { + /** + * navigate to the point + */ + this.store.dispatch( + atlasSelection.actions.navigateTo({ + navigation: { + position: ev.contactPoint.point.coordinates.map(v => v.value * 1e6) + }, + animation: true + }) + ) + return + } + if (ev.session) { + /** + * + */ + if (!this.ieegRedAnnLayer) { + this.ieegRedAnnLayer = new AnnotationLayer(FeatureEntryCmp.IEEG_ANNOTATION_LAYER_RED, "#ff0000") + } + if (!this.ieegWhiteAnnLayer) { + this.ieegWhiteAnnLayer = new AnnotationLayer(FeatureEntryCmp.IEEG_ANNOTATION_LAYER_WHITE, "#ffffff") + } + const allInRoiPoints: TNgAnnotationPoint[] = this.getPointsFromSession(ev.session, true) + const allNonInRoiPoints: TNgAnnotationPoint[] = this.getPointsFromSession(ev.session, false) + for (const pt of allInRoiPoints) { + this.ieegRedAnnLayer.addAnnotation(pt) + } + for (const pt of allNonInRoiPoints) { + this.ieegWhiteAnnLayer.addAnnotation(pt) + } + this.store.dispatch( + annotation.actions.addAnnotations({ + annotations: [...allInRoiPoints, ...allNonInRoiPoints].map(p => { + return { "@id": p.id } + }) + }) + ) + this.store.dispatch( + atlasAppearance.actions.setOctantRemoval({ + flag: false + }) + ) + } + } + ieegOnDefocus(ev: IeegOnDefocusEvent){ + if (ev.session) { + const allInRoiPoints: TNgAnnotationPoint[] = this.getPointsFromSession(ev.session, true) + const allNonInRoiPoints: TNgAnnotationPoint[] = this.getPointsFromSession(ev.session, false) + + this.store.dispatch( + annotation.actions.rmAnnotations({ + annotations: [...allInRoiPoints, ...allNonInRoiPoints].map(p => { + return { "@id": p.id } + }) + }) + ) + + this.store.dispatch( + atlasAppearance.actions.setOctantRemoval({ + flag: true + }) + ) + + if (this.ieegRedAnnLayer) { + for (const pt of allInRoiPoints) { + this.ieegRedAnnLayer.removeAnnotation(pt) + } + } + + if (this.ieegWhiteAnnLayer) { + for (const pt of allNonInRoiPoints) { + this.ieegWhiteAnnLayer.removeAnnotation(pt) + } + } + + + } + } + ngOnDestroy(): void { + if (this.ieegRedAnnLayer) this.ieegRedAnnLayer.dispose() + if (this.ieegWhiteAnnLayer) this.ieegWhiteAnnLayer.dispose() + } + + constructor( + private store: Store + ){ + + } + + private getPointsFromSession(session: Session<string>, inRoi: boolean):TNgAnnotationPoint[]{ + const allPoints: TNgAnnotationPoint[] = [] + for (const electrodeKey in session.electrodes) { + const electrode = session.electrodes[electrodeKey] + const points = this.getPointsFromElectrode(electrode, inRoi) + allPoints.push(...points) + } + return allPoints.map(pt => { + return { + ...pt, + id: `${session.sub_id}:${pt.id}` + } + }) + } + + private getPointsFromElectrode(electrode: Electrode<string>, inRoi: boolean): TNgAnnotationPoint[] { + const allPoints: TNgAnnotationPoint[] = [] + for (const ctptKey in electrode.contact_points) { + const ctpt = electrode.contact_points[ctptKey] + if (!inRoi !== !ctpt.inRoi) { + continue + } + const point = this.getPointFromCtPt(ctpt) + allPoints.push(point) + } + return allPoints.map(pt => { + return { + ...pt, + id: `${electrode.electrode_id}:${pt.id}` + } + }) + } + + private getPointFromCtPt(ctpt: ContactPoint<string>): TNgAnnotationPoint { + return { + id: ctpt.id, + point: ctpt.point.coordinates.map(coord => coord.value * 1e6 ) as [number, number, number], + type: 'point' + } } } diff --git a/src/atlasComponents/sapiViews/features/entry/entry.template.html b/src/atlasComponents/sapiViews/features/entry/entry.template.html index 0affeaa6282725139eb7fd7af2258061ff7d1b44..38a00def6eba0d9546e5c729431e4d53a05292bb 100644 --- a/src/atlasComponents/sapiViews/features/entry/entry.template.html +++ b/src/atlasComponents/sapiViews/features/entry/entry.template.html @@ -1,9 +1,22 @@ -<div [ngSwitch]="feature?.type"> - <sxplr-sapiviews-features-receptor-entry *ngSwitchCase="featureType.receptor" +<div [ngSwitch]="feature?.['@type']"> + <sxplr-sapiviews-features-receptor-entry + *ngSwitchCase="featureType.receptor" [sxplr-sapiviews-features-receptor-atlas]="atlas" [sxplr-sapiviews-features-receptor-parcellation]="parcellation" [sxplr-sapiviews-features-receptor-template]="space" [sxplr-sapiviews-features-receptor-region]="region" [sxplr-sapiviews-features-receptor-featureid]="feature['@id']"> </sxplr-sapiviews-features-receptor-entry> + <sxplr-sapiviews-features-ieeg-ieegdataset + *ngSwitchCase="featureType.ieeg" + [sxplr-sapiviews-features-ieeg-ieegdataset-atlas]="atlas" + [sxplr-sapiviews-features-ieeg-ieegdataset-space]="space" + [sxplr-sapiviews-features-ieeg-ieegdataset-parcellation]="parcellation" + [sxplr-sapiviews-features-ieeg-ieegdataset-region]="region" + [sxplr-sapiviews-features-ieeg-ieegdataset-feature]="feature" + (sxplr-sapiviews-features-ieeg-ieegdataset-on-focus)="ieegOnFocus($event)" + (sxplr-sapiviews-features-ieeg-ieegdataset-on-defocus)="ieegOnDefocus($event)" + > + + </sxplr-sapiviews-features-ieeg-ieegdataset> </div> diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts index d7fcb675825a2a3692e9acb8800f6181ff359627..6fb2e383091f37a59d814180e1fa208575b2af74 100644 --- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts +++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from "@angular/core"; import { SapiFeatureModel, SapiRegionalFeatureModel, SapiSpatialFeatureModel, SapiParcellationFeatureModel } from "src/atlasComponents/sapi"; -import { SapiDatasetModel, SapiParcellationFeatureMatrixModel, SapiRegionalFeatureReceptorModel, SapiSerializationErrorModel, SapiVOIDataResponse } from "src/atlasComponents/sapi/type"; +import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiParcellationFeatureMatrixModel, SapiRegionalFeatureReceptorModel, SapiSerializationErrorModel, SapiVOIDataResponse, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type"; @Component({ selector: `sxplr-sapiviews-features-entry-list-item`, @@ -12,23 +12,27 @@ import { SapiDatasetModel, SapiParcellationFeatureMatrixModel, SapiRegionalFeatu export class SapiViewsFeaturesEntryListItem{ @Input('sxplr-sapiviews-features-entry-list-item-feature') - feature: SapiFeatureModel + feature: SapiFeatureModel | SxplrCleanedFeatureModel @Input('sxplr-sapiviews-features-entry-list-item-ripple') ripple = true get label(): string{ if (!this.feature) return null - const { type } = this.feature + const { '@type': type } = this.feature if ( type === "siibra/core/dataset" || type === "siibra/features/receptor" || - type === "siibra/features/voi" + type === "siibra/features/voi" || + type === CLEANED_IEEG_DATASET_TYPE ) { - return (this.feature as (SapiDatasetModel | SapiRegionalFeatureReceptorModel | SapiVOIDataResponse) ).metadata.fullName + return (this.feature as (SapiDatasetModel | SapiRegionalFeatureReceptorModel | SapiVOIDataResponse | CleanedIeegDataset) ).metadata.fullName } - if (type === "siibra/features/connectivity") { + if ( + type === "siibra/features/connectivity" || + type === "siibra/features/connectivity/streamlineCounts" + ) { return (this.feature as SapiParcellationFeatureMatrixModel).name } if (type === "spy/serialization-error") { diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html index 5bbc24ee23c600789b6c3000e4bcbe8d73a407e3..76f584328b3e46b5a240defb44e98e7cc2abe4a2 100644 --- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html +++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html @@ -1,8 +1,10 @@ <div matRipple [matRippleDisabled]="!ripple" class="sxplr-p-2"> - <mat-chip-list [ngSwitch]="feature.type" class="sxplr-scale-80 transform-origin-left-center"> - <mat-chip *ngSwitchCase="'siibra/features/receptor'" + <mat-chip-list + *ngIf="feature | featureBadgeFlag" + class="sxplr-scale-80 transform-origin-left-center"> + <mat-chip [color]="feature | featureBadgeColour" selected> {{ feature | featureBadgeName }} diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListitem.stories.ts b/src/atlasComponents/sapiViews/features/entryListItem/entryListitem.stories.ts index 434fe9e1ec4fee2181e27077222441200d56dc3a..a93cd9816a00d9f29567b8a55ecfe481030e8c31 100644 --- a/src/atlasComponents/sapiViews/features/entryListItem/entryListitem.stories.ts +++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListitem.stories.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SapiFeatureModel } from "src/atlasComponents/sapi" -import { getHoc1Features, getJba29Features, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1RightFeatures, getHoc1RightSpatialFeatures, getJba29Features, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { SapiViewsFeaturesEntryListItem } from "./entryListItem.component" import { Component, EventEmitter, Input, Output } from "@angular/core" @@ -66,7 +66,10 @@ RegionalFeatures.args = { } RegionalFeatures.loaders = [ async () => { - const features = await getHoc1Features() + const features = [ + ...(await getHoc1RightSpatialFeatures()), + ...(await getHoc1RightFeatures()) + ] return { features } diff --git a/src/atlasComponents/sapiViews/features/featureBadgeColor.pipe.ts b/src/atlasComponents/sapiViews/features/featureBadgeColor.pipe.ts index 4e7704c1afe9cf250cf0e8417a05ef0ecb2b8486..474d6a2adfe72257c35a406abde767ab7054f143 100644 --- a/src/atlasComponents/sapiViews/features/featureBadgeColor.pipe.ts +++ b/src/atlasComponents/sapiViews/features/featureBadgeColor.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { SapiFeatureModel } from "src/atlasComponents/sapi"; +import { SapiFeatureModel, SxplrCleanedFeatureModel, CLEANED_IEEG_DATASET_TYPE } from "src/atlasComponents/sapi"; @Pipe({ name: 'featureBadgeColour', @@ -7,10 +7,13 @@ import { SapiFeatureModel } from "src/atlasComponents/sapi"; }) export class FeatureBadgeColourPipe implements PipeTransform{ - public transform(regionalFeature: SapiFeatureModel) { - if (regionalFeature.type === "siibra/features/receptor") { + public transform(regionalFeature: SapiFeatureModel|SxplrCleanedFeatureModel) { + if (regionalFeature['@type'] === "siibra/features/receptor") { return "accent" } + if (regionalFeature['@type'] === CLEANED_IEEG_DATASET_TYPE) { + return "primary" + } return "default" } } diff --git a/src/atlasComponents/sapiViews/features/featureBadgeFlag.pipe.ts b/src/atlasComponents/sapiViews/features/featureBadgeFlag.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..986edffac941e522ee27d76dd3ab626aa40b993d --- /dev/null +++ b/src/atlasComponents/sapiViews/features/featureBadgeFlag.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { SapiFeatureModel, SxplrCleanedFeatureModel, CLEANED_IEEG_DATASET_TYPE } from "src/atlasComponents/sapi"; + +@Pipe({ + name: 'featureBadgeFlag', + pure: true +}) + +export class FeatureBadgeFlagPipe implements PipeTransform{ + public transform(regionalFeature: SapiFeatureModel|SxplrCleanedFeatureModel) { + return regionalFeature['@type'] === "siibra/features/receptor" + || regionalFeature['@type'] === CLEANED_IEEG_DATASET_TYPE + } +} diff --git a/src/atlasComponents/sapiViews/features/featureBadgeName.pipe.ts b/src/atlasComponents/sapiViews/features/featureBadgeName.pipe.ts index 8072a85081587b7847123fafdbedcb355b0efcca..33570440c6aa4c0f601d7171cac61cce7c42e712 100644 --- a/src/atlasComponents/sapiViews/features/featureBadgeName.pipe.ts +++ b/src/atlasComponents/sapiViews/features/featureBadgeName.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { SapiFeatureModel } from "src/atlasComponents/sapi"; +import { SapiFeatureModel, SxplrCleanedFeatureModel, CLEANED_IEEG_DATASET_TYPE } from "src/atlasComponents/sapi"; @Pipe({ name: 'featureBadgeName', @@ -7,10 +7,13 @@ import { SapiFeatureModel } from "src/atlasComponents/sapi"; }) export class FeatureBadgeNamePipe implements PipeTransform{ - public transform(regionalFeature: SapiFeatureModel) { - if (regionalFeature.type === "siibra/features/receptor") { + public transform(regionalFeature: SapiFeatureModel|SxplrCleanedFeatureModel) { + if (regionalFeature['@type'] === "siibra/features/receptor") { return "receptor density" } + if (regionalFeature["@type"] === CLEANED_IEEG_DATASET_TYPE) { + return "IEEG dataset" + } return null } } diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.component.ts b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..de23194ff366b475d57954b3050aa2cf94cfb256 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.component.ts @@ -0,0 +1,113 @@ +import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges } from "@angular/core"; +import { BehaviorSubject, forkJoin } from "rxjs"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { CleanedIeegDataset, cleanIeegSessionDatasets, SapiAtlasModel, SapiIeegSessionModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; + +export type Session<SId extends string> = CleanedIeegDataset['sessions'][SId] +export type Electrode<EId extends string> = Session<string>['electrodes'][EId] +export type ContactPoint<CId extends string> = Electrode<string>['contact_points'][CId] + +export type IeegOnFocusEvent = { + contactPoint: ContactPoint<string> + electrode: Electrode<string> + session: Session<string> +} + +export type IeegOnDefocusEvent = { + contactPoint: ContactPoint<string> + electrode: Electrode<string> + session: Session<string> +} + +@Component({ + selector: `sxplr-sapiviews-features-ieeg-ieegdataset`, + templateUrl: `./ieegDataset.template.html`, + styleUrls: [ + `./ieegDataset.style.css` + ] +}) + +export class IEEGDatasetCmp implements OnChanges{ + + @Input('sxplr-sapiviews-features-ieeg-ieegdataset-atlas') + public atlas: SapiAtlasModel + + @Input('sxplr-sapiviews-features-ieeg-ieegdataset-space') + public space: SapiSpaceModel + + @Input('sxplr-sapiviews-features-ieeg-ieegdataset-parcellation') + public parcellation: SapiParcellationModel + + @Input('sxplr-sapiviews-features-ieeg-ieegdataset-region') + public region: SapiRegionModel + + /** + * we must assume that the passed feature does not have the detail flag on + * We need to fetch the + */ + @Input('sxplr-sapiviews-features-ieeg-ieegdataset-feature') + public feature: CleanedIeegDataset + public detailedFeature: CleanedIeegDataset + + @Output('sxplr-sapiviews-features-ieeg-ieegdataset-on-focus') + public onFocus = new EventEmitter<IeegOnFocusEvent>() + + @Output('sxplr-sapiviews-features-ieeg-ieegdataset-on-defocus') + public onDefocus = new EventEmitter<IeegOnDefocusEvent>() + + public busy$ = new BehaviorSubject<boolean>(false) + + ngOnChanges(changes: SimpleChanges): void { + if (!this.feature) { + return + } + if (this.feature && this.feature["@type"] !== "sxplr/cleanedIeegDataset") { + throw new Error(`expected @type to be sxplr-cleaned-ieeg-dataset, but is ${this.feature['@type']}.`) + } + this.busy$.next(true) + + forkJoin( + Object.entries(this.feature.sessions).map(([ key, session ]) => { + return this.sapi.getSpace(this.atlas["@id"], this.space["@id"]).getFeatureInstance(session["@id"], { parcellationId: this.parcellation["@id"], region: this.region.name }) + }) + ).subscribe(feats => { + + const ieegSessions: SapiIeegSessionModel[] = feats.filter(feat => feat["@type"] === "siibra/features/ieegSession") + const features = cleanIeegSessionDatasets(ieegSessions) + const foundFeat = features.find(f => f["@id"] === this.feature["@id"]) + if (foundFeat) { + this.detailedFeature = foundFeat + } + this.busy$.next(false) + }) + } + + public onContactPointClicked(cpt: ContactPoint<string>, ele: Electrode<string>, sess: Session<string>){ + this.onFocus.emit({ + contactPoint: cpt, + electrode: ele, + session: sess + }) + } + + public onPanelOpen(session: Session<string>){ + this.onFocus.emit({ + contactPoint: null, + electrode: null, + session: session + }) + } + + public onPanelClose(session: Session<string>){ + this.onDefocus.emit({ + contactPoint: null, + electrode: null, + session: session + }) + } + + constructor(private sapi: SAPI){ + + } + +} diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.stories.ts b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9f08880e4525ab8c2d9f550abee83bf60c876fc --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.stories.ts @@ -0,0 +1,113 @@ +import { Meta, moduleMetadata, Story } from "@storybook/angular" +import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi" +import { getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme, getMni152SpatialFeatureHoc1Right } from "src/atlasComponents/sapi/stories.base" +import { SxplrSapiViewsFeaturesIeegModule } from ".." +import { Component } from "@angular/core" +import { cleanIeegSessionDatasets, SapiSpatialFeatureModel } from "src/atlasComponents/sapi/type" +import { action } from "@storybook/addon-actions" + +@Component({ + selector: 'ieeg-entry-wrapper-cmp', + template: ` + <sxplr-sapiviews-features-ieeg-ieegdataset + [sxplr-sapiviews-features-ieeg-ieegdataset-atlas]="atlas" + [sxplr-sapiviews-features-ieeg-ieegdataset-space]="template" + [sxplr-sapiviews-features-ieeg-ieegdataset-parcellation]="parcellation" + [sxplr-sapiviews-features-ieeg-ieegdataset-region]="region" + [sxplr-sapiviews-features-ieeg-ieegdataset-feature]="feature" + (sxplr-sapiviews-features-ieeg-ieegdataset-on-focus)="handleCtptClick($event)" + (sxplr-sapiviews-features-ieeg-ieegdataset-on-defocus)="handleOnDeFocus($event)" + > + </sxplr-sapiviews-features-ieeg-ieegdataset> + `, + styles: [ + ` + :host + { + display: block; + width: 20rem; + } + ` + ] +}) +class EntryWrappercls { + atlas: SapiAtlasModel + template: SapiSpaceModel + feature: SapiSpatialFeatureModel + parcellation: SapiParcellationModel + region: SapiRegionModel + + handleOnFocus(cpt: unknown){} + handleOnDeFocus(cpt: unknown) {} +} + +export default { + component: EntryWrappercls, + decorators: [ + moduleMetadata({ + imports: [ + SxplrSapiViewsFeaturesIeegModule, + ], + providers: [ + SAPI, + ...provideDarkTheme, + ], + declarations: [ + EntryWrappercls + ] + }) + ], +} as Meta + +const Template: Story<EntryWrappercls> = (args: EntryWrappercls, { loaded }) => { + const { atlas, parc, space, region, feature } = loaded + return ({ + props: { + ...args, + atlas: atlas, + parcellation: parc, + template: space, + region: region, + feature, + handleOnFocus: action('handleOnFocus'), + handleOnDeFocus: action('handleOnDeFocus') + }, + }) +} + +const loadFeat = async () => { + const atlas = await getHumanAtlas() + const space = await getMni152() + const parc = await getJba29() + const region = await getHoc1Right() + + const features = await getMni152SpatialFeatureHoc1Right() + const spatialFeats = features.filter(f => f["@type"] === "siibra/features/ieegSession") + const feature = cleanIeegSessionDatasets(spatialFeats)[0] + return { + atlas, + space, + parc, + region, + feature + } +} + +export const Default = Template.bind({}) +Default.args = { + +} +Default.loaders = [ + async () => { + const { + atlas, + space, + feature, + parc, + region, + } = await loadFeat() + return { + atlas, space, feature, parc, region + } + } +] diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.style.css b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.style.css new file mode 100644 index 0000000000000000000000000000000000000000..1c554eef70e8adae01cba4530c534a389ce6c93a --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.style.css @@ -0,0 +1,4 @@ +mat-form-field +{ + width: 100%; +} diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.template.html b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.template.html new file mode 100644 index 0000000000000000000000000000000000000000..af7464f5e24b254d42985ef55766f755696c4909 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/ieegDataset/ieegDataset.template.html @@ -0,0 +1,112 @@ +<spinner-cmp *ngIf="busy$ | async; else resultTmpl"> +</spinner-cmp> + +<ng-template #resultTmpl> + + <mat-accordion *ngIf="!!detailedFeature"> + <ng-template + ngFor + [ngForOf]="detailedFeature.sessions | keyvalue" + let-sessionKeyVal> + + <ng-template + [ngIf]="sessionKeyVal.value.inRoi" + [ngTemplateOutlet]="sessionTmpl" + [ngTemplateOutletContext]="{ + $implicit: sessionKeyVal.value + }"> + + </ng-template> + + </ng-template> + </mat-accordion> + +</ng-template> + +<!-- session template --> +<ng-template #sessionTmpl let-session> + <mat-expansion-panel + (opened)="onPanelOpen(session)" + (closed)="onPanelClose(session)"> + <mat-expansion-panel-header> + SessionID: {{ session.sub_id }} + </mat-expansion-panel-header> + + <ng-template matExpansionPanelContent> + <ng-template + ngFor + [ngForOf]="session.electrodes | keyvalue | inRoi" + let-electrodeKeyVal> + <ng-template + [ngTemplateOutlet]="electrodeTmpl" + [ngTemplateOutletContext]="{ + electrode: electrodeKeyVal.value, + session: session + }"> + + </ng-template> + </ng-template> + + <mat-divider></mat-divider> + <ng-template + ngFor + [ngForOf]="session.electrodes | keyvalue | inRoi : false" + let-electrodeKeyVal> + <div class="sxplr-very-muted"> + <ng-template + [ngTemplateOutlet]="electrodeTmpl" + [ngTemplateOutletContext]="{ + electrode: electrodeKeyVal.value, + session: session + }"> + + </ng-template> + </div> + </ng-template> + </ng-template> + </mat-expansion-panel> +</ng-template> + +<!-- electrode template --> +<ng-template + #electrodeTmpl + let-electrode="electrode" + let-session="session"> + <mat-form-field appearance="fill"> + <mat-label> + ElectrodeID: {{ electrode.electrode_id }} + </mat-label> + <mat-chip-list> + <ng-template + ngFor + [ngForOf]="electrode.contact_points | keyvalue" + let-contactPointKeyVal> + + <ng-template + [ngTemplateOutlet]="contactPointTmpl" + [ngTemplateOutletContext]="{ + contactPointKey: contactPointKeyVal.key, + contactPoint: contactPointKeyVal.value, + electrode: electrode, + session: session + }"> + </ng-template> + </ng-template> + </mat-chip-list> + </mat-form-field> +</ng-template> + +<!-- contact point template --> +<ng-template + #contactPointTmpl + let-contactPoint="contactPoint" + let-key="contactPointKey" + let-electrode="electrode" + let-session="session"> + <mat-chip + (click)="onContactPointClicked(contactPoint, electrode, session)" + [color]="contactPoint.inRoi ? 'primary' : 'default'" + selected> + {{ key }} + </mat-chip> +</ng-template> \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.component.ts b/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.component.ts deleted file mode 100644 index 9725ac61ea28969bf932c600e7b82521b9b0ab5f..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component } from "@angular/core"; - -type Landmark = {} & { - showInSliceView -} - -@Component({ - templateUrl: `./ieegSession.template.html`, - styleUrls: [ - `./ieegSession.style.css` - ] -}) - -export class IEEGSessionCmp{ - private loadedLms: Landmark[] - - private unloadLandmarks(){} - private loadlandmarks(lms: Landmark[]){} - private handleDatumExpansion(dataset: any){ - - } - private handleContactPtClick(contactPt){ - // navigate there - - } -} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.template.html b/src/atlasComponents/sapiViews/features/ieeg/ieegSession/ieegSession.template.html deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/atlasComponents/sapiViews/features/ieeg/inRoi.pipe.ts b/src/atlasComponents/sapiViews/features/ieeg/inRoi.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f215c8ccd96ff7af503abc521e3c6558383c234 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/inRoi.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +interface InRoi { + key: string + value: { + inRoi: boolean + } +} + +@Pipe({ + name: 'inRoi', + pure: true +}) + +export class InRoiPipe implements PipeTransform{ + public transform(list: InRoi[], inroi=true): InRoi[] { + return list.filter(it => it.value.inRoi === inroi) + } +} diff --git a/src/atlasComponents/sapiViews/features/ieeg/index.ts b/src/atlasComponents/sapiViews/features/ieeg/index.ts index 663fabd8603621b891428afeb0d68e861690cfd4..757808e777ca4f96b83bba521ab6c71d3903cfaa 100644 --- a/src/atlasComponents/sapiViews/features/ieeg/index.ts +++ b/src/atlasComponents/sapiViews/features/ieeg/index.ts @@ -1 +1,2 @@ -export { IEEGSessionCmp } from "./ieegSession/ieegSession.component" \ No newline at end of file +export { IEEGDatasetCmp, IeegOnFocusEvent, IeegOnDefocusEvent, ContactPoint, Electrode, Session } from "./ieegDataset/ieegDataset.component" +export { SxplrSapiViewsFeaturesIeegModule } from "./module" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/ieeg/module.ts b/src/atlasComponents/sapiViews/features/ieeg/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..c91161eac8e261121aaa58e583265961b324c007 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/ieeg/module.ts @@ -0,0 +1,33 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatDividerModule } from "@angular/material/divider"; +import { MatExpansionModule } from "@angular/material/expansion"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { SAPIModule } from "src/atlasComponents/sapi/module"; +import { SpinnerModule } from "src/components/spinner"; +import { IEEGDatasetCmp } from "./ieegDataset/ieegDataset.component"; +import { InRoiPipe } from "./inRoi.pipe"; + +@NgModule({ + imports: [ + CommonModule, + MatExpansionModule, + MatChipsModule, + MatFormFieldModule, + MatDividerModule, + BrowserAnimationsModule, + SpinnerModule, + SAPIModule, + ], + declarations: [ + IEEGDatasetCmp, + InRoiPipe, + ], + exports: [ + IEEGDatasetCmp, + ] +}) + +export class SxplrSapiViewsFeaturesIeegModule{} \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/module.ts b/src/atlasComponents/sapiViews/features/module.ts index 654d96c9ca9143516658b3505c9916a83df26858..6ee7b852c0400d3bb9b6af371f8f930c4bfb2ceb 100644 --- a/src/atlasComponents/sapiViews/features/module.ts +++ b/src/atlasComponents/sapiViews/features/module.ts @@ -5,12 +5,13 @@ import { appendScriptFactory, APPEND_SCRIPT_TOKEN } from "src/util/constants" import { FeatureEntryCmp } from "./entry/entry.component" import { SapiViewsFeaturesEntryListItem } from "./entryListItem/entryListItem.component" import { FeatureBadgeColourPipe } from "./featureBadgeColor.pipe" +import { FeatureBadgeFlagPipe } from "./featureBadgeFlag.pipe" import { FeatureBadgeNamePipe } from "./featureBadgeName.pipe" import * as ieeg from "./ieeg" import * as receptor from "./receptors" const { - IEEGSessionCmp + SxplrSapiViewsFeaturesIeegModule } = ieeg const { ReceptorViewModule @@ -20,13 +21,14 @@ const { imports: [ CommonModule, ReceptorViewModule, + SxplrSapiViewsFeaturesIeegModule, AngularMaterialModule, ], declarations: [ - IEEGSessionCmp, FeatureEntryCmp, FeatureBadgeNamePipe, FeatureBadgeColourPipe, + FeatureBadgeFlagPipe, SapiViewsFeaturesEntryListItem, ], providers: [ @@ -37,7 +39,6 @@ const { } ], exports: [ - IEEGSessionCmp, FeatureEntryCmp, SapiViewsFeaturesEntryListItem, ] diff --git a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts index 5ebfdce9870f959574846ebd56383535d376fd4b..093ec6a110eaca060b0bf3e48204026ee5ed1946 100644 --- a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts +++ b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts @@ -5,7 +5,7 @@ import { FormsModule } from "@angular/forms" import { BrowserAnimationsModule } from "@angular/platform-browser/animations" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi" -import { getHoc1FeatureDetail, getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152 } from "src/atlasComponents/sapi/stories.base" +import { getHoc1RightFeatureDetail, getHoc1RightFeatures, getHoc1Right, getHumanAtlas, getJba29, getMni152 } from "src/atlasComponents/sapi/stories.base" import { SapiRegionalFeatureReceptorModel } from "src/atlasComponents/sapi/type" import { AngularMaterialModule } from "src/sharedModules" import { Autoradiography } from "./autoradiography.component" @@ -106,11 +106,11 @@ const Template: Story<AutoRadiographWrapperCls> = (args: AutoRadiographWrapperCl const loadFeat = async () => { const atlas = await getHumanAtlas() const parc = await getJba29() - const region = await getHoc1Left() + const region = await getHoc1Right() const space = await getMni152() - const features = await getHoc1Features() - const receptorfeat = features.find(f => f.type === "siibra/features/receptor") - const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) + const features = await getHoc1RightFeatures() + const receptorfeat = features.find(f => f['@type'] === "siibra/features/receptor") + const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) return { atlas, parc, diff --git a/src/atlasComponents/sapiViews/features/receptors/base.ts b/src/atlasComponents/sapiViews/features/receptors/base.ts index b393969c20c50862e35eabe9b8efe49738bc57eb..cbcd75fc2460c847e907d4c312eb0eb9ea19b081 100644 --- a/src/atlasComponents/sapiViews/features/receptors/base.ts +++ b/src/atlasComponents/sapiViews/features/receptors/base.ts @@ -74,9 +74,9 @@ export abstract class BaseReceptor{ this.error = `featureId needs to be defined, but is not` return } - const result = await this.sapi.getRegion(this.atlas["@id"], this.parcellation["@id"], this.region.name).getFeatureInstance(this.featureId, this.template["@id"]) - if (result.type !== "siibra/features/receptor") { - throw new Error(`BaseReceptor Error. Expected .type to be "siibra/features/receptor", but was "${result.type}"`) + const result = await this.sapi.getRegion(this.atlas["@id"], this.parcellation["@id"], this.region.name).getFeatureInstance(this.featureId, this.template["@id"]).toPromise() + if (result["@type"] !== "siibra/features/receptor") { + throw new Error(`BaseReceptor Error. Expected .type to be "siibra/features/receptor", but was "${result['@type']}"`) } return result } diff --git a/src/atlasComponents/sapiViews/features/receptors/entry/entry.stories.ts b/src/atlasComponents/sapiViews/features/receptors/entry/entry.stories.ts index 0c69f821626c7590721e6e91054d4e39633ea633..63f07ee1ef4f6dd57c402e07d08a4e9c5d742f01 100644 --- a/src/atlasComponents/sapiViews/features/receptors/entry/entry.stories.ts +++ b/src/atlasComponents/sapiViews/features/receptors/entry/entry.stories.ts @@ -1,10 +1,8 @@ import { CommonModule, DOCUMENT } from "@angular/common" import { HttpClientModule } from "@angular/common/http" -import { FormsModule } from "@angular/forms" -import { BrowserAnimationsModule } from "@angular/platform-browser/animations" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi" -import { getHoc1FeatureDetail, getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1RightFeatureDetail, getHoc1RightFeatures, getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { Entry } from "./entry.component" import { ReceptorViewModule } from ".." @@ -89,11 +87,11 @@ const Template: Story<Entry> = (args: Entry, { loaded }) => { const loadFeat = async () => { const atlas = await getHumanAtlas() const parc = await getJba29() - const region = await getHoc1Left() + const region = await getHoc1Right() const space = await getMni152() - const features = await getHoc1Features() - const receptorfeat = features.find(f => f.type === "siibra/features/receptor") - const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) + const features = await getHoc1RightFeatures() + const receptorfeat = features.find(f => f['@type'] === "siibra/features/receptor") + const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) return { atlas, parc, diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts index 7c5da4f82f24a859db259439ce6fef8ec71e29c3..1c3327ac1470cc2a7d650aa3b5196f70958dbbe2 100644 --- a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts +++ b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts @@ -3,7 +3,7 @@ import { HttpClientModule } from "@angular/common/http" import { Component, EventEmitter, Output } from "@angular/core" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi" -import { getHoc1FeatureDetail, getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1RightFeatureDetail, getHoc1RightFeatures, getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { SapiRegionalFeatureReceptorModel } from "src/atlasComponents/sapi/type" import { AngularMaterialModule } from "src/sharedModules" import { appendScriptFactory, APPEND_SCRIPT_TOKEN } from "src/util/constants" @@ -88,11 +88,11 @@ const Template: Story<FingerprintWrapperCls> = (args: FingerprintWrapperCls, { l const loadFeat = async () => { const atlas = await getHumanAtlas() const parc = await getJba29() - const region = await getHoc1Left() + const region = await getHoc1Right() const space = await getMni152() - const features = await getHoc1Features() - const receptorfeat = features.find(f => f.type === "siibra/features/receptor") - const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) + const features = await getHoc1RightFeatures() + const receptorfeat = features.find(f => f['@type'] === "siibra/features/receptor") + const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) return { atlas, parc, diff --git a/src/atlasComponents/sapiViews/features/receptors/profile/profile.stories.ts b/src/atlasComponents/sapiViews/features/receptors/profile/profile.stories.ts index f4dc8b4bdaadb96aff3ca76ad0f69450c7a0aeef..83d309f52d008d14183329f0539cf179c0354867 100644 --- a/src/atlasComponents/sapiViews/features/receptors/profile/profile.stories.ts +++ b/src/atlasComponents/sapiViews/features/receptors/profile/profile.stories.ts @@ -6,7 +6,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { BehaviorSubject, Subject } from "rxjs" import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi" -import { addAddonEventListener, getHoc1FeatureDetail, getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { addAddonEventListener, getHoc1RightFeatureDetail, getHoc1RightFeatures, getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" import { SapiRegionalFeatureReceptorModel } from "src/atlasComponents/sapi/type" import { AngularMaterialModule } from "src/sharedModules" import { appendScriptFactory, APPEND_SCRIPT_TOKEN } from "src/util/constants" @@ -115,11 +115,11 @@ const Template: Story<ProfileWrapperCls> = (args: ProfileWrapperCls, { loaded }) const loadFeat = async () => { const atlas = await getHumanAtlas() const parc = await getJba29() - const region = await getHoc1Left() + const region = await getHoc1Right() const space = await getMni152() - const features = await getHoc1Features() - const receptorfeat = features.find(f => f.type === "siibra/features/receptor") - const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) + const features = await getHoc1RightFeatures() + const receptorfeat = features.find(f => f['@type'] === "siibra/features/receptor") + const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) return { atlas, parc, diff --git a/src/atlasComponents/sapiViews/richDataset.stories.ts b/src/atlasComponents/sapiViews/richDataset.stories.ts index c49035dbe93ff1d54764413660bea9afd4bbef9e..4904bf1d163283ab146de866abd5274bd2a19c29 100644 --- a/src/atlasComponents/sapiViews/richDataset.stories.ts +++ b/src/atlasComponents/sapiViews/richDataset.stories.ts @@ -3,9 +3,9 @@ import { HttpClientModule } from "@angular/common/http" import { Component } from "@angular/core" import { Meta, moduleMetadata, Story } from "@storybook/angular" import { SAPI } from "src/atlasComponents/sapi" -import { getHoc1FeatureDetail, getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, provideDarkTheme } from "src/atlasComponents/sapi/stories.base" +import { getHoc1RightFeatureDetail, getHoc1RightFeatures, getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme, getMni152SpatialFeatureHoc1Right } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" -import { SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "../sapi/type" +import { cleanIeegSessionDatasets, SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "../sapi/type" import { SapiViewsCoreDatasetModule } from "./core/datasets" import { SapiViewsFeaturesModule } from "./features" @@ -88,7 +88,7 @@ const loadRegionMetadata = async () => { const atlas = await getHumanAtlas() const parcellation = await getJba29() const template = await getMni152() - const region = await getHoc1Left() + const region = await getHoc1Right() return { atlas, parcellation, @@ -98,7 +98,7 @@ const loadRegionMetadata = async () => { } const loadFeat = async () => { - const features = await getHoc1Features() + const features = await getHoc1RightFeatures() return { features } } @@ -112,10 +112,25 @@ ReceptorDataset.loaders = [ }, async () => { const { features } = await loadFeat() - const receptorfeat = features.find(f => f.type === "siibra/features/receptor") - const feature = await getHoc1FeatureDetail(receptorfeat["@id"]) - return { - feature - } + const receptorfeat = features.find(f => f['@type'] === "siibra/features/receptor") + const feature = await getHoc1RightFeatureDetail(receptorfeat["@id"]) + return { feature } + } +] + +export const IeegDataset = Template.bind({}) +IeegDataset.args = { + +} +IeegDataset.loaders = [ + async () => { + return await loadRegionMetadata() + }, + async () => { + const features = await getMni152SpatialFeatureHoc1Right() + const spatialFeats = features.filter(f => f["@type"] === "siibra/features/ieegSession") + const feature = cleanIeegSessionDatasets(spatialFeats)[0] + + return { feature } } ] diff --git a/src/atlasComponents/splashScreen/index.ts b/src/atlasComponents/splashScreen/index.ts deleted file mode 100644 index 0750a03038aaecc1a9de8097d87ecd240121e418..0000000000000000000000000000000000000000 --- a/src/atlasComponents/splashScreen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SplashScreen } from "./splashScreen/splashScreen.component"; -export { SplashUiModule } from './module' \ No newline at end of file diff --git a/src/atlasComponents/splashScreen/module.ts b/src/atlasComponents/splashScreen/module.ts deleted file mode 100644 index 587e1217dad6d50acbe9f78b7f9074b3c0e844c8..0000000000000000000000000000000000000000 --- a/src/atlasComponents/splashScreen/module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { ComponentsModule } from "src/components"; -import { KgTosModule } from "src/ui/kgtos/module"; -import { AngularMaterialModule } from "src/sharedModules"; -import { UtilModule } from "src/util"; -import { SplashScreen } from "./splashScreen/splashScreen.component"; - -@NgModule({ - imports: [ - AngularMaterialModule, - CommonModule, - UtilModule, - KgTosModule, - ComponentsModule, - ], - declarations: [ - SplashScreen, - ], - exports: [ - SplashScreen, - ] -}) - -export class SplashUiModule{} \ No newline at end of file diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 546cd1238464c9830f8778d91bc38d1810c9c62c..1381520da9c9a7fae35e5203c9e40a5790c7aa07 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -16,6 +16,8 @@ import { retry } from 'common/util' import { MatSnackBar } from "@angular/material/snack-bar"; import { actions } from "src/state/atlasSelection"; import { atlasSelection } from "src/state"; +import { SapiSpaceModel } from "src/atlasComponents/sapi"; +import { AnnotationLayer } from "src/atlasComponents/annotations"; const LOCAL_STORAGE_KEY = 'userAnnotationKey' @@ -85,11 +87,11 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private previewNgAnnIds: string[] = [] - private ngAnnotationLayer: any + private annotationLayer: AnnotationLayer private activeToolName: string private forcedAnnotationRefresh$ = new BehaviorSubject(null) - private selectedTmpl: any + private selectedTmpl: SapiSpaceModel private selectedTmpl$ = this.store.pipe( select(atlasSelection.selectors.selectedTemplate), ) @@ -321,7 +323,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ */ this.subscription.push( nehubaViewer$.subscribe(() => { - this.ngAnnotationLayer = null + this.annotationLayer = null }) ) @@ -411,23 +413,12 @@ export class ModularUserAnnotationToolService implements OnDestroy{ this.clearAllPreviewAnnotations() } for (let idx = 0; idx < previewNgAnnotation.length; idx ++) { - const localAnnotations = this.ngAnnotationLayer.layer.localAnnotations - const annSpec = { - ...parseNgAnnotation(previewNgAnnotation[idx]), + const spec = { + ...previewNgAnnotation[idx], id: `${ModularUserAnnotationToolService.TMP_PREVIEW_ANN_ID}_${idx}` } - const annRef = localAnnotations.references.get(annSpec.id) - if (annRef) { - localAnnotations.update( - annRef, - annSpec - ) - } else { - localAnnotations.add( - annSpec - ) - } - this.previewNgAnnIds[idx] = annSpec.id + this.annotationLayer.updateAnnotation(spec) + this.previewNgAnnIds[idx] = spec.id } }) ) @@ -439,7 +430,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ this.forcedAnnotationRefresh$, this.spaceFilteredManagedAnnotations$.pipe( switchMap(switchMapWaitFor({ - condition: () => !!this.ngAnnotationLayer, + condition: () => !!this.annotationLayer, leading: true })), ) @@ -479,20 +470,8 @@ export class ModularUserAnnotationToolService implements OnDestroy{ this.deleteNgAnnotationById(annotation.id) continue } - if (!this.ngAnnotationLayer) continue - const localAnnotations = this.ngAnnotationLayer.layer.localAnnotations - const annRef = localAnnotations.references.get(annotation.id) - const annSpec = parseNgAnnotation(annotation) - if (annRef) { - localAnnotations.update( - annRef, - annSpec - ) - } else { - localAnnotations.add( - annSpec - ) - } + if (!this.annotationLayer) continue + this.annotationLayer.updateAnnotation(annotation) } }) ) @@ -506,26 +485,18 @@ export class ModularUserAnnotationToolService implements OnDestroy{ ).subscribe(viewerMode => { this.currMode = viewerMode if (viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) { - if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(true) + if (this.annotationLayer) this.annotationLayer.setVisible(true) else { const viewer = (window as any).viewer - const voxelSize = IAV_VOXEL_SIZES_NM[this.selectedTmpl.fullId] - if (!voxelSize) throw new Error(`voxelSize of ${this.selectedTmpl.fullId} cannot be found!`) - const layer = viewer.layerSpecification.getLayer( + const voxelSize = IAV_VOXEL_SIZES_NM[this.selectedTmpl["@id"]] + if (!voxelSize) throw new Error(`voxelSize of ${this.selectedTmpl["@id"]} cannot be found!`) + if (this.annotationLayer) { + this.annotationLayer.dispose() + } + this.annotationLayer = new AnnotationLayer( ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME, - { - ...ModularUserAnnotationToolService.USER_ANNOTATION_LAYER_SPEC, - // since voxel coordinates are no longer defined, so voxel size will always be 1/1/1 - transform: [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - ] - } + ModularUserAnnotationToolService.USER_ANNOTATION_LAYER_SPEC.annotationColor ) - this.ngAnnotationLayer = viewer.layerManager.addManagedLayer(layer) - /** * on template changes, the layer gets lost * force redraw annotations if layer needs to be recreated @@ -533,7 +504,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ this.forcedAnnotationRefresh$.next(null) } } else { - if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(false) + if (this.annotationLayer) this.annotationLayer.setVisible(false) } }) ) @@ -667,12 +638,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } private deleteNgAnnotationById(annId: string) { - const localAnnotations = this.ngAnnotationLayer.layer.localAnnotations - const annRef = localAnnotations.references.get(annId) - if (annRef) { - localAnnotations.delete(annRef) - localAnnotations.references.delete(annId) - } + this.annotationLayer.removeAnnotation({ id: annId }) } public defaultTool: AbsToolClass<any> @@ -750,7 +716,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private currMode: string switchAnnotationMode(mode: 'on' | 'off' | 'toggle' = 'toggle') { - let payload = null + let payload: 'annotating' = null if (mode === 'on') payload = ARIA_LABELS.VIEWER_MODE_ANNOTATING if (mode === 'off') { if (this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) payload = null @@ -763,7 +729,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } this.store.dispatch( actions.setViewerMode({ - viewerMode: "annotating" + viewerMode: payload }) ) } diff --git a/src/components/flatHierarchy/treeView/treeControl.ts b/src/components/flatHierarchy/treeView/treeControl.ts new file mode 100644 index 0000000000000000000000000000000000000000..e664e5984ce4350f3e3f6d746a28fd2487ad0e34 --- /dev/null +++ b/src/components/flatHierarchy/treeView/treeControl.ts @@ -0,0 +1,100 @@ + +type IsParent<T> = (child: T, parent: T) => boolean + +export class Tree<T extends object>{ + + private _nodes: T[] = [] + protected set nodes(nodes: T[]) { + if (nodes === this._nodes) return + this._nodes = nodes + this.resetWeakMaps() + } + protected get nodes() { + return this._nodes + } + + private _isParent: IsParent<T> = (c, p) => false + protected set isParent(fn: IsParent<T>){ + if (fn === this._isParent) return + this._isParent = fn + this.resetWeakMaps() + } + protected get isParent(){ + return this._isParent + } + public someAncestor(node: T, predicate: (anc: T) => boolean) { + const parent = this.getParent(node) + if (!parent) { + return false + } + if (predicate(parent)) { + return true + } + return this.someAncestor(parent, predicate) + } + public getParent(node: T): T { + if (!this.parentWeakMap.has(node)) { + for (const parentNode of this.nodes) { + if (this.isParent(node, parentNode)) { + this.parentWeakMap.set(node, parentNode) + break + } + } + } + return this.parentWeakMap.get(node) + } + public getChildren(node: T): T[] { + if (!this.childrenWeakMap.has(node)) { + const children = this.nodes.filter(childNode => this.isParent(childNode, node)) + this.childrenWeakMap.set(node, children) + for (const c of children) { + this.parentWeakMap.set(c, node) + } + } + return this.childrenWeakMap.get(node) + } + public getLevel(node: T): number { + let level = -1, cursor = node + const maxLevel = 32 + do { + if (level >= maxLevel) { + level = 0 + break + } + level++ + cursor = this.getParent(cursor) + } while (!!cursor) + return level + } + protected childrenWeakMap = new WeakMap<T, T[]>() + protected parentWeakMap = new WeakMap<T, T>() + protected nodeAncestorIsLast = new WeakMap<T, boolean[]>() + + get rootNodes() { + const root = this.nodes.filter(node => !this.getParent(node)) + for (const el of root) { + this.parentWeakMap.set(el, null) + this.nodeAncestorIsLast.set(el, []) + } + return root + } + + constructor( + _nodes: T[] = [], + _isParent: IsParent<T> = (c, p) => false + ){ + this._nodes = _nodes + this._isParent = _isParent + } + + private resetWeakMaps() { + + /** + * on new nodes passed, reset all maps + * otherwise, tree will show stale data and will not update + */ + this.childrenWeakMap = new WeakMap() + this.parentWeakMap = new WeakMap() + this.nodeAncestorIsLast = new WeakMap() + } +} diff --git a/src/components/flatHierarchy/treeView/treeView.component.ts b/src/components/flatHierarchy/treeView/treeView.component.ts index cab03376d07031cf42a1f2bc137edf8ef5c9a912..99d55b39dc6e811e495fca162b83db92e3d2bfb9 100644 --- a/src/components/flatHierarchy/treeView/treeView.component.ts +++ b/src/components/flatHierarchy/treeView/treeView.component.ts @@ -2,6 +2,7 @@ import { FlatTreeControl } from "@angular/cdk/tree"; import { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree" import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges, TemplateRef } from "@angular/core"; import { TreeNode } from "../const" +import { Tree } from "./treeControl" @Component({ selector: `sxplr-flat-hierarchy-tree-view`, @@ -13,15 +14,15 @@ import { TreeNode } from "../const" exportAs: 'flatHierarchyTreeView' }) -export class SxplrFlatHierarchyTreeView<T extends object> implements OnChanges{ +export class SxplrFlatHierarchyTreeView<T extends object> extends Tree<T> implements OnChanges{ @HostBinding('class') class = 'sxplr-custom-cmp' @Input('sxplr-flat-hierarchy-nodes') - nodes: T[] = [] + sxplrNodes: T[] = [] @Input('sxplr-flat-hierarchy-is-parent') - isParent: (child: T, parent: T) => boolean + sxplrIsParent: (child: T, parent: T) => boolean @Input('sxplr-flat-hierarchy-render-node-tmpl') renderNodeTmplRef: TemplateRef<T> @@ -42,57 +43,15 @@ export class SxplrFlatHierarchyTreeView<T extends object> implements OnChanges{ nodeClicked = new EventEmitter<T>() ngOnChanges(changes: SimpleChanges): void { - if (changes.nodes) { - - /** - * on new nodes passed, reset all maps - * otherwise, tree will show stale data and will not update - */ - this.childrenWeakMap = new WeakMap() - this.parentWeakMap = new WeakMap() - this.nodeAncestorIsLast = new WeakMap() - - const root = this.nodes.filter(node => !this.getParent(node)) - for (const el of root) { - this.parentWeakMap.set(el, null) - this.nodeAncestorIsLast.set(el, []) - } - this.dataSource.data = root + if (changes.sxplrNodes || changes.sxplrIsParent) { + this.nodes = this.sxplrNodes + this.isParent = this.sxplrIsParent + this.dataSource.data = this.rootNodes if (this.expandOnInit) { this.treeControl.expandAll() } } } - - private getLevel(node: T): number { - let level = -1, cursor = node - const maxLevel = 32 - do { - if (level >= maxLevel) { - level = 0 - break - } - level++ - cursor = this.getParent(cursor) - } while (!!cursor) - return level - } - - private getParent(node: T): T { - if (!this.parentWeakMap.has(node)) { - for (const parentNode of this.nodes) { - if (this.isParent(node, parentNode)) { - this.parentWeakMap.set(node, parentNode) - break - } - } - } - return this.parentWeakMap.get(node) - } - - private childrenWeakMap = new WeakMap<T, T[]>() - private parentWeakMap = new WeakMap<T, T>() - private nodeAncestorIsLast = new WeakMap<T, boolean[]>() private getNodeAncestorIsLast(node: T, visited = new WeakSet<T>() ): boolean[] { if (!this.nodeAncestorIsLast.has(node)) { @@ -115,16 +74,6 @@ export class SxplrFlatHierarchyTreeView<T extends object> implements OnChanges{ } return this.nodeAncestorIsLast.get(node) } - private getChildren(node: T): T[] { - if (!this.childrenWeakMap.has(node)) { - const children = this.nodes.filter(childNode => this.isParent(childNode, node)) - this.childrenWeakMap.set(node, children) - for (const c of children) { - this.parentWeakMap.set(c, node) - } - } - return this.childrenWeakMap.get(node) - } public treeControl = new FlatTreeControl<TreeNode<T>>( ({ level }) => level, @@ -177,6 +126,6 @@ export class SxplrFlatHierarchyTreeView<T extends object> implements OnChanges{ } constructor(){ - + super() } } diff --git a/src/extra_styles.css b/src/extra_styles.css index d7e99033c285ee0b2f3894cadd4dae90631098c3..c4cd571217731a53be10b0b21b9d9e9520757471 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -858,6 +858,12 @@ iav-cmp-viewer-container .mat-chip-list-wrapper flex-wrap: nowrap; } +sxplr-sapiviews-features-ieeg-ieegdataset .mat-chip-list-wrapper +{ + overflow-y: hidden; + overflow-x: auto; +} + iav-cmp-viewer-container poly-update-cmp .mat-chip-list-wrapper { flex-wrap: wrap; diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts index 7bb2ae41a0c584bc69fae676ec90315c91a09067..3769be747cb1d2194df43637b6d043c445a59dda 100644 --- a/src/routerModule/routeStateTransform.service.ts +++ b/src/routerModule/routeStateTransform.service.ts @@ -104,6 +104,7 @@ export class RouteStateTransformSvc { selectedTemplate, selectedParcellation, selectedRegions, + allParcellationRegions } } @@ -180,11 +181,12 @@ export class RouteStateTransformSvc { try { - const { selectedAtlas, selectedParcellation, selectedRegions = [], selectedTemplate } = await this.getATPR(returnObj as TUrlPathObj<string[], TUrlAtlas<string[]>>) + const { selectedAtlas, selectedParcellation, selectedRegions = [], selectedTemplate, allParcellationRegions } = await this.getATPR(returnObj as TUrlPathObj<string[], TUrlAtlas<string[]>>) returnState["[state.atlasSelection]"].selectedAtlas = selectedAtlas returnState["[state.atlasSelection]"].selectedParcellation = selectedParcellation returnState["[state.atlasSelection]"].selectedTemplate = selectedTemplate - returnState["[state.atlasSelection]"].selectedRegions = selectedRegions + returnState["[state.atlasSelection]"].selectedRegions = selectedRegions || [] + returnState["[state.atlasSelection]"].selectedParcellationAllRegions = allParcellationRegions || [] returnState["[state.atlasSelection]"].navigation = parsedNavObj } catch (e) { // if error, show error on UI? @@ -193,7 +195,13 @@ export class RouteStateTransformSvc { return returnState } - cvtStateToRoute(state: MainState) { + cvtStateToRoute(_state: MainState) { + + /** + * need to create new references here + * or else, the memoized selector will spit out undefined + */ + const state:MainState = JSON.parse(JSON.stringify(_state)) const selectedAtlas = atlasSelection.selectors.selectedAtlas(state) const selectedParcellation = atlasSelection.selectors.selectedParcellation(state) diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index 3a05c6a11fab226f243d6331146fabec4dd1c27f..44f6983f90ef1eb95c3b701d2fc6f93f998b52ce 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -3,7 +3,7 @@ import { APP_BASE_HREF } from "@angular/common"; import { Inject } from "@angular/core"; import { NavigationEnd, Router } from '@angular/router' import { Store } from "@ngrx/store"; -import { debounceTime, distinctUntilChanged, filter, map, mapTo, shareReplay, startWith, switchMap, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators"; +import { debounceTime, distinctUntilChanged, filter, finalize, map, mapTo, shareReplay, startWith, switchMap, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators"; import { encodeCustomState, decodeCustomState, verifyCustomState } from "./util"; import { BehaviorSubject, combineLatest, concat, EMPTY, merge, NEVER, Observable, of, timer } from 'rxjs' import { scan } from 'rxjs/operators' @@ -120,9 +120,19 @@ export class RouterService { /** * does work too well =( */ - navEnd$.pipe( - map(navEv => navEv.urlAfterRedirects) + concat( + onload$.pipe( + mapTo(false) + ), + timer(160).pipe( + mapTo(false) + ), + ready$.pipe( + map(val => !!val) + ) ).pipe( + switchMap(() => navEnd$), + map(navEv => navEv.urlAfterRedirects), switchMap(url => routeToStateTransformSvc.cvtRouteToState( router.parseUrl( diff --git a/src/state/annotations/actions.ts b/src/state/annotations/actions.ts index da4ad8edeb6f86d3f80a5d8d3a310d56dc353428..daaddd8ce723f92010ab287f01056314e5a6b838 100644 --- a/src/state/annotations/actions.ts +++ b/src/state/annotations/actions.ts @@ -2,18 +2,20 @@ import { createAction, props } from "@ngrx/store" import { nameSpace } from "./const" import { Annotation } from "./store" -const clearAllAnnotations = createAction( +export const clearAllAnnotations = createAction( `${nameSpace} clearAllAnnotations` ) -const rmAnnotations = createAction( - `${nameSpace} rmAnnotation`, +export const rmAnnotations = createAction( + `${nameSpace} rmAnnotations`, props<{ annotations: Annotation[] }>() ) -export const actions = { - clearAllAnnotations, - rmAnnotations, -} +export const addAnnotations = createAction( + `${nameSpace} addAnnotations`, + props<{ + annotations: Annotation[] + }>() +) diff --git a/src/state/annotations/index.ts b/src/state/annotations/index.ts index 4071cad11b68b6473c88bb7d7374a4d1de77767d..4ad0f0680a29a2c7725e37ffa614f74122a0e152 100644 --- a/src/state/annotations/index.ts +++ b/src/state/annotations/index.ts @@ -1,4 +1,4 @@ -export { actions } from "./actions" +export * as actions from "./actions" export { Annotation, AnnotationState, reducer, defaultState } from "./store" export { nameSpace } from "./const" -export * as selectors from "./selectors" \ No newline at end of file +export * as selectors from "./selectors" diff --git a/src/state/annotations/store.ts b/src/state/annotations/store.ts index 2cb8146bfabd329d4a80b60bb3dff6c9a41663f8..113220b473143036be868d2479282d70bb2d33d7 100644 --- a/src/state/annotations/store.ts +++ b/src/state/annotations/store.ts @@ -1,4 +1,5 @@ -import { createReducer } from "@ngrx/store" +import { createReducer, on } from "@ngrx/store" +import * as actions from "./actions" export type Annotation = { "@id": string @@ -13,7 +14,29 @@ export const defaultState: AnnotationState = { } const reducer = createReducer( - defaultState + defaultState, + on( + actions.addAnnotations, + (state, { annotations }) => { + return { + ...state, + annotations: [ + ...state.annotations, + ...annotations, + ] + } + } + ), + on( + actions.rmAnnotations, + (state, { annotations }) => { + const annIdToBeRemoved = annotations.map(ann => ann["@id"]) + return { + ...state, + annotations: state.annotations.filter(ann => !annIdToBeRemoved.includes(ann["@id"]) ) + } + } + ) ) export { diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index f96dd19ee44328028247e58bba9f0b14b30aac85..8787234b909287c331473dfe6d0cceb578375111 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -24,7 +24,7 @@ export const selectParcellation = createAction( ) /** - * setATP is called as a final step to (potentially) set: + * setAtlasSelectionState is called as a final step to (potentially) set: * - selectedAtlas * - selectedTemplate * - selectedParcellation @@ -36,13 +36,9 @@ export const selectParcellation = createAction( * We may setup post hook for navigation adjustments/etc. * Probably easier is simply subscribe to store and react to selectedTemplate selector */ -export const setATP = createAction( - `${nameSpace} setATP`, - props<{ - atlas?: SapiAtlasModel, - template?: SapiSpaceModel, - parcellation?: SapiParcellationModel, - }>() +export const setAtlasSelectionState = createAction( + `${nameSpace} setAtlasSelectionState`, + props<Partial<AtlasSelectionState>>() ) export const setSelectedParcellationAllRegions = createAction( diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index ae69fa17c4345bd3e1f5d77873be1f37dd0cf4cb..6b988a8db2347fc1d0e3fe70cbb41b2cdba8744e 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -1,28 +1,62 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { concat, forkJoin, merge, of } from "rxjs"; +import { concat, forkJoin, merge, Observable, of } from "rxjs"; import { filter, map, mapTo, switchMap, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators"; -import { SAPI, SapiParcellationModel, SAPIRegion, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; +import { SAPI, SapiAtlasModel, SapiParcellationModel, SAPIRegion, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import * as mainActions from "../actions" import { select, Store } from "@ngrx/store"; import { selectors, actions } from '.' import { fromRootStore } from "./util"; +import { AtlasSelectionState } from "./const" import { ParcellationIsBaseLayer } from "src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe"; import { OrderParcellationByVersionPipe } from "src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe"; -import { atlasAppearance, atlasSelection } from ".."; +import { atlasAppearance } from ".."; import { ParcellationSupportedInSpacePipe } from "src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe"; +type OnTmplParcHookArg = { + previous: { + atlas: SapiAtlasModel + template: SapiSpaceModel + parcellation: SapiParcellationModel + } + current: { + atlas: SapiAtlasModel + template: SapiSpaceModel + parcellation: SapiParcellationModel + } +} + @Injectable() export class Effect { - parcSupportedInSpacePipe = new ParcellationSupportedInSpacePipe(this.sapiSvc) + onTemplateParcSelectionPostHook: ((arg: OnTmplParcHookArg) => Observable<Partial<AtlasSelectionState>>)[] = [ + /** + * This hook gets the region associated with the selected parcellation and template, + * and then set selectedParcellationAllRegions to it + */ + ({ current }) => { + const { atlas, parcellation, template } = current + return ( + !!atlas && !!parcellation && !!template + ? this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"]) + : of([]) + ).pipe( + map(regions => { + return { + selectedParcellationAllRegions: regions + } + }) + ) + } + ] + parcSupportedInSpacePipe = new ParcellationSupportedInSpacePipe(this.sapiSvc) onTemplateParcSelection = createEffect(() => merge<{ template: SapiSpaceModel, parcellation: SapiParcellationModel }>( this.action.pipe( ofType(actions.selectTemplate), map(({ template }) => { return { - template, + template, parcellation: null } }) @@ -52,6 +86,7 @@ export class Effect { */ if (flag) { return of({ + atlas: currAtlas, template: template || currTmpl, parcellation: parcellation || currParc, }) @@ -71,6 +106,7 @@ export class Effect { take(1), map(parcellation => { return { + atlas: currAtlas, template, parcellation } @@ -89,6 +125,7 @@ export class Effect { take(1), map(template => { return { + atlas: currAtlas, template, parcellation } @@ -97,15 +134,28 @@ export class Effect { } throw new Error(`neither template nor parcellation has been defined!`) }), - map(({ parcellation, template }) => { - return actions.setATP({ - parcellation, - template - }) - }) + switchMap(({ atlas, template, parcellation }) => + forkJoin( + this.onTemplateParcSelectionPostHook.map(fn => fn({ previous: { atlas: currAtlas, template: currTmpl, parcellation: currParc }, current: { atlas, template, parcellation } })) + ).pipe( + map(partialStates => { + let returnState: Partial<AtlasSelectionState> = { + selectedAtlas: atlas, + selectedTemplate: template, + selectedParcellation: parcellation + } + for (const s of partialStates) { + returnState = { + ...returnState, + ...s, + } + } + return actions.setAtlasSelectionState(returnState) + }) + ) + ) ) - }), - + }) )) onAtlasSelectionSelectTmplParc = createEffect(() => this.action.pipe( @@ -131,35 +181,30 @@ export class Effect { this.sapiSvc.getSpaceDetail(atlas["@id"], spaceId["@id"]) ) ).pipe( - map(spaces => { + switchMap(spaces => { const selectedSpace = spaces.find(s => /152/.test(s.fullName)) || spaces[0] - return actions.setATP({ - atlas, - template: selectedSpace, - parcellation - }) - }) - ) - }), - )) + return forkJoin( + this.onTemplateParcSelectionPostHook.map(fn => fn({ previous: null, current: { atlas, parcellation, template: selectedSpace } })) + ).pipe( + map(partialStates => { - onATPSelectionGetAndSetAllRegions = createEffect(() => this.store.pipe( - atlasSelection.fromRootStore.distinctATP(), - switchMap(({ atlas, template, parcellation }) => - !!atlas && !!template && !!parcellation - ? this.sapiSvc.getParcRegions(atlas["@id"], parcellation["@id"], template["@id"]).pipe( - map(regions => - actions.setSelectedParcellationAllRegions({ - regions + let returnState: Partial<AtlasSelectionState> = { + selectedAtlas: atlas, + selectedTemplate: selectedSpace, + selectedParcellation: parcellation + } + for (const s of partialStates) { + returnState = { + ...returnState, + ...s, + } + } + return actions.setAtlasSelectionState(returnState) }) ) - ) - : of( - actions.setSelectedParcellationAllRegions({ - regions: [] }) ) - ) + }) )) onATPSelectionClearBaseLayerColorMap = createEffect(() => this.store.pipe( diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts index 45bc4f669ead58ed58c6af97e3cb45e2c601f14c..08848c1efa9b81e649bfc8d7c29d3cc6581f77ed 100644 --- a/src/state/atlasSelection/store.ts +++ b/src/state/atlasSelection/store.ts @@ -24,13 +24,11 @@ export const defaultState: AtlasSelectionState = { const reducer = createReducer( defaultState, on( - actions.setATP, - (state, { atlas, parcellation, template }) => { + actions.setAtlasSelectionState, + (state, partialState) => { return { ...state, - selectedAtlas: atlas || state.selectedAtlas, - selectedTemplate: template || state.selectedTemplate, - selectedParcellation: parcellation || state.selectedParcellation, + ...partialState } } ), @@ -116,6 +114,15 @@ const reducer = createReducer( } } ), + on( + actions.selectAtlas, + (state, { atlas }) => { + return { + ...state, + selectedAtlas: atlas + } + } + ), on( actions.dismissBreadCrumb, (state, { id }) => { diff --git a/src/state/index.ts b/src/state/index.ts index d465bfb4a624ead5445fdf23388dd0e9bbedcc42..3eba9c31857ad2db855c3c9c8e4c461c8fd488ee 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -35,13 +35,11 @@ function generalApplyStateReducer(reducer: ActionReducer<MainState>): ActionRedu return function(_state, action) { let state = _state if (action.type === generalApplyState.type) { - state = { - ...state, - /** - * typing is a bit scuffed, but works - */ - ...(action as any).state - } + state = JSON.parse( + JSON.stringify( + (action as any).state + ) + ) } return reducer(state, action) } diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index a4d1fc05f373b2ca33076f7c8d690bfb9c8f7e1b..a485a115773c2953b3396c72a0d054d925fa85f6 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -1,7 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { Observable } from "rxjs"; -import { SplashUiModule } from "src/atlasComponents/splashScreen"; import { ComponentsModule } from "src/components"; import { ContextMenuModule, ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { LayoutModule } from "src/layouts/layout.module"; @@ -33,7 +32,6 @@ import { DialogModule } from "src/ui/dialogInfo/module"; ThreeSurferModule, LayoutModule, AngularMaterialModule, - SplashUiModule, TopMenuModule, UtilModule, ComponentsModule, diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index e74aee0c302bdf627dbe60b1e7a0cd758ff705e5..934f5ba8d3147fba7292b51436c2bdc1357f8417 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -37,8 +37,8 @@ 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"]) - .then(val => + return sapiRegion.getMapInfo(template["@id"]).pipe( + map(val => atlasAppearance.actions.addCustomLayer({ customLayer: { clType: "customlayer/nglayer", @@ -52,13 +52,14 @@ export class LayerCtrlEffects { }) } }) - ) + ), + catchError((err, obs) => of( + atlasAppearance.actions.removeCustomLayer({ + id: NehubaLayerControlService.PMAP_LAYER_NAME + }) + )) + ) }), - catchError((err, obs) => of( - atlasAppearance.actions.removeCustomLayer({ - id: NehubaLayerControlService.PMAP_LAYER_NAME - }) - )) )) onATP$ = this.store.pipe( diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index ad0528023661161f926a263545e72bc268f3c6d9..2f86c53b2a37395d57f7805a6df835025c018398 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -3,15 +3,13 @@ import { select, Store } from "@ngrx/store"; import { BehaviorSubject, combineLatest, merge, Observable, Subject, Subscription } from "rxjs"; import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, tap, withLatestFrom } from "rxjs/operators"; import { IColorMap, INgLayerCtrl, TNgLayerCtrl } from "./layerCtrl.util"; -import { IAuxMesh } from '../store' -import { IVolumeTypeDetail } from "src/util/siibraApiConstants/types"; -import { SAPI, SapiParcellationModel } from "src/atlasComponents/sapi"; -import { SAPISpace, SAPIRegion } from "src/atlasComponents/sapi/core"; +import { SAPIRegion } from "src/atlasComponents/sapi/core"; import { getParcNgId } from "../config.service" import { getRegionLabelIndex } from "../config.service/util"; import { annotation, atlasAppearance, atlasSelection } from "src/state"; import { serializeSegment } from "../util"; import { LayerCtrlEffects } from "./layerCtrl.effects"; +import { arrayEqual } from "src/util/array"; export const BACKUP_COLOR = { red: 255, @@ -96,33 +94,6 @@ export class NehubaLayerControlService implements OnDestroy{ ...cmAux })) ) - - private auxMeshes$: Observable<IAuxMesh[]> = this.selectedATP$.pipe( - map(({ template }) => template), - switchMap(tmpl => this.sapiSvc.registry.get<SAPISpace>(tmpl["@id"]).getVolumes().pipe( - map( - tmplVolumes => { - const auxMeshArr: IAuxMesh[] = [] - for (const vol of tmplVolumes) { - if (vol.data.detail["neuroglancer/precompmesh"]) { - const detail = vol.data.detail as IVolumeTypeDetail["neuroglancer/precompmesh"] - for (const auxMesh of detail["neuroglancer/precompmesh"].auxMeshes) { - auxMeshArr.push({ - "@id": `auxmesh-${tmpl["@id"]}-${auxMesh.name}`, - labelIndicies: auxMesh.labelIndicies, - name: auxMesh.name, - ngId: '', - rgb: [255, 255, 255], - visible: auxMesh.name !== "Sulci" - }) - } - } - } - return auxMeshArr - } - ) - )) - ) private sub: Subscription[] = [] @@ -132,7 +103,6 @@ export class NehubaLayerControlService implements OnDestroy{ constructor( private store$: Store<any>, - private sapiSvc: SAPI, private layerEffects: LayerCtrlEffects, ){ @@ -166,43 +136,22 @@ export class NehubaLayerControlService implements OnDestroy{ }) ) - this.sub.push( - this.store$.pipe( - select(atlasAppearance.selectors.customLayers), - map(cl => cl.filter(l => l.clType === "customlayer/colormap").length > 0), - distinctUntilChanged() - ).subscribe(flag => { - const pmapLayer = this.ngLayersRegister.find(l => l.id === NehubaLayerControlService.PMAP_LAYER_NAME) - if (!pmapLayer) return - const payload = { - type: 'update', - payload: { - [NehubaLayerControlService.PMAP_LAYER_NAME]: { - visible: !flag - } - } - } as TNgLayerCtrl<'update'> - this.manualNgLayersControl$.next(payload) - }) - ) - /** * on custom landmarks loaded, set mesh transparency */ this.sub.push( this.store$.pipe( select(annotation.selectors.annotations), - withLatestFrom(this.auxMeshes$) - ).subscribe(([landmarks, auxMeshes]) => { - + withLatestFrom(this.defaultNgLayers$) + ).subscribe(([landmarks, { tmplAuxNgLayers }]) => { const payload: { [key: string]: number } = {} const alpha = landmarks.length > 0 ? 0.2 : 1.0 - for (const auxMesh of auxMeshes) { - payload[auxMesh.ngId] = alpha + for (const ngId in tmplAuxNgLayers) { + payload[ngId] = alpha } this.manualNgLayersControl$.next({ @@ -242,11 +191,28 @@ export class NehubaLayerControlService implements OnDestroy{ }) ) - public visibleLayer$: Observable<string[]> = this.expectedLayerNames$.pipe( - map(expectedLayerNames => { - const ngIdSet = new Set<string>([...expectedLayerNames]) - return Array.from(ngIdSet) - }) + public visibleLayer$: Observable<string[]> = combineLatest([ + this.expectedLayerNames$.pipe( + map(expectedLayerNames => { + const ngIdSet = new Set<string>([...expectedLayerNames]) + return Array.from(ngIdSet) + }) + ), + this.store$.pipe( + select(atlasAppearance.selectors.customLayers), + map(cl => { + const otherColormapExist = cl.filter(l => l.clType === "customlayer/colormap").length > 0 + const pmapExist = cl.filter(l => l.clType === "customlayer/nglayer").length > 0 + return pmapExist && !otherColormapExist + }), + distinctUntilChanged(), + map(flag => flag + ? [ NehubaLayerControlService.PMAP_LAYER_NAME ] + : [] + ) + ) + ]).pipe( + map(([ expectedLayerNames, pmapLayer ]) => [...expectedLayerNames, ...pmapLayer]) ) /** @@ -303,6 +269,9 @@ export class NehubaLayerControlService implements OnDestroy{ private ngLayers$ = this.store$.pipe( select(atlasAppearance.selectors.customLayers), map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]), + distinctUntilChanged( + arrayEqual((o, n) => o.id === n.id) + ), map(customLayers => { const newLayers = customLayers.filter(l => { const registeredLayerNames = this.ngLayersRegister.map(l => l.id) diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index c1aadf90a5fdda983bc101105652574020317a58..6313ccba3200e4195fe17ae03dfa522daa5e7ef6 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -1,10 +1,13 @@ import { Injectable, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { merge, Observable, of } from "rxjs"; +import { combineLatest, merge, Observable, of } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { IMeshesToLoad } from '../constants' import { selectorAuxMeshes } from "../store"; import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects"; +import { atlasSelection } from "src/state"; +import { Tree } from "src/components/flatHierarchy/treeView/treeControl" +import { getParcNgId, getRegionLabelIndex } from "../config.service"; /** * control mesh loading etc @@ -33,13 +36,55 @@ export class NehubaMeshService implements OnDestroy { private ngLayers$ = this.effect.onATPDebounceNgLayers$ public loadMeshes$: Observable<IMeshesToLoad> = merge( - this.ngLayers$.pipe( - switchMap(ngLayers => { + combineLatest([ + this.store$.pipe( + atlasSelection.fromRootStore.distinctATP(), + ), + this.store$.pipe( + select(atlasSelection.selectors.selectedParcAllRegions), + ), + this.store$.pipe( + select(atlasSelection.selectors.selectedRegions), + ) + ]).pipe( + switchMap(([{ atlas, template, parcellation }, regions, selectedRegions]) => { + const tree = new Tree( + regions, + (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"]) + ) + + const ngIdRecord: Record<string, number[]> = {} + if (selectedRegions.length > 0) { + for (const r of selectedRegions) { + const ngId = getParcNgId(atlas, template, parcellation, r) + const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) + if (!ngIdRecord[ngId]) { + ngIdRecord[ngId] = [] + } + ngIdRecord[ngId].push(regionLabelIndex) + } + } else { + for (const r of regions) { + const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) + if (!regionLabelIndex) { + continue + } + if ( + tree.someAncestor(r, (anc) => !!getRegionLabelIndex(atlas, template, parcellation, anc)) + ) { + continue + } + const ngId = getParcNgId(atlas, template, parcellation, r) + if (!ngIdRecord[ngId]) { + ngIdRecord[ngId] = [] + } + ngIdRecord[ngId].push(regionLabelIndex) + } + } const arr: IMeshesToLoad[] = [] - const { parcNgLayers } = ngLayers - for (const ngId in parcNgLayers) { - const {labelIndicies} = parcNgLayers[ngId] + for (const ngId in ngIdRecord) { + const labelIndicies = ngIdRecord[ngId] arr.push({ labelIndicies, layer: { name: ngId } diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 964fdbb5646f7dd3edd385d6859e5ea22d3eba36..707a473bfe8a82b3ebb865bb4691639a0cdcc222 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -2,11 +2,10 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E import { select, Store } from "@ngrx/store"; import { Subject, Subscription } from "rxjs"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; -import { debounceTime, distinctUntilChanged, filter, map, startWith, switchMap, take } from "rxjs/operators"; +import { distinctUntilChanged, startWith } from "rxjs/operators"; import { ARIA_LABELS } from 'common/constants' import { LoggingService } from "src/logging"; import { EnumViewerEvt, IViewer, TViewerEvent } from "../../viewer.interface"; -import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { NehubaViewerContainerDirective, TMouseoverEvent } from "../nehubaViewerInterface/nehubaViewerInterface.directive"; import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service"; import { NehubaMeshService } from "../mesh.service"; @@ -19,13 +18,10 @@ import { getShader } from "src/util/constants"; import { EnumColorMapName } from "src/util/colorMaps"; import { MatDialog } from "@angular/material/dialog"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; -import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; -import { NehubaConfig, getNehubaConfig, getParcNgId, getRegionLabelIndex } from "../config.service"; +import { SapiRegionModel } from "src/atlasComponents/sapi"; +import { NehubaConfig, getParcNgId, getRegionLabelIndex } from "../config.service"; import { SET_MESHES_TO_LOAD } from "../constants"; import { annotation, atlasAppearance, atlasSelection, userInteraction } from "src/state"; -import { NgLayerCustomLayer } from "src/state/atlasAppearance"; -import { arrayEqual } from "src/util/array"; -import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects"; export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!` @@ -80,14 +76,10 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni private onhoverSegments: SapiRegionModel[] = [] private onDestroyCb: (() => void)[] = [] - private viewerUnit: NehubaViewerUnit private multiNgIdsRegionsLabelIndexMap = new Map<string, Map<number, SapiRegionModel>>() public nehubaConfig: NehubaConfig - private navigation: any - private newViewer$ = new Subject() - public customLandmarks$ = this.store$.pipe( select(annotation.selectors.annotations), ) @@ -187,7 +179,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni /** * clear existing container */ - this.viewerUnit = null this.nehubaContainerDirective && this.nehubaContainerDirective.clear() } @@ -256,14 +247,6 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni if (setViewerHandle) { console.warn(`NYI viewer handle is deprecated`) } - - // listen to navigation change from store - const navSub = this.store$.pipe( - select(atlasSelection.selectors.navigation) - ).subscribe(nav => { - this.navigation = nav - }) - this.onDestroyCb.push(() => navSub.unsubscribe()) }