diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 65db16cfc66501b73c06f4e5ced584eee91733ef..10ec8c368936e00df59fd3756bca317f4482e589 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -1,4 +1,4 @@ -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 diff --git a/src/atlasViewer/atlasViewer.dataService.service.ts b/src/atlasViewer/atlasViewer.dataService.service.ts index ee42e02befd2533f36d80be12359691001f95490..7838de6005301ba88cfab938fe8f372fb167a654 100644 --- a/src/atlasViewer/atlasViewer.dataService.service.ts +++ b/src/atlasViewer/atlasViewer.dataService.service.ts @@ -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 diff --git a/src/atlasViewer/atlasViewer.style.css b/src/atlasViewer/atlasViewer.style.css index 49d2164f79658817f66a6d907ba2fea5cfc2ca08..b58c1862ad96da157037d160caa3efa4bb031e24 100644 --- a/src/atlasViewer/atlasViewer.style.css +++ b/src/atlasViewer/atlasViewer.style.css @@ -127,4 +127,9 @@ markdown-dom[minReqMd] { margin: 1em; display:block; +} + +[fixedMouseContextualContainerDirective] +{ + width: 15rem; } \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index f46c1ad617e12958f42dcafc8b0292e8d35bdc4a..f3e8379055b4f3bd58d1329ad1e3756051944de0 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -1,6 +1,6 @@ <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> diff --git a/src/atlasViewer/widgetUnit/widgetUnit.template.html b/src/atlasViewer/widgetUnit/widgetUnit.template.html index a036a55361470c9195628a51aee429439812d583..7fc94cf63bc6102cda81c4076906d983fa40c869 100644 --- a/src/atlasViewer/widgetUnit/widgetUnit.template.html +++ b/src/atlasViewer/widgetUnit/widgetUnit.template.html @@ -12,7 +12,7 @@ <div #emptyspan emptyspan>.</div> <div title> <div *ngIf="!titleHTML"> - {{ title }} + {{ title }} </div> <div [innerHTML]="titleHTML" *ngIf="titleHTML"> diff --git a/src/components/dropdown/dropdown.template.html b/src/components/dropdown/dropdown.template.html index eceb97330e83e187f83ed26ceb75609a0e1ee01f..a228bc51b3a211b3d7d46aa4b89fa530fa225487 100644 --- a/src/components/dropdown/dropdown.template.html +++ b/src/components/dropdown/dropdown.template.html @@ -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)" diff --git a/src/components/flatTree/flatTree.template.html b/src/components/flatTree/flatTree.template.html index 5888e601e26ce777aa083e9656f3f94875ff6e1a..883c78e0e5fdea67043eb9029787ee735a5aa04c 100644 --- a/src/components/flatTree/flatTree.template.html +++ b/src/components/flatTree/flatTree.template.html @@ -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 diff --git a/src/components/pill/pill.style.css b/src/components/pill/pill.style.css index 7ecc86ab4ae6ec1d6c506dd80ee6eec507a2a64e..e2aaf541a20bb78635edbb6ebf711fbf95a6756a 100644 --- a/src/components/pill/pill.style.css +++ b/src/components/pill/pill.style.css @@ -9,7 +9,7 @@ .pill-title { - flex: 0 0 auto; + flex: 1 1 0; } .pill-close diff --git a/src/components/pill/pill.template.html b/src/components/pill/pill.template.html index e5b2618e9fc02a0db167df6035d2fa5eabbde4b5..9ce7bf503564227d872a657f5677907aca7052c9 100644 --- a/src/components/pill/pill.template.html +++ b/src/components/pill/pill.template.html @@ -2,7 +2,7 @@ [ngStyle]="containerStyle" class="pill-container"> <span - class="pill-title"> + class="text-truncate pill-title"> {{ title }} </span> <div diff --git a/src/main.module.ts b/src/main.module.ts index b167b47900fd14bf0cda60bf956efe95db3e7742..9a1e6229c558f0a8bc1868dd64e2596ee20ec8c7 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -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( diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 58d0c8a6a84a04fa15818eac82d72a6d64ae27b4..f0b57a3a4712a6ac8d24a59c59292c73ef51e048 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -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 diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index a360a6c3eaa1c6668b2c0d77e42e4adff1be73fa..e32ba854996cdf9652fd7f97d0011f2ac9cf0bec 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -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 */ diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index 2ebc06a8ecbb7797d53e5bb9315876b45d5371a4..684170b0526ddacbb89d4bfbb20d57c4301e0e65 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -1,12 +1,12 @@ -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 diff --git a/src/ui/databrowserModule/databrowser/databrowser.component.ts b/src/ui/databrowserModule/databrowser/databrowser.component.ts index 182a0cea7e9e06115ff764424b94808e9133231b..b314df6c4b69af7f38bfa12d3f83dc70f08b86b9 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.component.ts +++ b/src/ui/databrowserModule/databrowser/databrowser.component.ts @@ -1,9 +1,8 @@ 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() } } diff --git a/src/ui/databrowserModule/databrowser/databrowser.style.css b/src/ui/databrowserModule/databrowser/databrowser.style.css index 68f83fe7357575becf16ea1fca9ac131ee58c603..1759f5ba18fff9ff646f6a5555c99a9d4f85d647 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.style.css +++ b/src/ui/databrowserModule/databrowser/databrowser.style.css @@ -210,4 +210,4 @@ radio-list { max-height: 100%; overflow:auto; -} \ No newline at end of file +} diff --git a/src/ui/databrowserModule/databrowser/databrowser.template.html b/src/ui/databrowserModule/databrowser/databrowser.template.html index 825ebced1fb43775b8ad60a7ac8910cc41d1585d..cac513f151f10d952933c21d3842788be0a25a06 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.template.html +++ b/src/ui/databrowserModule/databrowser/databrowser.template.html @@ -1,86 +1,75 @@ +<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> + diff --git a/src/ui/databrowserModule/modalityPicker/modalityPicker.component.ts b/src/ui/databrowserModule/modalityPicker/modalityPicker.component.ts index f1481429aed4716cbb69e5358dbd123112dd9ee5..01bca9eb65e9c2408932916af8f4ae4352239fb2 100644 --- a/src/ui/databrowserModule/modalityPicker/modalityPicker.component.ts +++ b/src/ui/databrowserModule/modalityPicker/modalityPicker.component.ts @@ -1,7 +1,5 @@ -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 diff --git a/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css b/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css index 4205d6e7236657f7efe9cc237da8951f5b04d1f4..3b98629ee72bc7cad5fe813ef8aaa846e2b03e50 100644 --- a/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css +++ b/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css @@ -7,4 +7,4 @@ div { color:#dbb556; cursor:default; -} \ No newline at end of file +} diff --git a/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html b/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html index f5bb0d906f4833dc8a40145466f808fbd144d612..f980ef04c3a2689d8258523bf383ff4d4b29beb6 100644 --- a/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html +++ b/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html @@ -1,3 +1,21 @@ +<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)" diff --git a/src/ui/databrowserModule/regionHierachy/regionHierarchy.template.html b/src/ui/databrowserModule/regionHierachy/regionHierarchy.template.html deleted file mode 100644 index 0ae9f9856280a53a05f0bb174329409a7974d3fc..0000000000000000000000000000000000000000 --- a/src/ui/databrowserModule/regionHierachy/regionHierarchy.template.html +++ /dev/null @@ -1,38 +0,0 @@ -<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 diff --git a/src/ui/databrowserModule/util/filterDataEntriesByMethods.pipe.ts b/src/ui/databrowserModule/util/filterDataEntriesByMethods.pipe.ts index b6ffded850a9f1765b736142c2eb7a1071c6b44e..df159ba9cd5b5a8357de1838bf5d45bd8889199d 100644 --- a/src/ui/databrowserModule/util/filterDataEntriesByMethods.pipe.ts +++ b/src/ui/databrowserModule/util/filterDataEntriesByMethods.pipe.ts @@ -1,16 +1,16 @@ import { PipeTransform, Pipe } from "@angular/core"; import { DataEntry } from "src/services/stateStore.service"; -import { temporaryFilterDataentryName } from '../databrowser.service' +import { temporaryFilterDataentryName, CountedDataModality } from '../databrowser.service' @Pipe({ name : 'filterDataEntriesByMethods' }) export class FilterDataEntriesbyMethods implements PipeTransform{ - public transform(dataEntries:DataEntry[],showDataMethods:string[]):DataEntry[]{ - return dataEntries && showDataMethods && showDataMethods.length > 0 + public transform(dataEntries:DataEntry[],dataModalities:CountedDataModality[]):DataEntry[]{ + return dataEntries && dataModalities && dataModalities.length > 0 ? dataEntries.filter(dataEntry => { - return dataEntry.activity.some(a => a.methods.some(m => showDataMethods.findIndex(dm => dm === temporaryFilterDataentryName(m)) >= 0)) + return dataEntry.activity.some(a => a.methods.some(m => dataModalities.findIndex(dm => dm.name === temporaryFilterDataentryName(m)) >= 0)) }) : dataEntries } diff --git a/src/ui/menuicons/menuicons.component.ts b/src/ui/menuicons/menuicons.component.ts index 0a48cfa4af1542b77ec3445dd31540bd93c7840a..12537ec1699a45300e08146fae1d858a93395f79 100644 --- a/src/ui/menuicons/menuicons.component.ts +++ b/src/ui/menuicons/menuicons.component.ts @@ -6,6 +6,7 @@ import { LayerBrowser } from "src/ui/layerbrowser/layerbrowser.component"; import { DataBrowser } from "src/ui/databrowserModule/databrowser/databrowser.component"; import { PluginBannerUI } from "../pluginBanner/pluginBanner.component"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; +import { DatabrowserService } from "../databrowserModule/databrowser.service"; @Component({ selector: 'menu-icons', @@ -47,37 +48,37 @@ export class MenuIconsBar{ private widgetServices:WidgetServices, private injector:Injector, private constantService:AtlasViewerConstantsServices, + dbService: DatabrowserService, cfr: ComponentFactoryResolver ){ + + dbService.createDatabrowser = this.clickSearch.bind(this) + this.dbcf = cfr.resolveComponentFactory(DataBrowser) this.lbcf = cfr.resolveComponentFactory(LayerBrowser) this.pbcf = cfr.resolveComponentFactory(PluginBannerUI) } - public clickSearch(event: MouseEvent){ - if (this.dbWidget) { - this.dbWidget.destroy() - this.dbWidget = null - return - } - this.dataBrowser = this.dbcf.create(this.injector) - this.dbWidget = this.widgetServices.addNewWidget(this.dataBrowser, { + + /** + * TODO + * temporary measure + * migrate to nehubaOverlay + */ + public clickSearch({ regions, template, parcellation }){ + const dataBrowser = this.dbcf.create(this.injector) + dataBrowser.instance.regions = regions + dataBrowser.instance.template = template + dataBrowser.instance.parcellation = parcellation + const title = regions.length > 1 + ? `Data associated with ${regions.length} regions` + : `Data associated with ${regions[0].name}` + this.widgetServices.addNewWidget(dataBrowser, { exitable: true, persistency: true, state: 'floating', - title: this.dataBrowserTitle, - titleHTML: `<i class="fas fa-search"></i> ${this.dataBrowserTitle}` + title, + titleHTML: `<i class="fas fa-search"></i> ${title}` }) - - this.dbWidget.onDestroy(() => { - // sub.unsubscribe() - this.dataBrowser = null - this.dbWidget = null - }) - - const el = event.currentTarget as HTMLElement - const top = el.offsetTop - const left = el.offsetLeft + 50 - this.dbWidget.instance.position = [left, top] } public clickLayer(event: MouseEvent){ diff --git a/src/ui/menuicons/menuicons.template.html b/src/ui/menuicons/menuicons.template.html index 0bb0ffb64e8d60183fcf1747777d6b66a5c45568..7b1fe8bc9b0f5191a9be3912e419454c5465733e 100644 --- a/src/ui/menuicons/menuicons.template.html +++ b/src/ui/menuicons/menuicons.template.html @@ -14,6 +14,7 @@ </div> <div + *ngIf="false" [ngClass]="isMobile ? 'btnWrapper-lg' : ''" class="btnWrapper"> <div diff --git a/src/ui/databrowserModule/util/filterNameBySearch.pipe.ts b/src/ui/regionHierachy/filterNameBySearch.pipe.ts similarity index 100% rename from src/ui/databrowserModule/util/filterNameBySearch.pipe.ts rename to src/ui/regionHierachy/filterNameBySearch.pipe.ts diff --git a/src/ui/databrowserModule/regionHierachy/regionHierarchy.component.ts b/src/ui/regionHierachy/regionHierarchy.component.ts similarity index 83% rename from src/ui/databrowserModule/regionHierachy/regionHierarchy.component.ts rename to src/ui/regionHierachy/regionHierarchy.component.ts index 085a519a66837113b2a70608fcd20dd16daef301..b6c43e089856e0dd75deef6b9c1bf6dd16dc2d78 100644 --- a/src/ui/databrowserModule/regionHierachy/regionHierarchy.component.ts +++ b/src/ui/regionHierachy/regionHierarchy.component.ts @@ -1,8 +1,7 @@ import { EventEmitter, Component, ElementRef, ViewChild, HostListener, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Input, Output } from "@angular/core"; import { Subscription, Subject } from "rxjs"; import { buffer, debounceTime } from "rxjs/operators"; -import { FilterNameBySearch } from "../util/filterNameBySearch.pipe"; -import { DatabrowserService } from "../databrowser.service"; +import { FilterNameBySearch } from "./filterNameBySearch.pipe"; @Component({ selector: 'region-hierarchy', @@ -19,41 +18,41 @@ export class RegionHierarchy implements OnInit{ @Input() public selectedRegions: any[] = [] + @Input() + public selectedParcellation: any + private _showRegionTree: boolean = false @Output() - showRegionFlagChanged: EventEmitter<boolean> = new EventEmitter() + private showRegionFlagChanged: EventEmitter<boolean> = new EventEmitter() + + @Output() + private singleClickRegion: EventEmitter<any> = new EventEmitter() + + @Output() + private doubleClickRegion: EventEmitter<any> = new EventEmitter() public searchTerm: string = '' private subscriptions: Subscription[] = [] - @ViewChild('searchRegionPopover', { read: ElementRef }) - private searchRegionPopover: ElementRef - @ViewChild('searchTermInput', {read: ElementRef}) private searchTermInput: ElementRef @HostListener('document:click', ['$event']) closeRegion(event: MouseEvent) { - if (!this.searchRegionPopover) - return - const contains = this.searchRegionPopover.nativeElement.contains(event.target) + const contains = this.el.nativeElement.contains(event.target) this.showRegionTree = contains if (!this.showRegionTree) this.searchTerm = '' } - get selectedParcellation(){ - return this.dbService.selectedParcellation - } - get regionsLabelIndexMap() { - return this.dbService.regionsLabelIndexMap + return null } constructor( private cdr:ChangeDetectorRef, - private dbService: DatabrowserService + private el:ElementRef ){ } @@ -119,28 +118,33 @@ export class RegionHierarchy implements OnInit{ private handleRegionTreeClickSubject: Subject<any> = new Subject() handleClickRegion(obj: any) { - obj.event.stopPropagation() + const {event} = obj + /** + * TODO figure out why @closeRegion gets triggered, but also, contains returns false + */ + if (event) + event.stopPropagation() this.handleRegionTreeClickSubject.next(obj) } - /* double click navigate to the interested area */ - private doubleClick(obj: any) { + /* single click selects/deselects region(s) */ + private singleClick(obj: any) { if (!obj) return const { inputItem : region } = obj if (!region) return - this.dbService.doubleClickRegion(region) + this.singleClickRegion.emit(region) } - /* single click selects/deselects region(s) */ - private singleClick(obj: any) { - this.dbService.singleClickRegion(obj.inputItem) - - /** - * TODO may no longer be needed - */ - this.cdr.markForCheck() + /* double click navigate to the interested area */ + private doubleClick(obj: any) { + if (!obj) + return + const { inputItem : region } = obj + if (!region) + return + this.doubleClickRegion.emit(region) } private insertHighlight(name: string, searchTerm: string): string { diff --git a/src/ui/databrowserModule/regionHierachy/regionHierarchy.style.css b/src/ui/regionHierachy/regionHierarchy.style.css similarity index 95% rename from src/ui/databrowserModule/regionHierachy/regionHierarchy.style.css rename to src/ui/regionHierachy/regionHierarchy.style.css index c61fa909e637dc2d2a9fb099198385de3801e470..18ab174bfd729ef2bb897d87926bef8afd908b54 100644 --- a/src/ui/databrowserModule/regionHierachy/regionHierarchy.style.css +++ b/src/ui/regionHierachy/regionHierarchy.style.css @@ -27,7 +27,6 @@ div[treeContainer] overflow-y:auto; overflow-x:hidden; - /* color:white; background-color:rgba(12,12,12,0.8); */ } @@ -50,11 +49,6 @@ div[hideScrollbarcontainer] margin-top:2px; } -div[searchRegionPopover] -{ - /* height:2em; */ -} - input[type="text"] { border:none; diff --git a/src/ui/regionHierachy/regionHierarchy.template.html b/src/ui/regionHierachy/regionHierarchy.template.html new file mode 100644 index 0000000000000000000000000000000000000000..4bca1df173961ab907436be224abae80e6f53d53 --- /dev/null +++ b/src/ui/regionHierachy/regionHierarchy.template.html @@ -0,0 +1,38 @@ +<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> + + <ng-container *ngIf="selectedParcellation && selectedParcellation.regions as regions"> + + <flat-tree-component + *ngFor="let region of regions" + [flatTreeViewPort]="treeContainer" + (treeNodeClick)="handleClickRegion($event)" + [inputItem]="aggregatedRegionTree" + [renderNode]="displayTreeNode.bind(this)" + [searchFilter]="filterTreeBySearch.bind(this)"> + + </flat-tree-component> + </ng-container> + </div> +</div> \ No newline at end of file diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts index 117e8a01910741b676822abd815c5d822b25d921..d155395cb5bddb9763eb516e48919ae648f033a2 100644 --- a/src/ui/signinBanner/signinBanner.components.ts +++ b/src/ui/signinBanner/signinBanner.components.ts @@ -1,11 +1,12 @@ -import { Component, ChangeDetectionStrategy, OnDestroy } from "@angular/core"; +import { Component, ChangeDetectionStrategy, OnDestroy, OnInit } from "@angular/core"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; -import { AuthService, User, AuthMethod } from "src/services/auth.service"; +import { AuthService, User } from "src/services/auth.service"; import { Store, select } from "@ngrx/store"; import { ViewerConfiguration } from "src/services/state/viewerConfig.store"; import { Subscription, Observable } from "rxjs"; -import { safeFilter, isDefined, NEWVIEWER } from "src/services/stateStore.service"; +import { safeFilter, isDefined, NEWVIEWER, SELECT_REGIONS } from "src/services/stateStore.service"; import { map, filter, distinctUntilChanged } from "rxjs/operators"; +import { regionFlattener } from "src/util/regionFlattener"; @Component({ selector: 'signin-banner', @@ -17,11 +18,14 @@ import { map, filter, distinctUntilChanged } from "rxjs/operators"; changeDetection: ChangeDetectionStrategy.OnPush }) -export class SigninBanner implements OnDestroy{ +export class SigninBanner implements OnInit, OnDestroy{ private subscriptions: Subscription[] = [] public loadedTemplates$: Observable<any[]> public selectedTemplate$: Observable<any> + public selectedParcellation$: Observable<any> + public selectedRegions$: Observable<any[]> + private selectedRegions: any[] = [] constructor( private constantService: AtlasViewerConstantsServices, @@ -40,6 +44,29 @@ export class SigninBanner implements OnDestroy{ distinctUntilChanged((o, n) => o.templateSelected.name === n.templateSelected.name), map(state => state.templateSelected) ) + + this.selectedParcellation$ = this.store.pipe( + select('viewerState'), + safeFilter('parcellationSelected'), + map(state => state.parcellationSelected), + distinctUntilChanged((o, n) => o === n || (o && n && o.name === n.name)), + ) + + this.selectedRegions$ = this.store.pipe( + select('viewerState'), + safeFilter('regionsSelected'), + map(state => state.regionsSelected), + distinctUntilChanged((arr1, arr2) => arr1.length === arr2.length && (arr1 as any[]).every((item, index) => item.name === arr2[index].name)) + ) + } + + ngOnInit(){ + + this.subscriptions.push( + this.selectedRegions$.subscribe(regions => { + this.selectedRegions = regions + }) + ) } ngOnDestroy(){ @@ -56,8 +83,31 @@ export class SigninBanner implements OnDestroy{ }) } + changeParcellation({ current, previous }){ + + } + + handleRegionClick({ mode = 'single', region }){ + if (!region) + return + const flattenedRegion = regionFlattener(region).filter(r => isDefined(r.labelIndex)) + const flattenedRegionNames = new Set(flattenedRegion.map(r => r.name)) + const selectedRegionNames = new Set(this.selectedRegions.map(r => r.name)) + const selectAll = flattenedRegion.every(r => !selectedRegionNames.has(r.name)) + this.store.dispatch({ + type: SELECT_REGIONS, + selectRegions: selectAll + ? this.selectedRegions.concat(flattenedRegion) + : this.selectedRegions.filter(r => !flattenedRegionNames.has(r.name)) + }) + } + + displayActiveParcellation(parcellation:any){ + return `<div class="d-flex"><small>Parcellation</small> <small class = "flex-grow-1 mute-text">${parcellation ? '(' + parcellation.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>` + } + displayActiveTemplate(template: any) { - return `<small>Template</small> <small class = "mute-text">${template ? '(' + template.name + ')' : ''}</small> <span class = "caret"></span>` + return `<div class="d-flex"><small>Template</small> <small class = "flex-grow-1 mute-text">${template ? '(' + template.name + ')' : ''}</small> <span class = "fas fa-caret-down"></span></div>` } showHelp() { diff --git a/src/ui/signinBanner/signinBanner.style.css b/src/ui/signinBanner/signinBanner.style.css index 45c20228bd37aa12802a1409f05bb6e6086cb03c..f07029736eefa53d8ebbcfdfdb1f72bf25608458 100644 --- a/src/ui/signinBanner/signinBanner.style.css +++ b/src/ui/signinBanner/signinBanner.style.css @@ -25,5 +25,12 @@ :host > dropdown-component { min-width: 10em; - flex: 1 0 0px; + flex: 1 0 auto; +} + +region-hierarchy, +dropdown-component +{ + font-size:80%; + } \ No newline at end of file diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html index a5d7389f67ae23c60c124e9b762399ba50753207..ca76f39d4b97109d48f62471143ff2a972ad4183 100644 --- a/src/ui/signinBanner/signinBanner.template.html +++ b/src/ui/signinBanner/signinBanner.template.html @@ -1,3 +1,22 @@ +<ng-container *ngIf="selectedTemplate$ | async as selectedTemplate"> + <region-hierarchy + [selectedRegions]="selectedRegions$ | async | filterNull" + (singleClickRegion)="handleRegionClick({ mode: 'single', region: $event })" + (doubleClickRegion)="handleRegionClick({ mode: 'double', region: $event })" + *ngIf="selectedParcellation$ | async as selectedParcellation" + [selectedParcellation]="selectedParcellation"> + + </region-hierarchy> + <dropdown-component + *ngIf="selectedParcellation$ | async as selectedParcellation" + (itemSelected)="changeParcellation($event)" + [activeDisplay]="displayActiveParcellation" + [selectedItem]="selectedParcellation" + [inputArray]="selectedTemplate.parcellations"> + + </dropdown-component> +</ng-container> + <dropdown-component *ngIf="!isMobile" (itemSelected)="changeTemplate($event)" diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 407d38c7562406c11487bd2aa6f0eb0638967338..e01f63ed967836bcb0734bc2c6fdd2719912a879 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -12,7 +12,6 @@ import { GroupDatasetByRegion } from "../util/pipes/groupDataEntriesByRegion.pip import { filterRegionDataEntries } from "../util/pipes/filterRegionDataEntries.pipe"; import { MenuIconsBar } from './menuicons/menuicons.component' -import { GetPropMapPipe } from "../util/pipes/getPropMap.pipe"; import { GetUniquePipe } from "src/util/pipes/getUnique.pipe"; import { LandmarkUnit } from "./nehubaContainer/landmarkUnit/landmarkUnit.component"; @@ -41,6 +40,9 @@ import { DatabrowserModule } from "./databrowserModule/databrowser.module"; import { SigninBanner } from "./signinBanner/signinBanner.components"; import { SigninModal } from "./signinModal/signinModal.component"; import { FilterNgLayer } from "src/util/pipes/filterNgLayer.pipe"; +import { UtilModule } from "src/util/util.module"; +import { RegionHierarchy } from "./regionHierachy/regionHierarchy.component"; +import { FilterNameBySearch } from "./regionHierachy/filterNameBySearch.pipe"; @NgModule({ @@ -50,6 +52,7 @@ import { FilterNgLayer } from "src/util/pipes/filterNgLayer.pipe"; LayoutModule, ComponentsModule, DatabrowserModule, + UtilModule, PopoverModule.forRoot(), TooltipModule.forRoot() @@ -72,11 +75,11 @@ import { FilterNgLayer } from "src/util/pipes/filterNgLayer.pipe"; MenuIconsBar, SigninBanner, SigninModal, + RegionHierarchy, /* pipes */ GroupDatasetByRegion, filterRegionDataEntries, - GetPropMapPipe, GetUniquePipe, FlatmapArrayPipe, SafeStylePipe, @@ -85,6 +88,7 @@ import { FilterNgLayer } from "src/util/pipes/filterNgLayer.pipe"; SpatialLandmarksToDataBrowserItemPipe, FilterNullPipe, FilterNgLayer, + FilterNameBySearch, /* directive */ DownloadDirective, @@ -117,5 +121,4 @@ import { FilterNgLayer } from "src/util/pipes/filterNgLayer.pipe"; }) export class UIModule{ - } \ No newline at end of file diff --git a/src/util/directives/FixedMouseContextualContainerDirective.directive.ts b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..450dc8a00781377904e2882419a08f618bed9a8f --- /dev/null +++ b/src/util/directives/FixedMouseContextualContainerDirective.directive.ts @@ -0,0 +1,60 @@ +import { Directive, Input, HostBinding, HostListener, ElementRef, OnChanges, Output, EventEmitter } from "@angular/core"; + +@Directive({ + selector: '[fixedMouseContextualContainerDirective]' +}) + +export class FixedMouseContextualContainerDirective implements OnChanges{ + + private defaultPos: [number, number] = [-1e3, -1e3] + public isShown: boolean = false + + @Input() + public mousePos: [number, number] = this.defaultPos + + @Output() + public onShow: EventEmitter<null> = new EventEmitter() + + @Output() + public onHide: EventEmitter<null> = new EventEmitter() + + constructor( + private el: ElementRef + ){ + + } + + ngOnChanges(changes){ + console.log({changes}) + } + + public show(){ + this.transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` + this.styleDisplay = 'block' + this.isShown = true + this.onShow.emit() + } + + public hide(){ + this.transform = `translate(${this.defaultPos.map(v => v.toString() + 'px').join(', ')})` + this.styleDisplay = 'none' + this.isShown = false + this.onHide.emit() + } + + @HostBinding('style.display') + public styleDisplay = `none` + + @HostBinding('style.transform') + public transform = `translate(${this.mousePos.map(v => v.toString() + 'px').join(', ')})` + + @HostListener('document:click', ['$event']) + documentClick(event: MouseEvent){ + if (this.styleDisplay === 'none') + return + if (this.el.nativeElement.contains(event.target)) + return + + this.hide() + } +} \ No newline at end of file diff --git a/src/util/pipes/getPropMap.pipe.ts b/src/util/pipes/getPropMap.pipe.ts deleted file mode 100644 index a761b1d7a67707a58b5588158dc2b86817bbf93b..0000000000000000000000000000000000000000 --- a/src/util/pipes/getPropMap.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; - -/** - * nb, will return undefined elements - * nb, will throw error if one of the input element is not an object - */ - -@Pipe({ - name : 'getPropMapPipe' -}) - -export class GetPropMapPipe implements PipeTransform{ - public transform(arr:any[],prop:string):any[]{ - return arr.map(item => item[prop]) - } -} \ No newline at end of file diff --git a/src/util/regionFlattener.spec.ts b/src/util/regionFlattener.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/util/regionFlattener.ts b/src/util/regionFlattener.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d93fe4882c7c2f8dde2e4d965cbf4d1b734a11c --- /dev/null +++ b/src/util/regionFlattener.ts @@ -0,0 +1,6 @@ +export function regionFlattener(region:any){ + return[ + [ region ], + ...region.children && region.children.map && region.children.map(regionFlattener) + ].reduce((acc, item) => acc.concat(item), []) +} \ No newline at end of file diff --git a/src/util/util.module.ts b/src/util/util.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a83857d6198306aad9e955f7247b01baf29e4a2e --- /dev/null +++ b/src/util/util.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +@NgModule({ + declarations: [ + ], + exports: [ + ] +}) + +export class UtilModule{ + +} \ No newline at end of file