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