From 7a0068c74c7e7c6e73a36ddf17240155fc47d0af Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 15 Jan 2024 13:33:18 +0100 Subject: [PATCH 01/48] feat: support for compound feat --- .github/workflows/docker_img.yml | 1 + .storybook/preview-head.html | 1 - docs/releases/v2.14.5.md | 9 + mkdocs.yml | 1 + package.json | 2 +- src/atlasComponents/sapi/openapi.json | 425 +++++++++++-- src/atlasComponents/sapi/sapi.service.ts | 13 +- src/atlasComponents/sapi/schemaV3.ts | 256 ++++++-- src/atlasComponents/sapi/sxplrTypes.ts | 10 + src/atlasComponents/sapi/translateV3.ts | 40 +- src/atlasComponents/sapi/typeV3.ts | 4 +- .../compoundFtContainer.component.ts | 51 ++ .../compoundFtContainer.style.css | 16 + .../compoundFtContainer.template.html | 19 + src/features/compoundFtContainer/index.ts | 2 + .../compoundFtContainer/indexToText.pipe.ts | 15 + src/features/compoundFtContainer/module.ts | 20 + .../connectivityBrowser.component.spec.ts | 124 ---- .../connectivityBrowser.component.ts | 590 ------------------ .../connectivityBrowser.style.scss | 3 - .../connectivityBrowser.template.html | 162 ----- src/features/connectivity/index.ts | 2 - src/features/connectivity/module.ts | 33 - src/features/entry/entry.component.ts | 52 +- .../entry/entry.flattened.component.html | 56 +- .../feature-view/feature-view.component.html | 4 +- .../feature-view/feature-view.component.ts | 132 +++- src/features/guards.ts | 1 - src/features/list/list.directive.ts | 2 +- src/features/module.ts | 4 +- src/features/plotly/plot/plot.component.scss | 2 - src/features/plotly/plot/plot.component.ts | 36 +- src/index.html | 1 - .../routeStateTransform.service.spec.ts | 6 +- src/state/atlasSelection/actions.ts | 2 +- src/state/userInteraction/store.ts | 7 + 36 files changed, 996 insertions(+), 1108 deletions(-) create mode 100644 docs/releases/v2.14.5.md create mode 100644 src/features/compoundFtContainer/compoundFtContainer.component.ts create mode 100644 src/features/compoundFtContainer/compoundFtContainer.style.css create mode 100644 src/features/compoundFtContainer/compoundFtContainer.template.html create mode 100644 src/features/compoundFtContainer/index.ts create mode 100644 src/features/compoundFtContainer/indexToText.pipe.ts create mode 100644 src/features/compoundFtContainer/module.ts delete mode 100644 src/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts delete mode 100644 src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts delete mode 100644 src/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss delete mode 100644 src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html delete mode 100644 src/features/connectivity/index.ts delete mode 100644 src/features/connectivity/module.ts diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index a9826c04e..cc88357ec 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -97,6 +97,7 @@ jobs: BRANCH_NAME: ${{ steps.set-vars.outputs.BRANCH_NAME }} BUILD_TEXT: ${{ steps.set-vars.outputs.BUILD_TEXT }} DEPLOY_ID: ${{ steps.set-vars.outputs.DEPLOY_ID }} + SXPLR_VERSION: ${{ steps.set-vars.outputs.SXPLR_VERSION }} steps: - uses: actions/checkout@v3 - id: set-vars diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index e10e90c52..f77b809f6 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -14,5 +14,4 @@ } - diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md new file mode 100644 index 000000000..fedf235b8 --- /dev/null +++ b/docs/releases/v2.14.5.md @@ -0,0 +1,9 @@ +# v2.14.5 + +## Feature + +- Add support for compound feature + +## Behind the Scenes + +- Removed dependency on connectivity-component \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 322efe7b2..dfd5ace1d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ nav: - Differential gene expression analysis: "advanced/differential_gene_expression_analysis.md" - Release notes: + - v2.14.5: 'releases/v2.14.5.md' - v2.14.4: 'releases/v2.14.4.md' - v2.14.3: 'releases/v2.14.3.md' - v2.14.2: 'releases/v2.14.2.md' diff --git a/package.json b/package.json index 86ceb19f2..743b9f466 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siibra-explorer", - "version": "2.14.4", + "version": "2.14.5", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "lint": "eslint src --ext .ts", diff --git a/src/atlasComponents/sapi/openapi.json b/src/atlasComponents/sapi/openapi.json index 808fe0f2b..ac177af53 100644 --- a/src/atlasComponents/sapi/openapi.json +++ b/src/atlasComponents/sapi/openapi.json @@ -65,6 +65,72 @@ } } }, + "/feature/{feature_id}/intents": { + "get": { + "tags": [ + "feature" + ], + "summary": "Get Single Feature Intents", + "description": "Get feature intents from feature_id", + "operationId": "get_single_feature_intents_feature__feature_id__intents_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "title": "Feature Id" + }, + "name": "feature_id", + "in": "path" + }, + { + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "title": "Page", + "default": 1 + }, + "name": "page", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "title": "Size", + "default": 50 + }, + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_ColorizationIntent_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/feature/{feature_id}/download": { "get": { "tags": [ @@ -538,7 +604,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__" + "$ref": "#/components/schemas/Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__CompoundFeatureModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__" } } } @@ -1279,6 +1345,15 @@ "name": "parcellation_id", "in": "query" }, + { + "required": false, + "schema": { + "type": "string", + "title": "Region Id" + }, + "name": "region_id", + "in": "query" + }, { "required": false, "schema": { @@ -1318,7 +1393,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_SiibraRegionalConnectivityModel_" + "$ref": "#/components/schemas/Page_Union_SiibraRegionalConnectivityModel__CompoundFeatureModel__" } } } @@ -1342,7 +1417,7 @@ "feature" ], "summary": "Get Single Connectivity Feature", - "description": "subject is an optional param.\nIf provided, the specific matrix will be return.\nIf not provided, the matrix averaged between subjects will be returned under the key _average.", + "description": "Get single connectivity feature", "operationId": "get_single_connectivity_feature_feature_RegionalConnectivity__feature_id__get", "parameters": [ { @@ -1367,9 +1442,9 @@ "required": false, "schema": { "type": "string", - "title": "Subject" + "title": "Region Id" }, - "name": "subject", + "name": "region_id", "in": "query" }, { @@ -1388,7 +1463,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SiibraRegionalConnectivityModel" + "anyOf": [ + { + "$ref": "#/components/schemas/SiibraRegionalConnectivityModel" + }, + { + "$ref": "#/components/schemas/CompoundFeatureModel" + } + ], + "title": "Response Get Single Connectivity Feature Feature Regionalconnectivity Feature Id Get" } } } @@ -1472,7 +1555,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_SiibraCorticalProfileModel_" + "$ref": "#/components/schemas/Page_Union_SiibraCorticalProfileModel__CompoundFeatureModel__" } } } @@ -1542,7 +1625,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SiibraCorticalProfileModel" + "anyOf": [ + { + "$ref": "#/components/schemas/SiibraCorticalProfileModel" + }, + { + "$ref": "#/components/schemas/CompoundFeatureModel" + } + ], + "title": "Response Get Single Corticalprofile Feature Feature Corticalprofile Feature Id Get" } } } @@ -1626,7 +1717,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__" + "$ref": "#/components/schemas/Page_Union_CompoundFeatureModel__SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__" } } } @@ -1697,6 +1788,9 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/CompoundFeatureModel" + }, { "$ref": "#/components/schemas/SiibraCorticalProfileModel" }, @@ -1744,7 +1838,7 @@ "in": "query" }, { - "required": false, + "required": true, "schema": { "type": "string", "title": "Bbox" @@ -2239,6 +2333,9 @@ { "$ref": "#/components/schemas/SiibraCorticalProfileModel" }, + { + "$ref": "#/components/schemas/CompoundFeatureModel" + }, { "$ref": "#/components/schemas/SiibraRegionalConnectivityModel" }, @@ -2599,6 +2696,28 @@ "title": "BrainAtlasVersionModel", "description": "ConfigBaseModel" }, + "ColorizationIntent": { + "properties": { + "@type": { + "type": "string", + "title": "@Type" + }, + "region_mappings": { + "items": { + "$ref": "#/components/schemas/RegionMapping" + }, + "type": "array", + "title": "Region Mappings" + } + }, + "type": "object", + "required": [ + "@type", + "region_mappings" + ], + "title": "ColorizationIntent", + "description": "ConfigBaseModel" + }, "CommonCoordinateSpaceModel": { "properties": { "@type": { @@ -2715,6 +2834,63 @@ "title": "CommonCoordinateSpaceModel", "description": "CommonCoordinateSpaceModel" }, + "CompoundFeatureModel": { + "properties": { + "@type": { + "type": "string", + "title": "@Type" + }, + "id": { + "type": "string", + "title": "Id" + }, + "modality": { + "type": "string", + "title": "Modality" + }, + "category": { + "type": "string", + "title": "Category" + }, + "description": { + "type": "string", + "title": "Description" + }, + "name": { + "type": "string", + "title": "Name" + }, + "datasets": { + "items": { + "$ref": "#/components/schemas/EbrainsDatasetModel" + }, + "type": "array", + "title": "Datasets" + }, + "anchor": { + "$ref": "#/components/schemas/SiibraAnchorModel" + }, + "indices": { + "items": { + "$ref": "#/components/schemas/SubfeatureModel" + }, + "type": "array", + "title": "Indices" + } + }, + "type": "object", + "required": [ + "@type", + "id", + "category", + "description", + "name", + "datasets", + "indices" + ], + "title": "CompoundFeatureModel", + "description": "AbstractBaseModel\n\nsee [api.models._commons.ConfigBaseModel][]" + }, "CoordinatePointModel": { "properties": { "@type": { @@ -3288,6 +3464,43 @@ ], "title": "OtherContribution" }, + "Page_ColorizationIntent_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ColorizationIntent" + }, + "type": "array", + "title": "Items" + }, + "total": { + "type": "integer", + "minimum": 0, + "title": "Total" + }, + "page": { + "type": "integer", + "minimum": 1, + "title": "Page" + }, + "size": { + "type": "integer", + "minimum": 1, + "title": "Size" + }, + "pages": { + "type": "integer", + "minimum": 0, + "title": "Pages" + } + }, + "type": "object", + "required": [ + "items", + "total" + ], + "title": "Page[ColorizationIntent]" + }, "Page_CommonCoordinateSpaceModel_": { "properties": { "items": { @@ -3320,7 +3533,8 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], "title": "Page[CommonCoordinateSpaceModel]" }, @@ -3356,7 +3570,8 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], "title": "Page[FeatureMetaModel]" }, @@ -3392,7 +3607,8 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], "title": "Page[ParcellationEntityVersionModel]" }, @@ -3428,7 +3644,8 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], "title": "Page[RegionRelationAsmtModel]" }, @@ -3464,15 +3681,16 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], "title": "Page[SiibraAtlasModel]" }, - "Page_SiibraCorticalProfileModel_": { + "Page_SiibraEbrainsDataFeatureModel_": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/SiibraCorticalProfileModel" + "$ref": "#/components/schemas/SiibraEbrainsDataFeatureModel" }, "type": "array", "title": "Items" @@ -3500,15 +3718,16 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[SiibraCorticalProfileModel]" + "title": "Page[SiibraEbrainsDataFeatureModel]" }, - "Page_SiibraEbrainsDataFeatureModel_": { + "Page_SiibraParcellationModel_": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/SiibraEbrainsDataFeatureModel" + "$ref": "#/components/schemas/SiibraParcellationModel" }, "type": "array", "title": "Items" @@ -3536,15 +3755,16 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[SiibraEbrainsDataFeatureModel]" + "title": "Page[SiibraParcellationModel]" }, - "Page_SiibraParcellationModel_": { + "Page_SiibraTabularModel_": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/SiibraParcellationModel" + "$ref": "#/components/schemas/SiibraTabularModel" }, "type": "array", "title": "Items" @@ -3572,15 +3792,16 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[SiibraParcellationModel]" + "title": "Page[SiibraTabularModel]" }, - "Page_SiibraRegionalConnectivityModel_": { + "Page_SiibraVoiModel_": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/SiibraRegionalConnectivityModel" + "$ref": "#/components/schemas/SiibraVoiModel" }, "type": "array", "title": "Items" @@ -3608,15 +3829,29 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[SiibraRegionalConnectivityModel]" + "title": "Page[SiibraVoiModel]" }, - "Page_SiibraTabularModel_": { + "Page_Union_CompoundFeatureModel__SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/SiibraTabularModel" + "anyOf": [ + { + "$ref": "#/components/schemas/CompoundFeatureModel" + }, + { + "$ref": "#/components/schemas/SiibraCorticalProfileModel" + }, + { + "$ref": "#/components/schemas/SiibraReceptorDensityFp" + }, + { + "$ref": "#/components/schemas/SiibraTabularModel" + } + ] }, "type": "array", "title": "Items" @@ -3644,15 +3879,23 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[SiibraTabularModel]" + "title": "Page[Union[CompoundFeatureModel, SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]]" }, - "Page_SiibraVoiModel_": { + "Page_Union_SiibraCorticalProfileModel__CompoundFeatureModel__": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/SiibraVoiModel" + "anyOf": [ + { + "$ref": "#/components/schemas/SiibraCorticalProfileModel" + }, + { + "$ref": "#/components/schemas/CompoundFeatureModel" + } + ] }, "type": "array", "title": "Items" @@ -3680,23 +3923,21 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[SiibraVoiModel]" + "title": "Page[Union[SiibraCorticalProfileModel, CompoundFeatureModel]]" }, - "Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__": { + "Page_Union_SiibraRegionalConnectivityModel__CompoundFeatureModel__": { "properties": { "items": { "items": { "anyOf": [ { - "$ref": "#/components/schemas/SiibraCorticalProfileModel" - }, - { - "$ref": "#/components/schemas/SiibraReceptorDensityFp" + "$ref": "#/components/schemas/SiibraRegionalConnectivityModel" }, { - "$ref": "#/components/schemas/SiibraTabularModel" + "$ref": "#/components/schemas/CompoundFeatureModel" } ] }, @@ -3726,11 +3967,12 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[Union[SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]]" + "title": "Page[Union[SiibraRegionalConnectivityModel, CompoundFeatureModel]]" }, - "Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__": { + "Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__CompoundFeatureModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__": { "properties": { "items": { "items": { @@ -3741,6 +3983,9 @@ { "$ref": "#/components/schemas/SiibraCorticalProfileModel" }, + { + "$ref": "#/components/schemas/CompoundFeatureModel" + }, { "$ref": "#/components/schemas/SiibraRegionalConnectivityModel" }, @@ -3781,9 +4026,10 @@ }, "type": "object", "required": [ - "items" + "items", + "total" ], - "title": "Page[Union[SiibraVoiModel, SiibraCorticalProfileModel, SiibraRegionalConnectivityModel, SiibraReceptorDensityFp, SiibraTabularModel, SiibraEbrainsDataFeatureModel]]" + "title": "Page[Union[SiibraVoiModel, SiibraCorticalProfileModel, CompoundFeatureModel, SiibraRegionalConnectivityModel, SiibraReceptorDensityFp, SiibraTabularModel, SiibraEbrainsDataFeatureModel]]" }, "ParcellationEntityVersionModel": { "properties": { @@ -3973,6 +4219,26 @@ ], "title": "QuantitativeOverlapItem1" }, + "RegionMapping": { + "properties": { + "region": { + "$ref": "#/components/schemas/ParcellationEntityVersionModel" + }, + "rgb": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Rgb" + } + }, + "type": "object", + "required": [ + "region", + "rgb" + ], + "title": "RegionMapping" + }, "RegionRelationAsmtModel": { "properties": { "@type": { @@ -4584,19 +4850,16 @@ "type": "string", "title": "Cohort" }, - "subjects": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Subjects" + "subject": { + "type": "string", + "title": "Subject" }, - "matrices": { - "additionalProperties": { - "$ref": "#/components/schemas/DataFrameModel" - }, - "type": "object", - "title": "Matrices" + "feature": { + "type": "string", + "title": "Feature" + }, + "matrix": { + "$ref": "#/components/schemas/DataFrameModel" } }, "type": "object", @@ -4608,7 +4871,7 @@ "name", "datasets", "cohort", - "subjects" + "subject" ], "title": "SiibraRegionalConnectivityModel", "description": "SiibraRegionalConnectivityModel" @@ -4740,6 +5003,42 @@ ], "title": "StatisticModelInfo" }, + "SubfeatureModel": { + "properties": { + "@type": { + "type": "string", + "title": "@Type" + }, + "id": { + "type": "string", + "title": "Id" + }, + "index": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/CoordinatePointModel" + } + ], + "title": "Index" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "@type", + "id", + "index", + "name" + ], + "title": "SubfeatureModel", + "description": "ConfigBaseModel" + }, "ValidationError": { "properties": { "loc": { diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 03167dda1..33ea800e4 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -20,7 +20,7 @@ export const useViewer = { } as const export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' -export const EXPECTED_SIIBRA_API_VERSION = '0.3.16' +export const EXPECTED_SIIBRA_API_VERSION = '0.3.17' type PaginatedResponse = { items: T[] @@ -183,7 +183,7 @@ export class SAPI{ }) } - getFeaturePlot(id: string, params: RouteParam<"/feature/{feature_id}/plotly">["query"] = {}) { + getFeaturePlot(id: string, params: RouteParam<"/feature/{feature_id}/plotly">["query"] & Record = {}) { return this.v3Get("/feature/{feature_id}/plotly", { path: { feature_id: id @@ -191,6 +191,15 @@ export class SAPI{ query: params }) } + + getFeatureIntents(id: string, params: Record = {}) { + return this.v3Get("/feature/{feature_id}/intents", { + path: { + feature_id: id + }, + query: params + }) + } @CachedFunction({ serialization: (id, params) => `featDetail:${id}:${Object.entries(params || {}).map(([key, val]) => `${key},${val}`).join('.')}` diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index 2e26c5065..c29243103 100644 --- a/src/atlasComponents/sapi/schemaV3.ts +++ b/src/atlasComponents/sapi/schemaV3.ts @@ -14,6 +14,13 @@ export interface paths { */ get: operations["get_single_feature_plot_feature__feature_id__plotly_get"] } + "/feature/{feature_id}/intents": { + /** + * Get Single Feature Intents + * @description Get feature intents from feature_id + */ + get: operations["get_single_feature_intents_feature__feature_id__intents_get"] + } "/feature/{feature_id}/download": { /** * Get Single Feature Download @@ -146,6 +153,20 @@ export interface paths { */ get: operations["get_download_bundle_atlas_download_get"] } + "/atlas_download/{task_id}": { + /** + * Get Download Progress + * @description Get download task progress with task_id + */ + get: operations["get_download_progress_atlas_download__task_id__get"] + } + "/atlas_download/{task_id}/download": { + /** + * Get Download Result + * @description Download the bundle + */ + get: operations["get_download_result_atlas_download__task_id__download_get"] + } "/feature/_types": { /** * Get All Feature Types @@ -163,9 +184,7 @@ export interface paths { "/feature/RegionalConnectivity/{feature_id}": { /** * Get Single Connectivity Feature - * @description subject is an optional param. - * If provided, the specific matrix will be return. - * If not provided, the matrix averaged between subjects will be returned under the key _average. + * @description Get single connectivity feature */ get: operations["get_single_connectivity_feature_feature_RegionalConnectivity__feature_id__get"] } @@ -471,6 +490,16 @@ export interface components { */ versionInnovation: string } + /** + * ColorizationIntent + * @description ConfigBaseModel + */ + ColorizationIntent: { + /** @Type */ + "@type": string + /** Region Mappings */ + region_mappings: (components["schemas"]["RegionMapping"])[] + } /** * CommonCoordinateSpaceModel * @description CommonCoordinateSpaceModel @@ -553,6 +582,31 @@ export interface components { /** Datasets */ datasets?: (components["schemas"]["EbrainsDatasetModel"])[] } + /** + * CompoundFeatureModel + * @description AbstractBaseModel + * + * see [api.models._commons.ConfigBaseModel][] + */ + CompoundFeatureModel: { + /** @Type */ + "@type": string + /** Id */ + id: string + /** Modality */ + modality?: string + /** Category */ + category: string + /** Description */ + description: string + /** Name */ + name: string + /** Datasets */ + datasets: (components["schemas"]["EbrainsDatasetModel"])[] + anchor?: components["schemas"]["SiibraAnchorModel"] + /** Indices */ + indices: (components["schemas"]["SubfeatureModel"])[] + } /** * CoordinatePointModel * @description CoordinatePointModel @@ -849,6 +903,19 @@ export interface components { */ contributor: Record } + /** Page[ColorizationIntent] */ + Page_ColorizationIntent_: { + /** Items */ + items: (components["schemas"]["ColorizationIntent"])[] + /** Total */ + total: number + /** Page */ + page?: number + /** Size */ + size?: number + /** Pages */ + pages?: number + } /** Page[CommonCoordinateSpaceModel] */ Page_CommonCoordinateSpaceModel_: { /** Items */ @@ -914,10 +981,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[SiibraCorticalProfileModel] */ - Page_SiibraCorticalProfileModel_: { + /** Page[SiibraEbrainsDataFeatureModel] */ + Page_SiibraEbrainsDataFeatureModel_: { /** Items */ - items: (components["schemas"]["SiibraCorticalProfileModel"])[] + items: (components["schemas"]["SiibraEbrainsDataFeatureModel"])[] /** Total */ total: number /** Page */ @@ -927,10 +994,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[SiibraEbrainsDataFeatureModel] */ - Page_SiibraEbrainsDataFeatureModel_: { + /** Page[SiibraParcellationModel] */ + Page_SiibraParcellationModel_: { /** Items */ - items: (components["schemas"]["SiibraEbrainsDataFeatureModel"])[] + items: (components["schemas"]["SiibraParcellationModel"])[] /** Total */ total: number /** Page */ @@ -940,10 +1007,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[SiibraParcellationModel] */ - Page_SiibraParcellationModel_: { + /** Page[SiibraTabularModel] */ + Page_SiibraTabularModel_: { /** Items */ - items: (components["schemas"]["SiibraParcellationModel"])[] + items: (components["schemas"]["SiibraTabularModel"])[] /** Total */ total: number /** Page */ @@ -953,10 +1020,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[SiibraRegionalConnectivityModel] */ - Page_SiibraRegionalConnectivityModel_: { + /** Page[SiibraVoiModel] */ + Page_SiibraVoiModel_: { /** Items */ - items: (components["schemas"]["SiibraRegionalConnectivityModel"])[] + items: (components["schemas"]["SiibraVoiModel"])[] /** Total */ total: number /** Page */ @@ -966,10 +1033,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[SiibraTabularModel] */ - Page_SiibraTabularModel_: { + /** Page[Union[CompoundFeatureModel, SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]] */ + Page_Union_CompoundFeatureModel__SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__: { /** Items */ - items: (components["schemas"]["SiibraTabularModel"])[] + items: (components["schemas"]["CompoundFeatureModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"])[] /** Total */ total: number /** Page */ @@ -979,10 +1046,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[SiibraVoiModel] */ - Page_SiibraVoiModel_: { + /** Page[Union[SiibraCorticalProfileModel, CompoundFeatureModel]] */ + Page_Union_SiibraCorticalProfileModel__CompoundFeatureModel__: { /** Items */ - items: (components["schemas"]["SiibraVoiModel"])[] + items: (components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["CompoundFeatureModel"])[] /** Total */ total: number /** Page */ @@ -992,10 +1059,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[Union[SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]] */ - Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__: { + /** Page[Union[SiibraRegionalConnectivityModel, CompoundFeatureModel]] */ + Page_Union_SiibraRegionalConnectivityModel__CompoundFeatureModel__: { /** Items */ - items: (components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"])[] + items: (components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["CompoundFeatureModel"])[] /** Total */ total: number /** Page */ @@ -1005,10 +1072,10 @@ export interface components { /** Pages */ pages?: number } - /** Page[Union[SiibraVoiModel, SiibraCorticalProfileModel, SiibraRegionalConnectivityModel, SiibraReceptorDensityFp, SiibraTabularModel, SiibraEbrainsDataFeatureModel]] */ - Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__: { + /** Page[Union[SiibraVoiModel, SiibraCorticalProfileModel, CompoundFeatureModel, SiibraRegionalConnectivityModel, SiibraReceptorDensityFp, SiibraTabularModel, SiibraEbrainsDataFeatureModel]] */ + Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__CompoundFeatureModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__: { /** Items */ - items: (components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraEbrainsDataFeatureModel"])[] + items: (components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["CompoundFeatureModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraEbrainsDataFeatureModel"])[] /** Total */ total: number /** Page */ @@ -1136,6 +1203,12 @@ export interface components { /** minValueUnit */ minValueUnit?: Record } + /** RegionMapping */ + RegionMapping: { + region: components["schemas"]["ParcellationEntityVersionModel"] + /** Rgb */ + rgb: (number)[] + } /** * RegionRelationAsmtModel * @description ConfigBaseModel @@ -1410,12 +1483,11 @@ export interface components { anchor?: components["schemas"]["SiibraAnchorModel"] /** Cohort */ cohort: string - /** Subjects */ - subjects: (string)[] - /** Matrices */ - matrices?: { - [key: string]: components["schemas"]["DataFrameModel"] | undefined - } + /** Subject */ + subject: string + /** Feature */ + feature?: string + matrix?: components["schemas"]["DataFrameModel"] } /** * SiibraTabularModel @@ -1469,6 +1541,20 @@ export interface components { /** Max */ max: number } + /** + * SubfeatureModel + * @description ConfigBaseModel + */ + SubfeatureModel: { + /** @Type */ + "@type": string + /** Id */ + id: string + /** Index */ + index: string | components["schemas"]["CoordinatePointModel"] + /** Name */ + name: string + } /** ValidationError */ ValidationError: { /** Location */ @@ -1616,6 +1702,35 @@ export interface operations { } } } + get_single_feature_intents_feature__feature_id__intents_get: { + /** + * Get Single Feature Intents + * @description Get feature intents from feature_id + */ + parameters: { + query?: { + page?: number + size?: number + } + path: { + feature_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Page_ColorizationIntent_"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } get_single_feature_download_feature__feature_id__download_get: { /** * Get Single Feature Download @@ -1841,7 +1956,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__"] + "application/json": components["schemas"]["Page_Union_SiibraVoiModel__SiibraCorticalProfileModel__CompoundFeatureModel__SiibraRegionalConnectivityModel__SiibraReceptorDensityFp__SiibraTabularModel__SiibraEbrainsDataFeatureModel__"] } } /** @description Validation Error */ @@ -2096,6 +2211,56 @@ export interface operations { } } } + get_download_progress_atlas_download__task_id__get: { + /** + * Get Download Progress + * @description Get download task progress with task_id + */ + parameters: { + path: { + task_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + get_download_result_atlas_download__task_id__download_get: { + /** + * Get Download Result + * @description Download the bundle + */ + parameters: { + path: { + task_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } get_all_feature_types_feature__types_get: { /** * Get All Feature Types @@ -2130,6 +2295,7 @@ export interface operations { parameters: { query: { parcellation_id: string + region_id?: string type?: string page?: number size?: number @@ -2139,7 +2305,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["Page_SiibraRegionalConnectivityModel_"] + "application/json": components["schemas"]["Page_Union_SiibraRegionalConnectivityModel__CompoundFeatureModel__"] } } /** @description Validation Error */ @@ -2153,14 +2319,12 @@ export interface operations { get_single_connectivity_feature_feature_RegionalConnectivity__feature_id__get: { /** * Get Single Connectivity Feature - * @description subject is an optional param. - * If provided, the specific matrix will be return. - * If not provided, the matrix averaged between subjects will be returned under the key _average. + * @description Get single connectivity feature */ parameters: { query: { parcellation_id: string - subject?: string + region_id?: string type?: string } path: { @@ -2171,7 +2335,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["SiibraRegionalConnectivityModel"] + "application/json": components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["CompoundFeatureModel"] } } /** @description Validation Error */ @@ -2200,7 +2364,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["Page_SiibraCorticalProfileModel_"] + "application/json": components["schemas"]["Page_Union_SiibraCorticalProfileModel__CompoundFeatureModel__"] } } /** @description Validation Error */ @@ -2230,7 +2394,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["SiibraCorticalProfileModel"] + "application/json": components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["CompoundFeatureModel"] } } /** @description Validation Error */ @@ -2259,7 +2423,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__"] + "application/json": components["schemas"]["Page_Union_CompoundFeatureModel__SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__"] } } /** @description Validation Error */ @@ -2289,7 +2453,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] + "application/json": components["schemas"]["CompoundFeatureModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] } } /** @description Validation Error */ @@ -2308,7 +2472,7 @@ export interface operations { parameters: { query: { space_id: string - bbox?: string + bbox: string type?: string page?: number size?: number @@ -2495,7 +2659,7 @@ export interface operations { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraEbrainsDataFeatureModel"] + "application/json": components["schemas"]["SiibraVoiModel"] | components["schemas"]["SiibraCorticalProfileModel"] | components["schemas"]["CompoundFeatureModel"] | components["schemas"]["SiibraRegionalConnectivityModel"] | components["schemas"]["SiibraReceptorDensityFp"] | components["schemas"]["SiibraTabularModel"] | components["schemas"]["SiibraEbrainsDataFeatureModel"] } } /** @description Validation Error */ diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index e8d44c8db..115feaf3f 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -97,6 +97,16 @@ export type StatisticalMap = { * Features */ +export type SimpleCompoundFeature = { + id: string + name: string + indices: { + id: string + index: string|Point + name: string + }[] +} & AdditionalInfo + export type Feature = { id: string name: string diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index b0e0ad5a4..48be01723 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -1,7 +1,7 @@ import { - SxplrAtlas, SxplrParcellation, SxplrTemplate, SxplrRegion, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, VoiFeature, Point, TThreeMesh, LabelledMap, CorticalFeature, Feature, GenericInfo, BoundingBox + SxplrAtlas, SxplrParcellation, SxplrTemplate, SxplrRegion, NgLayerSpec, NgPrecompMeshSpec, NgSegLayerSpec, VoiFeature, Point, TThreeMesh, LabelledMap, CorticalFeature, Feature, GenericInfo, BoundingBox, SimpleCompoundFeature } from "./sxplrTypes" -import { PathReturn, MetaV1Schema } from "./typeV3" +import { PathReturn, MetaV1Schema, CompoundFeature } from "./typeV3" import { hexToRgb } from 'common/util' import { components } from "./schemaV3" import { defaultdict } from "src/util/fn" @@ -637,10 +637,32 @@ class TranslateV3 { } } - async translateFeature(feat: PathReturn<"/feature/{feature_id}">): Promise { + async translateFeature(feat: PathReturn<"/feature/{feature_id}">): Promise { if (this.#isVoi(feat)) { return await this.translateVoiFeature(feat) } + if (this.#isCompound(feat)) { + const link = feat.datasets.flatMap(ds => ds.urls).map(v => ({ + href: v.url, + text: v.url + })) + const v: SimpleCompoundFeature = { + id: feat.id, + name: feat.name, + indices: await Promise.all( + feat.indices.map( + async ({ id, index, name }) => ({ + id, + index: await this.#transformIndex(index), + name, + }) + ) + ), + desc: feat.description, + link + } + return v + } return await this.translateBaseFeature(feat) } @@ -665,6 +687,18 @@ class TranslateV3 { return feat['@type'].includes("feature/volume_of_interest") } + #isCompound(feat: unknown): feat is CompoundFeature { + return feat['@type'].includes("feature/compoundfeature") + } + + async #transformIndex(index: CompoundFeature['indices'][number]['index']): Promise { + if (typeof index === "string") { + return index + } + return await this.#translatePoint(index) + + } + async translateVoiFeature(feat: PathReturn<"/feature/Image/{feature_id}">): Promise { const [superObj, { loc: center }, { loc: maxpoint }, { loc: minpoint }, { "neuroglancer/precomputed": precomputedVol }] = await Promise.all([ this.translateBaseFeature(feat), diff --git a/src/atlasComponents/sapi/typeV3.ts b/src/atlasComponents/sapi/typeV3.ts index 0ae097aa9..d6cfad208 100644 --- a/src/atlasComponents/sapi/typeV3.ts +++ b/src/atlasComponents/sapi/typeV3.ts @@ -17,7 +17,7 @@ export type SapiFeatureModel = SapiSpatialFeatureModel | PathReturn<"/feature/Ta export type SapiRoute = keyof paths -type SapiRouteExcludePlotlyDownload = Exclude +type SapiRouteExcludePlotlyDownload = Exclude type _FeatureType = FeatureRoute extends `/feature/${infer FT}` ? FT extends "_types" @@ -134,3 +134,5 @@ export function isEnclosed(v: BestViewPoints[number]): v is EnclosedROI { } export type Qualification = components["schemas"]["Qualification"] + +export type CompoundFeature = components['schemas']['CompoundFeatureModel'] diff --git a/src/features/compoundFtContainer/compoundFtContainer.component.ts b/src/features/compoundFtContainer/compoundFtContainer.component.ts new file mode 100644 index 000000000..f1529e6f0 --- /dev/null +++ b/src/features/compoundFtContainer/compoundFtContainer.component.ts @@ -0,0 +1,51 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Store } from "@ngrx/store"; +import { BehaviorSubject } from "rxjs"; +import { SAPI } from "src/atlasComponents/sapi"; +import { SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; +import { userInteraction } from "src/state"; + +@Component({ + templateUrl: './compoundFtContainer.template.html', + styleUrls: [ + './compoundFtContainer.style.css' + ], + selector: 'compound-feature-container', +}) + +export class CompoundFtContainer { + @Input() + compoundFeature: SimpleCompoundFeature + + @Output() + dismiss = new EventEmitter() + + busy$ = new BehaviorSubject(false) + + constructor(private sapi: SAPI, private store: Store, private snackbar: MatSnackBar){ + } + async showSubfeature(id: string){ + try { + this.busy$.next(true) + const feature = await this.sapi.getV3FeatureDetailWithId(id).toPromise() + this.store.dispatch( + userInteraction.actions.showFeature({ feature }) + ) + this.dismiss.emit() + } catch (e) { + console.log('error', e) + this.snackbar.open(`Error: ${e.toString()}`, "Dismiss") + } finally { + this.busy$.next(false) + } + } +} + +/** + * TODO + * + * check http://localhost:10081/v3_0/feature/lq0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-300::r:Area%20hOc1%20(V1,%2017,%20CalcS)%20left::4c05163cac01b560cddf9d0ae2b63c94 + * + * see https://github.com/FZJ-INM1-BDA/siibra-python/issues/509 + */ \ No newline at end of file diff --git a/src/features/compoundFtContainer/compoundFtContainer.style.css b/src/features/compoundFtContainer/compoundFtContainer.style.css new file mode 100644 index 000000000..889029fd9 --- /dev/null +++ b/src/features/compoundFtContainer/compoundFtContainer.style.css @@ -0,0 +1,16 @@ +cdk-virtual-scroll-viewport +{ + display: block; + min-height:30vh; +} + +cdk-virtual-scroll-viewport button +{ + width: 100%; + justify-content: left; +} + +mat-divider +{ + margin: 1rem 0; +} diff --git a/src/features/compoundFtContainer/compoundFtContainer.template.html b/src/features/compoundFtContainer/compoundFtContainer.template.html new file mode 100644 index 000000000..c7c9e1152 --- /dev/null +++ b/src/features/compoundFtContainer/compoundFtContainer.template.html @@ -0,0 +1,19 @@ +
+ Please select a sub feature +
+ + + + + + + + + diff --git a/src/features/compoundFtContainer/index.ts b/src/features/compoundFtContainer/index.ts new file mode 100644 index 000000000..e89292139 --- /dev/null +++ b/src/features/compoundFtContainer/index.ts @@ -0,0 +1,2 @@ +export { CompoundFeatureModule } from "./module" +export { CompoundFtContainer } from "./compoundFtContainer.component" diff --git a/src/features/compoundFtContainer/indexToText.pipe.ts b/src/features/compoundFtContainer/indexToText.pipe.ts new file mode 100644 index 000000000..79fd38155 --- /dev/null +++ b/src/features/compoundFtContainer/indexToText.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; + +@Pipe({ + name: 'indexToStr', + pure: true +}) +export class IndexToStrPipe implements PipeTransform{ + public transform(value: SimpleCompoundFeature['indices'][number]['index']): string { + if (typeof value === "string") { + return value + } + return `Point(${value.loc.join(", ")})` + } +} diff --git a/src/features/compoundFtContainer/module.ts b/src/features/compoundFtContainer/module.ts new file mode 100644 index 000000000..f09d1d00d --- /dev/null +++ b/src/features/compoundFtContainer/module.ts @@ -0,0 +1,20 @@ +import { NgModule } from "@angular/core"; +import { CompoundFtContainer } from "./compoundFtContainer.component"; +import { AngularMaterialModule } from "src/sharedModules"; +import { CommonModule } from "@angular/common"; +import { IndexToStrPipe } from "./indexToText.pipe"; + +@NgModule({ + imports: [ + AngularMaterialModule, + CommonModule, + ], + declarations: [ + CompoundFtContainer, + IndexToStrPipe, + ], + exports: [ + CompoundFtContainer + ] +}) +export class CompoundFeatureModule{} diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts deleted file mode 100644 index 247cc5a9e..000000000 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import {ConnectivityBrowserComponent} from "./connectivityBrowser.component"; -import {ComponentFixture, TestBed} from "@angular/core/testing"; -import {Action} from "@ngrx/store"; -import {CUSTOM_ELEMENTS_SCHEMA, Directive, Input} from "@angular/core"; -import {provideMockActions} from "@ngrx/effects/testing"; -import {MockStore, provideMockStore} from "@ngrx/store/testing"; -import {Observable, of} from "rxjs"; -import {SAPI} from "src/atlasComponents/sapi"; -import {AngularMaterialModule} from "src/sharedModules"; -import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; -import { SxplrAtlas, SxplrParcellation } from "src/atlasComponents/sapi/sxplrTypes"; -import { UtilModule } from "src/util"; - -/** - * injecting databrowser module is bad idea - * since it relies on its own selectors - * since the only reason why data browser is imported is to use show dataset dialogue - * just use a dummy directive - */ -const MOCK_BS_ENDPOINT = `http://localhost:1234` - -@Directive({ - selector: '[iav-dataset-show-dataset-dialog]' -}) - -class DummyDirective{ - @Input('iav-dataset-show-dataset-dialog-name') - name: string - @Input('iav-dataset-show-dataset-dialog-description') - description: string - @Input('iav-dataset-show-dataset-dialog-kgid') - kgId: string - @Input('iav-dataset-show-dataset-dialog-kgschema') - kgSchema: string -} - -describe('ConnectivityComponent', () => { - - let component: ConnectivityBrowserComponent; - let fixture: ComponentFixture; - const actions$: Observable = of({type: 'TEST'}) - let httpTestingController: HttpTestingController; - let req - - const types: any[] = [{ - name: 'StreamlineCounts', - types: ['siibra/features/connectivity/streamlineCounts'] - },{ - name: 'StreamlineLengths', - types: ['siibra/features/connectivity/streamlineLengths'] - },{ - name: 'FunctionalConnectivity', - types: ['siibra/features/connectivity/functional'] - }] - - let datasetList: SxplrParcellation[] = [ - { - id: 'id1', - name: 'id1', - cohort: 'HCP', - subject: '100', - '@type': 'siibra/features/connectivity/streamlineCounts', - } as any, { - id: 'id2', - name: 'id2', - cohort: '1000BRAINS', - subject: 'average', - } as any - ] - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - AngularMaterialModule, - UtilModule, - ], - providers: [ - provideMockActions(() => actions$), - provideMockStore(), - { - provide: SAPI, - useValue: { - atlases$: of([]), - getParcellation: jasmine.createSpy('getParcellation'), // getFeatureInstance(instanceId: string): Observable - getSpaceDetail: jasmine.createSpy('getSpaceDetail'), - getParcDetail: jasmine.createSpy('getParcDetail'), - getParcRegions: jasmine.createSpy('getParcRegions'), - } - } - ], - declarations: [ - ConnectivityBrowserComponent, - DummyDirective, - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA, - ], - }).compileComponents() - httpTestingController = TestBed.inject(HttpTestingController); - }); - - it('> component can be created', async () => { - fixture = TestBed.createComponent(ConnectivityBrowserComponent) - component = fixture.componentInstance - expect(component).toBeTruthy() - }) - - describe('> Select modality', async () => { - beforeEach(async () => { - }) - - it('> Get request should be called', () => { - }) - - it('> Datasets are set correctly', () => { - }) - - // it('> Cohorts are set correctly', () => { - // expect(datasetList.map(d => d.cohort)).toEqual(component.cohorts) - // }) - }) - -}); diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts deleted file mode 100644 index c82856be5..000000000 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { Component, ElementRef, ViewChild, Input, SimpleChanges, HostListener, OnChanges, inject } from "@angular/core"; -import { Store, select} from "@ngrx/store"; -import { BehaviorSubject, combineLatest, merge, concat, NEVER} from "rxjs"; -import { switchMap, map, shareReplay, distinctUntilChanged, withLatestFrom, filter, finalize, debounceTime, takeUntil } from "rxjs/operators"; -import { atlasAppearance, atlasSelection } from "src/state"; -import { SAPI } from "src/atlasComponents/sapi/sapi.service"; -import { of } from "rxjs"; -import { SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; -import { actions } from "src/state/atlasSelection"; -import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"; -import { DS } from "src/features/filterCategories.pipe"; -import { FormControl, FormGroup } from "@angular/forms"; -import { PathReturn } from "src/atlasComponents/sapi/typeV3"; -import { arrayEqual } from "src/util/array"; -import { switchMapWaitFor } from "src/util/fn"; -import { DestroyDirective } from "src/util/directives/destroy.directive"; - -type PathParam = DS['value'][number] -type ConnFeat = PathReturn<"/feature/RegionalConnectivity/{feature_id}"> - -@Component({ - selector: 'sxplr-features-connectivity-browser', - templateUrl: './connectivityBrowser.template.html', - styleUrls: ['./connectivityBrowser.style.scss'], - hostDirectives: [ - DestroyDirective - ] -}) -export class ConnectivityBrowserComponent implements OnChanges { - - #destroy$ = inject(DestroyDirective).destroyed$ - - @Input('sxplr-features-connectivity-browser-atlas') - atlas: SxplrAtlas - - @Input('sxplr-features-connectivity-browser-template') - template: SxplrTemplate - - @Input('sxplr-features-connectivity-browser-parcellation') - parcellation: SxplrParcellation - - parcellation$ = new BehaviorSubject(null) - - #accordionExpanded$ = new BehaviorSubject(null) - @Input() - set accordionExpanded(flag: boolean) { - this.#accordionExpanded$.next(flag) - } - - region$ = new BehaviorSubject(null) - @Input() - set region(region: SxplrRegion) { - this.region$.next(region) - } - - types$ = new BehaviorSubject(null) - @Input() - types: PathParam[] - - connectivityFilterForm = new FormGroup({ - selectedType: new FormControl(null), - selectedView: new FormControl<'average'|'subject'>('subject'), - selectedCohort: new FormControl(null), - selectedDatasetIndex: new FormControl(0), - selectedSubjectIndex: new FormControl(0), - }) - - displayForm = new FormGroup({ - logChecked: new FormControl(false) - }) - - formValue$ = this.connectivityFilterForm.valueChanges.pipe( - debounceTime(160), - shareReplay(1), - ) - - static LayerId = 'connectivity-colormap-id' - - @ViewChild('connectivityComponent') public connectivityComponentElement: ElementRef - @ViewChild('fullConnectivityGrid') public fullConnectivityGridElement: ElementRef - - constructor( - private store$: Store, - protected sapi: SAPI - ) { - /** - * on accordion expansion, if nothing is selected, select default (0) type - */ - combineLatest([ - this.#accordionExpanded$, - this.types$, - concat( - of(null as PathParam), - this.formValue$.pipe( - map(v => v.selectedType), - distinctUntilChanged((n, o) => n.name === o.name) - ) - ), - ]).pipe( - takeUntil(this.#destroy$), - ).subscribe(([flag, types, selectedType]) => { - if (flag && !selectedType) { - this.connectivityFilterForm.patchValue({ - selectedType: types[0] - }) - } - }) - - /** - * on set log - */ - this.displayForm.valueChanges.pipe( - map(v => v.logChecked), - switchMap(switchMapWaitFor({ - condition: () => !!this.connectivityComponentElement, - leading: true - })), - takeUntil(this.#destroy$) - ).subscribe(flag => { - const el = this.connectivityComponentElement - el.nativeElement.setShowLog(flag) - }) - - /** - * on type selection, select first cohort - */ - this.formValue$.pipe( - map(v => v.selectedType), - distinctUntilChanged((n, o) => n.name === o.name), - switchMap(() => - this.cohorts$.pipe( - /** - * it's important to not use distinctUntilChanged - * new corhots emit should always trigger this flow - */ - ) - ), - takeUntil(this.#destroy$) - ).subscribe(cohorts => { - if (cohorts.length > 0) { - this.connectivityFilterForm.patchValue({ - selectedCohort: cohorts[0] - }) - } - }) - - /** - * on select cohort - */ - this.selectedCohort$.pipe( - switchMap(() => this.cohortDatasets$.pipe( - map(dss => dss.length), - distinctUntilChanged(), - filter(length => length > 0), - )), - takeUntil(this.#destroy$) - ).subscribe(() => { - this.connectivityFilterForm.patchValue({ - selectedDatasetIndex: 0, - selectedSubjectIndex: 0, - }) - }) - - /** - * on update colormap, add new custom layer - */ - combineLatest([ - this.#accordionExpanded$, - this.colormap$, - ]).pipe( - withLatestFrom( - this.store$.pipe( - select(atlasSelection.selectors.selectedParcAllRegions) - ) - ), - takeUntil(this.#destroy$) - ).subscribe(([[accordionExpanded, conn], allregions]) => { - if (!accordionExpanded || !conn) { - return - } - - const map = new Map() - for (const region of allregions) { - const area = conn.find(a => a.name === region.name) - if (area) { - map.set(region, Object.values(area.color)) - } else { - map.set(region, [255,255,255,0.1]) - } - } - - this.store$.dispatch( - atlasAppearance.actions.addCustomLayer({ - customLayer: { - clType: 'customlayer/colormap', - id: ConnectivityBrowserComponent.LayerId, - colormap: map - } - }) - ) - }) - - /** - * on - * - accordion update - * - colormap change - * - fetching matrix flag is true - * remove custom layer - */ - merge( - this.#accordionExpanded$.pipe( - filter(expanded => !expanded), - ), - this.#fetchingMatrix$.pipe( - filter(flag => !!flag), - ), - ).pipe( - takeUntil(this.#destroy$), - ).subscribe(() => { - this.removeCustomLayer() - }) - - /** - * on pure connection update, update logchecked box - */ - this.#pureConnections$.pipe( - takeUntil(this.#destroy$) - ).subscribe(v => { - if (!v) return - for (const val of Object.values(v)) { - if (val > 1) { - this.displayForm.get("logChecked").enable() - return - } - } - this.displayForm.get("logChecked").patchValue(false) - this.displayForm.get("logChecked").disable() - }) - - this.selectedDataset$.pipe( - takeUntil(this.#destroy$) - ).subscribe(selectedDs => { - this.selectedDataset = selectedDs - }) - - this.#destroy$.subscribe({ - complete: () => { - this.removeCustomLayer() - } - }) - } - - selectedDataset: PathReturn<"/feature/RegionalConnectivity/{feature_id}"> - - public ngOnChanges(changes: SimpleChanges): void { - const { parcellation, types } = changes - if (parcellation) { - this.parcellation$.next(parcellation.currentValue) - } - if (types) [ - this.types$.next(types.currentValue) - ] - } - - removeCustomLayer() { - this.store$.dispatch( - atlasAppearance.actions.removeCustomLayer({ - id: ConnectivityBrowserComponent.LayerId - }) - ) - } - - busy$ = new BehaviorSubject(false) - - #selectedType$ = this.formValue$.pipe( - map(v => v.selectedType), - distinctUntilChanged((o, n) => o?.name === n?.name), - shareReplay(1), - ) - - #connFeatures$ = this.parcellation$.pipe( - switchMap(parc => concat( - of(null as PathParam), - this.#selectedType$, - ).pipe( - switchMap(selectedType => { - if (!selectedType || !parc) { - return of([] as ConnFeat[]) - } - - const typedName = getType(selectedType.name) - const query = { - parcellation_id: parc.id, - type: typedName - } - this.busy$.next(true) - return concat( - of( - [] as PathReturn<"/feature/RegionalConnectivity/{feature_id}">[], - ), - this.sapi.v3Get( - "/feature/RegionalConnectivity", - { query } - ).pipe( - switchMap(resp => - this.sapi.iteratePages( - resp, - page => this.sapi.v3Get( - "/feature/RegionalConnectivity", - { query: { ...query, page } } - ) - ) - ), - finalize(() => { - this.busy$.next(false) - }) - ) - ) - }) - )), - ) - - cohorts$ = this.#connFeatures$.pipe( - map(v => { - const cohorts: string[] = [] - for (const item of v) { - if (!cohorts.includes(item.cohort)) { - cohorts.push(item.cohort) - } - } - return cohorts - }) - ) - - selectedCohort$ = this.formValue$.pipe( - map(v => v.selectedCohort), - distinctUntilChanged() - ) - - cohortDatasets$ = combineLatest([ - this.#connFeatures$, - this.formValue$.pipe( - map(v => v.selectedCohort), - distinctUntilChanged() - ), - ]).pipe( - map(([ features, selectedCohort ]) => features.filter(f => f.cohort === selectedCohort)), - distinctUntilChanged( - arrayEqual((o, n) => o?.id === n?.id) - ), - shareReplay(1), - ) - - selectedDataset$ = this.cohortDatasets$.pipe( - switchMap(features => this.formValue$.pipe( - map(v => v.selectedDatasetIndex), - distinctUntilChanged(), - map(dsIdx => features[dsIdx]), - shareReplay(1), - )), - ) - - selectedDatasetAdditionalInfos$ = this.selectedDataset$.pipe( - map(ds => ds ? ds.datasets : []) - ) - - #fetchingMatrix$ = new BehaviorSubject(false) - - #matrixInput$ = combineLatest([ - this.parcellation$, - this.formValue$, - this.cohortDatasets$, - ]).pipe( - map(([ parcellation, form, dss ]) => { - const { - selectedDatasetIndex: dsIdx, - selectedSubjectIndex: subIdx, - selectedView - } = form - const ds = dss[dsIdx] - if (!ds) { - return null - } - - const subject = ds.subjects[subIdx] - if (!subject) { - return null - } - return { - parcellation, - feature_id: ds.id, - subject, - selectedView - } - }), - distinctUntilChanged((o, n) => o?.feature_id === n?.feature_id && o?.subject === n?.subject && o?.selectedView === n?.selectedView && o?.parcellation?.id === n?.parcellation?.id), - shareReplay(1), - ) - - #selectedMatrix$ = this.#matrixInput$.pipe( - switchMap(input => { - if (!input) { - return NEVER - } - const { parcellation, feature_id, subject } = input - - this.#fetchingMatrix$.next(true) - return this.sapi.v3Get( - "/feature/RegionalConnectivity/{feature_id}", - { - query: { - parcellation_id: parcellation.id, - ...(input.selectedView === "average" - ? {} - : { subject }) - }, - path: { - feature_id - } - } - ).pipe( - finalize(() => { - this.#fetchingMatrix$.next(false) - }) - ) - }), - shareReplay(1), - ) - - #pureConnections$ = this.#matrixInput$.pipe( - switchMap(matrixInput => - this.#selectedMatrix$.pipe( - withLatestFrom(this.region$), - map(([ v, region ]) => { - const matrixKey = matrixInput?.selectedView === "average" ? "_average" : matrixInput?.subject - if (!v || !matrixInput || !v.matrices?.[matrixKey]) { - return null - } - const b = v.matrices[matrixKey] - const foundIdx = b.columns.findIndex(v => v['name'] === region.name) - if (typeof foundIdx !== 'number') { - return null - } - const profile = b.data[foundIdx] - if (!profile) { - return null - } - const rObj: Record = {} - b.columns.reduce((acc, curr, idx) => { - const rName = curr['name'] as string - acc[rName] = profile[idx] as number - return acc - }, rObj) - return rObj - }) - ), - ), - ) - - colormap$ = this.#matrixInput$.pipe( - switchMap(() => concat( - of(null as ConnectedArea[]), - combineLatest([ - this.#pureConnections$, - this.displayForm.valueChanges.pipe( - map(v => v.logChecked), - distinctUntilChanged() - ) - ]).pipe( - filter(conn => !!conn), - map(([ conn, flag ]) => processProfile(conn, flag)) - ) - )) - ) - - view$ = combineLatest([ - this.busy$, - this.selectedDataset$, - this.formValue$, - this.#fetchingMatrix$, - concat( - of(null as Record), - this.#pureConnections$, - ), - this.region$, - ]).pipe( - map(([busy, sDs, form, fetchingMatrix, pureConnections, region]) => { - return { - showSubject: sDs && form.selectedView === "subject", - numSubjects: sDs?.subjects.length, - connections: pureConnections, - region, - showAverageToggle: form.selectedCohort !== null && typeof form.selectedCohort !== "undefined", - busy: busy || fetchingMatrix, - selectedSubject: (sDs?.subjects || [])[form.selectedSubjectIndex], - selectedDataset: form?.selectedDatasetIndex - } - }), - shareReplay(1), - ) - - @HostListener('connectedRegionClicked', ['$event']) - onRegionClicked(event: CustomEvent) { - const regionName = event.detail.name as string - this.sapi.v3Get("/regions/{region_id}", { - path: {region_id: regionName}, - query: { - parcellation_id: this.parcellation.id, - space_id: this.template.id - } - }).pipe( - switchMap(r => translateV3Entities.translateRegion(r)) - ).subscribe(region => { - const centroid = region.centroid?.loc - if (centroid) { - this.store$.dispatch( - actions.navigateTo({ - navigation: { - position: centroid.map(v => v*1e6), - }, - animation: true - }) - ) - } - }) - } - - exportConnectivityProfile() { - const a = document.querySelector('hbp-connectivity-matrix-row'); - (a as any).downloadCSV() - } - - public exportFullConnectivity() { - this.fullConnectivityGridElement?.nativeElement['downloadCSV']() - } -} - -function clamp(min: number, max: number) { - return function(val: number) { - return Math.max(min, Math.min(max, val)) - } -} -const clamp01 = clamp(0, 1) -function interpolate255(val: number) { - return Math.round(clamp01(val) * 255) -} -function jet(val: number) { - return { - r: val < 0.7 - ? interpolate255(4 * val - 1.5) - : interpolate255(-4.0 * val + 4.5), - g: val < 0.5 - ? interpolate255(4.0 * val - 0.5) - : interpolate255(-4.0 * val + 3.5), - b: val < 0.3 - ? interpolate255(4.0 * val + 0.5) - : interpolate255(-4.0 * val + 2.5) - } -} - -function processProfile(areas: Record, logFlag=false): ConnectedArea[] { - const returnValue: Omit[] = [] - for (const areaname in areas) { - returnValue.push({ - name: areaname, - numberOfConnections: areas[areaname], - }) - } - returnValue.sort((a, b) => b.numberOfConnections - a.numberOfConnections) - if (returnValue.length === 0) { - return [] - } - const preprocess = (v: number) => logFlag ? Math.log10(v) : v - return returnValue.map(v => ({ - ...v, - color: jet( - preprocess(v.numberOfConnections) / preprocess(returnValue[0].numberOfConnections) - ) - })) -} - -function getType(name: string) { - return name.split(".").slice(-1)[0] -} - -type ConnectedArea = { - color: {r: number, g: number, b: number} - name: string - numberOfConnections: number -} diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss b/src/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss deleted file mode 100644 index 577f01f65..000000000 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.style.scss +++ /dev/null @@ -1,3 +0,0 @@ -::ng-deep label { - margin-bottom: 0 !important; -} \ No newline at end of file diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html deleted file mode 100644 index 7fc4c67ab..000000000 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ /dev/null @@ -1,162 +0,0 @@ -
-
- - - -
- - - Modality - - - - {{ type.display_name }} - - - - - - - - - - Cohort - - - - {{ cohort }} - - - - - - - - - Average - - - Subject - - - - - - - -
-
- - - -
-
- - Dataset: {{ view$ | async | getProperty : 'selectedDataset' }} - - - - -
-
-
-
- -
-
- - Subject: {{ view$ | async | getProperty : 'selectedSubject' }} - - - - -
-
- -
- - - - - - - - - - - - No connectivity Found - - - -
-
- - Log 10 - -
- - - - -
- - - -
-
-
-
- - - - - - - - -
- -
-
- - - - diff --git a/src/features/connectivity/index.ts b/src/features/connectivity/index.ts deleted file mode 100644 index b86944877..000000000 --- a/src/features/connectivity/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; -export { SapiViewsFeatureConnectivityModule } from "./module"; diff --git a/src/features/connectivity/module.ts b/src/features/connectivity/module.ts deleted file mode 100644 index 7ae3e1ae4..000000000 --- a/src/features/connectivity/module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CommonModule } from "@angular/common"; -import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from "@angular/core"; -import { SAPI } from "src/atlasComponents/sapi"; -import { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; -import { ReactiveFormsModule } from "@angular/forms"; -import { DialogModule } from "src/ui/dialogInfo"; -import { UtilModule } from "src/util"; -import { AngularMaterialModule } from "src/sharedModules"; - - -@NgModule({ - imports: [ - CommonModule, - ReactiveFormsModule, - AngularMaterialModule, - DialogModule, - UtilModule, - ], - declarations: [ - ConnectivityBrowserComponent, - ], - exports: [ - ConnectivityBrowserComponent, - ], - providers: [ - SAPI, - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA, - ], -}) - -export class SapiViewsFeatureConnectivityModule{} diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index c6e93a252..d37a03949 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChildren } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChild, ViewChildren } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { IDS, SAPI } from 'src/atlasComponents/sapi'; @@ -65,6 +65,9 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest @ViewChildren(CategoryAccDirective) catAccDirs: QueryList + @ViewChild('compoundFtTmpl', { read: TemplateRef }) + compoundFtTmpl: TemplateRef + constructor(private sapi: SAPI, private store: Store, private dialog: MatDialog, private cdr: ChangeDetectorRef) { super() } @@ -177,20 +180,6 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest select(atlasSelection.selectors.selectedAtlas) ) - public showConnectivity$ = combineLatest([ - this.selectedAtlas$.pipe( - map(atlas => WHITELIST_CONNECTIVITY.SPECIES.includes(atlas?.species) && !BANLIST_CONNECTIVITY.SPECIES.includes(atlas?.species)) - ), - this.TPRBbox$.pipe( - map(({ parcellation, template }) => ( - WHITELIST_CONNECTIVITY.SPACE.includes(template?.id) && !BANLIST_CONNECTIVITY.SPACE.includes(template?.id) - ) || ( - WHITELIST_CONNECTIVITY.PARCELLATION.includes(parcellation?.id) && !BANLIST_CONNECTIVITY.PARCELLATION.includes(parcellation?.id) - )) - ) - ]).pipe( - map(flags => flags.every(f => f)) - ) private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( switchMap(resp => @@ -205,18 +194,27 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest ) public cateogryCollections$ = this.TPRBbox$.pipe( - switchMap(({ template, parcellation, region }) => this.featureTypes$.pipe( + switchMap(({ template, parcellation, region, bbox }) => this.featureTypes$.pipe( map(features => { const filteredFeatures = features.filter(v => { - const params = [ - ...(v.path_params || []), - ...(v.query_params || []), + const { path_params, required_query_params } = v + + const requiredParams = [ + ...(path_params || []), + ...(required_query_params || []), ] - return [ - params.includes("space_id") === (!!template) && !!template, - params.includes("parcellation_id") === (!!parcellation) && !!parcellation, - params.includes("region_id") === (!!region) && !!region, - ].some(val => val) + const paramMapped = { + space_id: !!template, + parcellation_id: !!parcellation, + region_id: !!region, + bbox: !!bbox + } + for (const pParam in paramMapped){ + if (requiredParams.includes(pParam) && !paramMapped[pParam]) { + return false + } + } + return true }) return categoryAcc(filteredFeatures) }), @@ -224,6 +222,12 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest ) onClickFeature(feature: Feature) { + if (feature.id.startsWith("cf0::")) { + const ref = this.dialog.open(this.compoundFtTmpl, { + data: { feature, dismiss: () => ref.close() } + }) + return + } this.store.dispatch( userInteraction.actions.showFeature({ feature diff --git a/src/features/entry/entry.flattened.component.html b/src/features/entry/entry.flattened.component.html index 7ae6abda7..3fa5f5214 100644 --- a/src/features/entry/entry.flattened.component.html +++ b/src/features/entry/entry.flattened.component.html @@ -1,41 +1,13 @@ - + - - - - - - - - - - {{ conn.key }} - - - - - - - - - - @@ -213,3 +185,29 @@

+ + +

{{ data.feature.name }}

+ +
+ {{ data.feature.desc }} +
+ + + + + +
+ + +
diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index ba6567cf1..0150d953b 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -109,7 +109,9 @@ - + diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index c6950bbbf..987e5ba2c 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -1,30 +1,54 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges } from '@angular/core'; -import { BehaviorSubject, Observable, Subject, combineLatest, concat, of } from 'rxjs'; -import { catchError, distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators'; +import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges, OnDestroy } from '@angular/core'; +import { BehaviorSubject, EMPTY, Observable, Subject, combineLatest, concat, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; import { Feature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; import { DARKTHEME } from 'src/util/injectionTokens'; import { isVoiData, notQuiteRight } from "../guards" +import { Store, select } from '@ngrx/store'; +import { atlasAppearance, atlasSelection } from 'src/state'; +const CONNECTIVITY_LAYER_ID = "connectivity-colormap-id" + @Component({ selector: 'sxplr-feature-view', templateUrl: './feature-view.component.html', styleUrls: ['./feature-view.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class FeatureViewComponent implements OnChanges { +export class FeatureViewComponent implements OnChanges, OnDestroy { + + #cleanupCb: (() => void)[] = [] @Input() feature: Feature #featureId = new BehaviorSubject(null) + #isConnectivity$ = new BehaviorSubject(false) + + #selectedRegion$ = this.store.pipe( + select(atlasSelection.selectors.selectedRegions) + ) + + #allRegions$ = this.store.pipe( + select(atlasSelection.selectors.selectedParcAllRegions) + ) + + #additionalParams$: Observable> = this.#isConnectivity$.pipe( + withLatestFrom(this.#selectedRegion$), + map(([ isConnnectivity, selectedRegions ]) => isConnnectivity + ? {"regions": selectedRegions.map(r => r.name).join(" ")} + : {} ) + ) #plotlyInput$ = combineLatest([ this.#featureId, - this.darktheme$ + this.darktheme$, + this.#additionalParams$, ]).pipe( - map(([ id, darktheme ]) => ({ id, darktheme })), + debounceTime(16), + map(([ id, darktheme, additionalParams ]) => ({ id, darktheme, additionalParams })), distinctUntilChanged((o, n) => o.id === n.id && o.darktheme === n.darktheme), shareReplay(1), ) @@ -40,11 +64,20 @@ export class FeatureViewComponent implements OnChanges { ) plotly$ = this.#plotlyInput$.pipe( - switchMap(({ id, darktheme }) => !!id - ? this.sapi.getFeaturePlot(id, { template: darktheme ? 'plotly_dark' : 'plotly_white' }).pipe( + switchMap(({ id, darktheme, additionalParams }) => { + if (!id) { + return of(null) + } + return this.sapi.getFeaturePlot( + id, + { + template: darktheme ? 'plotly_dark' : 'plotly_white', + ...additionalParams + } + ).pipe( catchError(() => of(null)) ) - : of(null)), + }), shareReplay(1), ) @@ -72,16 +105,83 @@ export class FeatureViewComponent implements OnChanges { constructor( private sapi: SAPI, + private store: Store, @Inject(DARKTHEME) public darktheme$: Observable, - ) { } + ) { + const sub = this.#isConnectivity$.pipe( + withLatestFrom(this.#featureId, this.#selectedRegion$, this.#allRegions$), + switchMap(([flag, fid, selelectedRegion, allRegions]) => { + if (!flag) { + return EMPTY + } + return this.sapi.getFeatureIntents(fid, { + region: selelectedRegion.map(r => r.name).join(" ") + }).pipe( + map(pagedIntents => { + const foundCm = pagedIntents.items.find(intent => intent['@type'] === "siibra-0.4/intent/colorization") + if (!foundCm) { + return null + } + const { region_mappings: regionMappings } = foundCm + const regRgbTuple = regionMappings + .map(({ region, rgb }) => { + const foundRegion = allRegions.find(r => r.name === region.name) + if (!foundRegion) { + return null + } + return [foundRegion, rgb] as const + }) + .filter(v => !!v) + + const newMap = new Map(regRgbTuple) + return newMap + }), + ) + }), + ).subscribe(newCM => { + if (!newCM) { + this.store.dispatch( + atlasAppearance.actions.removeCustomLayer({ + id: CONNECTIVITY_LAYER_ID + }) + ) + return + } + + this.store.dispatch( + atlasAppearance.actions.addCustomLayer({ + customLayer: { + clType: 'customlayer/colormap', + id: CONNECTIVITY_LAYER_ID, + colormap: newCM + } + }) + ) + }) + + this.#cleanupCb.push(() => sub.unsubscribe()) + } + + ngOnDestroy(): void { + while (this.#cleanupCb.length > 0) { + this.#cleanupCb.pop()() + } + this.store.dispatch( + atlasAppearance.actions.removeCustomLayer({ + id: CONNECTIVITY_LAYER_ID + }) + ) + } ngOnChanges(): void { - this.voi$.next(null) this.busy$.next(true) this.#featureId.next(this.feature.id) + // TODO might actually not be right for bold + this.#isConnectivity$.next(this.feature.category === "connectivity") + this.sapi.getV3FeatureDetailWithId(this.feature.id).subscribe( val => { this.busy$.next(false) @@ -100,4 +200,14 @@ export class FeatureViewComponent implements OnChanges { () => this.busy$.next(false) ) } + + navigateToRegionByName(regionName: string){ + this.store.dispatch( + atlasSelection.actions.navigateToRegion({ + region: { + name: regionName + } + }) + ) + } } diff --git a/src/features/guards.ts b/src/features/guards.ts index 480e10dbe..57f1c0d3d 100644 --- a/src/features/guards.ts +++ b/src/features/guards.ts @@ -1,6 +1,5 @@ import { VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" - export function isVoiData(feature: unknown): feature is VoiFeature { return !!feature['bbox'] } diff --git a/src/features/list/list.directive.ts b/src/features/list/list.directive.ts index 11f5c9031..999000e36 100644 --- a/src/features/list/list.directive.ts +++ b/src/features/list/list.directive.ts @@ -16,7 +16,7 @@ export type TranslatedFeature = Awaited< ReturnType<(typeof translateV3Entities) selector: '[sxplr-feature-list-directive]', exportAs: 'featureListDirective' }) -export class ListDirective extends FeatureBase implements OnDestroy{ +export class ListDirective extends FeatureBase implements OnDestroy{ @Input() name: string diff --git a/src/features/module.ts b/src/features/module.ts index 43f0832a7..eae389719 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -5,7 +5,7 @@ import { UtilModule } from "src/util"; import { EntryComponent } from './entry/entry.component' import { FeatureNamePipe } from "./featureName.pipe"; import { CategoryAccDirective } from './category-acc.directive'; -import { SapiViewsFeatureConnectivityModule } from "./connectivity"; +import { CompoundFeatureModule } from "./compoundFtContainer"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { MarkdownModule } from "src/components/markdown"; import { FeatureViewComponent } from "./feature-view/feature-view.component"; @@ -25,7 +25,7 @@ import { AngularMaterialModule } from "src/sharedModules"; CommonModule, SpinnerModule, UtilModule, - SapiViewsFeatureConnectivityModule, + CompoundFeatureModule, ScrollingModule, MarkdownModule, NgLayerCtlModule, diff --git a/src/features/plotly/plot/plot.component.scss b/src/features/plotly/plot/plot.component.scss index 6daad6f17..e352d500d 100644 --- a/src/features/plotly/plot/plot.component.scss +++ b/src/features/plotly/plot/plot.component.scss @@ -1,6 +1,4 @@ .plotly-root { width:100%; - max-height:600px; - overflow: hidden; } diff --git a/src/features/plotly/plot/plot.component.ts b/src/features/plotly/plot/plot.component.ts index 7aaf1322c..7a48153e9 100644 --- a/src/features/plotly/plot/plot.component.ts +++ b/src/features/plotly/plot/plot.component.ts @@ -1,5 +1,21 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, ElementRef, Input, NgZone, OnChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnChanges, Output } from '@angular/core'; + +type PlotlyEvent = "plotly_click" + +type PlotlyPoint = { + label: string +} + +type PlotlyEv = { + event: MouseEvent + points: PlotlyPoint[] +} + + +type PlotlyElement = { + on: (eventName: PlotlyEvent, callback: (ev: PlotlyEv) => void) => void +} @Component({ selector: 'sxplr-plotly-component', @@ -16,15 +32,29 @@ export class PlotComponent implements OnChanges { @Input("plotly-json") plotlyJson: any + @Output("plotly-label-clicked") + labelClicked = new EventEmitter() + + bindOnClick(el: PlotlyElement) { + el.on("plotly_click", ev => { + const { points } = ev + for (const pt of points){ + this.labelClicked.emit(pt.label) + } + }) + } + plotlyRef: any - constructor(private el: ElementRef, private zone: NgZone) { } + constructor(private el: ElementRef) { } ngOnChanges(): void { if (!this.plotlyJson) return const rootEl = (this.el.nativeElement as HTMLElement).querySelector(".plotly-root") const { data, layout } = this.plotlyJson - this.plotlyRef = window['Plotly'].newPlot(rootEl, data, layout, { responsive: true }) + this.plotlyRef = window['Plotly'].newPlot(rootEl, data, layout, { responsive: true }); + + this.bindOnClick(rootEl as any) } } diff --git a/src/index.html b/src/index.html index 00ed70280..695145da0 100644 --- a/src/index.html +++ b/src/index.html @@ -14,7 +14,6 @@ - Siibra Explorer diff --git a/src/routerModule/routeStateTransform.service.spec.ts b/src/routerModule/routeStateTransform.service.spec.ts index 829e4823b..7f5e4159e 100644 --- a/src/routerModule/routeStateTransform.service.spec.ts +++ b/src/routerModule/routeStateTransform.service.spec.ts @@ -3,7 +3,7 @@ import { of } from "rxjs" import { SAPI } from "src/atlasComponents/sapi" import { RouteStateTransformSvc } from "./routeStateTransform.service" import { DefaultUrlSerializer } from "@angular/router" -import { atlasSelection, userInteraction } from "src/state" +import { atlasAppearance, atlasSelection, userInteraction, userInterface } from "src/state" import { QuickHash } from "src/util/fn" import { NEHUBA_CONFIG_SERVICE_TOKEN } from "src/viewerModule/nehuba/config.service" import { MockStore, provideMockStore } from "@ngrx/store/testing" @@ -135,6 +135,10 @@ describe("> routeStateTransform.service.ts", () => { store.overrideSelector(atlasSelection.selectors.navigation, navigation as any) store.overrideSelector(userInteraction.selectors.selectedFeature, null) + store.overrideSelector(userInterface.selectors.panelMode, "FOUR_PANEL") + store.overrideSelector(userInterface.selectors.panelOrder, "0123") + store.overrideSelector(atlasAppearance.selectors.octantRemoval, false) + store.overrideSelector(atlasAppearance.selectors.showDelineation, true) }) diff --git a/src/state/atlasSelection/actions.ts b/src/state/atlasSelection/actions.ts index 86bff9616..d8f0d01d1 100644 --- a/src/state/atlasSelection/actions.ts +++ b/src/state/atlasSelection/actions.ts @@ -156,7 +156,7 @@ export const navigateTo = createAction( export const navigateToRegion = createAction( `${nameSpace} navigateToRegion`, props<{ - region: SxplrRegion + region: Pick }>() ) diff --git a/src/state/userInteraction/store.ts b/src/state/userInteraction/store.ts index 7c5622493..c5fd9dbfa 100644 --- a/src/state/userInteraction/store.ts +++ b/src/state/userInteraction/store.ts @@ -30,6 +30,13 @@ export const reducer = createReducer( on( actions.showFeature, (state, { feature }) => { + /** + * do not process compound feature + * allow component to deal with with dialogbox + */ + if (feature.id.startsWith("cf0::")) { + return { ...state } + } return { ...state, selectedFeature: feature From 4aef49b13cc2ae1ddeb74092c8f9e4567a03b9a0 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 15 Jan 2024 13:37:09 +0100 Subject: [PATCH 02/48] fix lint --- package-lock.json | 12 +++++------ src/features/entry/entry.component.ts | 30 +-------------------------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4dacf3d98..cddb1a157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siibra-explorer", - "version": "2.14.0", + "version": "2.14.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siibra-explorer", - "version": "2.14.0", + "version": "2.14.5", "license": "apache-2.0", "dependencies": { "@angular/animations": "^15.2.10", @@ -23,7 +23,7 @@ "@ngrx/effects": "^15.4.0", "@ngrx/store": "^15.4.0", "acorn": "^8.4.1", - "export-nehuba": "^0.1.2", + "export-nehuba": "^0.1.3", "file-loader": "^6.2.0", "jszip": "^3.6.0", "postcss": "^8.3.6", @@ -7646,9 +7646,9 @@ "dev": true }, "node_modules/export-nehuba": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.2.tgz", - "integrity": "sha512-rzydWAaa9QUKZqbYQcAuwnGsMGBlEQFD5URkEi5IGTG8LS4eH/xqc97ol0ZpUExa6jyn6nLtAjFJQmKL1rdV0w==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.3.tgz", + "integrity": "sha512-pooT9Af8LNuHtGqA3zzl75lT4G6TZt4f7VhOrRNL27ma9OxJ7OilMqOrJCTkfc5A5DaBampM61ay3srf9xZG1Q==", "dependencies": { "pako": "^1.0.6" } diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index d37a03949..69cc3c7d6 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChild, ViewChildren } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators'; -import { IDS, SAPI } from 'src/atlasComponents/sapi'; +import { SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; import { FeatureBase } from '../base'; import * as userInteraction from "src/state/userInteraction" @@ -10,7 +10,6 @@ import { CategoryAccDirective } from "../category-acc.directive" import { combineLatest, concat, forkJoin, merge, of, Subject, Subscription } from 'rxjs'; import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; import { TranslatedFeature } from '../list/list.directive'; -import { SPECIES_ENUM } from 'src/util/constants'; import { MatDialog } from 'src/sharedModules/angularMaterial.exports'; const categoryAcc = >(categories: T[]) => { @@ -27,33 +26,6 @@ const categoryAcc = >(categories: T[]) => { return returnVal } -type ConnectiivtyFilter = { - SPECIES: string[] - PARCELLATION: string[] - SPACE: string[] -} - -const WHITELIST_CONNECTIVITY: ConnectiivtyFilter = { - SPECIES: [ - SPECIES_ENUM.RATTUS_NORVEGICUS, - SPECIES_ENUM.HOMO_SAPIENS - ], - PARCELLATION: [ - IDS.PARCELLATION.JBA29, - IDS.PARCELLATION.JBA30, - IDS.PARCELLATION.WAXHOLMV4 - ], - SPACE: [], -} - -const BANLIST_CONNECTIVITY: ConnectiivtyFilter = { - SPECIES: [], - PARCELLATION: [], - SPACE: [ - IDS.TEMPLATES.BIG_BRAIN - ] -} - @Component({ selector: 'sxplr-feature-entry', templateUrl: './entry.flattened.component.html', From 18ccf86d6e58a37ffcf3c71aceaec68620b08371 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 22 Jan 2024 09:33:56 +0100 Subject: [PATCH 03/48] maint: update compound feature label maint: updated repo README.md maint: typo, update k8s configmap value, cleaned up env var maint: plugin API documentation fix: plugin API generation maint: use stable as default siibra-api endpoint maint: add git repo to mkdocs --- .helm/adhoc/configmap-siibra-explorer.yml | 2 +- README.md | 40 +++++-- build_env.md | 3 +- docs/getstarted/atlas_elements.md | 2 +- mkdocs.yml | 3 + src/api/broadcast/README.md | 13 +++ src/api/generateSchema.mjs | 15 ++- src/api/handshake/README.md | 8 ++ .../sxplr.init__fromSxplr__request.json | 15 +-- .../sxplr.init__fromSxplr__response.json | 20 ++++ src/api/request/README.md | 22 ++++ src/api/tsUtil.mjs | 110 +++++++++++++++++- src/environments/environment.common.ts | 4 +- .../compoundFtContainer.template.html | 2 +- src/plugin/README.md | 8 +- src/share/saneUrl/saneUrl.service.ts | 8 +- src/util/constants.ts | 9 -- 17 files changed, 231 insertions(+), 53 deletions(-) create mode 100644 src/api/handshake/sxplr.init__fromSxplr__response.json diff --git a/.helm/adhoc/configmap-siibra-explorer.yml b/.helm/adhoc/configmap-siibra-explorer.yml index 9dd25153d..37fbb9fc0 100644 --- a/.helm/adhoc/configmap-siibra-explorer.yml +++ b/.helm/adhoc/configmap-siibra-explorer.yml @@ -5,7 +5,7 @@ data: SIIBRA_CACHEDIR: /siibra-api-volume HBP_DISCOVERY_URL: "https://iam.ebrains.eu/auth/realms/hbp" REDIS_ADDR: "cache-redis-service" - V2_7_PLUGIN_URLS: "https://siibra-toolbox-jugex.apps.hbp.eu/viewer_plugin/manifest.json;https://ngpy.apps.hbp.eu/viewer_plugin/manifest.json" + V2_7_PLUGIN_URLS: "https://siibra-jugex.apps.tc.humanbrainproject.eu/viewer_plugin/manifest.json;https://ngpy.apps.hbp.eu/viewer_plugin/manifest.json" LOGGER_DIR: "/sxplr-log" kind: ConfigMap diff --git a/README.md b/README.md index f07b241af..614683c39 100644 --- a/README.md +++ b/README.md @@ -10,27 +10,36 @@ Copyright 2020-2021, Forschungszentrum Jülich GmbH -`siibra-explorer` is an frontend module wrapping around [nehuba](https://github.com/HumanBrainProject/nehuba) for visualizing volumetric brain volumes at possible high resolutions, and connecting to `siibra-api` for offering access to brain atlases of different species, including to navigate their brain region hierarchies, maps in different coordinate spaces, and linked regional data features. It provides metadata integration with the [EBRAINS knowledge graph](https://kg.ebrains.eu), different forms of data visualisation, and a structured plugin system for implementing custom extensions. +`siibra-explorer` is a browser based 3D viewer for exploring brain atlases that cover different spatial resolutions and modalities. It is built around an interactive 3D view of the brain displaying a unique selection of detailed templates and parcellation maps for the human, macaque, rat or mouse brain, including BigBrain as a microscopic resolution human brain model at its full resolution of 20 micrometers. + +![](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/siibra-explorer-teaser.png) + +`siibra-explorer` builds on top [nehuba](https://github.com/HumanBrainProject/nehuba) for the visualization volumetric brain volumes at possible high resolutions, and [three-surfer](https://github.com/xgui3783/three-surfer) for the visualization of surface based atlases. By connecting to [siibra-api](https://github.com/fzj-inm1-bda/siibra-api), `siibra-explorer` gains access to brain atlases of different species, including to navigate their brain region hierarchies, maps in different coordinate spaces, and linked regional data features. It provides metadata integration with the [EBRAINS knowledge graph](https://kg.ebrains.eu), different forms of data visualisation, and a structured plugin system for implementing custom extensions. ## Getting Started -A live version of the siibra explorer is available at [https://atlases.ebrains.eu/viewer/](https://atlases.ebrains.eu/viewer/). This section is useful for developers who would like to develop this project. +A live version of the siibra explorer is available at [https://atlases.ebrains.eu/viewer/](https://atlases.ebrains.eu/viewer/). User documentation can be found at . This README.md is aimed at developers who would like to develop and run `siibra-explorer` locally. ### General information Siibra explorer is built with [Angular (v14.0)](https://angular.io/), [Bootstrap (v4)](http://getbootstrap.com/), and [fontawesome icons](https://fontawesome.com/). Some other notable packages used are [ngrx/store](https://github.com/ngrx/platform) for state management. -Releases newer than [v0.2.9](https://github.com/HumanBrainProject/interactive-viewer/tree/v0.2.9) also uses a nodejs backend, which uses [passportjs](http://www.passportjs.org/) for user authentication, [express](https://expressjs.com/) as a http framework. +Releases newer than [v0.2.9](https://github.com/fzj-inm1-bda/siibra-explorer/releases/tag/v0.2.9) also uses a nodejs backend, which uses [passportjs](http://www.passportjs.org/) for user authentication, [express](https://expressjs.com/) as the http framework. + +Releases newer than [v2.13.0](https://github.com/fzj-inm1-bda/siibra-explorer/releases/tagv2.13.0) uses a python backend, which uses [authlib](https://pypi.org/project/Authlib/) for user authentication, [fastapi](https://pypi.org/project/fastapi/) as the http framework. + ### Develop #### Prerequisites -- node 12.20.0 or later +- node 16 or later #### Environments -It is recommended to manage your environments with `.env` file. +Development environments are stored under `src/environments/environment.common.ts`. At build time, `src/environments/environment.prod.ts` will be used to overwrite the environment. + +Whilst this approach adds some complexity to the development/build process, it enhances developer experience by allowing the static typing of environment variables. ##### Buildtime environments @@ -44,28 +53,35 @@ Please see [deploy_env.md](deploy_env.md) Please see [e2e_env.md](e2e_env.md) -#### Start dev server +#### Development -To run a dev server, run: +To run a frontend dev server, run: ```bash $ git clone https://github.com/FZJ-INM1-BDA/siibra-explorer $ cd siibra-explorer $ npm i -$ npm run dev-server +$ npm start +``` + +To run backend dev server: + +```bash +$ cd backend +$ pip install -r requirements.txt +$ uvicorn app.app:app --host 0.0.0.0 --port 8080 ``` -Start backend (in a separate terminal): +### Test ```bash -$ cd deploy -$ node server.js +$ npm run test ``` #### Build ```bash -$ npm run build-aot +$ npm run build ``` ### Develop plugins diff --git a/build_env.md b/build_env.md index caaf0ecff..a2253593e 100644 --- a/build_env.md +++ b/build_env.md @@ -4,9 +4,10 @@ As siibra-explorer uses [webpack define plugin](https://webpack.js.org/plugins/d | name | description | default | example | | --- | --- | --- | --- | +| `GIT_HASH` | Used to finely identify siibra-explorer version | | | +| `VERSION` | Used to coarsely identify siibra-explorer version | | | | `PRODUCTION` | if the build is for production, toggles optimisations such as minification | `undefined` | true | | `BACKEND_URL` | backend that the viewer calls to fetch available template spaces, parcellations, plugins, datasets | `null` | https://interactive-viewer.apps.hbp.eu/ | -| ~~`BS_REST_URL`~~ _deprecated. use `SIIBRA_API_ENDPOINTS` instead_ | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | `https://siibra-api-stable.apps.hbp.eu/v1_0` | | `SIIBRA_API_ENDPOINTS` | Comma separated endpoints of [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | `https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0` | | `MATOMO_URL` | base url for matomo analytics | `null` | https://example.com/matomo/ | | `MATOMO_ID` | application id for matomo analytics | `null` | 6 | diff --git a/docs/getstarted/atlas_elements.md b/docs/getstarted/atlas_elements.md index 13421e4ac..2f4a2790c 100644 --- a/docs/getstarted/atlas_elements.md +++ b/docs/getstarted/atlas_elements.md @@ -36,4 +36,4 @@ siibra provides access to data features anchored to locations in the brain. Loca [^4]: Amunts K, Mohlberg H, Bludau S, Zilles K. Julich-Brain: A 3D probabilistic atlas of the human brain’s cytoarchitecture. Science. 2020;369(6506):988-992. doi:[10.1126/science.abb4588](https://doi.org/10.1126/science.abb4588) -[^5]: Lebenberg J, Labit M, Auzias G, Mohlberg H, Fischer C, Rivière D, Duchesnay E, Kabdebon C, Leroy F, Labra N, Poupon F, Dickscheid T, Hertz-Pannier L, Poupon C, Dehaene-Lambertz G, Hüppi P, Amunts K, Dubois J, Mangin JF. A framework based on sulcal constraints to align preterm, infant and adult human brain images acquired in vivo and post mortem. Brain Struct Funct. 2018;223(9):4153-4168. doi:[10.1007/s00429-018-1735-9](10.1007/s00429-018-1735-9) +[^5]: Lebenberg J, Labit M, Auzias G, Mohlberg H, Fischer C, Rivière D, Duchesnay E, Kabdebon C, Leroy F, Labra N, Poupon F, Dickscheid T, Hertz-Pannier L, Poupon C, Dehaene-Lambertz G, Hüppi P, Amunts K, Dubois J, Mangin JF. A framework based on sulcal constraints to align preterm, infant and adult human brain images acquired in vivo and post mortem. Brain Struct Funct. 2018;223(9):4153-4168. doi:[10.1007/s00429-018-1735-9](https://doi.org/10.1007/s00429-018-1735-9) diff --git a/mkdocs.yml b/mkdocs.yml index dfd5ace1d..e634fcee7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,8 @@ site_name: Siibra Explorer User Documentation theme: name: 'material' +repo_url: https://github.com/fzj-inm1-bda/siibra-explorer + extra_css: - extra.css @@ -33,6 +35,7 @@ nav: - Coordinate lookups: 'basics/looking_up_coordinates.md' - Multimodal data features: 'basics/finding_multimodal_data.md' - Advanced functionalities: + - State encoding in URL: "advanced/url_encoding.md" - Superimposing local files: "advanced/superimposing_local_files.md" - Annotating structures in the brain: "advanced/annotating_structures.md" - Differential gene expression analysis: "advanced/differential_gene_expression_analysis.md" diff --git a/src/api/broadcast/README.md b/src/api/broadcast/README.md index ebf43a4f8..358d06ae2 100644 --- a/src/api/broadcast/README.md +++ b/src/api/broadcast/README.md @@ -7,3 +7,16 @@ Broadcasting messages are sent under two circumstances: - immediately after the plugin client acknowledged `handshake.init` to the specific client. This is so that the client can get the current state of the viewer. Broadcasting messages never expects a response (and thus will never contain and `id` attribute) + +## API + + + +| event name | initiator | request | response | +| --- | --- | --- | --- | +| sxplr.on.allRegions | viewer | [jsonschema](sxplr.on.allRegions__fromSxplr__request.json) | | +| sxplr.on.atlasSelected | viewer | [jsonschema](sxplr.on.atlasSelected__fromSxplr__request.json) | | +| sxplr.on.navigation | viewer | [jsonschema](sxplr.on.navigation__fromSxplr__request.json) | | +| sxplr.on.parcellationSelected | viewer | [jsonschema](sxplr.on.parcellationSelected__fromSxplr__request.json) | | +| sxplr.on.regionsSelected | viewer | [jsonschema](sxplr.on.regionsSelected__fromSxplr__request.json) | | +| sxplr.on.templateSelected | viewer | [jsonschema](sxplr.on.templateSelected__fromSxplr__request.json) | | diff --git a/src/api/generateSchema.mjs b/src/api/generateSchema.mjs index 6f9eb2544..4a4b3c1b5 100644 --- a/src/api/generateSchema.mjs +++ b/src/api/generateSchema.mjs @@ -2,7 +2,7 @@ import ts from 'typescript' import path, { dirname } from 'path' import { fileURLToPath } from "url" import { readFile, writeFile } from "node:fs/promises" -import { clearDirectory, resolveAllDefs } from "./tsUtil.mjs" +import { clearDirectory, resolveAllDefs, populateReadme } from "./tsUtil.mjs" import { processNode } from "./tsUtil/index.mjs" /** @@ -57,6 +57,7 @@ async function populateBroadCast(broadcastNode, node){ newSchema = await resolveAllDefs(newSchema, node) await writeFile(path.join(dirnames.broadcast, filename), JSON.stringify(newSchema, null, 2), 'utf-8') } + await populateReadme(dirnames.broadcast) } /** @@ -136,7 +137,7 @@ async function populateHeartbeatEvents(convoNode, node){ /** * request */ - const respFilename = `${NAMESPACE}.${eventName}__fromSxplr__request.json` + const respFilename = `${NAMESPACE}.${eventName}__fromSxplr__response.json` /** * @type {JSchema} */ @@ -157,6 +158,7 @@ async function populateHeartbeatEvents(convoNode, node){ await writeFile(path.join(dirnames.handshake, respFilename), JSON.stringify(respSchema, null, 2), "utf-8") } } + await populateReadme(dirnames.handshake) } /** @@ -224,6 +226,8 @@ async function populateBoothEvents(convoNode, node){ await writeFile(path.join(dirnames.request, respFilename), JSON.stringify(respSchema, null, 2), "utf-8") } } + + await populateReadme(dirnames.request) } const main = async () => { @@ -246,7 +250,6 @@ const main = async () => { } }) - } - - main() - \ No newline at end of file +} + +main() diff --git a/src/api/handshake/README.md b/src/api/handshake/README.md index 02e05c5f8..30fe9a253 100644 --- a/src/api/handshake/README.md +++ b/src/api/handshake/README.md @@ -1,3 +1,11 @@ # Handshake API Handshake messages are meant for siibra-explorer to probe if the plugin is alive and well (and also a way for the plugin to check if siibra-explorer is responsive) + +## API + + + +| event name | initiator | request | response | +| --- | --- | --- | --- | +| sxplr.init | viewer | [jsonschema](sxplr.init__fromSxplr__request.json) | [jsonschema](sxplr.init__fromSxplr__response.json) | diff --git a/src/api/handshake/sxplr.init__fromSxplr__request.json b/src/api/handshake/sxplr.init__fromSxplr__request.json index d97b80726..ce4cbdf88 100644 --- a/src/api/handshake/sxplr.init__fromSxplr__request.json +++ b/src/api/handshake/sxplr.init__fromSxplr__request.json @@ -2,19 +2,14 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "jsonrpc": { - "const": "2.0" - }, "id": { "type": "string" }, - "result": { - "properties": { - "name": { - "type": "string" - } - }, - "type": "object" + "jsonrpc": { + "const": "2.0" + }, + "method": { + "const": "sxplr.init" } } } \ No newline at end of file diff --git a/src/api/handshake/sxplr.init__fromSxplr__response.json b/src/api/handshake/sxplr.init__fromSxplr__response.json new file mode 100644 index 000000000..d97b80726 --- /dev/null +++ b/src/api/handshake/sxplr.init__fromSxplr__response.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "jsonrpc": { + "const": "2.0" + }, + "id": { + "type": "string" + }, + "result": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object" + } + } +} \ No newline at end of file diff --git a/src/api/request/README.md b/src/api/request/README.md index 6ca2f74e3..7ee087007 100644 --- a/src/api/request/README.md +++ b/src/api/request/README.md @@ -33,3 +33,25 @@ window.addEventListener('pagehide', () => { }) }) ``` + +## API + + + +| event name | initiator | request | response | +| --- | --- | --- | --- | +| sxplr.addAnnotations | client | [jsonschema](sxplr.addAnnotations__toSxplr__request.json) | [jsonschema](sxplr.addAnnotations__toSxplr__response.json) | +| sxplr.cancelRequest | client | [jsonschema](sxplr.cancelRequest__toSxplr__request.json) | [jsonschema](sxplr.cancelRequest__toSxplr__response.json) | +| sxplr.exit | client | [jsonschema](sxplr.exit__toSxplr__request.json) | [jsonschema](sxplr.exit__toSxplr__response.json) | +| sxplr.getAllAtlases | client | [jsonschema](sxplr.getAllAtlases__toSxplr__request.json) | [jsonschema](sxplr.getAllAtlases__toSxplr__response.json) | +| sxplr.getSupportedParcellations | client | [jsonschema](sxplr.getSupportedParcellations__toSxplr__request.json) | [jsonschema](sxplr.getSupportedParcellations__toSxplr__response.json) | +| sxplr.getSupportedTemplates | client | [jsonschema](sxplr.getSupportedTemplates__toSxplr__request.json) | [jsonschema](sxplr.getSupportedTemplates__toSxplr__response.json) | +| sxplr.getUserToSelectARoi | client | [jsonschema](sxplr.getUserToSelectARoi__toSxplr__request.json) | [jsonschema](sxplr.getUserToSelectARoi__toSxplr__response.json) | +| sxplr.loadLayers | client | [jsonschema](sxplr.loadLayers__toSxplr__request.json) | [jsonschema](sxplr.loadLayers__toSxplr__response.json) | +| sxplr.navigateTo | client | [jsonschema](sxplr.navigateTo__toSxplr__request.json) | [jsonschema](sxplr.navigateTo__toSxplr__response.json) | +| sxplr.removeLayers | client | [jsonschema](sxplr.removeLayers__toSxplr__request.json) | [jsonschema](sxplr.removeLayers__toSxplr__response.json) | +| sxplr.rmAnnotations | client | [jsonschema](sxplr.rmAnnotations__toSxplr__request.json) | [jsonschema](sxplr.rmAnnotations__toSxplr__response.json) | +| sxplr.selectAtlas | client | [jsonschema](sxplr.selectAtlas__toSxplr__request.json) | [jsonschema](sxplr.selectAtlas__toSxplr__response.json) | +| sxplr.selectParcellation | client | [jsonschema](sxplr.selectParcellation__toSxplr__request.json) | [jsonschema](sxplr.selectParcellation__toSxplr__response.json) | +| sxplr.selectTemplate | client | [jsonschema](sxplr.selectTemplate__toSxplr__request.json) | [jsonschema](sxplr.selectTemplate__toSxplr__response.json) | +| sxplr.updateLayers | client | [jsonschema](sxplr.updateLayers__toSxplr__request.json) | [jsonschema](sxplr.updateLayers__toSxplr__response.json) | diff --git a/src/api/tsUtil.mjs b/src/api/tsUtil.mjs index ea2dda6d3..61f59565d 100644 --- a/src/api/tsUtil.mjs +++ b/src/api/tsUtil.mjs @@ -1,9 +1,11 @@ import ts from "typescript" import { readdir, mkdir, unlink } from "node:fs/promises" import path, { dirname } from 'path' -import { readFile } from "node:fs/promises" +import { readFile, writeFile } from "node:fs/promises" import { fileURLToPath } from "url" +const WARNINGTXT = `` + const __dirname = dirname(fileURLToPath(import.meta.url)) /** @@ -48,6 +50,112 @@ export async function clearDirectory(pathToDir){ } } + +/** + * + * @param {string} pathToDir + */ +export async function populateReadme(pathToDir){ + + /** + * @type {string} + */ + const text = await readFile(`${pathToDir}/README.md`, 'utf-8') + + /** + * @type {Array.} + */ + const newText = [] + + const lines = text.split("\n") + for (const line of lines) { + newText.push(line) + if (line.startsWith(WARNINGTXT)) { + break + } + } + + newText.push("") + + const files = await readdir(pathToDir) + + /** + * @typedef {Object} EventObj + * @property {'viewer'|'client'} initiator + * @property {string} requestFile + * @property {string} responseFile + */ + + /** + * @type {Object.} + */ + const events = {} + + for (const f of files) { + /** + * only remove json files + */ + if (f.endsWith(".json")) { + const [ evName, fromTo, reqResp ] = f.replace(/\.json$/, "").split("__") + if (['fromSxplr', 'toSxplr'].indexOf(fromTo) < 0) { + throw Error(`Expected ${fromTo} to be either 'fromSxplr' or 'toSxplr', but was neither`) + } + let initiator + if (fromTo === "fromSxplr") { + initiator = "viewer" + } + if (fromTo === "toSxplr") { + initiator = "client" + } + if (['request', 'response'].indexOf(reqResp) < 0) { + throw new Error(`Expected ${reqResp} to be either 'request' or 'response', but was neither`) + } + + /** + * @type {Object} + * @property {string} requestFile + * @property {string} responseFile + */ + const reqRespObj = {} + if (reqResp === "request") { + reqRespObj.requestFile = f + } + if (reqResp === "response") { + reqRespObj.responseFile = f + } + if (!events[evName]) { + events[evName] = { + initiator, + } + } + events[evName] = { + ...events[evName], + ...reqRespObj, + } + + } + } + + + function linkMd(file){ + if (!file) { + return `` + } + return `[jsonschema](${file})` + } + + newText.push( + `| event name | initiator | request | response |`, + `| --- | --- | --- | --- |`, + ...Object.entries(events).map( + ([ evName, { initiator, requestFile, responseFile }]) => `| ${evName} | ${initiator} | ${linkMd(requestFile)} | ${linkMd(responseFile)} |` + ), + `` + ) + + await writeFile(`${pathToDir}/README.md`, newText.join("\n"), "utf8") +} + /** * * @param {Object|Array|string} input diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 98a9c4814..6f2a371d3 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -8,8 +8,8 @@ export const environment = { // some libraries rely on the exact string formatting to work properly SIIBRA_API_ENDPOINTS: // 'http://localhost:10081/v3_0', // endpoint-local-10081 - 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //endpoint-latest - // 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable + // 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //endpoint-latest + 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, MATOMO_ID: null, diff --git a/src/features/compoundFtContainer/compoundFtContainer.template.html b/src/features/compoundFtContainer/compoundFtContainer.template.html index c7c9e1152..b7ace3dd7 100644 --- a/src/features/compoundFtContainer/compoundFtContainer.template.html +++ b/src/features/compoundFtContainer/compoundFtContainer.template.html @@ -1,5 +1,5 @@
- Please select a sub feature + Please select an element
diff --git a/src/plugin/README.md b/src/plugin/README.md index 6cee16b9c..2353fb91d 100644 --- a/src/plugin/README.md +++ b/src/plugin/README.md @@ -48,8 +48,10 @@ The API is generated automatically with the following script: npm run api-schema ``` -[handshake API](../api/handshake/README.md.md) +The references can be seen below: -[broadcast API](../api/broadcast/README.md.md) +[handshake API](../api/handshake/README.md) -[request API](../api/request/README.md.md) +[broadcast API](../api/broadcast/README.md) + +[request API](../api/request/README.md) diff --git a/src/share/saneUrl/saneUrl.service.ts b/src/share/saneUrl/saneUrl.service.ts index d838c72b8..0e35a1f08 100644 --- a/src/share/saneUrl/saneUrl.service.ts +++ b/src/share/saneUrl/saneUrl.service.ts @@ -2,22 +2,18 @@ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { throwError } from "rxjs"; import { catchError, mapTo } from "rxjs/operators"; -import { BACKENDURL } from 'src/util/constants' import { IKeyValStore, NotFoundError } from '../type' +import { environment } from "src/environments/environment"; @Injectable({ providedIn: 'root' }) export class SaneUrlSvc implements IKeyValStore{ - public saneUrlRoot = `${BACKENDURL}go/` + public saneUrlRoot = `${environment.BACKEND_URL || ''}go/` constructor( private http: HttpClient ){ - if (!BACKENDURL) { - const loc = window.location - this.saneUrlRoot = `${loc.protocol}//${loc.hostname}${!!loc.port ? (':' + loc.port) : ''}${loc.pathname}go/` - } } getKeyVal(key: string) { diff --git a/src/util/constants.ts b/src/util/constants.ts index d0878a40f..54afabe2d 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -12,15 +12,6 @@ export const LOCAL_STORAGE_CONST = { export const COOKIE_VERSION = '0.3.0' export const KG_TOS_VERSION = '0.3.0' -export const BACKENDURL = (() => { - const { BACKEND_URL } = environment - if (!BACKEND_URL) return `` - if (/^http/.test(BACKEND_URL)) return BACKEND_URL - - const url = new URL(window.location.href) - const { protocol, hostname, pathname } = url - return `${protocol}//${hostname}${pathname.replace(/\/$/, '')}/${BACKEND_URL}` -})() export const MIN_REQ_EXPLAINER = ` - Siibra explorer requires **webgl2.0**, and the \`EXT_color_buffer_float\` extension enabled. From 91b8af218f88b2b18283029864fc1fab7a7a41f4 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 22 Jan 2024 12:00:25 +0100 Subject: [PATCH 04/48] doc: add doc for URL encoding --- docs/advanced/url_encoding.md | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/advanced/url_encoding.md diff --git a/docs/advanced/url_encoding.md b/docs/advanced/url_encoding.md new file mode 100644 index 000000000..330475ee8 --- /dev/null +++ b/docs/advanced/url_encoding.md @@ -0,0 +1,54 @@ +# URL encoding + +!!! warning + It is generally advised that users leverage the [explorer module of siibra-python](https://siibra-python.readthedocs.io/en/latest/autoapi/siibra/explorer/index.html#module-siibra.explorer) to encode and decode URL. + +siibra-explorer achieves [state persistence](../basics/storing_and_sharing_3d_views.md) via URL encoding. + +## Basics + +State that are persisted is first serialized to string. These string are then prefixed by the label representing the string. The list is joined with `/` as a delimiter. + +The resultant string is then prepended by `#/`, where `` is usually `https://atlases.ebrains.eu/viewer/`. + +!!! example + User navigated to the following viewer configuration + + | state | prefix | value | serialized | prefix + serialized | + | --- | --- | --- | --- | --- | + | atlas | `a:` | Multilevel Human Atlas | `juelich:iav:atlas:v1.0.0:1` | `a:juelich:iav:atlas:v1.0.0:1` | + | space | `t:` | ICBM 2009c nonlinear asymmetrical | `minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2` | `t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2` | + | parcellation | `p:` | Julich Brain v3.0.3 | `minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-300` | `p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-300` | + + The URL would be + + ``` + https://atlases.ebrains.eu/viewer/#/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2/p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-300 + ``` + +## Escaping Characters + +As `/` character is used to separate state, it is escaped to `:`. + +## References + +Here is a comprehensive list of the state encoded in the URL: + +| selected state | prefix | value | example | +| --- | --- | --- | --- | +| atlas | `a:` | id property | | +| parcellation | `p:` | id property | | +| space | `t:` | id property | | +| region | `rn:` | Quick hash of region name[^1] | | +| navigation | `@:` | navigation state hash[^2] | | +| feature | `f:` | id property + additional escaping[^3] | | +| misc viewer state | `vs:` | misc viewer state serialization[^4] | | +| auto launch plugin | `pl` (query param) | stringified JSON representing `string[]` | `?pl=%5B%22http%3A%2F%2Flocalhost%3A1234%2Fmanifest.json%22%5D` . Modern browsers also accept `?pl=["http://localhost:1234/manifest.json"]` | + +[^1]: Quick hash. [[source]](https://github.com/FZJ-INM1-BDA/siibra-explorer/blob/v2.14.4/src/util/fn.ts#L146-L154) Quick one way hash. It will likely be deprecated in favor of [crypto.digest](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) in the near future. + +[^2]: Encoding navigation state. [[source]](https://github.com/FZJ-INM1-BDA/siibra-explorer/blob/v2.14.4/src/routerModule/routeStateTransform.service.ts#L366-L372) Each of the following state are encoded: `orientation`, `perspectiveOrientation`, `perspectiveZoom`, `position`, `zoom`. They are cast into `[f32, f32, f32, f32]`, `[f32, f32, f32, f32]`, `int`, `[int, int, int]` and `int` respective. Each of the number is base64 encoded [[source]](https://github.com/FZJ-INM1-BDA/siibra-explorer/blob/v2.14.4/common/util.js#L242-L313) with the following cipher: `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-`. Negation is denoted using `~` as the beginning of encoded value. If the state consists of a tuple of values, they are joined by a single separator (i.e. `:`). The encoded state is then joined with two separators (i.e. `::`) + +[^3]: additional feature id escaping: since feature id can be a lot more varied, they are further encoded by: first instance of `://` is replaced with `~ptc~`; all instances of `:` is replaced with `~`; *any* occurances `[()]` are URL encoded. + +[^4]: miscellaneous viewer state serialization. [[source]](https://github.com/FZJ-INM1-BDA/siibra-explorer/blob/v2.14.4/src/routerModule/routeStateTransform.service.ts#L272-L293) Various viewer configuration related state is encoded. This encoded state is versioned, in order to preserve backwards compatibility. The current version is `v1`. In the current version, three `uint8` values are base64 encoded. First encodes for panel mode ( four-panel, `FOUR_PANEL`, encoded as `1`; `PIP_PANEL`, encoded as `2`). Second encodes for panel order. Third encodes for the bit masked boolean flags for octant removal and show delination, with the remaining 6 bits ignored. From a199f8223df84afbb0598660949e155d09678fed Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 22 Jan 2024 12:13:30 +0100 Subject: [PATCH 05/48] fix: lint --- src/util/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/util/constants.ts b/src/util/constants.ts index 54afabe2d..b2a3735ee 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -1,5 +1,4 @@ import { HttpHeaders } from "@angular/common/http" -import { environment } from 'src/environments/environment' export const LOCAL_STORAGE_CONST = { GPU_LIMIT: 'fzj.xg.iv.GPU_LIMIT', From 16490fb528a921e39eb9e6799be2843d380bd944 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 22 Jan 2024 14:07:59 +0100 Subject: [PATCH 06/48] [skip ci] update doc --- docs/releases/v2.14.5.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index fedf235b8..05afa96fb 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -3,6 +3,8 @@ ## Feature - Add support for compound feature +- Added documentation for URL encoding +- Improved documentation for plugin API ## Behind the Scenes From ae543d2895919bb78abc364b67f38957d05b63a6 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 23 Jan 2024 16:00:48 +0100 Subject: [PATCH 07/48] fix: colorize atlas only happens when visualization tab is open maint: minor fix on spalshscreen refactor: reworked e2e checklist --- docs/basics/exploring_3d_parcellation_maps.md | 4 +- docs/basics/selecting_brain_regions.md | 4 +- docs/basics/storing_and_sharing_3d_views.md | 6 +- docs/getstarted/ui.md | 22 +-- e2e/checklist.md | 89 ------------- features/_convert.py | 125 ++++++++++++++++++ features/atlas-availability.feature | 34 +++++ features/atlas-download.feature | 24 ++++ features/basic-ui.feature | 16 +++ features/doc-launch-quicktour.feature | 12 ++ features/doc-user-interface-structure.feature | 47 +++++++ .../docs-exploring-parcellation-maps.feature | 41 ++++++ .../docs-multimodal-data-features.feature | 13 ++ features/docs-storing-sharing-3dview.feature | 6 + features/exploring-features.feature | 44 ++++++ features/exploring-selected-region.feature | 28 ++++ features/navigation.feature | 38 ++++++ features/plugin-jugex.feature | 44 ++++++ features/point-assignment.feature | 28 ++++ features/region-hierarchy.feature | 19 +++ features/saneurl.feature | 80 +++++++++++ features/selecting-region.feature | 28 ++++ features/switching-selection.feature | 17 +++ mkdocs.yml | 4 + .../atlas/splashScreen/splashScreen.style.css | 7 +- .../splashScreen/splashScreen.template.html | 20 ++- .../feature-view/feature-view.component.html | 8 +- .../feature-view/feature-view.component.ts | 107 +++++---------- src/features/intents/intents.component.ts | 88 ++++++++++++ src/features/intents/intents.style.css | 0 src/features/intents/intents.template.html | 0 src/features/module.ts | 2 + 32 files changed, 808 insertions(+), 197 deletions(-) delete mode 100644 e2e/checklist.md create mode 100644 features/_convert.py create mode 100644 features/atlas-availability.feature create mode 100644 features/atlas-download.feature create mode 100644 features/basic-ui.feature create mode 100644 features/doc-launch-quicktour.feature create mode 100644 features/doc-user-interface-structure.feature create mode 100644 features/docs-exploring-parcellation-maps.feature create mode 100644 features/docs-multimodal-data-features.feature create mode 100644 features/docs-storing-sharing-3dview.feature create mode 100644 features/exploring-features.feature create mode 100644 features/exploring-selected-region.feature create mode 100644 features/navigation.feature create mode 100644 features/plugin-jugex.feature create mode 100644 features/point-assignment.feature create mode 100644 features/region-hierarchy.feature create mode 100644 features/saneurl.feature create mode 100644 features/selecting-region.feature create mode 100644 features/switching-selection.feature create mode 100644 src/features/intents/intents.component.ts create mode 100644 src/features/intents/intents.style.css create mode 100644 src/features/intents/intents.template.html diff --git a/docs/basics/exploring_3d_parcellation_maps.md b/docs/basics/exploring_3d_parcellation_maps.md index 86a52f6ed..71eef60cb 100644 --- a/docs/basics/exploring_3d_parcellation_maps.md +++ b/docs/basics/exploring_3d_parcellation_maps.md @@ -11,7 +11,7 @@ Note: - Vice versa, when selecting a parcellation which is not available as a map in the currently selected reference space, you will be asked to select a different space. !!! tip "Downloading the current parcellation map" - You can download the currently selected reference template and parcellation map for offline use by clicking the download button on the top right. + You can download the currently selected reference template and parcellation map for offline use by clicking the download button :material-download: on the top right. In the case of a volumetric template, siibra-explorer combines a rotatable 3D surface view of a brain volume with three planar views of orthogonal image planes (coronal, sagittal, horizontal). It can visualize very large brain volumes in the Terabyte range (here: BigBrain model [^1]). @@ -19,7 +19,7 @@ In the case of a volumetric template, siibra-explorer combines a rotatable 3D su Each planar view allows zooming (`[mouse-wheel]`) and panning (`[mouse-drag]`). You can change the default planes to arbitrary oblique cutting planes using ` + [mouse-drag]`. This is especially useful for inspecting cortical layers and brain regions in their optimal 3D orientation when browsing a microscopic volume. -In addition, each planar view can be maximized to full screen (`[mouse-over]` then `` on `[ ]` icon) to behave like a 2D image viewer. +In addition, each planar view can be maximized to full screen (`[mouse-over]` then `` on :fontawesome-solid-expand: icon) to behave like a 2D image viewer. After maximizing a view, `[space]` cycles between the four available views. ![](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/julichbrain_bigbrain_coronal.png){: style="width:600px" } diff --git a/docs/basics/selecting_brain_regions.md b/docs/basics/selecting_brain_regions.md index 09845ff8c..ee57fd07a 100644 --- a/docs/basics/selecting_brain_regions.md +++ b/docs/basics/selecting_brain_regions.md @@ -45,9 +45,9 @@ For convenience, the region tree allows to you directly switch species, space an Regions are typically organized as a hierarchy of parent and child regions. For example, in the Julich-Brain parcellation, the top parent nodes are *Telencephalon*, *Metencephalon*, and *Diencephalon*, further subdivided into macroscopic structures such as lobes which then contain subhierarchies of cortical and subcortical regions. -## The region sidepanel +## The Region Side Panel -After finding and selecting a brain region, `siibra-explorer` opens the [region sidepanel](#the-region-sidepanel). +After finding and selecting a brain region, `siibra-explorer` opens the region side panel. ![](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/siibra-explorer-regionpanel-detail.png){ style="width:300px"} diff --git a/docs/basics/storing_and_sharing_3d_views.md b/docs/basics/storing_and_sharing_3d_views.md index 4540f926a..acae37537 100644 --- a/docs/basics/storing_and_sharing_3d_views.md +++ b/docs/basics/storing_and_sharing_3d_views.md @@ -7,11 +7,11 @@ Any views in `siibra-explorer` can be shared via their URLs. You can use any nat !!! info Automatic markdown renderer such as rocketchat can interfere with the rendering of the URL. To circumvent the issue, it is recommended that you generate a sane URL, or use a `code` block to escape markup -## Creating short URLs ("saneURL") +## Creating short URLs (saneURL) Whilst `siibra-explorer` encodes the current view directly in the URL, such full URLs are quite long and not easily recitable or memorable. You can create short URLs for the current view directly in `siibra-explorer`. To do so, -1. click the *view navigation panel* on the top left (see [main UI elements](../ui/main_elements.md#-and-plugins), +1. click the *view navigation panel* on the top left (see [main UI elements](../ui/main_elements.md#-and-plugins)), 2. click the "share icon", then 3. select `Create custom URL`. @@ -24,7 +24,7 @@ Whilst `siibra-explorer` encodes the current view directly in the URL, such full While you can use any screenshot tool provided by your browser or operating system, `siibra-explorer` offers a dedicated screenshot function which generates a clean image of the current 3D view which hides other user interface elements. To access the screenshot tool, -1. open the tool menu (`᎒᎒᎒`) from the top right (see [main UI elements](../ui/main_elements.md#tools-and-plugins)), then +1. open the tool menu (:material-apps:) from the top right (see [main UI elements](../ui/main_elements.md#tools-and-plugins)), then 2. select `Screenshot`. ![Creating a screenshot in siibra-explorer](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/siibra-explorer-screenshots.png){ style="width:700px"} diff --git a/docs/getstarted/ui.md b/docs/getstarted/ui.md index 28951dc9e..06309b395 100644 --- a/docs/getstarted/ui.md +++ b/docs/getstarted/ui.md @@ -17,7 +17,7 @@ For more information, read about [Exploring 3D parcellation maps](../basics/expl ## View navigation panel & coordinate lookups -At the top left of the user interface, `siibra-explorer` displays the 3D coordinate of the currently selected center of view, together with buttons for expanding the panel (⌄) and entering custom coordinates (✎). +At the top left of the user interface, `siibra-explorer` displays the 3D coordinate of the currently selected center of view, together with buttons for expanding the panel (:material-chevron-down:) and entering custom coordinates (:fontawesome-solid-pen:). Expanding the panel allows to allows to modify the center point and create a shareable link to the current view (see ["Storing and sharing 3D views"](../basics/storing_and_sharing_3d_views.md)). !!! tip "Use coordinate lookups do probabilistic assignment" @@ -28,14 +28,14 @@ Expanding the panel allows to allows to modify the center point and create a sha ## Atlas selection panel At the bottom of the window, you find buttons to switch between different species, reference templates and parcellation maps. -Working with parcellation maps is described in ["Exploring parcellation maps"](../basics/exploring_3d_parcellation_maps.md)). +Working with parcellation maps is described in [Exploring parcellation maps](../basics/exploring_3d_parcellation_maps.md). Note that some of the buttons may be hidden in case that only one option is available. ![](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/siibra-explorer-chips.png){ style="width:500px"} ## Region search panel -The magnifying glass icon (🔍) in the top left reveals the region search panel. Here you can type keywords to find matching brain regions in the currently selected parcellation, and open the extended regionstree search. +The magnifying glass icon (:octicons-search-16:) in the top left reveals the region search panel. Here you can type keywords to find matching brain regions in the currently selected parcellation, and open the extended regionstree search. To learn more, read about [selecting brain regions](../basics/selecting_brain_regions.md). ![](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/siibra-explorer-regionsearch.png){ style="width:500px"} @@ -47,17 +47,17 @@ At the top right of the viewer, there are several icons guiding you to additiona ![](https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/static/siibra-explorer-tools.png){ style="width:500px"} -#### (?) Help panel -The help button (?) opens information about keyboard shortcuts and terms of use. Here you can also launch the interactive quick tour, which is started automatically when you use `siibra-explorer` for the first time. +#### :octicons-question-16: Help panel +The help button :octicons-question-16: opens information about keyboard shortcuts and terms of use. Here you can also launch the interactive quick tour, which is started automatically when you use `siibra-explorer` for the first time. -#### Download current view -The download button () will retrieve the reference template and parcellation map currently displayed in a zip file package +#### :material-download: Download current view +The download button (:material-download:) will retrieve the reference template and parcellation map currently displayed in a zip file package -#### ᎒᎒᎒ Plugins -The plugin button (᎒᎒᎒) reveals a menu of interactive plugins, including advanced tools for [annotation](../advanced/annotating_structures.md) and [differential gene expression analysis](../advanced/differential_gene_expression_analysis.md) +#### :material-apps: Plugins +The plugin button (:material-apps:) reveals a menu of interactive plugins, including advanced tools for [annotation](../advanced/annotating_structures.md) and [differential gene expression analysis](../advanced/differential_gene_expression_analysis.md) -#### 👤 Sign in with EBRAINS -The login button (👤) allows you to sign in with an EBRAINS account to access some custom functionalities for sharing. +#### :fontawesome-solid-user: Sign in with EBRAINS +The login button (:fontawesome-solid-user:) allows you to sign in with an EBRAINS account to access some custom functionalities for sharing. !!! tip "Get an EBRAINS account!" You can sign up for a free EBRAINS account at diff --git a/e2e/checklist.md b/e2e/checklist.md deleted file mode 100644 index d6f3f1dce..000000000 --- a/e2e/checklist.md +++ /dev/null @@ -1,89 +0,0 @@ -# Staging Checklist - -**use incognito browser** - -[home page](https://atlases.ebrains.eu/viewer-staging/) - -## General - -- [ ] Can access front page -- [ ] Can login to oidc v2 via top-right - -## Verify testing correct siibra-explorer and siibra-api versions - -- [ ] Git hash from `[?]` -> `About` matches with the git hash of `HEAD` of staging -- [ ] Message `Expecting {VERSION}, but got {VERSION}, some functionalities may not work as expected` does **not** show - -## Atlas data specific - -- [ ] Human multilevel atlas - - [ ] on click from home page, MNI152, Julich v2.9 loads without issue - - [ ] on hover, show correct region name(s) - - [ ] Parcellation smart chip - - [ ] show/hide parcellation toggle exists and works - - [ ] `q` is a shortcut to show/hide parcellation toggle - - [ ] info button exists and works - - [ ] info button shows desc, and link to KG - - [ ] regional is fine :: select hOC1 right - - [ ] probabilistic map loads fine - - [ ] segmentation layer hides - - [ ] `navigate to` button exists, and works - - [ ] `Open in KG` button exists and works - - [ ] `Description` tabs exists and works - - [ ] `Regional features` tab exists and works - - [ ] `Receptor density` dataset exists and works - - [ ] `Open in KG` button exists and works - - [ ] `Preview` tab exists and works - - [ ] fingerprint is shown, interactable - - [ ] profiles can be loaded, interactable - - [ ] `Connectivity` tab exists and works - - [ ] on opening tab, PMap disappear, colour mapped segmentation appears - - [ ] on closing tab, PMap reappear, segmentation hides - - [ ] switching template/parc - - [ ] mni152 julich brain 29 (big brain) -> big brain julich brain 29 - - [ ] big brain julich brain 29 (long bundle) -> (asks to change template) - - [ ] in big brain v2.9 (or latest) - - [ ] high res hoc1, hoc2, hoc3, lam1-6 are visible - - [ ] pli dataset [link](https://atlases.ebrains.eu/viewer-staging/?templateSelected=Big+Brain+%28Histology%29&parcellationSelected=Grey%2FWhite+matter&cNavigation=0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..1uaTK.Bq5o~.lKmo~..NBW&previewingDatasetFiles=%5B%7B%22datasetId%22%3A%22minds%2Fcore%2Fdataset%2Fv1.0.0%2Fb08a7dbc-7c75-4ce7-905b-690b2b1e8957%22%2C%22filename%22%3A%22Overlay+of+data+modalities%22%7D%5D) - - [ ] redirects fine - - [ ] shows fine - - [ ] fsaverage - - [ ] can be loaded & visible -- [ ] Waxholm - - [ ] v4 are visible - - [ ] on hover, show correct region name(s) - - [ ] whole mesh loads - -## Pt Assignments - -- [ ] human MNI152 julich brain should work (statistical) -- [ ] rat waxholm v4 should work (labelled) -- [ ] csv can be downloaded -- [ ] big brain & fsaverage *shouldn't* work - -## Download atlas - -- [ ] human MNI152 julich brain can be downloaded -- [ ] human MNI152 julich brain hoc1 left can be downloaded -- [ ] rat waxholm v4 can be downloaded - -## saneURL -- [ ] saneurl generation functions properly - - [ ] try existing key (human), and get unavailable error - - [ ] try non existing key, and get available - - [ ] create use key `x_tmp_foo` and new url works -- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/bigbrainGreyWhite) redirects to big brain -- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/julichbrain) redirects to julich brain (colin 27) -- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/whs4) redirects to waxholm v4 -- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/allen2017) redirects to allen 2017 -- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/mebrains) redirects to monkey -- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/stnr) redirects to URL that contains annotations - -## VIP URL -- [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/human) redirects to human mni152 -- [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/monkey) redirects mebrains -- [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/rat) redirects to waxholm v4 -- [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/mouse) redirects allen mouse 2017 - -## plugins -- [ ] jugex plugin works diff --git a/features/_convert.py b/features/_convert.py new file mode 100644 index 000000000..3dc64092e --- /dev/null +++ b/features/_convert.py @@ -0,0 +1,125 @@ +from pathlib import Path +from typing import TypedDict, Literal +import sys + + +from gherkin.parser import Parser + +class Location(TypedDict): + line: int + column: int + + +class Base(TypedDict): + tags: list[str] + keyword: str + name: str + location: Location + description: str + + +class Step(TypedDict): + id: str + location: Location + keyword: list[str] + keywordType: Literal['Context', 'Action', 'Outcome'] + text: str + + +class Scenario(Base): + id: str + examples: list[str] + steps: list[Step] + + +class ScenarioDict(TypedDict): + scenario: Scenario + + +class Feature(Base): + language: str + children: list[ScenarioDict] + + +class Comment(TypedDict): + location: Location + text: str + + +class ParsedAST(TypedDict): + feature: Feature + comments: list[Comment] + + + +def gherkin_to_markdown(gherkin_text): + parser = Parser() + feature: ParsedAST = parser.parse(gherkin_text) + + ret_text: list[str] = [] + + f = feature['feature'] + + feature_name = f['name'] + + ret_text.append(f['description'].strip()) + + for scenario in f['children']: + s = scenario['scenario'] + ret_text.append( + f"### {s['name']}" + ) + for step in s['steps']: + verb = step['keywordType'] + if verb == "Context": + verb = "Given" + if verb == "Action": + verb = "When" + if verb == "Outcome": + verb = "Then" + + ret_text.append( + f"- **{verb}** {step['text']}" + ) + + ret_text.append( + f"- [ ] Works" + ) + + return ( + """
""" + + f"""{feature_name}""" + + "\n\n" + + '\n\n'.join(ret_text) + + "\n\n" + + """
""" + + "\n\n" + + "- [ ] All Checked" + + "\n\n" + + "---" + + "\n\n" + ) + + +def main(output: str="./e2e/checklist.md"): + + path_to_feature = Path("features") + markdown_txt = """# Staging Checklist + +**use incognito browser** + +[homepage](https://atlases.ebrains.eu/viewer-staging/) + +""" + for f in path_to_feature.iterdir(): + if f.suffix != ".feature": + continue + text = f.read_text() + markdown_txt += gherkin_to_markdown(text) + + with open(output, "w") as fp: + fp.write(markdown_txt) + + +if __name__ == "__main__": + main(*sys.argv[1:]) \ No newline at end of file diff --git a/features/atlas-availability.feature b/features/atlas-availability.feature new file mode 100644 index 000000000..6fa438681 --- /dev/null +++ b/features/atlas-availability.feature @@ -0,0 +1,34 @@ +Feature: Atlas data availability + + Users should expect all facets of atlas to be available + + Scenario: User checks out high resolution Julich Brain regions + Given User launched the atlas viewer + When User selects Julich Brain v2.9 in Big Brain space + Then User should find high resolution hOc1, hOc2, hOc3, lam1-6 + + Scenario: User checks out PLI dataset from KG + When User clicks the link curated in KG [link](https://atlases.ebrains.eu/viewer-staging/?templateSelected=Big+Brain+%28Histology%29&parcellationSelected=Grey%2FWhite+matter&cNavigation=0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..1uaTK.Bq5o~.lKmo~..NBW&previewingDatasetFiles=%5B%7B%22datasetId%22%3A%22minds%2Fcore%2Fdataset%2Fv1.0.0%2Fb08a7dbc-7c75-4ce7-905b-690b2b1e8957%22%2C%22filename%22%3A%22Overlay+of+data+modalities%22%7D%5D) + Then User is redirected, and everything shows fine + + Scenario: User checks out Julich Brain in fsaverage + Given User launched the atlas viewer + When User selects Julich Brain in fsaverage space + Then The atlas loads and shows fine + + Scenario: User checks out Waxholm atlas + Given User launched the atlas viewer + When User selects Waxholm atlas + Then User is taken to the latest version (v4) + + Scenario: User finds Waxholm atlas showing fine + Given User checked out waxholm atlas + When User hovers volumetric atlas + Then the label representing the voxel shows + + Scenario: User finds Waxholm atlas mesh loads fine + Given User checked out waxholm atlas + Then whole mesh loads + + + \ No newline at end of file diff --git a/features/atlas-download.feature b/features/atlas-download.feature new file mode 100644 index 000000000..3940af6dd --- /dev/null +++ b/features/atlas-download.feature @@ -0,0 +1,24 @@ +Feature: Atlas Download + + Users should be able to download atlas + + Scenario: User downloads Julich Brain v3.0.3 in MNI152 + Given User launched the atlas viewer + Given User selects Julich Brain v3.0.3 in MNI152 + When User click `[download]` button (top right of UI) + Then After a few seconds of preparation, the download should start automatically. A snack bar message should appear when it does. + + Scenario: The downloaded archive should contain the expected files + Given User downloaded Julich Brain v3.0.3 in MNI152 + Then The downloaded archive should contain: README.md, LICENSE.md, template (nii.gz + md), parcellation (nii.gz + md) + + Scenario: User downloads hOc1 left hemisphere in MNI152 + Given User launched the atlas viewer + Given User selects Julich Brain v3.0.3 in MNI152 + Given user selects hOc1 left hemisphere + When User click `[download]` button (top right of UI) + Then After a few seconds of preparation, the download should start automatically. A snack bar message should appear when it does. + + Scenario: The downloaded archive should contain the expected files (#2) + Given User downloaded hOc1 left hemisphere in MNI152 + Then the downloaded archive should contain: README.md, LICENSE.md, template (nii.gz + md), regional map (nii.gz + md) diff --git a/features/basic-ui.feature b/features/basic-ui.feature new file mode 100644 index 000000000..9f37cb905 --- /dev/null +++ b/features/basic-ui.feature @@ -0,0 +1,16 @@ +Feature: Basic UI + + User should expect basic UI to work. + + Scenario: User launches the atlas viewer + When User launches the atlas viewer + Then the user should be able to login via ebrains OIDC + + Scenario: siibra-explorer and siibra-api version is compatible + When User launches the atlas viewer + Then the user should *not* see the message `Expecting , but got , some functionalities may not work as expected` + + Scenario: User trying to find debug information + Given User launches the atlas viewer + When User navigates to `?` -> `About` + Then The git hash matches to that of [HEAD of staing](https://github.com/FZJ-INM1-BDA/siibra-explorer/commits/staging/) diff --git a/features/doc-launch-quicktour.feature b/features/doc-launch-quicktour.feature new file mode 100644 index 000000000..3cdd86029 --- /dev/null +++ b/features/doc-launch-quicktour.feature @@ -0,0 +1,12 @@ +Feature: Doc Launch and Quicktour + + From doc - Launch and Quicktour + + Scenario: Accessing quicktour on startup + Given User first launched siibra-explorer + Then User should be asked if they would like a quick tour + + Scenario: Accessing quicktour on return + Given User launched siibra-explorer second time + When User hit `?` key or clicks `(?)` button (top right) + Then User should be able to find `Quick Tour` button at the modal dialog \ No newline at end of file diff --git a/features/doc-user-interface-structure.feature b/features/doc-user-interface-structure.feature new file mode 100644 index 000000000..c9ad3afcc --- /dev/null +++ b/features/doc-user-interface-structure.feature @@ -0,0 +1,47 @@ +Feature: Doc User Interface Structure + + From doc - User Interface Structure + + Scenario: User can expand coordinate view + Given User launched the atlas viewer + Given User selects an atlas, parcellation, parcellation + When User clicks `[chevron-down]` button + Then The coordinate view expands + + Scenario: User can enter custom coordinates + Given User launched the atlas viewer + Given User selects an atlas, parcellation, parcellation + When User clicks `[pen]` button + Then User can directly enter/copy/paste/select MNI coordinates + + Scenario: User can select via atlas selection panel + Given user launched the atlas viewer + Then User should have access to atlas selection panel at the bottom of the UI + + Scenario: User can search for regions + Given User launched the atlas viewer + Given User selects an atlas, parcellation, parcellation + Given User minimized side panel + When User clicks `[magnifiying glass]` and then focus on the input box + Then User can type keywords to search for regions + + Scenario: User accesses help panel + Given User launched the atlas viewer + When User clicks the `[circled-question-mark]` button + Then A modal dialog about keyboard shortcuts, terms of use and so on should be visible + + Scenario: User downloads current view + Given User launched the atlas viewer + When User clicks the `[download]` button + Then A zip file containing current view is downloaded + + Scenario: User accesses tools + Given User launched the atlas viewer + When User clicks the `[apps]` button + Then User should see tools available to them, including screenshot, annotation, jugex + + Scenario: User signs in using ebrains credentials + Given User launched the atlas viewer + When User clicks the `[account]` button + Then User should be able to sign in with ebrains credential + \ No newline at end of file diff --git a/features/docs-exploring-parcellation-maps.feature b/features/docs-exploring-parcellation-maps.feature new file mode 100644 index 000000000..5e7f13c40 --- /dev/null +++ b/features/docs-exploring-parcellation-maps.feature @@ -0,0 +1,41 @@ +Feature: Doc Exploring 3D Parcellation Maps + + Scenario: Accessing to atlas selection panel + Given User launched siibra-explorer + Then User should have access to atlas selection panel + + Scenario: Accessing volumetric atlases + Given User launched siibra-explorer + When User selects multilevel human atlas, big brain template space + Then User should see four panel volumetric atlas viewer + + Scenario: Zooming the volumetric atlas + Given User is accessing a volumetric atlas + When User scroll the wheel + Then the view should zoom + + Scenario: Panning the volumetric atlas + Given User is accessing a volumetric atlas + When User drags with mouse + Then the view should pan + + Scenario: Oblique slicing the volumetric atlas + Given User is accessing a volumetric atlas + When User drags with mouse whilst holding shift + Then the view should rotate + + Scenario: Accessing the atlas viewer on 2D viewer + Given User is accessing a volumetric atlas + When User hovers, and clicks `[maximize]` button + Then The specific panel will maximize, with 3D view as a picture-in-picture view + + Scenario: Cycling 2D view + Given User is accessing the atlas viewer as a 2D viewer + When User hits `[space bar]` + Then The view rotates to the next orthogonal view + + Scenario: Restoring from 2D view + Given User is accessing the atlas viewer as a 2D viewer + When User clicks `[compress] button` + Then the view restores to the original four panel view + diff --git a/features/docs-multimodal-data-features.feature b/features/docs-multimodal-data-features.feature new file mode 100644 index 000000000..6d51bed7e --- /dev/null +++ b/features/docs-multimodal-data-features.feature @@ -0,0 +1,13 @@ +Feature: Doc Multimodal Data Features + + # Other modal features were tested already + + Scenario: Finding data features linked to 3D view + Given User launched siibra-explorer + Given User selected Big Brain space + Then User should find >0 spatial features + + Scenario: Inspect spatial data feature + Given User found data features linked to 3D view + When User clicks on one of the features + Then User should be shown the VOI interest, teleported to the best viewing point diff --git a/features/docs-storing-sharing-3dview.feature b/features/docs-storing-sharing-3dview.feature new file mode 100644 index 000000000..2b55f5e51 --- /dev/null +++ b/features/docs-storing-sharing-3dview.feature @@ -0,0 +1,6 @@ +Feature: Doc Storing and Sharing 3D view + + Scenario: Taking Screenshots + Given User launched siibra-explorer + When User takes a screenshot with the built in screenshot plugin + Then the screenshot should work as intended diff --git a/features/exploring-features.feature b/features/exploring-features.feature new file mode 100644 index 000000000..e31a93c0d --- /dev/null +++ b/features/exploring-features.feature @@ -0,0 +1,44 @@ +Feature: Exploring features + + User should be able to explore feature panel + + Scenario: User exploring the features related to a region of interest + Given User launched the atlas viewer + Given User selected Julich Brain v3.0.3 in ICBM 2007c nonlinear space + Given User selects a region of interest (hOc1 left hemisphere) + When User clicks the `Feature` tab + Then Feature panel of region of interest is visible + + Scenario: User checking out a feature + Given User is exploring features related to region of interest + When User expands a category, and clicks a feature + Then Feature side panel will be opened + + Scenario: User finding out more about selected feature + Given User checked out a feature + Then User can see description related to the selected feature + + Scenario: User checking out the DOI of the selected feature + Given User checked out a feature (category=molecular, feature=receptor density fingerprint) + When User clicks `DOI` button + Then A new window opens for the said DOI + + Scenario: User downloads the feature + Given User checked out a feature + When User clicks `Download` button + Then A zip file containing metadata and data is downloaded + + Scenario: User checking out feature visualization + Given User checked out a feature (category=molecular, feature="receptor density profile, 5-HT1A") + When User click the `Visualization` tab + Then The visualization of the feature (cortical profile) becomes visible + + Scenario: User checking out connectivity strength + Given User checked out a feature (category=connectivity) + When User click the `Visualization` tab + Then The probabilistic map hides, whilst connection strength is also visualized on the atlas + + Scenario: User quitting checking out connectivity strength + Given User checked out connectivity strength + When User unselects the feature + Then The connection strength color mapped atlas disapepars, whilst the probabilistic map reappears. diff --git a/features/exploring-selected-region.feature b/features/exploring-selected-region.feature new file mode 100644 index 000000000..61673ca84 --- /dev/null +++ b/features/exploring-selected-region.feature @@ -0,0 +1,28 @@ +Feature: Exploring the selected region + + User should be able to explore the selected region + + Scenario: User selecting a region of interest + Given User launched the atlas viewer + Given User selected Julich Brain v3.0.3 in ICBM 2007c nonlinear space + When User clicks a parcel + Then The clicked parcel is selected + + Scenario: User navigates to the selected region + Given User selected a region of interest + When User clicks `Centroid` button + Then The viewer navigates to the said centroid of the region + + Scenario: User finding out more about the selected region + Given User selected a region of interest + Then The user can find description about the region + + Scenario: User accesses the doi of the selected region + Given User selected a region of interest + When User clicks `DOI` button + Then A new window opens for the said DOI + + Scenario: User searches for related regions + Given User selected a region of interest (4p left, Julich Brain v3.0.3, MNI152) + When User clicks `Related Region` button + Then Related region modal shows previous/next versions, as well as homologeous regions \ No newline at end of file diff --git a/features/navigation.feature b/features/navigation.feature new file mode 100644 index 000000000..0cf56d4e4 --- /dev/null +++ b/features/navigation.feature @@ -0,0 +1,38 @@ +Feature: Navigating the viewer + + Users should be able to easily navigate the atlas + + Scenario: User launches the atlas viewer + When User launches the atlas viewer + Then The user should be directed to Human Multilevel Atlas, in MNI152 space, with Julich Brain 3.0 loaded + + Scenario: On hover shows region name(s) + Given User launched atlas viewer + Given User navigated to Human Multilevel Atals, in MNI152 space, with Julich brain 3.0 loaded + Given User has not enabled mobile view + Given The browser's width is at least 900px wide + + When User hovers over non empty voxel + Then Label representing the voxel should show as tooltip + + Scenario: User wishes to hide parcellation + Given User launched atlas viewer + When User clicks the `[eye]` icon + Then Active parcellation should hide, showing the template + + Scenario: User wishes to hide parcellation via keyboard shortcut + Given User launched atlas viewer + When User uses `q` shortcut + Then Active parcellation should hide, showing the template + + Scenario: User wishes to learn more about the active parcellation + Given User launched atlas viewer + Given User selected Julich Brain v3.0.3 + When User clicks `[info]` icon associated with the parcellation + Then User should see more information, including name, desc, doi. + + Scenario: User wishes to learn more about the active space + Given User launched atlas viewer + Given User selected ICBM 2007c nonlinear asym + When User clicks `[info]` icon associated with the space + Then User should see more information, including name, desc, doi. diff --git a/features/plugin-jugex.feature b/features/plugin-jugex.feature new file mode 100644 index 000000000..fee12f529 --- /dev/null +++ b/features/plugin-jugex.feature @@ -0,0 +1,44 @@ +Feature: Plugin jugex + + Plugin jugex should work fine. + + Scenario: User can launch jugex plugin + Given User launched the atlas viewer + Given User selects an human atlas, Julich Brain v3.0.3 parcellation, MNI152 template + When User expands the `[App]` icon at top right, and clicks siibra-jugex + Then siibra-jugex should launch as a plugin + + Scenario: User can select ROI via typing + Given User launched jugex plugin + When User focus on `Select ROI 1` and type `fp1` + Then A list of suggestions should be populated and shown to the user. They can be selected either via left click, or `Enter` key (selecting the first in the list). + + Scenario: User can select ROI via scanning atlas + Given User selected ROI1 via typing + When User clicks `[radar]` button next to `Select ROI 2` text box + Then User should be prompted to `Select a region`. Single click on any region will select the region. + + Scenario: User can select genes of interest by typing + Given User selected ROI2 via scanning atlas + When User focuses selected genes, and starts typing (e.g. `GABA`) + Then A list of suggestions should be populated and shown to the user. They can be selected either via left click, or `Enter` key (selecting the first in the list). + + Scenario: User can export notebook + Given User selected gene(s) of interest + When User clicks `notebook` button + Then A new window should be opened, allowing the user to download the notebook, running it on ebrains labs or my binder + + Scenario: User can run the analysis + Given User selected gene(s) of interest + When User clicks `run` button + Then Jugex analysis should be run based on the user's specification + + Scenario: Analysis should be downloadable + Given User ran the analysis + When User clicks the `Save` Button + Then A result.json file should be downloaded + + Scenario: Result can be visualized on the atlas viewer + Given User ran the analysis + When User toggles `Annotate` + Then the sample sites should be visible in the atlas viewer diff --git a/features/point-assignment.feature b/features/point-assignment.feature new file mode 100644 index 000000000..e37e72419 --- /dev/null +++ b/features/point-assignment.feature @@ -0,0 +1,28 @@ +Feature: Point assignment + + Users should expect point assignment to work as expected + + Scenario: User performs point assignment on Julich Brain v3.0.3 in MNI152 space + Given User launched the atlas viewer + Given User selects Julich Brain v3.0.3 in MNI152 + When User right clicks on any voxel on the viewer, and clicks `x, y, z (mm) Point` + Then User should see statistical assignment of the point, sorted by `map value` + + Scenario: User inspects the full table of the point assignment + Given User performed point assignment on Julich Brain v3.0.3 in MNI152 space + When User clicks `Show full assignment` button + Then User should see a full assignment table in a modal dialog + + Scenario: User wishes to download the assignment + Given User is inspecting the full table of point assignment + When User clicks `Download CSV` button + Then A CSV containing the data should be downloaded + + Scenario: user performs point assignment on Waxholm v4 + Given User launched the atlas viewer + Given User selects Waxholm atlas, v4 parcellation + When User right clicks on any voxel on the viewer, and clicks `x, y, z (mm) Point` + Then User should see labelled assignment of the point + + Scenario: User performs point assignment on fsaverage + Scenario: User performs point assignment on Julich Brain in Big Brain diff --git a/features/region-hierarchy.feature b/features/region-hierarchy.feature new file mode 100644 index 000000000..7a121cb07 --- /dev/null +++ b/features/region-hierarchy.feature @@ -0,0 +1,19 @@ +Feature: Explore region hierarchy + + User should be able to explore region hierarchy + + Scenario: User exploring region hierarchy + Given User launched the atlas viewer + Given User selected Julich Brain v3.0.3 in ICBM 2007c nonlinear space + When User clicks `[site-map]` icon + Then The full hierarchy modal view should be shown + + Scenario: User are given search context + Given User are exploring region hierarchy + Then User should see the context of region hierachy, including atlas, parcellation and template + + Scenario: User searches for branch + Given User are exploring region hierarchy + When User searches for `frontal lobe` + Then User should see the parent (cerebral cortex), the branch itself (frontal lobe) and its children (e.g. inferior frontal sulcus). + \ No newline at end of file diff --git a/features/saneurl.feature b/features/saneurl.feature new file mode 100644 index 000000000..b7082b530 --- /dev/null +++ b/features/saneurl.feature @@ -0,0 +1,80 @@ +Feature: SaneURL a.k.a. URL shortener + + SaneURL should continue to function. + + Scenario: User navigates to SaneURL UI + Given User launched the atlas viewer + Given User selects an atlas, parcellation, parcellation + When User expands navigation submenu, clicks `[share]` button, clicks `Create custom URL` button + Then User should see the SaneURL UI + + Scenario: SaneURL UI should be informative + Given User navigated to SaneURL UI + Then User should see that links expire if they are not loggedin + Then User should see the links they potentially generate (e.g. `https://atlases.ebrains.eu/viewer-staging/go/`) + + Scenario: User attempts to generate existing saneurl + Given User navigated to SaneURL UI + When User enters `human` + Then User should be informed that the shortlink is not available + + Scenario: User attempts to use illegal characters + Given User navigated to SaneURL UI + When User enters `foo:bar` + Then User should be informed that the shortlink is not legal + + Scenario: User attempts to generate valid saneurl + Given User navigated to SaneURL UI + When User enters `x_tmp_foo` + Then User should be informed that the shortlink is available + + Scenario: User generates a valid saneurl + Given User navigated to SaneURL UI + When User enters `x_tmp_foo` and clicks `Create` + Then The short link will be created. User will be informed, and given the option to copy the generated shortlink + + Scenario: Generated shortlink works + Given User generated a valid saneurl + When User enters the said saneURL to a browser + Then They should be taken to where the shortlink is generated + + Scenario: Permalink works (Big Brain) + Given + Then Works + + Scenario: Permalink works (Juclih Brain in Colin 27) + Given + Then Works + + Scenario: Permalink works (Waxholm v4) + Given + Then Works + + Scenario: Permalink works (Allen CCFv3) + Given + Then Works + + Scenario: Permalink works (MEBRAINS) + Given + Then Works + + Scenario: Permalink works (contains annotations) + Given + Then Works + + + Scenario: VIP link works (human) + Given + Then Works + + Scenario: VIP link works (monkey) + Given + Then Works + + Scenario: VIP link works (rat) + Given + Then Works + + Scenario: VIP link works (mouse) + Given + Then Works diff --git a/features/selecting-region.feature b/features/selecting-region.feature new file mode 100644 index 000000000..5955d86ca --- /dev/null +++ b/features/selecting-region.feature @@ -0,0 +1,28 @@ +Feature: Selecting a region + + User should be able to select a region + + Scenario: User selecting a region of interest + Given User launched the atlas viewer + Given User selected Julich Brain v3.0.3 in ICBM 2007c nonlinear space + When User clicks a parcel + Then The clicked parcel is selected + + Scenario: User searching for a region of interest via express search + Given User launched the atlas viewer + Given User selected Julich Brain v3.0.3 in ICBM 2007c nonlinear space + When User focuses on `Search for regions` and types `hoc1` + Then Three options (hoc1 parent, hoc1 left and hoc1 right) should be shown, in this order + + Scenario: User selecting branch via express search + Given User searched for `hoc1` via express search + Given Three options (hoc1 parent, hoc1 left and hoc1 right) are shown, in this order + When User hits `Enter` key + Then Full hierarchy modal view should be shown, with the term `Area hOc1 (V1, 17, CalcS)` populated in the search field + + Scenario: User selecting node via express search + Given User searched for `hoc1` via express search + Given Three options (hoc1 parent, hoc1 left and hoc1 right) are shown, in this order + When User clicks `hoc1 left` + Then The region `hoc1 left` should be selected + diff --git a/features/switching-selection.feature b/features/switching-selection.feature new file mode 100644 index 000000000..a08c6f6c3 --- /dev/null +++ b/features/switching-selection.feature @@ -0,0 +1,17 @@ +Feature: Switching Atlas, Parcellation, Template + + User should be able to freely switch atlas, parcellation and templates + + Scenario: User switches template + Given User launched the atlas viewer + Given User selected Julich Brain v3.0.3 in ICBM 2007c nonlinear space + When User selects `Big Brain` template + Then User should be taken to Julich Brain v2.9 in Big Brain space + + Scenario: User switches parcellation + Given User launched the atlas viewer + Given User selected Julich Brain v2.9 in Big Brain space + When User selects `Deep fibre bundle` parcellation + Then User should be taken to `Deep fibre bundle` in MNI152 space + + \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e634fcee7..6d2a03c78 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,10 @@ markdown_extensions: - mdx_truly_sane_lists - attr_list - toc + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg plugins: - search diff --git a/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.style.css b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.style.css index 0a7e4fcd6..4657360c4 100644 --- a/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.style.css +++ b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.style.css @@ -6,7 +6,12 @@ width: 100%; } +:host > h4 +{ + margin: 2rem; +} + mat-card { - margin: 5rem; + margin: 0.5rem; } \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.template.html b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.template.html index a68d5bdc6..983678081 100644 --- a/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.template.html +++ b/src/atlasComponents/sapiViews/core/atlas/splashScreen/splashScreen.template.html @@ -1,17 +1,13 @@ - +

+ siibra-explorer +

+ + -
- siibra-explorer -
+ {{ atlas.name }}
- - -
diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 0150d953b..8b44331d9 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -111,7 +111,13 @@ + [plotly-json]="plotly"> + + + + + diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index 987e5ba2c..4c9adcbfe 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -1,15 +1,12 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges } from '@angular/core'; import { BehaviorSubject, EMPTY, Observable, Subject, combineLatest, concat, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; import { Feature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; import { DARKTHEME } from 'src/util/injectionTokens'; import { isVoiData, notQuiteRight } from "../guards" -import { Store, select } from '@ngrx/store'; -import { atlasAppearance, atlasSelection } from 'src/state'; - - -const CONNECTIVITY_LAYER_ID = "connectivity-colormap-id" +import { Action, Store, select } from '@ngrx/store'; +import { atlasSelection } from 'src/state'; @Component({ selector: 'sxplr-feature-view', @@ -17,9 +14,7 @@ const CONNECTIVITY_LAYER_ID = "connectivity-colormap-id" styleUrls: ['./feature-view.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class FeatureViewComponent implements OnChanges, OnDestroy { - - #cleanupCb: (() => void)[] = [] +export class FeatureViewComponent implements OnChanges { @Input() feature: Feature @@ -31,10 +26,6 @@ export class FeatureViewComponent implements OnChanges, OnDestroy { select(atlasSelection.selectors.selectedRegions) ) - #allRegions$ = this.store.pipe( - select(atlasSelection.selectors.selectedParcAllRegions) - ) - #additionalParams$: Observable> = this.#isConnectivity$.pipe( withLatestFrom(this.#selectedRegion$), map(([ isConnnectivity, selectedRegions ]) => isConnnectivity @@ -103,74 +94,34 @@ export class FeatureViewComponent implements OnChanges, OnDestroy { warnings$ = new Subject() + intents$ = this.#isConnectivity$.pipe( + withLatestFrom(this.#featureId, this.#selectedRegion$), + switchMap(([flag, fid, selectedRegion]) => { + console.log(flag, fid, selectedRegion) + if (!flag) { + return EMPTY + } + return this.sapi.getFeatureIntents(fid, { + region: selectedRegion.map(r => r.name).join(" ") + }).pipe( + switchMap(val => + this.sapi.iteratePages( + val, + page => this.sapi.getFeatureIntents(fid, { + region: selectedRegion.map(r => r.name).join(" "), + page: page.toString() + } + ) + )) + ) + }) + ) + constructor( private sapi: SAPI, private store: Store, @Inject(DARKTHEME) public darktheme$: Observable, ) { - const sub = this.#isConnectivity$.pipe( - withLatestFrom(this.#featureId, this.#selectedRegion$, this.#allRegions$), - switchMap(([flag, fid, selelectedRegion, allRegions]) => { - if (!flag) { - return EMPTY - } - return this.sapi.getFeatureIntents(fid, { - region: selelectedRegion.map(r => r.name).join(" ") - }).pipe( - map(pagedIntents => { - const foundCm = pagedIntents.items.find(intent => intent['@type'] === "siibra-0.4/intent/colorization") - if (!foundCm) { - return null - } - const { region_mappings: regionMappings } = foundCm - const regRgbTuple = regionMappings - .map(({ region, rgb }) => { - const foundRegion = allRegions.find(r => r.name === region.name) - if (!foundRegion) { - return null - } - return [foundRegion, rgb] as const - }) - .filter(v => !!v) - - const newMap = new Map(regRgbTuple) - return newMap - }), - ) - }), - ).subscribe(newCM => { - if (!newCM) { - this.store.dispatch( - atlasAppearance.actions.removeCustomLayer({ - id: CONNECTIVITY_LAYER_ID - }) - ) - return - } - - this.store.dispatch( - atlasAppearance.actions.addCustomLayer({ - customLayer: { - clType: 'customlayer/colormap', - id: CONNECTIVITY_LAYER_ID, - colormap: newCM - } - }) - ) - }) - - this.#cleanupCb.push(() => sub.unsubscribe()) - } - - ngOnDestroy(): void { - while (this.#cleanupCb.length > 0) { - this.#cleanupCb.pop()() - } - this.store.dispatch( - atlasAppearance.actions.removeCustomLayer({ - id: CONNECTIVITY_LAYER_ID - }) - ) } ngOnChanges(): void { @@ -210,4 +161,8 @@ export class FeatureViewComponent implements OnChanges, OnDestroy { }) ) } + + onAction(action: Action){ + this.store.dispatch(action) + } } diff --git a/src/features/intents/intents.component.ts b/src/features/intents/intents.component.ts new file mode 100644 index 000000000..f6bf9da53 --- /dev/null +++ b/src/features/intents/intents.component.ts @@ -0,0 +1,88 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input, Output, inject } from "@angular/core"; +import { Store, select } from "@ngrx/store"; +import { BehaviorSubject, EMPTY, merge, of } from "rxjs"; +import { map, switchMap, withLatestFrom } from "rxjs/operators"; +import { PathReturn } from "src/atlasComponents/sapi/typeV3"; +import { AngularMaterialModule } from "src/sharedModules"; +import { atlasAppearance, atlasSelection } from "src/state"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; + +const CONNECTIVITY_LAYER_ID = "connectivity-colormap-id" + +type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] + +@Component({ + selector: 'feature-intents', + templateUrl: './intents.template.html', + styleUrls: [ + './intents.style.css' + ], + standalone: true, + imports: [ + CommonModule, + AngularMaterialModule, + ], + hostDirectives: [ + DestroyDirective + ] +}) +export class FeatureIntents{ + + readonly #destory$ = inject(DestroyDirective).destroyed$ + #intents$ = new BehaviorSubject([]) + + @Input() + set intents(val: Intent[]){ + this.#intents$.next(val) + } + + @Output() + actions = merge( + merge( + this.#destory$, + this.#intents$ + ).pipe( + map(() => atlasAppearance.actions.removeCustomLayer({ + id: CONNECTIVITY_LAYER_ID + })) + ), + this.#intents$.pipe( + withLatestFrom( + this.store.pipe( + select(atlasSelection.selectors.selectedParcAllRegions) + ) + ), + switchMap(([ intents, allRegions ]) => { + const foundCm = (intents || []).find(intent => intent['@type'] === "siibra-0.4/intent/colorization") + if (!foundCm) { + return EMPTY + } + + const { region_mappings: regionMappings } = foundCm + const regRgbTuple = regionMappings + .map(({ region, rgb }) => { + const foundRegion = allRegions.find(r => r.name === region.name) + if (!foundRegion) { + return null + } + return [foundRegion, rgb] as const + }) + .filter(v => !!v) + + const newMap = new Map(regRgbTuple) + return of( + atlasAppearance.actions.addCustomLayer({ + customLayer: { + clType: 'customlayer/colormap', + id: CONNECTIVITY_LAYER_ID, + colormap: newMap + } + }) + ) + }) + ) + ) + + constructor(private store: Store){ } +} diff --git a/src/features/intents/intents.style.css b/src/features/intents/intents.style.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/features/intents/intents.template.html b/src/features/intents/intents.template.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/features/module.ts b/src/features/module.ts index eae389719..f41e5319b 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -19,6 +19,7 @@ import { ReadmoreModule } from "src/components/readmore"; import { GroupFeatureTallyPipe } from "./grpFeatToTotal.pipe"; import { PlotlyComponent } from "./plotly"; import { AngularMaterialModule } from "src/sharedModules"; +import { FeatureIntents } from "./intents/intents.component"; @NgModule({ imports: [ @@ -36,6 +37,7 @@ import { AngularMaterialModule } from "src/sharedModules"; * standalone components */ PlotlyComponent, + FeatureIntents, ], declarations: [ EntryComponent, From bd939c3449967d5e828e6f610d97d41e8486f0a3 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 23 Jan 2024 16:05:48 +0100 Subject: [PATCH 08/48] fix: github action --- .github/workflows/manual_e2e.yml | 7 +++++++ features/_convert.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/manual_e2e.yml b/.github/workflows/manual_e2e.yml index a55bd1e16..8fddfeb8f 100644 --- a/.github/workflows/manual_e2e.yml +++ b/.github/workflows/manual_e2e.yml @@ -24,6 +24,13 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: 'Install gherkin-official' + run: 'pip install gherkin-official' + - name: 'Generate checklist' + run: 'python features/_convert.py' - name: 'Add checklist comment' uses: actions/github-script@v5 with: diff --git a/features/_convert.py b/features/_convert.py index 3dc64092e..d54b997f6 100644 --- a/features/_convert.py +++ b/features/_convert.py @@ -122,4 +122,4 @@ def main(output: str="./e2e/checklist.md"): if __name__ == "__main__": - main(*sys.argv[1:]) \ No newline at end of file + main(*sys.argv[1:]) From 876d64975408c944cfcd95f11d7c9a94c608b079 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 23 Jan 2024 17:40:29 +0100 Subject: [PATCH 09/48] feat: remove dups in feature --- src/features/category-acc.directive.ts | 5 +++-- src/util/pullable.ts | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts index a6f34d27d..40fc6c904 100644 --- a/src/features/category-acc.directive.ts +++ b/src/features/category-acc.directive.ts @@ -106,7 +106,8 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { if (filteredListCmps.length === 0) { return of( new ParentDatasource({ - children: [] as PulledDataSource[] + children: [] as PulledDataSource[], + serialize: f => f.id }) ) } @@ -114,7 +115,7 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy { filteredListCmps.map(cmp => cmp.datasource$) ).pipe( map(dss => { - this.datasource = new ParentDatasource({ children: dss }) + this.datasource = new ParentDatasource({ children: dss, serialize: f => f.id }) return this.datasource }), ) diff --git a/src/util/pullable.ts b/src/util/pullable.ts index 02bf6dc44..c6e20f526 100644 --- a/src/util/pullable.ts +++ b/src/util/pullable.ts @@ -12,6 +12,7 @@ interface PaginatedArg { pull?: () => Promise children?: PulledDataSource[] annotations?: Record + serialize?: (a: T) => string } export class IsAlreadyPulling extends Error {} @@ -115,20 +116,39 @@ export class ParentDatasource extends PulledDataSource { private _data$ = new BehaviorSubject([]) data$ = this._data$.pipe( shareReplay(1), + map(v => { + if (!this.#serialize) { + return v + } + const seen = new Set() + const returnVal: T[] = [] + for (const item of v){ + const key = this.#serialize(item) + const hasSeen = seen.has(key) + if (!hasSeen) { + returnVal.push(item) + } + seen.add(key) + } + return returnVal + }) ) + + #serialize: (a: T) => string #subscriptions: Subscription[] = [] _children: PulledDataSource[] = [] constructor(arg: PaginatedArg){ super({ pull: async () => [], annotations: arg.annotations }) - const { children } = arg + const { children, serialize } = arg this._children = children + this.#serialize = serialize } set isPulling(val: boolean){ throw new Error(`Cannot set isPulling for parent pullable`) } - get isPUlling(){ + get isPulling(){ return this._children.some(c => c.isPulling) } From 6feb12f35e3c08e82354ed83cf4e081a04fb3088 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 24 Jan 2024 16:55:22 +0100 Subject: [PATCH 10/48] rf: feature-view maint: update sapi schema --- src/atlasComponents/sapi/openapi.json | 127 +++++++++++++ src/atlasComponents/sapi/schemaV3.ts | 59 ++++++ src/atlasComponents/sapi/translateV3.ts | 5 + .../feature-view/feature-view.component.html | 63 +++---- .../feature-view/feature-view.component.ts | 171 +++++++++++++----- src/features/guards.ts | 2 + 6 files changed, 339 insertions(+), 88 deletions(-) diff --git a/src/atlasComponents/sapi/openapi.json b/src/atlasComponents/sapi/openapi.json index ac177af53..fae0512bd 100644 --- a/src/atlasComponents/sapi/openapi.json +++ b/src/atlasComponents/sapi/openapi.json @@ -2366,6 +2366,72 @@ } } } + }, + "/vocabularies/genes": { + "get": { + "tags": [ + "vocabularies" + ], + "summary": "Get Genes", + "description": "HTTP get (filtered) genes", + "operationId": "get_genes_vocabularies_genes_get", + "parameters": [ + { + "required": false, + "schema": { + "type": "string", + "title": "Find" + }, + "name": "find", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "title": "Page", + "default": 1 + }, + "name": "page", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "title": "Size", + "default": 50 + }, + "name": "size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_GeneModel_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { @@ -3179,6 +3245,30 @@ "title": "FeatureMetaModel", "description": "Meta feature type" }, + "GeneModel": { + "properties": { + "@type": { + "type": "string", + "title": "@Type" + }, + "symbol": { + "type": "string", + "title": "Symbol" + }, + "description": { + "type": "string", + "title": "Description" + } + }, + "type": "object", + "required": [ + "@type", + "symbol", + "description" + ], + "title": "GeneModel", + "description": "ConfigBaseModel" + }, "HTTPValidationError": { "properties": { "detail": { @@ -3575,6 +3665,43 @@ ], "title": "Page[FeatureMetaModel]" }, + "Page_GeneModel_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/GeneModel" + }, + "type": "array", + "title": "Items" + }, + "total": { + "type": "integer", + "minimum": 0, + "title": "Total" + }, + "page": { + "type": "integer", + "minimum": 1, + "title": "Page" + }, + "size": { + "type": "integer", + "minimum": 1, + "title": "Size" + }, + "pages": { + "type": "integer", + "minimum": 0, + "title": "Pages" + } + }, + "type": "object", + "required": [ + "items", + "total" + ], + "title": "Page[GeneModel]" + }, "Page_ParcellationEntityVersionModel_": { "properties": { "items": { diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index c29243103..9477b64af 100644 --- a/src/atlasComponents/sapi/schemaV3.ts +++ b/src/atlasComponents/sapi/schemaV3.ts @@ -268,6 +268,13 @@ export interface paths { */ get: operations["get_single_feature_feature__feature_id__get"] } + "/vocabularies/genes": { + /** + * Get Genes + * @description HTTP get (filtered) genes + */ + get: operations["get_genes_vocabularies_genes_get"] + } } export type webhooks = Record; @@ -745,6 +752,18 @@ export interface components { /** Category */ category?: string } + /** + * GeneModel + * @description ConfigBaseModel + */ + GeneModel: { + /** @Type */ + "@type": string + /** Symbol */ + symbol: string + /** Description */ + description: string + } /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -942,6 +961,19 @@ export interface components { /** Pages */ pages?: number } + /** Page[GeneModel] */ + Page_GeneModel_: { + /** Items */ + items: (components["schemas"]["GeneModel"])[] + /** Total */ + total: number + /** Page */ + page?: number + /** Size */ + size?: number + /** Pages */ + pages?: number + } /** Page[ParcellationEntityVersionModel] */ Page_ParcellationEntityVersionModel_: { /** Items */ @@ -2670,4 +2702,31 @@ export interface operations { } } } + get_genes_vocabularies_genes_get: { + /** + * Get Genes + * @description HTTP get (filtered) genes + */ + parameters?: { + query?: { + find?: string + page?: number + size?: number + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Page_GeneModel_"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } } diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index 48be01723..ad2fecc3a 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -669,6 +669,11 @@ class TranslateV3 { async translateBaseFeature(feat: PathReturn<"/feature/{feature_id}">): Promise{ const { id, name, category, description, datasets } = feat + if (!datasets) { + return { + id, name, category + } + } const dsDescs = datasets.map(ds => ds.description) const urls = datasets.flatMap(ds => ds.urls).map(v => ({ href: v.url, diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 8b44331d9..69a62e424 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -2,18 +2,9 @@ - - - - - Feature not specified. - - - - - - + + + @@ -21,28 +12,21 @@ - - - - {{ feature.category }} feature - - - Other feature - - - + + {{ view.category }} +
- {{ feature.name }} + {{ view.name }}
- + @@ -51,17 +35,15 @@ - - - - + + - +
{{ url.text || url.href }}
@@ -69,29 +51,26 @@
- +
{{ url }}
- - - -
Download
-
-
+ + +
Download
+
-
- + - + (null) @Input() - feature: Feature + set feature(val: Feature) { + this.#feature$.next(val) + } + + #featureId = this.#feature$.pipe( + map(f => f.id) + ) + + #featureDetail$ = this.#feature$.pipe( + switchMap(f => this.sapi.getV3FeatureDetailWithId(f.id)), + shareReplay(1), + ) + + + #featureDesc$ = this.#feature$.pipe( + switchMap(() => concat( + of(null as string), + this.#featureDetail$.pipe( + map(v => v.desc) + ) + )) + ) - #featureId = new BehaviorSubject(null) - #isConnectivity$ = new BehaviorSubject(false) + #voi$ = this.#feature$.pipe( + switchMap(() => concat( + of(null as VoiFeature), + this.#featureDetail$.pipe( + map(val => { + if (isVoiData(val)) { + return val + } + return null as VoiFeature + }) + ) + )) + ) + + #warnings$ = this.#feature$.pipe( + switchMap(() => concat( + of([] as string[]), + this.#featureDetail$.pipe( + map(notQuiteRight) + ) + )) + ) + #isConnectivity$ = this.#feature$.pipe( + map(v => v.category === "connectivity") + ) #selectedRegion$ = this.store.pipe( select(atlasSelection.selectors.selectedRegions) @@ -43,15 +88,23 @@ export class FeatureViewComponent implements OnChanges { distinctUntilChanged((o, n) => o.id === n.id && o.darktheme === n.darktheme), shareReplay(1), ) + + #loadingDetail$ = this.#feature$.pipe( + switchMap(() => concat( + of(true), + this.#featureDetail$.pipe( + map(() => false) + ) + )) + ) - loadingPlotly$ = this.#plotlyInput$.pipe( + #loadingPlotly$ = this.#plotlyInput$.pipe( switchMap(() => concat( of(true), this.plotly$.pipe( map(() => false) ) )), - distinctUntilChanged() ) plotly$ = this.#plotlyInput$.pipe( @@ -71,12 +124,21 @@ export class FeatureViewComponent implements OnChanges { }), shareReplay(1), ) + + #detailLinks = this.#feature$.pipe( + switchMap(() => concat( + of([] as string[]), + this.#featureDetail$.pipe( + map(val => (val.link || []).map(l => l.href)) + ) + )) + ) - #detailLinks = new Subject() additionalLinks$ = this.#detailLinks.pipe( distinctUntilChanged((o, n) => o.length == n.length), - map(links => { - const set = new Set((this.feature.link || []).map(v => v.href)) + withLatestFrom(this.#feature$), + map(([links, feature]) => { + const set = new Set((feature.link || []).map(v => v.href)) return links.filter(l => !set.has(l)) }) ) @@ -88,16 +150,9 @@ export class FeatureViewComponent implements OnChanges { )) ) - busy$ = new BehaviorSubject(false) - - voi$ = new BehaviorSubject(null) - - warnings$ = new Subject() - intents$ = this.#isConnectivity$.pipe( withLatestFrom(this.#featureId, this.#selectedRegion$), switchMap(([flag, fid, selectedRegion]) => { - console.log(flag, fid, selectedRegion) if (!flag) { return EMPTY } @@ -124,34 +179,6 @@ export class FeatureViewComponent implements OnChanges { ) { } - ngOnChanges(): void { - this.voi$.next(null) - this.busy$.next(true) - - this.#featureId.next(this.feature.id) - - // TODO might actually not be right for bold - this.#isConnectivity$.next(this.feature.category === "connectivity") - - this.sapi.getV3FeatureDetailWithId(this.feature.id).subscribe( - val => { - this.busy$.next(false) - - if (isVoiData(val)) { - this.voi$.next(val) - } - - this.warnings$.next( - notQuiteRight(val) - ) - - this.#detailLinks.next((val.link || []).map(l => l.href)) - - }, - () => this.busy$.next(false) - ) - } - navigateToRegionByName(regionName: string){ this.store.dispatch( atlasSelection.actions.navigateToRegion({ @@ -165,4 +192,56 @@ export class FeatureViewComponent implements OnChanges { onAction(action: Action){ this.store.dispatch(action) } + + specialView$ = combineLatest([ + this.#voi$, + this.plotly$ + ]).pipe( + map(([ voi, plotly ]) => { + return { + voi, plotly + } + }) + ) + + baseView$ = combineLatest([ + this.#feature$, + combineLatest([ + this.#loadingDetail$, + this.#loadingPlotly$ + ]).pipe( + map(flags => flags.some(f => f)) + ), + this.#warnings$, + this.additionalLinks$, + this.downloadLink$, + this.#featureDesc$ + ]).pipe( + map(([ feature, busy, warnings, additionalLinks, downloadLink, desc ]) => { + return { + name: feature.name, + links: feature.link, + category: feature.category === 'Unknown category' + ? `Other feature` + : `${feature.category} feature`, + busy, + warnings, + additionalLinks, + downloadLink, + desc + } + }) + ) + + view$ = combineLatest([ + this.baseView$, + this.specialView$ + ]).pipe( + map(([obj1, obj2]) => { + return { + ...obj1, + ...obj2, + } + }) + ) } diff --git a/src/features/guards.ts b/src/features/guards.ts index 57f1c0d3d..426d94acf 100644 --- a/src/features/guards.ts +++ b/src/features/guards.ts @@ -1,5 +1,7 @@ import { VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" +export { VoiFeature } + export function isVoiData(feature: unknown): feature is VoiFeature { return !!feature['bbox'] } From b96474c0799960255c7fef28d312046d23cda113 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 25 Jan 2024 09:34:25 +0100 Subject: [PATCH 11/48] fix: showing colormap, blank feature screen --- .../feature-view/feature-view.component.ts | 19 +++++++++++-------- src/features/intents/intents.component.ts | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index 12bf00883..4e6fee75e 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -112,14 +112,17 @@ export class FeatureViewComponent { if (!id) { return of(null) } - return this.sapi.getFeaturePlot( - id, - { - template: darktheme ? 'plotly_dark' : 'plotly_white', - ...additionalParams - } - ).pipe( - catchError(() => of(null)) + return concat( + of(null), + this.sapi.getFeaturePlot( + id, + { + template: darktheme ? 'plotly_dark' : 'plotly_white', + ...additionalParams + } + ).pipe( + catchError(() => of(null)) + ) ) }), shareReplay(1), diff --git a/src/features/intents/intents.component.ts b/src/features/intents/intents.component.ts index f6bf9da53..6ccab2838 100644 --- a/src/features/intents/intents.component.ts +++ b/src/features/intents/intents.component.ts @@ -54,7 +54,7 @@ export class FeatureIntents{ ) ), switchMap(([ intents, allRegions ]) => { - const foundCm = (intents || []).find(intent => intent['@type'] === "siibra-0.4/intent/colorization") + const foundCm = (intents || []).find(intent => intent['@type'].includes("intent/colorization")) if (!foundCm) { return EMPTY } From 51bb8091bb8d4855d73f5cfcbdd17f1457f93c1c Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 30 Jan 2024 08:40:19 +0100 Subject: [PATCH 12/48] fix: feature loading flag --- .../feature-view/feature-view.component.html | 2 +- .../feature-view/feature-view.component.ts | 50 +++++++++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 69a62e424..5098a05a3 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -85,7 +85,7 @@ - + @Component({ selector: 'sxplr-feature-view', @@ -41,15 +44,15 @@ export class FeatureViewComponent { )) ) - #voi$ = this.#feature$.pipe( + #voi$: Observable = this.#feature$.pipe( switchMap(() => concat( - of(null as VoiFeature), + of(null), this.#featureDetail$.pipe( map(val => { if (isVoiData(val)) { return val } - return null as VoiFeature + return null }) ) )) @@ -101,28 +104,25 @@ export class FeatureViewComponent { #loadingPlotly$ = this.#plotlyInput$.pipe( switchMap(() => concat( of(true), - this.plotly$.pipe( + this.#plotly$.pipe( map(() => false) ) )), ) - plotly$ = this.#plotlyInput$.pipe( + #plotly$: Observable = this.#plotlyInput$.pipe( switchMap(({ id, darktheme, additionalParams }) => { if (!id) { return of(null) } - return concat( - of(null), - this.sapi.getFeaturePlot( - id, - { - template: darktheme ? 'plotly_dark' : 'plotly_white', - ...additionalParams - } - ).pipe( - catchError(() => of(null)) - ) + return this.sapi.getFeaturePlot( + id, + { + template: darktheme ? 'plotly_dark' : 'plotly_white', + ...additionalParams + } + ).pipe( + catchError(() => of(null)) ) }), shareReplay(1), @@ -196,9 +196,15 @@ export class FeatureViewComponent { this.store.dispatch(action) } - specialView$ = combineLatest([ - this.#voi$, - this.plotly$ + #specialView$ = combineLatest([ + concat( + of(null as VoiFeature), + this.#voi$ + ), + concat( + of(null as PlotlyResponse), + this.#plotly$, + ) ]).pipe( map(([ voi, plotly ]) => { return { @@ -207,7 +213,7 @@ export class FeatureViewComponent { }) ) - baseView$ = combineLatest([ + #baseView$ = combineLatest([ this.#feature$, combineLatest([ this.#loadingDetail$, @@ -237,8 +243,8 @@ export class FeatureViewComponent { ) view$ = combineLatest([ - this.baseView$, - this.specialView$ + this.#baseView$, + this.#specialView$ ]).pipe( map(([obj1, obj2]) => { return { From 712e85fcadd6528121453baceb262787642c7c81 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 30 Jan 2024 12:33:09 +0100 Subject: [PATCH 13/48] fix: saneurl root url --- features/_convert.py | 2 +- src/environments/environment.common.ts | 3 ++- src/share/saneUrl/saneUrl.service.ts | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/features/_convert.py b/features/_convert.py index d54b997f6..ecd36cac4 100644 --- a/features/_convert.py +++ b/features/_convert.py @@ -87,7 +87,7 @@ def gherkin_to_markdown(gherkin_text): ) return ( - """
""" + """
""" + f"""{feature_name}""" + "\n\n" + '\n\n'.join(ret_text) diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 6f2a371d3..1fb19a6a6 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -4,11 +4,12 @@ export const environment = { VERSION: 'unknown version', PRODUCTION: false, BACKEND_URL: null, - // N.B. do not update the SIIBRA_API_ENDPOITNS directly + // N.B. do not update the SIIBRA_API_ENDPOINTS directly // some libraries rely on the exact string formatting to work properly SIIBRA_API_ENDPOINTS: // 'http://localhost:10081/v3_0', // endpoint-local-10081 // 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //endpoint-latest + // 'https://siibra-api-rc.apps.hbp.eu/v3_0', // endpoint-rc 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, diff --git a/src/share/saneUrl/saneUrl.service.ts b/src/share/saneUrl/saneUrl.service.ts index 0e35a1f08..6fc94492d 100644 --- a/src/share/saneUrl/saneUrl.service.ts +++ b/src/share/saneUrl/saneUrl.service.ts @@ -9,8 +9,19 @@ import { environment } from "src/environments/environment"; providedIn: 'root' }) -export class SaneUrlSvc implements IKeyValStore{ - public saneUrlRoot = `${environment.BACKEND_URL || ''}go/` +export class SaneUrlSvc implements IKeyValStore { + + #backendUrl = (() => { + if (environment.BACKEND_URL) { + return environment.BACKEND_URL.replace(/\/$/, '') + } + const url = new URL(window.location.href) + const { protocol, hostname, pathname } = url + return `${protocol}//${hostname}${pathname.replace(/\/$/, '')}` + })() + + public saneUrlRoot = `${this.#backendUrl}/go/` + constructor( private http: HttpClient ){ From 44a85c74e3beeff00c5748d2f8a6f2b54dd44ae8 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 1 Feb 2024 11:30:14 +0100 Subject: [PATCH 14/48] feat: reworked point assignment UI --- docs/releases/v2.14.5.md | 1 + .../point-assignment.component.html | 53 ++++++++++++++----- .../point-assignment.component.ts | 15 +++++- .../nehuba/statusCard/statusCard.component.ts | 13 ++--- .../statusCard/statusCard.template.html | 8 +-- 5 files changed, 60 insertions(+), 30 deletions(-) diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index 05afa96fb..2d040622b 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -5,6 +5,7 @@ - Add support for compound feature - Added documentation for URL encoding - Improved documentation for plugin API +- Reworded point assignment UI ## Behind the Scenes diff --git a/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html b/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html index 0e72e5a76..b77d79655 100644 --- a/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html +++ b/src/atlasComponents/sapiViews/volumes/point-assignment/point-assignment.component.html @@ -1,12 +1,45 @@ - - - + + + + + + + - + + + + +
@@ -24,12 +57,6 @@ - - df.columns as string[]) ) - constructor(private sapi: SAPI, private dialog: MatDialog, private store: Store) {} + constructor(private sapi: SAPI, private dialog: MatDialog, + private store: Store, + private clipboard: Clipboard, + private snackbar: MatSnackBar) {} #dialogRef: MatDialogRef openDialog(tmpl: TemplateRef){ @@ -163,6 +166,14 @@ export class PointAssignmentComponent implements OnDestroy { }) ) } + + copyCoord(coord: number[]){ + const strToCopy = coord.map(v => `${v.toFixed(2)}mm`).join(', ') + this.clipboard.copy(strToCopy) + this.snackbar.open(`Copied to clipboard`, 'Dismiss', { + duration: 4000 + }) + } } function generateReadMe(pt: TSandsPoint, parc: SxplrParcellation, tmpl: SxplrTemplate){ diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index 1b1f6f6f0..4ed2fa0fe 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -191,7 +191,7 @@ export class StatusCardComponent { } } - public selectPoint(pos: number[]) { + public selectPoint(posNm: number[]) { this.store$.dispatch( atlasSelection.actions.selectPoint({ point: { @@ -200,27 +200,24 @@ export class StatusCardComponent { coordinateSpace: { "@id": this.selectedTemplate.id }, - coordinates: pos.map(v => ({ + coordinates: posNm.map(v => ({ "@id": getUuid(), "@type": "https://openminds.ebrains.eu/core/QuantitativeValue", unit: { "@id": "id.link/mm" }, - value: v * 1e6, + value: v, uncertainty: [0, 0] })) } }) ) - } - - public navigateTo(pos: number[], positionReal=true) { this.store$.dispatch( atlasSelection.actions.navigateTo({ navigation: { - position: pos + position: posNm }, - physical: positionReal, + physical: true, animation: true }) ) diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html index 3dab5a5b1..c21ef051a 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.template.html +++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html @@ -139,18 +139,12 @@

- @@ -119,11 +121,12 @@ -

- Navigation Coordinate -

-
-
+ +

+ Navigation Coordinate +

+
@@ -131,32 +134,23 @@

- -

+
-
- - +
+ + - - - - + + + - - - -
+
+
\ No newline at end of file From 4fd3298a6042d036fc91e2dd498553f0e1886cd5 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 8 Feb 2024 12:09:12 +0100 Subject: [PATCH 16/48] feat: copy multi selected region name --- docs/releases/v2.14.5.md | 2 ++ src/index.html | 2 +- src/viewerModule/viewerCmp/viewerCmp.template.html | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index 4d176f89b..cc5a02f38 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -6,8 +6,10 @@ - Added documentation for URL encoding - Improved documentation for plugin API - Reworded point assignment UI +- Allow multi selected region names to be copied ## Behind the Scenes - Removed dependency on connectivity-component - Removed reference to JSC OKD instance, as the instance is no longer available +- Updated google-site-verification diff --git a/src/index.html b/src/index.html index 695145da0..3d3708ae3 100644 --- a/src/index.html +++ b/src/index.html @@ -3,7 +3,7 @@ - + diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 5406251d2..c89257b8c 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -637,6 +637,18 @@
Multiple regions selected
+ + + + + From 8b91b78491e2366fbf2c38b0aab6740c6ded3400 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 8 Feb 2024 18:16:23 +0100 Subject: [PATCH 17/48] bugfix: allow copy of text --- docs/releases/v2.14.5.md | 5 +++ package-lock.json | 8 ++--- package.json | 2 +- .../sapiViews/core/rich/module.ts | 2 ++ .../regionsHierarchy.style.css | 8 +++++ .../regionsHierarchy.template.html | 34 +++++++++++++++++++ 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index cc5a02f38..da8180f83 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -7,6 +7,11 @@ - Improved documentation for plugin API - Reworded point assignment UI - Allow multi selected region names to be copied +- Added legend to region hierarchy + +## Bugfix + +- Copy of free text (Ctrl + C) now works properly ## Behind the Scenes diff --git a/package-lock.json b/package-lock.json index cddb1a157..59b66bed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@ngrx/effects": "^15.4.0", "@ngrx/store": "^15.4.0", "acorn": "^8.4.1", - "export-nehuba": "^0.1.3", + "export-nehuba": "^0.1.5", "file-loader": "^6.2.0", "jszip": "^3.6.0", "postcss": "^8.3.6", @@ -7646,9 +7646,9 @@ "dev": true }, "node_modules/export-nehuba": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.3.tgz", - "integrity": "sha512-pooT9Af8LNuHtGqA3zzl75lT4G6TZt4f7VhOrRNL27ma9OxJ7OilMqOrJCTkfc5A5DaBampM61ay3srf9xZG1Q==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.5.tgz", + "integrity": "sha512-5Gsgvd0BLO4evuxp4bwrBS64Em1X92vyW2mwy9BvmaptHp9DbDf05zxi6Phu7apDY+Hzc3XEaxKOVGoMACeDJg==", "dependencies": { "pako": "^1.0.6" } diff --git a/package.json b/package.json index 743b9f466..4f08a89b8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@ngrx/effects": "^15.4.0", "@ngrx/store": "^15.4.0", "acorn": "^8.4.1", - "export-nehuba": "^0.1.3", + "export-nehuba": "^0.1.5", "file-loader": "^6.2.0", "jszip": "^3.6.0", "postcss": "^8.3.6", diff --git a/src/atlasComponents/sapiViews/core/rich/module.ts b/src/atlasComponents/sapiViews/core/rich/module.ts index f8fcc8a93..4596d5688 100644 --- a/src/atlasComponents/sapiViews/core/rich/module.ts +++ b/src/atlasComponents/sapiViews/core/rich/module.ts @@ -10,6 +10,7 @@ import { HighlightPipe } from "./regionsHierarchy/highlight.pipe"; import { SapiViewsCoreRichRegionsHierarchy } from "./regionsHierarchy/regionsHierarchy.component"; import { SapiViewsCoreRichRegionListSearch } from "./regionsListSearch/regionListSearch.component"; import { SapiViewsCoreRichRegionListTemplateDirective } from "./regionsListSearch/regionListSearchTmpl.directive"; +import { DialogModule } from "src/ui/dialogInfo"; @NgModule({ imports: [ @@ -20,6 +21,7 @@ import { SapiViewsCoreRichRegionListTemplateDirective } from "./regionsListSearc SxplrFlatHierarchyModule, SapiViewsUtilModule, UtilModule, + DialogModule, ], declarations: [ SapiViewsCoreRichRegionListSearch, diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.style.css b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.style.css index 669b600a3..72b9867ff 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.style.css +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.style.css @@ -14,3 +14,11 @@ sxplr-flat-hierarchy-tree-view { flex: 0px 1 1; } + +.legend-container +{ + margin: 1rem; + margin-top: -4rem; + flex-direction: row-reverse; + display: flex; +} diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html index 4638a66eb..7a74db2d2 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.template.html @@ -27,6 +27,29 @@ + +

Legend

+ + + + + Region mapped in the current selection + + + + Region not mapped in the current selection + + + + Region selected + + + + + +
+ + + + +
+ +
From 64bf4ee9dab92d194df243c49117a67b15a8df6a Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 14 Feb 2024 13:44:34 +0100 Subject: [PATCH 18/48] maint: allow spatial transform to be configured at runtime --- .helm/adhoc/configmap-siibra-explorer.yml | 2 + .../adhoc/example-secret-siibra-explorer.yml | 1 - backend/app/config.py | 2 + backend/app/const.py | 2 + backend/app/index_html.py | 6 ++- common/constants.js | 3 +- deploy_env.md | 1 + docs/releases/v2.14.5.md | 1 + .../interSpaceCoordXform.service.spec.ts | 45 ++++++++++++++++--- .../space/interSpaceCoordXform.service.ts | 10 +++-- src/main.module.ts | 19 +++++--- src/state/atlasSelection/effects.spec.ts | 11 ++++- src/util/constants.ts | 4 ++ 13 files changed, 89 insertions(+), 18 deletions(-) diff --git a/.helm/adhoc/configmap-siibra-explorer.yml b/.helm/adhoc/configmap-siibra-explorer.yml index 37fbb9fc0..4bc6d90b1 100644 --- a/.helm/adhoc/configmap-siibra-explorer.yml +++ b/.helm/adhoc/configmap-siibra-explorer.yml @@ -7,6 +7,8 @@ data: REDIS_ADDR: "cache-redis-service" V2_7_PLUGIN_URLS: "https://siibra-jugex.apps.tc.humanbrainproject.eu/viewer_plugin/manifest.json;https://ngpy.apps.hbp.eu/viewer_plugin/manifest.json" LOGGER_DIR: "/sxplr-log" + OVERWRITE_API_ENDPOINT: https://siibra-api-prod.apps.tc.humanbrainproject.eu/v3_0 + OVERWRITE_SPATIAL_ENDPOINT: 'https://siibra-spatial-backend.apps.tc.humanbrainproject.eu' kind: ConfigMap metadata: diff --git a/.helm/adhoc/example-secret-siibra-explorer.yml b/.helm/adhoc/example-secret-siibra-explorer.yml index e4118f801..c1c93e8a5 100644 --- a/.helm/adhoc/example-secret-siibra-explorer.yml +++ b/.helm/adhoc/example-secret-siibra-explorer.yml @@ -7,7 +7,6 @@ data: # n.b. echo -n "foobar" | base64 # or else the new line will also be encoded, and you will # wonder why your application does not work - OVERWRITE_API_ENDPOINT: Zm9vYmFy HBP_CLIENTID_V2: Zm9vYmFy HBP_CLIENTSECRET_V2: Zm9vYmFy SXPLR_EBRAINS_IAM_SA_CLIENT_ID: Zm9vYmFy diff --git a/backend/app/config.py b/backend/app/config.py index bc824fd52..43f49b315 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -8,6 +8,8 @@ OVERWRITE_API_ENDPOINT = os.getenv("OVERWRITE_API_ENDPOINT") +OVERWRITE_SPATIAL_ENDPOINT = os.getenv("OVERWRITE_SPATIAL_ENDPOINT") + LOCAL_CDN = os.getenv("LOCAL_CDN") HBP_CLIENTID_V2 = os.getenv("HBP_CLIENTID_V2", "no hbp id") diff --git a/backend/app/const.py b/backend/app/const.py index 9948b8e47..ca7201499 100644 --- a/backend/app/const.py +++ b/backend/app/const.py @@ -19,3 +19,5 @@ DATA_ERROR_ATTR = "data-error" OVERWRITE_SAPI_ENDPOINT_ATTR = "x-sapi-base-url" + +OVERWRITE_SPATIAL_BACKEND_ATTR = "x-spatial-backend-url" diff --git a/backend/app/index_html.py b/backend/app/index_html.py index 903ec1547..ea4baf68f 100644 --- a/backend/app/index_html.py +++ b/backend/app/index_html.py @@ -2,8 +2,8 @@ from pathlib import Path from fastapi.responses import Response from typing import Dict -from .const import ERROR_KEY, DATA_ERROR_ATTR, OVERWRITE_SAPI_ENDPOINT_ATTR, COOKIE_KWARGS -from .config import PATH_TO_PUBLIC, OVERWRITE_API_ENDPOINT +from .const import ERROR_KEY, DATA_ERROR_ATTR, OVERWRITE_SAPI_ENDPOINT_ATTR, COOKIE_KWARGS, OVERWRITE_SPATIAL_BACKEND_ATTR +from .config import PATH_TO_PUBLIC, OVERWRITE_API_ENDPOINT, OVERWRITE_SPATIAL_ENDPOINT path_to_index = Path(PATH_TO_PUBLIC) / "index.html" index_html: str = None @@ -32,6 +32,8 @@ async def get_index_html(request: Request): if OVERWRITE_API_ENDPOINT: attributes_to_append[OVERWRITE_SAPI_ENDPOINT_ATTR] = OVERWRITE_API_ENDPOINT + if OVERWRITE_SPATIAL_ENDPOINT: + attributes_to_append[OVERWRITE_SPATIAL_BACKEND_ATTR] = OVERWRITE_SPATIAL_ENDPOINT attr_string = " ".join([f"{key}={_monkey_sanitize(value)}" for key, value in attributes_to_append.items()]) diff --git a/common/constants.js b/common/constants.js index fb5a018be..4ddf69753 100644 --- a/common/constants.js +++ b/common/constants.js @@ -150,7 +150,8 @@ If you do not accept the Terms & Conditions you are not permitted to access or u AUXMESH_DESC: `Some templates contain auxiliary meshes, which compliment the appearance of the template in the perspective view.`, OVERWRITE_SAPI_ENDPOINT_ATTR: `x-sapi-base-url`, - DATA_ERROR_ATTR: `data-error` + OVERWRITE_SPATIAL_BACKEND_ATTR: `x-spatial-backend-url`, + DATA_ERROR_ATTR: `data-error`, } exports.QUICKTOUR_DESC ={ diff --git a/deploy_env.md b/deploy_env.md index 8c2b1a3d8..00df9cfd3 100644 --- a/deploy_env.md +++ b/deploy_env.md @@ -9,6 +9,7 @@ | `V2_7_STAGING_PLUGIN_URLS` | semi colon separated urls to be returned when user queries plugins | `''` | `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | | | `OVERWRITE_API_ENDPOINT` | overwrite build time siibra-api endpoint | +| `OVERWRITE_SPATIAL_ENDPOINT` | overwrite build time spatial transform endpoint | | | `PATH_TO_PUBLIC` | path to built frontend | `../dist/aot` | diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index da8180f83..a1c15104a 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -18,3 +18,4 @@ - Removed dependency on connectivity-component - Removed reference to JSC OKD instance, as the instance is no longer available - Updated google-site-verification +- Allow inter space transform to be configured at runtime diff --git a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.spec.ts b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.spec.ts index 30542d6fc..c9baedadf 100644 --- a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.spec.ts +++ b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.spec.ts @@ -1,16 +1,25 @@ import { InterSpaceCoordXformSvc, VALID_TEMPLATE_SPACE_NAMES } from './interSpaceCoordXform.service' import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing' import { TestBed, fakeAsync, tick } from '@angular/core/testing' +import { GET_ATTR_TOKEN } from 'src/util/constants' describe('InterSpaceCoordXformSvc.service.spec.ts', () => { describe('InterSpaceCoordXformSvc', () => { + let attr: string = null + const defaultUrl = 'https://hbp-spatial-backend.apps.hbp.eu/v1/transform-points' beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ - InterSpaceCoordXformSvc + InterSpaceCoordXformSvc, + { + provide: GET_ATTR_TOKEN, + useFactory: () => { + return () => attr + } + } ] }) }) @@ -39,7 +48,7 @@ describe('InterSpaceCoordXformSvc.service.spec.ts', () => { ).subscribe((_ev) => { }) - const req = httpTestingController.expectOne(service['url']) + const req = httpTestingController.expectOne(defaultUrl) expect(req.request.method).toEqual('POST') expect( JSON.parse(req.request.body) @@ -67,7 +76,7 @@ describe('InterSpaceCoordXformSvc.service.spec.ts', () => { expect(status).toEqual('completed') expect(result).toEqual([1e6, 2e6, 3e6]) }) - const req = httpTestingController.expectOne(service['url']) + const req = httpTestingController.expectOne(defaultUrl) req.flush({ 'target_points':[ [1, 2, 3] @@ -87,7 +96,7 @@ describe('InterSpaceCoordXformSvc.service.spec.ts', () => { expect(status).toEqual('error') done() }) - const req = httpTestingController.expectOne(service['url']) + const req = httpTestingController.expectOne(defaultUrl) req.flush('intercepted', { status: 500, statusText: 'internal server error' }) }) @@ -105,10 +114,36 @@ describe('InterSpaceCoordXformSvc.service.spec.ts', () => { expect(status).toEqual('error') expect(statusText).toEqual(`Timeout after 3s`) }) - const req = httpTestingController.expectOne(service['url']) + const req = httpTestingController.expectOne(defaultUrl) tick(4000) expect(req.cancelled).toBe(true) })) + + describe("if injected override endpoint", () => { + beforeEach(() => { + attr = "http://foo-bar/" + }) + afterEach(() => { + attr = null + }) + it("trasnforms argument properly", () => { + + const service = TestBed.inject(InterSpaceCoordXformSvc) + const httpTestingController = TestBed.inject(HttpTestingController) + + // subscriptions are necessary for http fetch to occur + service.transform( + VALID_TEMPLATE_SPACE_NAMES.MNI152, + VALID_TEMPLATE_SPACE_NAMES.BIG_BRAIN, + [1,2,3] + ).subscribe((_ev) => { + + }) + const req = httpTestingController.expectOne("http://foo-bar/v1/transform-points") + expect(req.request.method).toEqual('POST') + req.flush({}) + }) + }) }) }) }) diff --git a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts index 8dff3f67d..18518da17 100644 --- a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts +++ b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts @@ -1,9 +1,11 @@ -import { Injectable } from "@angular/core"; +import { Inject, Injectable } from "@angular/core"; import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http"; import { catchError, timeout, map } from "rxjs/operators"; import { of, Observable } from "rxjs"; import { environment } from 'src/environments/environment' import { IDS } from "src/atlasComponents/sapi/constants" +import { GET_ATTR_TOKEN, GetAttr } from "src/util/constants"; +import { CONST } from "common/constants" type ITemplateCoordXformResp = { status: 'pending' | 'error' | 'completed' | 'cached' @@ -49,9 +51,11 @@ export class InterSpaceCoordXformSvc { } } - constructor(private httpClient: HttpClient) {} + constructor(private httpClient: HttpClient, @Inject(GET_ATTR_TOKEN) getAttr: GetAttr) { + this.url = (getAttr(CONST.OVERWRITE_SPATIAL_BACKEND_ATTR) || environment.SPATIAL_TRANSFORM_BACKEND).replace(/\/$/, '') + '/v1/transform-points' + } - private url = `${environment.SPATIAL_TRANSFORM_BACKEND.replace(/\/$/, '')}/v1/transform-points` + private url: string // jasmine marble cannot test promise properly // see https://github.com/ngrx/platform/issues/498#issuecomment-337465179 diff --git a/src/main.module.ts b/src/main.module.ts index 2735d71ea..aedbee438 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -53,6 +53,7 @@ import { CONST } from "common/constants" import { ViewerCommonEffects } from './viewerModule'; import { environment } from './environments/environment'; import { SAPI } from './atlasComponents/sapi'; +import { GET_ATTR_TOKEN, GetAttr } from './util/constants'; @NgModule({ imports: [ @@ -187,12 +188,20 @@ import { SAPI } from './atlasComponents/sapi'; multi: true, deps: [ AuthService, Store ] }, + { + provide: GET_ATTR_TOKEN, + useFactory: (document: Document) => { + return (attr: string) => { + const rootEl = document.querySelector("atlas-viewer") + return rootEl?.getAttribute(attr) + } + }, + deps: [ DOCUMENT ] + }, { provide: APP_INITIALIZER, - useFactory: (sapi: SAPI, document: Document) => { - - const rootEl = document.querySelector("atlas-viewer") - const overwriteSapiUrl = rootEl?.getAttribute(CONST.OVERWRITE_SAPI_ENDPOINT_ATTR) + useFactory: (sapi: SAPI, getAttr: GetAttr) => { + const overwriteSapiUrl = getAttr(CONST.OVERWRITE_SAPI_ENDPOINT_ATTR) const { SIIBRA_API_ENDPOINTS } = environment const endpoints = (overwriteSapiUrl && [ overwriteSapiUrl ]) || SIIBRA_API_ENDPOINTS.split(',') @@ -207,7 +216,7 @@ import { SAPI } from './atlasComponents/sapi'; } }, multi: true, - deps: [ SAPI, DOCUMENT ] + deps: [ SAPI, GET_ATTR_TOKEN ] } ], bootstrap: [ diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts index a64b03233..aeefa764a 100644 --- a/src/state/atlasSelection/effects.spec.ts +++ b/src/state/atlasSelection/effects.spec.ts @@ -3,7 +3,7 @@ import { provideMockActions } from "@ngrx/effects/testing" import { Action } from "@ngrx/store" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { hot } from "jasmine-marbles" -import { NEVER, ReplaySubject, of, throwError } from "rxjs" +import { EMPTY, NEVER, ReplaySubject, of, throwError } from "rxjs" import { SAPI, SAPIModule } from "src/atlasComponents/sapi" import { SxplrRegion, SxplrAtlas, SxplrParcellation, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes" import { IDS } from "src/atlasComponents/sapi/constants" @@ -15,6 +15,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations" import { translateV3Entities } from "src/atlasComponents/sapi/translateV3" import { PathReturn } from "src/atlasComponents/sapi/typeV3" import { MatDialog } from 'src/sharedModules/angularMaterial.exports' +import { InterSpaceCoordXformSvc } from "src/atlasComponents/sapi/core/space/interSpaceCoordXform.service" describe("> effects.ts", () => { describe("> Effect", () => { @@ -104,6 +105,14 @@ describe("> effects.ts", () => { } } }, + { + provide: InterSpaceCoordXformSvc, + useValue: { + transform() { + return EMPTY + } + } + } ] }) }) diff --git a/src/util/constants.ts b/src/util/constants.ts index b2a3735ee..b1411dc43 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -101,3 +101,7 @@ export const parcBanList: string[] = [ "minds/core/parcellationatlas/v1.0.0/887da8eb4c36d944ef626ed5293db3ef", "minds/core/parcellationatlas/v1.0.0/f2b1ac621421708c1bef422bb5058456", ] + +export const GET_ATTR_TOKEN = new InjectionToken("GET_ATTR_TOKEN") + +export type GetAttr = (attr: string) => string|null \ No newline at end of file From d89d734a0343f8b99b9bc9da731e5f67aedf7968 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 19 Feb 2024 12:50:50 +0100 Subject: [PATCH 19/48] fix: ebrains sync checkout lfs --- .github/workflows/repo_sync_ebrains.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/repo_sync_ebrains.yml b/.github/workflows/repo_sync_ebrains.yml index e29122fa0..b12582323 100644 --- a/.github/workflows/repo_sync_ebrains.yml +++ b/.github/workflows/repo_sync_ebrains.yml @@ -9,7 +9,9 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + lfs: true - uses: wei/git-sync@v3 with: source_repo: ${GITHUB_REPOSITORY} From 3d63298c80467fb87b36b8fecbba1959e461744d Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 19 Feb 2024 14:56:00 +0100 Subject: [PATCH 20/48] fix lfs sync --- .github/workflows/repo_sync_ebrains.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/repo_sync_ebrains.yml b/.github/workflows/repo_sync_ebrains.yml index b12582323..c98e0f8de 100644 --- a/.github/workflows/repo_sync_ebrains.yml +++ b/.github/workflows/repo_sync_ebrains.yml @@ -9,10 +9,7 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - lfs: true - - uses: wei/git-sync@v3 + - uses: valtech-sd/git-sync@v9 with: source_repo: ${GITHUB_REPOSITORY} source_branch: ${GITHUB_REF_NAME} From a3f3e04b3afd371fb7160adfbb2121adb24b125f Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 22 Feb 2024 17:33:04 +0100 Subject: [PATCH 21/48] feat: show comp feature on side bar feat: preview compound feature point indices maint: helm: update config, chart, and env maint: make test less flaky maint: minor fix Annotation layer maint: more context when browser does not meet requirement --- .helm/adhoc/configmap-siibra-explorer.yml | 2 - .helm/siibra-explorer/Chart.yaml | 4 +- .../siibra-explorer/templates/deployment.yaml | 2 + .helm/siibra-explorer/values.yaml | 6 ++ common/util.spec.js | 4 +- .../annotations/annotation.service.ts | 5 +- src/atlasComponents/sapi/sapi.service.ts | 2 +- src/atlasComponents/sapi/sxplrTypes.ts | 8 +- src/atlasComponents/sapi/translateV3.ts | 35 +++---- src/atlasViewer/atlasViewer.component.ts | 2 + src/features/atlas-colormap-intents/index.ts | 1 + .../intents.component.ts | 4 +- .../intents.style.css | 0 .../intents.template.html | 0 .../compoundFeatureIndices.component.ts | 78 ++++++++++++++++ .../compoundFeatureIndices.style.css | 4 + .../compoundFeatureIndices.template.html | 31 +++++++ .../compoundFeatureIndices/idxToIcon.pipe.ts | 32 +++++++ .../compoundFeatureIndices/idxToText.pipe.ts | 15 +++ src/features/compoundFeatureIndices/index.ts | 3 + src/features/compoundFeatureIndices/module.ts | 26 ++++++ src/features/compoundFeatureIndices/util.ts | 8 ++ src/features/entry/entry.component.ts | 9 -- .../entry/entry.flattened.component.html | 25 ----- .../feature-view/feature-view.component.html | 15 ++- .../feature-view/feature-view.component.ts | 51 +++++++++-- src/features/module.ts | 6 +- src/features/pointcloud-intents/index.ts | 2 + .../pointcloud-intents/intents.component.ts | 91 +++++++++++++++++++ .../pointcloud-intents/intents.style.css | 0 .../pointcloud-intents/intents.template.html | 0 src/features/pointcloud-intents/util.ts | 24 +++++ src/sharedModules/angularMaterial.exports.ts | 3 +- src/sharedModules/angularMaterial.module.ts | 2 + src/sharedModules/index.ts | 4 +- src/state/userInteraction/store.ts | 7 -- .../viewerCmp/viewerCmp.component.ts | 3 +- .../viewerCmp/viewerCmp.template.html | 8 +- 38 files changed, 429 insertions(+), 93 deletions(-) create mode 100644 src/features/atlas-colormap-intents/index.ts rename src/features/{intents => atlas-colormap-intents}/intents.component.ts (97%) rename src/features/{intents => atlas-colormap-intents}/intents.style.css (100%) rename src/features/{intents => atlas-colormap-intents}/intents.template.html (100%) create mode 100644 src/features/compoundFeatureIndices/compoundFeatureIndices.component.ts create mode 100644 src/features/compoundFeatureIndices/compoundFeatureIndices.style.css create mode 100644 src/features/compoundFeatureIndices/compoundFeatureIndices.template.html create mode 100644 src/features/compoundFeatureIndices/idxToIcon.pipe.ts create mode 100644 src/features/compoundFeatureIndices/idxToText.pipe.ts create mode 100644 src/features/compoundFeatureIndices/index.ts create mode 100644 src/features/compoundFeatureIndices/module.ts create mode 100644 src/features/compoundFeatureIndices/util.ts create mode 100644 src/features/pointcloud-intents/index.ts create mode 100644 src/features/pointcloud-intents/intents.component.ts create mode 100644 src/features/pointcloud-intents/intents.style.css create mode 100644 src/features/pointcloud-intents/intents.template.html create mode 100644 src/features/pointcloud-intents/util.ts diff --git a/.helm/adhoc/configmap-siibra-explorer.yml b/.helm/adhoc/configmap-siibra-explorer.yml index 4bc6d90b1..a732f6bfc 100644 --- a/.helm/adhoc/configmap-siibra-explorer.yml +++ b/.helm/adhoc/configmap-siibra-explorer.yml @@ -1,7 +1,5 @@ apiVersion: v1 data: - HOST_PATHNAME: "/viewer" - HOSTNAME: "https://siibra-explorer.apps.tc.humanbrainproject.eu" SIIBRA_CACHEDIR: /siibra-api-volume HBP_DISCOVERY_URL: "https://iam.ebrains.eu/auth/realms/hbp" REDIS_ADDR: "cache-redis-service" diff --git a/.helm/siibra-explorer/Chart.yaml b/.helm/siibra-explorer/Chart.yaml index cfd3a2c27..301a17e42 100644 --- a/.helm/siibra-explorer/Chart.yaml +++ b/.helm/siibra-explorer/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.1.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.16.0" +appVersion: "2.14.5" diff --git a/.helm/siibra-explorer/templates/deployment.yaml b/.helm/siibra-explorer/templates/deployment.yaml index 2624dcf1f..098a16f64 100644 --- a/.helm/siibra-explorer/templates/deployment.yaml +++ b/.helm/siibra-explorer/templates/deployment.yaml @@ -53,6 +53,8 @@ spec: # httpGet: # path: / # port: http + env: + {{- toYaml .Values.env | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.volumeMounts }} diff --git a/.helm/siibra-explorer/values.yaml b/.helm/siibra-explorer/values.yaml index af3125286..31631298a 100644 --- a/.helm/siibra-explorer/values.yaml +++ b/.helm/siibra-explorer/values.yaml @@ -94,3 +94,9 @@ nodeSelector: {} tolerations: [] affinity: {} + +env: + - name: HOST_PATHNAME + value: "/viewer" + - name: HOSTNAME + value: "https://siibra-explorer.apps.tc.humanbrainproject.eu" diff --git a/common/util.spec.js b/common/util.spec.js index 552240c56..d8142ee15 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -171,7 +171,7 @@ describe('common/util.js', () => { } finally { const end = performance.now() - expect(end - start).toBeGreaterThan(defaultTimeout) + expect(end - start).toBeGreaterThanOrEqual(defaultTimeout) expect(end - start).toBeLessThan(defaultTimeout + 20) } }) @@ -196,7 +196,7 @@ describe('common/util.js', () => { } finally { const end = performance.now() - expect(end - start).toBeGreaterThan(timeout) + expect(end - start).toBeGreaterThanOrEqual(timeout) expect(end - start).toBeLessThan(timeout + 20) } }) diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index be1b3fe8e..d8242e62f 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -129,11 +129,12 @@ export class AnnotationLayer { this._onHover.complete() while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() try { - this.viewer.layerManager.removeManagedLayer(this.nglayer) + const l = this.viewer.layerManager.getLayerByName(this.name) + this.viewer.layerManager.removeManagedLayer(l) this.nglayer = null // eslint-disable-next-line no-empty } catch (e) { - + console.error("removing layer failed", e) } } diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 33ea800e4..b914e41db 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -438,7 +438,7 @@ export class SAPI{ */ return this.v3Get("/feature/Image", { query: { - space_id: bbox.space.id, + space_id: bbox.space?.id || bbox.spaceId, bbox: JSON.stringify([bbox.minpoint, bbox.maxpoint]), } }).pipe( diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 115feaf3f..11d685807 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -51,7 +51,8 @@ export type AdditionalInfo = { } type Location = { - readonly space: SxplrTemplate + readonly space?: SxplrTemplate + readonly spaceId: string } type LocTuple = [number, number, number] @@ -97,12 +98,13 @@ export type StatisticalMap = { * Features */ -export type SimpleCompoundFeature = { +export type SimpleCompoundFeature = { id: string name: string + category?: string indices: { id: string - index: string|Point + index: T name: string }[] } & AdditionalInfo diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index ad2fecc3a..a2abb1375 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -265,22 +265,20 @@ class TranslateV3 { const { ['@id']: regionId } = region this.#regionMap.set(regionId, region) this.#regionMap.set(region.name, region) + + const bestViewPoint = region.hasAnnotation?.bestViewPoint + return { id: region["@id"], name: region.name, color: hexToRgb(region.hasAnnotation?.displayColor) as [number, number, number], parentIds: region.hasParent.map( v => v["@id"] ), 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 - } - })() + centroid: bestViewPoint + ? { + loc: bestViewPoint.coordinates.map(v => v.value) as [number, number, number], + spaceId: bestViewPoint.coordinateSpace['@id'] + } : null } } @@ -626,14 +624,9 @@ class TranslateV3 { } async #translatePoint(point: components["schemas"]["CoordinatePointModel"]): Promise { - const getTmpl = (id: string) => { - return this.#sxplrTmplMap.get(id) - } return { loc: point.coordinates.map(v => v.value) as [number, number, number], - get space() { - return getTmpl(point.coordinateSpace['@id']) - } + spaceId: point.coordinateSpace['@id'], } } @@ -649,6 +642,7 @@ class TranslateV3 { const v: SimpleCompoundFeature = { id: feat.id, name: feat.name, + category: feat.category, indices: await Promise.all( feat.indices.map( async ({ id, index, name }) => ({ @@ -713,14 +707,11 @@ class TranslateV3 { this.#extractNgPrecompUnfrag(feat.volume.providedVolumes), ]) const { ['@id']: spaceId } = feat.boundingbox.space - const getSpace = (id: string) => this.#sxplrTmplMap.get(id) const bbox: BoundingBox = { center, maxpoint, minpoint, - get space() { - return getSpace(spaceId) - } + spaceId } return { ...superObj, @@ -750,6 +741,10 @@ class TranslateV3 { ] } } + + getSpaceFromId(id: string): SxplrTemplate { + return this.#sxplrTmplMap.get(id) + } } export const translateV3Entities = new TranslateV3() diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 5c1f800ec..acbcfd87f 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -184,12 +184,14 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { const gl = canvas.getContext('webgl2') as WebGLRenderingContext if (!gl) { + console.error(`Get GLContext failed!`) return false } const colorBufferFloat = gl.getExtension('EXT_color_buffer_float') if (!colorBufferFloat) { + console.error(`Get Extension failed!`) return false } diff --git a/src/features/atlas-colormap-intents/index.ts b/src/features/atlas-colormap-intents/index.ts new file mode 100644 index 000000000..c645d44e3 --- /dev/null +++ b/src/features/atlas-colormap-intents/index.ts @@ -0,0 +1 @@ +export { AtlasColorMapIntents } from "./intents.component" \ No newline at end of file diff --git a/src/features/intents/intents.component.ts b/src/features/atlas-colormap-intents/intents.component.ts similarity index 97% rename from src/features/intents/intents.component.ts rename to src/features/atlas-colormap-intents/intents.component.ts index 6ccab2838..b59773427 100644 --- a/src/features/intents/intents.component.ts +++ b/src/features/atlas-colormap-intents/intents.component.ts @@ -13,7 +13,7 @@ const CONNECTIVITY_LAYER_ID = "connectivity-colormap-id" type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] @Component({ - selector: 'feature-intents', + selector: 'atlas-colormap-intents', templateUrl: './intents.template.html', styleUrls: [ './intents.style.css' @@ -27,7 +27,7 @@ type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] DestroyDirective ] }) -export class FeatureIntents{ +export class AtlasColorMapIntents{ readonly #destory$ = inject(DestroyDirective).destroyed$ #intents$ = new BehaviorSubject([]) diff --git a/src/features/intents/intents.style.css b/src/features/atlas-colormap-intents/intents.style.css similarity index 100% rename from src/features/intents/intents.style.css rename to src/features/atlas-colormap-intents/intents.style.css diff --git a/src/features/intents/intents.template.html b/src/features/atlas-colormap-intents/intents.template.html similarity index 100% rename from src/features/intents/intents.template.html rename to src/features/atlas-colormap-intents/intents.template.html diff --git a/src/features/compoundFeatureIndices/compoundFeatureIndices.component.ts b/src/features/compoundFeatureIndices/compoundFeatureIndices.component.ts new file mode 100644 index 000000000..17522b425 --- /dev/null +++ b/src/features/compoundFeatureIndices/compoundFeatureIndices.component.ts @@ -0,0 +1,78 @@ +import { Component, EventEmitter, Input, Output, inject, ViewChild } from "@angular/core"; +import { BehaviorSubject, combineLatest } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; +import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +import { MatTableDataSource, MatPaginator } from "src/sharedModules"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { CFIndex } from "./util"; +import { switchMapWaitFor } from "src/util/fn"; + +@Component({ + selector: 'compound-feature-indices', + templateUrl: './compoundFeatureIndices.template.html', + styleUrls: [ + './compoundFeatureIndices.style.css' + ], + hostDirectives: [ + DestroyDirective + ] +}) + +export class CompoundFeatureIndices { + + public columns = ['name'] + + @ViewChild(MatPaginator) + paginator: MatPaginator + + readonly #destroy$ = inject(DestroyDirective).destroyed$ + + #indices$ = new BehaviorSubject([] as CFIndex[]) + #ds$ = this.#indices$.pipe( + switchMap( + switchMapWaitFor({ + condition: () => !!this.paginator, + interval: 160, + leading: true + }) + ), + map(points => { + const ds = new MatTableDataSource(points) + ds.paginator = this.paginator + return ds + }) + ) + + #selectedTemplate$ = new BehaviorSubject(null) + + @Input('indices') + set indices(val: CFIndex[]) { + this.#indices$.next(val) + } + + @Input('selected-template') + set selectedTemplate(tmpl: SxplrTemplate){ + this.#selectedTemplate$.next(tmpl) + } + + view$ = combineLatest([ + this.#indices$, + this.#ds$, + this.#selectedTemplate$, + ]).pipe( + map(([ indices, datasource, selectedTemplate ]) => { + return { + indices, + datasource, + selectedTemplate, + } + }) + ) + + @Output('on-click-index') + onClick = new EventEmitter() + + handleOnClick(item: CFIndex){ + this.onClick.next(item) + } +} diff --git a/src/features/compoundFeatureIndices/compoundFeatureIndices.style.css b/src/features/compoundFeatureIndices/compoundFeatureIndices.style.css new file mode 100644 index 000000000..1024e08f3 --- /dev/null +++ b/src/features/compoundFeatureIndices/compoundFeatureIndices.style.css @@ -0,0 +1,4 @@ +.mat-mdc-cell +{ + cursor: pointer; +} diff --git a/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html b/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html new file mode 100644 index 000000000..727aa06ae --- /dev/null +++ b/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html @@ -0,0 +1,31 @@ + + +

+ + + + + + + +
name + + {{ element.index | indexToStr }} + + + + + +
+ + + +
+ + + diff --git a/src/features/compoundFeatureIndices/idxToIcon.pipe.ts b/src/features/compoundFeatureIndices/idxToIcon.pipe.ts new file mode 100644 index 000000000..a6f560182 --- /dev/null +++ b/src/features/compoundFeatureIndices/idxToIcon.pipe.ts @@ -0,0 +1,32 @@ +import { Pipe, PipeTransform } from "@angular/core" +import { CFIndex, isPoint } from "./util" +import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes" +import { translateV3Entities } from "src/atlasComponents/sapi/translateV3" + +type Icon = { + fontSet: string + fontIcon: string + message: string +} + +@Pipe({ + name: 'idxToIcon', + pure: true +}) + +export class IndexToIconPipe implements PipeTransform{ + transform(index: CFIndex, selectedTemplate: SxplrTemplate): Icon[] { + if (!isPoint(index.index)) { + return [] + } + if (index.index.spaceId !== selectedTemplate.id) { + const tmpl = translateV3Entities.getSpaceFromId(index.index.spaceId) + return [{ + fontSet: 'fas', + fontIcon: 'fa-exclamation-triangle', + message: `This point is in space ${tmpl?.name || 'Unknown'}(id=${index.index.spaceId}). It cannot be shown in the currently selected space ${selectedTemplate.name}(id=${selectedTemplate.id})` + }] + } + return [] + } +} diff --git a/src/features/compoundFeatureIndices/idxToText.pipe.ts b/src/features/compoundFeatureIndices/idxToText.pipe.ts new file mode 100644 index 000000000..b2b0f30d3 --- /dev/null +++ b/src/features/compoundFeatureIndices/idxToText.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { CFIndex } from "./util" + +@Pipe({ + name: 'indexToStr', + pure: true +}) +export class IndexToStrPipe implements PipeTransform{ + public transform(value: CFIndex['index']): string { + if (typeof value === "string") { + return value + } + return `Point(${value.loc.map(v => v.toFixed(2)).join(", ")})` + } +} diff --git a/src/features/compoundFeatureIndices/index.ts b/src/features/compoundFeatureIndices/index.ts new file mode 100644 index 000000000..10694c74c --- /dev/null +++ b/src/features/compoundFeatureIndices/index.ts @@ -0,0 +1,3 @@ +export { CompoundFeatureIndices } from "./compoundFeatureIndices.component" +export { CFIndex } from "./util" +export { CompoundFeatureIndicesModule } from "./module" diff --git a/src/features/compoundFeatureIndices/module.ts b/src/features/compoundFeatureIndices/module.ts new file mode 100644 index 000000000..387b0081f --- /dev/null +++ b/src/features/compoundFeatureIndices/module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AngularMaterialModule } from "src/sharedModules"; +import { CompoundFeatureIndices } from "./compoundFeatureIndices.component"; +import { IndexToStrPipe } from "./idxToText.pipe"; +import { IndexToIconPipe } from "./idxToIcon.pipe"; +import { PointCloudIntents, FilterPointTransformer } from "src/features/pointcloud-intents"; + +@NgModule({ + imports: [ + CommonModule, + AngularMaterialModule, + PointCloudIntents, + ], + declarations: [ + CompoundFeatureIndices, + IndexToStrPipe, + IndexToIconPipe, + FilterPointTransformer, + ], + exports: [ + CompoundFeatureIndices, + ] +}) + +export class CompoundFeatureIndicesModule{} diff --git a/src/features/compoundFeatureIndices/util.ts b/src/features/compoundFeatureIndices/util.ts new file mode 100644 index 000000000..8c028e502 --- /dev/null +++ b/src/features/compoundFeatureIndices/util.ts @@ -0,0 +1,8 @@ +import { Point, SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; + +export type CFIndex = SimpleCompoundFeature['indices'][number] + +export function isPoint(val: string|Point): val is Point{ + return !!(val as any).spaceId && !!(val as any).loc + } + \ No newline at end of file diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index 69cc3c7d6..dc162a46a 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -37,9 +37,6 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest @ViewChildren(CategoryAccDirective) catAccDirs: QueryList - @ViewChild('compoundFtTmpl', { read: TemplateRef }) - compoundFtTmpl: TemplateRef - constructor(private sapi: SAPI, private store: Store, private dialog: MatDialog, private cdr: ChangeDetectorRef) { super() } @@ -194,12 +191,6 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest ) onClickFeature(feature: Feature) { - if (feature.id.startsWith("cf0::")) { - const ref = this.dialog.open(this.compoundFtTmpl, { - data: { feature, dismiss: () => ref.close() } - }) - return - } this.store.dispatch( userInteraction.actions.showFeature({ feature diff --git a/src/features/entry/entry.flattened.component.html b/src/features/entry/entry.flattened.component.html index 3fa5f5214..9582c934c 100644 --- a/src/features/entry/entry.flattened.component.html +++ b/src/features/entry/entry.flattened.component.html @@ -186,28 +186,3 @@

- -

{{ data.feature.name }}

- -
- {{ data.feature.desc }} -
- - - - - -
- - -
diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 5098a05a3..86e523780 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -69,6 +69,17 @@ + + + + + + + + + @@ -94,9 +105,9 @@ - - + diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index c393df9f8..c96cd7ee6 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -2,15 +2,20 @@ import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core import { BehaviorSubject, EMPTY, Observable, combineLatest, concat, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; -import { Feature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; +import { Feature, SimpleCompoundFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; import { DARKTHEME } from 'src/util/injectionTokens'; import { isVoiData, notQuiteRight } from "../guards" import { Action, Store, select } from '@ngrx/store'; -import { atlasSelection } from 'src/state'; +import { atlasSelection, userInteraction } from 'src/state'; import { PathReturn } from 'src/atlasComponents/sapi/typeV3'; +import { MatSnackBar } from '@angular/material/snack-bar'; type PlotlyResponse = PathReturn<"/feature/{feature_id}/plotly"> +function isSimpleCompoundFeature(feat: unknown): feat is SimpleCompoundFeature{ + return !!feat['indices'] +} + @Component({ selector: 'sxplr-feature-view', templateUrl: './feature-view.component.html', @@ -19,9 +24,11 @@ type PlotlyResponse = PathReturn<"/feature/{feature_id}/plotly"> }) export class FeatureViewComponent { - #feature$ = new BehaviorSubject(null) + busy$ = new BehaviorSubject(false) + + #feature$ = new BehaviorSubject(null) @Input() - set feature(val: Feature) { + set feature(val: Feature|SimpleCompoundFeature) { this.#feature$.next(val) } @@ -137,6 +144,15 @@ export class FeatureViewComponent { )) ) + #compoundFeatEmts$ = this.#feature$.pipe( + map(f => { + if (isSimpleCompoundFeature(f)) { + return f.indices + } + return null + }) + ) + additionalLinks$ = this.#detailLinks.pipe( distinctUntilChanged((o, n) => o.length == n.length), withLatestFrom(this.#feature$), @@ -178,6 +194,7 @@ export class FeatureViewComponent { constructor( private sapi: SAPI, private store: Store, + private snackbar: MatSnackBar, @Inject(DARKTHEME) public darktheme$: Observable, ) { } @@ -204,11 +221,15 @@ export class FeatureViewComponent { concat( of(null as PlotlyResponse), this.#plotly$, + ), + this.#compoundFeatEmts$, + this.store.pipe( + select(atlasSelection.selectors.selectedTemplate) ) ]).pipe( - map(([ voi, plotly ]) => { + map(([ voi, plotly, cmpFeatElmts, selectedTemplate ]) => { return { - voi, plotly + voi, plotly, cmpFeatElmts, selectedTemplate } }) ) @@ -217,7 +238,8 @@ export class FeatureViewComponent { this.#feature$, combineLatest([ this.#loadingDetail$, - this.#loadingPlotly$ + this.#loadingPlotly$, + this.busy$, ]).pipe( map(flags => flags.some(f => f)) ), @@ -253,4 +275,19 @@ export class FeatureViewComponent { } }) ) + + async showSubfeature(id: string){ + try { + this.busy$.next(true) + const feature = await this.sapi.getV3FeatureDetailWithId(id).toPromise() + this.store.dispatch( + userInteraction.actions.showFeature({ feature }) + ) + } catch (e) { + console.log('error', e) + this.snackbar.open(`Error: ${e.toString()}`, "Dismiss") + } finally { + this.busy$.next(false) + } + } } diff --git a/src/features/module.ts b/src/features/module.ts index f41e5319b..c168d8929 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -19,7 +19,8 @@ import { ReadmoreModule } from "src/components/readmore"; import { GroupFeatureTallyPipe } from "./grpFeatToTotal.pipe"; import { PlotlyComponent } from "./plotly"; import { AngularMaterialModule } from "src/sharedModules"; -import { FeatureIntents } from "./intents/intents.component"; +import { AtlasColorMapIntents } from "./atlas-colormap-intents"; +import { CompoundFeatureIndicesModule } from "./compoundFeatureIndices" @NgModule({ imports: [ @@ -32,12 +33,13 @@ import { FeatureIntents } from "./intents/intents.component"; NgLayerCtlModule, ReadmoreModule, AngularMaterialModule, + CompoundFeatureIndicesModule, /** * standalone components */ PlotlyComponent, - FeatureIntents, + AtlasColorMapIntents, ], declarations: [ EntryComponent, diff --git a/src/features/pointcloud-intents/index.ts b/src/features/pointcloud-intents/index.ts new file mode 100644 index 000000000..5e0e6b170 --- /dev/null +++ b/src/features/pointcloud-intents/index.ts @@ -0,0 +1,2 @@ +export { isPoint, FilterPointTransformer, CFIndex } from "./util" +export { PointCloudIntents } from "./intents.component" diff --git a/src/features/pointcloud-intents/intents.component.ts b/src/features/pointcloud-intents/intents.component.ts new file mode 100644 index 000000000..a9949ed59 --- /dev/null +++ b/src/features/pointcloud-intents/intents.component.ts @@ -0,0 +1,91 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, Output, inject } from "@angular/core"; +import { BehaviorSubject, Observable, combineLatest } from "rxjs"; +import { Point, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +import { PathReturn } from "src/atlasComponents/sapi/typeV3"; +import { AngularMaterialModule } from "src/sharedModules"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { CFIndex } from "./util"; +import { AnnotationLayer } from "src/atlasComponents/annotations"; +import { map, takeUntil } from "rxjs/operators"; + +type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] + +type Annotation = { + id: string + type: 'point', + point: [number, number, number] +} + +function serializeToId(pt: Point): Annotation{ + return { + id: `${pt.spaceId}-${pt.loc.join("-")}`, + type: 'point', + point: pt.loc.map(v => v*1e6) as [number, number, number] + } +} + +@Component({ + selector: 'pointcloud-intents', + templateUrl: './intents.template.html', + styleUrls: [ + './intents.style.css' + ], + standalone: true, + imports: [ + CommonModule, + AngularMaterialModule + ], + hostDirectives: [ + DestroyDirective + ] +}) + +export class PointCloudIntents { + + readonly #destroy$ = inject(DestroyDirective).destroyed$ + + // not yet used + #intents: Observable + + #points$ = new BehaviorSubject[]>([] as CFIndex[]) + #selectedTemplate$ = new BehaviorSubject(null) + + @Input('points') + set points(val: CFIndex[]) { + this.#points$.next(val) + } + + @Input('selected-template') + set selectedTemplate(tmpl: SxplrTemplate){ + this.#selectedTemplate$.next(tmpl) + } + + spaceMatchedPoints$ = combineLatest([ + this.#points$, + this.#selectedTemplate$ + ]).pipe( + map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id).map(v => v.index)) + ) + + + @Output('on-click') + onClick = new EventEmitter() + + annLayer: AnnotationLayer + constructor(){ + this.annLayer = new AnnotationLayer("intents", "#ff0000") + this.spaceMatchedPoints$.pipe( + takeUntil(this.#destroy$) + ).subscribe(pts => { + const anns = pts.map(serializeToId) + this.annLayer.addAnnotation(anns) + }, + () => {}, + () => { + console.log("dismissing!") + this.annLayer.dispose() + }) + } + +} diff --git a/src/features/pointcloud-intents/intents.style.css b/src/features/pointcloud-intents/intents.style.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/features/pointcloud-intents/intents.template.html b/src/features/pointcloud-intents/intents.template.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/features/pointcloud-intents/util.ts b/src/features/pointcloud-intents/util.ts new file mode 100644 index 000000000..bae7f91ac --- /dev/null +++ b/src/features/pointcloud-intents/util.ts @@ -0,0 +1,24 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { Point, SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; + +export function isPoint(val: string|Point): val is Point{ + return !!(val as any).spaceId && !!(val as any).loc +} + + +export type CFIndex = SimpleCompoundFeature['indices'][number] + + +function cfIndexHasPoint(val: CFIndex): val is CFIndex{ + return isPoint(val.index) +} + +@Pipe({ + name: 'filterForPoints', + pure: true +}) +export class FilterPointTransformer implements PipeTransform{ + public transform(value: CFIndex[]): CFIndex[] { + return value.filter(cfIndexHasPoint) + } +} diff --git a/src/sharedModules/angularMaterial.exports.ts b/src/sharedModules/angularMaterial.exports.ts index bfff6a835..9769df1c2 100644 --- a/src/sharedModules/angularMaterial.exports.ts +++ b/src/sharedModules/angularMaterial.exports.ts @@ -10,5 +10,6 @@ export { Clipboard } from "@angular/cdk/clipboard"; export { UntypedFormControl } from "@angular/forms"; export { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree" export { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +export { MatPaginator } from "@angular/material/paginator"; -export { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing' \ No newline at end of file +export { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing' diff --git a/src/sharedModules/angularMaterial.module.ts b/src/sharedModules/angularMaterial.module.ts index 2a533436b..7e873b5e9 100644 --- a/src/sharedModules/angularMaterial.module.ts +++ b/src/sharedModules/angularMaterial.module.ts @@ -34,6 +34,7 @@ import { MatRadioModule } from "@angular/material/radio"; import { MatTableModule } from "@angular/material/table"; import { MatSortModule } from "@angular/material/sort"; import { A11yModule } from "@angular/cdk/a11y"; +import { MatPaginatorModule } from '@angular/material/paginator' const defaultDialogOption: MatDialogConfig = new MatDialogConfig() @@ -72,6 +73,7 @@ const defaultDialogOption: MatDialogConfig = new MatDialogConfig() MatTableModule, MatSortModule, A11yModule, + MatPaginatorModule, ], providers: [{ provide: MAT_DIALOG_DEFAULT_OPTIONS, diff --git a/src/sharedModules/index.ts b/src/sharedModules/index.ts index 2f35196f4..31092dd26 100644 --- a/src/sharedModules/index.ts +++ b/src/sharedModules/index.ts @@ -1,3 +1,5 @@ export { AngularMaterialModule -} from './angularMaterial.module' \ No newline at end of file +} from './angularMaterial.module' + +export * from './angularMaterial.exports' diff --git a/src/state/userInteraction/store.ts b/src/state/userInteraction/store.ts index c5fd9dbfa..7c5622493 100644 --- a/src/state/userInteraction/store.ts +++ b/src/state/userInteraction/store.ts @@ -30,13 +30,6 @@ export const reducer = createReducer( on( actions.showFeature, (state, { feature }) => { - /** - * do not process compound feature - * allow component to deal with with dialogbox - */ - if (feature.id.startsWith("cf0::")) { - return { ...state } - } return { ...state, selectedFeature: feature diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 1954de2ba..fdb4f3c31 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -507,7 +507,8 @@ export class ViewerCmp implements OnDestroy { userInteraction.actions.mouseoverPosition({ position: { loc: nav.position as [number, number, number], - space: this.templateSelected + space: this.templateSelected, + spaceId: this.templateSelected.id, } }) ) diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index c89257b8c..2526996c8 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -888,10 +888,10 @@ [attr.aria-label]="ARIA_LABELS.CLOSE" class="sxplr-mb-2" > - - Back + Dismiss +

@@ -909,10 +909,10 @@ [attr.aria-label]="ARIA_LABELS.CLOSE" class="sxplr-mb-2" > - - Back + Dismiss +
From 30c7d9bab7ec021af03cfc84e996aaf50325f37e Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 22 Feb 2024 17:35:25 +0100 Subject: [PATCH 22/48] fix lint --- src/features/entry/entry.component.ts | 2 +- src/features/pointcloud-intents/intents.component.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index dc162a46a..6201f9761 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChild, ViewChildren } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChildren } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi'; diff --git a/src/features/pointcloud-intents/intents.component.ts b/src/features/pointcloud-intents/intents.component.ts index a9949ed59..3df8d06bf 100644 --- a/src/features/pointcloud-intents/intents.component.ts +++ b/src/features/pointcloud-intents/intents.component.ts @@ -13,7 +13,7 @@ type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] type Annotation = { id: string - type: 'point', + type: 'point' point: [number, number, number] } @@ -81,7 +81,9 @@ export class PointCloudIntents { const anns = pts.map(serializeToId) this.annLayer.addAnnotation(anns) }, - () => {}, + e => { + console.error("error", e) + }, () => { console.log("dismissing!") this.annLayer.dispose() From d3f413cdbc3fe34ac8e8bcd6b4c31a80ac8add42 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 22 Feb 2024 17:36:17 +0100 Subject: [PATCH 23/48] fix: tests --- src/features/feature-view/feature-view.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/feature-view/feature-view.component.spec.ts b/src/features/feature-view/feature-view.component.spec.ts index d74ac91e2..817347a3a 100644 --- a/src/features/feature-view/feature-view.component.spec.ts +++ b/src/features/feature-view/feature-view.component.spec.ts @@ -6,6 +6,7 @@ import { DARKTHEME } from 'src/util/injectionTokens'; import { FeatureViewComponent } from './feature-view.component'; import { provideMockStore } from '@ngrx/store/testing'; +import { AngularMaterialModule } from 'src/sharedModules'; describe('FeatureViewComponent', () => { let component: FeatureViewComponent; @@ -15,6 +16,7 @@ describe('FeatureViewComponent', () => { await TestBed.configureTestingModule({ imports: [ CommonModule, + AngularMaterialModule, ], declarations: [ FeatureViewComponent ], providers: [ From 6db8fdced715306b782e15e40f29bece3991c72e Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Fri, 23 Feb 2024 14:15:57 +0100 Subject: [PATCH 24/48] feat: added hover compound feat index maint: rc deploy on rancher --- .github/workflows/deploy-helm.yml | 6 ++ .github/workflows/docker_img.yml | 24 ++++- .helm/siibra-explorer/Chart.yaml | 2 +- src/atlasViewer/onhoverSegment.pipe.ts | 29 ------ .../compoundFeatureIndices.template.html | 1 + src/features/compoundFeatureIndices/module.ts | 12 +++ .../pointcloud-intents/intents.component.ts | 94 ++++++++++++++++--- src/mouseoverModule/mouseover.directive.ts | 11 +++ src/mouseoverModule/mouseover.module.ts | 19 +++- src/mouseoverModule/service.ts | 27 ++++++ src/util/injectionTokens.ts | 13 +++ .../viewerCmp/viewerCmp.template.html | 11 +++ 12 files changed, 205 insertions(+), 44 deletions(-) delete mode 100644 src/atlasViewer/onhoverSegment.pipe.ts create mode 100644 src/mouseoverModule/service.ts diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index cce906fa4..cf98e4d1b 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -9,6 +9,10 @@ on: IMAGE_TAG: required: true type: string + IMAGE_DIGEST: + required: false + type: string + default: 'unknown-digest' secrets: KUBECONFIG: @@ -32,12 +36,14 @@ jobs: helm --kubeconfig=$kubecfg_path \ upgrade \ --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ else echo "tag ${{ inputs.DEPLOYMENT_NAME }} not found. Install" helm --kubeconfig=$kubecfg_path \ install \ --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ fi diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index 0e60c8d7b..f9a9b2d21 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -25,6 +25,9 @@ jobs: SIIBRA_API_RC: 'https://siibra-api-rc.apps.hbp.eu/v3_0' SIIBRA_API_LATEST: 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0' + outputs: + IMAGE_DIGEST: ${{ steps.build-docker-image.outputs.IMAGE_DIGEST }} + steps: - uses: actions/checkout@v4 with: @@ -59,7 +62,8 @@ jobs: else echo "dev bulid, enable experimental features" fi - - name: 'Build docker image' + - id: 'build-docker-image' + name: 'Build docker image' run: | DOCKER_BUILT_TAG=${{ env.DOCKER_REGISTRY }}siibra-explorer:$BRANCH_NAME echo "Building $DOCKER_BUILT_TAG" @@ -73,6 +77,10 @@ jobs: echo "Successfully built $DOCKER_BUILT_TAG" echo "DOCKER_BUILT_TAG=$DOCKER_BUILT_TAG" >> $GITHUB_ENV + IMAGE_DIGEST=$(docker inspect --format='{{ index .RepoDigests 0 }}' $DOCKER_BUILT_TAG) + echo "Built image digest: $IMAGE_DIGEST" + echo "IMAGE_DIGEST=$IMAGE_DIGEST" >> $GITHUB_OUTPUT + - name: 'Push to docker registry' run: | echo "Login to docker registry" @@ -138,6 +146,19 @@ jobs: secrets: okd_token: ${{ secrets.OKD_PROD_SECRET }} + trigger-deploy-rc-rancher: + if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'rc' && success() }} + needs: + - build-docker-img + - setting-vars + uses: ./.github/workflows/deploy-helm.yml + with: + DEPLOYMENT_NAME: rc + IMAGE_TAG: ${{ needs.setting-vars.outputs.SXPLR_VERSION }} + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} + secrets: + KUBECONFIG: ${{ secrets.KUBECONFIG }} + trigger-deploy-master-rancher: if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'master' && success() }} needs: @@ -147,6 +168,7 @@ jobs: with: DEPLOYMENT_NAME: master IMAGE_TAG: ${{ needs.setting-vars.outputs.SXPLR_VERSION }} + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.helm/siibra-explorer/Chart.yaml b/.helm/siibra-explorer/Chart.yaml index 301a17e42..6cdf72464 100644 --- a/.helm/siibra-explorer/Chart.yaml +++ b/.helm/siibra-explorer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.1 +version: 0.1.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/src/atlasViewer/onhoverSegment.pipe.ts b/src/atlasViewer/onhoverSegment.pipe.ts deleted file mode 100644 index 5199a582a..000000000 --- a/src/atlasViewer/onhoverSegment.pipe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Pipe, PipeTransform, SecurityContext } from "@angular/core"; -import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; - -@Pipe({ - name: 'transformOnhoverSegment', -}) - -export class TransformOnhoverSegmentPipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) { - - } - - private sanitizeHtml(inc: string): SafeHtml { - return this.sanitizer.sanitize(SecurityContext.HTML, inc) - } - - private getStatus(text: string) { - return ` (${this.sanitizeHtml(text)})` - } - - public transform(segment: any | number): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(( - ( this.sanitizeHtml(segment.name) || segment) + - (segment.status - ? this.getStatus(segment.status) - : '') - )) - } -} diff --git a/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html b/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html index 727aa06ae..2cf127296 100644 --- a/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html +++ b/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html @@ -23,6 +23,7 @@ diff --git a/src/features/compoundFeatureIndices/module.ts b/src/features/compoundFeatureIndices/module.ts index 387b0081f..1f0174e0e 100644 --- a/src/features/compoundFeatureIndices/module.ts +++ b/src/features/compoundFeatureIndices/module.ts @@ -5,6 +5,8 @@ import { CompoundFeatureIndices } from "./compoundFeatureIndices.component"; import { IndexToStrPipe } from "./idxToText.pipe"; import { IndexToIconPipe } from "./idxToIcon.pipe"; import { PointCloudIntents, FilterPointTransformer } from "src/features/pointcloud-intents"; +import { RENDER_CF_POINT, RenderCfPoint } from "../pointcloud-intents/intents.component"; + @NgModule({ imports: [ @@ -20,6 +22,16 @@ import { PointCloudIntents, FilterPointTransformer } from "src/features/pointclo ], exports: [ CompoundFeatureIndices, + ], + providers: [ + { + provide: RENDER_CF_POINT, + useFactory: () => { + const pipe = new IndexToStrPipe() + const renderCfPoint: RenderCfPoint = cfIndex => pipe.transform(cfIndex.index) + return renderCfPoint + } + } ] }) diff --git a/src/features/pointcloud-intents/intents.component.ts b/src/features/pointcloud-intents/intents.component.ts index 3df8d06bf..c0c5af5bb 100644 --- a/src/features/pointcloud-intents/intents.component.ts +++ b/src/features/pointcloud-intents/intents.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Input, Output, inject } from "@angular/core"; +import { Component, EventEmitter, Inject, InjectionToken, Input, Optional, Output, inject } from "@angular/core"; import { BehaviorSubject, Observable, combineLatest } from "rxjs"; import { Point, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { PathReturn } from "src/atlasComponents/sapi/typeV3"; @@ -7,7 +7,8 @@ import { AngularMaterialModule } from "src/sharedModules"; import { DestroyDirective } from "src/util/directives/destroy.directive"; import { CFIndex } from "./util"; import { AnnotationLayer } from "src/atlasComponents/annotations"; -import { map, takeUntil } from "rxjs/operators"; +import { map, takeUntil, withLatestFrom } from "rxjs/operators"; +import { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor, HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] @@ -61,33 +62,104 @@ export class PointCloudIntents { this.#selectedTemplate$.next(tmpl) } - spaceMatchedPoints$ = combineLatest([ + #spaceMatchedCfIndices$ = combineLatest([ this.#points$, this.#selectedTemplate$ ]).pipe( - map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id).map(v => v.index)) + map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id)) ) + #spaceMatchedAnnIdToCfIdx$ = this.#spaceMatchedCfIndices$.pipe( + map(indices => { + const idToIndexMap = new Map>() + for (const idx of indices){ + idToIndexMap.set( + serializeToId(idx.index).id, + idx + ) + } + return idToIndexMap + }) + ) - @Output('on-click') - onClick = new EventEmitter() + @Output('point-clicked') + pointClicked = new EventEmitter>() annLayer: AnnotationLayer - constructor(){ + constructor( + @Inject(RENDER_CF_POINT) render: RenderCfPoint, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) hoverInterceptor: HoverInterceptor, + ){ this.annLayer = new AnnotationLayer("intents", "#ff0000") - this.spaceMatchedPoints$.pipe( + this.#spaceMatchedCfIndices$.pipe( takeUntil(this.#destroy$) - ).subscribe(pts => { - const anns = pts.map(serializeToId) + ).subscribe(indices => { + const anns = indices.map(idx => serializeToId(idx.index)) this.annLayer.addAnnotation(anns) }, e => { console.error("error", e) }, () => { - console.log("dismissing!") this.annLayer.dispose() }) + + this.annLayer.onHover.pipe( + takeUntil(this.#destroy$), + withLatestFrom(this.#spaceMatchedAnnIdToCfIdx$), + ).subscribe(([hover, map]) => { + + if (hoverInterceptor && !!this.#hoveredMessage){ + const { remove } = hoverInterceptor + remove(this.#hoveredMessage) + this.#hoveredMessage = null + } + + this.#hoveredCfIndex = null + + if (!hover) { + return + } + + const idx = map.get(hover.id) + if (!idx) { + console.error(`Couldn't find AnnId: ${hover.id}`) + return + } + + this.#hoveredCfIndex = idx + + if (hoverInterceptor) { + const { append } = hoverInterceptor + const text = render(idx) + this.#hoveredMessage = { + message: `Hovering ${text}` + } + append(this.#hoveredMessage) + } + }) + + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + const onClickHandler = this.onViewerClick.bind(this) + register(onClickHandler) + this.#destroy$.subscribe(() => deregister(onClickHandler)) + } } + onViewerClick(){ + if (this.#hoveredCfIndex) { + this.pointClicked.next(this.#hoveredCfIndex) + return false + } + return true + } + + #hoveredCfIndex: CFIndex = null + #hoveredMessage: THoverConfig = null + } + +export const RENDER_CF_POINT = new InjectionToken("RENDER_CF_POINT") +export type RenderCfPoint = (cfIndex: CFIndex) => string diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts index fad1fbf85..a69fccd76 100644 --- a/src/mouseoverModule/mouseover.directive.ts +++ b/src/mouseoverModule/mouseover.directive.ts @@ -6,6 +6,7 @@ import { TOnHoverObj, temporalPositveScanFn } from "./util" import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; import { userInteraction } from "src/state" import { arrayEqual } from "src/util/array" +import { MouseOverSvc } from "./service" @Directive({ selector: '[iav-mouse-hover]', @@ -14,6 +15,13 @@ import { arrayEqual } from "src/util/array" export class MouseHoverDirective { + /** + * TODO move + * - mousing over regions + * - hovering annotation + * - hovering voi feature + * to use hover interceptor + */ public currentOnHoverObs$: Observable = merge( this.store$.pipe( select(userInteraction.selectors.mousingOverRegions), @@ -58,6 +66,9 @@ export class MouseHoverDirective { constructor( private store$: Store, private annotSvc: ModularUserAnnotationToolService, + private svc: MouseOverSvc, ) { } + + messages$ = this.svc.messages$ } diff --git a/src/mouseoverModule/mouseover.module.ts b/src/mouseoverModule/mouseover.module.ts index b5fbcc9fe..f4c549348 100644 --- a/src/mouseoverModule/mouseover.module.ts +++ b/src/mouseoverModule/mouseover.module.ts @@ -1,8 +1,10 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { TransformOnhoverSegmentPipe } from "src/atlasViewer/onhoverSegment.pipe"; +import { TransformOnhoverSegmentPipe } from "./transformOnhoverSegment.pipe"; import { MouseHoverDirective } from "./mouseover.directive"; import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; +import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; +import { MouseOverSvc } from "./service"; @NgModule({ @@ -18,7 +20,20 @@ import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; MouseHoverDirective, TransformOnhoverSegmentPipe, MouseOverConvertPipe, + ], + providers: [ + MouseOverSvc, + { + provide: HOVER_INTERCEPTOR_INJECTOR, + useFactory: (svc: MouseOverSvc) => { + return { + append: svc.append.bind(svc), + remove: svc.remove.bind(svc), + } + }, + deps: [ MouseOverSvc ] + } ] }) -export class MouseoverModule{} \ No newline at end of file +export class MouseoverModule{} diff --git a/src/mouseoverModule/service.ts b/src/mouseoverModule/service.ts new file mode 100644 index 000000000..eba057bcc --- /dev/null +++ b/src/mouseoverModule/service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { THoverConfig } from "src/util/injectionTokens"; + +@Injectable() +export class MouseOverSvc { + + #messages: THoverConfig[] = [] + + messages$ = new BehaviorSubject(this.#messages) + + set messages(messages: THoverConfig[]){ + this.#messages = messages + this.messages$.next(this.#messages) + } + + get messages(): THoverConfig[]{ + return this.#messages + } + + append(message: THoverConfig){ + this.messages = this.messages.concat(message) + } + remove(message: THoverConfig){ + this.messages = this.messages.filter(v => v !== message) + } +} diff --git a/src/util/injectionTokens.ts b/src/util/injectionTokens.ts index 250a8cce5..3b62ed171 100644 --- a/src/util/injectionTokens.ts +++ b/src/util/injectionTokens.ts @@ -19,6 +19,19 @@ export interface ClickInterceptor{ deregister: (interceptorFunction: (ev: any) => any) => void } +export const HOVER_INTERCEPTOR_INJECTOR = new InjectionToken("HOVER_INTERCEPTOR_INJECTOR") + +export type THoverConfig = { + fontSet?: string + fontIcon?: string + message: string +} + +export interface HoverInterceptor { + append(message: THoverConfig): void + remove(message: THoverConfig): void +} + export const CONTEXT_MENU_ITEM_INJECTOR = new InjectionToken('CONTEXT_MENU_ITEM_INJECTOR') export type TContextMenu = { diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 2526996c8..7530460b3 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -22,6 +22,17 @@ {{ cvtOutput.text }} + + + + + + + {{ message.message }} +
From 975ce49f7cac26d9a1af13ce6ea091636f4ae0d0 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Fri, 23 Feb 2024 15:35:51 +0100 Subject: [PATCH 25/48] fix docker build --- .github/workflows/docker_img.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index f9a9b2d21..8c7ee3cb5 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -77,7 +77,10 @@ jobs: echo "Successfully built $DOCKER_BUILT_TAG" echo "DOCKER_BUILT_TAG=$DOCKER_BUILT_TAG" >> $GITHUB_ENV - IMAGE_DIGEST=$(docker inspect --format='{{ index .RepoDigests 0 }}' $DOCKER_BUILT_TAG) + inspect_str=$(docker inspect --format='json' $DOCKER_BUILT_TAG) + echo "Inspected tag: $inspect_str" + + IMAGE_DIGEST=$(echo $inspect_str | jq -r '.[0].RepoDigests[0]') echo "Built image digest: $IMAGE_DIGEST" echo "IMAGE_DIGEST=$IMAGE_DIGEST" >> $GITHUB_OUTPUT From 6b1339dc89d018c296a2880b1470ee92aba472dd Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Fri, 23 Feb 2024 16:38:54 +0100 Subject: [PATCH 26/48] fix: point cloud intent on destroy remove mouse over maint: helm deploy --- .github/workflows/deploy-helm.yml | 2 +- .github/workflows/docker_img.yml | 13 +++++++-- .helm/adhoc/ingress-main.yml | 29 +++++++++++++++++++ .helm/siibra-explorer/values.yaml | 6 +++- .../pointcloud-intents/intents.component.ts | 10 +++++++ 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 .helm/adhoc/ingress-main.yml diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index cf98e4d1b..561738e2b 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -12,7 +12,7 @@ on: IMAGE_DIGEST: required: false type: string - default: 'unknown-digest' + default: 'default-digest' secrets: KUBECONFIG: diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index 8c7ee3cb5..4f569a2bb 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -77,11 +77,18 @@ jobs: echo "Successfully built $DOCKER_BUILT_TAG" echo "DOCKER_BUILT_TAG=$DOCKER_BUILT_TAG" >> $GITHUB_ENV - inspect_str=$(docker inspect --format='json' $DOCKER_BUILT_TAG) + inspect_str=$(docker image inspect --format='json' $DOCKER_BUILT_TAG) echo "Inspected tag: $inspect_str" IMAGE_DIGEST=$(echo $inspect_str | jq -r '.[0].RepoDigests[0]') echo "Built image digest: $IMAGE_DIGEST" + + IMAGE_DIGEST=$(echo $IMAGE_DIGEST | grep -oP '[a-f0-9]+$') + + # 62 char limit in label + IMAGE_DIGEST=$(echo $IMAGE_DIGEST | grep -oP '^.{6}') + echo "Using first 6 chars of hash: $IMAGE_DIGEST" + echo "IMAGE_DIGEST=$IMAGE_DIGEST" >> $GITHUB_OUTPUT - name: 'Push to docker registry' @@ -150,14 +157,14 @@ jobs: okd_token: ${{ secrets.OKD_PROD_SECRET }} trigger-deploy-rc-rancher: - if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'rc' && success() }} + if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'staging' && success() }} needs: - build-docker-img - setting-vars uses: ./.github/workflows/deploy-helm.yml with: DEPLOYMENT_NAME: rc - IMAGE_TAG: ${{ needs.setting-vars.outputs.SXPLR_VERSION }} + IMAGE_TAG: staging IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.helm/adhoc/ingress-main.yml b/.helm/adhoc/ingress-main.yml new file mode 100644 index 000000000..b26fbad65 --- /dev/null +++ b/.helm/adhoc/ingress-main.yml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: siibra-explorer-main-ingress + labels: + name: siibra-explorer-main-ingress +spec: + rules: + - host: siibra-explorer.apps.tc.humanbrainproject.eu + http: + paths: + - pathType: Prefix + path: "/viewer" + backend: + service: + name: master-siibra-explorer + port: + number: 8080 + - pathType: Prefix + path: "/viewer-staging" + backend: + service: + name: rc-siibra-explorer + port: + number: 8080 + tls: + - secretName: siibra-explorer-prod-secret + hosts: + - siibra-explorer.apps.tc.humanbrainproject.eu diff --git a/.helm/siibra-explorer/values.yaml b/.helm/siibra-explorer/values.yaml index 31631298a..3d78cea24 100644 --- a/.helm/siibra-explorer/values.yaml +++ b/.helm/siibra-explorer/values.yaml @@ -44,7 +44,7 @@ service: port: 8080 ingress: - enabled: true + enabled: false className: "" annotations: {} # kubernetes.io/ingress.class: nginx @@ -100,3 +100,7 @@ env: value: "/viewer" - name: HOSTNAME value: "https://siibra-explorer.apps.tc.humanbrainproject.eu" + - name: OVERWRITE_SPATIAL_ENDPOINT + value: "https://siibra-spatial-backend.apps.tc.humanbrainproject.eu" + - name: OVERWRITE_API_ENDPOINT + value: "https://siibra-api-prod.apps.tc.humanbrainproject.eu/v3_0" diff --git a/src/features/pointcloud-intents/intents.component.ts b/src/features/pointcloud-intents/intents.component.ts index c0c5af5bb..1f9514d9f 100644 --- a/src/features/pointcloud-intents/intents.component.ts +++ b/src/features/pointcloud-intents/intents.component.ts @@ -140,6 +140,16 @@ export class PointCloudIntents { } }) + this.#destroy$.subscribe(() => { + if (hoverInterceptor) { + const { remove } = hoverInterceptor + if (this.#hoveredMessage) { + remove(this.#hoveredMessage) + this.#hoveredMessage = null + } + } + }) + if (clickInterceptor) { const { register, deregister } = clickInterceptor const onClickHandler = this.onViewerClick.bind(this) From 314ae32072d032e084d97e9aa67c27b3d73b59f0 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 28 Feb 2024 10:15:01 +0100 Subject: [PATCH 27/48] expmt feat: allow addition of new custom space --- docs/releases/v2.14.5.md | 1 + .../coordTextBox/coordTextBox.component.ts | 139 ++++++++++++++++++ .../coordTextBox/coordTextBox.style.css | 15 ++ .../coordTextBox/coordTextBox.template.html | 13 ++ src/components/coordTextBox/index.ts | 1 + src/sharedModules/angularMaterial.exports.ts | 1 + src/theme.scss | 2 +- src/viewerModule/nehuba/module.ts | 5 + .../nehubaViewer/nehubaViewer.component.ts | 4 +- .../nehuba/statusCard/statusCard.component.ts | 74 +++++++--- .../statusCard/statusCard.template.html | 132 ++++++++++++----- 11 files changed, 326 insertions(+), 61 deletions(-) create mode 100644 src/components/coordTextBox/coordTextBox.component.ts create mode 100644 src/components/coordTextBox/coordTextBox.style.css create mode 100644 src/components/coordTextBox/coordTextBox.template.html create mode 100644 src/components/coordTextBox/index.ts diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index a1c15104a..6b645d432 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -8,6 +8,7 @@ - Reworded point assignment UI - Allow multi selected region names to be copied - Added legend to region hierarchy +- (experimental) allow addition of custom linear coordinate space ## Bugfix diff --git a/src/components/coordTextBox/coordTextBox.component.ts b/src/components/coordTextBox/coordTextBox.component.ts new file mode 100644 index 000000000..e15e9b04b --- /dev/null +++ b/src/components/coordTextBox/coordTextBox.component.ts @@ -0,0 +1,139 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, HostListener, Input, Output, ViewChild, inject } from "@angular/core"; +import { BehaviorSubject, combineLatest } from "rxjs"; +import { map, shareReplay } from "rxjs/operators"; +import { AngularMaterialModule, MatInput } from "src/sharedModules"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; + +type TTriplet = [number, number, number] +type TVec4 = [number, number, number, number] +export type TAffine = [TVec4, TVec4, TVec4, TVec4] +export type Render = (v: TTriplet) => string + +export function isTVec4(val: unknown): val is TAffine { + if (!Array.isArray(val)) { + return false + } + if (val.length !== 4) { + return false + } + return val.every(v => typeof v === "number") +} + +export function isAffine(val: unknown): val is TAffine { + if (!Array.isArray(val)) { + return false + } + if (val.length !== 4) { + return false + } + return val.every(v => isTVec4(v)) +} + +export function isTriplet(val: unknown): val is TTriplet{ + if (!Array.isArray(val)) { + return false + } + if (val.some(v => typeof v !== "number")) { + return false + } + return val.length === 3 +} + +@Component({ + selector: 'coordinate-text-input', + templateUrl: './coordTextBox.template.html', + styleUrls: [ + './coordTextBox.style.css' + ], + standalone: true, + imports: [ + CommonModule, + AngularMaterialModule + ], + hostDirectives: [ + DestroyDirective + ] +}) + +export class CoordTextBox { + + #destroyed$ = inject(DestroyDirective).destroyed$ + + @ViewChild(MatInput) + input: MatInput + + @Output('enter') + enter = new EventEmitter() + + @HostListener('keydown.enter') + @HostListener('keydown.tab') + enterHandler() { + this.enter.emit() + } + + #coordinates = new BehaviorSubject([0, 0, 0]) + + @Input() + set coordinates(val: unknown) { + if (!isTriplet(val)) { + console.error(`${val} is not TTriplet`) + return + } + this.#coordinates.next(val) + } + + + #affine = new BehaviorSubject([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + + @Input() + set affine(val: unknown) { + if (!isAffine(val)) { + console.error(`${val} is not TAffine!`) + return + } + this.#affine.next(val) + } + + #render = new BehaviorSubject(v => v.join(`, `)) + + @Input() + set render(val: Render) { + this.#render.next(val) + } + + inputValue$ = combineLatest([ + this.#coordinates, + this.#affine, + this.#render, + ]).pipe( + map(([ coord, flattenedAffine, render ]) => { + const [ + [m00, m10, m20, m30], + [m01, m11, m21, m31], + [m02, m12, m22, m32], + // [m03, m13, m23, m33], + ] = flattenedAffine + + const newCoord: TTriplet = [ + coord[0] * m00 + coord[1] * m10 + coord[2] * m20 + 1 * m30, + coord[0] * m01 + coord[1] * m11 + coord[2] * m21 + 1 * m31, + coord[0] * m02 + coord[1] * m12 + coord[2] * m22 + 1 * m32 + ] + return render(newCoord) + }), + shareReplay(1), + ) + + @Input() + label: string = "Coordinates" + + get inputValue(){ + return this.input?.value + } +} diff --git a/src/components/coordTextBox/coordTextBox.style.css b/src/components/coordTextBox/coordTextBox.style.css new file mode 100644 index 000000000..e0db9f988 --- /dev/null +++ b/src/components/coordTextBox/coordTextBox.style.css @@ -0,0 +1,15 @@ +:host +{ + display: flex; + width: 100%; +} + +mat-form-field +{ + flex: 1 1 auto; +} + +.suffix +{ + flex: 0 0 auto; +} diff --git a/src/components/coordTextBox/coordTextBox.template.html b/src/components/coordTextBox/coordTextBox.template.html new file mode 100644 index 000000000..ddf0c59a1 --- /dev/null +++ b/src/components/coordTextBox/coordTextBox.template.html @@ -0,0 +1,13 @@ + + + {{ label }} + + + + + + +
+ +
diff --git a/src/components/coordTextBox/index.ts b/src/components/coordTextBox/index.ts new file mode 100644 index 000000000..fe8e63ce1 --- /dev/null +++ b/src/components/coordTextBox/index.ts @@ -0,0 +1 @@ +export * from "./coordTextBox.component" diff --git a/src/sharedModules/angularMaterial.exports.ts b/src/sharedModules/angularMaterial.exports.ts index 9769df1c2..09611d70f 100644 --- a/src/sharedModules/angularMaterial.exports.ts +++ b/src/sharedModules/angularMaterial.exports.ts @@ -11,5 +11,6 @@ export { UntypedFormControl } from "@angular/forms"; export { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree" export { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; export { MatPaginator } from "@angular/material/paginator"; +export { MatInput } from "@angular/material/input"; export { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing' diff --git a/src/theme.scss b/src/theme.scss index 4898089b8..d184be4f4 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -133,7 +133,7 @@ $sxplr-dark-theme: mat.define-dark-theme(( { @include mat.all-component-themes($sxplr-dark-theme); @include custom-cmp($sxplr-dark-theme); - input[type="text"] + input[type="text"],textarea { caret-color: white!important; } diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index 485c478b8..f7e7026c1 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -29,6 +29,8 @@ import { NgAnnotationEffects } from "./annotation/effects"; import { NehubaViewerContainer } from "./nehubaViewerInterface/nehubaViewerContainer.component"; import { NehubaUserLayerModule } from "./userLayers"; import { DialogModule } from "src/ui/dialogInfo"; +import { CoordTextBox } from "src/components/coordTextBox"; +import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; @NgModule({ imports: [ @@ -60,6 +62,9 @@ import { DialogModule } from "src/ui/dialogInfo"; QuickTourModule, NehubaLayoutOverlayModule, DialogModule, + + CoordTextBox, + ExperimentalFlagDirective ], declarations: [ NehubaViewerContainerDirective, diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 0c4b2ca09..6e2f8251f 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -67,7 +67,7 @@ export class NehubaViewerUnit implements OnDestroy { public viewerPosInVoxel$ = new BehaviorSubject(null) public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null) public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null) - public mousePosInReal$ = new BehaviorSubject(null) + public mousePosInReal$ = new BehaviorSubject<[number, number, number]>(null) private exportNehuba: any @@ -869,7 +869,7 @@ export class NehubaViewerUnit implements OnDestroy { if (this.#translateVoxelToReal) { const coordInReal = this.#translateVoxelToReal(coordInVoxel) - this.mousePosInReal$.next( coordInReal ) + this.mousePosInReal$.next( coordInReal as [number, number, number] ) } }), diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index 7d9a0886b..7dc07eb9a 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -9,7 +9,7 @@ import { select, Store } from "@ngrx/store"; import { LoggingService } from "src/logging"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { Observable, Subject, concat, of } from "rxjs"; -import { map, filter, takeUntil, switchMap, shareReplay, debounceTime } from "rxjs/operators"; +import { map, filter, takeUntil, switchMap, shareReplay, debounceTime, scan } from "rxjs/operators"; import { Clipboard, MatBottomSheet, MatSnackBar } from "src/sharedModules/angularMaterial.exports" import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { FormControl, FormGroup } from "@angular/forms"; @@ -22,6 +22,12 @@ import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { NEHUBA_CONFIG_SERVICE_TOKEN, NehubaConfigSvc } from "../config.service"; import { DestroyDirective } from "src/util/directives/destroy.directive"; import { getUuid } from "src/util/fn"; +import { Render, TAffine, isAffine } from "src/components/coordTextBox" + +type TSpace = { + label: string + affine: TAffine +} @Component({ selector : 'iav-cmp-viewer-nehuba-status', @@ -33,6 +39,41 @@ import { getUuid } from "src/util/fn"; }) export class StatusCardComponent { + #newSpace = new Subject() + additionalSpace$ = this.#newSpace.pipe( + scan((acc, v) => acc.concat(v), [] as TSpace[]) + ) + readonly idAffStr = `[ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] +] +` + readonly defaultLabel = `New Space` + reset(label: HTMLInputElement, affine: HTMLTextAreaElement){ + label.value = this.defaultLabel + affine.value = this.idAffStr + } + add(label: HTMLInputElement, affine: HTMLTextAreaElement) { + try { + const aff = JSON.parse(affine.value) + if (!isAffine(aff)) { + throw new Error(`${affine.value} cannot be parsed into 4x4 affine`) + } + this.#newSpace.next({ + label: label.value, + affine: aff + }) + } catch (e) { + console.error(`Error: ${e.toString()}`) + } + + } + + readonly renderMm: Render = v => v.map(i => `${i}mm`).join(", ") + readonly renderDefault: Render = v => v.map(i => i.toFixed(3)).join(", ") + readonly #destroy$ = inject(DestroyDirective).destroyed$ public nehubaViewer: NehubaViewerUnit @@ -57,30 +98,25 @@ export class StatusCardComponent { zoom: number perspectiveOrientation: number[] perspectiveZoom: number -} + } - public readonly navVal$ = this.nehubaViewer$.pipe( + readonly navigation$ = this.nehubaViewer$.pipe( filter(v => !!v), - switchMap(nehubaViewer => - concat( - of(`nehubaViewer initialising`), - nehubaViewer.viewerPosInReal$.pipe( - filter(v => !!v), - map(real => real.map(v => `${ (v / 1e6).toFixed(3) }mm`).join(', ')) - ) - ) - ), + switchMap(nv => nv.viewerPosInReal$.pipe( + map(vals => (vals || [0, 0, 0]).map(v => Number((v / 1e6).toFixed(3)))) + )), shareReplay(1), ) - public readonly mouseVal$ = this.nehubaViewer$.pipe( + + readonly navVal$ = this.navigation$.pipe( + map(v => v.map(v => `${v}mm`).join(", ")) + ) + readonly mouseVal$ = this.nehubaViewer$.pipe( filter(v => !!v), switchMap(nehubaViewer => - concat( - of(``), - nehubaViewer.mousePosInReal$.pipe( - filter(v => !!v), - map(real => real.map(v => `${ (v/1e6).toFixed(3) }mm`).join(', ')) - ) + nehubaViewer.mousePosInReal$.pipe( + filter(v => !!v), + map(real => real.map(v => Number((v/1e6).toFixed(3)))) ), ) ) diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html index 57da3b774..2b17c8e75 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.template.html +++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html @@ -47,48 +47,67 @@
+ + + + + + + + +
- - - Physical Coord - - - - - - - - + +
+ + + + + +
+ + + + - - - Cursor Position - - - +
+ + +
@@ -153,4 +172,39 @@

- \ No newline at end of file + + + +

+ Add a new coordinate space +

+ + + + Label + + + + + + + + Affine + + + + + + + + + + + +
From 09920ff00813c3824c916ec6991808dcdc30ce96 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 28 Feb 2024 10:55:37 +0100 Subject: [PATCH 28/48] tests: add and fix tests --- common/util.spec.js | 2 - .../coordTextBox.component.spec.ts | 240 ++++++++++++++++++ .../coordTextBox/coordTextBox.component.ts | 6 +- 3 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 src/components/coordTextBox/coordTextBox.component.spec.ts diff --git a/common/util.spec.js b/common/util.spec.js index d8142ee15..54609f3bf 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -172,7 +172,6 @@ describe('common/util.js', () => { } finally { const end = performance.now() expect(end - start).toBeGreaterThanOrEqual(defaultTimeout) - expect(end - start).toBeLessThan(defaultTimeout + 20) } }) }) @@ -197,7 +196,6 @@ describe('common/util.js', () => { } finally { const end = performance.now() expect(end - start).toBeGreaterThanOrEqual(timeout) - expect(end - start).toBeLessThan(timeout + 20) } }) }) diff --git a/src/components/coordTextBox/coordTextBox.component.spec.ts b/src/components/coordTextBox/coordTextBox.component.spec.ts new file mode 100644 index 000000000..2596b8dce --- /dev/null +++ b/src/components/coordTextBox/coordTextBox.component.spec.ts @@ -0,0 +1,240 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" +import { CoordTextBox, Render, isAffine, isTVec4, isTriplet } from "./coordTextBox.component" +import { Component } from "@angular/core" +import { NoopAnimationsModule } from "@angular/platform-browser/animations" + +describe("isTriplet", () => { + describe("> correctly returns true", () => { + const triplets = [ + [1, 2, 3], + [0, 0, 0], + [1e10, -1e10, 0] + ] + for (const triplet of triplets){ + it(`> for ${triplet}`, () => { + expect( + isTriplet(triplet) + ).toBeTrue() + }) + } + }) + + describe("> correctly returns false", () => { + const notTriplets = [ + [1, 2], + [1, 2, 3, 4], + ['foo', 1, 2], + [NaN, 1, 2], + [[], 1, 2] + ] + for (const notTriplet of notTriplets) { + it(`> for ${notTriplet}`, () => { + expect( + isTriplet(notTriplet) + ).toBeFalse() + }) + } + }) +}) + +describe("isTVec4", () => { + describe("> correctly returns true", () => { + const triplets = [ + [1, 2, 3, 4], + [0, 0, 0, 0], + [1e10, -1e10, 0, 0] + ] + for (const triplet of triplets){ + it(`> for ${triplet}`, () => { + expect( + isTVec4(triplet) + ).toBeTrue() + }) + } + }) + + describe("> correctly returns false", () => { + const notTriplets = [ + [1, 2, 3], + [1, 2, 3, 4, 5], + ['foo', 1, 2, 3], + [NaN, 1, 2, 3], + [[], 1, 2, 3] + ] + for (const notTriplet of notTriplets) { + it(`> for ${notTriplet}`, () => { + expect( + isTVec4(notTriplet) + ).toBeFalse() + }) + } + }) +}) + +describe("isAffine", () => { + describe("> correctly returns true", () => { + const triplets = [ + [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[1e10, -1e10, 0, 0], [1e10, -1e10, 0, 0], [1e10, -1e10, 0, 0], [1e10, -1e10, 0, 0]] + ] + for (const triplet of triplets){ + it(`> for ${triplet}`, () => { + expect( + isAffine(triplet) + ).toBeTrue() + }) + } + }) + + describe("> correctly returns false", () => { + const notTriplets = [ + [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3]], + [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4, 5]], + ] + for (const notTriplet of notTriplets) { + it(`> for ${notTriplet}`, () => { + expect( + isAffine(notTriplet) + ).toBeFalse() + }) + } + }) +}) + + +describe("CoordTextBox", () => { + + @Component({ + template: `` + }) + class Dummy { + coord = [1, 2, 3] + iden = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + translate = [ + [1, 0, 0, 2], + [0, 1, 0, 4], + [0, 0, 1, 6], + [0, 0, 0, 1], + ] + scale = [ + [2, 0, 0, 0], + [0, 4, 0, 0], + [0, 0, 8, 0], + [0, 0, 0, 1], + ] + + render: Render = v => v.map(v => `${v}f`).join(":") + } + + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CoordTextBox, + NoopAnimationsModule, + ], + declarations: [Dummy] + }) + // not yet compiled + }) + + describe("> correct affine inputs", () => { + describe("> iden", () => { + beforeEach(async () => { + await TestBed.overrideComponent(Dummy, { + set: { + template: ` + + + ` + } + }).compileComponents() + }) + + it("> renders correctly", () => { + fixture = TestBed.createComponent(Dummy) + fixture.detectChanges() + const input = fixture.nativeElement.querySelector('input') + expect(input.value).toEqual(`1, 2, 3`) + }) + }) + describe("> translate", () => { + beforeEach(async () => { + await TestBed.overrideComponent(Dummy, { + set: { + template: ` + + + ` + } + }).compileComponents() + }) + + it("> renders correctly", () => { + fixture = TestBed.createComponent(Dummy) + fixture.detectChanges() + const input = fixture.nativeElement.querySelector('input') + expect(input.value).toEqual(`3, 6, 9`) + }) + }) + describe("> scale", () => { + beforeEach(async () => { + await TestBed.overrideComponent(Dummy, { + set: { + template: ` + + + ` + } + }).compileComponents() + }) + + it("> renders correctly", () => { + fixture = TestBed.createComponent(Dummy) + fixture.detectChanges() + const input = fixture.nativeElement.querySelector('input') + expect(input.value).toEqual(`2, 8, 24`) + }) + }) + }) + + describe("> correct render inputs", () => { + describe("> render", () => { + beforeEach(async () => { + await TestBed.overrideComponent(Dummy, { + set: { + template: ` + + + ` + } + }).compileComponents() + }) + + it("> renders correctly", () => { + fixture = TestBed.createComponent(Dummy) + fixture.detectChanges() + const input = fixture.nativeElement.querySelector('input') + expect(input.value).toEqual(`1f:2f:3f`) + }) + }) + }) +}) \ No newline at end of file diff --git a/src/components/coordTextBox/coordTextBox.component.ts b/src/components/coordTextBox/coordTextBox.component.ts index e15e9b04b..6f3d33fd7 100644 --- a/src/components/coordTextBox/coordTextBox.component.ts +++ b/src/components/coordTextBox/coordTextBox.component.ts @@ -17,7 +17,7 @@ export function isTVec4(val: unknown): val is TAffine { if (val.length !== 4) { return false } - return val.every(v => typeof v === "number") + return val.every(v => typeof v === "number" && !isNaN(v)) } export function isAffine(val: unknown): val is TAffine { @@ -34,10 +34,10 @@ export function isTriplet(val: unknown): val is TTriplet{ if (!Array.isArray(val)) { return false } - if (val.some(v => typeof v !== "number")) { + if (val.length !== 3) { return false } - return val.length === 3 + return val.every(v => typeof v === "number" && !isNaN(v)) } @Component({ From 599fdf8cd6f0c30d06ae50f9aae04f06301faba6 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 28 Feb 2024 12:50:57 +0100 Subject: [PATCH 29/48] fix: action update checkout version fix: docker image hash to git hash --- .github/workflows/deploy-helm.yml | 2 +- .github/workflows/docker_img.yml | 20 +++++++++----------- .github/workflows/e2e.yml | 2 +- .github/workflows/manual_e2e.yml | 4 ++-- .github/workflows/release-ci.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index 561738e2b..e30c8159c 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -22,7 +22,7 @@ jobs: trigger-deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 'Deploy' run: | kubecfg_path=${{ runner.temp }}/.kube_config diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index 4f569a2bb..ad5df0476 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -26,7 +26,7 @@ jobs: SIIBRA_API_LATEST: 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0' outputs: - IMAGE_DIGEST: ${{ steps.build-docker-image.outputs.IMAGE_DIGEST }} + GIT_DIGEST: ${{ steps.build-docker-image.outputs.GIT_DIGEST }} steps: - uses: actions/checkout@v4 @@ -80,16 +80,14 @@ jobs: inspect_str=$(docker image inspect --format='json' $DOCKER_BUILT_TAG) echo "Inspected tag: $inspect_str" - IMAGE_DIGEST=$(echo $inspect_str | jq -r '.[0].RepoDigests[0]') - echo "Built image digest: $IMAGE_DIGEST" - - IMAGE_DIGEST=$(echo $IMAGE_DIGEST | grep -oP '[a-f0-9]+$') + GIT_DIGEST=${{ github.sha }} + echo "Git digest: $GIT_DIGEST" # 62 char limit in label - IMAGE_DIGEST=$(echo $IMAGE_DIGEST | grep -oP '^.{6}') - echo "Using first 6 chars of hash: $IMAGE_DIGEST" + GIT_DIGEST=$(echo $GIT_DIGEST | grep -oP '^.{6}') + echo "Using first 6 chars of hash: $GIT_DIGEST" - echo "IMAGE_DIGEST=$IMAGE_DIGEST" >> $GITHUB_OUTPUT + echo "GIT_DIGEST=$GIT_DIGEST" >> $GITHUB_OUTPUT - name: 'Push to docker registry' run: | @@ -117,7 +115,7 @@ jobs: DEPLOY_ID: ${{ steps.set-vars.outputs.DEPLOY_ID }} SXPLR_VERSION: ${{ steps.set-vars.outputs.SXPLR_VERSION }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: set-vars name: Set vars run: | @@ -165,7 +163,7 @@ jobs: with: DEPLOYMENT_NAME: rc IMAGE_TAG: staging - IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.GIT_DIGEST }} secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} @@ -178,7 +176,7 @@ jobs: with: DEPLOYMENT_NAME: master IMAGE_TAG: ${{ needs.setting-vars.outputs.SXPLR_VERSION }} - IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.GIT_DIGEST }} secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ec46c942a..5ddc69dde 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -59,7 +59,7 @@ jobs: failure-state: ${{ steps.failure-state-step.outputs.failure-state }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.ref }} diff --git a/.github/workflows/manual_e2e.yml b/.github/workflows/manual_e2e.yml index 8fddfeb8f..945946d7a 100644 --- a/.github/workflows/manual_e2e.yml +++ b/.github/workflows/manual_e2e.yml @@ -9,7 +9,7 @@ jobs: hide_previous_if_exists: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: 'master' - uses: actions/github-script@v5 @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml index a4ab67194..792eacb98 100644 --- a/.github/workflows/release-ci.yml +++ b/.github/workflows/release-ci.yml @@ -9,7 +9,7 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | MASTER_VERSION=$(git show origin/master:package.json | jq '.version') THIS_VERSION=$(jq '.version' < package.json) @@ -19,7 +19,7 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | VERSION_NUM=$(jq '.version' < package.json) VERSION_NUM=${VERSION_NUM#\"} @@ -30,7 +30,7 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | VERSION_NUM=$(jq '.version' < package.json) VERSION_NUM=${VERSION_NUM#\"} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bd7a7b10..0262f2b79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set version id: set-version run: | @@ -24,7 +24,7 @@ jobs: if: success() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Create Release id: create_release uses: actions/create-release@v1 From a4a4a87fe0911d792c87a671aabad3d440d3249a Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 28 Feb 2024 15:46:24 +0100 Subject: [PATCH 30/48] feat: experimental update to selected region side panel feat: allow experimental flag to be set at deploy time via env var maint: update charts --- .github/workflows/deploy-helm.yml | 77 ++++++++++++++----- .helm/adhoc/ingress-main.yml | 7 ++ .helm/siibra-explorer/Chart.yaml | 2 +- .../siibra-explorer/templates/deployment.yaml | 6 +- .helm/siibra-explorer/values.yaml | 14 ++-- backend/app/config.py | 2 + backend/app/const.py | 2 + backend/app/index_html.py | 7 +- common/constants.js | 1 + docs/releases/v2.14.5.md | 1 + .../regionListSearch.template.html | 3 +- .../bottomMenuCmp/bottomMenu.component.ts | 24 ++++-- .../bottomMenuCmp/bottomMenu.style.scss | 4 +- .../bottomMenuCmp/bottomMenu.template.html | 49 ++++++++++++ src/ui/bottomMenu/module.ts | 3 + .../topMenu/topMenuCmp/topMenu.components.ts | 8 ++ .../topMenu/topMenuCmp/topMenu.template.html | 4 +- src/viewerModule/module.ts | 4 + .../viewerCmp/viewerCmp.template.html | 19 ++++- 19 files changed, 190 insertions(+), 47 deletions(-) diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index e30c8159c..be72a299c 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -5,7 +5,7 @@ on: inputs: DEPLOYMENT_NAME: required: true - type: string + type: string # prod, rc, expmt IMAGE_TAG: required: true type: string @@ -19,32 +19,67 @@ on: required: true jobs: - trigger-deploy: + + trigger-deploy-prod: + runs-on: ubuntu-latest + if: ${{ inputs.DEPLOYMENT_NAME == 'prod' }} + steps: + - uses: actions/checkout@v4 + - name: 'Deploy' + run: | + kubecfg_path=${{ runner.temp }}/.kube_config + echo "${{ secrets.KUBECONFIG }}" > $kubecfg_path + + helm --kubeconfig=$kubecfg_path \ + upgrade \ + --history-max 3 \ + --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ + ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ + + rm $kubecfg_path + + trigger-deploy-rc: + runs-on: ubuntu-latest + if: ${{ inputs.DEPLOYMENT_NAME == 'rc' }} + steps: + - uses: actions/checkout@v4 + - name: 'Deploy' + run: | + kubecfg_path=${{ runner.temp }}/.kube_config + echo "${{ secrets.KUBECONFIG }}" > $kubecfg_path + + helm --kubeconfig=$kubecfg_path \ + upgrade \ + --history-max 3 \ + --reuse-values \ + --set envObj.HOST_PATHNAME=/viewer-staging \ + --set envObj.OVERWRITE_API_ENDPOINT=https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0 \ + --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ + ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ + + rm $kubecfg_path + + trigger-deploy-expmt: runs-on: ubuntu-latest + if: ${{ inputs.DEPLOYMENT_NAME == 'expmt' }} steps: - uses: actions/checkout@v4 - name: 'Deploy' run: | kubecfg_path=${{ runner.temp }}/.kube_config echo "${{ secrets.KUBECONFIG }}" > $kubecfg_path - helm --kubeconfig=$kubecfg_path status ${{ inputs.DEPLOYMENT_NAME }} - helm_status=$(echo $?) - - if [[ $helm_status = "0" ]] - then - echo "tag ${{ inputs.DEPLOYMENT_NAME }} found. Update" - helm --kubeconfig=$kubecfg_path \ - upgrade \ - --set image.tag=${{ inputs.IMAGE_TAG }} \ - --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ - ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ - else - echo "tag ${{ inputs.DEPLOYMENT_NAME }} not found. Install" - helm --kubeconfig=$kubecfg_path \ - install \ - --set image.tag=${{ inputs.IMAGE_TAG }} \ - --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ - ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ - fi + helm --kubeconfig=$kubecfg_path \ + upgrade \ + --history-max 3 \ + --reuse-values \ + --set envObj.OVERWRITE_EXPERIMENTAL_FLAG_ATTR=1 \ + --set envObj.HOST_PATHNAME=/viewer-exmpt \ + --set envObj.OVERWRITE_API_ENDPOINT=https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0 \ + --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ + ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ + rm $kubecfg_path diff --git a/.helm/adhoc/ingress-main.yml b/.helm/adhoc/ingress-main.yml index b26fbad65..3c69b3b6a 100644 --- a/.helm/adhoc/ingress-main.yml +++ b/.helm/adhoc/ingress-main.yml @@ -23,6 +23,13 @@ spec: name: rc-siibra-explorer port: number: 8080 + - pathType: Prefix + path: "/viewer-expmt" + backend: + service: + name: expmt-siibra-explorer + port: + number: 8080 tls: - secretName: siibra-explorer-prod-secret hosts: diff --git a/.helm/siibra-explorer/Chart.yaml b/.helm/siibra-explorer/Chart.yaml index 6cdf72464..99dfe3ddc 100644 --- a/.helm/siibra-explorer/Chart.yaml +++ b/.helm/siibra-explorer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 +version: 0.1.5 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/.helm/siibra-explorer/templates/deployment.yaml b/.helm/siibra-explorer/templates/deployment.yaml index 098a16f64..ce0cfb66e 100644 --- a/.helm/siibra-explorer/templates/deployment.yaml +++ b/.helm/siibra-explorer/templates/deployment.yaml @@ -54,7 +54,11 @@ spec: # path: / # port: http env: - {{- toYaml .Values.env | nindent 12 }} + {{- range $key, $val := .Values.envObj }} + - name: {{ $key }} + value: {{ $val }} + {{- end }} + resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.volumeMounts }} diff --git a/.helm/siibra-explorer/values.yaml b/.helm/siibra-explorer/values.yaml index 3d78cea24..602d70a8b 100644 --- a/.helm/siibra-explorer/values.yaml +++ b/.helm/siibra-explorer/values.yaml @@ -95,12 +95,8 @@ tolerations: [] affinity: {} -env: - - name: HOST_PATHNAME - value: "/viewer" - - name: HOSTNAME - value: "https://siibra-explorer.apps.tc.humanbrainproject.eu" - - name: OVERWRITE_SPATIAL_ENDPOINT - value: "https://siibra-spatial-backend.apps.tc.humanbrainproject.eu" - - name: OVERWRITE_API_ENDPOINT - value: "https://siibra-api-prod.apps.tc.humanbrainproject.eu/v3_0" +envObj: + HOSTNAME: https://siibra-explorer.apps.tc.humanbrainproject.eu + OVERWRITE_SPATIAL_ENDPOINT: https://siibra-spatial-backend.apps.tc.humanbrainproject.eu + HOST_PATHNAME: /viewer + OVERWRITE_API_ENDPOINT: https://siibra-api-prod.apps.tc.humanbrainproject.eu/v3_0 diff --git a/backend/app/config.py b/backend/app/config.py index 43f49b315..27c5b3640 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -10,6 +10,8 @@ OVERWRITE_SPATIAL_ENDPOINT = os.getenv("OVERWRITE_SPATIAL_ENDPOINT") +EXPERIMENTAL_FLAG = os.getenv("EXPERIMENTAL_FLAG") + LOCAL_CDN = os.getenv("LOCAL_CDN") HBP_CLIENTID_V2 = os.getenv("HBP_CLIENTID_V2", "no hbp id") diff --git a/backend/app/const.py b/backend/app/const.py index ca7201499..071545dee 100644 --- a/backend/app/const.py +++ b/backend/app/const.py @@ -21,3 +21,5 @@ OVERWRITE_SAPI_ENDPOINT_ATTR = "x-sapi-base-url" OVERWRITE_SPATIAL_BACKEND_ATTR = "x-spatial-backend-url" + +OVERWRITE_EXPERIMENTAL_FLAG_ATTR = "x-experimental-flag" diff --git a/backend/app/index_html.py b/backend/app/index_html.py index ea4baf68f..504ec7bb1 100644 --- a/backend/app/index_html.py +++ b/backend/app/index_html.py @@ -2,8 +2,8 @@ from pathlib import Path from fastapi.responses import Response from typing import Dict -from .const import ERROR_KEY, DATA_ERROR_ATTR, OVERWRITE_SAPI_ENDPOINT_ATTR, COOKIE_KWARGS, OVERWRITE_SPATIAL_BACKEND_ATTR -from .config import PATH_TO_PUBLIC, OVERWRITE_API_ENDPOINT, OVERWRITE_SPATIAL_ENDPOINT +from .const import ERROR_KEY, DATA_ERROR_ATTR, OVERWRITE_SAPI_ENDPOINT_ATTR, COOKIE_KWARGS, OVERWRITE_SPATIAL_BACKEND_ATTR, OVERWRITE_EXPERIMENTAL_FLAG_ATTR +from .config import PATH_TO_PUBLIC, OVERWRITE_API_ENDPOINT, OVERWRITE_SPATIAL_ENDPOINT, EXPERIMENTAL_FLAG path_to_index = Path(PATH_TO_PUBLIC) / "index.html" index_html: str = None @@ -35,6 +35,9 @@ async def get_index_html(request: Request): if OVERWRITE_SPATIAL_ENDPOINT: attributes_to_append[OVERWRITE_SPATIAL_BACKEND_ATTR] = OVERWRITE_SPATIAL_ENDPOINT + if EXPERIMENTAL_FLAG: + attributes_to_append[OVERWRITE_EXPERIMENTAL_FLAG_ATTR] = EXPERIMENTAL_FLAG + attr_string = " ".join([f"{key}={_monkey_sanitize(value)}" for key, value in attributes_to_append.items()]) resp_string = index_html.replace("", f"") diff --git a/common/constants.js b/common/constants.js index 4ddf69753..0e9f6b312 100644 --- a/common/constants.js +++ b/common/constants.js @@ -151,6 +151,7 @@ If you do not accept the Terms & Conditions you are not permitted to access or u OVERWRITE_SAPI_ENDPOINT_ATTR: `x-sapi-base-url`, OVERWRITE_SPATIAL_BACKEND_ATTR: `x-spatial-backend-url`, + OVERWRITE_EXPERIMENTAL_FLAG_ATTR: `x-experimental-flag`, DATA_ERROR_ATTR: `data-error`, } diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index 6b645d432..895c81d44 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -8,6 +8,7 @@ - Reworded point assignment UI - Allow multi selected region names to be copied - Added legend to region hierarchy +- Allow experimental flag to be set to be on at runtime (this also shows the button, allow toggling of experimental features) - (experimental) allow addition of custom linear coordinate space ## Bugfix diff --git a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html index 92e413e06..2de2fc4b5 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html +++ b/src/atlasComponents/sapiViews/core/rich/regionsListSearch/regionListSearch.template.html @@ -6,8 +6,7 @@ { + map(([ { atlas, parcellation, template }, selectedRegions, selectedFeature ]) => { return { selectedAtlas: atlas, selectedParcellation: parcellation, selectedTemplate: template, - selectedRegions + selectedRegions, + selectedFeature } - }) + }), + shareReplay(1) ) constructor(private store: Store){} @@ -49,6 +55,12 @@ export class BottomMenuCmp{ ) } + clearFeature(){ + this.store.dispatch( + userInteraction.actions.clearShownFeature() + ) + } + onATPMenuOpen(opts: { all: boolean, some: boolean, none: boolean }){ if (opts.all) { this.menuOpen = 'all' diff --git a/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.style.scss b/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.style.scss index 872726cdc..726a53f74 100644 --- a/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.style.scss +++ b/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.style.scss @@ -18,12 +18,12 @@ flex-wrap: nowrap; } -:host sxplr-smart-chip +sxplr-smart-chip.region-chip { margin-left: -2.5rem; } -sxplr-smart-chip .regionname +sxplr-smart-chip.region-chip .regionname { margin-left: 0.5rem; } diff --git a/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html b/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html index aba953d67..f0e7a78b0 100644 --- a/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html +++ b/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html @@ -1,3 +1,5 @@ + + @@ -8,6 +10,7 @@ + + + + + + + + + + + + + Feature + + + + + + {{ feature.name }} + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/bottomMenu/module.ts b/src/ui/bottomMenu/module.ts index 48ff73d96..3e8abc5ac 100644 --- a/src/ui/bottomMenu/module.ts +++ b/src/ui/bottomMenu/module.ts @@ -5,6 +5,7 @@ import { ATPSelectorModule } from "src/atlasComponents/sapiViews/core/rich/ATPSe import { SmartChipModule } from "src/components/smartChip"; import { SapiViewsCoreRegionModule } from "src/atlasComponents/sapiViews/core/region"; import { AngularMaterialModule } from "src/sharedModules"; +import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; @NgModule({ imports: [ @@ -13,6 +14,8 @@ import { AngularMaterialModule } from "src/sharedModules"; SmartChipModule, SapiViewsCoreRegionModule, AngularMaterialModule, + + ExperimentalFlagDirective, ], declarations: [ BottomMenuCmp, diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index 580ab86fc..4adf087fe 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, + Inject, Input, TemplateRef, } from "@angular/core"; @@ -14,6 +15,7 @@ import { TypeMatBtnColor, TypeMatBtnStyle } from "src/components/dynamicMaterial import { select, Store } from "@ngrx/store"; import { userPreference } from "src/state"; import { environment } from "src/environments/environment" +import { GET_ATTR_TOKEN, GetAttr } from "src/util/constants"; @Component({ selector: 'top-menu-cmp', @@ -84,9 +86,15 @@ export class TopMenuCmp { private authService: AuthService, private dialog: MatDialog, public bottomSheet: MatBottomSheet, + @Inject(GET_ATTR_TOKEN) getAttr: GetAttr ) { this.user$ = this.authService.user$ + const experimentalFlag = getAttr(CONST.OVERWRITE_EXPERIMENTAL_FLAG_ATTR) + if (experimentalFlag) { + this.showExperimentalToggle = !!experimentalFlag + } + this.userBtnTooltip$ = this.user$.pipe( map(user => user ? `Logged in as ${(user && user.name) ? user.name : 'Unknown name'}` diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index 1a36b61c6..43d695b91 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -145,7 +145,7 @@ matTooltip="Toggle experimental flag"> @@ -153,7 +153,7 @@ - + diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index e2b3b4589..81d404525 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -39,6 +39,7 @@ import { CURRENT_TEMPLATE_DIM_INFO, TemplateInfo, Z_TRAVERSAL_MULTIPLIER } from import { Store } from "@ngrx/store"; import { atlasSelection, userPreference } from "src/state"; import { TabComponent } from "src/components/tab/tab.components"; +import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; @NgModule({ imports: [ @@ -67,6 +68,9 @@ import { TabComponent } from "src/components/tab/tab.components"; ReactiveFormsModule, BottomMenuModule, TabComponent, + + ExperimentalFlagDirective, + ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], declarations: [ diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 7530460b3..9528a948d 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -560,10 +560,12 @@ + +
@@ -588,7 +590,22 @@ (sxplr-sapiviews-core-region-navigate-to)="navigateTo($event)" #regionDirective="sapiViewsCoreRegionRich" > -
+ + + + + +
+
+ + + + +
+ From 40924a0fcf7e172838323a4e35eb721c8d3e6823 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Fri, 1 Mar 2024 17:57:24 +0100 Subject: [PATCH 31/48] refactor: remove ad hoc mouse over, migrated to generalized impl refactor: reworked mouseover module refactor: (dedup) moved click interceptor from nehuba/threesurfer to their common parent refactor: move mouseover & viewer wrapper from viewercmp to two child components --- .github/workflows/deploy-helm.yml | 6 +- .github/workflows/docker_img.yml | 13 ++ .../annotationMode.component.ts | 4 +- .../userAnnotations/tools/service.ts | 49 ++++- src/atlasViewer/atlasViewer.component.ts | 3 - .../ctxMenuHost.directive.ts | 4 +- .../dismissCtxMenu.directive.ts | 4 +- src/features/voi-bbox.directive.ts | 81 +++++-- src/mouseoverModule/index.ts | 5 +- src/mouseoverModule/mouseOverCvt.pipe.ts | 84 -------- src/mouseoverModule/mouseover.component.ts | 22 ++ src/mouseoverModule/mouseover.directive.ts | 74 ------- src/mouseoverModule/mouseover.module.ts | 39 ---- src/mouseoverModule/mouseover.style.css | 11 + src/mouseoverModule/mouseover.template.html | 17 ++ src/mouseoverModule/service.ts | 13 +- .../transformOnhoverSegment.pipe.ts | 29 --- src/mouseoverModule/util.spec.ts | 65 ------ src/mouseoverModule/util.ts | 34 --- src/viewerModule/module.ts | 24 ++- src/viewerModule/nehuba/module.ts | 2 - .../nehubaViewerGlue.component.spec.ts | 80 ------- .../nehubaViewerGlue.component.ts | 57 +---- .../pipes/nehubaVCtxToBbox.pipe.ts | 6 +- .../threeSurferGlue/threeSurfer.component.ts | 34 --- src/viewerModule/viewer.interface.ts | 39 +++- .../viewerCmp/viewerCmp.component.ts | 166 ++++++-------- .../viewerCmp/viewerCmp.style.css | 20 +- .../viewerCmp/viewerCmp.template.html | 66 +----- .../viewerWrapper/viewerWrapper.component.ts | 204 ++++++++++++++++++ .../viewerWrapper/viewerWrapper.style.css | 0 .../viewerWrapper/viewerWrapper.template.html | 37 ++++ 32 files changed, 561 insertions(+), 731 deletions(-) delete mode 100644 src/mouseoverModule/mouseOverCvt.pipe.ts create mode 100644 src/mouseoverModule/mouseover.component.ts delete mode 100644 src/mouseoverModule/mouseover.directive.ts delete mode 100644 src/mouseoverModule/mouseover.module.ts create mode 100644 src/mouseoverModule/mouseover.style.css create mode 100644 src/mouseoverModule/mouseover.template.html delete mode 100644 src/mouseoverModule/transformOnhoverSegment.pipe.ts delete mode 100644 src/mouseoverModule/util.spec.ts delete mode 100644 src/mouseoverModule/util.ts create mode 100644 src/viewerModule/viewerWrapper/viewerWrapper.component.ts create mode 100644 src/viewerModule/viewerWrapper/viewerWrapper.style.css create mode 100644 src/viewerModule/viewerWrapper/viewerWrapper.template.html diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index be72a299c..59796a18d 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -33,6 +33,7 @@ jobs: helm --kubeconfig=$kubecfg_path \ upgrade \ --history-max 3 \ + --reuse-values \ --set image.tag=${{ inputs.IMAGE_TAG }} \ --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ @@ -53,8 +54,6 @@ jobs: upgrade \ --history-max 3 \ --reuse-values \ - --set envObj.HOST_PATHNAME=/viewer-staging \ - --set envObj.OVERWRITE_API_ENDPOINT=https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0 \ --set image.tag=${{ inputs.IMAGE_TAG }} \ --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ @@ -75,9 +74,6 @@ jobs: upgrade \ --history-max 3 \ --reuse-values \ - --set envObj.OVERWRITE_EXPERIMENTAL_FLAG_ATTR=1 \ - --set envObj.HOST_PATHNAME=/viewer-exmpt \ - --set envObj.OVERWRITE_API_ENDPOINT=https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0 \ --set image.tag=${{ inputs.IMAGE_TAG }} \ --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index ad5df0476..9fc356880 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -167,6 +167,19 @@ jobs: secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} + trigger-deploy-expmt-rancher: + if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'staging' && success() }} + needs: + - build-docker-img + - setting-vars + uses: ./.github/workflows/deploy-helm.yml + with: + DEPLOYMENT_NAME: expmt + IMAGE_TAG: staging + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.GIT_DIGEST }} + secrets: + KUBECONFIG: ${{ secrets.KUBECONFIG }} + trigger-deploy-master-rancher: if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'master' && success() }} needs: diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index 322d59bd2..f11a93778 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, OnDestroy, Optional } from "@angular/core"; import { ModularUserAnnotationToolService } from "../tools/service"; import { ARIA_LABELS } from 'common/constants' import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util"; -import { TContextArg } from "src/viewerModule/viewer.interface"; +import { TViewerEvtCtxData } from "src/viewerModule/viewer.interface"; import { TContextMenuReg } from "src/contextMenuModule"; import { MatSnackBar } from 'src/sharedModules/angularMaterial.exports' @@ -26,7 +26,7 @@ export class AnnotationMode implements OnDestroy{ private modularToolSvc: ModularUserAnnotationToolService, snackbar: MatSnackBar, @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, - @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu>> + @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu>> ) { /** diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 1d36acf63..002fa3189 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -18,6 +18,7 @@ import { atlasSelection } from "src/state"; import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { AnnotationLayer } from "src/atlasComponents/annotations"; import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"; +import { HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; const LOCAL_STORAGE_KEY = 'userAnnotationKey' const ANNOTATION_LAYER_NAME = "modular_tool_layer_name" @@ -276,12 +277,52 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) ) + #hoverMsgs: THoverConfig[] = [] + + #dismimssHoverMsgs(){ + if (!this.hoverInterceptor) { + return + } + const { remove } = this.hoverInterceptor + for (const msg of this.#hoverMsgs){ + remove(msg) + } + } + #appendHoverMsgs(geometries: IAnnotationGeometry[]){ + if (!this.hoverInterceptor) { + return + } + const { append } = this.hoverInterceptor + this.#hoverMsgs = geometries.map(geom => { + let fontIcon = 'fa-file' + if (geom.annotationType === 'Point') { + fontIcon = 'fa-circle' + } + if (geom.annotationType === 'Line') { + fontIcon = 'fa-slash' + } + if (geom.annotationType === 'Polygon') { + fontIcon = 'fa-draw-polygon' + } + return { + message: geom.name || `Unnamed ${geom.annotationType}`, + fontSet: 'fas', + fontIcon + } + }) + for (const msg of this.#hoverMsgs){ + append(msg) + } + } + constructor( private store: Store, private snackbar: MatSnackBar, @Inject(INJ_ANNOT_TARGET) annotTarget$: Observable, @Inject(ANNOTATION_EVENT_INJ_TOKEN) private annotnEvSubj: Subject>, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) + private hoverInterceptor: HoverInterceptor, ){ /** @@ -323,7 +364,13 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } } as TAnnotationEvent<'mousedown' | 'mouseup' | 'mousemove'> this.annotnEvSubj.next(payload) - }) + }), + this.hoveringAnnotations$.subscribe(ev => { + this.#dismimssHoverMsgs() + if (ev) { + this.#appendHoverMsgs([ev]) + } + }), ) /** diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index acbcfd87f..e326167d1 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -15,7 +15,6 @@ import { Observable, Subscription, merge, timer, fromEvent } from "rxjs"; import { filter, delay, switchMapTo, take, startWith } from "rxjs/operators"; import { colorAnimation } from "./atlasViewer.animation" -import { MouseHoverDirective } from "src/mouseoverModule"; import { MatSnackBar } from 'src/sharedModules/angularMaterial.exports' import { MatDialog, MatDialogRef } from "src/sharedModules/angularMaterial.exports"; import { CONST } from 'common/constants' @@ -49,8 +48,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef - @ViewChild(MouseHoverDirective) private mouseOverNehuba: MouseHoverDirective - @ViewChild('idleOverlay', {read: TemplateRef}) idelTmpl: TemplateRef @HostBinding('attr.ismobile') diff --git a/src/contextMenuModule/ctxMenuHost.directive.ts b/src/contextMenuModule/ctxMenuHost.directive.ts index 3f1b2b7b8..71bcff9e3 100644 --- a/src/contextMenuModule/ctxMenuHost.directive.ts +++ b/src/contextMenuModule/ctxMenuHost.directive.ts @@ -1,6 +1,6 @@ import { AfterViewInit, Directive, HostListener, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core"; import { ContextMenuService } from "./service"; -import { TContextArg } from "src/viewerModule/viewer.interface"; +import { TViewerEvtCtxData } from "src/viewerModule/viewer.interface"; @Directive({ selector: '[ctx-menu-host]' @@ -18,7 +18,7 @@ export class CtxMenuHost implements OnDestroy, AfterViewInit{ constructor( private vcr: ViewContainerRef, - private svc: ContextMenuService>, + private svc: ContextMenuService>, ){ } diff --git a/src/contextMenuModule/dismissCtxMenu.directive.ts b/src/contextMenuModule/dismissCtxMenu.directive.ts index 36576fccc..dd57df2ac 100644 --- a/src/contextMenuModule/dismissCtxMenu.directive.ts +++ b/src/contextMenuModule/dismissCtxMenu.directive.ts @@ -1,5 +1,5 @@ import { Directive, HostListener } from "@angular/core"; -import { TContextArg } from "src/viewerModule/viewer.interface"; +import { TViewerEvtCtxData } from "src/viewerModule/viewer.interface"; import { ContextMenuService } from "./service"; @Directive({ @@ -13,7 +13,7 @@ export class DismissCtxMenuDirective{ } constructor( - private svc: ContextMenuService> + private svc: ContextMenuService> ){ } diff --git a/src/features/voi-bbox.directive.ts b/src/features/voi-bbox.directive.ts index f150225b9..5408bc08c 100644 --- a/src/features/voi-bbox.directive.ts +++ b/src/features/voi-bbox.directive.ts @@ -1,25 +1,27 @@ -import { Directive, Inject, Input, OnDestroy, Optional } from "@angular/core"; +import { Directive, Inject, Input, Optional, inject } from "@angular/core"; import { Store } from "@ngrx/store"; -import { concat, interval, of, Subject, Subscription } from "rxjs"; -import { debounce, distinctUntilChanged, filter, pairwise, take } from "rxjs/operators"; +import { concat, interval, of, Subject } from "rxjs"; +import { debounce, distinctUntilChanged, filter, pairwise, take, takeUntil } from "rxjs/operators"; import { AnnotationLayer, TNgAnnotationAABBox, TNgAnnotationPoint } from "src/atlasComponents/annotations"; import { Feature, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes"; import { userInteraction } from "src/state"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { arrayEqual } from "src/util/array"; import { isVoiData } from "./guards" +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; @Directive({ selector: '[voiBbox]', + hostDirectives: [ DestroyDirective ] }) -export class VoiBboxDirective implements OnDestroy { - - #onDestroyCb: (() => void)[] = [] +export class VoiBboxDirective { + + #destory$ = inject(DestroyDirective).destroyed$ static VOI_LAYER_NAME = 'voi-annotation-layer' static VOI_ANNOTATION_COLOR = "#ffff00" - #voiSubs: Subscription[] = [] private _voiBBoxSvc: AnnotationLayer get voiBBoxSvc(): AnnotationLayer { if (this._voiBBoxSvc) return this._voiBBoxSvc @@ -29,14 +31,15 @@ export class VoiBboxDirective implements OnDestroy { VoiBboxDirective.VOI_ANNOTATION_COLOR ) this._voiBBoxSvc = layer - this.#voiSubs.push( - layer.onHover.subscribe(val => this.handleOnHoverFeature(val || {})) - ) - this.#onDestroyCb.push(() => { + layer.onHover.pipe( + takeUntil(this.#destory$) + ).subscribe(val => this.handleOnHoverFeature(val || {})) + + this.#destory$.subscribe(() => { this._voiBBoxSvc.dispose() this._voiBBoxSvc = null }) - return layer + return this._voiBBoxSvc } catch (e) { return null } @@ -54,25 +57,27 @@ export class VoiBboxDirective implements OnDestroy { return this.#voiFeatures } - ngOnDestroy(): void { - while (this.#onDestroyCb.length > 0) this.#onDestroyCb.pop()() - } + #hoverMsgs: THoverConfig[] = [] constructor( private store: Store, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) + clickInterceptor: ClickInterceptor, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) + private hoverInterceptor: HoverInterceptor, ){ if (clickInterceptor) { const { register, deregister } = clickInterceptor const handleClick = this.handleClick.bind(this) register(handleClick) - this.#onDestroyCb.push(() => deregister(handleClick)) + this.#destory$.subscribe(() => deregister(handleClick)) } - const sub = concat( + concat( of([] as VoiFeature[]), this.#features$ ).pipe( + takeUntil(this.#destory$), distinctUntilChanged(arrayEqual((o, n) => o.id === n.id)), pairwise(), debounce(() => @@ -109,10 +114,38 @@ export class VoiBboxDirective implements OnDestroy { if (this.voiBBoxSvc) this.voiBBoxSvc.setVisible(true) }) - this.#onDestroyCb.push(() => sub.unsubscribe()) - this.#onDestroyCb.push(() => this.store.dispatch( - userInteraction.actions.setMouseoverVoi({ feature: null }) - )) + this.#destory$.subscribe(() => { + this.store.dispatch( + userInteraction.actions.setMouseoverVoi({ feature: null }) + ) + this.#dismissHoverMsg() + }) + } + + #dismissHoverMsg(){ + if (!this.hoverInterceptor) { + return + } + + const { remove } = this.hoverInterceptor + for (const msg of this.#hoverMsgs){ + remove(msg) + } + } + + #appendHoverMsg(feats: VoiFeature[]){ + if (!this.hoverInterceptor) { + return + } + const { append } = this.hoverInterceptor + this.#hoverMsgs = feats.map(feat => ({ + message: `${feat?.name}`, + fontIcon: 'fa-database', + fontSet: 'fas' + })) + for (const msg of this.#hoverMsgs){ + append(msg) + } } handleClick(){ @@ -135,6 +168,10 @@ export class VoiBboxDirective implements OnDestroy { this.store.dispatch( userInteraction.actions.setMouseoverVoi({ feature }) ) + this.#dismissHoverMsg() + if (feature) { + this.#appendHoverMsg([feature]) + } } #pointsToAABB(pointA: [number, number, number], pointB: [number, number, number]): TNgAnnotationAABBox{ diff --git a/src/mouseoverModule/index.ts b/src/mouseoverModule/index.ts index 8dea7b959..a419a4a25 100644 --- a/src/mouseoverModule/index.ts +++ b/src/mouseoverModule/index.ts @@ -1,3 +1,2 @@ -export { MouseHoverDirective } from './mouseover.directive' -export { MouseoverModule } from './mouseover.module' -export { TransformOnhoverSegmentPipe } from './transformOnhoverSegment.pipe' \ No newline at end of file +export { MouseOver } from "./mouseover.component" +export { MouseOverSvc } from "./service" diff --git a/src/mouseoverModule/mouseOverCvt.pipe.ts b/src/mouseoverModule/mouseOverCvt.pipe.ts deleted file mode 100644 index fabd1d489..000000000 --- a/src/mouseoverModule/mouseOverCvt.pipe.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { TOnHoverObj } from "./util"; - -function render(key: T, value: TOnHoverObj[T]){ - if (!value) return [] - switch (key) { - case 'regions': { - return (value as TOnHoverObj['regions']).map(seg => { - return { - icon: { - fontSet: 'fas', - fontIcon: 'fa-brain', - cls: 'fas fa-brain', - }, - text: seg?.name || "Unknown" - } - }) - } - case 'voi': { - const { name } = value as TOnHoverObj['voi'] - return [{ - icon: { - fontSet: 'fas', - fontIcon: 'fa-database', - cls: 'fas fa-database' - }, - text: name - }] - } - case 'annotation': { - const { annotationType, name } = (value as TOnHoverObj['annotation']) - let fontIcon: string - if (annotationType === 'Point') fontIcon = 'fa-circle' - if (annotationType === 'Line') fontIcon = 'fa-slash' - if (annotationType === 'Polygon') fontIcon = 'fa-draw-polygon' - if (!annotationType) fontIcon = 'fa-file' - return [{ - icon: { - fontSet: 'fas', - fontIcon, - cls: `fas ${fontIcon}`, - }, - text: name || `Unnamed ${annotationType}` - }] - } - default: { - return [{ - icon: { - fontSet: 'fas', - fontIcon: 'fa-file', - cls: 'fas fa-file' - }, - text: `Unknown hovered object` - }] - } - } -} - -type TCvtOutput = { - icon: { - fontSet: string - fontIcon: string - cls: string - } - text: string -} - -@Pipe({ - name: 'mouseoverCvt', - pure: true -}) - -export class MouseOverConvertPipe implements PipeTransform{ - - public transform(dict: TOnHoverObj){ - const output: TCvtOutput[] = [] - for (const key in dict) { - output.push( - ...render(key as keyof TOnHoverObj, dict[key]) - ) - } - return output - } -} \ No newline at end of file diff --git a/src/mouseoverModule/mouseover.component.ts b/src/mouseoverModule/mouseover.component.ts new file mode 100644 index 000000000..4c5b58b5e --- /dev/null +++ b/src/mouseoverModule/mouseover.component.ts @@ -0,0 +1,22 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { AngularMaterialModule } from "src/sharedModules"; +import { MouseOverSvc } from "./service"; + +@Component({ + selector: 'mouseover-info', + templateUrl: './mouseover.template.html', + styleUrls: [ + './mouseover.style.css' + ], + standalone: true, + imports: [ + AngularMaterialModule, + CommonModule + ], +}) + +export class MouseOver { + constructor(private svc: MouseOverSvc) {} + messages$ = this.svc.messages$ +} diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts deleted file mode 100644 index a69fccd76..000000000 --- a/src/mouseoverModule/mouseover.directive.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Directive } from "@angular/core" -import { select, Store } from "@ngrx/store" -import { merge, Observable } from "rxjs" -import { distinctUntilChanged, map, scan } from "rxjs/operators" -import { TOnHoverObj, temporalPositveScanFn } from "./util" -import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; -import { userInteraction } from "src/state" -import { arrayEqual } from "src/util/array" -import { MouseOverSvc } from "./service" - -@Directive({ - selector: '[iav-mouse-hover]', - exportAs: 'iavMouseHover', -}) - -export class MouseHoverDirective { - - /** - * TODO move - * - mousing over regions - * - hovering annotation - * - hovering voi feature - * to use hover interceptor - */ - public currentOnHoverObs$: Observable = merge( - this.store$.pipe( - select(userInteraction.selectors.mousingOverRegions), - ).pipe( - distinctUntilChanged(arrayEqual((o, n) => o?.name === n?.name)), - map(regions => { - return { regions } - }), - ), - this.annotSvc.hoveringAnnotations$.pipe( - distinctUntilChanged(), - map(annotation => { - return { annotation } - }), - ), - this.store$.pipe( - select(userInteraction.selectors.mousingOverVoiFeature), - distinctUntilChanged((o, n) => o?.id === n?.id), - map(voi => ({ voi })) - ) - ).pipe( - scan(temporalPositveScanFn, []), - map(arr => { - - let returnObj: TOnHoverObj = { - regions: null, - annotation: null, - voi: null - } - - for (const val of arr) { - returnObj = { - ...returnObj, - ...val - } - } - - return returnObj - }), - ) - - constructor( - private store$: Store, - private annotSvc: ModularUserAnnotationToolService, - private svc: MouseOverSvc, - ) { - } - - messages$ = this.svc.messages$ -} diff --git a/src/mouseoverModule/mouseover.module.ts b/src/mouseoverModule/mouseover.module.ts deleted file mode 100644 index f4c549348..000000000 --- a/src/mouseoverModule/mouseover.module.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { TransformOnhoverSegmentPipe } from "./transformOnhoverSegment.pipe"; -import { MouseHoverDirective } from "./mouseover.directive"; -import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; -import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; -import { MouseOverSvc } from "./service"; - - -@NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - MouseHoverDirective, - TransformOnhoverSegmentPipe, - MouseOverConvertPipe, - ], - exports: [ - MouseHoverDirective, - TransformOnhoverSegmentPipe, - MouseOverConvertPipe, - ], - providers: [ - MouseOverSvc, - { - provide: HOVER_INTERCEPTOR_INJECTOR, - useFactory: (svc: MouseOverSvc) => { - return { - append: svc.append.bind(svc), - remove: svc.remove.bind(svc), - } - }, - deps: [ MouseOverSvc ] - } - ] -}) - -export class MouseoverModule{} diff --git a/src/mouseoverModule/mouseover.style.css b/src/mouseoverModule/mouseover.style.css new file mode 100644 index 000000000..15f65ddeb --- /dev/null +++ b/src/mouseoverModule/mouseover.style.css @@ -0,0 +1,11 @@ +:host +{ + display: inline-block; +} + +.centered +{ + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/mouseoverModule/mouseover.template.html b/src/mouseoverModule/mouseover.template.html new file mode 100644 index 000000000..00d649b30 --- /dev/null +++ b/src/mouseoverModule/mouseover.template.html @@ -0,0 +1,17 @@ + + + + + + + {{ message.message }} + + + + + + {{ message.message }} + + + + diff --git a/src/mouseoverModule/service.ts b/src/mouseoverModule/service.ts index eba057bcc..284faa28f 100644 --- a/src/mouseoverModule/service.ts +++ b/src/mouseoverModule/service.ts @@ -1,17 +1,24 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs"; +import { debounceTime, shareReplay } from "rxjs/operators"; import { THoverConfig } from "src/util/injectionTokens"; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class MouseOverSvc { #messages: THoverConfig[] = [] - messages$ = new BehaviorSubject(this.#messages) + #messages$ = new BehaviorSubject(this.#messages) + messages$ = this.#messages$.pipe( + debounceTime(16), + shareReplay(1), + ) set messages(messages: THoverConfig[]){ this.#messages = messages - this.messages$.next(this.#messages) + this.#messages$.next(this.#messages) } get messages(): THoverConfig[]{ diff --git a/src/mouseoverModule/transformOnhoverSegment.pipe.ts b/src/mouseoverModule/transformOnhoverSegment.pipe.ts deleted file mode 100644 index 5199a582a..000000000 --- a/src/mouseoverModule/transformOnhoverSegment.pipe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Pipe, PipeTransform, SecurityContext } from "@angular/core"; -import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; - -@Pipe({ - name: 'transformOnhoverSegment', -}) - -export class TransformOnhoverSegmentPipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) { - - } - - private sanitizeHtml(inc: string): SafeHtml { - return this.sanitizer.sanitize(SecurityContext.HTML, inc) - } - - private getStatus(text: string) { - return ` (${this.sanitizeHtml(text)})` - } - - public transform(segment: any | number): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(( - ( this.sanitizeHtml(segment.name) || segment) + - (segment.status - ? this.getStatus(segment.status) - : '') - )) - } -} diff --git a/src/mouseoverModule/util.spec.ts b/src/mouseoverModule/util.spec.ts deleted file mode 100644 index 31920019b..000000000 --- a/src/mouseoverModule/util.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {} from 'jasmine' -import { forkJoin, Subject } from 'rxjs'; -import { scan, skip, take } from 'rxjs/operators'; -import { temporalPositveScanFn } from './util' - -const segmentsPositive = { segments: [{ hello: 'world' }] } as {segments: any} -const segmentsNegative = { segments: [] } - -const userLandmarkPostive = { userLandmark: true } -const userLandmarkNegative = { userLandmark: null } - -describe('temporalPositveScanFn', () => { - const subscriptions = [] - afterAll(() => { - while (subscriptions.length > 0) { subscriptions.pop().unsubscribe() } - }) - - it('should scan obs as expected', (done) => { - - const source = new Subject() - - const testFirstEv = source.pipe( - scan(temporalPositveScanFn, []), - take(1), - ) - - const testSecondEv = source.pipe( - scan(temporalPositveScanFn, []), - skip(1), - take(1), - ) - - const testThirdEv = source.pipe( - scan(temporalPositveScanFn, []), - skip(2), - take(1), - ) - - const testFourthEv = source.pipe( - scan(temporalPositveScanFn, []), - skip(3), - take(1), - ) - - forkJoin([ - testFirstEv, - testSecondEv, - testThirdEv, - testFourthEv, - ]).pipe( - take(1), - ).subscribe(([ arr1, arr2, arr3, arr4 ]) => { - expect(arr1).toEqual([ segmentsPositive ] as any) - expect(arr2).toEqual([ userLandmarkPostive, segmentsPositive ] as any) - expect(arr3).toEqual([ userLandmarkPostive ] as any) - expect(arr4).toEqual([]) - done() - }) - - source.next(segmentsPositive) - source.next(userLandmarkPostive) - source.next(segmentsNegative) - source.next(userLandmarkNegative) - }) -}) diff --git a/src/mouseoverModule/util.ts b/src/mouseoverModule/util.ts deleted file mode 100644 index 208c01cee..000000000 --- a/src/mouseoverModule/util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { SxplrRegion, VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" -import { IAnnotationGeometry } from "src/atlasComponents/userAnnotations/tools/type" - -export type TOnHoverObj = { - regions: SxplrRegion[] - annotation: IAnnotationGeometry - voi: VoiFeature -} - -/** - * Scan function which prepends newest positive (i.e. defined) value - * - * e.g. const source = new Subject() - * source.pipe( - * scan(temporalPositveScanFn, []) - * ).subscribe(this.log.log) // outputs - * - * - * - */ -export const temporalPositveScanFn = (acc: Array, curr: Partial) => { - - const keys = Object.keys(curr) - - // empty array is truthy - const isPositive = keys.some(key => Array.isArray(curr[key]) - ? curr[key].length > 0 - : !!curr[key] - ) - - return isPositive - ? [curr, ...(acc.filter(item => !keys.some(key => !!item[key])))] as Array - : acc.filter(item => !keys.some(key => !!item[key])) -} diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 81d404525..2ecdfdf8b 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -15,14 +15,14 @@ import { QuickTourModule } from "src/ui/quickTour/module"; import { INJ_ANNOT_TARGET } from "src/atlasComponents/userAnnotations/tools/type"; import { NEHUBA_INSTANCE_INJTKN } from "./nehuba/util"; import { map, switchMap } from "rxjs/operators"; -import { TContextArg } from "./viewer.interface"; +import { TViewerEvtCtxData } from "./viewer.interface"; import { KeyFrameModule } from "src/keyframesModule/module"; import { ViewerInternalStateSvc } from "./viewerInternalState.service"; import { SAPI, SAPIModule } from 'src/atlasComponents/sapi'; import { NehubaVCtxToBbox } from "./pipes/nehubaVCtxToBbox.pipe"; import { SapiViewsModule, SapiViewsUtilModule } from "src/atlasComponents/sapiViews"; import { DialogModule } from "src/ui/dialogInfo/module"; -import { MouseoverModule } from "src/mouseoverModule"; +import { MouseOver, MouseOverSvc } from "src/mouseoverModule"; import { LogoContainer } from "src/ui/logoContainer/logoContainer.component"; import { FloatingMouseContextualContainerDirective } from "src/util/directives/floatingMouseContextualContainer.directive"; import { ShareModule } from "src/share"; @@ -40,6 +40,8 @@ import { Store } from "@ngrx/store"; import { atlasSelection, userPreference } from "src/state"; import { TabComponent } from "src/components/tab/tab.components"; import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; +import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; +import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; @NgModule({ imports: [ @@ -59,7 +61,6 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di SapiViewsModule, SapiViewsUtilModule, DialogModule, - MouseoverModule, ShareModule, ATPSelectorModule, FeatureModule, @@ -69,6 +70,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di BottomMenuModule, TabComponent, + MouseOver, ExperimentalFlagDirective, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) @@ -78,6 +80,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di NehubaVCtxToBbox, LogoContainer, FloatingMouseContextualContainerDirective, + ViewerWrapper, ], providers: [ { @@ -93,11 +96,11 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di }, { provide: CONTEXT_MENU_ITEM_INJECTOR, - useFactory: (svc: ContextMenuService>) => { + useFactory: (svc: ContextMenuService>) => { return { register: svc.register.bind(svc), deregister: svc.deregister.bind(svc) - } as TContextMenu>> + } as TContextMenu>> }, deps: [ ContextMenuService ] }, @@ -141,6 +144,17 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di ), deps: [ Store, SAPI ] }, + + { + provide: HOVER_INTERCEPTOR_INJECTOR, + useFactory: (svc: MouseOverSvc) => { + return { + append: svc.append.bind(svc), + remove: svc.remove.bind(svc), + } + }, + deps: [ MouseOverSvc ] + } ], exports: [ ViewerCmp, diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index f7e7026c1..d6fb5813f 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -12,7 +12,6 @@ import { NehubaGlueCmp } from "./nehubaViewerGlue/nehubaViewerGlue.component"; import { UtilModule } from "src/util"; import { ComponentsModule } from "src/components"; import { AngularMaterialModule } from "src/sharedModules"; -import { MouseoverModule } from "src/mouseoverModule"; import { StatusCardComponent } from "./statusCard/statusCard.component"; import { ShareModule } from "src/share"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; @@ -40,7 +39,6 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di UtilModule, AngularMaterialModule, ComponentsModule, - MouseoverModule, ShareModule, WindowResizeModule, NehubaUserLayerModule, diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts index 10d189784..263982f2d 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts @@ -169,85 +169,5 @@ describe('> nehubaViewerGlue.component.ts', () => { expect(fixture.componentInstance).toBeTruthy() }) - describe('> selectHoveredRegion', () => { - let dispatchSpy: jasmine.Spy - let clickIntServ: ClickInterceptorService - beforeEach(() => { - dispatchSpy = spyOn(mockStore, 'dispatch') - clickIntServ = TestBed.inject(ClickInterceptorService) - }) - afterEach(() => { - dispatchSpy.calls.reset() - }) - - describe('> if on hover is empty array', () => { - let fallbackSpy: jasmine.Spy - beforeEach(() => { - fallbackSpy = spyOn(clickIntServ, 'fallback') - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - }) - it('> dispatch not called', () => { - expect(dispatchSpy).not.toHaveBeenCalled() - }) - it('> fallback called', () => { - expect(fallbackSpy).toHaveBeenCalled() - }) - }) - - describe('> if on hover is non object array', () => { - let fallbackSpy: jasmine.Spy - - const testObj0 = { - segment: 'hello world' - } - const testObj1 = 'hello world' - beforeEach(() => { - fallbackSpy = spyOn(clickIntServ, 'fallback') - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - }) - it('> dispatch not called', () => { - expect(dispatchSpy).not.toHaveBeenCalled() - }) - it('> fallback called', () => { - expect(fallbackSpy).toHaveBeenCalled() - }) - }) - - describe('> if on hover array containing at least 1 obj, only dispatch the first obj', () => { - let fallbackSpy: jasmine.Spy - const testObj0 = { - segment: 'hello world' - } - const testObj1 = { - segment: { - foo: 'baz' - } - } - const testObj2 = { - segment: { - hello: 'world' - } - } - beforeEach(() => { - fallbackSpy = spyOn(clickIntServ, 'fallback') - - }) - afterEach(() => { - fallbackSpy.calls.reset() - }) - it('> dispatch called with obj1', () => { - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - const { segment } = testObj1 - }) - it('> fallback called (does not intercept)', () => { - TestBed.createComponent(NehubaGlueCmp) - clickIntServ.callRegFns(null) - expect(fallbackSpy).toHaveBeenCalled() - }) - }) - }) }) diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 1f47b5680..a32c784f1 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,15 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Inject, OnDestroy, Optional, Output } from "@angular/core"; -import { select, Store } from "@ngrx/store"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; -import { distinctUntilChanged } from "rxjs/operators"; +import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, Output } from "@angular/core"; import { IViewer, TViewerEvent } from "../../viewer.interface"; import { NehubaMeshService } from "../mesh.service"; import { NehubaLayerControlService, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service"; import { EXTERNAL_LAYER_CONTROL, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util"; -import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"; import { NehubaConfig } from "../config.service"; import { SET_MESHES_TO_LOAD } from "../constants"; -import { atlasSelection, userInteraction } from "src/state"; @Component({ @@ -58,7 +53,6 @@ import { atlasSelection, userInteraction } from "src/state"; export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy { - private onhoverSegments: SxplrRegion[] = [] private onDestroyCb: (() => void)[] = [] public nehubaConfig: NehubaConfig @@ -70,53 +64,4 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy { @Output() public viewerEvent = new EventEmitter>() - constructor( - private store$: Store, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, - ){ - - /** - * define onclick behaviour - */ - if (clickInterceptor) { - const { deregister, register } = clickInterceptor - const selOnhoverRegion = this.selectHoveredRegion.bind(this) - register(selOnhoverRegion, { last: true }) - this.onDestroyCb.push(() => deregister(selOnhoverRegion)) - } - - /** - * on hover segment - */ - const onhovSegSub = this.store$.pipe( - select(userInteraction.selectors.mousingOverRegions), - distinctUntilChanged(), - ).subscribe(arr => { - this.onhoverSegments = arr - }) - this.onDestroyCb.push(() => onhovSegSub.unsubscribe()) - } - - private selectHoveredRegion(ev: PointerEvent): boolean{ - /** - * If label indicies are not defined by the ontology, it will be a string in the format of `{ngId}#{labelIndex}` - */ - const trueOnhoverSegments = this.onhoverSegments && this.onhoverSegments.filter(v => typeof v === 'object') - if (!trueOnhoverSegments || (trueOnhoverSegments.length === 0)) return true - - if (ev.ctrlKey) { - this.store$.dispatch( - atlasSelection.actions.toggleRegion({ - region: trueOnhoverSegments[0] - }) - ) - } else { - this.store$.dispatch( - atlasSelection.actions.selectRegion({ - region: trueOnhoverSegments[0] - }) - ) - } - return true - } } diff --git a/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts b/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts index e3ceaad46..37a68c4d3 100644 --- a/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts +++ b/src/viewerModule/pipes/nehubaVCtxToBbox.pipe.ts @@ -1,4 +1,4 @@ -import { TContextArg } from './../viewer.interface'; +import { TViewerEvtCtxData } from './../viewer.interface'; import { Pipe, PipeTransform } from "@angular/core"; type Point = [number, number, number] @@ -12,7 +12,7 @@ const MAGIC_RADIUS = 256 }) export class NehubaVCtxToBbox implements PipeTransform{ - public transform(event: TContextArg<'nehuba' | 'threeSurfer'>, unit: string = "mm"): BBox{ + public transform(event: TViewerEvtCtxData<'nehuba' | 'threeSurfer'>, unit: string = "mm"): BBox{ if (!event) { return null } @@ -23,7 +23,7 @@ export class NehubaVCtxToBbox implements PipeTransform{ if (unit === "mm") { divisor = 1e6 } - const { payload } = event as TContextArg<'nehuba'> + const { payload } = event as TViewerEvtCtxData<'nehuba'> if (!payload.nav) return null diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 9a44a02e1..f1a325045 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -4,9 +4,7 @@ import { BehaviorSubject, combineLatest, concat, forkJoin, from, merge, NEVER, O import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators"; import { ComponentStore, LockError } from "src/viewerModule/componentStore"; import { select, Store } from "@ngrx/store"; -import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { MatSnackBar } from "src/sharedModules/angularMaterial.exports" -import { CONST } from 'common/constants' import { getUuid, switchMapWaitFor } from "src/util/fn"; import { AUTO_ROTATE, TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service"; import { atlasAppearance, atlasSelection } from "src/state"; @@ -400,7 +398,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit private sapi: SAPI, private snackbar: MatSnackBar, @Optional() intViewerStateSvc: ViewerInternalStateSvc, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, ){ if (intViewerStateSvc) { const { @@ -430,37 +427,6 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit this.onDestroyCb.push(() => done()) } - /** - * intercept click and act - */ - if (clickInterceptor) { - const handleClick = (ev: MouseEvent) => { - - // if does not click inside container, ignore - if (!(el.nativeElement as HTMLElement).contains(ev.target as HTMLElement)) { - return true - } - - if (this.mouseoverRegions.length === 0) return true - if (this.mouseoverRegions.length > 1) { - this.snackbar.open(CONST.DOES_NOT_SUPPORT_MULTI_REGION_SELECTION, 'Dismiss', { - duration: 3000 - }) - return true - } - - const regions = this.mouseoverRegions.slice(0, 1) as any[] - this.store$.dispatch( - atlasSelection.actions.setSelectedRegions({ regions }) - ) - return true - } - const { register, deregister } = clickInterceptor - register(handleClick) - this.onDestroyCb.push( - () => { deregister(register) } - ) - } this.domEl = el.nativeElement diff --git a/src/viewerModule/viewer.interface.ts b/src/viewerModule/viewer.interface.ts index cbc85aca0..488f8ee12 100644 --- a/src/viewerModule/viewer.interface.ts +++ b/src/viewerModule/viewer.interface.ts @@ -35,7 +35,9 @@ export interface IViewerCtx { 'threeSurfer': TThreeSurferContextInfo } -export type TContextArg = ({ +export type ViewerType = "nehuba" | "threeSurfer" + +export type TViewerEvtCtxData = ({ viewerType: K payload: RecursivePartial }) @@ -45,25 +47,40 @@ export enum EnumViewerEvt { VIEWER_CTX, } -type TViewerEventViewerLoaded = { +export type TViewerEventViewerLoaded = { type: EnumViewerEvt.VIEWERLOADED data: boolean } -export type TViewerEvent = TViewerEventViewerLoaded | - { - type: EnumViewerEvt.VIEWER_CTX - data: TContextArg - } +type TViewerEventCtx = { + type: EnumViewerEvt.VIEWER_CTX + data: TViewerEvtCtxData +} + +export type TViewerEvent< + T extends ViewerType=ViewerType +> = TViewerEventViewerLoaded | TViewerEventCtx + +export function isViewerCtx(ev: TViewerEvent): ev is TViewerEventCtx { + return ev.type === EnumViewerEvt.VIEWER_CTX +} + +export function isNehubaVCtxEvt(ev: TViewerEvent): ev is TViewerEventCtx<"nehuba"> { + return ev.type === EnumViewerEvt.VIEWER_CTX && ev.data.viewerType === "nehuba" +} + +export function isThreeSurferVCtxEvt(ev: TViewerEvent): ev is TViewerEventCtx<"threeSurfer"> { + return ev.type === EnumViewerEvt.VIEWER_CTX && ev.data.viewerType === "threeSurfer" +} -export type TSupportedViewers = keyof IViewerCtx +export type TSupportedViewers = ViewerType -export interface IViewer { +export interface IViewer { viewerCtrlHandler?: IViewerCtrl viewerEvent: EventEmitter> } export interface IGetContextInjArg { - register: (fn: (contextArg: TContextArg) => void) => void - deregister: (fn: (contextArg: TContextArg) => void) => void + register: (fn: (contextArg: TViewerEvtCtxData) => void) => void + deregister: (fn: (contextArg: TViewerEvtCtxData) => void) => void } diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index fdb4f3c31..55094ebf6 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,11 +1,11 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, TemplateRef, ViewChild, inject } from "@angular/core"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, TemplateRef, ViewChild, inject } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, Observable, of, Subscription } from "rxjs"; +import { BehaviorSubject, combineLatest, Observable, of } from "rxjs"; import { debounceTime, distinctUntilChanged, map, shareReplay, switchMap, takeUntil } from "rxjs/operators"; import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { animate, state, style, transition, trigger } from "@angular/animations"; import { IQuickTourData } from "src/ui/quickTour"; -import { EnumViewerEvt, TContextArg, TSupportedViewers, TViewerEvent } from "../viewer.interface"; +import { EnumViewerEvt, TViewerEvtCtxData, TSupportedViewers, TViewerEvent } from "../viewer.interface"; import { ContextMenuService, TContextMenuReg } from "src/contextMenuModule"; import { DialogService } from "src/services/dialogService.service"; import { SAPI } from "src/atlasComponents/sapi"; @@ -55,7 +55,7 @@ interface HasName { ] }) -export class ViewerCmp implements OnDestroy { +export class ViewerCmp { public readonly destroy$ = inject(DestroyDirective).destroyed$ @@ -74,8 +74,6 @@ export class ViewerCmp implements OnDestroy { description: QUICKTOUR_DESC.ATLAS_SELECTOR, } - private subscriptions: Subscription[] = [] - private onDestroyCb: (() => void)[] = [] public viewerLoaded: boolean = false private selectedATP = this.store$.pipe( @@ -238,7 +236,7 @@ export class ViewerCmp implements OnDestroy { constructor( private store$: Store, - private ctxMenuSvc: ContextMenuService>, + private ctxMenuSvc: ContextMenuService>, private dialogSvc: DialogService, private cdr: ChangeDetectorRef, private sapi: SAPI, @@ -293,51 +291,51 @@ export class ViewerCmp implements OnDestroy { this.#fullNavBarSwitch$.next(flag) }) - this.subscriptions.push( - this.templateSelected$.subscribe( - t => this.templateSelected = t - ), - combineLatest([ - this.templateSelected$, - this.parcellationSelected$, - this.selectedAtlas$, - ]).pipe( - debounceTime(160) - ).subscribe(async ([tmpl, parc, atlas]) => { - const regex = /pre.?release/i - const checkPrerelease = (obj: any) => { - if (obj?.name) return regex.test(obj.name) - return false - } - const message: string[] = [] - if (checkPrerelease(atlas)) { - message.push(`- _${atlas.name}_`) - } - if (checkPrerelease(tmpl)) { - message.push(`- _${tmpl.name}_`) - } - if (checkPrerelease(parc)) { - message.push(`- _${parc.name}_`) - } - if (message.length > 0) { - message.unshift(`The following have been tagged pre-release, and may be updated frequently:`) - try { - await this.dialogSvc.getUserConfirm({ - title: `Pre-release warning`, - markdown: message.join('\n\n'), - confirmOnly: true - }) - // eslint-disable-next-line no-empty - } catch (e) { - - } - } - }) + this.templateSelected$.pipe( + takeUntil(this.destroy$) + ).subscribe( + t => this.templateSelected = t ) - } - ngAfterViewInit(): void{ - const cb: TContextMenuReg> = ({ append, context }) => { + combineLatest([ + this.templateSelected$, + this.parcellationSelected$, + this.selectedAtlas$, + ]).pipe( + takeUntil(this.destroy$), + debounceTime(160), + ).subscribe(async ([tmpl, parc, atlas]) => { + const regex = /pre.?release/i + const checkPrerelease = (obj: any) => { + if (obj?.name) return regex.test(obj.name) + return false + } + const message: string[] = [] + if (checkPrerelease(atlas)) { + message.push(`- _${atlas.name}_`) + } + if (checkPrerelease(tmpl)) { + message.push(`- _${tmpl.name}_`) + } + if (checkPrerelease(parc)) { + message.push(`- _${parc.name}_`) + } + if (message.length > 0) { + message.unshift(`The following have been tagged pre-release, and may be updated frequently:`) + try { + await this.dialogSvc.getUserConfirm({ + title: `Pre-release warning`, + markdown: message.join('\n\n'), + confirmOnly: true + }) + // eslint-disable-next-line no-empty + } catch (e) { + + } + } + }) + + const cb: TContextMenuReg> = ({ append, context }) => { if (this.#lastSelectedPoint && this.lastViewedPointTmpl) { const { point, template, face, vertices } = this.#lastSelectedPoint @@ -373,14 +371,14 @@ export class ViewerCmp implements OnDestroy { */ let hoveredRegions = [] if (context.viewerType === 'nehuba') { - hoveredRegions = ((context as TContextArg<'nehuba'>).payload.nehuba || []).reduce( + hoveredRegions = ((context as TViewerEvtCtxData<'nehuba'>).payload.nehuba || []).reduce( (acc, curr) => acc.concat(...curr.regions), [] ) } if (context.viewerType === 'threeSurfer') { - hoveredRegions = (context as TContextArg<'threeSurfer'>).payload.regions + hoveredRegions = (context as TViewerEvtCtxData<'threeSurfer'>).payload.regions } if (hoveredRegions.length > 0) { @@ -397,14 +395,11 @@ export class ViewerCmp implements OnDestroy { return true } this.ctxMenuSvc.register(cb) - this.onDestroyCb.push( - () => this.ctxMenuSvc.deregister(cb) - ) - } - ngOnDestroy(): void { - while (this.subscriptions.length) this.subscriptions.pop().unsubscribe() - while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + this.destroy$.subscribe(() => { + this.ctxMenuSvc.deregister(cb) + }) + } public clearRoi(): void{ @@ -484,48 +479,6 @@ export class ViewerCmp implements OnDestroy { ) } - public handleViewerEvent(event: TViewerEvent<'nehuba' | 'threeSurfer'>): void{ - switch(event.type) { - case EnumViewerEvt.VIEWERLOADED: - this.viewerLoaded = event.data - this.cdr.detectChanges() - break - case EnumViewerEvt.VIEWER_CTX: - this.ctxMenuSvc.deepMerge(event.data) - if (event.data.viewerType === "nehuba") { - const { nehuba, nav } = (event.data as TContextArg<"nehuba">).payload - if (nehuba) { - const mousingOverRegions = (nehuba || []).reduce((acc, { regions }) => acc.concat(...regions), []) - this.store$.dispatch( - userInteraction.actions.mouseoverRegions({ - regions: mousingOverRegions - }) - ) - } - if (nav) { - this.store$.dispatch( - userInteraction.actions.mouseoverPosition({ - position: { - loc: nav.position as [number, number, number], - space: this.templateSelected, - spaceId: this.templateSelected.id, - } - }) - ) - } - } - if (event.data.viewerType === "threeSurfer") { - const { regions=[] } = (event.data as TContextArg<"threeSurfer">).payload - this.store$.dispatch( - userInteraction.actions.mouseoverRegions({ - regions: regions as SxplrRegion[] - }) - ) - } - break - default: - } - } public disposeCtxMenu(): void{ this.ctxMenuSvc.dismissCtxMenu() @@ -598,4 +551,15 @@ export class ViewerCmp implements OnDestroy { nameEql(a: HasName, b: HasName){ return a.name === b.name } + + handleViewerCtxEvent(event: TViewerEvent) { + if (event.type === EnumViewerEvt.VIEWERLOADED) { + this.viewerLoaded = event.data + this.cdr.detectChanges() + return + } + if (event.type === EnumViewerEvt.VIEWER_CTX) { + this.ctxMenuSvc.deepMerge(event.data) + } + } } diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index d72febea4..05cfce23c 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -93,13 +93,13 @@ mat-drawer display: block; } -mat-list.contextual-block +.contextual-block { display: inline-block; background-color:rgba(200,200,200,0.8); } -:host-context([darktheme="true"]) mat-list.contextual-block +:host-context([darktheme="true"]) .contextual-block { background-color : rgba(30,30,30,0.8); } @@ -155,13 +155,6 @@ sxplr-sapiviews-core-region-region-list-item align-items: center; } -.centered -{ - display: flex; - justify-content: center; - align-items: center; -} - .leave-me-alone { margin-top: 0.5rem; @@ -194,4 +187,11 @@ sxplr-tab { display: inline-flex; flex-direction: column; -} \ No newline at end of file +} + +viewer-wrapper +{ + width: 100%; + height: 100%; + display: block; +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 9528a948d..4e15b6b1c 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -12,30 +12,9 @@
- - - - - {{ cvtOutput.text }} - - - - - - - - {{ message.message }} - - + floatingMouseContextualContainerDirective> +
-
@@ -429,44 +408,9 @@
- - - - - - - - - - - - -
Template not supported by any of the viewers
- - -
- -
-
- - - Loading - {{ (selectedAtlas$ | async).name }} - -
-
-
- - - - -
-
+ +
diff --git a/src/viewerModule/viewerWrapper/viewerWrapper.component.ts b/src/viewerModule/viewerWrapper/viewerWrapper.component.ts new file mode 100644 index 000000000..4e7e7cc5e --- /dev/null +++ b/src/viewerModule/viewerWrapper/viewerWrapper.component.ts @@ -0,0 +1,204 @@ +import { Component, ElementRef, Inject, Optional, Output, inject } from "@angular/core"; +import { Observable, Subject, merge } from "rxjs"; +import { TSupportedViewers, TViewerEvent, isNehubaVCtxEvt, isThreeSurferVCtxEvt, isViewerCtx } from "../viewer.interface"; +import { Store, select } from "@ngrx/store"; +import { MainState, atlasAppearance, atlasSelection, userInteraction } from "src/state"; +import { distinctUntilChanged, filter, finalize, map, shareReplay, takeUntil } from "rxjs/operators"; +import { arrayEqual } from "src/util/array"; +import { DestroyDirective } from "src/util/directives/destroy.directive"; +import { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor, HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; +import { SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; + +@Component({ + selector: 'viewer-wrapper', + templateUrl: './viewerWrapper.template.html', + styleUrls: [ + './viewerWrapper.style.css' + ], + hostDirectives: [ + DestroyDirective + ] +}) +export class ViewerWrapper { + + #destroy$ = inject(DestroyDirective).destroyed$ + + @Output('viewer-event') + viewerEvent$ = new Subject< + TViewerEvent + >() + + selectedAtlas$ = this.store$.pipe( + select(atlasSelection.selectors.selectedAtlas) + ) + + useViewer$: Observable = this.store$.pipe( + select(atlasAppearance.selectors.useViewer), + map(useviewer => { + if (useviewer === "NEHUBA") return "nehuba" + if (useviewer === "THREESURFER") return "threeSurfer" + if (useviewer === "NOT_SUPPORTED") return "notsupported" + return null + }) + ) + + constructor( + el: ElementRef, + private store$: Store, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) + hoverInterceptor: HoverInterceptor, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) + clickInterceptor: ClickInterceptor, + ){ + + this.store$.pipe( + select(atlasSelection.selectors.selectedTemplate), + takeUntil(this.#destroy$) + ).subscribe(tmpl => { + this.#selectedTemplate = tmpl + }) + + /** + * handling nehuba event + */ + this.#nehubaViewerCtxEv$.pipe( + takeUntil(this.#destroy$) + ).subscribe(ev => { + const { nehuba, nav } = ev.data.payload + if (nehuba) { + const mousingOverRegions = (nehuba || []).reduce((acc, { regions }) => acc.concat(...regions), []) + this.store$.dispatch( + userInteraction.actions.mouseoverRegions({ + regions: mousingOverRegions + }) + ) + } + if (nav) { + this.store$.dispatch( + userInteraction.actions.mouseoverPosition({ + position: { + loc: nav.position as [number, number, number], + space: this.#selectedTemplate, + spaceId: this.#selectedTemplate.id, + } + }) + ) + } + }) + + /** + * handling threesurfer event + */ + this.#threeSurferViewerCtxEv$.pipe( + takeUntil(this.#destroy$) + ).subscribe(ev => { + const { regions = [] } = ev.data.payload + this.store$.dispatch( + userInteraction.actions.mouseoverRegions({ + regions: regions as SxplrRegion[] + }) + ) + }) + + if (hoverInterceptor) { + let hoverRegionMessages: THoverConfig[] = [] + const { append, remove } = hoverInterceptor + this.#hoveredRegions$.pipe( + takeUntil(this.#destroy$), + finalize(() => { + for (const msg of hoverRegionMessages) { + remove(msg) + } + }) + ).subscribe(regions => { + + for (const msg of hoverRegionMessages) { + remove(msg) + } + + hoverRegionMessages = regions.map(region => ({ + message: region.name || 'Unknown Region', + fontIcon: 'fa-brain', + fontSet: 'fas' + })) + + for (const msg of hoverRegionMessages){ + append(msg) + } + }) + } + + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + let hoveredRegions: SxplrRegion[] + this.#hoveredRegions$.subscribe(reg => { + hoveredRegions = reg as SxplrRegion[] + }) + const handleClick = (ev: PointerEvent) => { + if (!el?.nativeElement?.contains(ev.target)) { + return true + } + if (hoveredRegions.length === 0) { + return true + } + if (ev.ctrlKey) { + this.store$.dispatch( + atlasSelection.actions.toggleRegion({ + region: hoveredRegions[0] + }) + ) + } else { + this.store$.dispatch( + atlasSelection.actions.selectRegion({ + region: hoveredRegions[0] + }) + ) + } + return true + } + register(handleClick, { last: true }) + this.#destroy$.subscribe(() => { + deregister(handleClick) + }) + } + } + + public handleViewerEvent(event: TViewerEvent): void{ + this.viewerEvent$.next(event) + } + + #viewerCtxEvent$ = this.viewerEvent$.pipe( + filter(isViewerCtx), + shareReplay(1), + ) + + #nehubaViewerCtxEv$ = this.#viewerCtxEvent$.pipe( + filter(isNehubaVCtxEvt) + ) + + #threeSurferViewerCtxEv$ = this.#viewerCtxEvent$.pipe( + filter(isThreeSurferVCtxEvt) + ) + + #hoveredRegions$ = merge( + this.#nehubaViewerCtxEv$.pipe( + filter(ev => !!ev.data.payload.nehuba), + map(ev => { + const { nehuba } = ev.data.payload + return nehuba.map(n => n.regions).flatMap(v => v) + }) + ), + this.#threeSurferViewerCtxEv$.pipe( + map(ev => { + const { regions = [] } = ev.data.payload + return regions + }) + ) + ).pipe( + distinctUntilChanged( + arrayEqual((o, n) => o.name === n.name) + ) + ) + + #selectedTemplate: SxplrTemplate = null +} diff --git a/src/viewerModule/viewerWrapper/viewerWrapper.style.css b/src/viewerModule/viewerWrapper/viewerWrapper.style.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/viewerModule/viewerWrapper/viewerWrapper.template.html b/src/viewerModule/viewerWrapper/viewerWrapper.template.html new file mode 100644 index 000000000..5ea7ec0ce --- /dev/null +++ b/src/viewerModule/viewerWrapper/viewerWrapper.template.html @@ -0,0 +1,37 @@ + + + + + + + + + + + +
Template not supported by any of the viewers
+ + +
+ +
+
+ + + Loading + {{ atlas.name }} + +
+
+
+ + + + +
+
From 328c8d3d18235b3093941d899537b9e7b2a930d3 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Fri, 1 Mar 2024 20:31:56 +0100 Subject: [PATCH 32/48] fix lint --- .../threeSurfer/threeSurferGlue/threeSurfer.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index f1a325045..4a5c12380 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -1,4 +1,4 @@ -import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Inject, Optional, ChangeDetectionStrategy } from "@angular/core"; +import { Component, Output, EventEmitter, ElementRef, OnDestroy, AfterViewInit, Optional, ChangeDetectionStrategy } from "@angular/core"; import { EnumViewerEvt, IViewer, TViewerEvent } from "src/viewerModule/viewer.interface"; import { BehaviorSubject, combineLatest, concat, forkJoin, from, merge, NEVER, Observable, of, Subject } from "rxjs"; import { catchError, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, startWith, switchMap, tap, withLatestFrom } from "rxjs/operators"; From f5046a6a146d128c5d2f4a6e31c16a0f7c44a0f5 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 14 Mar 2024 17:16:14 +0100 Subject: [PATCH 33/48] fix: annotation mode occassional malfunctions fix: gracefully handles when saneurl is not present --- backend/app/index_html.py | 4 +- backend/app/sane_url.py | 13 +++-- docs/releases/v2.14.5.md | 5 +- .../annotations/annotation.service.ts | 8 ++-- src/components/coordTextBox/index.ts | 9 ++++ src/sharedModules/angularMaterial.exports.ts | 2 +- .../bottomMenuCmp/bottomMenu.template.html | 48 ------------------- ...atingMouseContextualContainer.directive.ts | 1 + src/util/directives/mediaQuery.directive.ts | 3 +- src/util/util.module.ts | 3 -- src/viewerModule/module.ts | 4 +- src/viewerModule/nehuba/module.ts | 2 + .../nehuba/statusCard/statusCard.component.ts | 33 +++++++++++-- .../statusCard/statusCard.template.html | 7 ++- .../userlayerInfo/userlayerInfo.component.ts | 29 +++++------ .../userlayerInfo/userlayerInfo.template.html | 3 -- .../viewerCmp/viewerCmp.template.html | 37 ++++---------- 17 files changed, 92 insertions(+), 119 deletions(-) diff --git a/backend/app/index_html.py b/backend/app/index_html.py index 504ec7bb1..c054d76ca 100644 --- a/backend/app/index_html.py +++ b/backend/app/index_html.py @@ -26,7 +26,7 @@ async def get_index_html(request: Request): error = None attributes_to_append: Dict[str, str] = {} if ERROR_KEY in request.session: - error = request.session[ERROR_KEY] + error = request.session.pop(ERROR_KEY) attributes_to_append[DATA_ERROR_ATTR] = error if OVERWRITE_API_ENDPOINT: @@ -38,7 +38,7 @@ async def get_index_html(request: Request): if EXPERIMENTAL_FLAG: attributes_to_append[OVERWRITE_EXPERIMENTAL_FLAG_ATTR] = EXPERIMENTAL_FLAG - attr_string = " ".join([f"{key}={_monkey_sanitize(value)}" for key, value in attributes_to_append.items()]) + attr_string = " ".join([f'{key}="{_monkey_sanitize(value)}"' for key, value in attributes_to_append.items()]) resp_string = index_html.replace("", f"") diff --git a/backend/app/sane_url.py b/backend/app/sane_url.py index e65c1982c..8a68cf884 100644 --- a/backend/app/sane_url.py +++ b/backend/app/sane_url.py @@ -10,7 +10,7 @@ from pydantic import BaseModel from .config import SXPLR_EBRAINS_IAM_SA_CLIENT_ID, SXPLR_EBRAINS_IAM_SA_CLIENT_SECRET, SXPLR_BUCKET_NAME, HOST_PATHNAME -from .const import EBRAINS_IAM_DISCOVERY_URL +from .const import EBRAINS_IAM_DISCOVERY_URL, ERROR_KEY from ._store import DataproxyStore from .user import get_user_from_request @@ -135,10 +135,11 @@ def delete(self, key: str): @router.get("/{short_id:str}") async def get_short(short_id:str, request: Request): + accept = request.headers.get("Accept", "") + is_browser = "text/html" in accept try: existing_json: Dict[str, Any] = data_proxy_store.get(short_id) - accept = request.headers.get("Accept", "") - if "text/html" in accept: + if is_browser: hashed_path = existing_json.get("hashPath") extra_routes = [] for key in existing_json: @@ -151,8 +152,14 @@ async def get_short(short_id:str, request: Request): return RedirectResponse(f"{HOST_PATHNAME}/#{hashed_path}{extra_routes_str}") return JSONResponse(existing_json) except DataproxyStore.NotFound as e: + if is_browser: + request.session[ERROR_KEY] = f"Short ID {short_id} not found." + return RedirectResponse(HOST_PATHNAME or "/") raise HTTPException(404, str(e)) except DataproxyStore.GenericException as e: + if is_browser: + request.session[ERROR_KEY] = f"Error: {str(e)}" + return RedirectResponse(HOST_PATHNAME or "/") raise HTTPException(500, str(e)) diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index 895c81d44..634e10296 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -10,14 +10,17 @@ - Added legend to region hierarchy - Allow experimental flag to be set to be on at runtime (this also shows the button, allow toggling of experimental features) - (experimental) allow addition of custom linear coordinate space +- (experimental) show BigBrain slice number ## Bugfix - Copy of free text (Ctrl + C) now works properly +- Fixes issue where annotation mode was not displaying correctly, after selecting volume of interest +- When saneURL is not found, siibra-explorer will not correctly redirects to default app, and show the error message ## Behind the Scenes - Removed dependency on connectivity-component - Removed reference to JSC OKD instance, as the instance is no longer available - Updated google-site-verification -- Allow inter space transform to be configured at runtime +- Allow inter-space transform to be configured at runtime diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index d8242e62f..cd47c8d37 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -82,7 +82,10 @@ export class AnnotationLayer { distinctUntilChanged((o, n) => o?.id === n?.id) ) private onDestroyCb: (() => void)[] = [] - private nglayer: NgAnnotationLayer + + get nglayer(){ + return this.viewer.layerManager.getLayerByName(this.name) + } private idset = new Set() constructor( private name: string = getUuid(), @@ -99,7 +102,7 @@ export class AnnotationLayer { transform: affine, } ) - this.nglayer = this.viewer.layerManager.addManagedLayer(layerSpec) + this.viewer.layerManager.addManagedLayer(layerSpec) const mouseState = this.viewer.mouseState const res: () => void = mouseState.changed.add(() => { const payload = mouseState.active @@ -131,7 +134,6 @@ export class AnnotationLayer { try { const l = this.viewer.layerManager.getLayerByName(this.name) this.viewer.layerManager.removeManagedLayer(l) - this.nglayer = null // eslint-disable-next-line no-empty } catch (e) { console.error("removing layer failed", e) diff --git a/src/components/coordTextBox/index.ts b/src/components/coordTextBox/index.ts index fe8e63ce1..a157bc32e 100644 --- a/src/components/coordTextBox/index.ts +++ b/src/components/coordTextBox/index.ts @@ -1 +1,10 @@ +import { TAffine } from "./coordTextBox.component" + export * from "./coordTextBox.component" + +export const ID_AFFINE = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], +] as TAffine diff --git a/src/sharedModules/angularMaterial.exports.ts b/src/sharedModules/angularMaterial.exports.ts index 09611d70f..4c83edd64 100644 --- a/src/sharedModules/angularMaterial.exports.ts +++ b/src/sharedModules/angularMaterial.exports.ts @@ -1,3 +1,4 @@ +export { MatTab, MatTabGroup } from "@angular/material/tabs"; export { ErrorStateMatcher } from "@angular/material/core"; export { MatDialogConfig, MatDialog, MatDialogRef } from "@angular/material/dialog"; export { MatSnackBar, MatSnackBarRef, SimpleSnackBar, MatSnackBarConfig } from "@angular/material/snack-bar"; @@ -12,5 +13,4 @@ export { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree" export { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; export { MatPaginator } from "@angular/material/paginator"; export { MatInput } from "@angular/material/input"; - export { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing' diff --git a/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html b/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html index f0e7a78b0..3703c3633 100644 --- a/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html +++ b/src/ui/bottomMenu/bottomMenuCmp/bottomMenu.template.html @@ -1,5 +1,3 @@ - - @@ -83,49 +81,3 @@ - - - - - - - - - - - - - Feature - - - - - - {{ feature.name }} - - - - - - - - - - - - - - - - - - - - diff --git a/src/util/directives/floatingMouseContextualContainer.directive.ts b/src/util/directives/floatingMouseContextualContainer.directive.ts index d924d7133..2c2f0def8 100644 --- a/src/util/directives/floatingMouseContextualContainer.directive.ts +++ b/src/util/directives/floatingMouseContextualContainer.directive.ts @@ -3,6 +3,7 @@ import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; @Directive({ selector: '[floatingMouseContextualContainerDirective]', + standalone: true, }) export class FloatingMouseContextualContainerDirective { diff --git a/src/util/directives/mediaQuery.directive.ts b/src/util/directives/mediaQuery.directive.ts index 08d77130d..68fa61fd2 100644 --- a/src/util/directives/mediaQuery.directive.ts +++ b/src/util/directives/mediaQuery.directive.ts @@ -24,7 +24,8 @@ enum EnumMediaBreakPoints{ @Directive({ selector: '[iav-media-query]', - exportAs: 'iavMediaQuery' + exportAs: 'iavMediaQuery', + standalone: true }) export class MediaQueryDirective{ diff --git a/src/util/util.module.ts b/src/util/util.module.ts index f2b7ccc21..210445361 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -5,7 +5,6 @@ import { SafeResourcePipe } from "./pipes/safeResource.pipe"; import { CaptureClickListenerDirective } from "./directives/captureClickListener.directive"; import { NmToMm } from "./pipes/nmToMm.pipe"; import { SwitchDirective } from "./directives/switch.directive"; -import { MediaQueryDirective } from './directives/mediaQuery.directive' import { LayoutModule } from "@angular/cdk/layout"; import { MapToPropertyPipe } from "./pipes/mapToProperty.pipe"; import { ClickOutsideDirective } from "src/util/directives/clickOutside.directive"; @@ -38,7 +37,6 @@ import { PrettyPresentPipe } from './pretty-present.pipe'; CaptureClickListenerDirective, NmToMm, SwitchDirective, - MediaQueryDirective, MapToPropertyPipe, ClickOutsideDirective, GetNthElementPipe, @@ -62,7 +60,6 @@ import { PrettyPresentPipe } from './pretty-present.pipe'; CaptureClickListenerDirective, NmToMm, SwitchDirective, - MediaQueryDirective, MapToPropertyPipe, ClickOutsideDirective, GetNthElementPipe, diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 2ecdfdf8b..0d2d81925 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -42,6 +42,7 @@ import { TabComponent } from "src/components/tab/tab.components"; import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; +import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; @NgModule({ imports: [ @@ -71,6 +72,8 @@ import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; TabComponent, MouseOver, + MediaQueryDirective, + FloatingMouseContextualContainerDirective, ExperimentalFlagDirective, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) @@ -79,7 +82,6 @@ import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; ViewerCmp, NehubaVCtxToBbox, LogoContainer, - FloatingMouseContextualContainerDirective, ViewerWrapper, ], providers: [ diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts index d6fb5813f..76b1be3db 100644 --- a/src/viewerModule/nehuba/module.ts +++ b/src/viewerModule/nehuba/module.ts @@ -30,6 +30,7 @@ import { NehubaUserLayerModule } from "./userLayers"; import { DialogModule } from "src/ui/dialogInfo"; import { CoordTextBox } from "src/components/coordTextBox"; import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; +import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; @NgModule({ imports: [ @@ -42,6 +43,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di ShareModule, WindowResizeModule, NehubaUserLayerModule, + MediaQueryDirective, /** * should probably break this into its own... diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index 7dc07eb9a..e945b578e 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -8,8 +8,8 @@ import { import { select, Store } from "@ngrx/store"; import { LoggingService } from "src/logging"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; -import { Observable, Subject, concat, of } from "rxjs"; -import { map, filter, takeUntil, switchMap, shareReplay, debounceTime, scan } from "rxjs/operators"; +import { Observable, Subject, combineLatest, concat, of } from "rxjs"; +import { map, filter, takeUntil, switchMap, shareReplay, debounceTime, scan, startWith } from "rxjs/operators"; import { Clipboard, MatBottomSheet, MatSnackBar } from "src/sharedModules/angularMaterial.exports" import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { FormControl, FormGroup } from "@angular/forms"; @@ -22,11 +22,13 @@ import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { NEHUBA_CONFIG_SERVICE_TOKEN, NehubaConfigSvc } from "../config.service"; import { DestroyDirective } from "src/util/directives/destroy.directive"; import { getUuid } from "src/util/fn"; -import { Render, TAffine, isAffine } from "src/components/coordTextBox" +import { Render, TAffine, isAffine, ID_AFFINE } from "src/components/coordTextBox" +import { IDS } from "src/atlasComponents/sapi"; type TSpace = { label: string affine: TAffine + render?: Render } @Component({ @@ -40,8 +42,29 @@ type TSpace = { export class StatusCardComponent { #newSpace = new Subject() - additionalSpace$ = this.#newSpace.pipe( - scan((acc, v) => acc.concat(v), [] as TSpace[]) + additionalSpace$ = combineLatest([ + this.store$.pipe( + select(atlasSelection.selectors.selectedTemplate), + map(tmpl => { + if (tmpl.id === IDS.TEMPLATES.BIG_BRAIN) { + const tspace: TSpace = { + affine: ID_AFFINE, + label: "BigBrain slice index", + render: v => `Slice ${Math.ceil((v[1] + 70.010) / 0.02)}` + } + return [tspace] + } + return [] + }) + ), + concat( + of([] as TSpace[]), + this.#newSpace.pipe( + scan((acc, v) => acc.concat(v), [] as TSpace[]), + ) + ) + ]).pipe( + map(([predefined, custom]) => [...predefined, ...custom]) ) readonly idAffStr = `[ [1, 0, 0, 0], diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html index 2b17c8e75..f3226b50c 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.template.html +++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html @@ -72,13 +72,15 @@ +
@@ -91,6 +93,7 @@
+
- +
@@ -588,17 +578,6 @@ - - - - - From 400d758b3db8877868c9d86140384fca203f478f Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 14 Mar 2024 17:48:31 +0100 Subject: [PATCH 34/48] fix lint, test --- src/atlasComponents/annotations/annotation.service.ts | 2 +- .../nehuba/statusCard/statusCard.component.spec.ts | 4 +++- src/viewerModule/nehuba/statusCard/statusCard.component.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index cd47c8d37..a7590df02 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -83,7 +83,7 @@ export class AnnotationLayer { ) private onDestroyCb: (() => void)[] = [] - get nglayer(){ + get nglayer(): NgAnnotationLayer{ return this.viewer.layerManager.getLayerByName(this.name) } private idset = new Set() diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts index df15daa10..d582da672 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts @@ -15,6 +15,7 @@ import { QuickTourModule } from "src/ui/quickTour/module"; import { atlasSelection } from "src/state" import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes" import { NEHUBA_INSTANCE_INJTKN } from "../util" +import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive" const mockNehubaConfig = { dataset: { @@ -60,7 +61,8 @@ describe('> statusCard.component.ts', () => { ReactiveFormsModule, NoopAnimationsModule, UtilModule, - QuickTourModule + QuickTourModule, + MediaQueryDirective, ], declarations: [ StatusCardComponent, diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index e945b578e..2f14e2201 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -9,7 +9,7 @@ import { select, Store } from "@ngrx/store"; import { LoggingService } from "src/logging"; import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component"; import { Observable, Subject, combineLatest, concat, of } from "rxjs"; -import { map, filter, takeUntil, switchMap, shareReplay, debounceTime, scan, startWith } from "rxjs/operators"; +import { map, filter, takeUntil, switchMap, shareReplay, debounceTime, scan } from "rxjs/operators"; import { Clipboard, MatBottomSheet, MatSnackBar } from "src/sharedModules/angularMaterial.exports" import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { FormControl, FormGroup } from "@angular/forms"; From 927bcde802131edd843a65613a288278226e414b Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Tue, 16 Apr 2024 16:05:47 +0200 Subject: [PATCH 35/48] maint: remove HOSTNAME as deploy env var, typo, typing feat: add queried concept feat: added back btn to child feat (to compound feat) bugfix: dismiss plot when new feature is selected --- .helm/adhoc/certificate-sxplr-ebrains.yml | 21 ++++ backend/app/auth.py | 5 +- backend/app/config.py | 2 - deploy_env.md | 1 - docs/releases/v2.14.5.md | 1 + package-lock.json | 6 +- src/api/broadcast/README.md | 2 +- src/environments/environment.common.ts | 3 +- src/features/TPBRView/TPBRView.component.ts | 40 +++++++ src/features/TPBRView/TPBRView.style.scss | 0 src/features/TPBRView/TPBRView.template.html | 22 ++++ src/features/base.ts | 4 +- .../compoundFtContainer.component.ts | 51 -------- .../compoundFtContainer.style.css | 16 --- .../compoundFtContainer.template.html | 19 --- src/features/compoundFtContainer/index.ts | 2 - .../compoundFtContainer/indexToText.pipe.ts | 15 --- src/features/compoundFtContainer/module.ts | 20 ---- src/features/entry/entry.component.spec.ts | 9 +- src/features/entry/entry.component.ts | 113 ++++++++++-------- .../feature-view/feature-view.component.html | 62 +++++++++- .../feature-view.component.spec.ts | 7 ++ .../feature-view/feature-view.component.ts | 108 ++++++++++++----- src/features/module.ts | 21 +++- src/features/util.ts | 19 +++ src/util/constants.ts | 2 +- src/viewerModule/module.ts | 2 + .../viewerCmp/viewerCmp.component.ts | 6 - .../viewerCmp/viewerCmp.template.html | 34 ++---- 29 files changed, 355 insertions(+), 258 deletions(-) create mode 100644 .helm/adhoc/certificate-sxplr-ebrains.yml create mode 100644 src/features/TPBRView/TPBRView.component.ts create mode 100644 src/features/TPBRView/TPBRView.style.scss create mode 100644 src/features/TPBRView/TPBRView.template.html delete mode 100644 src/features/compoundFtContainer/compoundFtContainer.component.ts delete mode 100644 src/features/compoundFtContainer/compoundFtContainer.style.css delete mode 100644 src/features/compoundFtContainer/compoundFtContainer.template.html delete mode 100644 src/features/compoundFtContainer/index.ts delete mode 100644 src/features/compoundFtContainer/indexToText.pipe.ts delete mode 100644 src/features/compoundFtContainer/module.ts create mode 100644 src/features/util.ts diff --git a/.helm/adhoc/certificate-sxplr-ebrains.yml b/.helm/adhoc/certificate-sxplr-ebrains.yml new file mode 100644 index 000000000..19a96659e --- /dev/null +++ b/.helm/adhoc/certificate-sxplr-ebrains.yml @@ -0,0 +1,21 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: siibra-explorer-certificate +spec: + secretName: sxplr-ebrains-secret + renewBefore: 120h + commonName: siibra-explorer.apps.ebrains.eu + isCA: false + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + dnsNames: + # (CHANGE ME! same as `commonName`) + - siibra-explorer.apps.ebrains.eu + issuerRef: + name: letsencrypt-production-issuer-1 + kind: ClusterIssuer diff --git a/backend/app/auth.py b/backend/app/auth.py index fc3fed6a8..9f2eaef47 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -7,7 +7,7 @@ import json from .const import EBRAINS_IAM_DISCOVERY_URL, SCOPES, PROFILE_KEY -from .config import HBP_CLIENTID_V2, HBP_CLIENTSECRET_V2, HOST_PATHNAME, HOSTNAME +from .config import HBP_CLIENTID_V2, HBP_CLIENTSECRET_V2, HOST_PATHNAME from ._store import RedisEphStore _store = RedisEphStore.Ephemeral() @@ -38,13 +38,12 @@ def process_ebrains_user(resp): router = APIRouter() -redirect_uri = HOSTNAME.rstrip("/") + HOST_PATHNAME + "/hbp-oidc-v2/cb" - @router.get("/hbp-oidc-v2/auth") async def login_via_ebrains(request: Request, state: str = None): kwargs = {} if state: kwargs["state"] = state + redirect_uri = str(request.base_url).rstrip("/") + HOST_PATHNAME + "/hbp-oidc-v2/cb" return await oauth.ebrains.authorize_redirect(request, redirect_uri=redirect_uri, **kwargs) @router.get("/hbp-oidc-v2/cb") diff --git a/backend/app/config.py b/backend/app/config.py index 27c5b3640..d51631de3 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -4,8 +4,6 @@ HOST_PATHNAME = os.getenv("HOST_PATHNAME", "") -HOSTNAME = os.getenv("HOSTNAME", "http://localhost:3000") - OVERWRITE_API_ENDPOINT = os.getenv("OVERWRITE_API_ENDPOINT") OVERWRITE_SPATIAL_ENDPOINT = os.getenv("OVERWRITE_SPATIAL_ENDPOINT") diff --git a/deploy_env.md b/deploy_env.md index 00df9cfd3..1ead2b474 100644 --- a/deploy_env.md +++ b/deploy_env.md @@ -17,7 +17,6 @@ | name | description | default | example | | --- | --- | --- | --- | -| `HOSTNAME` | | `HOST_PATHNAME` | pathname to listen on, restrictions: leading slash, no trailing slash | `''` | `/viewer` | | `HBP_CLIENTID_V2` | | `HBP_CLIENTSECRET_V2` | diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index 634e10296..ec6e17c76 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -8,6 +8,7 @@ - Reworded point assignment UI - Allow multi selected region names to be copied - Added legend to region hierarchy +- Allow latest queried concept in feature view - Allow experimental flag to be set to be on at runtime (this also shows the button, allow toggling of experimental features) - (experimental) allow addition of custom linear coordinate space - (experimental) show BigBrain slice number diff --git a/package-lock.json b/package-lock.json index 59b66bed8..2fd6dcfa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7646,9 +7646,9 @@ "dev": true }, "node_modules/export-nehuba": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.5.tgz", - "integrity": "sha512-5Gsgvd0BLO4evuxp4bwrBS64Em1X92vyW2mwy9BvmaptHp9DbDf05zxi6Phu7apDY+Hzc3XEaxKOVGoMACeDJg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/export-nehuba/-/export-nehuba-0.1.7.tgz", + "integrity": "sha512-LakXeWGkEtHwWrV69snlM2GGmeVP+jGnTaevOpWQJePkdkPq6DvCkCSH0mLBriR8yOPyO0e+VHuE3V4AnV4fPA==", "dependencies": { "pako": "^1.0.6" } diff --git a/src/api/broadcast/README.md b/src/api/broadcast/README.md index 358d06ae2..fd7719946 100644 --- a/src/api/broadcast/README.md +++ b/src/api/broadcast/README.md @@ -6,7 +6,7 @@ Broadcasting messages are sent under two circumstances: - immediately after the plugin client acknowledged `handshake.init` to the specific client. This is so that the client can get the current state of the viewer. -Broadcasting messages never expects a response (and thus will never contain and `id` attribute) +Broadcasting messages never expects a response (and thus will never contain an `id` attribute) ## API diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 1fb19a6a6..4ac980129 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -10,7 +10,8 @@ export const environment = { // 'http://localhost:10081/v3_0', // endpoint-local-10081 // 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0', //endpoint-latest // 'https://siibra-api-rc.apps.hbp.eu/v3_0', // endpoint-rc - 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable + // 'https://siibra-api-stable.apps.hbp.eu/v3_0', // endpoint-stable + 'https://siibra-api-rc.apps.tc.humanbrainproject.eu/v3_0', // endpoint-rc-tc SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, MATOMO_ID: null, diff --git a/src/features/TPBRView/TPBRView.component.ts b/src/features/TPBRView/TPBRView.component.ts new file mode 100644 index 000000000..866739f60 --- /dev/null +++ b/src/features/TPBRView/TPBRView.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from "@angular/core"; +import { TPRB } from "../util"; +import { CommonModule } from "@angular/common"; +import { AngularMaterialModule } from "src/sharedModules"; +import { BehaviorSubject } from "rxjs"; +import { map, throttleTime } from "rxjs/operators"; + +@Component({ + selector: 'tpbr-viewer', + templateUrl: './TPBRView.template.html', + styleUrls: [ + './TPBRView.style.scss' + ], + standalone: true, + imports: [ + CommonModule, + AngularMaterialModule, + ] +}) +export class TPBRViewCmp { + @Input('tpbr-concept') + set _tpbr(value: TPRB){ + this.#tpbr.next(value) + } + #tpbr = new BehaviorSubject(null) + + view$ = this.#tpbr.pipe( + throttleTime(16), + map(v => { + if (!v) return null + return { + ...v, + bboxString: v.bbox && { + from: v.bbox[0].map(v => v.toFixed(2)).join(", "), + to: v.bbox[1].map(v => v.toFixed(2)).join(", "), + } + } + }) + ) +} diff --git a/src/features/TPBRView/TPBRView.style.scss b/src/features/TPBRView/TPBRView.style.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/features/TPBRView/TPBRView.template.html b/src/features/TPBRView/TPBRView.template.html new file mode 100644 index 000000000..3f5845846 --- /dev/null +++ b/src/features/TPBRView/TPBRView.template.html @@ -0,0 +1,22 @@ + +
+ {{ tpbr.template.name }} +
+ + +
+ from {{ bboxString.from }} +
+
+ to {{ bboxString.to }} +
+
+ +
+ {{ tpbr.parcellation.name }} +
+
+ {{ tpbr.region.name }} +
+ +
diff --git a/src/features/base.ts b/src/features/base.ts index c5085aabd..353118ae8 100644 --- a/src/features/base.ts +++ b/src/features/base.ts @@ -2,8 +2,8 @@ import { Input, OnChanges, Directive, SimpleChanges } from "@angular/core"; import { BehaviorSubject, combineLatest } from "rxjs"; import { debounceTime, map } from "rxjs/operators"; import { SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +import { BBox } from "./util"; -type BBox = [[number, number, number], [number, number, number]] @Directive() export class FeatureBase implements OnChanges{ @@ -31,7 +31,7 @@ export class FeatureBase implements OnChanges{ debounceTime(500) ) ]).pipe( - map(([ v1, v2 ]) => ({ ...v1, ...v2 })) + map(([ v1, v2 ]) => ({ ...v1, ...v2 })), ) ngOnChanges(sc: SimpleChanges): void { diff --git a/src/features/compoundFtContainer/compoundFtContainer.component.ts b/src/features/compoundFtContainer/compoundFtContainer.component.ts deleted file mode 100644 index f1529e6f0..000000000 --- a/src/features/compoundFtContainer/compoundFtContainer.component.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { Store } from "@ngrx/store"; -import { BehaviorSubject } from "rxjs"; -import { SAPI } from "src/atlasComponents/sapi"; -import { SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; -import { userInteraction } from "src/state"; - -@Component({ - templateUrl: './compoundFtContainer.template.html', - styleUrls: [ - './compoundFtContainer.style.css' - ], - selector: 'compound-feature-container', -}) - -export class CompoundFtContainer { - @Input() - compoundFeature: SimpleCompoundFeature - - @Output() - dismiss = new EventEmitter() - - busy$ = new BehaviorSubject(false) - - constructor(private sapi: SAPI, private store: Store, private snackbar: MatSnackBar){ - } - async showSubfeature(id: string){ - try { - this.busy$.next(true) - const feature = await this.sapi.getV3FeatureDetailWithId(id).toPromise() - this.store.dispatch( - userInteraction.actions.showFeature({ feature }) - ) - this.dismiss.emit() - } catch (e) { - console.log('error', e) - this.snackbar.open(`Error: ${e.toString()}`, "Dismiss") - } finally { - this.busy$.next(false) - } - } -} - -/** - * TODO - * - * check http://localhost:10081/v3_0/feature/lq0::BigBrainIntensityProfile::p:minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-300::r:Area%20hOc1%20(V1,%2017,%20CalcS)%20left::4c05163cac01b560cddf9d0ae2b63c94 - * - * see https://github.com/FZJ-INM1-BDA/siibra-python/issues/509 - */ \ No newline at end of file diff --git a/src/features/compoundFtContainer/compoundFtContainer.style.css b/src/features/compoundFtContainer/compoundFtContainer.style.css deleted file mode 100644 index 889029fd9..000000000 --- a/src/features/compoundFtContainer/compoundFtContainer.style.css +++ /dev/null @@ -1,16 +0,0 @@ -cdk-virtual-scroll-viewport -{ - display: block; - min-height:30vh; -} - -cdk-virtual-scroll-viewport button -{ - width: 100%; - justify-content: left; -} - -mat-divider -{ - margin: 1rem 0; -} diff --git a/src/features/compoundFtContainer/compoundFtContainer.template.html b/src/features/compoundFtContainer/compoundFtContainer.template.html deleted file mode 100644 index b7ace3dd7..000000000 --- a/src/features/compoundFtContainer/compoundFtContainer.template.html +++ /dev/null @@ -1,19 +0,0 @@ -
- Please select an element -
- - - - - - - - - diff --git a/src/features/compoundFtContainer/index.ts b/src/features/compoundFtContainer/index.ts deleted file mode 100644 index e89292139..000000000 --- a/src/features/compoundFtContainer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CompoundFeatureModule } from "./module" -export { CompoundFtContainer } from "./compoundFtContainer.component" diff --git a/src/features/compoundFtContainer/indexToText.pipe.ts b/src/features/compoundFtContainer/indexToText.pipe.ts deleted file mode 100644 index 79fd38155..000000000 --- a/src/features/compoundFtContainer/indexToText.pipe.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { SimpleCompoundFeature } from "src/atlasComponents/sapi/sxplrTypes"; - -@Pipe({ - name: 'indexToStr', - pure: true -}) -export class IndexToStrPipe implements PipeTransform{ - public transform(value: SimpleCompoundFeature['indices'][number]['index']): string { - if (typeof value === "string") { - return value - } - return `Point(${value.loc.join(", ")})` - } -} diff --git a/src/features/compoundFtContainer/module.ts b/src/features/compoundFtContainer/module.ts deleted file mode 100644 index f09d1d00d..000000000 --- a/src/features/compoundFtContainer/module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from "@angular/core"; -import { CompoundFtContainer } from "./compoundFtContainer.component"; -import { AngularMaterialModule } from "src/sharedModules"; -import { CommonModule } from "@angular/common"; -import { IndexToStrPipe } from "./indexToText.pipe"; - -@NgModule({ - imports: [ - AngularMaterialModule, - CommonModule, - ], - declarations: [ - CompoundFtContainer, - IndexToStrPipe, - ], - exports: [ - CompoundFtContainer - ] -}) -export class CompoundFeatureModule{} diff --git a/src/features/entry/entry.component.spec.ts b/src/features/entry/entry.component.spec.ts index dd5fc359a..94c60cf1a 100644 --- a/src/features/entry/entry.component.spec.ts +++ b/src/features/entry/entry.component.spec.ts @@ -5,6 +5,7 @@ import { SAPIModule } from 'src/atlasComponents/sapi'; import { EntryComponent } from './entry.component'; import { provideMockStore } from '@ngrx/store/testing'; import { FeatureModule } from '../module'; +import { FEATURE_CONCEPT_TOKEN, TPRB } from '../util'; describe('EntryComponent', () => { let component: EntryComponent; @@ -18,7 +19,13 @@ describe('EntryComponent', () => { ], declarations: [ ], providers: [ - provideMockStore() + provideMockStore(), + { + provide: FEATURE_CONCEPT_TOKEN, + useValue: { + register(id: string, tprb: TPRB){} + } + } ] }) .compileComponents(); diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index 6201f9761..f076c6482 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,16 +1,17 @@ -import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, QueryList, TemplateRef, ViewChildren } from '@angular/core'; -import { select, Store } from '@ngrx/store'; -import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { AfterViewInit, ChangeDetectorRef, Component, Inject, QueryList, TemplateRef, ViewChildren, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, takeUntil, withLatestFrom } 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" -import { atlasSelection } from 'src/state'; import { CategoryAccDirective } from "../category-acc.directive" -import { combineLatest, concat, forkJoin, merge, of, Subject, Subscription } from 'rxjs'; +import { combineLatest, concat, forkJoin, merge, of, Subject } from 'rxjs'; import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable'; import { TranslatedFeature } from '../list/list.directive'; import { MatDialog } from 'src/sharedModules/angularMaterial.exports'; +import { DestroyDirective } from 'src/util/directives/destroy.directive'; +import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from '../util'; const categoryAcc = >(categories: T[]) => { const returnVal: Record = {} @@ -30,18 +31,35 @@ const categoryAcc = >(categories: T[]) => { selector: 'sxplr-feature-entry', templateUrl: './entry.flattened.component.html', styleUrls: ['./entry.flattened.component.scss'], - exportAs: 'featureEntryCmp' + exportAs: 'featureEntryCmp', + hostDirectives: [ + DestroyDirective + ] }) -export class EntryComponent extends FeatureBase implements AfterViewInit, OnDestroy { +export class EntryComponent extends FeatureBase implements AfterViewInit { + + ondestroy$ = inject(DestroyDirective).destroyed$ @ViewChildren(CategoryAccDirective) catAccDirs: QueryList - constructor(private sapi: SAPI, private store: Store, private dialog: MatDialog, private cdr: ChangeDetectorRef) { + constructor( + private sapi: SAPI, + private store: Store, + private dialog: MatDialog, + private cdr: ChangeDetectorRef, + @Inject(FEATURE_CONCEPT_TOKEN) private featConcept: FeatureConcept, + ) { super() + + this.TPRBbox$.pipe( + takeUntil(this.ondestroy$) + ).subscribe(tprb => { + this.#tprb = tprb + }) } + #tprb: TPRB - #subscriptions: Subscription[] = [] #catAccDirs = new Subject() features$ = this.#catAccDirs.pipe( switchMap(dirs => concat( @@ -106,50 +124,42 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest )) ) - ngOnDestroy(): void { - while (this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe() - } ngAfterViewInit(): void { - this.#subscriptions.push( - merge( - of(null), - this.catAccDirs.changes - ).pipe( - map(() => Array.from(this.catAccDirs)) - ).subscribe(dirs => this.#catAccDirs.next(dirs)), - - this.#pullAll.pipe( - debounceTime(320), - withLatestFrom(this.#catAccDirs), - switchMap(([_, dirs]) => combineLatest(dirs.map(dir => dir.datasource$))), - ).subscribe(async dss => { - await Promise.all( - dss.map(async ds => { - // eslint-disable-next-line no-constant-condition - while (true) { - try { - await ds.pull() - } catch (e) { - if (e instanceof DsExhausted) { - break - } - if (e instanceof IsAlreadyPulling ) { - continue - } - throw e + merge( + of(null), + this.catAccDirs.changes + ).pipe( + map(() => Array.from(this.catAccDirs)), + takeUntil(this.ondestroy$), + ).subscribe(dirs => this.#catAccDirs.next(dirs)) + + this.#pullAll.pipe( + debounceTime(320), + withLatestFrom(this.#catAccDirs), + switchMap(([_, dirs]) => combineLatest(dirs.map(dir => dir.datasource$))), + takeUntil(this.ondestroy$), + ).subscribe(async dss => { + await Promise.all( + dss.map(async ds => { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await ds.pull() + } catch (e) { + if (e instanceof DsExhausted) { + break } + if (e instanceof IsAlreadyPulling ) { + continue + } + throw e } - }) - ) - }) - ) + } + }) + ) + }) } - public selectedAtlas$ = this.store.pipe( - select(atlasSelection.selectors.selectedAtlas) - ) - - private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( switchMap(resp => this.sapi.iteratePages( @@ -191,6 +201,13 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest ) onClickFeature(feature: Feature) { + + /** + * register of TPRB (template, parcellation, region, bbox) *has* to + * happen at the moment when feature is selected + */ + this.featConcept.register(feature.id, this.#tprb) + this.store.dispatch( userInteraction.actions.showFeature({ feature diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 86e523780..05bffb881 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -1,9 +1,30 @@ - - - - - + + + + + + + + + + + + + @@ -42,6 +63,35 @@ + + + + + + + + + Queried Concepts + + + Concepts queried to get this feature. Please note this property is session dependent. + + + + + + + + + + + + @@ -74,7 +124,7 @@ + (on-click-index)="showSubfeature($event)"> diff --git a/src/features/feature-view/feature-view.component.spec.ts b/src/features/feature-view/feature-view.component.spec.ts index 817347a3a..567a8c882 100644 --- a/src/features/feature-view/feature-view.component.spec.ts +++ b/src/features/feature-view/feature-view.component.spec.ts @@ -7,6 +7,7 @@ import { DARKTHEME } from 'src/util/injectionTokens'; import { FeatureViewComponent } from './feature-view.component'; import { provideMockStore } from '@ngrx/store/testing'; import { AngularMaterialModule } from 'src/sharedModules'; +import { FEATURE_CONCEPT_TOKEN } from '../util'; describe('FeatureViewComponent', () => { let component: FeatureViewComponent; @@ -36,6 +37,12 @@ describe('FeatureViewComponent', () => { }, sapiEndpoint$: EMPTY } + }, + { + provide: FEATURE_CONCEPT_TOKEN, + useValue: { + concept$: EMPTY + } } ] }) diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index c96cd7ee6..f95b096cd 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, inject } from '@angular/core'; import { BehaviorSubject, EMPTY, Observable, combineLatest, concat, of } from 'rxjs'; -import { catchError, debounceTime, distinctUntilChanged, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; import { SAPI } from 'src/atlasComponents/sapi/sapi.service'; import { Feature, SimpleCompoundFeature, VoiFeature } from 'src/atlasComponents/sapi/sxplrTypes'; import { DARKTHEME } from 'src/util/injectionTokens'; @@ -8,22 +8,37 @@ import { isVoiData, notQuiteRight } from "../guards" import { Action, Store, select } from '@ngrx/store'; import { atlasSelection, userInteraction } from 'src/state'; import { PathReturn } from 'src/atlasComponents/sapi/typeV3'; -import { MatSnackBar } from '@angular/material/snack-bar'; +import { CFIndex } from '../compoundFeatureIndices'; +import { ComponentStore } from '@ngrx/component-store'; +import { DestroyDirective } from 'src/util/directives/destroy.directive'; +import { FEATURE_CONCEPT_TOKEN, FeatureConcept } from '../util'; + +type FeatureCmpStore = { + selectedCmpFeature: SimpleCompoundFeature|null +} type PlotlyResponse = PathReturn<"/feature/{feature_id}/plotly"> function isSimpleCompoundFeature(feat: unknown): feat is SimpleCompoundFeature{ - return !!feat['indices'] + return !!(feat?.['indices']) } @Component({ selector: 'sxplr-feature-view', templateUrl: './feature-view.component.html', styleUrls: ['./feature-view.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + ComponentStore + ], + hostDirectives: [ + DestroyDirective + ] }) export class FeatureViewComponent { + destroyed$ = inject(DestroyDirective).destroyed$ + busy$ = new BehaviorSubject(false) #feature$ = new BehaviorSubject(null) @@ -122,14 +137,17 @@ export class FeatureViewComponent { if (!id) { return of(null) } - return this.sapi.getFeaturePlot( - id, - { - template: darktheme ? 'plotly_dark' : 'plotly_white', - ...additionalParams - } - ).pipe( - catchError(() => of(null)) + return concat( + of(null), + this.sapi.getFeaturePlot( + id, + { + template: darktheme ? 'plotly_dark' : 'plotly_white', + ...additionalParams + } + ).pipe( + catchError(() => of(null)) + ) ) }), shareReplay(1), @@ -194,9 +212,18 @@ export class FeatureViewComponent { constructor( private sapi: SAPI, private store: Store, - private snackbar: MatSnackBar, - @Inject(DARKTHEME) public darktheme$: Observable, + private readonly cmpStore: ComponentStore, + @Inject(DARKTHEME) public darktheme$: Observable, + @Inject(FEATURE_CONCEPT_TOKEN) private featConcept: FeatureConcept, ) { + this.cmpStore.setState({ selectedCmpFeature: null }) + + this.#feature$.pipe( + takeUntil(this.destroyed$), + filter(isSimpleCompoundFeature), + ).subscribe(selectedCmpFeature => { + this.cmpStore.patchState({ selectedCmpFeature }) + }) } navigateToRegionByName(regionName: string){ @@ -212,6 +239,21 @@ export class FeatureViewComponent { onAction(action: Action){ this.store.dispatch(action) } + + #etheralView$ = combineLatest([ + this.cmpStore.state$, + this.#feature$, + this.featConcept.concept$ + ]).pipe( + map(([ { selectedCmpFeature }, feature, selectedConcept ]) => { + const { id: selectedConceptFeatId, concept } = selectedConcept + const prevCmpFeat: SimpleCompoundFeature = selectedCmpFeature?.indices.some(idx => idx.id === feature?.id) && selectedCmpFeature || null + return { + prevCmpFeat, + concept: selectedConceptFeatId === feature.id && concept || null + } + }) + ) #specialView$ = combineLatest([ concat( @@ -225,11 +267,11 @@ export class FeatureViewComponent { this.#compoundFeatEmts$, this.store.pipe( select(atlasSelection.selectors.selectedTemplate) - ) + ), ]).pipe( map(([ voi, plotly, cmpFeatElmts, selectedTemplate ]) => { return { - voi, plotly, cmpFeatElmts, selectedTemplate + voi, plotly, cmpFeatElmts, selectedTemplate, } }) ) @@ -266,28 +308,30 @@ export class FeatureViewComponent { view$ = combineLatest([ this.#baseView$, - this.#specialView$ + this.#specialView$, + this.#etheralView$ ]).pipe( - map(([obj1, obj2]) => { + map(([obj1, obj2, obj3]) => { return { ...obj1, ...obj2, + ...obj3, } }) ) - async showSubfeature(id: string){ - try { - this.busy$.next(true) - const feature = await this.sapi.getV3FeatureDetailWithId(id).toPromise() - this.store.dispatch( - userInteraction.actions.showFeature({ feature }) - ) - } catch (e) { - console.log('error', e) - this.snackbar.open(`Error: ${e.toString()}`, "Dismiss") - } finally { - this.busy$.next(false) - } + showSubfeature(item: CFIndex|Feature){ + this.store.dispatch( + userInteraction.actions.showFeature({ + feature: item + }) + ) + } + + clearSelectedFeature(): void{ + this.store.dispatch( + userInteraction.actions.clearShownFeature() + ) } + } diff --git a/src/features/module.ts b/src/features/module.ts index c168d8929..e23945532 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -5,7 +5,6 @@ import { UtilModule } from "src/util"; import { EntryComponent } from './entry/entry.component' import { FeatureNamePipe } from "./featureName.pipe"; import { CategoryAccDirective } from './category-acc.directive'; -import { CompoundFeatureModule } from "./compoundFtContainer"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { MarkdownModule } from "src/components/markdown"; import { FeatureViewComponent } from "./feature-view/feature-view.component"; @@ -21,25 +20,30 @@ import { PlotlyComponent } from "./plotly"; import { AngularMaterialModule } from "src/sharedModules"; import { AtlasColorMapIntents } from "./atlas-colormap-intents"; import { CompoundFeatureIndicesModule } from "./compoundFeatureIndices" +import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from "./util"; +import { BehaviorSubject } from "rxjs"; +import { TPBRViewCmp } from "./TPBRView/TPBRView.component"; +import { DialogModule } from "src/ui/dialogInfo"; @NgModule({ imports: [ CommonModule, SpinnerModule, UtilModule, - CompoundFeatureModule, ScrollingModule, MarkdownModule, NgLayerCtlModule, ReadmoreModule, AngularMaterialModule, CompoundFeatureIndicesModule, + DialogModule, /** * standalone components */ PlotlyComponent, AtlasColorMapIntents, + TPBRViewCmp, ], declarations: [ EntryComponent, @@ -61,6 +65,19 @@ import { CompoundFeatureIndicesModule } from "./compoundFeatureIndices" VoiBboxDirective, ListDirective, ], + providers: [ + { + provide: FEATURE_CONCEPT_TOKEN, + useFactory: () => { + const obs = new BehaviorSubject<{ id: string, concept: TPRB}>({id: null, concept: {}}) + const returnObj: FeatureConcept = { + register: (id, concept) => obs.next({ id, concept }), + concept$: obs.asObservable() + } + return returnObj + } + } + ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, ] diff --git a/src/features/util.ts b/src/features/util.ts new file mode 100644 index 000000000..928661e75 --- /dev/null +++ b/src/features/util.ts @@ -0,0 +1,19 @@ +import { InjectionToken } from "@angular/core"; +import { Observable } from "rxjs"; +import { SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; + +export type BBox = [[number, number, number], [number, number, number]] + +export type TPRB = { + template?: SxplrTemplate + parcellation?: SxplrParcellation + region?: SxplrRegion + bbox?: BBox +} + +export type FeatureConcept = { + register: (id: string, concept: TPRB) => void + concept$: Observable<{ id: string, concept: TPRB }> +} + +export const FEATURE_CONCEPT_TOKEN = new InjectionToken("FEATURE_CONCEPT_TOKEN") diff --git a/src/util/constants.ts b/src/util/constants.ts index b1411dc43..e3974a544 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -21,7 +21,7 @@ export const MIN_REQ_EXPLAINER = ` export const APPEND_SCRIPT_TOKEN: InjectionToken<(url: string) => Promise> = new InjectionToken(`APPEND_SCRIPT_TOKEN`) export const appendScriptFactory = (document: Document, defer: boolean = false) => { - return src => new Promise((rs, rj) => { + return (src: string) => new Promise((rs, rj) => { const scriptEl = document.createElement('script') if (defer) { scriptEl.defer = true diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 0d2d81925..cdfcf7f43 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -43,6 +43,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; import { ViewerWrapper } from "./viewerWrapper/viewerWrapper.component"; import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; +import { TPBRViewCmp } from "src/features/TPBRView/TPBRView.component"; @NgModule({ imports: [ @@ -75,6 +76,7 @@ import { MediaQueryDirective } from "src/util/directives/mediaQuery.directive"; MediaQueryDirective, FloatingMouseContextualContainerDirective, ExperimentalFlagDirective, + TPBRViewCmp, ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index 55094ebf6..ac84f5c6e 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -493,12 +493,6 @@ export class ViewerCmp { ) } - clearSelectedFeature(): void{ - this.store$.dispatch( - userInteraction.actions.clearShownFeature() - ) - } - navigateTo(position: number[]): void { this.store$.dispatch( atlasSelection.actions.navigateTo({ diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index dea9c8548..b837c843a 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -829,23 +829,7 @@ - - -
- - -
- +
@@ -892,16 +876,14 @@ Anchored to current view -
- {{ view.selectedTemplate.name }} -
+ -
- from {{ bbox[0] | numbers | addUnitAndJoin : '' }} -
-
- to {{ bbox[1] | numbers | addUnitAndJoin : '' }} -
+ + +
From 4aa3a8250fdb076b64c1f8cfdb962e6610681d74 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Thu, 18 Apr 2024 14:39:18 +0200 Subject: [PATCH 36/48] feat: added code snippet --- .helm/adhoc/certificate-sxplr-ebrains.yml | 2 +- .helm/adhoc/ingress-main.yml | 29 +++++- docs/releases/v2.14.5.md | 1 + .../sapi/codeSnippets/codeSnippet.dialog.ts | 31 +++++++ .../codeSnippets/codeSnippet.directive.ts | 88 +++++++++++++++++++ .../sapi/codeSnippets/codeSnippet.style.scss | 20 +++++ .../codeSnippets/codeSnippet.template.html | 31 +++++++ src/atlasComponents/sapi/sxplrTypes.ts | 1 + src/atlasComponents/sapi/translateV3.ts | 1 + .../sapiViews/core/region/module.ts | 2 + .../region/rich/region.rich.template.html | 26 ++++++ .../userAnnotations/tools/module.ts | 5 +- .../textareaCopyExport.component.ts | 17 +++- .../textareaCopyExport.style.css | 0 .../textareaCopyExport.template.html | 2 + .../feature-view/feature-view.component.html | 24 +++++ .../feature-view/feature-view.component.ts | 28 ++++-- src/features/guards.ts | 2 +- src/features/module.ts | 2 + src/sharedModules/angularMaterial.exports.ts | 3 +- src/util/priority.ts | 33 +++++-- 21 files changed, 324 insertions(+), 24 deletions(-) create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html rename src/{atlasComponents/userAnnotations/tools => components}/textareaCopyExport/textareaCopyExport.component.ts (71%) rename src/{atlasComponents/userAnnotations/tools => components}/textareaCopyExport/textareaCopyExport.style.css (100%) rename src/{atlasComponents/userAnnotations/tools => components}/textareaCopyExport/textareaCopyExport.template.html (94%) diff --git a/.helm/adhoc/certificate-sxplr-ebrains.yml b/.helm/adhoc/certificate-sxplr-ebrains.yml index 19a96659e..6e73e4c5e 100644 --- a/.helm/adhoc/certificate-sxplr-ebrains.yml +++ b/.helm/adhoc/certificate-sxplr-ebrains.yml @@ -1,7 +1,7 @@ apiVersion: cert-manager.io/v1 kind: Certificate metadata: - name: siibra-explorer-certificate + name: siibra-explorer-ebrains-certificate spec: secretName: sxplr-ebrains-secret renewBefore: 120h diff --git a/.helm/adhoc/ingress-main.yml b/.helm/adhoc/ingress-main.yml index 3c69b3b6a..230fcf5aa 100644 --- a/.helm/adhoc/ingress-main.yml +++ b/.helm/adhoc/ingress-main.yml @@ -13,7 +13,31 @@ spec: path: "/viewer" backend: service: - name: master-siibra-explorer + name: prod-siibra-explorer + port: + number: 8080 + - pathType: Prefix + path: "/viewer-staging" + backend: + service: + name: rc-siibra-explorer + port: + number: 8080 + - pathType: Prefix + path: "/viewer-expmt" + backend: + service: + name: expmt-siibra-explorer + port: + number: 8080 + - host: siibra-explorer.apps.ebrains.eu + http: + paths: + - pathType: Prefix + path: "/viewer" + backend: + service: + name: prod-siibra-explorer port: number: 8080 - pathType: Prefix @@ -34,3 +58,6 @@ spec: - secretName: siibra-explorer-prod-secret hosts: - siibra-explorer.apps.tc.humanbrainproject.eu + - secretName: sxplr-ebrains-secret + hosts: + - siibra-explorer.apps.ebrains.eu diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index ec6e17c76..243ce4cf3 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -10,6 +10,7 @@ - Added legend to region hierarchy - Allow latest queried concept in feature view - Allow experimental flag to be set to be on at runtime (this also shows the button, allow toggling of experimental features) +- Added code snippet to limited panels - (experimental) allow addition of custom linear coordinate space - (experimental) show BigBrain slice number diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts b/src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts new file mode 100644 index 000000000..63c4e5c2f --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts @@ -0,0 +1,31 @@ +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; +import { TextareaCopyExportCmp } from "src/components/textareaCopyExport/textareaCopyExport.component"; +import { AngularMaterialModule, Clipboard, MAT_DIALOG_DATA } from "src/sharedModules"; + +@Component({ + templateUrl: './codeSnippet.template.html', + standalone: true, + styleUrls: [ + './codeSnippet.style.scss' + ], + imports: [ + TextareaCopyExportCmp, + AngularMaterialModule, + CommonModule, + ] +}) + +export class CodeSnippetCmp { + constructor( + @Inject(MAT_DIALOG_DATA) + public data: any, + public clipboard: Clipboard, + ){ + + } + + copy(){ + this.clipboard.copy(this.data.code) + } +} diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts b/src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts new file mode 100644 index 000000000..1967c3cdc --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts @@ -0,0 +1,88 @@ +import { Directive, HostListener, Input } from "@angular/core"; +import { RouteParam, SapiRoute } from "../typeV3"; +import { SAPI } from "../sapi.service"; +import { BehaviorSubject, from, of } from "rxjs"; +import { switchMap, take } from "rxjs/operators"; +import { MatDialog } from "src/sharedModules" +import { CodeSnippetCmp } from "./codeSnippet.dialog"; + +type V = {route: T, param: RouteParam} + +@Directive({ + selector: '[code-snippet]', + standalone: true, + exportAs: "codeSnippet" +}) + +export class CodeSnippet{ + + code$ = this.sapi.sapiEndpoint$.pipe( + switchMap(endpt => this.#path.pipe( + switchMap(path => { + if (!path) { + return of(null) + } + return from(this.#getCode(`${endpt}${path}`)) + }) + )), + ) + + #busy$ = new BehaviorSubject(false) + busy$ = this.#busy$.asObservable() + + @HostListener("click") + async handleClick(){ + this.#busy$.next(true) + const code = await this.code$.pipe( + take(1) + ).toPromise() + this.#busy$.next(false) + this.matDialog.open(CodeSnippetCmp, { + data: { code } + }) + } + + @Input() + set routeParam(value: V|null|undefined){ + if (!value) { + return + } + const { param, route } = value + const { params, path } = this.sapi.v3GetRoute(route, param) + + let url = encodeURI(path) + const queryParam = new URLSearchParams() + for (const key in params) { + queryParam.set(key, params[key].toString()) + } + const result = `${url}?${queryParam.toString()}` + this.#path.next(result) + } + + @Input() + set path(value: string) { + this.#path.next(value) + } + #path = new BehaviorSubject(null) + + constructor(private sapi: SAPI, private matDialog: MatDialog){} + + async #getCode(url: string): Promise { + try { + const resp = await fetch(url, { + headers: { + Accept: `text/x-sapi-python` + } + }) + if (!resp.ok){ + console.warn(`${url} returned not ok`) + return null + } + const result = await resp.text() + return result + } catch (e) { + console.warn(`Error: ${e}`) + return null + } + } +} diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss b/src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss new file mode 100644 index 000000000..2a159cdfc --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss @@ -0,0 +1,20 @@ +.textarea +{ + width: 75vw; + +} + +textarea-copy-export +{ + display: block; + width: 75vw; + + ::ng-deep mat-form-field { + width: 100%; + + textarea + { + resize: none; + } + } +} diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html b/src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html new file mode 100644 index 000000000..0dbf045b3 --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html @@ -0,0 +1,31 @@ + + + + Code snippet + + + + + + + + + + + + + diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 11d685807..331767f01 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -103,6 +103,7 @@ export type SimpleCompoundFeature = { name: string category?: string indices: { + category?: string id: string index: T name: string diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index a2abb1375..03659664a 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -649,6 +649,7 @@ class TranslateV3 { id, index: await this.#transformIndex(index), name, + category: feat.category }) ) ), diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index e72f1b154..8bb8f8bf7 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -15,6 +15,7 @@ import { SapiViewsCoreParcellationModule } from "../parcellation"; import { TranslateQualificationPipe } from "./translateQualification.pipe"; import { DedupRelatedRegionPipe } from "./dedupRelatedRegion.pipe"; import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; +import { CodeSnippet } from "src/atlasComponents/sapi/codeSnippets/codeSnippet.directive"; @NgModule({ imports: [ @@ -30,6 +31,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di SapiViewsCoreParcellationModule, ExperimentalFlagDirective, + CodeSnippet, ], 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 25a7643b6..33e467679 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 @@ -37,6 +37,32 @@ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ +
+
+ + + + diff --git a/src/features/connectivity/index.ts b/src/features/connectivity/index.ts new file mode 100644 index 000000000..b86944877 --- /dev/null +++ b/src/features/connectivity/index.ts @@ -0,0 +1,2 @@ +export { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; +export { SapiViewsFeatureConnectivityModule } from "./module"; diff --git a/src/features/connectivity/module.ts b/src/features/connectivity/module.ts new file mode 100644 index 000000000..7ae3e1ae4 --- /dev/null +++ b/src/features/connectivity/module.ts @@ -0,0 +1,33 @@ +import { CommonModule } from "@angular/common"; +import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from "@angular/core"; +import { SAPI } from "src/atlasComponents/sapi"; +import { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; +import { ReactiveFormsModule } from "@angular/forms"; +import { DialogModule } from "src/ui/dialogInfo"; +import { UtilModule } from "src/util"; +import { AngularMaterialModule } from "src/sharedModules"; + + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + AngularMaterialModule, + DialogModule, + UtilModule, + ], + declarations: [ + ConnectivityBrowserComponent, + ], + exports: [ + ConnectivityBrowserComponent, + ], + providers: [ + SAPI, + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA, + ], +}) + +export class SapiViewsFeatureConnectivityModule{} diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts index f076c6482..6b9542044 100644 --- a/src/features/entry/entry.component.ts +++ b/src/features/entry/entry.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, Inject, QueryList, TemplateRef, ViewChildren, inject } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { debounceTime, distinctUntilChanged, map, scan, shareReplay, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators'; -import { SAPI } from 'src/atlasComponents/sapi'; +import { IDS, SAPI } from 'src/atlasComponents/sapi'; import { Feature } from 'src/atlasComponents/sapi/sxplrTypes'; import { FeatureBase } from '../base'; import * as userInteraction from "src/state/userInteraction" @@ -12,6 +12,8 @@ import { TranslatedFeature } from '../list/list.directive'; import { MatDialog } from 'src/sharedModules/angularMaterial.exports'; import { DestroyDirective } from 'src/util/directives/destroy.directive'; import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from '../util'; +import { SPECIES_ENUM } from 'src/util/constants'; +import { atlasSelection } from 'src/state'; const categoryAcc = >(categories: T[]) => { const returnVal: Record = {} @@ -26,6 +28,32 @@ const categoryAcc = >(categories: T[]) => { } return returnVal } +type ConnectiivtyFilter = { + SPECIES: string[] + PARCELLATION: string[] + SPACE: string[] +} + +const WHITELIST_CONNECTIVITY: ConnectiivtyFilter = { + SPECIES: [ + SPECIES_ENUM.RATTUS_NORVEGICUS, + SPECIES_ENUM.HOMO_SAPIENS + ], + PARCELLATION: [ + IDS.PARCELLATION.JBA29, + IDS.PARCELLATION.JBA30, + IDS.PARCELLATION.WAXHOLMV4 + ], + SPACE: [], +} + +const BANLIST_CONNECTIVITY: ConnectiivtyFilter = { + SPECIES: [], + PARCELLATION: [], + SPACE: [ + IDS.TEMPLATES.BIG_BRAIN + ] +} @Component({ selector: 'sxplr-feature-entry', @@ -160,6 +188,25 @@ export class EntryComponent extends FeatureBase implements AfterViewInit { }) } + public selectedAtlas$ = this.store.pipe( + select(atlasSelection.selectors.selectedAtlas) + ) + + public showConnectivity$ = combineLatest([ + this.selectedAtlas$.pipe( + map(atlas => WHITELIST_CONNECTIVITY.SPECIES.includes(atlas?.species) && !BANLIST_CONNECTIVITY.SPECIES.includes(atlas?.species)) + ), + this.TPRBbox$.pipe( + map(({ parcellation, template }) => ( + WHITELIST_CONNECTIVITY.SPACE.includes(template?.id) && !BANLIST_CONNECTIVITY.SPACE.includes(template?.id) + ) || ( + WHITELIST_CONNECTIVITY.PARCELLATION.includes(parcellation?.id) && !BANLIST_CONNECTIVITY.PARCELLATION.includes(parcellation?.id) + )) + ) + ]).pipe( + map(flags => flags.every(f => f)) + ) + private featureTypes$ = this.sapi.v3Get("/feature/_types", {}).pipe( switchMap(resp => this.sapi.iteratePages( diff --git a/src/features/entry/entry.flattened.component.html b/src/features/entry/entry.flattened.component.html index 9582c934c..e06d804e0 100644 --- a/src/features/entry/entry.flattened.component.html +++ b/src/features/entry/entry.flattened.component.html @@ -1,7 +1,7 @@ - + + + + + + + {{ conn.key }} + + + + + + + + + + + - - + --> diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index bbcbfb28b..5897f9ebe 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -200,27 +200,27 @@ export class FeatureViewComponent { )) ) - intents$ = this.#isConnectivity$.pipe( - withLatestFrom(this.#featureId, this.#selectedRegion$), - switchMap(([flag, fid, selectedRegion]) => { - if (!flag) { - return EMPTY - } - return this.sapi.getFeatureIntents(fid, { - region: selectedRegion.map(r => r.name).join(" ") - }).pipe( - switchMap(val => - this.sapi.iteratePages( - val, - page => this.sapi.getFeatureIntents(fid, { - region: selectedRegion.map(r => r.name).join(" "), - page: page.toString() - } - ) - )) - ) - }) - ) + // intents$ = this.#isConnectivity$.pipe( + // withLatestFrom(this.#featureId, this.#selectedRegion$), + // switchMap(([flag, fid, selectedRegion]) => { + // if (!flag) { + // return EMPTY + // } + // return this.sapi.getFeatureIntents(fid, { + // region: selectedRegion.map(r => r.name).join(" ") + // }).pipe( + // switchMap(val => + // this.sapi.iteratePages( + // val, + // page => this.sapi.getFeatureIntents(fid, { + // region: selectedRegion.map(r => r.name).join(" "), + // page: page.toString() + // } + // ) + // )) + // ) + // }) + // ) constructor( private sapi: SAPI, diff --git a/src/features/module.ts b/src/features/module.ts index cfd719d63..9582f6fcc 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -5,6 +5,7 @@ import { UtilModule } from "src/util"; import { EntryComponent } from './entry/entry.component' import { FeatureNamePipe } from "./featureName.pipe"; import { CategoryAccDirective } from './category-acc.directive'; +import { SapiViewsFeatureConnectivityModule } from "./connectivity"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { MarkdownModule } from "src/components/markdown"; import { FeatureViewComponent } from "./feature-view/feature-view.component"; @@ -18,7 +19,7 @@ import { ReadmoreModule } from "src/components/readmore"; import { GroupFeatureTallyPipe } from "./grpFeatToTotal.pipe"; import { PlotlyComponent } from "./plotly"; import { AngularMaterialModule } from "src/sharedModules"; -import { AtlasColorMapIntents } from "./atlas-colormap-intents"; +// import { AtlasColorMapIntents } from "./atlas-colormap-intents"; import { CompoundFeatureIndicesModule } from "./compoundFeatureIndices" import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from "./util"; import { BehaviorSubject } from "rxjs"; @@ -32,6 +33,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di CommonModule, SpinnerModule, UtilModule, + SapiViewsFeatureConnectivityModule, ScrollingModule, MarkdownModule, NgLayerCtlModule, @@ -44,7 +46,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di * standalone components */ PlotlyComponent, - AtlasColorMapIntents, + // AtlasColorMapIntents, TPBRViewCmp, CodeSnippet, ExperimentalFlagDirective, diff --git a/src/features/pointcloud-intents/index.ts b/src/features/pointcloud-intents/index.ts index 5e0e6b170..4f66186c5 100644 --- a/src/features/pointcloud-intents/index.ts +++ b/src/features/pointcloud-intents/index.ts @@ -1,2 +1,2 @@ export { isPoint, FilterPointTransformer, CFIndex } from "./util" -export { PointCloudIntents } from "./intents.component" +// export { PointCloudIntents } from "./intents.component" diff --git a/src/features/pointcloud-intents/intents.component.ts b/src/features/pointcloud-intents/intents.component.ts index 1f9514d9f..eab9ad9ef 100644 --- a/src/features/pointcloud-intents/intents.component.ts +++ b/src/features/pointcloud-intents/intents.component.ts @@ -1,175 +1,175 @@ -import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Inject, InjectionToken, Input, Optional, Output, inject } from "@angular/core"; -import { BehaviorSubject, Observable, combineLatest } from "rxjs"; -import { Point, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; -import { PathReturn } from "src/atlasComponents/sapi/typeV3"; -import { AngularMaterialModule } from "src/sharedModules"; -import { DestroyDirective } from "src/util/directives/destroy.directive"; -import { CFIndex } from "./util"; -import { AnnotationLayer } from "src/atlasComponents/annotations"; -import { map, takeUntil, withLatestFrom } from "rxjs/operators"; -import { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor, HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; - -type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] - -type Annotation = { - id: string - type: 'point' - point: [number, number, number] -} - -function serializeToId(pt: Point): Annotation{ - return { - id: `${pt.spaceId}-${pt.loc.join("-")}`, - type: 'point', - point: pt.loc.map(v => v*1e6) as [number, number, number] - } -} - -@Component({ - selector: 'pointcloud-intents', - templateUrl: './intents.template.html', - styleUrls: [ - './intents.style.css' - ], - standalone: true, - imports: [ - CommonModule, - AngularMaterialModule - ], - hostDirectives: [ - DestroyDirective - ] -}) - -export class PointCloudIntents { - - readonly #destroy$ = inject(DestroyDirective).destroyed$ - - // not yet used - #intents: Observable - - #points$ = new BehaviorSubject[]>([] as CFIndex[]) - #selectedTemplate$ = new BehaviorSubject(null) - - @Input('points') - set points(val: CFIndex[]) { - this.#points$.next(val) - } - - @Input('selected-template') - set selectedTemplate(tmpl: SxplrTemplate){ - this.#selectedTemplate$.next(tmpl) - } - - #spaceMatchedCfIndices$ = combineLatest([ - this.#points$, - this.#selectedTemplate$ - ]).pipe( - map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id)) - ) - - #spaceMatchedAnnIdToCfIdx$ = this.#spaceMatchedCfIndices$.pipe( - map(indices => { - const idToIndexMap = new Map>() - for (const idx of indices){ - idToIndexMap.set( - serializeToId(idx.index).id, - idx - ) - } - return idToIndexMap - }) - ) - - @Output('point-clicked') - pointClicked = new EventEmitter>() - - annLayer: AnnotationLayer - constructor( - @Inject(RENDER_CF_POINT) render: RenderCfPoint, - @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, - @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) hoverInterceptor: HoverInterceptor, - ){ - this.annLayer = new AnnotationLayer("intents", "#ff0000") - this.#spaceMatchedCfIndices$.pipe( - takeUntil(this.#destroy$) - ).subscribe(indices => { - const anns = indices.map(idx => serializeToId(idx.index)) - this.annLayer.addAnnotation(anns) - }, - e => { - console.error("error", e) - }, - () => { - this.annLayer.dispose() - }) - - this.annLayer.onHover.pipe( - takeUntil(this.#destroy$), - withLatestFrom(this.#spaceMatchedAnnIdToCfIdx$), - ).subscribe(([hover, map]) => { - - if (hoverInterceptor && !!this.#hoveredMessage){ - const { remove } = hoverInterceptor - remove(this.#hoveredMessage) - this.#hoveredMessage = null - } - - this.#hoveredCfIndex = null - - if (!hover) { - return - } - - const idx = map.get(hover.id) - if (!idx) { - console.error(`Couldn't find AnnId: ${hover.id}`) - return - } - - this.#hoveredCfIndex = idx - - if (hoverInterceptor) { - const { append } = hoverInterceptor - const text = render(idx) - this.#hoveredMessage = { - message: `Hovering ${text}` - } - append(this.#hoveredMessage) - } - }) - - this.#destroy$.subscribe(() => { - if (hoverInterceptor) { - const { remove } = hoverInterceptor - if (this.#hoveredMessage) { - remove(this.#hoveredMessage) - this.#hoveredMessage = null - } - } - }) - - if (clickInterceptor) { - const { register, deregister } = clickInterceptor - const onClickHandler = this.onViewerClick.bind(this) - register(onClickHandler) - this.#destroy$.subscribe(() => deregister(onClickHandler)) - } - } - - onViewerClick(){ - if (this.#hoveredCfIndex) { - this.pointClicked.next(this.#hoveredCfIndex) - return false - } - return true - } - - #hoveredCfIndex: CFIndex = null - #hoveredMessage: THoverConfig = null - -} - -export const RENDER_CF_POINT = new InjectionToken("RENDER_CF_POINT") -export type RenderCfPoint = (cfIndex: CFIndex) => string +// import { CommonModule } from "@angular/common"; +// import { Component, EventEmitter, Inject, InjectionToken, Input, Optional, Output, inject } from "@angular/core"; +// import { BehaviorSubject, Observable, combineLatest } from "rxjs"; +// import { Point, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; +// import { PathReturn } from "src/atlasComponents/sapi/typeV3"; +// import { AngularMaterialModule } from "src/sharedModules"; +// import { DestroyDirective } from "src/util/directives/destroy.directive"; +// import { CFIndex } from "./util"; +// import { AnnotationLayer } from "src/atlasComponents/annotations"; +// import { map, takeUntil, withLatestFrom } from "rxjs/operators"; +// import { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor, HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; + +// type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] + +// type Annotation = { +// id: string +// type: 'point' +// point: [number, number, number] +// } + +// function serializeToId(pt: Point): Annotation{ +// return { +// id: `${pt.spaceId}-${pt.loc.join("-")}`, +// type: 'point', +// point: pt.loc.map(v => v*1e6) as [number, number, number] +// } +// } + +// @Component({ +// selector: 'pointcloud-intents', +// templateUrl: './intents.template.html', +// styleUrls: [ +// './intents.style.css' +// ], +// standalone: true, +// imports: [ +// CommonModule, +// AngularMaterialModule +// ], +// hostDirectives: [ +// DestroyDirective +// ] +// }) + +// export class PointCloudIntents { + +// readonly #destroy$ = inject(DestroyDirective).destroyed$ + +// // not yet used +// #intents: Observable + +// #points$ = new BehaviorSubject[]>([] as CFIndex[]) +// #selectedTemplate$ = new BehaviorSubject(null) + +// @Input('points') +// set points(val: CFIndex[]) { +// this.#points$.next(val) +// } + +// @Input('selected-template') +// set selectedTemplate(tmpl: SxplrTemplate){ +// this.#selectedTemplate$.next(tmpl) +// } + +// #spaceMatchedCfIndices$ = combineLatest([ +// this.#points$, +// this.#selectedTemplate$ +// ]).pipe( +// map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id)) +// ) + +// #spaceMatchedAnnIdToCfIdx$ = this.#spaceMatchedCfIndices$.pipe( +// map(indices => { +// const idToIndexMap = new Map>() +// for (const idx of indices){ +// idToIndexMap.set( +// serializeToId(idx.index).id, +// idx +// ) +// } +// return idToIndexMap +// }) +// ) + +// @Output('point-clicked') +// pointClicked = new EventEmitter>() + +// annLayer: AnnotationLayer +// constructor( +// @Inject(RENDER_CF_POINT) render: RenderCfPoint, +// @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, +// @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) hoverInterceptor: HoverInterceptor, +// ){ +// this.annLayer = new AnnotationLayer("intents", "#ff0000") +// this.#spaceMatchedCfIndices$.pipe( +// takeUntil(this.#destroy$) +// ).subscribe(indices => { +// const anns = indices.map(idx => serializeToId(idx.index)) +// this.annLayer.addAnnotation(anns) +// }, +// e => { +// console.error("error", e) +// }, +// () => { +// this.annLayer.dispose() +// }) + +// this.annLayer.onHover.pipe( +// takeUntil(this.#destroy$), +// withLatestFrom(this.#spaceMatchedAnnIdToCfIdx$), +// ).subscribe(([hover, map]) => { + +// if (hoverInterceptor && !!this.#hoveredMessage){ +// const { remove } = hoverInterceptor +// remove(this.#hoveredMessage) +// this.#hoveredMessage = null +// } + +// this.#hoveredCfIndex = null + +// if (!hover) { +// return +// } + +// const idx = map.get(hover.id) +// if (!idx) { +// console.error(`Couldn't find AnnId: ${hover.id}`) +// return +// } + +// this.#hoveredCfIndex = idx + +// if (hoverInterceptor) { +// const { append } = hoverInterceptor +// const text = render(idx) +// this.#hoveredMessage = { +// message: `Hovering ${text}` +// } +// append(this.#hoveredMessage) +// } +// }) + +// this.#destroy$.subscribe(() => { +// if (hoverInterceptor) { +// const { remove } = hoverInterceptor +// if (this.#hoveredMessage) { +// remove(this.#hoveredMessage) +// this.#hoveredMessage = null +// } +// } +// }) + +// if (clickInterceptor) { +// const { register, deregister } = clickInterceptor +// const onClickHandler = this.onViewerClick.bind(this) +// register(onClickHandler) +// this.#destroy$.subscribe(() => deregister(onClickHandler)) +// } +// } + +// onViewerClick(){ +// if (this.#hoveredCfIndex) { +// this.pointClicked.next(this.#hoveredCfIndex) +// return false +// } +// return true +// } + +// #hoveredCfIndex: CFIndex = null +// #hoveredMessage: THoverConfig = null + +// } + +// export const RENDER_CF_POINT = new InjectionToken("RENDER_CF_POINT") +// export type RenderCfPoint = (cfIndex: CFIndex) => string diff --git a/src/index.html b/src/index.html index 8158adec9..95d9da0bf 100644 --- a/src/index.html +++ b/src/index.html @@ -12,8 +12,9 @@ - + + Siibra Explorer diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index 74d86122d..68822f1b2 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -725,8 +725,8 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit const { evDetail: detail, latMeshRecord, latLblIdxRecord, latLblIdxReg, meshVisibility } = arg const evMesh = detail.mesh && { faceIndex: detail.mesh.faceIndex, - // typo in three-surfer - verticesIndicies: detail.mesh.verticesIdicies + verticesIndicies: detail.mesh.verticesIndicies, + vertexIndex: detail.mesh.vertexIndex, } const custEv: THandlingCustomEv = { regions: [], @@ -739,9 +739,9 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit const { geometry: evGeometry, - // typo in three-surfer - verticesIdicies: evVerticesIndicies, - } = detail.mesh as { geometry: TThreeGeometry, verticesIdicies: number[] } + verticesIndicies: evVerticesIndicies, + vertexIndex + } = detail.mesh as { geometry: TThreeGeometry, verticesIndicies: number[], vertexIndex: number } for (const laterality in latMeshRecord) { const meshRecord = latMeshRecord[laterality] @@ -770,14 +770,22 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit * translate vertex indices to label indicies via set, to remove duplicates */ const labelIndexSet = new Set() - for (const idx of evVerticesIndicies){ - const labelOfInterest = labelIndexRecord.vertexIndices[idx] - if (!labelOfInterest) { - continue - } - labelIndexSet.add(labelOfInterest) + if (labelIndexRecord.vertexIndices[vertexIndex]) { + labelIndexSet.add(labelIndexRecord.vertexIndices[vertexIndex]) } + /** + * old implementation (perhaps less CPU intensive) + * gets all vertices and label them + */ + // for (const idx of evVerticesIndicies){ + // const labelOfInterest = labelIndexRecord.vertexIndices[idx] + // if (!labelOfInterest) { + // continue + // } + // labelIndexSet.add(labelOfInterest) + // } + /** * decode label index to region */