From 359d855e11223a89aaa9233862a0be3309c0f7af Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Wed, 14 Oct 2020 21:17:42 +0200 Subject: [PATCH] bugfix: dataset retrieval on regionId basis --- common/constants.js | 7 + common/util.js | 6 + deploy/datasets/index.js | 9 +- deploy/datasets/query.js | 14 +- deploy/datasets/util.js | 93 ++++++++++- deploy/datasets/util.spec.js | 71 ++++++++- e2e/src/util.js | 148 +++++++++++++++--- .../atlasLayerSelector.component.ts | 9 +- .../atlasLayerSelector.template.html | 2 +- .../databrowserModule/databrowser.service.ts | 25 ++- .../databrowser/databrowser.base.ts | 8 +- .../databrowser/databrowser.template.html | 3 +- .../util/filterDataEntriesByRegion.pipe.ts | 19 ++- .../nehubaContainer.component.ts | 3 +- .../nehubaContainer.template.html | 8 +- .../regionSearch/regionSearch.component.ts | 5 +- .../regionSearch/regionSearch.template.html | 1 + 17 files changed, 375 insertions(+), 56 deletions(-) diff --git a/common/constants.js b/common/constants.js index 3c00fdf2c..d94ab3a30 100644 --- a/common/constants.js +++ b/common/constants.js @@ -17,6 +17,8 @@ DOWNLOAD_PREVIEW_CSV: `Download CSV`, DATASET_FILE_PREVIEW: `Preview of dataset`, PIN_DATASET: 'Toggle pinning dataset', + TEXT_INPUT_SEARCH_REGION: 'Search for any region of interest in the atlas selected', + CLEAR_SELECTED_REGION: 'Clear selected region', // overlay/layout specific SELECT_ATLAS: 'Select a different atlas', @@ -29,6 +31,7 @@ SHOW_FULL_STATUS_PANEL: 'Show full status panel', HIDE_FULL_STATUS_PANEL: 'Hide full status panel', TOGGLE_SIDE_PANEL: 'Toggle side panel', + TOGGLE_ATLAS_LAYER_SELECTOR: 'Toggle atlas layer selector', // sharing module SHARE_BTN: `Share this view`, @@ -52,4 +55,8 @@ // mesh loading status MESH_LOADING_STATUS: 'mesh-loading-status' } + + exports.CONST = { + REGIONAL_FEATURES: 'Regional features' + } })(typeof exports === 'undefined' ? module.exports : exports) diff --git a/common/util.js b/common/util.js index cb1004eef..de5efde33 100644 --- a/common/util.js +++ b/common/util.js @@ -34,6 +34,12 @@ return returnV } + exports.getUniqueRegionId = (template, parcellation, region) => { + const templateId = template['@id'] || template['name'] + const parcId = parcellation['@id'] || parcellation['name'] + return `${templateId}/${parcId}/${region['name']}` + } + exports.getIdObj = getIdObj exports.getIdFromFullId = fullId => { diff --git a/deploy/datasets/index.js b/deploy/datasets/index.js index dc98c0bae..502a4c350 100644 --- a/deploy/datasets/index.js +++ b/deploy/datasets/index.js @@ -2,7 +2,7 @@ const express = require('express') const path = require('path') const fs = require('fs') const datasetsRouter = express.Router() -const { init, getDatasets, getPreview, getDatasetFromId, getExternalSchemaDatasets, getDatasetFileAsZip, getTos, hasPreview } = require('./query') +const { init, getDatasets, getPreview, getDatasetFromId, getExternalSchemaDatasets, getDatasetFileAsZip, getTos, hasPreview, getDatasetsByRegion } = require('./query') const { retry } = require('./util') const url = require('url') const qs = require('querystring') @@ -72,6 +72,13 @@ datasetsRouter.get('/templateNameParcellationName/:templateName/:parcellationNam }) }) +datasetsRouter.get('/byRegion/:regionId', noCacheMiddleWare, async (req, res) => { + const { regionId } = req.params + const { user } = req + const ds = await getDatasetsByRegion({ regionId, user }) + res.status(200).json(ds) +}) + const deprecatedNotice = (_req, res) => { res.status(400).send(`querying datasets with /templateName or /parcellationName separately have been deprecated. Please use /templateNameParcellationName/:templateName/:parcellationName instead`) } diff --git a/deploy/datasets/query.js b/deploy/datasets/query.js index c1a11c28d..091bc7fcd 100644 --- a/deploy/datasets/query.js +++ b/deploy/datasets/query.js @@ -4,7 +4,7 @@ const URL = require('url') const path = require('path') const archiver = require('archiver') const { getPreviewFile, hasPreview } = require('./supplements/previewFile') -const { constants, init: kgQueryUtilInit, getUserKGRequestParam, filterDatasets } = require('./util') +const { constants, init: kgQueryUtilInit, getUserKGRequestParam, filterDatasets, filterDatasetsByRegion } = require('./util') const ibc = require('./importIBS') let cachedData = null @@ -126,6 +126,14 @@ const init = async () => { return await getPublicDs() } +const getDatasetsByRegion = async ({ regionId, user }) => { + /** + * potentially add other sources of datasets + */ + const kgDatasets = await getDs({ user }) + return filterDatasetsByRegion(kgDatasets, regionId) +} + const getDatasets = ({ templateName, parcellationName, user }) => { // Get Local datasets const localDatasets = [ @@ -136,7 +144,6 @@ const getDatasets = ({ templateName, parcellationName, user }) => { // Get all datasets and merge local ones return getDs({ user }) .then(json => { - // console.log(json.map(j=> j.parcellationRegion)) json = [...json, ...localDatasets] return filterDatasets(json, { templateName, parcellationName }) }) @@ -233,6 +240,7 @@ module.exports = { getPreview, hasPreview, getTos, - getExternalSchemaDatasets + getExternalSchemaDatasets, + getDatasetsByRegion, } diff --git a/deploy/datasets/util.js b/deploy/datasets/util.js index eea0c8f74..ae9a088df 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, flattenRegions } = require('../../common/util') +const { getIdFromFullId, retry, flattenRegions, getUniqueRegionId } = require('../../common/util') let getPublicAccessToken @@ -79,6 +79,57 @@ const populateSet = (flattenedRegions, set = new Set()) => { const initPrArray = [] +/** + * regionMap maps schema/id to { parent, children } + */ +const regionMap = new Map() + +const getParseRegion = (template, parcellation) => { + + const getRegionIdFromRegion = region => { + return region.fullId + ? getIdFromFullId(region.fullId) + : getUniqueRegionId(template, parcellation, region) + } + + const parseRegion = (region, parent) => { + const regionId = getRegionIdFromRegion(region) + const { children, relatedAreas } = region + const childrenIds = [ + ...(children || []).map(getRegionIdFromRegion) + ] + + const alternateIds = [ + ...(relatedAreas || []).map(getRegionIdFromRegion) + ] + + regionMap.set(regionId, { + parent, + self: [ regionId, ...alternateIds ], + children: childrenIds + }) + for (const altId of alternateIds) { + regionMap.set(altId, { + parent, + self: [ regionId, ...alternateIds ], + children: childrenIds + }) + } + for (const c of (children || [])) { + parseRegion(c, regionId) + } + } + return parseRegion +} + +const processParc = (t, p) => { + const parseRegion = getParseRegion(t, p) + const { regions } = p + for (const r of regions) { + parseRegion(r) + } +} + let juBrainSet = new Set(), bigbrainCytoSet = new Set() shortBundleSet = new Set(), @@ -93,6 +144,9 @@ initPrArray.push( readConfigFile('bigbrain.json') .then(data => JSON.parse(data)) .then(json => { + for (const p of json.parcellations) { + processParc(json, p) + } const bigbrainCyto = flattenRegions(json.parcellations.find(({ name }) => name === 'Cytoarchitectonic Maps').regions) bigbrainCytoSet = populateSet(bigbrainCyto) }) @@ -103,6 +157,9 @@ initPrArray.push( readConfigFile('MNI152.json') .then(data => JSON.parse(data)) .then(json => { + for (const p of json.parcellations) { + processParc(json, p) + } const longBundle = flattenRegions(json.parcellations.find(({ ['@id']: id }) => id === KG_IDS.PARCELLATIONS.LONG_BUNDLE).regions) const shortBundle = flattenRegions(json.parcellations.find(({ ['@id']: id }) => id === KG_IDS.PARCELLATIONS.SHORT_BUNDLE).regions) const jubrain = flattenRegions(json.parcellations.find(({ ['@id']: id }) => id === KG_IDS.PARCELLATIONS.JULICH_BRAIN).regions) @@ -117,6 +174,9 @@ initPrArray.push( readConfigFile('waxholmRatV2_0.json') .then(data => JSON.parse(data)) .then(json => { + for (const p of json.parcellations) { + processParc(json, p) + } const waxholm3 = flattenRegions(json.parcellations[0].regions) const waxholm2 = flattenRegions(json.parcellations[1].regions) const waxholm1 = flattenRegions(json.parcellations[2].regions) @@ -132,6 +192,9 @@ initPrArray.push( readConfigFile('allenMouse.json') .then(data => JSON.parse(data)) .then(json => { + for (const p of json.parcellations) { + processParc(json, p) + } const flattenedAllen2017 = flattenRegions(json.parcellations[0].regions) allen2017Set = populateSet(flattenedAllen2017) @@ -197,6 +260,33 @@ const datasetBelongToParcellation = ({ parcellationName = null, dataset = {parce ? true : (dataset.parcellationAtlas || []).some(({ name }) => name === parcellationName) +const relatedRegionsCache = new Map() +const traverseRegionMap = regionSchemaId => { + if (relatedRegionsCache.has(regionSchemaId)) return relatedRegionsCache.get(regionSchemaId) + const out = regionMap.get(regionSchemaId) + if (!out) { + return [] + } + const { parent, self, children } = out + + /** + * how to determine how to traverse the tree to determine related regions? + * for now, will traverse towards the parents + * ie, when selecting a leaf node, all nodes up to the root will be considered important + */ + + const relatedSchemaIds = self.concat( + parent ? traverseRegionMap(parent) : [] + ) + relatedRegionsCache.set(regionSchemaId, relatedSchemaIds) + return relatedSchemaIds +} + +const filterDatasetsByRegion = async (datasets = [], regionSchemaId) => { + const allRelevantSchemaSet = new Set(traverseRegionMap(regionSchemaId)) + return datasets.filter(ds => ds['parcellationRegion'].some(pr => allRelevantSchemaSet.has(getIdFromFullId(pr.fullId)))) +} + /** * NB: if changed, also change ~/docs/advanced/dataset.md * @param {*} dataset @@ -309,6 +399,7 @@ module.exports = { datasetBelongToParcellation, datasetRegionExistsInParcellationRegion, datasetBelongsInTemplate, + filterDatasetsByRegion, _getParcellations: async () => { await Promise.all(initPrArray) return { diff --git a/deploy/datasets/util.spec.js b/deploy/datasets/util.spec.js index aadd7283f..802edd662 100644 --- a/deploy/datasets/util.spec.js +++ b/deploy/datasets/util.spec.js @@ -1,4 +1,4 @@ -const { populateSet, datasetBelongToParcellation, retry, datasetBelongsInTemplate, filterDatasets, datasetRegionExistsInParcellationRegion, _getParcellations } = require('./util') +const { populateSet, datasetBelongToParcellation, retry, datasetBelongsInTemplate, filterDatasets, datasetRegionExistsInParcellationRegion, _getParcellations, filterDatasetsByRegion } = require('./util') const { fake } = require('sinon') const { assert, expect } = require('chai') const waxholmv2 = require('./testData/waxholmv2') @@ -289,4 +289,73 @@ describe('datasets/util.js', () => { ]) }) }) + + describe('filterDatasetsByRegion', () => { + + const idHumanArea7ASPL = 'minds/core/parcellationregion/v1.0.0/e26e999f-77ad-4934-9569-8290ed05ebda' + const idHumanArea7A = `minds/core/parcellationregion/v1.0.0/811f4adb-4a7c-45c1-8034-4afa9edf586a` + const idMouseWholeBrain = `minds/core/parcellationregion/v1.0.0/be45bc91-8db5-419f-9471-73a320f44e06` + const idMousePrimaryMotor = `minds/core/parcellationregion/v1.0.0/a07b4390-62db-451b-b211-a45f67c6b18e` + const idMousePrimarySomatosensory = `minds/core/parcellationregion/v1.0.0/f99995b6-a3d0-42be-88c3-eff8a83e60ea` + + const dataHumanArea7ASPL = { + name: 'dataHumanArea7ASPL', + parcellationRegion: [{ + fullId: idHumanArea7ASPL + }] + } + const dataMouseWholeBrain = { + name: 'dataMouseWholeBrain', + parcellationRegion: [{ + fullId: idMouseWholeBrain + }] + } + + const dataMousePrimaryMotor = { + name: 'dataMousePrimaryMotor', + parcellationRegion: [{ + // Mouse Primary motor area (2017) + fullId: idMousePrimaryMotor + }] + } + + describe('human parc regions', () => { + it('should leave in data with matching reference space', async () => { + const result = await filterDatasetsByRegion([dataHumanArea7ASPL], idHumanArea7ASPL) + expect(result).to.deep.equal([dataHumanArea7ASPL]) + }) + it('should filter out data with no matching reference space', async () => { + const result = await filterDatasetsByRegion([dataMouseWholeBrain], idHumanArea7ASPL) + expect(result).to.deep.equal([]) + }) + it('if query region is relatedAreas, should also leave in dataset', async () => { + const result = await filterDatasetsByRegion([dataHumanArea7ASPL], idHumanArea7A) + expect(result).to.deep.equal([dataHumanArea7ASPL]) + }) + }) + + describe('mouse parc regions', () => { + + it('should leave in data with matchin reference space', async () => { + const result = await filterDatasetsByRegion([dataMouseWholeBrain], idMouseWholeBrain) + expect(result).to.deep.equal([dataMouseWholeBrain]) + + const result2 = await filterDatasetsByRegion([dataMousePrimaryMotor], idMousePrimaryMotor) + expect(result2).to.deep.equal([dataMousePrimaryMotor]) + }) + it('should filter out data with no matching referene space', async () => { + const result = await filterDatasetsByRegion([dataHumanArea7ASPL], idMouseWholeBrain) + expect(result).to.deep.equal([]) + }) + it('should filter out data if sup region is selected', async () => { + // example: whole brain is selected, but dataset in primary motor area will be FILTERED OUT + const result = await filterDatasetsByRegion([dataMousePrimaryMotor], idMouseWholeBrain) + expect(result).to.deep.equal([]) + }) + it('should leave in data when sub region is selected', async () => { + const result = await filterDatasetsByRegion([dataMouseWholeBrain], idMousePrimaryMotor) + expect(result).to.deep.equal([dataMouseWholeBrain]) + }) + }) + }) }) diff --git a/e2e/src/util.js b/e2e/src/util.js index 5d9884acf..7b536b72e 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -8,7 +8,7 @@ const { By, Key, until } = require('selenium-webdriver') const CITRUS_LIGHT_URL = `https://unpkg.com/citruslight@0.1.0/citruslight.js` const { polyFillClick } = require('./material-util') -const { ARIA_LABELS } = require('../../common/constants') +const { ARIA_LABELS, CONST } = require('../../common/constants') const { retry } = require('../../common/util') function getActualUrl(url) { @@ -26,8 +26,6 @@ async function _getIndexFromArrayOfWebElements(search, webElements) { return texts.findIndex(text => text.indexOf(search) >= 0) } -const regionSearchAriaLabelText = 'Search for any region of interest in the atlas selected' - const verifyPosition = position => { if (!position) throw new Error(`cursorGoto: position must be defined!`) @@ -550,8 +548,8 @@ class WdLayoutPage extends WdBase{ async _findTitleCard(title) { const titleCards = await this._browser - .findElement( By.tagName('ui-splashscreen') ) - .findElements( By.tagName('mat-card') ) + .findElement( By.css('ui-splashscreen') ) + .findElements( By.css('mat-card') ) const idx = await _getIndexFromArrayOfWebElements(title, titleCards) if (idx >= 0) return titleCards[idx] else throw new Error(`${title} does not fit any titleCards`) @@ -563,17 +561,106 @@ class WdLayoutPage extends WdBase{ } async selectTitleTemplateParcellation(templateName, parcellationName){ - const titleCard = await this._findTitleCard(templateName) - const parcellations = await titleCard - .findElement( By.css('mat-card-content.available-parcellations-container') ) - .findElements( By.tagName('button') ) - const idx = await _getIndexFromArrayOfWebElements( parcellationName, parcellations ) - if (idx >= 0) await parcellations[idx].click() - else throw new Error(`parcellationName ${parcellationName} does not exist`) + throw new Error(`selectTitleTemplateParcellation has been deprecated. use selectAtlasTemplateParcellation`) + } + + /** + * _setAtlasSelectorExpanded + * toggle/set the open state of the atlas-layer-selector element + * If the only argument (flag) is not provided, it will toggle the atlas-layer-selector + * + * Will throw if atlas-layer-selector is not in the DOM + * + * @param {boolean} flag + * + */ + async _setAtlasSelectorExpanded(flag) { + const atlasLayerSelectorEl = this._browser.findElement( + By.css('atlas-layer-selector') + ) + const openedFlag = (await atlasLayerSelectorEl.getAttribute('data-opened')) === 'true' + if (typeof flag === 'undefined' || flag !== openedFlag) { + await atlasLayerSelectorEl.findElement(By.css(`button[aria-label="${ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR}"]`)).click() + } } + async changeTemplate(templateName){ + if (!templateName) throw new Error(`templateName needs to be provided`) + await this._setAtlasSelectorExpanded(true) + await this.wait(1000) + const allTiles = await this._browser + .findElement( By.css('atlas-layer-selector') ) + .findElements( By.css(`mat-grid-tile`) ) + + const idx = await _getIndexFromArrayOfWebElements(templateName, allTiles) + if (idx >= 0) await allTiles[idx].click() + else throw new Error(`#changeTemplate: templateName ${templateName} cannot be found.`) + } + + async changeParc(parcName) { + throw new Error(`changeParc NYI`) + } + + async selectAtlasTemplateParcellation(atlasName, templateName, parcellationName, parcVersion) { + if (!atlasName) throw new Error(`atlasName needs to be provided`) + try { + /** + * if at title screen + */ + await (await this._findTitleCard(atlasName)).click() + } catch (e) { + /** + * if not at title screen + * select from dropdown + */ + } + + if (templateName) { + await this.wait(1000) + await this.waitUntilAllChunksLoaded() + await this.changeTemplate(templateName) + } + + if (parcellationName) { + await this.wait(1000) + await this.waitUntilAllChunksLoaded() + await this.changeParc(parcellationName) + } + + await this._setAtlasSelectorExpanded(false) + } // SideNav + _getSideNavPrimary(){ + return this._browser.findElement( + By.css('mat-drawer[data-mat-drawer-primary-open]') + ) + } + + async _getSideNavPrimaryExpanded(){ + return (await this._getSideNavPrimary() + .getAttribute('data-mat-drawer-primary-open')) === 'true' + } + + _getSideNavSecondary(){ + return this._browser.findElement( + By.css('mat-drawer[data-mat-drawer-secondary-open]') + ) + } + + async _getSideNavSecondaryExpanded(){ + return (await this._getSideNavSecondary() + .getAttribute('data-mat-drawer-secondary-open')) === 'true' + } + + async _setSideNavPrimaryExpanded(flag) { + const matDrawerPrimaryEl = this._getSideNavPrimary() + const openedFlag = await this._getSideNavPrimaryExpanded() + if (typeof flag === 'undefined' || flag !== openedFlag) { + await this._browser.findElement(By.css(`button[aria-label="${ARIA_LABELS.TOGGLE_SIDE_PANEL}"]`)).click() + } + } + _getSideNav() { throw new Error(`side bar no longer exist`) } @@ -787,7 +874,7 @@ class WdIavPage extends WdLayoutPage{ async clearAllSelectedRegions() { const clearAllRegionBtn = await this._browser.findElement( - By.css('[aria-label="Clear all regions"]') + By.css(`[aria-label="${ARIA_LABELS.CLEAR_SELECTED_REGION}"]`) ) await clearAllRegionBtn.click() await this.wait(500) @@ -834,9 +921,15 @@ class WdIavPage extends WdLayoutPage{ else throw new Error(`${title} is not found as one of the dropdown templates`) } - _getSearchRegionInput(){ - return this._getSideNav() - .findElement( By.css(`[aria-label="${regionSearchAriaLabelText}"]`) ) + async _getSearchRegionInput(){ + await this._setSideNavPrimaryExpanded(true) + await this.wait(500) + const secondaryOpen = await this._getSideNavSecondaryExpanded() + if (secondaryOpen) { + return this._getSideNavSecondary().findElement( By.css(`[aria-label="${ARIA_LABELS.TEXT_INPUT_SEARCH_REGION}"]`) ) + } else { + return this._getSideNavPrimary().findElement( By.css(`[aria-label="${ARIA_LABELS.TEXT_INPUT_SEARCH_REGION}"]`) ) + } } async searchRegionWithText(text=''){ @@ -888,8 +981,8 @@ class WdIavPage extends WdLayoutPage{ _getModalityListView(){ return this._browser - .findElement( By.tagName('modality-picker') ) - .findElements( By.tagName('mat-checkbox') ) + .findElement( By.css('modality-picker') ) + .findElements( By.css('mat-checkbox') ) } async getModalities(){ @@ -905,9 +998,22 @@ class WdIavPage extends WdLayoutPage{ _getSingleDatasetListView(){ return this._browser - .findElement( By.tagName('data-browser') ) - .findElement( By.css('div.cdk-virtual-scroll-content-wrapper') ) - .findElements( By.tagName('single-dataset-list-view') ) + .findElement( By.css('data-browser') ) + .findElements( By.css('single-dataset-list-view') ) + } + + _getRegionalFeatureEl(){ + return this._getSideNavSecondary().findElement( + By.css(`mat-expansion-panel[data-mat-expansion-title="${CONST.REGIONAL_FEATURES}"]`) + ) + } + + async _setRegionalFeaturesExpanded(flag){ + const regionFeatureExpEl = this._getRegionalFeatureEl() + const openedFlag = (await regionFeatureExpEl.getAttribute('data-opened')) === 'true' + if (typeof flag === 'undefined' || flag !== openedFlag) { + await regionFeatureExpEl.findElement(By.css(`mat-expansion-panel-header`)).click() + } } async getVisibleDatasets() { diff --git a/src/ui/atlasLayerSelector/atlasLayerSelector.component.ts b/src/ui/atlasLayerSelector/atlasLayerSelector.component.ts index af2558291..fe54b4608 100644 --- a/src/ui/atlasLayerSelector/atlasLayerSelector.component.ts +++ b/src/ui/atlasLayerSelector/atlasLayerSelector.component.ts @@ -1,12 +1,12 @@ -import {Component, OnInit, ViewChildren, QueryList, Output, EventEmitter} from "@angular/core"; +import { Component, OnInit, ViewChildren, QueryList, HostBinding } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { safeFilter } from "src/services/stateStore.service"; -import { distinctUntilChanged, map, withLatestFrom, shareReplay, groupBy, mergeMap, toArray, switchMap, scan, tap, filter } from "rxjs/operators"; +import { distinctUntilChanged, map, withLatestFrom, shareReplay, groupBy, mergeMap, toArray, switchMap, scan, filter } from "rxjs/operators"; import { Observable, Subscription, from, zip, of, combineLatest } from "rxjs"; import { viewerStateSelectTemplateWithId, viewerStateToggleLayer } from "src/services/state/viewerState.store.helper"; import { MatMenuTrigger } from "@angular/material/menu"; import { viewerStateGetSelectedAtlas, viewerStateAtlasLatestParcellationSelector } from "src/services/state/viewerState/selectors"; -import {CLEAR_CONNECTIVITY_REGION, SET_CONNECTIVITY_VISIBLE} from "src/services/state/viewerState.store"; +import { ARIA_LABELS } from 'common/constants' @Component({ selector: 'atlas-layer-selector', @@ -16,6 +16,8 @@ import {CLEAR_CONNECTIVITY_REGION, SET_CONNECTIVITY_VISIBLE} from "src/services/ }) export class AtlasLayerSelector implements OnInit { + public TOGGLE_ATLAS_LAYER_SELECTOR = ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR + @ViewChildren(MatMenuTrigger) matMenuTriggers: QueryList<MatMenuTrigger> public atlas: any @@ -30,6 +32,7 @@ export class AtlasLayerSelector implements OnInit { public selectedAtlas$: Observable<any> private subscriptions: Subscription[] = [] + @HostBinding('attr.data-opened') public selectorExpanded: boolean = false public selectedTemplatePreviewUrl: string = '' diff --git a/src/ui/atlasLayerSelector/atlasLayerSelector.template.html b/src/ui/atlasLayerSelector/atlasLayerSelector.template.html index fa3219cf9..2398853b0 100644 --- a/src/ui/atlasLayerSelector/atlasLayerSelector.template.html +++ b/src/ui/atlasLayerSelector/atlasLayerSelector.template.html @@ -65,7 +65,7 @@ matTooltip="Select layer" mat-mini-fab *ngIf="((availableTemplates$ | async).length > 1) || ((groupedLayers$ | async).length + (nonGroupedLayers$ | async).length > 1)" - aria-label="Layer selector expand button" + [attr.aria-label]="TOGGLE_ATLAS_LAYER_SELECTOR" (click)="selectorExpanded = !selectorExpanded"> <i class="fas fa-layer-group"></i> </button> diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index 3f9c3245c..8d3828f0b 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from "@angular/common/http"; import {ComponentRef, Injectable, OnDestroy} from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { BehaviorSubject, combineLatest, from, fromEvent, Observable, of, Subscription } from "rxjs"; +import { BehaviorSubject, combineLatest, forkJoin, from, fromEvent, Observable, of, Subscription } from "rxjs"; import { catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, tap, withLatestFrom } from "rxjs/operators"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; @@ -12,12 +12,13 @@ import { WidgetUnit } from "src/widget"; import { LoggingService } from "src/logging"; import { SHOW_KG_TOS } from "src/services/state/uiState.store"; import { FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, IavRootStoreInterface, IDataEntry, safeFilter } from "src/services/stateStore.service"; -import { regionFlattener } from "src/util/regionFlattener"; import { DataBrowser } from "./databrowser/databrowser.component"; import { NO_METHODS } from "./util/filterDataEntriesByMethods.pipe"; import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe"; import { datastateActionToggleFav, datastateActionUnfavDataset, datastateActionFavDataset } from "src/services/state/dataState/actions"; +import { getIdFromFullId } from 'common/util' + const noMethodDisplayName = 'No methods described' /** @@ -62,12 +63,10 @@ export class DatabrowserService implements OnDestroy { }) } public createDatabrowser: (arg: {regions: any[], template: any, parcellation: any}) => {dataBrowser: ComponentRef<DataBrowser>, widgetUnit: ComponentRef<WidgetUnit>} - public getDataByRegion: ({regions, parcellation, template}: {regions: any[], parcellation: any, template: any}) => Promise<IDataEntry[]> = ({regions, parcellation, template}) => new Promise((resolve, reject) => { - this.lowLevelQuery(template.name, parcellation.name) - .then(de => this.filterDEByRegion.transform(de, regions, parcellation.regions.map(regionFlattener).reduce((acc, item) => acc.concat(item), []))) - .then(resolve) - .catch(reject) - }) + public getDataByRegion: ({ regions, parcellation, template }: {regions: any[], parcellation: any, template: any}) => Promise<IDataEntry[]> = ({regions, parcellation, template}) => + forkJoin(regions.map(this.getDatasetsByRegion.bind(this))).pipe( + map((arrOfArr: IDataEntry[][]) => arrOfArr.reduce((acc, curr) => acc.concat(curr), [])) + ).toPromise() private filterDEByRegion: FilterDataEntriesByRegion = new FilterDataEntriesByRegion() private dataentries: IDataEntry[] = [] @@ -258,6 +257,16 @@ export class DatabrowserService implements OnDestroy { public fetchingFlag: boolean = false private mostRecentFetchToken: any + private getDatasetsByRegion(region: { fullId: string }){ + return this.http.get<IDataEntry>( + `${this.constantService.backendUrl}datasets/byRegion/${encodeURIComponent(getIdFromFullId(region.fullId))}`, + { + headers: this.constantService.getHttpHeader(), + responseType: 'json' + } + ) + } + private lowLevelQuery(templateName: string, parcellationName: string): Promise<IDataEntry[]> { const encodedTemplateName = encodeURIComponent(templateName) const encodedParcellationName = encodeURIComponent(parcellationName) diff --git a/src/ui/databrowserModule/databrowser/databrowser.base.ts b/src/ui/databrowserModule/databrowser/databrowser.base.ts index ce800d094..04b10b309 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.base.ts +++ b/src/ui/databrowserModule/databrowser/databrowser.base.ts @@ -3,6 +3,7 @@ import { LoggingService } from "src/logging" import { DatabrowserService } from "../singleDataset/singleDataset.base" import { Observable } from "rxjs" import { IDataEntry } from "src/services/stateStore.service" +import { getUniqueRegionId } from 'common/util' export class DatabrowserBase{ @@ -35,17 +36,18 @@ export class DatabrowserBase{ ngOnChanges(){ - + const { regions, parcellation, template } = this this.regions = this.regions.map(r => { /** * TODO to be replaced with properly region UUIDs from KG */ + const uniqueRegionId = getUniqueRegionId(template, parcellation, r) return { - id: `${this.parcellation?.name || 'untitled_parcellation'}/${r.name}`, + fullId: uniqueRegionId, + id: uniqueRegionId, ...r, } }) - const { regions, parcellation, template } = this this.fetchingFlag = true // input may be undefined/null diff --git a/src/ui/databrowserModule/databrowser/databrowser.template.html b/src/ui/databrowserModule/databrowser/databrowser.template.html index 27e4efbc0..0621b5e53 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.template.html +++ b/src/ui/databrowserModule/databrowser/databrowser.template.html @@ -72,7 +72,8 @@ minBufferPx="200" maxBufferPx="400" itemSize="50"> - <div class="virtual-scroll-element overflow-hidden" *cdkVirtualFor="let dataset of filteredDataEntry; trackBy: trackByFn; templateCacheSize: 20; let index = index"> + <div *cdkVirtualFor="let dataset of filteredDataEntry; trackBy: trackByFn; templateCacheSize: 20; let index = index" + class="virtual-scroll-element overflow-hidden"> <!-- divider, show if not first --> <mat-divider *ngIf="index !== 0"></mat-divider> diff --git a/src/ui/databrowserModule/util/filterDataEntriesByRegion.pipe.ts b/src/ui/databrowserModule/util/filterDataEntriesByRegion.pipe.ts index d976cba7a..cea115981 100644 --- a/src/ui/databrowserModule/util/filterDataEntriesByRegion.pipe.ts +++ b/src/ui/databrowserModule/util/filterDataEntriesByRegion.pipe.ts @@ -12,13 +12,17 @@ export const regionsEqual = (r1, r2) => { } const isSubRegion = (high, low) => regionsEqual(high, low) - ? true - : high.children && Array.isArray(high.children) - ? high.children.some(r => isSubRegion(r, low)) - : false + || ( + high.children + && Array.isArray(high.children) + && high.children.some(r => isSubRegion(r, low)) + ) + -const filterSubSelect = (dataEntry, selectedRegions) => - dataEntry.parcellationRegion.some(pr => selectedRegions.some(sr => isSubRegion(pr, sr))) +const filterSubSelect = (dataEntry, selectedRegions) => { + if (dataEntry.name === 'Density measurements of different receptors for Area 7A (SPL) [human, v1.0]') console.log(dataEntry) + return dataEntry.parcellationRegion.some(pr => selectedRegions.some(sr => isSubRegion(pr, sr))) +} @Pipe({ name: 'filterDataEntriesByRegion', @@ -27,8 +31,7 @@ const filterSubSelect = (dataEntry, selectedRegions) => export class FilterDataEntriesByRegion implements PipeTransform { public transform(dataentries: IDataEntry[], selectedRegions: any[], flattenedAllRegions: any[]) { return dataentries && selectedRegions && selectedRegions.length > 0 - ? dataentries - .filter(de => filterSubSelect(de, selectedRegions)) + ? dataentries.filter(de => filterSubSelect(de, selectedRegions)) : dataentries } } diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 6a80e1fb4..7f0e469ac 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -25,7 +25,7 @@ import { AtlasViewerAPIServices, IUserLandmark } from "src/atlasViewer/atlasView import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { compareLandmarksChanged } from "src/util/constants"; import { PureContantService } from "src/util"; -import { ARIA_LABELS, IDS } from 'common/constants' +import { ARIA_LABELS, IDS, CONST } from 'common/constants' import { ngViewerActionSetPerspOctantRemoval, PANELS, ngViewerActionToggleMax, ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "src/services/state/ngViewerState.store.helper"; import { viewerStateSelectRegionWithIdDeprecated, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, viewerStateCustomLandmarkSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector } from 'src/services/state/viewerState.store.helper' import { SwitchDirective } from "src/util/directives/switch.directive"; @@ -146,6 +146,7 @@ const { export class NehubaContainer implements OnInit, OnChanges, OnDestroy { + public CONST = CONST public ARIA_LABEL_ZOOM_IN = ZOOM_IN public ARIA_LABEL_ZOOM_OUT = ZOOM_OUT public ARIA_LABEL_TOGGLE_SIDE_PANEL = TOGGLE_SIDE_PANEL diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index bde6da235..68a7f3450 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -48,7 +48,7 @@ <!-- sidenav-content --> <mat-drawer class="box-shadow-none border-0 pe-none bg-none col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2" mode="side" - [attr.data-mat-drawer-open]="matDrawerMaster.opened" + [attr.data-mat-drawer-primary-open]="matDrawerMaster.opened" [opened]="sideNavMasterSwitch.switchState" [autoFocus]="false" (closedStart)="sideNavSwitch.switchState && matDrawerMinor.close()" @@ -121,7 +121,7 @@ <!-- sidenav-content --> <mat-drawer class="darker-bg iv-custom-comp visible col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2 d-flex flex-column pe-all" mode="push" - [attr.data-mat-drawer-open]="matDrawerMinor.opened" + [attr.data-mat-drawer-secondary-open]="matDrawerMinor.opened" [autoFocus]="false" #matDrawerMinor="matDrawer" (openedChange)="$event && sideNavSwitch.open()" @@ -309,6 +309,8 @@ let-iavNgIf="iavNgIf" let-content="content"> <mat-expansion-panel class="mt-1 mb-1" + [attr.data-opened]="expansionPanel.expanded" + [attr.data-mat-expansion-title]="title" hideToggle *ngIf="iavNgIf" #expansionPanel="matExpansionPanel"> @@ -389,7 +391,7 @@ </ng-template> <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: 'Regional features', + title: CONST.REGIONAL_FEATURES, desc: iavDbDirective?.dataentries?.length, iconClass: 'fas fa-database', iconTooltip: iavDbDirective?.dataentries?.length | regionAccordionTooltipTextPipe : 'regionalFeatures', diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index 2d6acd658..43fbe5722 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -11,6 +11,7 @@ import { MatDialog } from "@angular/material/dialog"; import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; import { PureContantService } from "src/util"; import { viewerStateToggleRegionSelect, viewerStateNavigateToRegion, viewerStateSetSelectedRegions, viewerStateSetSelectedRegionsWithIds } from "src/services/state/viewerState.store.helper"; +import { ARIA_LABELS } from 'common/constants' const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase()) || (region.relatedAreas && region.relatedAreas.some(relatedArea => relatedArea.name && relatedArea.name.toLowerCase().includes(searchTerm.toLowerCase()))) @@ -36,7 +37,9 @@ export class RegionTextSearchAutocomplete { public compareFn = compareFn - @Input() public ariaLabel: string = `Search for any region of interest in the atlas selected` + public CLEAR_SELECTED_REGION = ARIA_LABELS.CLEAR_SELECTED_REGION + + @Input() public ariaLabel: string = ARIA_LABELS.TEXT_INPUT_SEARCH_REGION @Input() public showBadge: boolean = false @Input() public showAutoComplete: boolean = true diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.template.html b/src/ui/viewerStateController/regionSearch/regionSearch.template.html index 42ddf8f77..b2f543412 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.template.html +++ b/src/ui/viewerStateController/regionSearch/regionSearch.template.html @@ -17,6 +17,7 @@ <!-- close region selection --> <button *ngIf="(regionsSelected$ | async)?.length > 0" mat-icon-button + [attr.aria-label]="CLEAR_SELECTED_REGION" (click)="optionSelected()" matSuffix> <i class="fas fa-times"></i> -- GitLab