Skip to content
Snippets Groups Projects
Commit cfe287ec authored by Xiao Gui's avatar Xiao Gui
Browse files

feat: databrowser redesign

parent 63833c36
No related branches found
No related tags found
No related merge requests found
Showing
with 509 additions and 475 deletions
import { Component, HostBinding, ViewChild, ViewContainerRef, OnDestroy, ElementRef, OnInit, HostListener, TemplateRef } from "@angular/core";
import { Component, HostBinding, ViewChild, ViewContainerRef, OnDestroy, OnInit, TemplateRef, Injector } from "@angular/core";
import { Store, select } from "@ngrx/store";
import { ViewerStateInterface, isDefined, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA, TOGGLE_SIDE_PANEL, safeFilter } from "../services/stateStore.service";
import { Observable, Subscription, combineLatest } from "rxjs";
......@@ -15,6 +15,8 @@ import { AtlasViewerAPIServices } from "./atlasViewer.apiService.service";
import '../res/css/extra_styles.css'
import { NehubaContainer } from "../ui/nehubaContainer/nehubaContainer.component";
import { colorAnimation } from "./atlasViewer.animation"
import { FixedMouseContextualContainerDirective } from "src/util/directives/FixedMouseContextualContainerDirective.directive";
import { DatabrowserService } from "src/ui/databrowserModule/databrowser.service";
@Component({
selector: 'atlas-viewer',
......@@ -29,7 +31,6 @@ import { colorAnimation } from "./atlasViewer.animation"
export class AtlasViewer implements OnDestroy, OnInit {
@ViewChild('databrowser', { read: ElementRef }) databrowser: ElementRef
@ViewChild('floatingMouseContextualContainer', { read: ViewContainerRef }) floatingMouseContextualContainer: ViewContainerRef
@ViewChild('helpComponent', {read: TemplateRef}) helpComponent : TemplateRef<any>
@ViewChild('viewerConfigComponent', {read: TemplateRef}) viewerConfigComponent : TemplateRef<any>
......@@ -38,6 +39,7 @@ export class AtlasViewer implements OnDestroy, OnInit {
@ViewChild(NehubaContainer) nehubaContainer: NehubaContainer
@ViewChild(FixedMouseContextualContainerDirective) rClContextualMenu: FixedMouseContextualContainerDirective
/**
* required for styling of all child components
*/
......@@ -49,12 +51,14 @@ export class AtlasViewer implements OnDestroy, OnInit {
public sidePanelView$: Observable<string|null>
private newViewer$: Observable<any>
public selectedRegions$: Observable<any[]>
public selectedPOI$ : Observable<any[]>
private showHelp$: Observable<any>
private showConfig$: Observable<any>
public dedicatedView$: Observable<string | null>
public onhoverSegment$: Observable<string>
public onhoverSegmentForFixed$: Observable<string>
public onhoverLandmark$ : Observable<string | null>
private subscriptions: Subscription[] = []
......@@ -77,7 +81,9 @@ export class AtlasViewer implements OnDestroy, OnInit {
private constantsService: AtlasViewerConstantsServices,
public urlService: AtlasViewerURLService,
public apiService: AtlasViewerAPIServices,
private modalService: BsModalService
private modalService: BsModalService,
private databrowserService: DatabrowserService,
private injector: Injector
) {
this.ngLayerNames$ = this.store.pipe(
select('viewerState'),
......@@ -101,13 +107,15 @@ export class AtlasViewer implements OnDestroy, OnInit {
debounceTime(170)
)
this.selectedRegions$ = this.store.pipe(
select('viewerState'),
filter(state=>isDefined(state)&&isDefined(state.regionsSelected)),
map(state=>state.regionsSelected),
distinctUntilChanged()
)
this.selectedPOI$ = combineLatest(
this.store.pipe(
select('viewerState'),
filter(state=>isDefined(state)&&isDefined(state.regionsSelected)),
map(state=>state.regionsSelected),
distinctUntilChanged()
),
this.selectedRegions$,
this.store.pipe(
select('viewerState'),
filter(state => isDefined(state) && isDefined(state.landmarksSelected)),
......@@ -158,22 +166,42 @@ export class AtlasViewer implements OnDestroy, OnInit {
this.store.pipe(
select('uiState'),
/* cannot filter by state, as the template expects a default value, or it will throw ExpressionChangedAfterItHasBeenCheckedError */
map(state => isDefined(state) ?
state.mouseOverSegment ?
state.mouseOverSegment.constructor === Number ?
state.mouseOverSegment.toString() :
state.mouseOverSegment.name :
null :
null),
distinctUntilChanged()
map(state => state
&& state.mouseOverSegment
&& (isNaN(state.mouseOverSegment)
? state.mouseOverSegment
: state.mouseOverSegment.toString())),
distinctUntilChanged((o, n) => o === n || (o && n && o.name && n.name && o.name === n.name))
),
this.onhoverLandmark$
).pipe(
map(([segment, onhoverLandmark]) => onhoverLandmark ? null : segment )
)
this.onhoverSegmentForFixed$ = this.onhoverSegment$.pipe(
filter(() => !this.rClContextualMenu || !this.rClContextualMenu.isShown )
)
this.selectedParcellation$ = this.store.pipe(
select('viewerState'),
safeFilter('parcellationSelected'),
map(state=>state.parcellationSelected),
distinctUntilChanged(),
)
this.subscriptions.push(
this.selectedParcellation$.subscribe(parcellation => this.selectedParcellation = parcellation)
)
this.subscriptions.push(
this.newViewer$.subscribe(template => this.selectedTemplate = template)
)
}
private selectedParcellation$: Observable<any>
private selectedParcellation: any
ngOnInit() {
this.meetsRequirement = this.meetsRequirements()
......@@ -305,6 +333,15 @@ export class AtlasViewer implements OnDestroy, OnInit {
this.nehubaContainer.nehubaViewer.nehubaViewer.redraw()
}
nehubaClickHandler(event:MouseEvent){
if (!this.rClContextualMenu) return
this.rClContextualMenu.mousePos = [
event.clientX,
event.clientY
]
this.rClContextualMenu.show()
}
toggleSidePanel(panelName:string){
this.store.dispatch({
type : TOGGLE_SIDE_PANEL,
......@@ -312,6 +349,12 @@ export class AtlasViewer implements OnDestroy, OnInit {
})
}
private selectedTemplate: any
searchRegion(regions:any[]){
this.rClContextualMenu.hide()
this.databrowserService.createDatabrowser({ regions, parcellation: this.selectedParcellation, template: this.selectedTemplate })
}
@HostBinding('attr.version')
public _version : string = VERSION
......
......@@ -79,6 +79,10 @@ export class AtlasViewerDataService implements OnDestroy{
))
}
public searchDataset(){
}
/* all units in mm */
public spatialSearch(obj:any){
const {center,searchWidth,templateSpace,pageNo} = obj
......
......@@ -127,4 +127,9 @@ markdown-dom[minReqMd]
{
margin: 1em;
display:block;
}
[fixedMouseContextualContainerDirective]
{
width: 15rem;
}
\ No newline at end of file
<div *ngIf = "meetsRequirement" class = "atlas-container" helpdirective>
<ui-nehuba-container>
<ui-nehuba-container (contextmenu)="nehubaClickHandler($event)">
</ui-nehuba-container>
<div bannerWrapper>
......@@ -16,19 +16,53 @@
<div floatingContainerDirective>
</div>
<panel-component class="shadow" fixedMouseContextualContainerDirective #rClContextMenu>
<div heading>
<h5 class="pe-all p-2 m-0">
What's here?
</h5>
</div>
<div body>
<div
*ngIf="onhoverSegmentForFixed$ | async; let onhoverSegmentFixed"
(click)="searchRegion([onhoverSegmentFixed])"
class="ws-no-wrap text-left pe-all btn btn-sm btn-secondary btn-block">
Search KG for {{ onhoverSegmentFixed.name }}
</div>
<div
*ngIf="selectedRegions$ | async; let selectedRegions"
(click)="searchRegion(selectedRegions)"
class="ws-no-wrap text-left pe-all mt-0 btn btn-sm btn-secondary btn-block">
Search KG for {{ selectedRegions && selectedRegions.length }} selected regions
</div>
<ng-template #noRegionSelected>
<div
(click)="searchRegion()"
class="ws-no-wrap text-left pe-all mt-0 btn btn-sm btn-secondary btn-block">
No region selected. Search KG for all datasets in this template space.
</div>
</ng-template>
</div>
</panel-component>
<div floatingMouseContextualContainer floatingMouseContextualContainerDirective>
<div
*ngIf = "onhoverLandmark$ | async"
*ngIf="onhoverLandmark$ | async"
contextualBlock>
{{ onhoverLandmark$ | async }} <i><small class = "mute-text">{{ toggleMessage }}</small></i>
</div>
<div
*ngIf = "onhoverSegment$ | async as onhoverSegment "
*ngIf="onhoverSegment$ | async; let onhoverSegment"
contextualBlock>
{{ onhoverSegment }} <i><small class = "mute-text">{{ toggleMessage }}</small></i>
{{ onhoverSegment.name }} <i><small class = "mute-text">{{ toggleMessage }}</small></i>
</div>
<!-- TODO Potentially implementing plugin contextual info -->
</div>
<div toastContainer>
<div toastDirective>
</div>
......
......@@ -12,7 +12,7 @@
<div #emptyspan emptyspan>.</div>
<div title>
<div *ngIf="!titleHTML">
{{ title }}
{{ title }}
</div>
<div [innerHTML]="titleHTML" *ngIf="titleHTML">
......
......@@ -5,6 +5,13 @@
#dropdownToggle
dropdownToggle>
</span>
<!-- needed to ensure dropdown width matches -->
<ul class="m-0 h-0 o-h">
<li *ngFor="let item of inputArray">
{{ listDisplay(item) }}
</li>
</ul>
<radio-list
[ulClass]="'dropdown-menu'"
(itemSelected)="itemSelected.emit($event)"
......
......@@ -26,11 +26,6 @@
(click) = "$event.stopPropagation(); toggleCollapse(flattenedItem)" >
<i [ngClass] = "isCollapsed(flattenedItem) ? '' : 'r-270'" class="fas fa-chevron-down"></i>
</span>
<ng-template #noChildren>
<i class="fas fa-none">
</i>
</ng-template>
<span
(click) = "treeNodeClick.emit({event:$event,inputItem:flattenedItem})"
class = "render-node-text"
......@@ -41,4 +36,10 @@
<div [attr.clusterindex] = "index" flatTreeEnd #flatTreeEnd>
</div>
</div>
\ No newline at end of file
</div>
<ng-template #noChildren>
<i class="fas fa-none">
</i>
</ng-template>
\ No newline at end of file
......@@ -9,7 +9,7 @@
.pill-title
{
flex: 0 0 auto;
flex: 1 1 0;
}
.pill-close
......
......@@ -2,7 +2,7 @@
[ngStyle]="containerStyle"
class="pill-container">
<span
class="pill-title">
class="text-truncate pill-title">
{{ title }}
</span>
<div
......
......@@ -33,6 +33,8 @@ import { PluginFactoryDirective } from "./util/directives/pluginFactory.directiv
import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive";
import { AuthService } from "./services/auth.service";
import { ViewerConfiguration } from "./services/state/viewerConfig.store";
import { FixedMouseContextualContainerDirective } from "./util/directives/FixedMouseContextualContainerDirective.directive";
import { DatabrowserService } from "./ui/databrowserModule/databrowser.service";
@NgModule({
imports : [
......@@ -74,6 +76,7 @@ import { ViewerConfiguration } from "./services/state/viewerConfig.store";
FloatingContainerDirective,
PluginFactoryDirective,
FloatingMouseContextualContainerDirective,
FixedMouseContextualContainerDirective,
/* pipes */
GetNamesPipe,
......@@ -94,7 +97,13 @@ import { ViewerConfiguration } from "./services/state/viewerConfig.store";
AtlasViewerAPIServices,
ToastService,
AtlasWorkerService,
AuthService
AuthService,
/**
* TODO
* once nehubacontainer is separated into viewer + overlay, migrate to nehubaContainer module
*/
DatabrowserService
],
bootstrap : [
AtlasViewer
......@@ -105,7 +114,14 @@ export class MainModule{
constructor(
authServce: AuthService,
store: Store<ViewerConfiguration>
store: Store<ViewerConfiguration>,
/**
* instantiate singleton
* allow for pre fetching of dataentry
* TODO only fetch when traffic is idle
*/
dbSerivce: DatabrowserService
){
authServce.authReloadState()
store.pipe(
......
......@@ -247,7 +247,66 @@ markdown-dom pre code
color: rgba(255, 255, 255, 1.0);
}
.darktheme.popover
{
background-color:rgba(0, 0, 0, 0.8);
}
.darktheme.popover .popover-body
{
color:white;
}
.darktheme.popover.popover-bottom>.arrow::after
{
border-bottom-color: rgba(0, 0, 0, 0.8);
}
.r-90
{
transform: rotate(90deg)!important;
}
.ws-no-wrap
{
white-space: nowrap!important;
}
.ws-initial
{
white-space: initial!important;
}
.mw-100
{
max-width: 100%!important;
}
.mw-50
{
max-width: 50%!important;
}
.mw-60
{
max-width: 60%!important;
}
.pe-all
{
pointer-events: all;
}
.t-a-ease-500
{
transition: all ease 500ms;
}
.o-h
{
overflow:hidden;
}
.h-0
{
height: 0px;
}
\ No newline at end of file
......@@ -4,10 +4,7 @@ import { DataBrowser } from "./databrowser/databrowser.component";
import { DatasetViewerComponent } from "./datasetViewer/datasetViewer.component";
import { ComponentsModule } from "src/components/components.module";
import { ModalityPicker } from "./modalityPicker/modalityPicker.component";
import { RegionHierarchy } from "./regionHierachy/regionHierarchy.component";
import { FilterNameBySearch } from "./util/filterNameBySearch.pipe";
import { FormsModule } from "@angular/forms";
import { DatabrowserService } from "./databrowser.service";
import { PathToNestedChildren } from "./util/pathToNestedChildren.pipe";
import { CopyPropertyPipe } from "./util/copyProperty.pipe";
import { FilterDataEntriesbyMethods } from "./util/filterDataEntriesByMethods.pipe";
......@@ -21,6 +18,8 @@ import { LineChart } from "./fileviewer/line/line.chart.component";
import { DedicatedViewer } from "./fileviewer/dedicated/dedicated.component";
import { Chart } from 'chart.js'
import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
import { PopoverModule } from "ngx-bootstrap/popover";
import { UtilModule } from "src/util/util.module";
@NgModule({
imports:[
......@@ -28,13 +27,14 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta
CommonModule,
ComponentsModule,
FormsModule,
TooltipModule.forRoot()
UtilModule,
TooltipModule.forRoot(),
PopoverModule.forRoot()
],
declarations: [
DataBrowser,
DatasetViewerComponent,
ModalityPicker,
RegionHierarchy,
PreviewComponent,
FileViewer,
RadarChart,
......@@ -44,7 +44,6 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta
/**
* pipes
*/
FilterNameBySearch,
PathToNestedChildren,
CopyPropertyPipe,
FilterDataEntriesbyMethods,
......@@ -55,10 +54,7 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta
],
entryComponents:[
DataBrowser
],
providers:[
DatabrowserService
],
]
/**
* shouldn't need bootstrap, so no need for browser module
*/
......
import { Injectable, ComponentRef, OnDestroy } from "@angular/core";
import { Store, select } from "@ngrx/store";
import { Injectable, OnDestroy } from "@angular/core";
import { Subscription, Observable, combineLatest, BehaviorSubject, fromEvent } from "rxjs";
import { ViewerConfiguration } from "src/services/state/viewerConfig.store";
import { SELECT_REGIONS, extractLabelIdx, CHANGE_NAVIGATION, DataEntry, File, safeFilter, isDefined, getLabelIndexMap, FETCHED_DATAENTRIES, SELECT_PARCELLATION, ADD_NG_LAYER, NgViewerStateInterface, REMOVE_NG_LAYER } from "src/services/stateStore.service";
import { WidgetServices } from "src/atlasViewer/widgetUnit/widgetService.service";
import { map, distinctUntilChanged, filter, debounceTime } from "rxjs/operators";
import { Subscription, combineLatest, Observable, BehaviorSubject, fromEvent } from "rxjs";
import { select, Store } from "@ngrx/store";
import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
import { ADD_NG_LAYER, REMOVE_NG_LAYER, DataEntry, safeFilter, FETCHED_DATAENTRIES } from "src/services/stateStore.service";
import { map, distinctUntilChanged, debounceTime, filter, tap } from "rxjs/operators";
import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe";
export function temporaryFilterDataentryName(name: string):string{
return /autoradiography/.test(name)
......@@ -18,36 +18,33 @@ function generateToken() {
return Date.now().toString()
}
@Injectable()
@Injectable({
providedIn: 'root'
})
export class DatabrowserService implements OnDestroy{
private subscriptions: Subscription[] = []
public selectedParcellation: any
public selectedTemplate: any
public selectedRegions$: Observable<any[]>
public selectedRegions: any[] = []
public rebuiltSelectedRegions: any[] = []
public rebuiltSomeSelectedRegions: any[] = []
public darktheme: boolean = false
public regionsLabelIndexMap: Map<number, any> = new Map()
public fetchingFlag: boolean = false
public fetchedFlag: boolean = false
public fetchError: string
private mostRecentFetchToken: any
public createDatabrowser: (arg:{regions:any[], template:any, parcellation:any}) => void
public getDataByRegion: ({regions, parcellation, template}:{regions:any[], parcellation:any, template: any}) => Promise<DataEntry[]> = ({regions, parcellation, template}) => new Promise((resolve, reject) => {
this.lowLevelQuery(template.name, parcellation.name)
.then(de => this.filterDEByRegion.transform(de, regions))
.then(resolve)
.catch(reject)
})
public fetchedDataEntries$: Observable<DataEntry[]>
private filterDEByRegion: FilterDataEntriesByRegion = new FilterDataEntriesByRegion()
private dataentries: DataEntry[] = []
private fetchDataStatus$: Observable<any>
private subscriptions: Subscription[] = []
public fetchDataObservable$: Observable<any>
public manualFetchDataset$: BehaviorSubject<null> = new BehaviorSubject(null)
constructor(
private workerService: AtlasWorkerService,
private constantService: AtlasViewerConstantsServices,
private store: Store<ViewerConfiguration>,
private widgetService: WidgetServices,
private workerService: AtlasWorkerService
private store: Store<ViewerConfiguration>
){
this.subscriptions.push(
......@@ -57,54 +54,22 @@ export class DatabrowserService implements OnDestroy{
this.ngLayers = new Set(layersInterface.layers.map(l => l.source.replace(/^nifti\:\/\//, ''))))
)
this.selectedRegions$ = this.store.pipe(
select('viewerState'),
filter(state => isDefined(state) && isDefined(state.regionsSelected)),
map(state => state.regionsSelected)
)
/**
* This service is provided on init. Angular does not provide
* lazy loading of module unless for routing
*/
this.subscriptions.push(
this.store.pipe(
select('viewerState'),
safeFilter('parcellationSelected'),
map(({ parcellationSelected, templateSelected }) => {
return {
parcellationSelected,
templateSelected
}
}),
distinctUntilChanged()
).subscribe(({ parcellationSelected, templateSelected }) => {
this.selectedParcellation = parcellationSelected
this.selectedTemplate = templateSelected
this.regionsLabelIndexMap = getLabelIndexMap(this.selectedParcellation.regions)
store.pipe(
select('dataStore'),
safeFilter('fetchedDataEntries'),
map(v => v.fetchedDataEntries)
).subscribe(de => {
this.dataentries = de
})
)
this.fetchedDataEntries$ = store.pipe(
select('dataStore'),
safeFilter('fetchedDataEntries'),
map(v => v.fetchedDataEntries)
)
this.subscriptions.push(
this.selectedRegions$.subscribe(r => {
this.selectedRegions = r
this.workerService.worker.postMessage({
type: 'BUILD_REGION_SELECTION_TREE',
selectedRegions: r,
regions: this.selectedParcellation.regions
})
})
)
this.fetchDataObservable$ = combineLatest(
this.store.pipe(
select('viewerState'),
safeFilter('templateSelected'),
tap(({templateSelected}) => this.darktheme = templateSelected.useTheme === 'dark'),
map(({templateSelected})=>(templateSelected.name)),
distinctUntilChanged()
),
......@@ -117,6 +82,10 @@ export class DatabrowserService implements OnDestroy{
this.manualFetchDataset$
)
this.fetchDataStatus$ = combineLatest(
this.fetchDataObservable$
)
this.subscriptions.push(
this.fetchDataObservable$.pipe(
debounceTime(16)
......@@ -133,8 +102,9 @@ export class DatabrowserService implements OnDestroy{
* selected as a result of all of its children that are selectted
*/
const { rebuiltSelectedRegions, rebuiltSomeSelectedRegions } = payload
this.rebuiltSomeSelectedRegions = rebuiltSomeSelectedRegions
this.rebuiltSelectedRegions = rebuiltSelectedRegions
/**
* apply filter and populate databrowser instances
*/
})
)
}
......@@ -142,93 +112,48 @@ export class DatabrowserService implements OnDestroy{
ngOnDestroy(){
this.subscriptions.forEach(s => s.unsubscribe())
}
public updateRegionSelection(regions: any[]) {
const filteredRegion = regions.filter(r => r.labelIndex !== null && typeof r.labelIndex !== 'undefined')
this.store.dispatch({
type: SELECT_REGIONS,
selectRegions: filteredRegion
})
}
public deselectRegion(region) {
const regionsToDelect = []
const recursiveFlatten = (region:any) => {
regionsToDelect.push(region)
if (region.children && region.children.map)
region.children.map(recursiveFlatten)
}
recursiveFlatten(region)
const selectedRegions = this.selectedRegions.filter(r => !regionsToDelect.some(deR => deR.name === r.name))
this.updateRegionSelection(selectedRegions)
}
public changeParcellation({ current, previous }){
if (previous && current && current.name === previous.name)
return
this.store.dispatch({
type: SELECT_PARCELLATION,
selectParcellation: current
public fetchPreviewData(datasetName: string){
const encodedDatasetName = encodeURI(datasetName)
return new Promise((resolve, reject) => {
fetch(`${this.constantService.backendUrl}datasets/preview/${encodedDatasetName}`)
.then(res => res.json())
.then(resolve)
.catch(reject)
})
}
public singleClickRegion(region) {
const selectedSet = new Set(extractLabelIdx(region))
const filteredSelectedRegion = this.selectedRegions.filter(r => r.labelIndex)
const intersection = new Set([...filteredSelectedRegion.map(r => r.labelIndex)].filter(v => selectedSet.has(v)))
this.updateRegionSelection(
intersection.size > 0
? filteredSelectedRegion.filter(v => !intersection.has(v.labelIndex))
: filteredSelectedRegion.concat([...selectedSet].map(idx => this.regionsLabelIndexMap.get(idx)))
)
}
public doubleClickRegion(region) {
if (!region.POIs && region.position)
return
const newPos = region.position || region.POIs && region.POIs.constructor === Array && region.POIs[0]
public ngLayers : Set<string> = new Set()
public showNewNgLayer({ url }):void{
const layer = {
name : url,
source : `nifti://${url}`,
mixability : 'nonmixable',
shader : this.constantService.getActiveColorMapFragmentMain()
}
this.store.dispatch({
type: CHANGE_NAVIGATION,
navigation: {
position: newPos,
animation: {
/* empty object is enough to be truthy */
}
},
})
}
public attachFileViewer(comp:ComponentRef<any>, file:File) {
return this.widgetService.addNewWidget(comp, {
title: file.name,
exitable: true,
state: 'floating'
type: ADD_NG_LAYER,
layer
})
}
private dispatchData(arr:DataEntry[][]){
private dispatchData(arr:DataEntry[]){
this.store.dispatch({
type : FETCHED_DATAENTRIES,
fetchedDataEntries : arr.reduce((acc,curr)=>acc.concat(curr),[])
fetchedDataEntries : arr
})
}
private fetchData(templateName: string, parcellationName: string){
this.dispatchData([])
public fetchedFlag: boolean = false
public fetchError: string
public fetchingFlag: boolean = false
private mostRecentFetchToken: any
const requestToken = generateToken()
this.mostRecentFetchToken = requestToken
this.fetchingFlag = true
private lowLevelQuery(templateName: string, parcellationName: string){
const encodedTemplateName = encodeURI(templateName)
const encodedParcellationName = encodeURI(parcellationName)
/**
* TODO instead of using promise.all, use stepwise fetching and
* dispatching of dataentries
*/
Promise.all([
return Promise.all([
fetch(`${this.constantService.backendUrl}datasets/templateName/${encodedTemplateName}`)
.then(res => res.json()),
fetch(`${this.constantService.backendUrl}datasets/parcellationName/${encodedParcellationName}`)
......@@ -239,9 +164,19 @@ export class DatabrowserService implements OnDestroy{
const newMap = new Map(acc)
return newMap.set(item.name, item)
}, new Map()))
.then(map => {
.then(map => Array.from(map.values() as DataEntry[]))
}
private fetchData(templateName: string, parcellationName: string){
this.dispatchData([])
const requestToken = generateToken()
this.mostRecentFetchToken = requestToken
this.fetchingFlag = true
this.lowLevelQuery(templateName, parcellationName)
.then(array => {
if (this.mostRecentFetchToken === requestToken) {
const array = Array.from(map.values()) as DataEntry[][]
this.dispatchData(array)
this.mostRecentFetchToken = null
this.fetchedFlag = true
......@@ -263,35 +198,6 @@ export class DatabrowserService implements OnDestroy{
})
}
public fetchPreviewData(datasetName: string){
const encodedDatasetName = encodeURI(datasetName)
return new Promise((resolve, reject) => {
fetch(`${this.constantService.backendUrl}datasets/preview/${encodedDatasetName}`)
.then(res => res.json())
.then(resolve)
.catch(reject)
})
}
/**
* dedicated viewing (nifti heat maps etc)
*/
private niftiLayerName: string = `nifty layer`
public ngLayers : Set<string> = new Set()
public showNewNgLayer({ url }):void{
const layer = {
name : url,
source : `nifti://${url}`,
mixability : 'nonmixable',
shader : this.constantService.getActiveColorMapFragmentMain()
}
this.store.dispatch({
type: ADD_NG_LAYER,
layer
})
}
removeNgLayer({ url }) {
this.store.dispatch({
type : REMOVE_NG_LAYER,
......@@ -301,5 +207,49 @@ export class DatabrowserService implements OnDestroy{
})
}
public temporaryFilterDataentryName = temporaryFilterDataentryName
rebuildRegionTree(selectedRegions, regions){
this.workerService.worker.postMessage({
type: 'BUILD_REGION_SELECTION_TREE',
selectedRegions,
regions
})
}
public getModalityFromDE = getModalityFromDE
}
export function reduceDataentry(accumulator:{name:string, occurance:number}[], dataentry: DataEntry) {
const methods = dataentry.activity
.map(a => a.methods)
.reduce((acc, item) => acc.concat(item), [])
.map(temporaryFilterDataentryName)
const newDE = Array.from(new Set(methods))
.filter(m => !accumulator.some(a => a.name === m))
return newDE.map(name => {
return {
name,
occurance: 1
}
}).concat(accumulator.map(({name, occurance, ...rest}) => {
return {
...rest,
name,
occurance: methods.some(m => m === name)
? occurance + 1
: occurance
}
}))
}
export function getModalityFromDE(dataentries:DataEntry[]):CountedDataModality[] {
return dataentries.reduce((acc, de) => reduceDataentry(acc, de), [])
}
export interface CountedDataModality{
name: string
occurance: number
visible: boolean
}
\ No newline at end of file
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { DataEntry } from "src/services/stateStore.service";
import { DataEntry, DataStateInterface } from "src/services/stateStore.service";
import { Subscription, merge } from "rxjs";
import { DatabrowserService } from "../databrowser.service";
import { DatabrowserService, CountedDataModality, getModalityFromDE } from "../databrowser.service";
import { ModalityPicker } from "../modalityPicker/modalityPicker.component";
import { skip } from "rxjs/operators";
@Component({
selector : 'data-browser',
......@@ -15,33 +14,29 @@ import { skip } from "rxjs/operators";
export class DataBrowser implements OnDestroy,OnInit{
public currentPage: number = 0
public hitsPerPage: number = 5
public dataEntries: DataEntry[] = []
get selectedRegions(){
return this.dbService.selectedRegions
}
public regions: any[] = []
public template: any
public parcellation: any
get rebuiltSomeSelectedRegions(){
return this.dbService.rebuiltSomeSelectedRegions
}
public dataentries: DataEntry[] = []
get selectedParcellation(){
return this.dbService.selectedParcellation
}
public currentPage: number = 0
public hitsPerPage: number = 5
get availableParcellations(){
return (this.dbService.selectedTemplate && this.dbService.selectedTemplate.parcellations) || []
}
public fetchingFlag: boolean = false
public fetchError: boolean = false
/**
* TODO filter types
*/
private subscriptions : Subscription[] = []
public countedDataM: CountedDataModality[] = []
public visibleCountedDataM: CountedDataModality[] = []
get fetchingFlag(){
return this.dbService.fetchingFlag
}
@ViewChild(ModalityPicker)
modalityPicker: ModalityPicker
get fetchError(){
return this.dbService.fetchError
get darktheme(){
return this.dbService.darktheme
}
/**
......@@ -58,16 +53,26 @@ export class DataBrowser implements OnDestroy,OnInit{
}
/**
* TODO filter types
*/
public modalityFilter: string[] = []
private subscriptions : Subscription[] = []
@ViewChild(ModalityPicker)
modalityPicker: ModalityPicker
ngOnInit(){
const { regions, parcellation, template } = this
this.fetchingFlag = true
this.dbService.getDataByRegion({ regions, parcellation, template })
.then(de => {
this.dataentries = de
this.fetchingFlag = false
return de
})
.then(this.dbService.getModalityFromDE)
.then(modalities => {
this.countedDataM = modalities
})
.catch(e => {
console.error(e)
this.fetchingFlag = false
this.fetchError = true
})
this.subscriptions.push(
merge(
// this.dbService.selectedRegions$,
......@@ -78,7 +83,7 @@ export class DataBrowser implements OnDestroy,OnInit{
* Only reset modality picker
* resetting all creates infinite loop
*/
this.modalityPicker.clearAll()
this.clearAll()
})
)
......@@ -94,6 +99,23 @@ export class DataBrowser implements OnDestroy,OnInit{
this.subscriptions.forEach(s=>s.unsubscribe())
}
clearAll(){
this.countedDataM = this.countedDataM.map(cdm => {
return {
...cdm,
visible: false
}
})
this.visibleCountedDataM = []
this.resetCurrentPage()
}
handleModalityFilterEvent(modalityFilter:CountedDataModality[]){
this.countedDataM = modalityFilter
this.visibleCountedDataM = modalityFilter.filter(dm => dm.visible)
this.resetCurrentPage()
}
retryFetchData(event: MouseEvent){
event.preventDefault()
this.dbService.manualFetchDataset$.next(null)
......@@ -101,28 +123,12 @@ export class DataBrowser implements OnDestroy,OnInit{
public showParcellationList: boolean = false
/**
* when user clicks x on region selector
*/
deselectRegion(region:any){
this.dbService.deselectRegion(region)
}
uncheckModality(modality:string){
this.modalityPicker.toggleModality({name: modality})
}
public filePreviewName: string
onShowPreviewDataset(payload: {datasetName:string, event:MouseEvent}){
const { datasetName, event } = payload
this.filePreviewName = datasetName
}
changeParcellation(payload) {
this.showParcellationList = false
this.dbService.changeParcellation(payload)
}
/**
* when filter changes, it is necessary to set current page to 0,
* or one may overflow and see no dataset
......@@ -132,9 +138,7 @@ export class DataBrowser implements OnDestroy,OnInit{
}
resetFilters(event?:MouseEvent){
event && event.preventDefault()
this.modalityPicker.clearAll()
this.dbService.updateRegionSelection([])
this.clearAll()
}
}
......
......@@ -210,4 +210,4 @@ radio-list
{
max-height: 100%;
overflow:auto;
}
\ No newline at end of file
}
<div
[ngStyle]="filePreviewName ? {'transform': 'translateX(-50%)'} : {}"
class="dataEntryWrapper">
<!-- modality picker -->
<div>
<i (click)="modalityReadmore.show = !modalityReadmore.show" class="clickable">
Filter by Modality <small *ngIf="modalityFilter.length > 0" class="text-muted">({{ modalityFilter.length }})</small>
</i>
<readmore-component
#modalityReadmore
[animationLength]="0"
[collapsedHeight]="0">
<div class="filterWrapper">
<modality-picker
(modalityFilterEmitter)="modalityFilter = $event; resetCurrentPage()">
</modality-picker>
<!-- main window -->
<div
class="t-a-ease-500"
[style.height]="filePreviewName ? '0px' : 'auto'">
<!-- description -->
<readmore-component>
<div class="p-2">
Datasets relevant to
<span
*ngFor="let region of regions"
class="badge badge-secondary mr-1">
{{ region.name }}
</span>
</div>
</readmore-component>
<!-- modality picker -->
<div>
<span
placement="bottom"
container="body"
[popover]="countedDataM.length > 0 ? modalityPicker : null"
[outsideClick]="true"
[containerClass]="darktheme ? 'darktheme' : ''"
class="clickable btn-sm btn btn-secondary btn-block">
Filter by Modality <small *ngIf="visibleCountedDataM.length as visibleDMLength">({{ visibleDMLength }})</small>
</span>
</div>
</readmore-component>
<div *ngIf="!modalityReadmore.show">
<pill-component
[containerStyle]="{backgroundColor:'rgba(128,128,128,0.2)'}"
[closeBtnStyle]="{backgroundColor:'rgba(128,128,128,0.5)'}"
(closeClicked)="uncheckModality(modality)"
[title]="modality"
*ngFor="let modality of modalityFilter">
</pill-component>
</div>
</div>
<!-- region hierarchy -->
<div>
<i (click)="!regionHierarchy.showRegionTree ? regionHierarchy.focusInput($event) : {}" class="clickable">
Filter by parcellation region <small *ngIf="selectedRegions.length > 0" class="text-muted">({{ selectedRegions.length }})</small>
</i>
<!-- parcellation toggle btn -->
<div class="parcellationSelectionWrapper">
<!-- region selector -->
<region-hierarchy
(showRegionFlagChanged)="$event ? (showParcellationList = false) : {}"
#regionHierarchy>
</region-hierarchy>
<!-- datasets container -->
<div
(click)="showParcellationList = !showParcellationList"
class="toggleParcellationBtn btn btn-secondary btn-sm rounded-circle">
<i [ngClass]="showParcellationList ? '' : 'r-90' " class="fas fa-chevron-down"></i>
*ngIf="fetchingFlag; else fetched"
class="spinnerAnimationCircleContainer">
<div class="spinnerAnimationCircle"></div>
<div>Fetching datasets...</div>
</div>
</div>
</div>
<!-- parcellation selector -->
<radio-list
*ngIf="selectedParcellation && showParcellationList"
class="mt-1 mb-0"
(itemSelected)="changeParcellation($event)"
[selectedItem]="selectedParcellation"
[inputArray]="availableParcellations">
</radio-list>
<!-- file previewer -->
<div
class="filePreview">
<div class="filePreviewContainer">
<div (click)="filePreviewName=null" class="rounded-circle btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i>
</div>
<preview-component
*ngIf="filePreviewName"
[datasetName]="filePreviewName">
<readmore-component
[hidden]="selectedRegions.length === 0"
[animationLength]="0"
#selectedRegionReadmore
[collapsedHeight]="45">
<div class="filterWrapper">
<pill-component
[containerStyle]="{backgroundColor:'rgba(128,128,128,0.2)'}"
[closeBtnStyle]="{backgroundColor:'rgba(128,128,128,0.5)'}"
(closeClicked)="deselectRegion(region)"
[title]="region.name"
*ngFor="let region of selectedRegions">
</pill-component>
</preview-component>
</div>
</readmore-component>
</div>
</div>
<div
*ngIf="fetchingFlag; else fetched"
class="spinnerAnimationCircleContainer">
<div class="spinnerAnimationCircle"></div>
<div>Fetching datasets...</div>
</div>
<ng-template #modalityPicker>
<div class="filterWrapper">
<modality-picker
(click)="$event.stopPropagation();"
class="mw-100"
[countedDataM]="countedDataM"
(modalityFilterEmitter)="handleModalityFilterEvent($event)">
</modality-picker>
</div>
</ng-template>
<ng-template #fetched>
<div class="ml-2 mr-2 alert alert-danger" *ngIf="fetchError; else showData">
......@@ -90,18 +79,16 @@
<ng-template #showData>
<!-- datawrapper -->
<div
*ngIf="dbService.fetchedDataEntries$ | async | filterDataEntriesByMethods : modalityFilter | filterDataEntriesByRegion : rebuiltSomeSelectedRegions as filteredDataEntry"
[ngStyle]="filePreviewName ? {'transform': 'translateX(-50%)'} : {}"
class="dataEntryWrapper">
<div *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry"
>
<!-- dataentries -->
<div class="dataEntry">
<div>
<i *ngIf="dbService.fetchedDataEntries$ | async">
{{ (dbService.fetchedDataEntries$ | async).length }} total results.
<i *ngIf="dataentries.length > 0">
{{ dataentries.length }} total results.
<span
*ngIf="rebuiltSomeSelectedRegions.length + modalityFilter.length > 0 ">
*ngIf="visibleCountedDataM.length > 0 ">
{{ filteredDataEntry.length }}
filtered results.
<a
......@@ -111,11 +98,11 @@
</a>
</span>
</i>
<i *ngIf="!(dbService.fetchedDataEntries$ | async)">
<i *ngIf="dataentries.length === 0">
No results to show.
</i>
</div>
<div *ngIf="dbService.fetchedDataEntries$ | async">
<div *ngIf="dataentries.length > 0">
<dataset-viewer
class="mt-1"
*ngFor="let dataset of filteredDataEntry | searchResultPagination : currentPage : hitsPerPage"
......@@ -133,20 +120,6 @@
</pagination-component>
</div>
<!-- file preview -->
<div
class="filePreview">
<div class="filePreviewContainer">
<div (click)="filePreviewName=null" class="rounded-circle btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i>
</div>
<preview-component
*ngIf="filePreviewName"
[datasetName]="filePreviewName">
</preview-component>
</div>
</div>
</div>
</ng-template>
\ No newline at end of file
</ng-template>
import { Component, OnInit, OnDestroy, EventEmitter, Input, Output } from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { DataEntry } from "src/services/stateStore.service";
import { DatabrowserService } from "../databrowser.service";
import { Component, EventEmitter, Input, Output, OnChanges } from "@angular/core";
import { CountedDataModality } from "../databrowser.service";
@Component({
selector: 'modality-picker',
......@@ -11,92 +9,56 @@ import { DatabrowserService } from "../databrowser.service";
]
})
export class ModalityPicker implements OnInit, OnDestroy{
export class ModalityPicker implements OnChanges{
private subscrptions: Subscription[] = []
public modalities$: Observable<string>
public modalityVisibility: Set<string> = new Set()
@Input()
public countedDataM: CountedDataModality[] = []
@Output()
public modalityFilterEmitter: EventEmitter<string[]> = new EventEmitter()
constructor(
private dbService:DatabrowserService
){
}
public checkedModality: CountedDataModality[] = []
filter(dataentries:DataEntry[]) {
return this.modalityVisibility.size === 0
? dataentries
: dataentries.filter(de => de.activity.some(a => a.methods.some(m => this.modalityVisibility.has(this.dbService.temporaryFilterDataentryName(m)))))
}
@Output()
public modalityFilterEmitter: EventEmitter<CountedDataModality[]> = new EventEmitter()
ngOnInit(){
this.subscrptions.push(
this.dbService.fetchedDataEntries$.subscribe(de =>
this.countedDataM = this.getModalityFromDE(de))
)
}
// filter(dataentries:DataEntry[]) {
// return this.modalityVisibility.size === 0
// ? dataentries
// : dataentries.filter(de => de.activity.some(a => a.methods.some(m => this.modalityVisibility.has(this.dbService.temporaryFilterDataentryName(m)))))
// }
ngOnDestroy(){
this.subscrptions.forEach(s => s.unsubscribe())
ngOnChanges(){
this.checkedModality = this.countedDataM.filter(d => d.visible)
}
/**
* TODO
* togglemodailty should emit event, and let parent handle state
*/
toggleModality(modality: Partial<CountedDataModality>){
const dm = this.countedDataM.find(dm => dm.name === modality.name)
if (dm) {
dm.visible = !dm.visible
}
this.modalityFilterEmitter.emit(
this.countedDataM.filter(dm => dm.visible).map(dm => dm.name)
this.countedDataM.map(d => d.name === modality.name
? {
...d,
visible: !d.visible
}
: d)
)
}
clearAll(){
this.countedDataM = this.countedDataM.map(cdm => {
return {
...cdm,
visible: false
}
})
this.modalityFilterEmitter.emit([])
}
reduceDataentry(accumulator:{name:string, occurance:number}[], dataentry: DataEntry) {
const methods = dataentry.activity
.map(a => a.methods)
.reduce((acc, item) => acc.concat(item), [])
.map(this.dbService.temporaryFilterDataentryName)
const newDE = Array.from(new Set(methods))
.filter(m => !accumulator.some(a => a.name === m))
return accumulator.map(({name, occurance, ...rest}) => {
return {
...rest,
name,
occurance: methods.some(m => m === name)
? occurance + 1
: occurance
}
}).concat(newDE.map(name => {
return {
name,
occurance: 1
}
}))
uncheckModality(modality:string){
this.toggleModality({name: modality})
}
getModalityFromDE(dataentries:DataEntry[]):CountedDataModality[] {
return dataentries.reduce((acc, de) => this.reduceDataentry(acc, de), [])
clearAll(){
this.modalityFilterEmitter.emit(
this.countedDataM.map(d => {
return {
...d,
visible: false
}
})
)
}
}
interface CountedDataModality{
name: string
occurance: number
visible: boolean
}
\ No newline at end of file
......@@ -7,4 +7,4 @@ div
{
color:#dbb556;
cursor:default;
}
\ No newline at end of file
}
<div class="ws-initial">
<div
*ngIf="checkedModality.length > 0"
(click)="clearAll()"
class="btn btn-sm btn-link ">
clear all
</div>
<pill-component
class="mw-60"
[containerStyle]="{backgroundColor:'rgba(128,128,128,0.5)'}"
[closeBtnStyle]="{backgroundColor:'rgba(128,128,128,0.8)'}"
(closeClicked)="uncheckModality(dataM.name)"
[title]="dataM.name"
*ngFor="let dataM of checkedModality">
</pill-component>
</div>
<div
*ngFor="let datamodality of countedDataM"
(click)="toggleModality(datamodality)"
......
<div
#searchRegionPopover
searchRegionPopover>
<div class="input-group regionSearch">
<input
#searchTermInput
tabindex="0"
(keydown.esc)="escape($event)"
(focus)="showRegionTree = true"
[value]="searchTerm"
(input)="changeSearchTerm($event)"
class="form-control form-control-sm"
type="text"
[placeholder]="getInputPlaceholder(selectedParcellation)"/>
</div>
<div
*ngIf="showRegionTree"
hideScrollbarContainer>
<div treeContainer #treeContainer>
<div *ngIf="false" treeHeader>
<span>{{ selectedRegions.length }} {{ selectedRegions.length > 1 ? 'regions' : 'region' }} selected</span>
<span (click)="clearRegions($event)" *ngIf="selectedRegions.length > 0" class="btn btn-link">clear all</span>
</div>
<flat-tree-component
[flatTreeViewPort]="treeContainer"
(treeNodeClick)="handleClickRegion($event)"
[inputItem]="aggregatedRegionTree"
[renderNode]="displayTreeNode.bind(this)"
[searchFilter]="filterTreeBySearch.bind(this)">
</flat-tree-component>
</div>
</div>
</div>
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment