From cd1e08ed283762cc107ad749bf86d1833333df86 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 24 Feb 2023 17:13:04 +0100 Subject: [PATCH] refactor: adapt to new siibra-python/API --- angular.json | 4 +- src/atlasComponents/sapi/core/base.ts | 5 +- src/atlasComponents/sapi/core/sapiSpace.ts | 2 +- src/atlasComponents/sapi/sapi.service.ts | 42 ++++- src/atlasComponents/sapi/schemaV3.ts | 164 ++++++++++++------ src/atlasComponents/sapi/sxplrTypes.ts | 32 ++-- src/atlasComponents/sapi/translateV3.ts | 40 ++++- src/atlasComponents/sapi/typeV3.ts | 14 +- src/atlasComponents/sapiViews/core/module.ts | 3 - .../parcellationGroupSelected.pipe.ts | 2 +- .../sapiViews/core/region/module.ts | 2 + .../region/rich/region.rich.template.html | 9 +- .../pureDumb/pureATPSelector.template.html | 2 +- .../feature-view/feature-view.component.html | 68 ++++++++ .../feature-view/feature-view.component.scss | 11 ++ .../feature-view.component.spec.ts | 23 +++ .../feature-view/feature-view.component.ts | 50 ++++++ .../sapiViews/features/features.module.ts | 31 ++++ .../features/transform-pd-to-ds.pipe.spec.ts | 8 + .../features/transform-pd-to-ds.pipe.ts | 23 +++ src/atlasComponents/sapiViews/module.ts | 5 +- .../sapiViews/richDataset.stories.ts | 2 - .../component/smartChip.component.ts | 2 +- ...artChip.style.css => smartChip.style.scss} | 0 src/features/base.ts | 37 ++++ src/features/category-acc.directive.spec.ts | 8 + src/features/category-acc.directive.ts | 54 ++++++ src/features/entry/entry.component.html | 65 +++++++ src/features/entry/entry.component.scss | 62 +++++++ src/features/entry/entry.component.spec.ts | 23 +++ src/features/entry/entry.component.ts | 73 ++++++++ src/features/featureName.pipe.ts | 12 ++ src/features/fetch.directive.spec.ts | 8 + src/features/fetch.directive.ts | 71 ++++++++ src/features/index.ts | 1 + src/features/list/list.component.html | 11 ++ src/features/list/list.component.scss | 15 ++ src/features/list/list.component.spec.ts | 23 +++ src/features/list/list.component.ts | 67 +++++++ src/features/module.ts | 40 +++++ src/overwrite.scss | 2 +- .../routeStateTransform.service.ts | 10 +- src/state/atlasSelection/effects.ts | 1 - src/util/priority.ts | 2 +- .../viewerCmp/viewerCmp.template.html | 6 +- 45 files changed, 1038 insertions(+), 97 deletions(-) create mode 100644 src/atlasComponents/sapiViews/features/feature-view/feature-view.component.html create mode 100644 src/atlasComponents/sapiViews/features/feature-view/feature-view.component.scss create mode 100644 src/atlasComponents/sapiViews/features/feature-view/feature-view.component.spec.ts create mode 100644 src/atlasComponents/sapiViews/features/feature-view/feature-view.component.ts create mode 100644 src/atlasComponents/sapiViews/features/features.module.ts create mode 100644 src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.spec.ts create mode 100644 src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.ts rename src/components/smartChip/component/{smartChip.style.css => smartChip.style.scss} (100%) create mode 100644 src/features/base.ts create mode 100644 src/features/category-acc.directive.spec.ts create mode 100644 src/features/category-acc.directive.ts create mode 100644 src/features/entry/entry.component.html create mode 100644 src/features/entry/entry.component.scss create mode 100644 src/features/entry/entry.component.spec.ts create mode 100644 src/features/entry/entry.component.ts create mode 100644 src/features/featureName.pipe.ts create mode 100644 src/features/fetch.directive.spec.ts create mode 100644 src/features/fetch.directive.ts create mode 100644 src/features/index.ts create mode 100644 src/features/list/list.component.html create mode 100644 src/features/list/list.component.scss create mode 100644 src/features/list/list.component.spec.ts create mode 100644 src/features/list/list.component.ts create mode 100644 src/features/module.ts diff --git a/angular.json b/angular.json index 3964720d4..af94adeb5 100644 --- a/angular.json +++ b/angular.json @@ -10,7 +10,7 @@ "projectType": "application", "schematics": { "@schematics/angular:component": { - "style": "sass" + "style": "scss" }, "@schematics/angular:application": { "strict": true @@ -18,7 +18,7 @@ }, "root": "", "sourceRoot": "src", - "prefix": "app", + "prefix": "sxplr", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", diff --git a/src/atlasComponents/sapi/core/base.ts b/src/atlasComponents/sapi/core/base.ts index 9783082a2..2bfaa27d6 100644 --- a/src/atlasComponents/sapi/core/base.ts +++ b/src/atlasComponents/sapi/core/base.ts @@ -4,13 +4,12 @@ import { RouteParam } from "../typeV3" import { SapiQueryPriorityArg } from "../sxplrTypes" const AllFeatures = { - // Feature: "Feature", CorticalProfile: "CorticalProfile", // EbrainsDataFeature: "EbrainsDataFeature", RegionalConnectivity: "RegionalConnectivity", Tabular: "Tabular", - GeneExpressions: "GeneExpressions", - VolumeOfInterest: "VolumeOfInterest", + // GeneExpressions: "GeneExpressions", + Image: "Image", } as const type AF = keyof typeof AllFeatures diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts index e1826a6d9..c6e8741ea 100644 --- a/src/atlasComponents/sapi/core/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -8,7 +8,7 @@ import { SAPIBase } from "./base" * All valid parcellation features */ const SpaceFeatures = { - VolumeOfInterest: "VolumeOfInterest", + Image: "Image", } as const export type SF = keyof typeof SpaceFeatures diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 924fce5ce..fb51ddd60 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -12,7 +12,7 @@ import { PRIORITY_HEADER } from "src/util/priority"; import { forkJoin, from, NEVER, Observable, of, throwError } from "rxjs"; import { SAPIFeature } from "./features"; import { environment } from "src/environments/environment" -import { PathReturn, RouteParam, SapiRoute } from "./typeV3"; +import { FeatureType, PathReturn, RouteParam, SapiRoute } from "./typeV3"; import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature, SapiQueryPriorityArg } from "./sxplrTypes"; @@ -173,6 +173,43 @@ export class SAPI{ } }) } + + #isPaged<T>(resp: any): resp is PaginatedResponse<T>{ + if (!!resp.total) return true + return false + } + getV3Features<T extends FeatureType>(featureType: T, sapiParam: RouteParam<`/feature/${T}`>): Observable<PathReturn<`/feature/${T}/{feature_id}`>[]> { + const query = structuredClone(sapiParam) + return this.v3Get<`/feature/${T}`>(`/feature/${featureType}`, { + ...query + }).pipe( + switchMap(resp => { + if (!this.#isPaged(resp)) return throwError(`endpoint not returning paginated response`) + return this.iteratePages( + resp, + page => { + const query = structuredClone(sapiParam) + query.query.page = page + return this.v3Get(`/feature/${featureType}`, { + ...query, + }).pipe( + map(val => { + if (this.#isPaged(val)) return val + return { items: [], total: 0, page: 0, size: 0 } + }) + ) + } + ) + }), + catchError(() => of([])) + ) + } + + getV3FeatureDetail<T extends FeatureType>(featureType: T, sapiParam: RouteParam<`/feature/${T}/{feature_id}`>): Observable<PathReturn<`/feature/${T}/{feature_id}`>> { + return this.v3Get<`/feature/${T}/{feature_id}`>(`/feature/${featureType}/{feature_id}`, { + ...sapiParam + }) + } getFeature(featureId: string, opts: Record<string, string> = {}) { return new SAPIFeature(this, featureId, opts) @@ -257,7 +294,6 @@ export class SAPI{ }), shareReplay(1), ) - public getAllSpaces(atlas: SxplrAtlas): Observable<SxplrTemplate[]> { return forkJoin( @@ -391,7 +427,7 @@ export class SAPI{ /** * FIXME iterate over all pages */ - return this.v3Get("/feature/VolumeOfInterest", { + return this.v3Get("/feature/Image", { query: { space_id: bbox.space.id, bbox: JSON.stringify([bbox.minpoint, bbox.maxpoint]), diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index 7147fd8c7..98036ee8f 100644 --- a/src/atlasComponents/sapi/schemaV3.ts +++ b/src/atlasComponents/sapi/schemaV3.ts @@ -42,11 +42,23 @@ export interface paths { get: operations["route_get_map_map_get"] } "/map/labelled_map.nii.gz": { - /** Route Get Parcellation Labelled Map */ + /** + * Route Get Parcellation Labelled Map + * @description Returns a labelled map if region_id is not provided. + * + * Returns a mask if a region_id is provided. + * + * region_id MAY refer to ANY region on the region hierarchy, and a combined mask will be returned. + */ get: operations["route_get_parcellation_labelled_map_map_labelled_map_nii_gz_get"] } "/map/statistical_map.nii.gz": { - /** Route Get Region Statistical Map */ + /** + * Route Get Region Statistical Map + * @description Returns a statistic map. + * + * region_id MUST refer to leaf region on the region hierarchy. + */ get: operations["route_get_region_statistical_map_map_statistical_map_nii_gz_get"] } "/map/statistical_map.info.json": { @@ -81,13 +93,13 @@ export interface paths { /** Get Single Tabular */ get: operations["get_single_tabular_feature_Tabular__feature_id__get"] } - "/feature/VolumeOfInterest": { + "/feature/Image": { /** Get All Voi */ - get: operations["get_all_voi_feature_VolumeOfInterest_get"] + get: operations["get_all_voi_feature_Image_get"] } - "/feature/VolumeOfInterest/{feature_id}": { + "/feature/Image/{feature_id}": { /** Get Single Voi */ - get: operations["get_single_voi_feature_VolumeOfInterest__feature_id__get"] + get: operations["get_single_voi_feature_Image__feature_id__get"] } "/feature/GeneExpressions": { /** Get All Gene */ @@ -97,6 +109,16 @@ export interface paths { /** Get All Gene */ get: operations["get_all_gene_feature_GeneExpressions__feature_id__get"] } + "/feature/{feature_id}": { + /** + * Get Single Feature + * @description This endpoint allows detail of a single feature to be fetched, without the necessary context. However, the tradeoff for this endpoint is: + * + * - the endpoint typing is the union of all possible return types + * - the client needs to supply any necessary query param (e.g. subject for regional connectivity, gene for gene expression etc) + */ + get: operations["get_single_feature_feature__feature_id__get"] + } } export type webhooks = Record<string, never>; @@ -341,9 +363,9 @@ export interface components { axesOrigin: (components["schemas"]["AxesOrigin"])[] /** * defaultImage - * @description Two or three dimensional image that particluarly represents a specific coordinate space. + * @description Two or three dimensional image that particluarly represents a specific coordinate space. Overriden by Siibra API to use as VolumeModel */ - defaultImage?: components["schemas"]["VolumeModel"][] + defaultImage?: (components["schemas"]["VolumeModel"])[] /** * digitalIdentifier * @description Digital handle to identify objects or legal persons. @@ -505,6 +527,19 @@ export interface components { /** Url */ url: string } + /** FeatureMetaModel */ + FeatureMetaModel: { + /** Name */ + name: string + /** Path */ + path?: string + /** Query Params */ + query_params?: (string)[] + /** Path Params */ + path_params?: (string)[] + /** Category */ + category?: string + } /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -662,8 +697,17 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number + } + /** Page[FeatureMetaModel] */ + Page_FeatureMetaModel_: { + /** Items */ + items: (components["schemas"]["FeatureMetaModel"])[] + /** Total */ + total: number + /** Page */ + page: number + /** Size */ + size: number } /** Page[ParcellationEntityVersionModel] */ Page_ParcellationEntityVersionModel_: { @@ -675,8 +719,6 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number } /** Page[SiibraAtlasModel] */ Page_SiibraAtlasModel_: { @@ -688,8 +730,6 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number } /** Page[SiibraCorticalProfileModel] */ Page_SiibraCorticalProfileModel_: { @@ -701,8 +741,6 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number } /** Page[SiibraParcellationModel] */ Page_SiibraParcellationModel_: { @@ -714,8 +752,6 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number } /** Page[SiibraRegionalConnectivityModel] */ Page_SiibraRegionalConnectivityModel_: { @@ -727,8 +763,6 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number } /** Page[SiibraTabularModel] */ Page_SiibraTabularModel_: { @@ -740,8 +774,6 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number } /** Page[SiibraVoiModel] */ Page_SiibraVoiModel_: { @@ -753,34 +785,17 @@ export interface components { page: number /** Size */ size: number - /** Pages */ - pages?: number - } - /** Page[Union[SiibraCorticalProfileModel, SiibraTabularModel, SiibraReceptorDensityFp]] */ - Page_Union_SiibraCorticalProfileModel__SiibraTabularModel__SiibraReceptorDensityFp__: { - /** Items */ - items: (components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraReceptorDensityFp"])[] - /** Total */ - total: number - /** Page */ - page: number - /** Size */ - size: number - /** Pages */ - pages?: number } - /** Page[str] */ - Page_str_: { + /** Page[Union[SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]] */ + Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__: { /** Items */ - items: (string)[] + items: (components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"])[] /** Total */ total: number /** Page */ page: number /** Size */ size: number - /** Pages */ - pages?: number } /** ParcellationEntityVersionModel */ ParcellationEntityVersionModel: { @@ -979,6 +994,8 @@ export interface components { id: string /** Modality */ modality: string + /** Category */ + category: string /** Description */ description: string /** Name */ @@ -1038,6 +1055,8 @@ export interface components { id: string /** Modality */ modality: string + /** Category */ + category: string /** Description */ description: string /** Name */ @@ -1067,6 +1086,8 @@ export interface components { id: string /** Modality */ modality: string + /** Category */ + category: string /** Description */ description: string /** Name */ @@ -1091,6 +1112,8 @@ export interface components { id: string /** Modality */ modality: string + /** Category */ + category: string /** Description */ description: string /** Name */ @@ -1108,6 +1131,8 @@ export interface components { id: string /** Modality */ modality: string + /** Category */ + category: string /** Description */ description: string /** Name */ @@ -1453,11 +1478,19 @@ export interface operations { } } route_get_parcellation_labelled_map_map_labelled_map_nii_gz_get: { - /** Route Get Parcellation Labelled Map */ + /** + * Route Get Parcellation Labelled Map + * @description Returns a labelled map if region_id is not provided. + * + * Returns a mask if a region_id is provided. + * + * region_id MAY refer to ANY region on the region hierarchy, and a combined mask will be returned. + */ parameters: { query: { parcellation_id: string space_id: string + region_id?: string } } responses: { @@ -1472,7 +1505,12 @@ export interface operations { } } route_get_region_statistical_map_map_statistical_map_nii_gz_get: { - /** Route Get Region Statistical Map */ + /** + * Route Get Region Statistical Map + * @description Returns a statistic map. + * + * region_id MUST refer to leaf region on the region hierarchy. + */ parameters: { query: { parcellation_id: string @@ -1527,7 +1565,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["Page_str_"] + "application/json": components["schemas"]["Page_FeatureMetaModel_"] } } /** @description Validation Error */ @@ -1658,7 +1696,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["Page_Union_SiibraCorticalProfileModel__SiibraTabularModel__SiibraReceptorDensityFp__"] + "application/json": components["schemas"]["Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__"] } } /** @description Validation Error */ @@ -1685,7 +1723,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraReceptorDensityFp"] + "application/json": components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] } } /** @description Validation Error */ @@ -1696,7 +1734,7 @@ export interface operations { } } } - get_all_voi_feature_VolumeOfInterest_get: { + get_all_voi_feature_Image_get: { /** Get All Voi */ parameters: { query: { @@ -1721,7 +1759,7 @@ export interface operations { } } } - get_single_voi_feature_VolumeOfInterest__feature_id__get: { + get_single_voi_feature_Image__feature_id__get: { /** Get Single Voi */ parameters: { query: { @@ -1800,4 +1838,32 @@ export interface operations { } } } + get_single_feature_feature__feature_id__get: { + /** + * Get Single Feature + * @description This endpoint allows detail of a single feature to be fetched, without the necessary context. However, the tradeoff for this endpoint is: + * + * - the endpoint typing is the union of all possible return types + * - the client needs to supply any necessary query param (e.g. subject for regional connectivity, gene for gene expression etc) + */ + parameters: { + path: { + feature_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } } diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 7626acbf3..28622c040 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -77,6 +77,17 @@ export { TThreeSurferMesh, TThreeMesh, TThreeMeshLabel } */ export type TemplateDefaultImage = NgLayerSpec | TThreeMesh +export type LabelledMap = { + name: string + label: number +} + +export type StatisticalMap = { + url: string + min: number + max: number +} + /** * Features */ @@ -84,6 +95,7 @@ export type TemplateDefaultImage = NgLayerSpec | TThreeMesh export type Feature = { id: string name: string + category?: string } & Partial<AdditionalInfo> type DataFrame = { @@ -94,25 +106,21 @@ export type VoiFeature = { bbox: BoundingBox } & Feature +type CorticalDataType = number + +export type CorticalFeature<T extends CorticalDataType, IndexType extends string|number=string> = { + indices?: IndexType[] + corticalProfile?: T[] +} & Feature + type TabularDataType = number | string | number[] export type TabularFeature<T extends TabularDataType> = { index: string[] columns: string[] - data: T[][] + data?: T[][] } & Feature -export type LabelledMap = { - name: string - label: number -} - -export type StatisticalMap = { - url: string - min: number - max: number -} - /** * Support types diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index d8505ceda..4134af67f 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -1,5 +1,5 @@ import { - SxplrAtlas, SxplrParcellation, SxplrTemplate, SxplrRegion, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, VoiFeature, Point, TemplateDefaultImage, TThreeSurferMesh, TThreeMesh, LabelledMap + SxplrAtlas, SxplrParcellation, SxplrTemplate, SxplrRegion, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, VoiFeature, Point, TemplateDefaultImage, TThreeSurferMesh, TThreeMesh, LabelledMap, CorticalFeature } from "./sxplrTypes" import { PathReturn } from "./typeV3" import { hexToRgb } from 'common/util' @@ -63,7 +63,18 @@ class TranslateV3 { name: region.name, color: hexToRgb(region.hasAnnotation?.displayColor) as [number, number, number], parentIds: region.hasParent.map( v => v["@id"] ), - type: "SxplrRegion" + type: "SxplrRegion", + centroid: region.hasAnnotation?.bestViewPoint + ? await (async () => { + const bestViewPoint = region.hasAnnotation?.bestViewPoint + const fullSpace = this.#templateMap.get(bestViewPoint.coordinateSpace['@id']) + const space = await this.translateTemplate(fullSpace) + return { + loc: bestViewPoint.coordinates.map(v => v.value) as [number, number, number], + space + } + })() + : null } } @@ -322,7 +333,7 @@ class TranslateV3 { } } - async translateVoi(voi: PathReturn<"/feature/VolumeOfInterest/{feature_id}">): Promise<VoiFeature> { + async translateVoi(voi: PathReturn<"/feature/Image/{feature_id}">): Promise<VoiFeature> { const { boundingbox } = voi const { loc: center, space } = await this.translatePoint(boundingbox.center) const { loc: maxpoint } = await this.translatePoint(boundingbox.maxpoint) @@ -339,6 +350,29 @@ class TranslateV3 { id: voi.id } } + + + async translateCorticalProfile(feat: PathReturn<"/feature/CorticalProfile/{feature_id}">): Promise<CorticalFeature<number>> { + return { + id: feat.id, + name: feat.name, + desc: feat.description, + link: [ + ...feat.datasets + .map(ds => ds.urls) + .flatMap(v => v) + .map(url => ({ + href: url.url, + text: url.url + })), + ...feat.datasets + .map(ds => ({ + href: ds.ebrains_page, + text: "ebrains resource" + })) + ] + } + } } export const translateV3Entities = new TranslateV3() diff --git a/src/atlasComponents/sapi/typeV3.ts b/src/atlasComponents/sapi/typeV3.ts index 7aa938fda..d65db66de 100644 --- a/src/atlasComponents/sapi/typeV3.ts +++ b/src/atlasComponents/sapi/typeV3.ts @@ -12,11 +12,23 @@ export type SxplrCoordinatePointExtension = { color: string '@id': string // should match the id of opendminds specs } -export type SapiSpatialFeatureModel = PathReturn<"/feature/VolumeOfInterest/{feature_id}"> +export type SapiSpatialFeatureModel = PathReturn<"/feature/Image/{feature_id}"> export type SapiFeatureModel = SapiSpatialFeatureModel | PathReturn<"/feature/Tabular/{feature_id}"> | PathReturn<"/feature/RegionalConnectivity/{feature_id}"> | PathReturn<"/feature/CorticalProfile/{feature_id}"> export type SapiRoute = keyof paths +type _FeatureType<FeatureRoute extends SapiRoute> = FeatureRoute extends `/feature/${infer FT}` + ? FT extends "_types" + ? never + : FT extends "{feature_id}" + ? never + : FT extends `${infer _FT}/{${infer _FID}}` + ? never + : FT + : never + +export type FeatureType = _FeatureType<SapiRoute> + /** * Support types */ diff --git a/src/atlasComponents/sapiViews/core/module.ts b/src/atlasComponents/sapiViews/core/module.ts index c30a1d83e..d307b3b54 100644 --- a/src/atlasComponents/sapiViews/core/module.ts +++ b/src/atlasComponents/sapiViews/core/module.ts @@ -1,7 +1,6 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { SapiViewsCoreAtlasModule } from "./atlas/module"; -import { SapiViewsCoreDatasetModule } from "./datasets"; import { SapiViewsCoreParcellationModule } from "./parcellation/module"; import { SapiViewsCoreRegionModule } from "./region"; import { SapiViewsCoreRichModule } from "./rich/module"; @@ -10,7 +9,6 @@ import { SapiViewsCoreSpaceModule } from "./space"; @NgModule({ imports: [ CommonModule, - SapiViewsCoreDatasetModule, SapiViewsCoreRegionModule, SapiViewsCoreAtlasModule, SapiViewsCoreSpaceModule, @@ -18,7 +16,6 @@ import { SapiViewsCoreSpaceModule } from "./space"; SapiViewsCoreRichModule, ], exports: [ - SapiViewsCoreDatasetModule, SapiViewsCoreRegionModule, SapiViewsCoreAtlasModule, SapiViewsCoreSpaceModule, diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts index 31b8acbe2..97867311c 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationGroupSelected.pipe.ts @@ -14,6 +14,6 @@ function isGroupedParc(parc: GroupedParcellation|unknown): parc is GroupedParcel export class ParcellationGroupSelectedPipe implements PipeTransform { public transform(parc: GroupedParcellation|unknown, selectedParcellation: SxplrParcellation): boolean { if (!isGroupedParc(parc)) return false - return parc.parcellations.some(p => p["@id"] === selectedParcellation["@id"]) + return parc.parcellations.some(p => p.id === selectedParcellation.id) } } diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index 50759b93a..faee32dfa 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { MarkdownModule } from "src/components/markdown"; import { SpinnerModule } from "src/components/spinner"; +import { FeatureModule } from "src/features"; import { AngularMaterialModule } from "src/sharedModules"; import { StrictLocalModule } from "src/strictLocal"; // import { SapiViewsFeaturesModule } from "../../features"; @@ -21,6 +22,7 @@ import { SapiViewsCoreRegionRegionRich } from "./region/rich/region.rich.compone SpinnerModule, MarkdownModule, StrictLocalModule, + FeatureModule, ], declarations: [ SapiViewsCoreRegionRegionListItem, diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html index 461fcf176..c3acb2dfb 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html @@ -82,6 +82,11 @@ </markdown-dom> </ng-template> + <sxplr-feature-entry + [parcellation]="parcellation" + [region]="region"> + </sxplr-feature-entry> + <mat-accordion class="d-block mt-2"> <!-- desc --> @@ -100,7 +105,7 @@ <ng-template [ngIf]="!environment.STRICT_LOCAL"> <!-- feature list --> - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + <!-- <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { title: CONST.REGIONAL_FEATURES, iconClass: 'fas fa-database', content: kgRegionalFeatureList, @@ -108,7 +113,7 @@ iconTooltip: 'Regional Features', iavNgIf: true }"> - </ng-container> + </ng-container> --> <!-- connectivity --> <ng-template #sxplrSapiviewsFeaturesConnectivityBrowser> diff --git a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html index 044965ed5..58378b95d 100644 --- a/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html +++ b/src/atlasComponents/sapiViews/core/rich/ATPSelector/pureDumb/pureATPSelector.template.html @@ -99,7 +99,7 @@ <ng-template [ngIf]="selectedIds" let-selectedIds> <mat-icon fontSet="fas" - [fontIcon]="selectedIds.includes(item['@id']) ? 'fa-circle' : 'fa-none'" + [fontIcon]="selectedIds.includes(item.id) ? 'fa-circle' : 'fa-none'" > </mat-icon> </ng-template> diff --git a/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.html b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.html new file mode 100644 index 000000000..203d751df --- /dev/null +++ b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.html @@ -0,0 +1,68 @@ +<ng-template #headerTmpl> + <ng-content select="[header]"></ng-content> +</ng-template> + +<mat-card *ngIf="!feature"> + + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + <span> + Feature not specified. + </span> +</mat-card> + +<mat-card *ngIf="feature" + class="mat-elevation-z4 sxplr-z-4"> + <mat-card-title> + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + <div class="feature-title"> + {{ feature.name }} + </div> + </mat-card-title> + + <mat-card-subtitle class="sxplr-d-inline-flex sxplr-align-items-stretch"> + <ng-template [ngIf]="feature.category"> + <mat-icon class="sxplr-m-a" fontSet="fas" fontIcon="fa-database"></mat-icon> + <span class="sxplr-m-a"> + {{ feature.category }} + </span> + </ng-template> + + <mat-divider class="sxplr-pl-1" [vertical]="true"></mat-divider> + + <ng-template [ngIf]="busy$ | async"> + <spinner-cmp></spinner-cmp> + </ng-template> + + <!-- <a mat-icon-button sxplr-hide-when-local *ngFor="let url of dataset.link" [href]="url.doi | parseDoi" target="_blank"> + <i class="fas fa-external-link-alt"></i> + </a> --> + </mat-card-subtitle> +</mat-card> + +<mat-card *ngIf="feature" class="sxplr-z-0"> + <mat-card-content> + <!-- TODO fix feature typing! with proper translate fn --> + <markdown-dom class="sxplr-muted" [markdown]="feature['description']"> + </markdown-dom> + </mat-card-content> +</mat-card> + + +<!-- tabular special view --> +<ng-template [ngIf]="tabular$ | async" let-tabular> + <table class="feature-detail" mat-table [dataSource]="tabular | transformPdToDs"> + + <ng-container *ngFor="let column of columns$ | async" + [matColumnDef]="column"> + <th mat-header-cell *matHeaderCellDef> + {{ column }} + </th> + <td mat-cell *matCellDef="let element"> + {{ element[column] }} + </td> + </ng-container> + + <tr mat-header-row *matHeaderRowDef="columns$ | async"></tr> + <tr mat-row *matRowDef="let row; columns: columns$ | async;"></tr> + </table> +</ng-template> diff --git a/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.scss b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.scss new file mode 100644 index 000000000..e39c0ecb5 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.scss @@ -0,0 +1,11 @@ +.feature-title +{ + max-height: 8rem; + overflow-x: hidden; + overflow-y: auto; +} + +.feature-detail +{ + width: 100%; +} diff --git a/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.spec.ts b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.spec.ts new file mode 100644 index 000000000..6606d5571 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FeatureViewComponent } from './feature-view.component'; + +describe('FeatureViewComponent', () => { + let component: FeatureViewComponent; + let fixture: ComponentFixture<FeatureViewComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FeatureViewComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FeatureViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.ts b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.ts new file mode 100644 index 000000000..903ec3ffe --- /dev/null +++ b/src/atlasComponents/sapiViews/features/feature-view/feature-view.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; +import { Feature, TabularFeature } from 'src/atlasComponents/sapi/sxplrTypes'; + +function isTabularData(feature: unknown): feature is { data: TabularFeature<number|string|number[]> } { + return feature['@type'].includes("siibra-0.4/feature/tabular") +} + +@Component({ + selector: 'sxplr-feature-view', + templateUrl: './feature-view.component.html', + styleUrls: ['./feature-view.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FeatureViewComponent implements OnChanges { + + @Input() + feature: Feature + + busy$ = new BehaviorSubject<boolean>(false) + + tabular$: BehaviorSubject<TabularFeature<number|string|number[]>> = new BehaviorSubject(null) + columns$: Observable<string[]> = this.tabular$.pipe( + map(data => data + ? ['index', ...data.columns] + : []), + ) + constructor(private sapi: SAPI) { } + + ngOnChanges(): void { + console.log(this.feature) + this.tabular$.next(null) + this.busy$.next(true) + this.sapi.v3Get("/feature/{feature_id}", { + path: { + feature_id: this.feature.id + } + }).subscribe( + val => { + this.busy$.next(false) + + if (!isTabularData(val)) return + this.tabular$.next(val.data) + }, + () => this.busy$.next(false) + ) + } +} diff --git a/src/atlasComponents/sapiViews/features/features.module.ts b/src/atlasComponents/sapiViews/features/features.module.ts new file mode 100644 index 000000000..c05518863 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/features.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FeatureViewComponent } from './feature-view/feature-view.component'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatTableModule } from "@angular/material/table" +import { MarkdownModule } from 'src/components/markdown'; +import { TransformPdToDsPipe } from './transform-pd-to-ds.pipe'; +import { SpinnerModule } from 'src/components/spinner'; + + +@NgModule({ + declarations: [ + FeatureViewComponent, + TransformPdToDsPipe, + ], + imports: [ + CommonModule, + MatCardModule, + MatIconModule, + MatDividerModule, + MarkdownModule, + MatTableModule, + SpinnerModule, + ], + exports: [ + FeatureViewComponent, + ] +}) +export class FeaturesModule { } diff --git a/src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.spec.ts b/src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.spec.ts new file mode 100644 index 000000000..66eb0d5f6 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.spec.ts @@ -0,0 +1,8 @@ +import { TransformPdToDsPipe } from './transform-pd-to-ds.pipe'; + +describe('TransformPdToDsPipe', () => { + it('create an instance', () => { + const pipe = new TransformPdToDsPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.ts b/src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.ts new file mode 100644 index 000000000..09f598d9d --- /dev/null +++ b/src/atlasComponents/sapiViews/features/transform-pd-to-ds.pipe.ts @@ -0,0 +1,23 @@ +import { CdkTableDataSourceInput } from '@angular/cdk/table'; +import { Pipe, PipeTransform } from '@angular/core'; +import { TabularFeature } from 'src/atlasComponents/sapi/sxplrTypes'; + +@Pipe({ + name: 'transformPdToDs', + pure: true +}) +export class TransformPdToDsPipe implements PipeTransform { + + transform(pd: TabularFeature<string|number|number[]>): CdkTableDataSourceInput<unknown> { + return pd.data.map((arr, idx) => { + const returnVal: Record<string, string|number|number[]> = { + index: pd.index[idx], + } + arr.forEach((val, colIdx) => { + returnVal[pd.columns[colIdx]] = val + }) + return returnVal + }) + } + +} diff --git a/src/atlasComponents/sapiViews/module.ts b/src/atlasComponents/sapiViews/module.ts index dd53fa975..7e62c1e29 100644 --- a/src/atlasComponents/sapiViews/module.ts +++ b/src/atlasComponents/sapiViews/module.ts @@ -1,13 +1,14 @@ import { NgModule } from "@angular/core"; import { SapiViewsCoreModule } from "./core"; +import { FeaturesModule } from "./features/features.module"; @NgModule({ imports: [ - // SapiViewsFeaturesModule, + FeaturesModule, SapiViewsCoreModule, ], exports: [ - // SapiViewsFeaturesModule, + FeaturesModule, SapiViewsCoreModule, ] }) diff --git a/src/atlasComponents/sapiViews/richDataset.stories.ts b/src/atlasComponents/sapiViews/richDataset.stories.ts index 03ac405b6..112fcebe0 100644 --- a/src/atlasComponents/sapiViews/richDataset.stories.ts +++ b/src/atlasComponents/sapiViews/richDataset.stories.ts @@ -7,7 +7,6 @@ import { SAPI } from "src/atlasComponents/sapi" import { getHoc1RightFeatureDetail, getHoc1RightFeatures, getHoc1Right, getHumanAtlas, getJba29, getMni152, provideDarkTheme, getMni152SpatialFeatureHoc1Right } from "src/atlasComponents/sapi/stories.base" import { AngularMaterialModule } from "src/sharedModules" import { cleanIeegSessionDatasets, SapiAtlasModel, SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "../sapi/type" -import { SapiViewsCoreDatasetModule } from "./core/datasets" import { SapiViewsFeaturesModule } from "./features" @Component({ @@ -52,7 +51,6 @@ export default { AngularMaterialModule, CommonModule, HttpClientModule, - SapiViewsCoreDatasetModule, SapiViewsFeaturesModule, ], providers: [ diff --git a/src/components/smartChip/component/smartChip.component.ts b/src/components/smartChip/component/smartChip.component.ts index d933960ef..2e2ee0b7a 100644 --- a/src/components/smartChip/component/smartChip.component.ts +++ b/src/components/smartChip/component/smartChip.component.ts @@ -36,7 +36,7 @@ const cssColorToHsl = (input: string) => { selector: `sxplr-smart-chip`, templateUrl: `./smartChip.template.html`, styleUrls: [ - `/smartChip.style.css` + `./smartChip.style.scss` ], changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/src/components/smartChip/component/smartChip.style.css b/src/components/smartChip/component/smartChip.style.scss similarity index 100% rename from src/components/smartChip/component/smartChip.style.css rename to src/components/smartChip/component/smartChip.style.scss diff --git a/src/features/base.ts b/src/features/base.ts new file mode 100644 index 000000000..cda6bb56e --- /dev/null +++ b/src/features/base.ts @@ -0,0 +1,37 @@ +import { Input, OnChanges, Directive } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; + +@Directive() +export class FeatureBase implements OnChanges{ + + @Input() + template: SxplrTemplate + + @Input() + parcellation: SxplrParcellation + + @Input() + region: SxplrRegion + + @Input() + queryParams: Record<string, string> = {} + + protected TPR$ = new BehaviorSubject<{ template?: SxplrTemplate, parcellation?: SxplrParcellation, region?: SxplrRegion }>({ template: null, parcellation: null, region: null }) + + ngOnChanges(): void { + const { template, parcellation, region } = this + this.TPR$.next({ template, parcellation, region }) + } +} + + + +export const AllFeatures = { + CorticalProfile: "CorticalProfile", + // EbrainsDataFeature: "EbrainsDataFeature", + RegionalConnectivity: "RegionalConnectivity", + Tabular: "Tabular", + // GeneExpressions: "GeneExpressions", + Image: "Image", +} as const \ No newline at end of file diff --git a/src/features/category-acc.directive.spec.ts b/src/features/category-acc.directive.spec.ts new file mode 100644 index 000000000..2cdbb5846 --- /dev/null +++ b/src/features/category-acc.directive.spec.ts @@ -0,0 +1,8 @@ +import { CategoryAccDirective } from './category-acc.directive'; + +describe('CategoryAccDirective', () => { + it('should create an instance', () => { + const directive = new CategoryAccDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts new file mode 100644 index 000000000..b998efb0a --- /dev/null +++ b/src/features/category-acc.directive.ts @@ -0,0 +1,54 @@ +import { AfterContentInit, ContentChildren, Directive, OnDestroy, QueryList } from '@angular/core'; +import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ListComponent } from './list/list.component'; + +@Directive({ + selector: '[sxplrCategoryAcc]', + exportAs: 'categoryAcc' +}) +export class CategoryAccDirective implements AfterContentInit, OnDestroy { + + constructor() { } + + public isBusy$ = new BehaviorSubject<boolean>(false) + public total$ = new BehaviorSubject<number>(0) + + @ContentChildren(ListComponent, { read: ListComponent, descendants: true }) + listCmps: QueryList<ListComponent> + + #changeSub: Subscription + ngAfterContentInit(): void { + this.#registerListCmps() + this.#changeSub = this.listCmps.changes.subscribe(() => this.#registerListCmps()) + } + + ngOnDestroy(): void { + this.#cleanup() + } + + #subscriptions: Subscription[] = [] + #cleanup(){ + if (this.#changeSub) this.#changeSub.unsubscribe() + while(this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe() + } + #registerListCmps(){ + this.#cleanup() + + const listCmp = Array.from(this.listCmps) + + this.#subscriptions.push( + combineLatest( + listCmp.map(listCmp => listCmp.features$) + ).pipe( + map(features => features.reduce((acc, curr) => acc + curr.length, 0)) + ).subscribe(total => this.total$.next(total)), + + combineLatest( + listCmp.map(listCmp => listCmp.state$) + ).pipe( + map(states => states.some(state => state === "busy")) + ).subscribe(flag => this.isBusy$.next(flag)) + ) + } +} diff --git a/src/features/entry/entry.component.html b/src/features/entry/entry.component.html new file mode 100644 index 000000000..bd413fc42 --- /dev/null +++ b/src/features/entry/entry.component.html @@ -0,0 +1,65 @@ +<mat-card> + <mat-accordion> + <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue)" + sxplrCategoryAcc + #categoryAcc="categoryAcc" + [ngClass]="{ + 'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0 + }"> + + <mat-expansion-panel-header> + + <mat-panel-title> + {{ keyvalue.key }} + </mat-panel-title> + + <mat-panel-description> + <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp> + <span> + {{ categoryAcc.total$ | async }} + </span> + </mat-panel-description> + </mat-expansion-panel-header> + + <div class="c3-outer"> + <div class="c3-inner"> + + <mat-card class="c3 mat-elevation-z4" + *ngFor="let feature of keyvalue.value" + [ngClass]="{ + 'sxplr-d-none': (list.state$ | async) === 'noresult' + }"> + <mat-card-header> + + <mat-card-title> + <span class="category-title sxplr-white-space-nowrap"> + {{ feature.name | featureNamePipe }} + </span> + </mat-card-title> + + </mat-card-header> + + + <mat-card-content> + <sxplr-feature-list + [template]="template" + [parcellation]="parcellation" + [region]="region" + [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }" + [featureRoute]="feature.path" + (onClickFeature)="onClickFeature($event)" + #list="featureList" + > + </sxplr-feature-list> + + <spinner-cmp *ngIf="(list.state$ | async) === 'busy'"></spinner-cmp> + + </mat-card-content> + </mat-card> + </div> + </div> + </mat-expansion-panel> + </mat-accordion> + +</mat-card> + diff --git a/src/features/entry/entry.component.scss b/src/features/entry/entry.component.scss new file mode 100644 index 000000000..44bcd0aaa --- /dev/null +++ b/src/features/entry/entry.component.scss @@ -0,0 +1,62 @@ +:host > mat-card +{ + padding-left: 0; + padding-right: 0; +} + +mat-list-item +{ + text-overflow: ellipsis; + white-space: nowrap; +} + +// card in card container +.c3-outer +{ + display: inline-block; + overflow-x: auto; + height: 15rem; + width: 100%; +} + +.c3-inner +{ + + height: 100%; + display: inline-flex; + gap: 1.5rem; + flex-wrap: nowrap; + + margin: 0 2rem; + align-items: stretch; +} + +.c3-inner > mat-card +{ + width: 16rem; + overflow:hidden; + height: 95%; +} + +.category-title:hover +{ + cursor: default; +} + +mat-card.c3 +{ + display: flex; + flex-direction: column; +} + +mat-card.c3 > mat-card-header +{ + flex: 0 0 auto; +} + +mat-card.c3 > mat-card-content +{ + flex: 1 1 auto; + height: 75%; + overflow: auto; +} \ No newline at end of file diff --git a/src/features/entry/entry.component.spec.ts b/src/features/entry/entry.component.spec.ts new file mode 100644 index 000000000..198c166cd --- /dev/null +++ b/src/features/entry/entry.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EntryComponent } from './entry.component'; + +describe('EntryComponent', () => { + let component: EntryComponent; + let fixture: ComponentFixture<EntryComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EntryComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EntryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts new file mode 100644 index 000000000..e2bb436d0 --- /dev/null +++ b/src/features/entry/entry.component.ts @@ -0,0 +1,73 @@ +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { SAPI } from 'src/atlasComponents/sapi'; +import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; +import { FeatureBase } from '../base'; +import * as userInteraction from "src/state/userInteraction" + +const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => { + const returnVal: Record<string, T[]> = {} + for (const item of categories) { + const { category, ...rest } = item + if (!category) continue + if (typeof category !== "string") continue + if (!returnVal[category]) { + returnVal[category] = [] + } + returnVal[category].push(item) + } + return returnVal +} + +@Component({ + selector: 'sxplr-feature-entry', + templateUrl: './entry.component.html', + styleUrls: ['./entry.component.scss'] +}) +export class EntryComponent extends FeatureBase { + + constructor(private sapi: SAPI, private store: Store) { + super() + } + + private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( + switchMap(resp => + this.sapi.iteratePages( + resp, + page => this.sapi.v3Get( + "/feature/_types", + { query: { page } } + ) + ) + ), + ) + + public cateogryCollections$ = this.TPR$.pipe( + switchMap(({ template, parcellation, region }) => this.featureTypes$.pipe( + map(features => { + const filteredFeatures = features.filter(v => { + const params = [ + ...(v.path_params || []), + ...(v.query_params || []), + ] + return [ + params.includes("space_id") === (!!template), + params.includes("parcellation_id") === (!!parcellation), + params.includes("region_id") === (!!region), + ].some(val => val) + }) + return categoryAcc(filteredFeatures) + }), + )), + ) + + onClickFeature(feature: Feature) { + this.store.dispatch( + userInteraction.actions.showFeature({ + feature + }) + ) + } +} + diff --git a/src/features/featureName.pipe.ts b/src/features/featureName.pipe.ts new file mode 100644 index 000000000..004eedba3 --- /dev/null +++ b/src/features/featureName.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'featureNamePipe', + pure: true, +}) + +export class FeatureNamePipe implements PipeTransform{ + public transform(name: string): string { + return name.split(".").slice(-1)[0] + } +} diff --git a/src/features/fetch.directive.spec.ts b/src/features/fetch.directive.spec.ts new file mode 100644 index 000000000..5765d9f6e --- /dev/null +++ b/src/features/fetch.directive.spec.ts @@ -0,0 +1,8 @@ +import { FetchDirective } from './fetch.directive'; + +describe('FetchDirective', () => { + it('should create an instance', () => { + const directive = new FetchDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/features/fetch.directive.ts b/src/features/fetch.directive.ts new file mode 100644 index 000000000..eb40e4fd0 --- /dev/null +++ b/src/features/fetch.directive.ts @@ -0,0 +1,71 @@ +import { Directive, Input, OnChanges, Output, SimpleChanges, EventEmitter } from '@angular/core'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { SAPI } from 'src/atlasComponents/sapi'; +import { SxplrParcellation, SxplrRegion, SxplrTemplate } from 'src/atlasComponents/sapi/sxplrTypes'; +import { FeatureType, SapiRoute } from 'src/atlasComponents/sapi/typeV3'; +import { Feature, CorticalFeature, VoiFeature, TabularFeature } from "src/atlasComponents/sapi/sxplrTypes" + +type ObservableOf<Obs extends Observable<unknown>> = Parameters<Obs['subscribe']>[0] extends Function +? Parameters<Parameters<Obs['subscribe']>[0]>[0] +: never + +const b = new Subject<{ t: string }>() + +type typeOfB = ObservableOf<typeof b> + +type FeatureMap = { + "RegionalConnectivity": Feature + "CorticalProfile": CorticalFeature<number> + "Tabular": TabularFeature<number|string|number[]> + "Image": VoiFeature +} + +@Directive({ + selector: '[sxplrFeatureFetch]', + exportAs: "sxplrFeatureFetch" +}) +export class FetchDirective<T extends keyof FeatureMap> implements OnChanges { + + /** + * TODO check if the decorated property survive on inheritence + */ + + @Input() + template: SxplrTemplate + + @Input() + parcellation: SxplrParcellation + + @Input() + region: SxplrRegion + + @Input() + featureType: T + + @Output() + features: BehaviorSubject<FeatureMap[T][]> = new BehaviorSubject([]) + + @Output() + busy$: BehaviorSubject<boolean> = new BehaviorSubject(false) + + constructor( + private sapi: SAPI + ) { } + + async ngOnChanges(changes: SimpleChanges) { + if (!this.featureType) { + console.warn(`featureType must be defined!`) + } + this.busy$.next(true) + const features = await this.sapi.getV3Features(this.featureType, { + query: { + parcellation_id: this.parcellation?.id, + space_id: this.template?.id, + region_id: this.region?.name, + } + }).toPromise() + this.busy$.next(false) + this.features.next(features as any[]) + } +} diff --git a/src/features/index.ts b/src/features/index.ts new file mode 100644 index 000000000..d99543461 --- /dev/null +++ b/src/features/index.ts @@ -0,0 +1 @@ +export { FeatureModule } from "./module" \ No newline at end of file diff --git a/src/features/list/list.component.html b/src/features/list/list.component.html new file mode 100644 index 000000000..c50515f25 --- /dev/null +++ b/src/features/list/list.component.html @@ -0,0 +1,11 @@ +<mat-list> + <mat-list-item mat-ripple + *ngFor="let feature of features$ | async" + [matTooltip]="feature.name" + matTooltipPosition="right" + (click)="onClickItem(feature)"> + <span class="feature-name"> + {{ feature.name }} + </span> + </mat-list-item> +</mat-list> diff --git a/src/features/list/list.component.scss b/src/features/list/list.component.scss new file mode 100644 index 000000000..994486936 --- /dev/null +++ b/src/features/list/list.component.scss @@ -0,0 +1,15 @@ +:host +{ + display: block; + width: 100%; +} + +.feature-name +{ + white-space: nowrap; +} + +.feature-name:hover +{ + cursor: default; +} diff --git a/src/features/list/list.component.spec.ts b/src/features/list/list.component.spec.ts new file mode 100644 index 000000000..54ae34808 --- /dev/null +++ b/src/features/list/list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: ListComponent; + let fixture: ComponentFixture<ListComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ListComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/features/list/list.component.ts b/src/features/list/list.component.ts new file mode 100644 index 000000000..9d81ab404 --- /dev/null +++ b/src/features/list/list.component.ts @@ -0,0 +1,67 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { BehaviorSubject, combineLatest, NEVER, Observable, of, throwError } from 'rxjs'; +import { catchError, switchMap, tap } from 'rxjs/operators'; +import { SAPI } from 'src/atlasComponents/sapi'; +import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; +import { FeatureType } from 'src/atlasComponents/sapi/typeV3'; +import { AllFeatures, FeatureBase } from '../base'; + +@Component({ + selector: 'sxplr-feature-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'], + exportAs: "featureList" +}) +export class ListComponent extends FeatureBase { + + @Output() + onClickFeature = new EventEmitter<Feature>() + + @Input() + featureRoute: string + private guardedRoute$ = new BehaviorSubject<FeatureType>(null) + + public state$ = new BehaviorSubject<'busy'|'noresult'|'result'>('noresult') + + constructor(private sapi: SAPI) { + super() + } + + ngOnChanges(): void { + super.ngOnChanges() + const featureType = (this.featureRoute || '').split("/").slice(-1)[0] + this.guardedRoute$.next(AllFeatures[featureType]) + } + + public features$: Observable<Feature[]> = combineLatest([ + this.guardedRoute$, + this.TPR$, + ]).pipe( + tap(() => this.state$.next('busy')), + switchMap(([route, { template, parcellation, region }]) => { + if (!route) { + return throwError("noresult") + } + const query = {} + if (template) query['space_id'] = template.id + if (parcellation) query['parcellation_id'] = parcellation.id + if (region) query['region_id'] = region.name + return this.sapi.getV3Features(route, { + query: { + ...this.queryParams, + ...query, + } as any + }).pipe( + ) + }), + catchError(() => { + this.state$.next("noresult") + return of([] as Feature[]) + }), + tap(result => this.state$.next(result.length > 0 ? 'result' : 'noresult')), + ) + + onClickItem(feature: Feature){ + this.onClickFeature.emit(feature) + } +} diff --git a/src/features/module.ts b/src/features/module.ts new file mode 100644 index 000000000..16214d8b1 --- /dev/null +++ b/src/features/module.ts @@ -0,0 +1,40 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatRippleModule } from "@angular/material/core"; +import { MatExpansionModule } from "@angular/material/expansion"; +import { MatListModule } from "@angular/material/list"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { SpinnerModule } from "src/components/spinner"; +import { UtilModule } from "src/util"; +import { EntryComponent } from './entry/entry.component' +import { FeatureNamePipe } from "./featureName.pipe"; +import { FetchDirective } from "./fetch.directive"; +import { ListComponent } from './list/list.component'; +import { CategoryAccDirective } from './category-acc.directive'; + +@NgModule({ + imports: [ + CommonModule, + MatCardModule, + MatExpansionModule, + SpinnerModule, + MatListModule, + MatTooltipModule, + UtilModule, + MatRippleModule, + ], + declarations: [ + EntryComponent, + ListComponent, + + FetchDirective, + CategoryAccDirective, + + FeatureNamePipe, + ], + exports: [ + EntryComponent, + ] +}) +export class FeatureModule{} \ No newline at end of file diff --git a/src/overwrite.scss b/src/overwrite.scss index 86f98096c..a60e27556 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -153,7 +153,7 @@ $transform-origin-maps: ( margin-bottom: auto!important; } -$display-vars: none, block, inline-block, flex, inline-flex; +$display-vars: none, block, inline-block, flex, inline-flex, grid; @each $display-var in $display-vars { .d-#{$display-var} { diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts index c1e2460f8..8a6fe4d33 100644 --- a/src/routerModule/routeStateTransform.service.ts +++ b/src/routerModule/routeStateTransform.service.ts @@ -39,7 +39,7 @@ export class RouteStateTransformSvc { allParcellationRegions = [] ] = await Promise.all([ this.sapi.atlases$.pipe( - map(atlases => atlases.find(atlas => atlas["@id"] === selectedAtlasId)) + map(atlases => atlases.find(atlas => atlas.id === selectedAtlasId)) ).toPromise(), this.sapi.v3Get("/spaces/{space_id}", { path: { @@ -296,17 +296,17 @@ export class RouteStateTransformSvc { routes = { // for atlas - a: selectedAtlas && encodeId(selectedAtlas['@id']), + a: selectedAtlas && encodeId(selectedAtlas.id), // for template - t: selectedTemplate && encodeId(selectedTemplate['@id']), + t: selectedTemplate && encodeId(selectedTemplate.id), // for parcellation - p: selectedParcellation && encodeId(selectedParcellation['@id']), + p: selectedParcellation && encodeId(selectedParcellation.id), // for regions r: selectedRegionsString && encodeURIFull(selectedRegionsString), // nav ['@']: cNavString, // showing dataset - f: selectedFeature && encodeId(selectedFeature["@id"]) + f: selectedFeature && encodeId(selectedFeature.id) } /** diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 92442f079..7b25c2e9a 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -160,7 +160,6 @@ export class Effect { this.onTemplateParcSelectionPostHook.map(fn => fn({ previous: { atlas: currAtlas, template: currTmpl, parcellation: currParc }, current: { atlas, template, parcellation } })) ).pipe( map(partialStates => { - console.log('selected teplate', template, parcellation) let returnState: Partial<AtlasSelectionState> = { selectedAtlas: atlas, selectedTemplate: template, diff --git a/src/util/priority.ts b/src/util/priority.ts index b12abb243..24cdd9472 100644 --- a/src/util/priority.ts +++ b/src/util/priority.ts @@ -170,7 +170,7 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ filter(v => v.urlWithParams === urlWithParams), take(1), map(v => { - if (v instanceof Error) { + if (v['error'] instanceof Error) { throw v } return (v as Result<unknown>).result diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 0731f9bf2..ce5992c13 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -911,8 +911,8 @@ let-backCb="backCb" let-feature="feature"> - <sxplr-sapiviews-core-datasets-dataset class="sxplr-z-2 mat-elevation-z2" - [sxplr-sapiviews-core-datasets-dataset-input]="feature"> + <sxplr-feature-view class="sxplr-z-2 mat-elevation-z2" + [feature]="feature"> <div header> <!-- back btn --> @@ -929,7 +929,7 @@ </button> </div> - </sxplr-sapiviews-core-datasets-dataset> + </sxplr-feature-view> <!-- TODO FIXME --> <!-- <sxplr-sapiviews-features-entry -- GitLab