diff --git a/e2e/src/navigating/navigateFromRegion.e2e-spec.js b/e2e/src/navigating/navigateFromRegion.e2e-spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0cbe20e896a45c7dd16d2392acbe49a1e6b9efeb --- /dev/null +++ b/e2e/src/navigating/navigateFromRegion.e2e-spec.js @@ -0,0 +1,111 @@ +const {AtlasPage} = require('../util') + +const TEST_DATA = [ + { + url: "/?templateSelected=MNI+152+ICBM+2009c+Nonlinear+Asymmetric&parcellationSelected=JuBrain+Cytoarchitectonic+Atlas", + templateName: 'MNI 152 ICBM 2009c Nonlinear Asymmetric', + position: [450, 200], + expectedRegion: 'Area hOc1 (V1, 17, CalcS) - left hemisphere', + expectedTemplateLabels: [ + { + name: 'Big Brain (Histology)', + expectedPosition: [3187941, -50436480, 3430986] + }, + { + name: 'MNI Colin 27', + expectedPosition: [ + -8533787, + -84646549, + 1855106 + ] + }, + ], + }, + { + url: "/?templateSelected=Big+Brain+%28Histology%29&parcellationSelected=Cytoarchitectonic+Maps&cNavigation=0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..1n5q~.1FYC.2Is-..1B9C", + templateName: 'Big Brain (Histology)', + position: [691,678], // [370, 150], + expectedRegion: 'Area STS1 (STS)', + expectedTemplateLabels: [ + { + name: 'MNI Colin 27', + hemisphere: 'Left', + expectedPosition: [-54514755, -16753913, -5260713] + }, + { + name: 'MNI Colin 27', + hemisphere: 'Right', + expectedPosition: [54536567, -17992636, -5712544] + }, + { + name: 'MNI 152 ICBM 2009c Nonlinear Asymmetric', + hemisphere: 'Left', + expectedPosition: [-55442669, -18314601, -6381831] + }, + { + name: 'MNI 152 ICBM 2009c Nonlinear Asymmetric', + hemisphere: 'Right', + expectedPosition: [52602966, -18339402, -5666868] + }, + ], + }, +] + + +describe('explore same region in different templates', () => { + + let iavPage + beforeAll(async () => { + iavPage = new AtlasPage() + await iavPage.init() + }) + + TEST_DATA.forEach(template => { + template.expectedTemplateLabels.forEach(expectedTemplate => { + it (`testing ${template.expectedRegion} exploring at: ${template.name}`, async () => { + if (template.url) { + await iavPage.goto(template.url) + } else { + await iavPage.goto() + await iavPage.selectTitleTemplateParcellation(template.name) + } + const {position} = template + + const tag = await iavPage.getSideNavTag() + await tag.click() + await iavPage.wait(1000) + await iavPage.waitUntilAllChunksLoaded() + await iavPage.cursorMoveToAndClick({ position }) + + await iavPage.showOtherTemplateMenu() + await iavPage.wait(500) + + const otherTemplates = await iavPage.getAllOtherTemplates() + const { name, hemisphere, expectedPosition } = expectedTemplate + const idx = otherTemplates.findIndex(template => { + if (hemisphere) { + if (template.indexOf(hemisphere) < 0) return false + } + return template.indexOf(name) >= 0 + }) + + expect(idx).toBeGreaterThanOrEqual(0) + + await iavPage.clickNthItemAllOtherTemplates(idx) + + await iavPage.wait(500) + await iavPage.waitUntilAllChunksLoaded() + + const navState = await iavPage.getNavigationState() + + // somehow there are slight deviations (1nm in most cases) + // giving a tolerance of 0.1um + for (const idx in navState.position) { + expect( + Math.abs(navState.position[idx] - expectedPosition[idx]) + ).toBeLessThanOrEqual(100) + } + }) + }) + }) +}) diff --git a/e2e/src/util.js b/e2e/src/util.js index 0aafd3928627de8c5f37d372519cf320ff5162a8..0b8e60e8e38d0ef98ce089bd581b83d37ef8a70b 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -23,6 +23,20 @@ async function _getIndexFromArrayOfWebElements(search, webElements) { const regionSearchAriaLabelText = 'Search for any region of interest in the atlas selected' +const vertifyPos = position => { + + if (!position) throw new Error(`cursorGoto: position must be defined!`) + const x = Array.isArray(position) ? position[0] : position.x + const y = Array.isArray(position) ? position[1] : position.y + if (!x) throw new Error(`cursorGoto: position.x or position[0] must be defined`) + if (!y) throw new Error(`cursorGoto: position.y or position[1] must be defined`) + + return { + x, + y + } +} + class WdBase{ constructor() { browser.waitForAngularEnabled(false) @@ -79,12 +93,19 @@ class WdBase{ } async cursorMoveTo({ position }) { - if (!position) throw new Error(`cursorGoto: position must be defined!`) - const x = Array.isArray(position) ? position[0] : position.x - const y = Array.isArray(position)? position[1] : position.y - if (!x) throw new Error(`cursorGoto: position.x or position[0] must be defined`) - if (!y) throw new Error(`cursorGoto: position.y or position[1] must be defined`) + const { x, y } = vertifyPos(position) + return this._driver.actions() + .move() + .move({ + x, + y, + duration: 1000 + }) + .perform() + } + async cursorMoveToAndClick({ position }) { + const { x, y } = vertifyPos(position) return this._driver.actions() .move() .move({ @@ -92,6 +113,7 @@ class WdBase{ y, duration: 1000 }) + .click() .perform() } @@ -461,6 +483,37 @@ class WdLayoutPage extends WdBase{ else await menuItems[index].click() } + // other templates + async showOtherTemplateMenu(){ + await this._driver + .findElement( By.css('[aria-label="Show availability in other reference spaces"]') ) + .click() + } + + _getOtherTemplateMenu(){ + return this._driver + .findElement( By.css('[aria-label="Availability in other reference spaces"]') ) + } + + _getAllOtherTemplates(){ + return this._getOtherTemplateMenu().findElements( By.css('[mat-menu-item]') ) + } + + async getAllOtherTemplates(){ + const els = await this._getAllOtherTemplates() + const returnArr = [] + for (const el of els) { + returnArr.push(await _getTextFromWebElement(el)) + } + return returnArr + } + + async clickNthItemAllOtherTemplates(index){ + const arr = await this._getAllOtherTemplates() + if (!arr[index]) throw new Error(`index out of bound: trying to access ${index} from arr with length ${arr.length}`) + await arr[index].click() + } + _getFavDatasetIcon(){ return this._driver .findElement( By.css('[aria-label="Show pinned datasets"]') ) @@ -745,4 +798,4 @@ class PptrIAVPage{ exports.waitMultiple = process.env.WAIT_ULTIPLE || 1 exports.AtlasPage = WdIavPage -exports.LayoutPage = WdLayoutPage \ No newline at end of file +exports.LayoutPage = WdLayoutPage diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index 455ea30b863b88370210002763fcaebf1319248d..6f567f110d992f896a4cad3ebb3bbe3b28f28a0c 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -106,7 +106,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part // taken care of by effect.ts // landmarksSelected : [], - // navigation : {}, + navigation : action.navigation, dedicatedView : null, } } diff --git a/src/ui/parcellationRegion/region.base.ts b/src/ui/parcellationRegion/region.base.ts index 9f382991a4d07440f9141bda6ce12055b7a6ed7c..cec5a668c416bc6c4012831cb92e3f03635ef671 100644 --- a/src/ui/parcellationRegion/region.base.ts +++ b/src/ui/parcellationRegion/region.base.ts @@ -1,12 +1,14 @@ import {EventEmitter, Input, Output} from "@angular/core"; -import { Store } from "@ngrx/store"; -import {SET_CONNECTIVITY_REGION} from "src/services/state/viewerState.store"; +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"; export class RegionBase { @@ -20,10 +22,40 @@ 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[] = [] + constructor( private store$: Store<IavRootStoreInterface>, ) { + 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'), + distinctUntilChanged(), + ) } public navigateToRegion() { @@ -56,4 +88,65 @@ export class RegionBase { 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) === 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) + } + } + } + }) + }) + } + }) + } + + changeView(index) { + this.closeRegionMenu.emit() + + this.store$.dispatch({ + type : NEWVIEWER, + selectTemplate : this.sameRegionTemplate[index].template, + selectParcellation : this.sameRegionTemplate[index].parcellation, + navigation: {position: this.sameRegionTemplate[index].region.position}, + }) + } + + public getAllRegionsFromParcellation = (regions) => { + for (const region of regions) { + if (region.children && region.children.length) { + this.getAllRegionsFromParcellation(region.children) + } else { + this.parcellationRegions.push(region) + } + } + } + } diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts index d6b84425c69210634101345df9b6d7cc802bed43..6d58ff5be48a65adbae8a72d1200e7dae2e7167c 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts @@ -1,6 +1,7 @@ -import { Component } from "@angular/core"; +import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from "@angular/core"; import { Store } from "@ngrx/store"; - +import {MatMenuTrigger} from "@angular/material/menu"; +import {Subscription} from "rxjs"; import { IavRootStoreInterface } from "src/services/stateStore.service"; import { RegionBase } from '../region.base' @@ -9,11 +10,37 @@ import { RegionBase } from '../region.base' templateUrl: './regionMenu.template.html', styleUrls: ['./regionMenu.style.css'], }) -export class RegionMenuComponent extends RegionBase { +export class RegionMenuComponent extends RegionBase implements OnInit, OnDestroy { + + @ViewChild('additionalActionsPanel', {read: ElementRef}) additionalActionsPanelElement: ElementRef + + private subscriptions: Subscription[] = [] constructor( store$: Store<IavRootStoreInterface>, ) { 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 b3271e18f5c696351a1a65dbcf5b33c76f357bc1..d5234089fd8ef778a51966485d297d9152a84d6d 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -11,7 +11,7 @@ <mat-card-content> {{ region.description }} </mat-card-content> - <mat-card-actions class="d-flex flex-row flex-wrap"> + <div class="d-flex flex-row flex-wrap"> <button mat-button (click)="toggleRegionSelected()" [color]="isSelected ? 'primary': 'basic'"> @@ -46,5 +46,34 @@ </button> </div> </mat-menu> - </mat-card-actions> -</mat-card> \ No newline at end of file + + + <!-- Menu to navigate between template spaces to explore same region --> + <div> + <button mat-button + aria-label="Show availability in other reference spaces" + *ngIf="sameRegionTemplate.length" + [matMenuTriggerFor]="additionalActions"> + <i class="fas fa-brain"></i> + <span> + Change template + </span> + <i class="fas fa-angle-right"></i> + </button> + </div> + + <mat-menu + [aria-label]="'Availability in other reference spaces'" + #additionalActions="matMenu" + xPosition="before" + hasBackdrop="false"> + <div> + <button mat-menu-item *ngFor="let sameRegion of sameRegionTemplate; let i = index" (click)="changeView(i)" class="d-flex"> + <span class="overflow-x-hidden text-truncate"> {{sameRegion.template.name}} </span> + <span *ngIf="sameRegion.hemisphere"> - {{sameRegion.hemisphere}}</span> + </button> + </div> + </mat-menu> + + </div> +</mat-card>