diff --git a/docs/releases/v2.1.0.md b/docs/releases/v2.1.0.md index 226989eb2b9b3b5844a7a59ef53b5f5bf8db831e..5dcb07c16b30253af20c8ab0b0319ce071ab1362 100644 --- a/docs/releases/v2.1.0.md +++ b/docs/releases/v2.1.0.md @@ -1,5 +1,6 @@ # v2.1.0 New features: +- Region search also searches for relatedAreas - updating the querying logic of datasets - connectivity browsing for JuBrain atlas \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index b8da64e12f55949a4fcee0c1f13a838555ec6b9c..c68c60bf953f2cd2951278dab547dbd3a17d5460 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { Observable } from "rxjs"; -import { distinctUntilChanged, map } from "rxjs/operators"; +import { distinctUntilChanged, map, filter } from "rxjs/operators"; import { DialogService } from "src/services/dialogService.service"; import { LoggingService } from "src/services/logging.service"; import { getLabelIndexMap, getMultiNgIdsRegionsLabelIndexMap, IavRootStoreInterface, safeFilter } from "src/services/stateStore.service"; @@ -140,7 +140,10 @@ export class AtlasViewerAPIServices { private init() { this.loadedTemplates$.subscribe(templates => this.interactiveViewer.metadata.loadedTemplates = templates) - this.selectParcellation$.subscribe(parcellation => { + this.selectParcellation$.pipe( + filter(p => !!p && p.regions), + distinctUntilChanged() + ).subscribe(parcellation => { this.interactiveViewer.metadata.regionsLabelIndexMap = getLabelIndexMap(parcellation.regions) this.interactiveViewer.metadata.layersRegionLabelIndexMap = getMultiNgIdsRegionsLabelIndexMap(parcellation) }) diff --git a/src/services/stateStore.service.spec.ts b/src/services/stateStore.service.spec.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..49b43b786229527a1330d76916300a378507444a 100644 --- a/src/services/stateStore.service.spec.ts +++ b/src/services/stateStore.service.spec.ts @@ -0,0 +1,146 @@ +import { getMultiNgIdsRegionsLabelIndexMap } from './stateStore.service' + +const getRandomDummyData = () => Math.round(Math.random() * 1000).toString(16) + +const region1 = { + name: 'region 1', + labelIndex: 15, + ngId: 'left', + dummydata: getRandomDummyData() +} +const region2 = { + name: 'region 2', + labelIndex: 16, + ngId: 'right', + dummydata: getRandomDummyData() +} +const region3 = { + name: 'region 3', + labelIndex: 17, + ngId: 'right', + dummydata: getRandomDummyData() +} + +const dummyParcellationWithNgId = { + name: 'dummy parcellation name', + regions: [ + region1, + region2, + region3 + ] +} + +const dummyParcellationWithoutNgId = { + name: 'dummy parcellation name', + ngId: 'toplevel', + regions: [ + region1, + region2, + region3 + ].map(({ ngId, ...rest }) => { + return { + ...rest, + ...(ngId === 'left' ? { ngId } : {}) + } + }) +} + +describe('stateStore.service.ts', () => { + describe('getMultiNgIdsRegionsLabelIndexMap', () => { + describe('should not mutate original regions', () => { + + }) + + describe('should populate map properly', () => { + const map = getMultiNgIdsRegionsLabelIndexMap(dummyParcellationWithNgId) + it('populated map should have 2 top level', () => { + expect(map.size).toBe(2) + }) + + it('should container left and right top level', () => { + expect(map.get('left')).toBeTruthy() + expect(map.get('right')).toBeTruthy() + }) + + it('left top level should have 1 member', () => { + const leftMap = map.get('left') + expect(leftMap.size).toBe(1) + }) + + it('left top level should map 15 => region1', () => { + const leftMap = map.get('left') + expect(leftMap.get(15)).toEqual(region1) + }) + + it('right top level should have 2 member', () => { + const rightMap = map.get('right') + expect(rightMap.size).toBe(2) + }) + + it('right top level should map 16 => region2, 17 => region3', () => { + const rightMap = map.get('right') + expect(rightMap.get(16)).toEqual(region2) + expect(rightMap.get(17)).toEqual(region3) + }) + }) + + describe('should allow inheritance of ngId', () => { + + const map = getMultiNgIdsRegionsLabelIndexMap(dummyParcellationWithoutNgId) + it('populated map should have 2 top level', () => { + expect(map.size).toBe(2) + }) + + it('should container left and right top level', () => { + expect(map.get('left')).toBeTruthy() + expect(map.get('toplevel')).toBeTruthy() + }) + + it('left top level should have 1 member', () => { + const leftMap = map.get('left') + expect(leftMap.size).toBe(1) + }) + + it('left top level should map 15 => region1', () => { + const leftMap = map.get('left') + console.log(leftMap.get(15), region1) + expect(leftMap.get(15)).toEqual(region1) + }) + + it('toplevel top level should have 2 member', () => { + const toplevelMap = map.get('toplevel') + expect(toplevelMap.size).toBe(2) + }) + + it('toplevel top level should map 16 => region2, 17 => region3', () => { + const toplevelMap = map.get('toplevel') + expect(toplevelMap.get(16).dummydata).toEqual(region2.dummydata) + expect(toplevelMap.get(17).dummydata).toEqual(region3.dummydata) + }) + }) + + describe('should allow inheritance of attr when specified', () => { + const attr = { + dummyattr: 'default dummy attr' + } + const map = getMultiNgIdsRegionsLabelIndexMap({ + ...dummyParcellationWithNgId, + dummyattr: 'p dummy attr' + }, attr) + it('every region should have dummyattr set properly', () => { + let regions = [] + for (const [ _key1, mMap ] of Array.from(map)) { + for (const [_key2, rs] of Array.from(mMap)) { + regions = [...regions, rs] + } + } + + for (const r of regions) { + console.log(r) + expect(r.dummyattr).toEqual('p dummy attr') + } + }) + + }) + }) +}) \ No newline at end of file diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index 0890905a4e97c6ce86ff0473e3f3cd5feb574baa..dadde7dc67e49a1d0d041f611a953aa825f432cc 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -73,9 +73,11 @@ export function getNgIdLabelIndexFromRegion({ region }) { throw new Error(`ngId: ${ngId} or labelIndex: ${labelIndex} not defined`) } -export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}): Map<string, Map<number, any>> { +export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}, inheritAttrsOpt: any = { ngId: 'root' }): Map<string, Map<number, any>> { const map: Map<string, Map<number, any>> = new Map() - const { ngId = 'root'} = parcellation + + const inheritAttrs = Object.keys(inheritAttrsOpt) + if (inheritAttrs.indexOf('children') >=0 ) throw new Error(`children attr cannot be inherited`) const processRegion = (region: any) => { const { ngId: rNgId } = region @@ -91,21 +93,27 @@ export function getMultiNgIdsRegionsLabelIndexMap(parcellation: any = {}): Map<s } } - if (region.children && region.children.forEach) { - region.children.forEach(child => { - processRegion({ - ngId: rNgId, - ...child, - }) - }) + if (region.children && Array.isArray(region.children)) { + for (const r of region.children) { + const copiedRegion = { ...r } + for (const attr of inheritAttrs){ + copiedRegion[attr] = copiedRegion[attr] || region[attr] || parcellation[attr] + } + processRegion(copiedRegion) + } } } - if (parcellation && parcellation.regions && parcellation.regions.forEach) { - parcellation.regions.forEach(r => processRegion({ - ngId, - ...r, - })) + if (!parcellation) throw new Error(`parcellation needs to be defined`) + if (!parcellation.regions) throw new Error(`parcellation.regions needs to be defined`) + if (!Array.isArray(parcellation.regions)) throw new Error(`parcellation.regions needs to be an array`) + + for (const region of parcellation.regions){ + const copiedregion = { ...region } + for (const attr of inheritAttrs){ + copiedregion[attr] = copiedregion[attr] || parcellation[attr] + } + processRegion(copiedregion) } return map diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts index f57e80fc4799d492afc0baa81f0e173b6080fbb3..2534f5548129666f3201ea821e1319c6b2bbc572 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts @@ -21,7 +21,15 @@ const getDisplayTreeNode: (searchTerm: string, selectedRegions: any[]) => (item: : `<span class="cursor-default regionNotSelected">${insertHighlight(name, searchTerm)}</span>` + (status ? ` <span class="text-muted">(${insertHighlight(status, searchTerm)})</span>` : ``) } -const getFilterTreeBySearch = (pipe: FilterNameBySearch, searchTerm: string) => (node: any) => pipe.transform([node.name, node.status], searchTerm) +const getFilterTreeBySearch = (pipe: FilterNameBySearch, searchTerm: string) => + (node: any) => { + const searchFields = [ + node.name, + node.status, + ...(node.relatedAreas ? node.relatedAreas : []) + ] + return pipe.transform(searchFields, searchTerm) + } @Component({ selector: 'region-hierarchy', diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index fc4f852a2a2afb50e09ded6ee50618383b300d7a..01fb37ceb42e8522275cf26f86377ad180c18bfa 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -9,8 +9,11 @@ import { VIEWER_STATE_ACTION_TYPES } from "src/services/effect/effect"; import { ADD_TO_REGIONS_SELECTION_WITH_IDS, CHANGE_NAVIGATION, SELECT_REGIONS } from "src/services/state/viewerState.store"; import { generateLabelIndexId, getMultiNgIdsRegionsLabelIndexMap, IavRootStoreInterface } from "src/services/stateStore.service"; import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerState.base"; +import { LoggingService } from "src/services/logging.service"; const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase()) + || (region.relatedAreas && region.relatedAreas.some(relatedArea => relatedArea.toLowerCase().includes(searchTerm.toLowerCase()))) + const compareFn = (it, item) => it.name === item.name @Component({ @@ -40,6 +43,7 @@ export class RegionTextSearchAutocomplete { private store$: Store<IavRootStoreInterface>, private dialog: MatDialog, private constantService: AtlasViewerConstantsServices, + private log: LoggingService ) { this.useMobileUI$ = this.constantService.useMobileUI$ @@ -52,21 +56,26 @@ export class RegionTextSearchAutocomplete { this.regionsWithLabelIndex$ = viewerState$.pipe( select('parcellationSelected'), distinctUntilChanged(), - filter(p => !!p), + filter(p => !!p && p.regions), map(parcellationSelected => { - const returnArray = [] - const ngIdMap = getMultiNgIdsRegionsLabelIndexMap(parcellationSelected) - for (const [ngId, labelIndexMap] of ngIdMap) { - for (const [labelIndex, region] of labelIndexMap) { - returnArray.push({ - ...region, - ngId, - labelIndex, - labelIndexId: generateLabelIndexId({ ngId, labelIndex }), - }) + try { + const returnArray = [] + const ngIdMap = getMultiNgIdsRegionsLabelIndexMap(parcellationSelected, { ngId: 'root', relatedAreas: [] }) + for (const [ngId, labelIndexMap] of ngIdMap) { + for (const [labelIndex, region] of labelIndexMap) { + returnArray.push({ + ...region, + ngId, + labelIndex, + labelIndexId: generateLabelIndexId({ ngId, labelIndex }), + }) + } } + return returnArray + } catch (e) { + this.log.warn(`getMultiNgIdsRegionsLabelIndexMap error`, e) + return [] } - return returnArray }), shareReplay(1), )