diff --git a/common/helpOnePager.md b/common/helpOnePager.md index 2f66d4b38b167b2f9ac0d93a93f0ca347da43c3d..be7927406de2cdd4130a98f2035c3894795c5115 100644 --- a/common/helpOnePager.md +++ b/common/helpOnePager.md @@ -4,7 +4,7 @@ | Oblique rotation | `<shift>` + `[drag]` any of the slice views | `[rotate]` any of the slice views | | Zoom | `[mousewheel]` | `[pinch zoom]` | | Zoom | `[hover]` on any slice views > `[click]` magnifier | `[tap]` on magnifier | -| Next slice | `<ctrl>` + `[mousewheel]` | - | +| Next slice | `<ctrl>` + `[mousewheel]` / `[p]`revious / `[n]`ext | - | | Next 10 slice | `<shift>` + `[mousewheel]` | - | | Toggle delineation | `[q]` | - | | Toggle cross hair | `[a]` | - | diff --git a/docs/releases/v2.14.0.md b/docs/releases/v2.14.0.md new file mode 100644 index 0000000000000000000000000000000000000000..0339fb1b060df2270fc83d5ef8cc26bead46987b --- /dev/null +++ b/docs/releases/v2.14.0.md @@ -0,0 +1,10 @@ +# v2.14.0 + +## Feature + +- added `[p]` and `[n]` as keyboard shortcut to navigate previous/next slice +- experimental support for other versions of regions + +## Behind the scenes + +- Minor refactoring diff --git a/e2e/checklist.md b/e2e/checklist.md index 8378541a0f49d69c4ca72bd7a48d9489b2a185cb..d6f3f1dcebd8778ffc9c1880616b52546ea4964d 100644 --- a/e2e/checklist.md +++ b/e2e/checklist.md @@ -9,6 +9,11 @@ - [ ] 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 diff --git a/mkdocs.yml b/mkdocs.yml index 4e782a75f743f652947a8e4899fd4243710ce17e..683054e07f3097a9c1b3cc699afc0485aff729fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.14.0: 'releases/v2.14.0.md' - v2.13.5: 'releases/v2.13.5.md' - v2.13.4: 'releases/v2.13.4.md' - v2.13.3: 'releases/v2.13.3.md' diff --git a/package.json b/package.json index a8d4d91d281340a14821537ae29b3cf76def7d3f..101d49c59e5b92380e94010ac86ee15d81cea2fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siibra-explorer", - "version": "2.13.5", + "version": "2.14.0", "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/core/index.ts b/src/atlasComponents/sapi/core/index.ts index ed68003d55ce542a35f5e173aaa173345202d597..f008a6194a10af74a8e653a18c3afde6fab48c4d 100644 --- a/src/atlasComponents/sapi/core/index.ts +++ b/src/atlasComponents/sapi/core/index.ts @@ -1,3 +1,2 @@ export { SAPIAtlas } from "./sapiAtlas" export { SAPIParcellation } from "./sapiParcellation" -export { SAPIRegion } from "./sapiRegion" diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts deleted file mode 100644 index 5c9db2f9fa82eed38c7c85afaebb57653bab0cee..0000000000000000000000000000000000000000 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { SAPI } from ".."; -import { SapiRegionModel, RouteParam } from "../typeV3"; -import { Observable, of } from "rxjs"; -import { map } from "rxjs/operators"; -import { SAPIBase } from "./base"; -import { SxplrRegion } from "../sxplrTypes"; - -/** - * All valid region features - */ -const RegionFeatures = { - // ReceptorDensityFingerprint: "ReceptorDensityFingerprint", - // GeneExpressions: "GeneExpressions", - EbrainsDataFeature: "EbrainsDataFeature", - - // ReceptorDensityProfile: "ReceptorDensityProfile", - // BigBrainIntensityProfile: "BigBrainIntensityProfile", - // CellDensityProfile: "CellDensityProfile", - // LayerwiseBigBrainIntensities: "LayerwiseBigBrainIntensities", - // LayerwiseCellDensity: "LayerwiseCellDensity", - - Tabular: "Tabular", - CorticalProfile: "CorticalProfile", -} as const - -type RF = keyof typeof RegionFeatures - -export class SAPIRegion extends SAPIBase<RF>{ - - static GetDisplayColor(region: SxplrRegion): [number, number, number]{ - if (!region) { - throw new Error(`region must be provided!`) - } - return region.color - // if (region.hasAnnotation?.displayColor) { - // return hexToRgb(region.hasAnnotation.displayColor) - // } - // return strToRgb(JSON.stringify(region)) - } - - - constructor( - private sapi: SAPI, - public atlasId: string, - public parcId: string, - public id: string, - ){ - super(sapi) - } - - static Features: RF[] = Object.keys(RegionFeatures) as RF[] - static Features$ = of(SAPIRegion.Features) - public features$ = SAPIRegion.Features$ - - /** - * @param spaceId - * @returns - */ - getMapInfo(spaceId: string): Observable<{min: number, max: number}> { - return this.sapi.v3Get("/map/statistical_map.info.json", { - query: { - parcellation_id: this.parcId, - region_id: this.id, - space_id: spaceId - } - }) - } - - /** - * @param spaceId - * @returns - */ - getMapUrl(spaceId: string): Observable<string> { - return SAPI.BsEndpoint$.pipe( - map(endpoint => { - const { path, params } = this.sapi.v3GetRoute("/map/statistical_map.nii.gz", { - query: { - parcellation_id: this.parcId, - region_id: this.id, - space_id: spaceId - } - }) - - const search = new URLSearchParams() - for (const key in params) { - search.set(key, params[key].toString()) - } - - return `${endpoint}${path}?${search.toString()}` - }) - ) - } - - getDetail(spaceId: string): Observable<SapiRegionModel> { - return this.sapi.v3Get("/regions/{region_id}", { - path: { - region_id: this.id - }, - query: { - parcellation_id: this.parcId, - space_id: spaceId - } - }) - } - - getMap(spaceId: string, mapType: RouteParam<"/map">['query']['map_type']) { - return this.sapi.v3Get("/map", { - query: { - space_id: spaceId, - parcellation_id: this.parcId, - map_type: mapType, - } - }) - } -} diff --git a/src/atlasComponents/sapi/index.ts b/src/atlasComponents/sapi/index.ts index 2ec890861ec737a5a1aec4b1f6d4d899fac532c7..fc42070f901fc363822b43855d6a245c2cf1b678 100644 --- a/src/atlasComponents/sapi/index.ts +++ b/src/atlasComponents/sapi/index.ts @@ -4,7 +4,6 @@ export { SAPI } from "./sapi.service" export { SAPIAtlas, SAPIParcellation, - SAPIRegion } from "./core" export { diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index f2b2050ff889ab7d1590ab57280cb5e050831f5d..53868ab6a8ec59cc9ff63fa6bb50b78ed69b92be 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -22,7 +22,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.14' +export const EXPECTED_SIIBRA_API_VERSION = '0.3.15' let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index 7e7f00636a8c72af362e4534f8aa47a0b2689a34..53b4c245caa70ddc2171bb3f75d5e4ec9dab88f2 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}/download": { + /** + * Get Single Feature Download + * @description Get a zip archive of the downloadables from a feature. + */ + get: operations["get_single_feature_download_feature__feature_id__download_get"] + } "/atlases": { /** * Get All Atlases @@ -65,17 +72,24 @@ export interface paths { } "/regions/{region_id}/features": { /** - * Get All Regions + * Get All Features Region * @description HTTP get all features of a single region */ - get: operations["get_all_regions_regions__region_id__features_get"] + get: operations["get_all_features_region_regions__region_id__features_get"] + } + "/regions/{region_id}/related": { + /** + * Get Related Region + * @description HTTP get_related_regions of the specified region + */ + get: operations["get_related_region_regions__region_id__related_get"] } "/regions/{region_id}": { /** - * Get All Regions + * Get Single Regions * @description HTTP get a single region */ - get: operations["get_all_regions_regions__region_id__get"] + get: operations["get_single_regions_regions__region_id__get"] } "/map": { /** @@ -132,20 +146,6 @@ 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 @@ -171,17 +171,17 @@ export interface paths { } "/feature/CorticalProfile": { /** - * Get All Connectivity Features + * Get All Corticalprofile Features * @description Get all CorticalProfile features */ - get: operations["get_all_connectivity_features_feature_CorticalProfile_get"] + get: operations["get_all_corticalprofile_features_feature_CorticalProfile_get"] } "/feature/CorticalProfile/{feature_id}": { /** - * Get Single Connectivity Feature + * Get Single Corticalprofile Feature * @description Get a single CorticalProfile feature */ - get: operations["get_single_connectivity_feature_feature_CorticalProfile__feature_id__get"] + get: operations["get_single_corticalprofile_feature_feature_CorticalProfile__feature_id__get"] } "/feature/Tabular": { /** @@ -886,6 +886,19 @@ export interface components { /** Pages */ pages?: number } + /** Page[RegionRelationAsmtModel] */ + Page_RegionRelationAsmtModel_: { + /** Items */ + items: (components["schemas"]["RegionRelationAsmtModel"])[] + /** Total */ + total: number + /** Page */ + page?: number + /** Size */ + size?: number + /** Pages */ + pages?: number + } /** Page[SiibraAtlasModel] */ Page_SiibraAtlasModel_: { /** Items */ @@ -1059,6 +1072,14 @@ export interface components { * @enum {unknown} */ PlotlyTemplate: "plotly" | "plotly_white" | "plotly_dark" | "ggplot2" | "seaborn" | "simple_white" | "none" + /** + * Qualification + * @description Qualification + * + * Exactly match to Qualification in siibra.core.relation_quantification.Quantification + * @enum {string} + */ + Qualification: "EXACT" | "OVERLAPS" | "CONTAINED" | "CONTAINS" | "APPROXIMATE" | "HOMOLOGOUS" | "OTHER_VERSION" /** QuantitativeOverlapItem */ QuantitativeOverlapItem: { /** @@ -1113,6 +1134,18 @@ export interface components { /** minValueUnit */ minValueUnit?: Record<string, never> } + /** + * RegionRelationAsmtModel + * @description ConfigBaseModel + */ + RegionRelationAsmtModel: { + /** @Type */ + "@type": string + qualification: components["schemas"]["Qualification"] + query_structure: components["schemas"]["ParcellationEntityVersionModel"] + assigned_structure: components["schemas"]["ParcellationEntityVersionModel"] + assigned_structure_parcellation: components["schemas"]["SiibraParcellationModel"] + } /** RelationAssessmentItem */ RelationAssessmentItem: { /** @@ -1170,9 +1203,9 @@ export interface components { /** Qualification */ qualification: string /** Query Structure */ - query_structure: components["schemas"]["LocationModel"] | components["schemas"]["ParcellationEntityVersionModel"] + query_structure: components["schemas"]["LocationModel"] | components["schemas"]["ParcellationEntityVersionModel"] | components["schemas"]["SiibraParcellationModel"] /** Assigned Structure */ - assigned_structure: components["schemas"]["LocationModel"] | components["schemas"]["ParcellationEntityVersionModel"] + assigned_structure: components["schemas"]["LocationModel"] | components["schemas"]["ParcellationEntityVersionModel"] | components["schemas"]["SiibraParcellationModel"] /** Explanation */ explanation: string } @@ -1228,7 +1261,7 @@ export interface components { /** Id */ id: string /** Modality */ - modality: string + modality?: string /** Category */ category: string /** Description */ @@ -1258,7 +1291,7 @@ export interface components { /** Id */ id: string /** Modality */ - modality: string + modality?: string /** Category */ category: string /** Description */ @@ -1326,7 +1359,7 @@ export interface components { /** Id */ id: string /** Modality */ - modality: string + modality?: string /** Category */ category: string /** Description */ @@ -1363,7 +1396,7 @@ export interface components { /** Id */ id: string /** Modality */ - modality: string + modality?: string /** Category */ category: string /** Description */ @@ -1392,7 +1425,7 @@ export interface components { /** Id */ id: string /** Modality */ - modality: string + modality?: string /** Category */ category: string /** Description */ @@ -1414,7 +1447,7 @@ export interface components { /** Id */ id: string /** Modality */ - modality: string + modality?: string /** Category */ category: string /** Description */ @@ -1581,6 +1614,31 @@ export interface operations { } } } + get_single_feature_download_feature__feature_id__download_get: { + /** + * Get Single Feature Download + * @description Get a zip archive of the downloadables from a feature. + */ + parameters: { + path: { + feature_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record<string, never> + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } get_all_atlases_atlases_get: { /** * Get All Atlases @@ -1762,9 +1820,9 @@ export interface operations { } } } - get_all_regions_regions__region_id__features_get: { + get_all_features_region_regions__region_id__features_get: { /** - * Get All Regions + * Get All Features Region * @description HTTP get all features of a single region */ parameters: { @@ -1792,9 +1850,39 @@ export interface operations { } } } - get_all_regions_regions__region_id__get: { + get_related_region_regions__region_id__related_get: { /** - * Get All Regions + * Get Related Region + * @description HTTP get_related_regions of the specified region + */ + parameters: { + query: { + parcellation_id: string + page?: number + size?: number + } + path: { + region_id: string + } + } + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["Page_RegionRelationAsmtModel_"] + } + } + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"] + } + } + } + } + get_single_regions_regions__region_id__get: { + /** + * Get Single Regions * @description HTTP get a single region */ parameters: { @@ -1988,56 +2076,7 @@ export interface operations { space_id: string parcellation_id: string region_id?: string - } - } - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": Record<string, never> - } - } - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"] - } - } - } - } - 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<string, never> - } - } - /** @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 + feature_id?: string } } responses: { @@ -2141,9 +2180,9 @@ export interface operations { } } } - get_all_connectivity_features_feature_CorticalProfile_get: { + get_all_corticalprofile_features_feature_CorticalProfile_get: { /** - * Get All Connectivity Features + * Get All Corticalprofile Features * @description Get all CorticalProfile features */ parameters: { @@ -2170,9 +2209,9 @@ export interface operations { } } } - get_single_connectivity_feature_feature_CorticalProfile__feature_id__get: { + get_single_corticalprofile_feature_feature_CorticalProfile__feature_id__get: { /** - * Get Single Connectivity Feature + * Get Single Corticalprofile Feature * @description Get a single CorticalProfile feature */ parameters: { diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index 00a7e88ec1774c3ec0bf1164258830a2ec3b5e3b..6d08f219997b32eefa77edf5f571a3a66fb2e0b5 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -104,6 +104,13 @@ const TMP_META_REGISTRY: Record<string, MetaV1Schema> = { type: "image/3d" }, transform: [[-0.74000001,0,0,38134608],[0,-0.26530117,-0.6908077,13562314],[0,-0.6908077,0.26530117,-3964904],[0,0,0,1]] + }, + "https://neuroglancer.humanbrainproject.eu/precomputed/chenonceau_dti_rgb_200um/precomputed": { + version: 1, + data: { + type: "image/3d" + }, + transform: [[-0.2, 0.0, 0.0, 96400000.0], [0.0, -0.2, 0.0, 96400000.0], [0.0, 0.0, -0.2, 114400000.0], [0.0, 0.0, 0.0, 1.0]] } } diff --git a/src/atlasComponents/sapi/typeV3.ts b/src/atlasComponents/sapi/typeV3.ts index 13d2d393b2cfee465f4927ed21522832b70726d9..0ae097aa9c77422881e912038c155fcce189b5ff 100644 --- a/src/atlasComponents/sapi/typeV3.ts +++ b/src/atlasComponents/sapi/typeV3.ts @@ -17,9 +17,9 @@ export type SapiFeatureModel = SapiSpatialFeatureModel | PathReturn<"/feature/Ta export type SapiRoute = keyof paths -type SapiRouteExcludePlotly = Exclude<SapiRoute, "/feature/{feature_id}/plotly"> +type SapiRouteExcludePlotlyDownload = Exclude<SapiRoute, "/feature/{feature_id}/plotly" | "/feature/{feature_id}/download"> -type _FeatureType<FeatureRoute extends SapiRouteExcludePlotly> = FeatureRoute extends `/feature/${infer FT}` +type _FeatureType<FeatureRoute extends SapiRouteExcludePlotlyDownload> = FeatureRoute extends `/feature/${infer FT}` ? FT extends "_types" ? never : FT extends "{feature_id}" @@ -30,7 +30,7 @@ type _FeatureType<FeatureRoute extends SapiRouteExcludePlotly> = FeatureRoute ex : FT : never -export type FeatureType = _FeatureType<SapiRouteExcludePlotly> +export type FeatureType = _FeatureType<SapiRouteExcludePlotlyDownload> /** * Support types @@ -132,3 +132,5 @@ interface EnclosedROI { export function isEnclosed(v: BestViewPoints[number]): v is EnclosedROI { return v.type === "enclosed" } + +export type Qualification = components["schemas"]["Qualification"] diff --git a/src/atlasComponents/sapiViews/core/region/dedupRelatedRegion.pipe.ts b/src/atlasComponents/sapiViews/core/region/dedupRelatedRegion.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..f655d7031766ab49876098c0ddade38d75458e8e --- /dev/null +++ b/src/atlasComponents/sapiViews/core/region/dedupRelatedRegion.pipe.ts @@ -0,0 +1,34 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { Qualification } from "src/atlasComponents/sapi/typeV3" +import { SxplrRegion, SxplrParcellation } from "src/atlasComponents/sapi/sxplrTypes" + +type InputType = { + qualification: Qualification + region: SxplrRegion, + parcellation: SxplrParcellation +} + +@Pipe({ + name: "dedupRelatedRegionPipe", + pure: true +}) +export class DedupRelatedRegionPipe implements PipeTransform{ + public transform(arr: InputType[]): InputType[] { + if (!arr) { + return [] + } + const accumulator: InputType[] = [] + for (const item of arr) { + const exists = accumulator.find(v => ( + v.qualification === item.qualification + && item.region.name === v.region.name + && item.parcellation.id === v.parcellation.id) + ) + if (exists) { + continue + } + accumulator.push(item) + } + return accumulator + } +} diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index 908761ba5a664ab5a27f1ebeac38c53cf549b829..46b01dfb024e630fd67a352ca85c35ef0aa9f3f1 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -16,6 +16,9 @@ import { MatListModule } from "@angular/material/list"; import { DialogModule } from "src/ui/dialogInfo"; import { SapiViewsCoreParcellationModule } from "../parcellation"; import { MatTooltipModule } from "@angular/material/tooltip"; +import { TranslateQualificationPipe } from "./translateQualification.pipe"; +import { DedupRelatedRegionPipe } from "./dedupRelatedRegion.pipe"; +import { MatExpansionModule } from "@angular/material/expansion"; @NgModule({ imports: [ @@ -33,11 +36,16 @@ import { MatTooltipModule } from "@angular/material/tooltip"; DialogModule, SapiViewsCoreParcellationModule, MatTooltipModule, + MatExpansionModule, + ExperimentalModule, ], declarations: [ SapiViewsCoreRegionRegionListItem, SapiViewsCoreRegionRegionRich, SapiViewsCoreRegionRegionBase, + + TranslateQualificationPipe, + DedupRelatedRegionPipe, ], exports: [ SapiViewsCoreRegionRegionListItem, diff --git a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts index 59b09ba91bb2e0eddbac28add6ed03fdd4c0cade..fc5b5c1dfae5eeab36776b0adad11945e38f85fe 100644 --- a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts +++ b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts @@ -3,8 +3,7 @@ import { SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/a import { translateV3Entities } from "src/atlasComponents/sapi/translateV3" import { rgbToHsl } from 'common/util' import { SAPI } from "src/atlasComponents/sapi/sapi.service"; -import { BehaviorSubject, combineLatest } from "rxjs"; -import { SAPIRegion } from "src/atlasComponents/sapi/core"; +import { BehaviorSubject, combineLatest, forkJoin, of } from "rxjs"; import { map, switchMap } from "rxjs/operators"; @Directive({ @@ -95,7 +94,7 @@ export class SapiViewsCoreRegionRegionBase { /** * color */ - const rgb = SAPIRegion.GetDisplayColor(this.region) || [200, 200, 200] + const rgb = this.region?.color || [200, 200, 200] this.regionRgbString = `rgb(${rgb.join(',')})` const [ /* _h */, /* _s */, l] = rgbToHsl(...rgb) this.regionDarkmode = l < 0.4 @@ -130,6 +129,28 @@ export class SapiViewsCoreRegionRegionBase { ).toPromise() } + protected async fetchRelated(region: SxplrRegion){ + const getPage = (page: number) => this.sapi.v3Get("/regions/{region_id}/related", { + path: { + region_id: region.name + }, + query: { + parcellation_id: this.parcellation.id, + page + } + }) + return getPage(1).pipe( + switchMap(resp => this.sapi.iteratePages(resp, getPage)), + switchMap(arr => forkJoin( + arr.map(({ qualification, assigned_structure, assigned_structure_parcellation }) => forkJoin({ + qualification: of(qualification), + region: translateV3Entities.translateRegion(assigned_structure), + parcellation: translateV3Entities.translateParcellation(assigned_structure_parcellation), + })) + )) + ).toPromise() + } + constructor(protected sapi: SAPI){ } diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts index d774b1e05f61c2ebe3a4f676d6464417626b4cdd..ce52048ff4ac2fdab09a1efaa372d69faa66fdbb 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts @@ -92,4 +92,9 @@ export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase } }), ) + + public relatedRegions$ = this.ATPR$.pipe( + switchMap(({ region }) => this.fetchRelated(region)), + shareReplay(1), + ) } 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 bfa0c7cd41ecc40131b9f23a7468fc72d0a67d2f..b48e7804e752ec61a567d11c2a415a8ab9f6f62e 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 @@ -62,6 +62,62 @@ <div mat-line>{{ doi }}</div> </a> </ng-template> + + <ng-template sxplrExperimentalFlag [experimental]="true" + #relatedRegionsExport="sxplrExperimentalFlag" + [ngIf]="relatedRegionsExport.show$ | async"> + + <ng-template [ngIf]="relatedRegions$ | async | dedupRelatedRegionPipe" let-relatedRegions> + <!-- only show icon if related regions length > 0 --> + <ng-template [ngIf]="relatedRegions.length > 0"> + <button mat-list-item + [sxplr-dialog]="relatedRegionsTmpl" + sxplr-dialog-size="auto"> + <mat-icon mat-list-icon fontSet="fas" fontIcon="fa-link"></mat-icon> + <div mat-line class="overview-content">Related Regions ({{ relatedRegions.length }})</div> + </button> + </ng-template> + + <!-- dialog when user clicks related regions --> + <ng-template #relatedRegionsTmpl> + <!-- header --> + <h2 mat-dialog-title>Current region {{ region.name }} ...</h2> + + <!-- body --> + <mat-dialog-content> + + <!-- iterate over all related --> + <ng-template ngFor [ngForOf]="relatedRegions" let-related let-isLast="last"> + + <!-- related region body --> + <div class="sxplr-p-2"> + <div> + {{ related.qualification | translateQualificationPipe }} + </div> + + <div> + {{ related.region.name }} in + </div> + <div> + {{ related.parcellation.name}} + </div> + </div> + + <!-- divider --> + <ng-template [ngIf]="!isLast"> + <mat-divider></mat-divider> + </ng-template> + </ng-template> + </mat-dialog-content> + + <!-- footer --> + <mat-dialog-actions> + <button mat-button mat-dialog-close>close</button> + </mat-dialog-actions> + </ng-template> + </ng-template> + + </ng-template> </mat-action-list> @@ -94,6 +150,60 @@ #featureEntryCmp="featureEntryCmp"> </sxplr-feature-entry> </mat-tab> + + + <ng-template sxplrExperimentalFlag [experimental]="true" + #exmptRelatedFeat="sxplrExperimentalFlag" + [ngIf]="exmptRelatedFeat.show$ | async"> + + <!-- related region features --> + <ng-template [ngIf]="relatedRegions$ | async | dedupRelatedRegionPipe" let-relatedRegions> + <ng-template [ngIf]="relatedRegions.length > 0"> + + <mat-tab> + + <!-- tab label --> + <ng-template mat-tab-label> + <span> + Related Region Features + </span> + </ng-template> + + <!-- tab-content --> + <mat-accordion> + + <mat-expansion-panel *ngFor="let relatedRegion of relatedRegions" hideToggle> + <mat-expansion-panel-header> + <mat-panel-title class="ws-no-wrap"> + {{ relatedRegion.region.name }} + </mat-panel-title> + <mat-panel-description class="ws-no-wrap"> + {{ relatedRegion.parcellation.name }} + </mat-panel-description> + </mat-expansion-panel-header> + + <ng-template matExpansionPanelContent> + + <div> + Current region + <i>{{ region.name }}</i> {{ relatedRegion.qualification | translateQualificationPipe }} + <i>{{ relatedRegion.region.name }}</i> in + <b>{{ relatedRegion.parcellation.name }}</b> + </div> + + <sxplr-feature-entry + [parcellation]="relatedRegion.parcellation" + [region]="relatedRegion.region"> + </sxplr-feature-entry> + + </ng-template> + </mat-expansion-panel> + </mat-accordion> + </mat-tab> + </ng-template> + </ng-template> + + </ng-template> </mat-tab-group> </ng-template> diff --git a/src/atlasComponents/sapiViews/core/region/translateQualification.pipe.ts b/src/atlasComponents/sapiViews/core/region/translateQualification.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..62eaa9714b4294a497a2957cf9ed401a92a79347 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/region/translateQualification.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { Qualification } from "src/atlasComponents/sapi/typeV3" + +const QUALIFICATION_TRANSLATION: Record<Qualification, string> = { + APPROXIMATE: "is approximately", + CONTAINED: "is contained by", + CONTAINS: "contains", + EXACT: "is exactly", + HOMOLOGOUS: "is homologuous to", + OTHER_VERSION: "is another version of", + OVERLAPS: "overlaps with", +} + +@Pipe({ + name: "translateQualificationPipe", + pure: true +}) +export class TranslateQualificationPipe implements PipeTransform{ + public transform(value: Qualification): string { + return QUALIFICATION_TRANSLATION[value] + } +} diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 5967e510255e45ba3eb1c980d7cf65a53018c429..3960d862437dcea9db00aea3812a0f60b420c331 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from "@ngrx/effects"; import { forkJoin, merge, NEVER, Observable, of } from "rxjs"; import { catchError, filter, map, mapTo, switchMap, switchMapTo, take, withLatestFrom } from "rxjs/operators"; -import { SAPI, SAPIRegion } from "src/atlasComponents/sapi"; +import { SAPI } from "src/atlasComponents/sapi"; import * as mainActions from "../actions" import { select, Store } from "@ngrx/store"; import { selectors, actions } from '.' @@ -264,7 +264,7 @@ export class Effect { switchMap(([regions, layers]) => { const map = new Map<SxplrRegion, number[]>() for (const region of regions) { - map.set(region, SAPIRegion.GetDisplayColor(region)) + map.set(region, region.color) } const actions = [ ...layers.map(({ id }) => diff --git a/src/ui/dialogInfo/dialog.directive.ts b/src/ui/dialogInfo/dialog.directive.ts index 3b24370b99282a6b8386e4c81b35c8b7286f552f..62640509efdc3a65f053989d50464ea81dd2285b 100644 --- a/src/ui/dialogInfo/dialog.directive.ts +++ b/src/ui/dialogInfo/dialog.directive.ts @@ -2,7 +2,7 @@ import { Directive, HostListener, Input, TemplateRef } from "@angular/core"; import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; import { DialogFallbackCmp } from "./tmpl/tmpl.component" -type DialogSize = 's' | 'm' | 'l' | 'xl' +type DialogSize = 's' | 'm' | 'l' | 'xl' | 'auto' const sizeDict: Record<DialogSize, Partial<MatDialogConfig>> = { 's': { @@ -20,7 +20,8 @@ const sizeDict: Record<DialogSize, Partial<MatDialogConfig>> = { 'xl': { width: '90vw', height: '90vh' - } + }, + 'auto': {} } @Directive({ diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index 0ac608a713e16025f5ffcbb6049162fa1d37d15f..62aca0e77422e585a95e6e31951e6301dfdff8e4 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -439,6 +439,8 @@ export class NehubaViewerUnit implements OnDestroy { } })() viewer.inputEventBindings.sliceView.set("at:wheel", "proxy-wheel-1") + viewer.inputEventBindings.sliceView.set("at:keyp", "proxy-wheel-1") + viewer.inputEventBindings.sliceView.set("at:keyn", "proxy-wheel-1") viewer.inputEventBindings.sliceView.set("at:control+shift+wheel", "proxy-wheel-10") viewer.display.panels.forEach(sliceView => patchSliceViewPanel(sliceView, this.exportNehuba, this.multplier)) } @@ -962,8 +964,18 @@ const patchSliceViewPanel = (sliceViewPanel: any, exportNehuba: any, mulitplier: registerActionListener(sliceViewPanel.element, `proxy-wheel-${val}`, event => { const e = event.detail + let keyDelta = null + if (e.key === "p") { + keyDelta = -1 + } + if (e.key === "n") { + keyDelta = 1 + } const offset = tempVec3 - const delta = e.deltaY !== 0 ? e.deltaY : e.deltaX + const wheelDelta = e.deltaY !== 0 ? e.deltaY : e.deltaX + + const delta = keyDelta ?? wheelDelta + offset[0] = 0 offset[1] = 0 offset[2] = (delta > 0 ? -1 : 1) * mulitplier[0] * val