diff --git a/common/util.js b/common/util.js index f38bc575a128262c88718cb865f1cb8302a6cb12..11515f2778d94463ae55b5f788a46116446be085 100644 --- a/common/util.js +++ b/common/util.js @@ -42,6 +42,13 @@ throw new Error(`fn failed ${retries} times. Aborting.`) } + const flattenRegions = regions => regions.concat( + ...regions.map(region => region.children && region.children instanceof Array + ? flattenRegions(region.children) + : []) + ) + + exports.flattenRegions = flattenRegions exports.getRandomHex = (digit = 1024 * 1024 * 1024 * 1024) => Math.round(Math.random() * digit).toString(16) })(typeof exports === 'undefined' ? module.exports : exports) diff --git a/deploy/datasets/util.js b/deploy/datasets/util.js index cdc9ca91a3905fc72d14e50222342249b38c7e13..25953080f60a025c8d321ee976dd50cf894cc4f3 100644 --- a/deploy/datasets/util.js +++ b/deploy/datasets/util.js @@ -3,7 +3,7 @@ const { getCommonSenseDsFilter } = require('./supplements/commonSense') const { hasPreview } = require('./supplements/previewFile') const path = require('path') const fs = require('fs') -const { getIdFromFullId, retry } = require('../../common/util') +const { getIdFromFullId, retry, flattenRegions } = require('../../common/util') let getPublicAccessToken @@ -34,14 +34,6 @@ const getUserKGRequestParam = async ({ user }) => { * Needed by filter by parcellation */ -const flattenArray = (array) => { - return array.concat( - ...array.map(item => item.children && item.children instanceof Array - ? flattenArray(item.children) - : []) - ) -} - const readConfigFile = (filename) => new Promise((resolve, reject) => { let filepath if (process.env.NODE_ENV === 'production') { @@ -93,7 +85,7 @@ initPrArray.push( readConfigFile('bigbrain.json') .then(data => JSON.parse(data)) .then(json => { - const bigbrainCyto = flattenArray(json.parcellations.find(({ name }) => name === 'Cytoarchitectonic Maps').regions) + const bigbrainCyto = flattenRegions(json.parcellations.find(({ name }) => name === 'Cytoarchitectonic Maps').regions) bigbrainCytoSet = populateSet(bigbrainCyto) }) .catch(console.error) @@ -103,9 +95,9 @@ initPrArray.push( readConfigFile('MNI152.json') .then(data => JSON.parse(data)) .then(json => { - const longBundle = flattenArray(json.parcellations.find(({ name }) => name === 'Fibre Bundle Atlas - Long Bundle').regions) - const shortBundle = flattenArray(json.parcellations.find(({ name }) => name === 'Fibre Bundle Atlas - Short Bundle').regions) - const jubrain = flattenArray(json.parcellations.find(({ name }) => 'JuBrain Cytoarchitectonic Atlas' === name).regions) + const longBundle = flattenRegions(json.parcellations.find(({ name }) => name === 'Fibre Bundle Atlas - Long Bundle').regions) + const shortBundle = flattenRegions(json.parcellations.find(({ name }) => name === 'Fibre Bundle Atlas - Short Bundle').regions) + const jubrain = flattenRegions(json.parcellations.find(({ name }) => 'JuBrain Cytoarchitectonic Atlas' === name).regions) longBundleSet = populateSet(longBundle) shortBundleSet = populateSet(shortBundle) juBrainSet = populateSet(jubrain) @@ -117,9 +109,9 @@ initPrArray.push( readConfigFile('waxholmRatV2_0.json') .then(data => JSON.parse(data)) .then(json => { - const waxholm3 = flattenArray(json.parcellations[0].regions) - const waxholm2 = flattenArray(json.parcellations[1].regions) - const waxholm1 = flattenArray(json.parcellations[2].regions) + const waxholm3 = flattenRegions(json.parcellations[0].regions) + const waxholm2 = flattenRegions(json.parcellations[1].regions) + const waxholm1 = flattenRegions(json.parcellations[2].regions) waxholm1Set = populateSet(waxholm1) waxholm2Set = populateSet(waxholm2) @@ -132,10 +124,10 @@ initPrArray.push( readConfigFile('allenMouse.json') .then(data => JSON.parse(data)) .then(json => { - const flattenedAllen2017 = flattenArray(json.parcellations[0].regions) + const flattenedAllen2017 = flattenRegions(json.parcellations[0].regions) allen2017Set = populateSet(flattenedAllen2017) - const flattenedAllen2015 = flattenArray(json.parcellations[1].regions) + const flattenedAllen2015 = flattenRegions(json.parcellations[1].regions) allen2015Set = populateSet(flattenedAllen2015) }) .catch(console.error) diff --git a/docs/releases/v2.2.3.md b/docs/releases/v2.2.3.md index 0c0e21b7153971b074497d2353d359803d2acafa..e2b926a4a2bc13522b3039a22e7133698f4e2248 100644 --- a/docs/releases/v2.2.3.md +++ b/docs/releases/v2.2.3.md @@ -5,3 +5,12 @@ ## New features - Import "Individual Brain Charting (IBC)" data to explorable datasets + +## Bugfixes + +- Fixed `undefined` showing for hemisphere metadata (#537) +- Fixed non atlas image e2e test URL + +## Under the hood stuff + +- renamed css class for consistency diff --git a/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js b/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js index c8cd197adabbe037d22a2ec514c778d2c98568be..89d645b7d937aef4259c7b5a46fb0062f628966f 100644 --- a/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js +++ b/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js @@ -113,6 +113,40 @@ describe('> non-atlas images', () => { } ) ) + + const arr = [ + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-FOM-HSV_R", + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-FOM-HSV_G", + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-FOM-HSV_B", + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI", + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-TIM", + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-MRI", + "https://neuroglancer.humanbrainproject.eu/precomputed/PLI_FOM/BI-MRS", + ] + + expect( + interceptedCalls + ).toContain( + jasmine.objectContaining( + { + method: 'GET', + url: 'https://neuroglancer.humanbrainproject.org/precomputed/BigBrainRelease.2015/8bit/info' + } + ) + ) + + for (const url of arr) { + expect( + interceptedCalls + ).toContain( + jasmine.objectContaining( + { + method: 'GET', + url: `${url}/info` + } + ) + ) + } }) it('> if ref tmpl is not right, only tmpl is loaded', async () => { diff --git a/e2e/src/navigating/navigateFromRegion.prod.e2e-spec.js b/e2e/src/navigating/navigateFromRegion.prod.e2e-spec.js index 480296e6e91c7aa143ffeac170f5f42776eaa700..3c90b7ceb5e394c5074a10b63947ac871f66e99b 100644 --- a/e2e/src/navigating/navigateFromRegion.prod.e2e-spec.js +++ b/e2e/src/navigating/navigateFromRegion.prod.e2e-spec.js @@ -31,22 +31,22 @@ const TEST_DATA = [ expectedTemplateLabels: [ { name: 'MNI Colin 27', - hemisphere: 'Left', + hemisphere: 'left hemisphere', expectedPosition: [-54514755, -16753913, -5260713] }, { name: 'MNI Colin 27', - hemisphere: 'Right', + hemisphere: 'right hemisphere', expectedPosition: [54536567, -17992636, -5712544] }, { name: 'MNI 152 ICBM 2009c Nonlinear Asymmetric', - hemisphere: 'Left', + hemisphere: 'left hemisphere', expectedPosition: [-55442669, -18314601, -6381831] }, { name: 'MNI 152 ICBM 2009c Nonlinear Asymmetric', - hemisphere: 'Right', + hemisphere: 'right hemisphere', expectedPosition: [52602966, -18339402, -5666868] }, ], @@ -106,11 +106,13 @@ describe('> explore same region in different templates', () => { describe(`> moving to ${name}`, () => { it('> works as expected', async () => { - const otherTemplates = await iavPage.getText(`[aria-label="${SHOW_IN_OTHER_REF_SPACE}: ${name}${hemisphere ? (' - ' + hemisphere) : ''}"]`) + const otherTemplate = await iavPage.getText(`[aria-label="${SHOW_IN_OTHER_REF_SPACE}: ${name}${hemisphere ? (' - ' + hemisphere) : ''}"]`) + const trimmedTemplate = otherTemplate.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' ') if (hemisphere) { - expect(otherTemplates.indexOf(hemisphere)).toBeGreaterThanOrEqual(0) + expect(trimmedTemplate).toEqual(`${name} (${hemisphere})`) + } else { + expect(trimmedTemplate).toEqual(name) } - expect(otherTemplates.indexOf(name)).toBeGreaterThanOrEqual(0) await iavPage.click(`[aria-label="${SHOW_IN_OTHER_REF_SPACE}: ${name}${hemisphere ? (' - ' + hemisphere) : ''}"]`) await iavPage.wait(500) diff --git a/mkdocs.yml b/mkdocs.yml index 351f3d9143020530c2373f88521052a98f1cf5b6..1c35290ef120aa15db485eb356448d3973077d08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.2.3: 'releases/v2.2.3.md' - v2.2.2: 'releases/v2.2.2.md' - v2.2.1: 'releases/v2.2.1.md' - v2.2.0: 'releases/v2.2.0.md' diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 1f3066bb2843b1853060526c7b1bc90932fd0a7a..fb5b963afd5f4e48580ea82277af4d229963b1bb 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -34,7 +34,7 @@ import { FixedMouseContextualContainerDirective } from "src/util/directives/Fixe import { isSame } from "src/util/fn"; import { NehubaContainer } from "../ui/nehubaContainer/nehubaContainer.component"; import { colorAnimation } from "./atlasViewer.animation" -import { MouseHoverDirective } from "src/util/directives/mouseOver.directive"; +import { MouseHoverDirective } from "src/atlasViewer/mouseOver.directive"; import {MatSnackBar, MatSnackBarRef} from "@angular/material/snack-bar"; import {MatDialog, MatDialogRef} from "@angular/material/dialog"; import { ARIA_LABELS } from 'common/constants' diff --git a/src/util/directives/mouseOver.directive.spec.ts b/src/atlasViewer/mouseOver.directive.spec.ts similarity index 100% rename from src/util/directives/mouseOver.directive.spec.ts rename to src/atlasViewer/mouseOver.directive.spec.ts diff --git a/src/util/directives/mouseOver.directive.ts b/src/atlasViewer/mouseOver.directive.ts similarity index 100% rename from src/util/directives/mouseOver.directive.ts rename to src/atlasViewer/mouseOver.directive.ts diff --git a/src/main.module.ts b/src/main.module.ts index c402df53fcadec932037d8cba0d05cfb40605bb9..46ad275e166bb931703cd72d78502fcfb056281b 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -37,7 +37,7 @@ import { NewViewerDisctinctViewToLayer } from "./util/pipes/newViewerDistinctVie import { UtilModule } from "./util/util.module"; import { SpotLightModule } from 'src/spotlight/spot-light.module' import { TryMeComponent } from "./ui/tryme/tryme.component"; - +import { MouseHoverDirective, MouseOverIconPipe, MouseOverTextPipe } from "./atlasViewer/mouseOver.directive"; import { UiStateUseEffect, getMouseoverSegmentsFactory, GET_MOUSEOVER_SEGMENTS_TOKEN } from "src/services/state/uiState.store"; import { AtlasViewerHistoryUseEffect } from "./atlasViewer/atlasViewer.history.service"; import { PluginServiceUseEffect } from './services/effect/pluginUseEffect'; @@ -117,12 +117,15 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { FloatingContainerDirective, FloatingMouseContextualContainerDirective, DragDropDirective, + MouseHoverDirective, /* pipes */ GetNamesPipe, GetNamePipe, TransformOnhoverSegmentPipe, NewViewerDisctinctViewToLayer, + MouseOverTextPipe, + MouseOverIconPipe, ], entryComponents : [ ModalUnit, diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index fe7d4d4e320b24ee0149726a15a29781d31d646b..cda477782c63ce59b76ff3fd6f8acc234cfc9583 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -455,8 +455,8 @@ markdown-dom pre code outline: none; } -.cursorPointer { - cursor: pointer; +.cursor-pointer { + cursor: pointer!important; } cdk-virtual-scroll-viewport:not(.cdk-virtual-scroll-orientation-horizontal) > .cdk-virtual-scroll-content-wrapper diff --git a/src/services/state/uiState.store.helper.ts b/src/services/state/uiState.store.helper.ts index a9e69797a5f830897418bd7ec8dbc46ec04c3b7d..8f3ead0a17d7911b8bafedec1612cfc3a2cfa451 100644 --- a/src/services/state/uiState.store.helper.ts +++ b/src/services/state/uiState.store.helper.ts @@ -30,6 +30,10 @@ export const uiActionSetPreviewingDatasetFiles = createAction( props<{previewingDatasetFiles: {datasetId: string, filename: string}[]}>() ) +export const uiActionShowSidePanelConnectivity = createAction( + `[uiState] showSidePanelConnectivity` +) + export enum EnumWidgetTypes{ DATASET_PREVIEW, } diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index c0753c644e9236147672c100fec5df71005687ef..a1aea6f7f7829d7ad92c1cdf9a8d9d4cdd99ae75 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -7,7 +7,7 @@ import { filter, map, mapTo, scan, startWith, take } from "rxjs/operators"; import { COOKIE_VERSION, KG_TOS_VERSION, LOCAL_STORAGE_CONST } from 'src/util/constants' import { IavRootStoreInterface, GENERAL_ACTION_TYPES } from '../stateStore.service' import { MatBottomSheetRef, MatBottomSheet } from '@angular/material/bottom-sheet'; -import { uiStateCloseSidePanel, uiStateOpenSidePanel, uiStateCollapseSidePanel, uiStateExpandSidePanel, uiActionSetPreviewingDatasetFiles, uiStateShowBottomSheet } from './uiState.store.helper'; +import { uiStateCloseSidePanel, uiStateOpenSidePanel, uiStateCollapseSidePanel, uiStateExpandSidePanel, uiActionSetPreviewingDatasetFiles, uiStateShowBottomSheet, uiActionShowSidePanelConnectivity } from './uiState.store.helper'; export const defaultState: StateInterface = { previewingDatasetFiles: [], @@ -108,6 +108,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Stat sidePanelCurrentViewContent: 'Dataset', } + case uiActionShowSidePanelConnectivity.type: case SHOW_SIDE_PANEL_CONNECTIVITY: return { ...prevState, diff --git a/src/services/state/viewerState.store.helper.ts b/src/services/state/viewerState.store.helper.ts index 05079abf9c286cb2e50340c4bf65c18d340dc0b3..5b4c59676b5e6150d9a16c2cdceea9d2166d6c0a 100644 --- a/src/services/state/viewerState.store.helper.ts +++ b/src/services/state/viewerState.store.helper.ts @@ -10,3 +10,18 @@ export const viewerStateSetSelectedRegions = createAction( '[viewerState] setSelectedRegions', props<{ selectRegions: IRegion[] }>() ) + +export const viewerStateSetConnectivityRegion = createAction( + `[viewerState] setConnectivityRegion`, + props<{ connectivityRegion: any }>() +) + +export const viewerStateNavigateToRegion = createAction( + `[viewerState] navigateToRegion`, + props<{ payload: { region: any } }>() +) + +export const viewerStateToggleRegionSelect = createAction( + `[viewerState] toggleRegionSelect`, + props<{ payload: { region: any } }>() +) diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index e7ec17c3ec2d176fbe83a376ebc35c7e1fe9a852..c39b1603221aabec3bf8c2cf3408cc0a58342e26 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -10,7 +10,7 @@ import { LoggingService } from 'src/logging'; import { generateLabelIndexId, IavRootStoreInterface } from '../stateStore.service'; import { GENERAL_ACTION_TYPES } from '../stateStore.service' import { MOUSEOVER_USER_LANDMARK, CLOSE_SIDE_PANEL } from './uiState.store'; -import { viewerStateSetSelectedRegions } from './viewerState.store.helper'; +import { viewerStateSetSelectedRegions, viewerStateSetConnectivityRegion } from './viewerState.store.helper'; export interface StateInterface { fetchedTemplates: any[] @@ -185,6 +185,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part const { viewerState } = (action as any).state return viewerState } + case viewerStateSetConnectivityRegion.type: case SET_CONNECTIVITY_REGION: return { ...prevState, diff --git a/src/ui/connectivityBrowser/connectivityBrowser.template.html b/src/ui/connectivityBrowser/connectivityBrowser.template.html index 61ef3010b3a960b6d8677d1ffceaffa40bd85ea3..970cb6d35015c6d2570b7ec13444b7d401dea6f4 100644 --- a/src/ui/connectivityBrowser/connectivityBrowser.template.html +++ b/src/ui/connectivityBrowser/connectivityBrowser.template.html @@ -14,7 +14,7 @@ [customHeight]="componentHeight + 'px'"> <div slot="header" class="w-100 d-flex justify-content-between mt-3"> <span>Connectivity Browser</span> - <i (click)="closeConnectivityView(); setDefaultMap()" class="far fa-times-circle cursorPointer"></i> + <i (click)="closeConnectivityView(); setDefaultMap()" class="far fa-times-circle cursor-pointer"></i> </div> <div slot="dataset"> diff --git a/src/ui/parcellationRegion/region.base.spec.ts b/src/ui/parcellationRegion/region.base.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e2153d3cf3ad0b3ddb198879728d19d7ea1e91e --- /dev/null +++ b/src/ui/parcellationRegion/region.base.spec.ts @@ -0,0 +1,249 @@ +import { regionInOtherTemplateSelector } from './region.base' + +const mr1wrong = { + labelIndex: 1, + name: 'mr1', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'fff-bbb' + } + } +} + +const mr0wrong = { + labelIndex: 1, + name: 'mr0', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'aaa-fff' + } + } +} + + +const mr1lh = { + labelIndex: 1, + name: 'mr1 - left hemisphere', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'ccc-bbb' + } + } +} + +const mr1rh = { + labelIndex: 1, + name: 'mr1 - right hemisphere', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'ccc-bbb' + } + } +} + +const mr0lh = { + labelIndex: 1, + name: 'mr0 - left hemisphere', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'aaa-bbb' + } + } +} + +const mr0rh = { + labelIndex: 1, + name: 'mr0 - right hemisphere', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'aaa-bbb' + } + } +} + +const mr1 = { + labelIndex: 1, + name: 'mr1', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'ccc-bbb' + } + } +} + +const mr0 = { + labelIndex: 1, + name: 'mr0', + fullId: { + kg: { + kgSchema: 'fzj/mock/pr', + kgId: 'aaa-bbb' + } + } +} + +// parcellations + +const mp1h = { + name: 'mp1h', + regions: [ mr1lh, mr0lh, mr0rh, mr1rh ] +} + +const mpWrong = { + name: 'mp1h', + regions: [ mr1wrong, mr0wrong ] +} + +const mp0 = { + name: 'mp0', + regions: [ mr1, mr0 ] +} + +// templates + +const mt0 = { + name: 'mt0', + fullId: 'fzj/mock/rs/v0.0.0/aaa-bbb', + parcellations: [ mp0 ] +} + +const mt1 = { + name: 'mt1', + fullId: 'fzj/mock/rs/v0.0.0/bbb-bbb', + parcellations: [ mp0 ] +} + +const mt2 = { + name: 'mt2', + fullId: 'fzj/mock/rs/v0.0.0/ccc-bbb', + parcellations: [ mp1h ] +} + +const mt3 = { + name: 'mt3', + fullId: 'fzj/mock/rs/v0.0.0/ddd-bbb', + parcellations: [ mp1h ] +} + +const mtWrong = { + name: 'mtWrong', + fullId: 'fzj/mock/rs/v0.0.0/ddd-bbb', + parcellations: [ mpWrong ] +} + +const mockFetchedTemplates = [ mt0, mt1, mt2, mt3, mtWrong ] + +describe('> region.base.ts', () => { + describe('> getRegionInOtherTemplatesSelector', () => { + describe('> no hemisphere selected, simulates big brain cyto map', () => { + + let result: any[] + beforeAll(() => { + result = regionInOtherTemplateSelector.projector({ fetchedTemplates: mockFetchedTemplates, templateSelected: mt0 }, { region: mr0 }) + }) + + it('> length checks out', () => { + expect(result.length).toEqual(5) + }) + + it('> does not contain itself', () => { + expect(result).not.toContain( + jasmine.objectContaining({ + template: mt0, + parcellation: mp0, + region: mr0 + }) + ) + }) + + it('> no hemisphere result has no hemisphere meta data', () => { + expect(result).toContain( + jasmine.objectContaining({ + template: mt1, + parcellation: mp0, + region: mr0 + }) + ) + }) + + it('> hemisphere result has hemisphere metadata # 1', () => { + expect(result).toContain( + jasmine.objectContaining({ + template: mt2, + parcellation: mp1h, + region: mr0lh, + hemisphere: 'left hemisphere' + }) + ) + }) + it('> hemisphere result has hemisphere metadata # 2', () => { + expect(result).toContain( + jasmine.objectContaining({ + template: mt3, + parcellation: mp1h, + region: mr0lh, + hemisphere: 'left hemisphere' + }) + ) + }) + it('> hemisphere result has hemisphere metadata # 3', () => { + expect(result).toContain( + jasmine.objectContaining({ + template: mt3, + parcellation: mp1h, + region: mr0rh, + hemisphere: 'right hemisphere' + }) + ) + }) + it('> hemisphere result has hemisphere metadata # 4', () => { + expect(result).toContain( + jasmine.objectContaining({ + template: mt3, + parcellation: mp1h, + region: mr0rh, + hemisphere: 'right hemisphere' + }) + ) + }) + }) + + describe('> hemisphere data selected (left hemisphere), simulates julich-brain in mni152', () => { + let result + beforeAll(() => { + result = regionInOtherTemplateSelector.projector({ fetchedTemplates: mockFetchedTemplates, templateSelected: mt2 }, { region: mr0lh }) + }) + + it('> length checks out', () => { + expect(result.length).toEqual(3) + }) + + it('> does not select wrong hemisphere (right hemisphere)', () => { + expect(result).not.toContain( + jasmine.objectContaining({ + template: mt3, + parcellation: mp1h, + region: mr0rh, + }) + ) + }) + + it('> select the corresponding hemisphere (left hemisphere), but without hemisphere metadata', () => { + expect(result).toContain( + jasmine.objectContaining({ + template: mt3, + parcellation: mp1h, + region: mr0lh + }) + ) + }) + }) + }) +}) diff --git a/src/ui/parcellationRegion/region.base.ts b/src/ui/parcellationRegion/region.base.ts index 0b12f98d2af402be9e9369f62555da8f14703286..eb514bd7dd6e4c0c1551cd9e61e23530166b7993 100644 --- a/src/ui/parcellationRegion/region.base.ts +++ b/src/ui/parcellationRegion/region.base.ts @@ -1,20 +1,28 @@ -import {EventEmitter, Input, Output} from "@angular/core"; -import {select, Store} from "@ngrx/store"; -import {NEWVIEWER, SET_CONNECTIVITY_REGION} from "src/services/state/viewerState.store"; -import { - EXPAND_SIDE_PANEL_CURRENT_VIEW, - IavRootStoreInterface, OPEN_SIDE_PANEL, - SHOW_SIDE_PANEL_CONNECTIVITY, -} from "src/services/stateStore.service"; -import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerStateController/viewerState.base"; -import {distinctUntilChanged, shareReplay} from "rxjs/operators"; -import {Observable} from "rxjs"; +import {EventEmitter, Input, Output } from "@angular/core"; +import {select, Store, createSelector} from "@ngrx/store"; +import { uiStateOpenSidePanel, uiStateExpandSidePanel, uiActionShowSidePanelConnectivity } from 'src/services/state/uiState.store.helper' +import {distinctUntilChanged, switchMap, filter} from "rxjs/operators"; +import { Observable, BehaviorSubject } from "rxjs"; import { ARIA_LABELS } from 'common/constants' +import { flattenRegions, getIdFromFullId } from 'common/util' +import { viewerStateSetConnectivityRegion, viewerStateNavigateToRegion, viewerStateToggleRegionSelect } from "src/services/state/viewerState.store.helper"; export class RegionBase { + + private _region: any + + get region(){ + return this._region + } + @Input() - public region: any + set region(val) { + this._region = val + this.region$.next(this.region) + } + + private region$: BehaviorSubject<any> = new BehaviorSubject(null) @Input() public isSelected: boolean = false @@ -23,136 +31,140 @@ export class RegionBase { @Output() public closeRegionMenu: EventEmitter<boolean> = new EventEmitter() - public loadedTemplate$: Observable<any[]> - public templateSelected$: Observable<any[]> - public parcellationSelected$: Observable<any[]> - - protected loadedTemplates: any[] - protected selectedTemplate: any - protected selectedParcellation: any public sameRegionTemplate: any[] = [] - - private parcellationRegions: any[] = [] + public regionInOtherTemplates$: Observable<any[]> constructor( - private store$: Store<IavRootStoreInterface>, + private store$: Store<any>, ) { - const viewerState$ = this.store$.pipe( - select('viewerState'), - shareReplay(1), - ) - - this.loadedTemplate$ = viewerState$.pipe( - select('fetchedTemplates'), - distinctUntilChanged() - ) - - this.templateSelected$ = viewerState$.pipe( - select('templateSelected'), - distinctUntilChanged(), - ) - - this.parcellationSelected$ = viewerState$.pipe( - select('parcellationSelected'), + this.regionInOtherTemplates$ = this.region$.pipe( distinctUntilChanged(), + filter(v => !!v), + switchMap(region => this.store$.pipe( + select( + regionInOtherTemplateSelector, + { region } + ) + )) ) } + public navigateToRegion() { this.closeRegionMenu.emit() const { region } = this - this.store$.dispatch({ - type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.NAVIGATETO_REGION, - payload: { region }, - }) + this.store$.dispatch( + viewerStateNavigateToRegion({ payload: { region } }) + ) } public toggleRegionSelected() { this.closeRegionMenu.emit() const { region } = this - this.store$.dispatch({ - type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.TOGGLE_REGION_SELECT, - payload: { region }, - }) + this.store$.dispatch( + viewerStateToggleRegionSelect({ payload: { region } }) + ) } public showConnectivity(regionName) { this.closeRegionMenu.emit() // ToDo trigger side panel opening with effect - this.store$.dispatch({type: OPEN_SIDE_PANEL}) - this.store$.dispatch({type: EXPAND_SIDE_PANEL_CURRENT_VIEW}) - this.store$.dispatch({type: SHOW_SIDE_PANEL_CONNECTIVITY}) + this.store$.dispatch(uiStateOpenSidePanel()) + this.store$.dispatch(uiStateExpandSidePanel()) + this.store$.dispatch(uiActionShowSidePanelConnectivity()) - this.store$.dispatch({ - type: SET_CONNECTIVITY_REGION, - connectivityRegion: regionName, - }) - } - - - getDifferentTemplatesSameRegion() { - this.sameRegionTemplate = [] - this.loadedTemplates.forEach(template => { - if (this.selectedTemplate.name !== template.name) { - template.parcellations.forEach(parcellation => { - this.parcellationRegions = [] - this.getAllRegionsFromParcellation(parcellation.regions) - this.parcellationRegions.forEach(pr => { - if (!(JSON.stringify(pr.fullId) === 'null' || JSON.stringify(this.region.fullId) === 'null') - && JSON.stringify(pr.fullId) === JSON.stringify(this.region.fullId)) { - const baseAreaHemisphere = - this.region.name.includes(' - right hemisphere')? 'Right' : - this.region.name.includes(' - left hemisphere')? 'Left' - : null - const areaHemisphere = - pr.name.includes(' - right hemisphere')? 'Right' - : pr.name.includes(' - left hemisphere')? 'Left' - : null - - const sameRegionSpace = {template: template, parcellation: parcellation, region: pr} - - if (!baseAreaHemisphere && areaHemisphere) { - this.sameRegionTemplate.push({ - ...sameRegionSpace, - hemisphere: areaHemisphere - }) - } else - if (!this.sameRegionTemplate.map(sr => sr.template).includes(template)) { - if (!(baseAreaHemisphere && areaHemisphere && baseAreaHemisphere !== areaHemisphere)) { - this.sameRegionTemplate.push(sameRegionSpace) - } - } - } - }) - }) - } - }) + this.store$.dispatch( + viewerStateSetConnectivityRegion({ connectivityRegion: regionName }) + ) } - changeView(index) { + changeView(sameRegion) { + const { + template, + parcellation, + region + } = sameRegion + const { position } = region this.closeRegionMenu.emit() + /** + * TODO use createAction in future + * for now, not importing const because it breaks tests + */ this.store$.dispatch({ - type : NEWVIEWER, - selectTemplate : this.sameRegionTemplate[index].template, - selectParcellation : this.sameRegionTemplate[index].parcellation, - navigation: {position: this.sameRegionTemplate[index].region.position}, + type: `NEWVIEWER`, + selectTemplate: template, + selectParcellation: parcellation, + navigation: { + position + }, }) } - public getAllRegionsFromParcellation = (regions) => { - for (const region of regions) { - if (region.children && region.children.length) { - this.getAllRegionsFromParcellation(region.children) - } else { - this.parcellationRegions.push(region) - } - } - } - public SHOW_CONNECTIVITY_DATA = ARIA_LABELS.SHOW_CONNECTIVITY_DATA public SHOW_IN_OTHER_REF_SPACE = ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE public SHOW_ORIGIN_DATASET = ARIA_LABELS.SHOW_ORIGIN_DATASET public AVAILABILITY_IN_OTHER_REF_SPACE = ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE } + +export const regionInOtherTemplateSelector = createSelector( + (state: any) => state.viewerState, + (viewerState, prop) => { + const { region: regionOfInterest } = prop + const returnArr = [] + const regionOfInterestHemisphere = regionOfInterest.name.includes('- right hemisphere') + ? 'right hemisphere' + : regionOfInterest.name.includes('- left hemisphere') + ? 'left hemisphere' + : null + + const regionOfInterestId = getIdFromFullId(regionOfInterest.fullId) + const { fetchedTemplates, templateSelected } = viewerState + const selectedTemplateId = getIdFromFullId(templateSelected.fullId) + const otherTemplates = fetchedTemplates.filter(({ fullId }) => getIdFromFullId(fullId) !== selectedTemplateId) + for (const template of otherTemplates) { + for (const parcellation of template.parcellations) { + const flattenedRegions = flattenRegions(parcellation.regions) + const selectableRegions = flattenedRegions.filter(({ labelIndex }) => !!labelIndex) + + for (const region of selectableRegions) { + const id = getIdFromFullId(region.fullId) + if (!!id) { + const regionHemisphere = region.name.includes('- right hemisphere') + ? 'right hemisphere' + : region.name.includes('- left hemisphere') + ? 'left hemisphere' + : null + + if (id === regionOfInterestId) { + /** + * if both hemisphere metadatas are defined + */ + if ( + !!regionOfInterestHemisphere && + !!regionHemisphere + ) { + if (regionHemisphere === regionOfInterestHemisphere) { + returnArr.push({ + template, + parcellation, + region, + }) + } + } else { + returnArr.push({ + template, + parcellation, + region, + hemisphere: regionHemisphere + }) + } + } + } + } + } + } + return returnArr + } +) \ No newline at end of file diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts b/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c98dc9754190acbeebff6af212f7fedce139cfeb --- /dev/null +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts @@ -0,0 +1,219 @@ +import { TestBed, async } from "@angular/core/testing" +import { RegionMenuComponent } from "./regionMenu.component" +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module" +import { UtilModule } from "src/util/util.module" +import { CommonModule } from "@angular/common" +import { PreviewDatasetFile } from "src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive" +import { provideMockStore, MockStore } from "@ngrx/store/testing" +import { regionInOtherTemplateSelector } from '../region.base' +import { ARIA_LABELS } from 'common/constants' +import { By } from "@angular/platform-browser" + +const mt0 = { + name: 'mt0' +} + +const mt1 = { + name: 'mt1' +} + +const mr0 = { + name: 'mr0' +} + +const mr1 = { + name: 'mr1' +} + +const mp0 = { + name: 'mp0' +} + +const mp1 = { + name: 'mp1' +} + +const mrm0 = { + template: mt0, + parcellation: mp0, + region: mr0 +} + +const mrm1 = { + template: mt1, + parcellation: mp1, + region: mr1 +} + +const hemisphereMrms = [ { + ...mrm0, + hemisphere: 'left hemisphere' +}, { + ...mrm1, + hemisphere: 'left hemisphere' +} ] + +const nohemisphereHrms = [mrm0, mrm1] + +describe('> regionMenu.component.ts', () => { + describe('> RegionMenuComponent', () => { + beforeEach(async(() => { + + TestBed.configureTestingModule({ + imports: [ + UtilModule, + AngularMaterialModule, + CommonModule, + ], + declarations: [ + RegionMenuComponent, + + /** + * Used by regionMenu.template.html to show region preview + */ + PreviewDatasetFile, + ], + providers: [ + provideMockStore({ initialState: {} }) + ] + }).compileComponents() + + })) + + it('> init just fine', () => { + const fixture = TestBed.createComponent(RegionMenuComponent) + expect(fixture).toBeTruthy() + }) + + describe('> regionInOtherTemplatesTmpl', () => { + + it('> toggleBtn does not exists if selector returns empty array', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + [] + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + expect(toggleBtn).toBeFalsy() + }) + + it('> toggleBtn exists if selector returns non empty array', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + nohemisphereHrms + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + expect(toggleBtn).toBeTruthy() + }) + + it('> even if toggleBtn exists, list should be hidden by default', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + nohemisphereHrms + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + expect(listContainer).toBeFalsy() + }) + + it('> on click of toggle btn, list to become visible', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + nohemisphereHrms + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + toggleBtn.triggerEventHandler('click', null) + fixture.detectChanges() + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + expect(listContainer).toBeTruthy() + }) + + it('> once list becomes available, there should be 2 items on the list', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + nohemisphereHrms + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + toggleBtn.triggerEventHandler('click', null) + fixture.detectChanges() + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + expect(listContainer.nativeElement.children.length).toEqual(2) + }) + + it('> the text (no hemisphere metadata) on the list is as expected', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + nohemisphereHrms + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + toggleBtn.triggerEventHandler('click', null) + fixture.detectChanges() + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + + // trim white spaces before and after + const texts = Array.from(listContainer.nativeElement.children).map((c: HTMLElement) => c.textContent.replace(/^\s+/, '').replace(/\s+$/, '')) + expect(texts).toContain(mt0.name) + expect(texts).toContain(mt1.name) + }) + + it('> the text (with hemisphere metadata) on the list is as expected', () => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector( + regionInOtherTemplateSelector, + hemisphereMrms + ) + + const fixture = TestBed.createComponent(RegionMenuComponent) + fixture.componentInstance.region = mr1 + fixture.detectChanges() + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + toggleBtn.triggerEventHandler('click', null) + fixture.detectChanges() + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + + // trim white spaces before and after, and middle white spaces into a single white space + const texts = Array.from(listContainer.nativeElement.children).map((c: HTMLElement) => c.textContent.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' ')) + expect(texts).toContain(`${mt0.name} (left hemisphere)`) + expect(texts).toContain(`${mt1.name} (left hemisphere)`) + }) + }) + }) +}) \ No newline at end of file diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts index 4890920d3ea4fc398d7de4bc0dc1f26eefa10e8e..8d121014612f34788c6b252540ff557dedb5f3c3 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts @@ -1,7 +1,6 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy } from "@angular/core"; import { Store } from "@ngrx/store"; import { Subscription } from "rxjs"; -import { IavRootStoreInterface } from "src/services/stateStore.service"; import { RegionBase } from '../region.base' @Component({ @@ -9,33 +8,16 @@ import { RegionBase } from '../region.base' templateUrl: './regionMenu.template.html', styleUrls: ['./regionMenu.style.css'], }) -export class RegionMenuComponent extends RegionBase implements OnInit, OnDestroy { +export class RegionMenuComponent extends RegionBase implements OnDestroy { private subscriptions: Subscription[] = [] constructor( - store$: Store<IavRootStoreInterface>, + store$: Store<any>, ) { super(store$) } - ngOnInit(): void { - this.subscriptions.push( - this.templateSelected$.subscribe(template => { - this.selectedTemplate = template - }), - this.parcellationSelected$.subscribe(parcellation => { - this.selectedParcellation = parcellation - }), - this.loadedTemplate$.subscribe(templates => { - this.loadedTemplates = templates - this.getDifferentTemplatesSameRegion() - // this.bigBrainJubrainSwitch() - // this.getSameParcellationTemplates() - }), - ) - } - ngOnDestroy(): void { this.subscriptions.forEach(s => s.unsubscribe()) } diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index d74dd07ac79fe71001bc036b74e4c7a0a1df3480..714b6e70af6d1c12eb02122b4c0a8a2148bd1149 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -15,79 +15,6 @@ <mat-card-content> <mat-divider></mat-divider> - <ng-template #safeHarbour> - - <!-- enlarged region actions --> - <mat-grid-list cols="2" rowHeight="6rem" gutterSize="0px"> - - <mat-grid-tile> - <iav-v-button - class="h-100 w-100" - mat-ripple - [color]="isSelected ? 'primary' : 'default'" - (click)="toggleRegionSelected()"> - <i iav-v-button-icon class="far" [ngClass]="{'fa-check-square': isSelected, 'fa-square': !isSelected}"></i> - <span iav-v-button-text>Select</span> - </iav-v-button> - </mat-grid-tile> - - <mat-grid-tile> - <iav-v-button - class="h-100 w-100" - mat-ripple - (click)="navigateToRegion()"> - <i class="fas fa-map-marked-alt" iav-v-button-icon></i> - <span iav-v-button-text>Navigate</span> - </iav-v-button> - </mat-grid-tile> - - <ng-container *ngFor="let originDataset of (region.originDatasets || [])"> - <mat-grid-tile class="iv-custom-comp"> - <iav-v-button - iav-dataset-preview-dataset-file - [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" - [iav-dataset-preview-dataset-file-filename]="originDataset.filename" - #previewDirective="iavDatasetPreviewDatasetFile" - class="h-100 w-100" - mat-ripple> - <i class="far fa-eye" iav-v-button-icon></i> - <span iav-v-button-text [class]="previewDirective.active ? 'iv-custom-comp primary' : ''">Probability Map {{ previewDirective.active }}</span> - </iav-v-button> - </mat-grid-tile> - </ng-container> - - <mat-grid-tile> - <iav-v-button *ngIf="hasConnectivity" - class="h-100 w-100" - mat-ripple - [matMenuTriggerFor]="connectivitySourceDatasets" - #connectivityMenuButton="matMenuTrigger" - iav-captureClickListenerDirective - [iav-captureClickListenerDirective-captureDocument]="true" - (iav-captureClickListenerDirective-onMousedown)="connectivityMenuButton.closeMenu()"> - <i class="fab fa-connectdevelop" iav-v-button-icon></i> - <span iav-v-button-text>Connectivity</span> - <i class="fas fa-chevron-down" iav-v-button-footer></i> - </iav-v-button> - </mat-grid-tile> - - <mat-grid-tile> - <iav-v-button *ngIf="sameRegionTemplate.length" - class="h-100 w-100" - mat-ripple - [matMenuTriggerFor]="additionalActions" - #changeTmplTrigger="matMenuTrigger" - iav-captureClickListenerDirective - [iav-captureClickListenerDirective-captureDocument]="true" - (iav-captureClickListenerDirective-onMousedown)="changeTmplTrigger.closeMenu()"> - <i class="fas fa-brain" iav-v-button-icon></i> - <span iav-v-button-text>Change template</span> - <i class="fas fa-chevron-down" iav-v-button-footer></i> - </iav-v-button> - </mat-grid-tile> - </mat-grid-list> - </ng-template> - <!-- region desc --> <ng-container *ngIf="region?.description?.length > 0"> <mat-divider></mat-divider> @@ -165,42 +92,9 @@ </div> <!-- change template --> - <div iav-switch #changeTmplSwitch="iavSwitch"> + <ng-container *ngTemplateOutlet="regionInOtherTemplatesTmpl; context: { regionInOtherTemplates: regionInOtherTemplates$ | async }"> + </ng-container> - <mat-list-item *ngIf="sameRegionTemplate.length" - mat-ripple - [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE" - (click)="changeTmplSwitch && changeTmplSwitch.toggle()"> - <mat-icon fontSet="fas" fontIcon="fa-brain" mat-list-icon></mat-icon> - <div mat-line> - <span> - Explore in other templates - </span> - <span class="muted"> - ({{ sameRegionTemplate.length }}) - </span> - </div> - <mat-icon fontSet="fas" [fontIcon]="changeTmplSwitch.switchState ? 'fa-chevron-up' : 'fa-chevron-down'"></mat-icon> - </mat-list-item> - - <!-- change template items --> - <div *ngIf="changeTmplSwitch.switchState" - [attr.aria-label]="AVAILABILITY_IN_OTHER_REF_SPACE"> - <mat-list-item *ngFor="let sameRegion of sameRegionTemplate; let i = index" - [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE + ': ' + sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') " - (click)="changeView(i)" - mat-ripple> - <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> - <div class="cursorPointer" #exploreTemplateButton mat-line> - <span #exploreTemplateName class="overflow-x-hidden text-truncate" - [matTooltip]="sameRegion.template.name + ' ' + sameRegion.hemisphere"> - {{ sameRegion.template.name + ' ' + sameRegion.hemisphere }} - </span> - </div> - </mat-list-item> - </div> - - </div> </mat-list> </mat-card-content> </mat-card> @@ -217,15 +111,54 @@ </div> </mat-menu> - -<mat-menu - #additionalActions="matMenu" - xPosition="before" - hasBackdrop="false"> - <div> - <button mat-menu-item *ngFor="let sameRegion of sameRegionTemplate; let i = index" (mousedown)="changeView(i)" class="d-flex"> - <span class="overflow-x-hidden text-truncate"> {{sameRegion.template.name}} </span> - <span *ngIf="sameRegion.hemisphere"> - {{sameRegion.hemisphere}}</span> - </button> +<!-- template for switching template --> +<ng-template #regionInOtherTemplatesTmpl let-regionInOtherTemplates="regionInOtherTemplates"> + + <div iav-switch #changeTmplSwitch="iavSwitch"> + + <mat-list-item *ngIf="regionInOtherTemplates.length" + mat-ripple + [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE" + (click)="changeTmplSwitch && changeTmplSwitch.toggle()"> + <mat-icon fontSet="fas" fontIcon="fa-brain" mat-list-icon></mat-icon> + <div mat-line> + <span> + Explore in other templates + </span> + <span class="muted"> + ({{ regionInOtherTemplates.length }}) + </span> + </div> + <mat-icon fontSet="fas" [fontIcon]="changeTmplSwitch.switchState ? 'fa-chevron-up' : 'fa-chevron-down'"></mat-icon> + </mat-list-item> + + <!-- change template items --> + <div *ngIf="changeTmplSwitch.switchState" + [attr.aria-label]="AVAILABILITY_IN_OTHER_REF_SPACE"> + <mat-list-item *ngFor="let sameRegion of regionInOtherTemplates; let i = index" + [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE + ': ' + sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') " + (click)="changeView(sameRegion)" + mat-ripple + [attr.role]="'button'"> + <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> + <div mat-line> + <ng-container *ngTemplateOutlet="regionInOtherTemplate; context: sameRegion"> + </ng-container> + </div> + </mat-list-item> + </div> </div> -</mat-menu> +</ng-template> + +<!-- template for rendering template name and template hemisphere --> +<ng-template #regionInOtherTemplate let-template="template" let-hemisphere="hemisphere"> + <span class="overflow-x-hidden text-truncate" + [matTooltip]="template.name + (hemisphere ? (' ' + hemisphere) : '')"> + <span> + {{ template.name }} + </span> + <span *ngIf="hemisphere" class="text-muted"> + ({{ hemisphere }}) + </span> + </span> +</ng-template> diff --git a/src/ui/viewerStateController/viewerState.base.ts b/src/ui/viewerStateController/viewerState.base.ts index c74aa7128e374ee01b305992ddae68cd8bf330fa..fdbabfcf73a3f66e5b8338dbbcc826c77370a9e9 100644 --- a/src/ui/viewerStateController/viewerState.base.ts +++ b/src/ui/viewerStateController/viewerState.base.ts @@ -14,7 +14,6 @@ const ACTION_TYPES = { SELECT_TEMPLATE_WITH_NAME: 'SELECT_TEMPLATE_WITH_NAME', SELECT_PARCELLATION_WITH_NAME: 'SELECT_PARCELLATION_WITH_NAME', - TOGGLE_REGION_SELECT: 'TOGGLE_REGION_SELECT', NAVIGATETO_REGION: 'NAVIGATETO_REGION', } diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index 23876b16b5d91f9110c9e25259603fabdb1deef2..741b8d6166ed82d21aa47a2465d038129542ca14 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -10,6 +10,7 @@ import { regionFlattener } from "src/util/regionFlattener"; import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "./viewerState.base"; import {TemplateCoordinatesTransformation} from "src/services/templateCoordinatesTransformation.service"; import { CLEAR_STANDALONE_VOLUMES } from "src/services/state/viewerState.store"; +import { viewerStateToggleRegionSelect } from "src/services/state/viewerState.store.helper"; @Injectable({ providedIn: 'root', @@ -249,16 +250,11 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy { this.singleClickOnHierarchy$ = this.actions$.pipe( ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.SINGLE_CLICK_ON_REGIONHIERARCHY), - map(action => { - return { - ...action, - type: VIEWERSTATE_CONTROLLER_ACTION_TYPES.TOGGLE_REGION_SELECT, - } - }), + map(action => viewerStateToggleRegionSelect(action as any)), ) this.toggleRegionSelection$ = this.actions$.pipe( - ofType(VIEWERSTATE_CONTROLLER_ACTION_TYPES.TOGGLE_REGION_SELECT), + ofType(viewerStateToggleRegionSelect.type), withLatestFrom(this.selectedRegions$), map(([action, regionsSelected]) => { diff --git a/src/util/util.module.ts b/src/util/util.module.ts index 1f8c3f09fb96e5ae7f842102b9f4b95353d31477..f60c57f7efeaba1c4812663f0d0ee62320f50c9e 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -2,7 +2,7 @@ import { NgModule } from "@angular/core"; import { FilterRowsByVisbilityPipe } from "src/components/flatTree/filterRowsByVisibility.pipe"; import { DelayEventDirective } from "./directives/delayEvent.directive"; import { KeyListner } from "./directives/keyDownListener.directive"; -import { MouseHoverDirective, MouseOverIconPipe, MouseOverTextPipe } from "./directives/mouseOver.directive"; + import { StopPropagationDirective } from "./directives/stopPropagation.directive"; import { FilterNullPipe } from "./pipes/filterNull.pipe"; import { IncludesPipe } from "./pipes/includes.pipe"; @@ -23,9 +23,6 @@ import { LayoutModule } from "@angular/cdk/layout"; FilterRowsByVisbilityPipe, StopPropagationDirective, DelayEventDirective, - MouseHoverDirective, - MouseOverTextPipe, - MouseOverIconPipe, KeyListner, IncludesPipe, SafeResourcePipe, @@ -40,9 +37,6 @@ import { LayoutModule } from "@angular/cdk/layout"; FilterRowsByVisbilityPipe, StopPropagationDirective, DelayEventDirective, - MouseHoverDirective, - MouseOverTextPipe, - MouseOverIconPipe, KeyListner, IncludesPipe, SafeResourcePipe,