diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index e62861864810e27237ce38c9fec1dae40f19aeaa..af0debbaa29ca391c872b09a1a52fe1b78ecced3 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -14,10 +14,10 @@ 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"; -import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS } from "src/services/state/uiState.store"; +import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS, SHOW_SIDEBAR_TEMPLATE, SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; import { TabsetComponent } from "ngx-bootstrap/tabs"; import { LocalFileService } from "src/services/localFile.service"; -import { MatDialog, MatDialogRef, MatSnackBar, MatSnackBarRef } from "@angular/material"; +import { MatDialog, MatDialogRef, MatSnackBar, MatSnackBarRef, MatBottomSheet, MatBottomSheetRef } from "@angular/material"; /** * TODO @@ -74,6 +74,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { private snackbarRef: MatSnackBarRef<any> public snackbarMessage$: Observable<string> + private bottomSheetRef: MatBottomSheetRef + private bottomSheet$: Observable<TemplateRef<any>> public dedicatedView$: Observable<string | null> public onhoverSegments$: Observable<string[]> @@ -95,6 +97,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public unsupportedPreviews: any[] = UNSUPPORTED_PREVIEW public sidePanelOpen$: Observable<boolean> + public sideNavTemplate$: Observable<TemplateRef<any>> get toggleMessage(){ return this.constantsService.toggleMessage @@ -112,7 +115,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { private dispatcher$: ActionsSubject, private rd: Renderer2, public localFileService: LocalFileService, - private snackbar: MatSnackBar + private snackbar: MatSnackBar, + private bottomSheet: MatBottomSheet ) { this.snackbarMessage$ = this.store.pipe( @@ -120,6 +124,12 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { select("snackbarMessage") ) + this.bottomSheet$ = this.store.pipe( + select('uiState'), + select('bottomSheetTemplate'), + distinctUntilChanged() + ) + /** * TODO deprecated */ @@ -143,6 +153,12 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { map(state => state.sidePanelOpen) ) + this.sideNavTemplate$ = this.store.pipe( + select('uiState'), + select('sidebarTemplate'), + distinctUntilChanged() + ) + this.showHelp$ = this.constantsService.showHelpSubject$.pipe( debounceTime(170) ) @@ -245,6 +261,23 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.selectedParcellation = parcellation }) ) + + this.subscriptions.push( + this.bottomSheet$.subscribe(templateRef => { + if (!templateRef) { + this.bottomSheetRef && this.bottomSheetRef.dismiss() + } else { + this.bottomSheetRef = this.bottomSheet.open(templateRef) + this.bottomSheetRef.afterDismissed().subscribe(() => { + this.store.dispatch({ + type: SHOW_BOTTOM_SHEET, + bottomSheetTemplate: null + }) + this.bottomSheetRef = null + }) + } + }) + ) } diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index a7c83d3a90d12397635fe02f3ba72a02ebe3d96c..5ca66e94df07bcda5cff5314d0293c573430a290 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -36,7 +36,7 @@ </layout-floating-container> </tab> <tab heading="Containers"> - <menu-icons iconWrapper hidden #MenuIcons> + <menu-icons hidden #MenuIcons> </menu-icons> <panel-component> <div class="m-2" heading> @@ -154,10 +154,32 @@ <ui-nehuba-container (contextmenu)="$event.stopPropagation(); $event.preventDefault();"> <!--nehubaClickHandler($event)--> </ui-nehuba-container> - <div *ngIf="!isMobile" bannerWrapper> - <menu-icons iconWrapper> - </menu-icons> + <div class="z-index-10 position-absolute pe-none w-100 h-100"> + <mat-drawer-container + [hasBackdrop]="false" + class="w-100 h-100 bg-none"> + <mat-drawer + mode="push" + class="pe-all col-sm-12 col-md-3" + [disableClose]="true" + [autoFocus]="false" + [opened]="sideNavTemplate$ | async"> + + <!-- template outlet --> + <mat-card class="h-100"> + <ng-container *ngTemplateOutlet="sideNavTemplate$ | async"> + </ng-container> + </mat-card> + + </mat-drawer> + <div class="d-flex h-100 justify-content-between align-items-start bg-none pe-none"> + <menu-icons> + </menu-icons> + </div> + </mat-drawer-container> + </div> + <div class="d-flex flex-row justify-content-end z-index-10 position-absolute pe-none w-100 h-100"> <signin-banner signinWrapper [ngStyle]="{'margin-right': !selectedTemplate? '20px': ''}"> </signin-banner> </div> diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 8ddb291d165ab8771f9e6b1f6e36ea8ea465f2d4..49785f32d110204bbd4dbe19f4386508956aa94f 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -20,7 +20,6 @@ import { FlatTreeComponent } from './flatTree/flatTree.component'; import { FlattenTreePipe } from './flatTree/flattener.pipe'; import { RenderPipe } from './flatTree/render.pipe'; import { HighlightPipe } from './flatTree/highlight.pipe'; -import { FitlerRowsByVisibilityPipe } from './flatTree/filterRowsByVisibility.pipe'; import { AppendSiblingFlagPipe } from './flatTree/appendSiblingFlag.pipe'; import { ClusteringPipe } from './flatTree/clustering.pipe'; import { TimerComponent } from './timer/timer.component'; @@ -33,6 +32,7 @@ import { ProgressBar } from './progress/progress.component'; import { SleightOfHand } from './sleightOfHand/soh.component'; import { DialogComponent } from './dialog/dialog.component'; import { ConfirmDialogComponent } from './confirmDialog/confirmDialog.component'; +import { UtilModule } from 'src/util/util.module'; @NgModule({ @@ -41,7 +41,8 @@ import { ConfirmDialogComponent } from './confirmDialog/confirmDialog.component' ScrollingModule, FormsModule, BrowserAnimationsModule, - AngularMaterialModule + AngularMaterialModule, + UtilModule ], declarations : [ /* components */ @@ -72,7 +73,6 @@ import { ConfirmDialogComponent } from './confirmDialog/confirmDialog.component' FlattenTreePipe, RenderPipe, HighlightPipe, - FitlerRowsByVisibilityPipe, AppendSiblingFlagPipe, ClusteringPipe, FilterCollapsePipe diff --git a/src/components/flatTree/filterRowsByVisibility.pipe.ts b/src/components/flatTree/filterRowsByVisibility.pipe.ts index 612e206126a74e363eb450cbe93ed237fd290200..044e894fca976adfbbcc0419a28a3fdb601abfba 100644 --- a/src/components/flatTree/filterRowsByVisibility.pipe.ts +++ b/src/components/flatTree/filterRowsByVisibility.pipe.ts @@ -1,16 +1,18 @@ import { Pipe, PipeTransform } from "@angular/core"; +// TODO fix typo + @Pipe({ name : 'filterRowsByVisbilityPipe' }) -export class FitlerRowsByVisibilityPipe implements PipeTransform{ +export class FilterRowsByVisbilityPipe implements PipeTransform{ public transform(rows:any[], getChildren : (item:any)=>any[], filterFn : (item:any)=>boolean){ return rows.filter(row => this.recursive(row, getChildren, filterFn) ) } private recursive(single : any, getChildren : (item:any) => any[], filterFn:(item:any) => boolean):boolean{ - return filterFn(single) || getChildren(single).some(c => this.recursive(c, getChildren, filterFn)) + return filterFn(single) || (getChildren && getChildren(single).some(c => this.recursive(c, getChildren, filterFn))) } } \ No newline at end of file diff --git a/src/components/flatTree/flatTree.style.css b/src/components/flatTree/flatTree.style.css index 9ee266f33ce80735ba9ebae0689f01c031aaaeaf..2141fb09a515b4b00818afd5f45306edcbeed0c5 100644 --- a/src/components/flatTree/flatTree.style.css +++ b/src/components/flatTree/flatTree.style.css @@ -71,7 +71,7 @@ span[renderText] content: ' '; height:1.5em; width: 1.5em; - bottom: 0.75em; + bottom: 0.25em; left: -0.5em; position: absolute; border-left: rgba(128,128,128,0.6) 1px dashed; diff --git a/src/main.module.ts b/src/main.module.ts index 16eebb864f29c387ccbffcec9058aa2d2e2da374..74f30b3bce9af921a53a7a71d8f98b4296012d48 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -49,6 +49,7 @@ import { ViewerStateControllerUseEffect } from "./ui/viewerStateController/viewe import { ConfirmDialogComponent } from "./components/confirmDialog/confirmDialog.component"; import { ViewerStateUseEffect } from "./services/state/viewerState.store"; import { NgViewerUseEffect } from "./services/state/ngViewerState.store"; +import { DatabrowserModule } from "./ui/databrowserModule/databrowser.module"; import 'hammerjs' @@ -60,6 +61,7 @@ import 'hammerjs' ComponentsModule, DragDropModule, UIModule, + DatabrowserModule, AngularMaterialModule, TooltipModule.forRoot(), diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index a468c39ec4f04fae5aac61317faee33784745f9c..a835d33bcacfdd4356b9afbe7ce25264784a3325 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -297,9 +297,10 @@ markdown-dom pre code { white-space: nowrap!important; } -.ws-initial + +.w-0 { - white-space: initial!important; + width: 0!important; } .w-5em @@ -336,6 +337,11 @@ markdown-dom pre code max-width: 20em!important; } +.w-90 +{ + width: 90%!important; +} + .w-20em { width: 20em!important; @@ -480,4 +486,29 @@ bs-modal-backdrop.modal-backdrop cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper { width: 100%; -} \ No newline at end of file +} + +.z-index-10 +{ + z-index: 10!important; +} + +.bg-none +{ + background:none!important; +} + +.dot +{ + width: 0.5rem; + height: 0.5rem; + display: inline-block; + border-radius: 50%; +} + +/* temp */ +/* https://github.com/angular/components/issues/4591#issuecomment-305046461 */ +.mat-tab-body-wrapper +{ + flex-grow:1; +} diff --git a/src/services/localFile.service.ts b/src/services/localFile.service.ts index 8ef82668dd4ec7e237a5e1e2e6de43d8b4983dac..10ece9c15e3775c82ba3310421c37e71323cd600 100644 --- a/src/services/localFile.service.ts +++ b/src/services/localFile.service.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; -import { MatSnackBar } from "@angular/material"; import { DatabrowserService } from "src/ui/databrowserModule/databrowser.service"; import { Store } from "@ngrx/store"; import { SNACKBAR_MESSAGE } from "./state/uiState.store"; +import { KgSingleDatasetService } from "src/ui/databrowserModule/kgSingleDatasetService.service"; /** * experimental service handling local user files such as nifti and gifti @@ -17,8 +17,8 @@ export class LocalFileService { private supportedExtSet = new Set(SUPPORTED_EXT) constructor( - private dbService: DatabrowserService , - private store: Store<any> + private store: Store<any>, + private singleDsService: KgSingleDatasetService ){ } @@ -70,7 +70,7 @@ export class LocalFileService { URL.revokeObjectURL(this.niiUrl) } this.niiUrl = URL.createObjectURL(file) - this.dbService.showNewNgLayer({ + this.singleDsService.showNewNgLayer({ url: this.niiUrl }) diff --git a/src/services/state/dataStore.store.ts b/src/services/state/dataStore.store.ts index 44f21f0bb825e10cdb63e51c23813c7f3fa7a0ec..18ab1e52561844b47c0c6de9e6538022596c0bb3 100644 --- a/src/services/state/dataStore.store.ts +++ b/src/services/state/dataStore.store.ts @@ -164,7 +164,8 @@ export interface FileSupplementData{ const ACTION_TYPES = { FAV_DATASET: `FAV_DATASET`, UPDATE_FAV_DATASETS: `UPDATE_FAV_DATASETS`, - UNFAV_DATASET: 'UNFAV_DATASET' + UNFAV_DATASET: 'UNFAV_DATASET', + TOGGLE_FAV_DATASET: 'TOGGLE_FAV_DATASET' } export const DATASETS_ACTIONS_TYPES = ACTION_TYPES \ No newline at end of file diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index 09639a50a5c50bbb9bab8f8f2d2fefcdecd09005..1caa0f94259618da7b63c2972b39a81562695065 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -1,4 +1,5 @@ import { Action } from '@ngrx/store' +import { TemplateRef } from '@angular/core'; const agreedCookieKey = 'agreed-cokies' const aggredKgTosKey = 'agreed-kg-tos' @@ -12,6 +13,9 @@ const defaultState : UIStateInterface = { snackbarMessage: null, + sidebarTemplate: null, + bottomSheetTemplate: null, + /** * replace with server side logic (?) */ @@ -83,6 +87,18 @@ export function uiState(state:UIStateInterface = defaultState,action:UIAction){ ...state, agreedKgTos: true } + case SHOW_SIDEBAR_TEMPLATE: + const { sidebarTemplate } = action + return { + ...state, + sidebarTemplate + } + case SHOW_BOTTOM_SHEET: + const { bottomSheetTemplate } = action + return { + ...state, + bottomSheetTemplate + } default: return state } @@ -104,6 +120,9 @@ export interface UIStateInterface{ agreedCookies: boolean agreedKgTos: boolean + + sidebarTemplate: TemplateRef<any> + bottomSheetTemplate: TemplateRef<any> } export interface UIAction extends Action{ @@ -117,6 +136,9 @@ export interface UIAction extends Action{ segment: any | null }[], snackbarMessage: string + + sidebarTemplate: TemplateRef<any> + bottomSheetTemplate: TemplateRef<any> } export const MOUSE_OVER_SEGMENT = `MOUSE_OVER_SEGMENT` @@ -131,4 +153,6 @@ export const AGREE_COOKIE = `AGREE_COOKIE` export const AGREE_KG_TOS = `AGREE_KG_TOS` export const SHOW_KG_TOS = `SHOW_KG_TOS` -export const SNACKBAR_MESSAGE = `SNACKBAR_MESSAGE` \ No newline at end of file +export const SNACKBAR_MESSAGE = `SNACKBAR_MESSAGE` +export const SHOW_SIDEBAR_TEMPLATE = `SHOW_SIDEBAR_TEMPLATE` +export const SHOW_BOTTOM_SHEET = `SHOW_BOTTOM_SHEET` \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index 5cafeeec944f4da3ae540608372cbcdcad5f07e2..25b236e98897b953e6ec167b1f038243c8534b27 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -1,10 +1,9 @@ import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; 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 { FormsModule } from "@angular/forms"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { PathToNestedChildren } from "./util/pathToNestedChildren.pipe"; import { CopyPropertyPipe } from "./util/copyProperty.pipe"; import { FilterDataEntriesbyMethods } from "./util/filterDataEntriesByMethods.pipe"; @@ -27,12 +26,20 @@ import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.modu import { DoiParserPipe } from "src/util/pipes/doiPipe.pipe"; import { DatasetIsFavedPipe } from "./util/datasetIsFaved.pipe"; import { RegionBackgroundToRgbPipe } from "./util/regionBackgroundToRgb.pipe"; +import { RegionTextSearchAutocomplete } from "../viewerStateController/regionSearch/regionSearch.component"; +import { RegionHierarchy } from "../viewerStateController/regionHierachy/regionHierarchy.component"; +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { GetKgSchemaIdFromFullIdPipe } from "./util/getKgSchemaIdFromFullId.pipe"; +import { PreviewFileIconPipe } from "./preview/previewFileIcon.pipe"; +import { PreviewFileTypePipe } from "./preview/previewFileType.pipe"; @NgModule({ imports:[ ChartsModule, CommonModule, ComponentsModule, + ReactiveFormsModule, + ScrollingModule, FormsModule, UtilModule, AngularMaterialModule, @@ -41,7 +48,6 @@ import { RegionBackgroundToRgbPipe } from "./util/regionBackgroundToRgb.pipe"; ], declarations: [ DataBrowser, - DatasetViewerComponent, ModalityPicker, PreviewComponent, FileViewer, @@ -49,6 +55,8 @@ import { RegionBackgroundToRgbPipe } from "./util/regionBackgroundToRgb.pipe"; LineChart, DedicatedViewer, SingleDatasetView, + RegionTextSearchAutocomplete, + RegionHierarchy, /** * pipes @@ -60,7 +68,10 @@ import { RegionBackgroundToRgbPipe } from "./util/regionBackgroundToRgb.pipe"; AggregateArrayIntoRootPipe, DoiParserPipe, DatasetIsFavedPipe, - RegionBackgroundToRgbPipe + RegionBackgroundToRgbPipe, + GetKgSchemaIdFromFullIdPipe, + PreviewFileIconPipe, + PreviewFileTypePipe ], exports:[ DataBrowser, @@ -68,10 +79,13 @@ import { RegionBackgroundToRgbPipe } from "./util/regionBackgroundToRgb.pipe"; PreviewComponent, ModalityPicker, FilterDataEntriesbyMethods, - FileViewer + FileViewer, + RegionTextSearchAutocomplete, + RegionHierarchy ], entryComponents:[ - DataBrowser + DataBrowser, + FileViewer ], providers: [ KgSingleDatasetService diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index e7e3c2fd77ab56126a86b009908a02fb5f67c478..f175880e7addf7d65b30f7b6d1c5410dfa171c5c 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -4,7 +4,7 @@ import { ViewerConfiguration } from "src/services/state/viewerConfig.store"; 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, FETCHED_SPATIAL_DATA, UPDATE_SPATIAL_DATA } from "src/services/stateStore.service"; -import { map, distinctUntilChanged, debounceTime, filter, tap, switchMap, catchError, shareReplay } from "rxjs/operators"; +import { map, distinctUntilChanged, debounceTime, filter, tap, switchMap, catchError, shareReplay, withLatestFrom } from "rxjs/operators"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe"; import { NO_METHODS } from "./util/filterDataEntriesByMethods.pipe"; @@ -69,14 +69,13 @@ export class DatabrowserService implements OnDestroy{ 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) - private fetchSpatialData$: Observable<any> public spatialDatasets$: Observable<any> + public viewportBoundingBox$: Observable<[Point, Point]> constructor( private workerService: AtlasWorkerService, @@ -84,6 +83,7 @@ export class DatabrowserService implements OnDestroy{ private store: Store<ViewerConfiguration>, private http: HttpClient ){ + this.kgTos$ = this.http.get(`${this.constantService.backendUrl}datasets/tos`, { responseType: 'text' }).pipe( @@ -100,13 +100,6 @@ export class DatabrowserService implements OnDestroy{ shareReplay(1) ) - this.subscriptions.push( - this.store.pipe( - select('ngViewerState') - ).subscribe(layersInterface => - this.ngLayers = new Set(layersInterface.layers.map(l => l.source.replace(/^nifti\:\/\//, '')))) - ) - this.subscriptions.push( store.pipe( select('dataStore'), @@ -117,46 +110,45 @@ export class DatabrowserService implements OnDestroy{ }) ) - this.fetchSpatialData$ = combineLatest( - this.store.pipe( - select('viewerState'), - select('navigation') - ), - this.store.pipe( + this.viewportBoundingBox$ = this.store.pipe( select('viewerState'), - select('templateSelected') + select('navigation'), + distinctUntilChanged(), + debounceTime(SPATIAL_SEARCH_DEBOUNCE), + filter(v => !!v), + map(navigation => { + + // in mm + const center = navigation.position.map(n=>n/1e6) + const searchWidth = this.constantService.spatialWidth / 4 * navigation.zoom / 1e6 + const pt1 = center.map(v => (v - searchWidth)) as [number, number, number] + const pt2 = center.map(v => (v + searchWidth)) as [number, number, number] + + return [pt1, pt2] as [Point, Point] + }) ) - ).pipe( - debounceTime(SPATIAL_SEARCH_DEBOUNCE) - ) - this.spatialDatasets$ = this.fetchSpatialData$.pipe( - filter(([navigation, templateSelected]) => !!navigation && !!navigation.position && !!templateSelected && !!templateSelected.name), - switchMap(([navigation, templateSelected]) => { + this.spatialDatasets$ = this.viewportBoundingBox$.pipe( + withLatestFrom(this.store.pipe( + select('viewerState'), + select('templateSelected'), + distinctUntilChanged(), + filter(v => !!v) + )), + switchMap(([ bbox, templateSelected ]) => { + const _bbox = bbox.map(pt => pt.map(v => v.toFixed(SPATIAL_SEARCH_PRECISION))) /** - * templateSelected and templateSelected.name must be defined for spatial search - */ - if (!templateSelected || !templateSelected.name) - return from(Promise.reject('templateSelected must not be empty')) - const encodedTemplateName = encodeURI(templateSelected.name) - - // in mm - const center = navigation.position.map(n=>n/1e6) - const searchWidth = this.constantService.spatialWidth / 4 * navigation.zoom / 1e6 - const pt1 = center.map(v => (v - searchWidth).toFixed(SPATIAL_SEARCH_PRECISION)) - const pt2 = center.map(v => (v + searchWidth).toFixed(SPATIAL_SEARCH_PRECISION)) - - return from(fetch(`${this.constantService.backendUrl}datasets/spatialSearch/templateName/${encodedTemplateName}/bbox/${pt1.join('_')}__${pt2.join("_")}`) - .then(res => res.json())) - }), - /** - * TODO pipe to constantService.catchError - */ - catchError((err) => (console.log(err), of([]))) - ) - - this.fetchDataObservable$ = combineLatest( + * templateSelected and templateSelected.name must be defined for spatial search + */ + if (!templateSelected || !templateSelected.name) return from(Promise.reject('templateSelected must not be empty')) + const encodedTemplateName = encodeURI(templateSelected.name) + return from(fetch(`${this.constantService.backendUrl}datasets/spatialSearch/templateName/${encodedTemplateName}/bbox/${_bbox[0].join('_')}__${_bbox[1].join("_")}`).then(res => res.json())) + }), + catchError((err) => (console.log(err), of([]))) + ) + + this.fetchDataObservable$ = combineLatest( this.store.pipe( select('viewerState'), safeFilter('templateSelected'), @@ -173,10 +165,6 @@ export class DatabrowserService implements OnDestroy{ this.manualFetchDataset$ ) - this.fetchDataStatus$ = combineLatest( - this.fetchDataObservable$ - ) - this.subscriptions.push( this.spatialDatasets$.subscribe(arr => { this.store.dispatch({ @@ -217,6 +205,13 @@ export class DatabrowserService implements OnDestroy{ this.subscriptions.forEach(s => s.unsubscribe()) } + public toggleFav(dataentry: DataEntry){ + this.store.dispatch({ + type: DATASETS_ACTIONS_TYPES.TOGGLE_FAV_DATASET, + payload: dataentry + }) + } + public saveToFav(dataentry: DataEntry){ this.store.dispatch({ type: DATASETS_ACTIONS_TYPES.FAV_DATASET, @@ -241,21 +236,6 @@ export class DatabrowserService implements OnDestroy{ }) } - 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 - }) - } - private dispatchData(arr:DataEntry[]){ this.store.dispatch({ type : FETCHED_DATAENTRIES, @@ -319,15 +299,6 @@ export class DatabrowserService implements OnDestroy{ }) } - removeNgLayer({ url }) { - this.store.dispatch({ - type : REMOVE_NG_LAYER, - layer : { - name : url - } - }) - } - rebuildRegionTree(selectedRegions, regions){ this.workerService.worker.postMessage({ type: 'BUILD_REGION_SELECTION_TREE', @@ -389,3 +360,5 @@ export interface CountedDataModality{ occurance: number visible: boolean } + +type Point = [number, number, number] \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.useEffect.ts b/src/ui/databrowserModule/databrowser.useEffect.ts index 01a594fe4ae54e52c7fda10347ab11f047ddef54..11341c3553f45dac39b68441afe299041018d129 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.ts @@ -26,6 +26,23 @@ export class DataBrowserUseEffect implements OnDestroy{ select('favDataEntries') ) + this.toggleDataset$ = this.actions$.pipe( + ofType(DATASETS_ACTIONS_TYPES.TOGGLE_FAV_DATASET), + withLatestFrom(this.favDataEntries$), + map(([action, prevFavDataEntries]) => { + const { payload = {} } = action as any + const { id } = payload + + const wasFav = prevFavDataEntries.findIndex(ds => ds.id === id) >= 0 + return { + type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, + favDataEntries: wasFav + ? prevFavDataEntries.filter(ds => ds.id !== id) + : prevFavDataEntries.concat(payload) + } + }) + ) + this.unfavDataset$ = this.actions$.pipe( ofType(DATASETS_ACTIONS_TYPES.UNFAV_DATASET), withLatestFrom(this.favDataEntries$), @@ -139,6 +156,9 @@ export class DataBrowserUseEffect implements OnDestroy{ @Effect() public unfavDataset$: Observable<any> + + @Effect() + public toggleDataset$: Observable<any> } const LOCAL_STORAGE_CONST = { diff --git a/src/ui/databrowserModule/databrowser/databrowser.component.ts b/src/ui/databrowserModule/databrowser/databrowser.component.ts index 3b9c6ace0ddeecca61f7fd57fadb5eaa940576d7..8190e7d302b4b49a22cc5995382fc2806df193e9 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.component.ts +++ b/src/ui/databrowserModule/databrowser/databrowser.component.ts @@ -1,20 +1,26 @@ -import { Component, OnDestroy, OnInit, ViewChild, Input } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, Input, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges, Output,EventEmitter, TemplateRef } from "@angular/core"; import { DataEntry } from "src/services/stateStore.service"; import { Subscription, merge, Observable } from "rxjs"; import { DatabrowserService, CountedDataModality } from "../databrowser.service"; import { ModalityPicker } from "../modalityPicker/modalityPicker.component"; +import { MatDialog } from "@angular/material"; +import { KgSingleDatasetService } from "../kgSingleDatasetService.service"; +import { scan, shareReplay } from "rxjs/operators"; +import { ViewerPreviewFile } from "src/services/state/dataStore.store"; + +const scanFn: (acc: any[], curr: any) => any[] = (acc, curr) => [curr, ...acc] @Component({ selector : 'data-browser', templateUrl : './databrowser.template.html', styleUrls : [ `./databrowser.style.css` - ] + ], + exportAs: 'dataBrowser', + changeDetection: ChangeDetectionStrategy.OnPush }) -export class DataBrowser implements OnDestroy,OnInit{ - - public favedDataentries$: Observable<DataEntry[]> +export class DataBrowser implements OnChanges, OnDestroy,OnInit{ @Input() public regions: any[] = [] @@ -25,7 +31,11 @@ export class DataBrowser implements OnDestroy,OnInit{ @Input() public parcellation: any + @Output() + dataentriesUpdated: EventEmitter<DataEntry[]> = new EventEmitter() + public dataentries: DataEntry[] = [] + public focusedDataset: DataEntry public currentPage: number = 0 public hitsPerPage: number = 5 @@ -39,9 +49,16 @@ export class DataBrowser implements OnDestroy,OnInit{ public countedDataM: CountedDataModality[] = [] public visibleCountedDataM: CountedDataModality[] = [] + public history$: Observable<{file:ViewerPreviewFile, dataset: DataEntry}[]> + @ViewChild(ModalityPicker) modalityPicker: ModalityPicker + @ViewChild('detailDataset', {read: TemplateRef}) + detailDatasetTemplateRef: TemplateRef<any> + + public favDataentries$: Observable<DataEntry[]> + get darktheme(){ return this.dbService.darktheme } @@ -55,13 +72,20 @@ export class DataBrowser implements OnDestroy,OnInit{ public gemoetryFilter: any constructor( - private dbService: DatabrowserService + private dbService: DatabrowserService, + private cdr:ChangeDetectorRef, + private dialog: MatDialog, + private singleDatasetSservice: KgSingleDatasetService ){ - this.favedDataentries$ = this.dbService.favedDataentries$ + this.favDataentries$ = this.dbService.favedDataentries$ + this.history$ = this.singleDatasetSservice.previewingFile$.pipe( + scan(scanFn, []), + shareReplay(1) + ) } - ngOnInit(){ - this.dbService.dbComponentInit(this) + ngOnChanges(changes){ + this.regions = this.regions.map(r => { /** * TODO to be replaced with properly region UUIDs from KG @@ -73,6 +97,14 @@ export class DataBrowser implements OnDestroy,OnInit{ }) const { regions, parcellation, template } = this this.fetchingFlag = true + + /** + * reconstructing parcellation region is async (done on worker thread) + * if parcellation region is not yet defined, return. + * parccellation will eventually be updated with the correct region + */ + if (!parcellation.regions) return + this.dbService.getDataByRegion({ regions, parcellation, template }) .then(de => { this.dataentries = de @@ -88,8 +120,17 @@ export class DataBrowser implements OnDestroy,OnInit{ }) .finally(() => { this.fetchingFlag = false + this.dataentriesUpdated.emit(this.dataentries) + this.cdr.markForCheck() }) + } + + ngOnInit(){ + /** + * TODO gets init'ed everytime when appends to ngtemplateoutlet + */ + this.dbService.dbComponentInit(this) this.subscriptions.push( merge( // this.dbService.selectedRegions$, @@ -138,6 +179,10 @@ export class DataBrowser implements OnDestroy,OnInit{ this.dbService.manualFetchDataset$.next(null) } + toggleFavourite(dataset: DataEntry){ + this.dbService.toggleFav(dataset) + } + saveToFavourite(dataset: DataEntry){ this.dbService.saveToFav(dataset) } @@ -165,6 +210,11 @@ export class DataBrowser implements OnDestroy,OnInit{ resetFilters(event?:MouseEvent){ this.clearAll() } + + showFocusedDataset(dataset:DataEntry){ + this.focusedDataset = dataset + this.dialog.open(this.detailDatasetTemplateRef) + } } export interface DataEntryFilter{ diff --git a/src/ui/databrowserModule/databrowser/databrowser.style.css b/src/ui/databrowserModule/databrowser/databrowser.style.css index cbed0d7d4ce53d523971d02ff5bf1f3d6323de87..1e2d2b6162c93d87c9f0d37724d42e1445c2bcc9 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.style.css +++ b/src/ui/databrowserModule/databrowser/databrowser.style.css @@ -111,11 +111,6 @@ div[noSelectedRegion] background-color: rgba(0,0,0,0.2); } -dataset-viewer -{ - display: block; -} - div[regionsContainer] { background-color:rgba(0, 0, 0, 0.3); @@ -133,14 +128,6 @@ modality-picker display:inline-block; } -.filterWrapper -{ - white-space:normal; - max-height: 15em; - overflow-y:auto; - overflow-x:hidden; -} - .spinnerAnimationCircleContainer { display: flex; @@ -180,6 +167,12 @@ radio-list display: block; } +:host +{ + height: 100%; + width: 100%; +} + /* datawrapper */ :host .dataEntryWrapper { @@ -196,7 +189,6 @@ radio-list white-space: initial; width: 50%; display:inline-block; - padding: 0.5em 1em; } .filePreviewContainer @@ -205,23 +197,6 @@ radio-list overflow:auto; } -.overflow-container -{ - overflow:hidden; - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} - -.dot -{ - width: 0.5rem; - height: 0.5rem; - display: inline-block; - border-radius: 50%; - background-color:white; -} div[regionTagsContainer] { diff --git a/src/ui/databrowserModule/databrowser/databrowser.template.html b/src/ui/databrowserModule/databrowser/databrowser.template.html index 9df68602e046068f4758d8445076d6b10ac55040..7e802978d7b0b34e06f3c4d9b369531159072418 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.template.html +++ b/src/ui/databrowserModule/databrowser/databrowser.template.html @@ -1,83 +1,112 @@ -<div class="overflow-container"> - - <div - [ngStyle]="filePreviewName ? {'transform': 'translateX(-50%)'} : {}" - class="dataEntryWrapper"> - - <!-- main window --> - <div - class="t-a-ease-500" - [style.height]="filePreviewName ? '0px' : 'auto'"> - - <!-- description --> - <readmore-component> - <div class="p-2 mh-20em overflow-auto"> - Datasets relevant to - <span - *ngFor="let region of regions" - class="badge badge-secondary mr-1 mw-100"> - <span [ngStyle]="{backgroundColor: (region | regionBackgroundToRgbPipe)}" class="dot"> - +<mat-tab-group [dynamicHeight]="false" class="h-100 w-100 overflow-hidden"> + <mat-tab label="Search Criteria"> + <ng-container *ngTemplateOutlet="searchPanel"> + + </ng-container> + </mat-tab> + <mat-tab label="History"> + <ng-container *ngTemplateOutlet="history"> + + </ng-container> + </mat-tab> +</mat-tab-group> + +<ng-template #searchPanel> + + <div class="h-100 w-100 d-flex flex-column overflow-hidden"> + + <!-- search criterial --> + <mat-selection-list class="flex-grow-0 flex-shrink-0 mb-2" checkboxPosition="before"> + <h3 mat-subheader> + Search criteria + </h3> + + <!-- selected regions --> + <mat-list-option + selected="true" + checkboxPosition="before"> + <h4 mat-line> + {{ regions.length }} selected region{{ regions.length !== 1 ? 's' : ''}} + + <!-- stop mousedown propagation to avoid ripple from mat-list-option --> + <region-text-search-autocomplete (mousedown)="$event.stopPropagation()" (click)="$event.stopPropagation()" [showAutoComplete]="false"> + </region-text-search-autocomplete> + </h4> + </mat-list-option> + + <mat-list-option + *ngIf="dbService.viewportBoundingBox$ | async as bbox" + checkboxPosition="before" + [selected]="true"> + <h4 class="mat-line"> + viewport bounding box + </h4> + + <!-- from --> + <p class="mat-line"> + <small> + <span *ngFor="let v of bbox[0]; let lastval = last"> + {{ v | number : '1.2-2' }}<span *ngIf="!lastval">, </span> </span> - <span class="d-inline-block mw-100 overflow-hidden text-truncate"> - {{ region.name }} + <span> + to </span> - </span> - </div> - </readmore-component> - - <!-- modality picker --> - <!-- TODO use material for popover, then remove popover module and ngx/bootstrap --> - <div> - <span - placement="right" - 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> - - <!-- datasets container --> - <div - *ngIf="fetchingFlag; else fetched" - class="spinnerAnimationCircleContainer"> - <div class="spinnerAnimationCircle"></div> - <div>Fetching datasets...</div> - </div> + <span *ngFor="let v of bbox[1]; let lastval = last"> + {{ v | number : '1.2-2' }}<span *ngIf="!lastval">, </span> + </span> + </small> + </p> + + </mat-list-option> + </mat-selection-list> - </div> + <mat-divider class="position-relative"></mat-divider> + + <!-- modality picker / filter --> + <mat-accordion class="flex-grow-0 flex-shrink-0 mb-2"> + <mat-expansion-panel> + <mat-expansion-panel-header> + <mat-panel-title> + Filter + </mat-panel-title> + <mat-panel-description> + <i *ngIf="dataentries.length > 0"> + <span *ngIf="visibleCountedDataM && visibleCountedDataM.length > 0 "> + {{ (dataentries | filterDataEntriesByMethods : visibleCountedDataM).length }} filtered / + </span> + {{ dataentries.length }} results + </i> + <i *ngIf="dataentries.length === 0"> + No results to show. + </i> + </mat-panel-description> + </mat-expansion-panel-header> + + <ng-container *ngTemplateOutlet="modalityPicker"> + + </ng-container> + </mat-expansion-panel> + </mat-accordion> - <!-- 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"> - - </preview-component> - - </div> + <!-- datasets container --> + <div *ngIf="fetchingFlag; else fetched" + class="spinnerAnimationCircleContainer"> + <div class="spinnerAnimationCircle"></div> + <div>Fetching datasets...</div> </div> + </div> -</div> + +</ng-template> <ng-template #modalityPicker> - <div class="filterWrapper"> - <modality-picker - (click)="$event.stopPropagation();" - class="mw-100" - [countedDataM]="countedDataM" - (modalityFilterEmitter)="handleModalityFilterEvent($event)"> - - </modality-picker> - </div> + <modality-picker + (click)="$event.stopPropagation();" + class="w-100" + [countedDataM]="countedDataM" + (modalityFilterEmitter)="handleModalityFilterEvent($event)"> + + </modality-picker> </ng-template> <ng-template #fetched> @@ -88,63 +117,50 @@ <ng-template #showData> <!-- datawrapper --> - <div *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry" -> + <ng-container *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry"> <!-- dataentries --> - <div class="dataEntry"> - <div> - <i *ngIf="dataentries.length > 0"> - {{ dataentries.length }} total results. - <span - *ngIf="visibleCountedDataM.length > 0 "> - {{ filteredDataEntry.length }} - filtered results. - <a - href="#" - class="btn btn-sm btn-link" - (click)="resetFilters($event)">reset filters - </a> - </span> - </i> - <i *ngIf="dataentries.length === 0"> - No results to show. - </i> - </div> - <div *ngIf="dataentries.length > 0"> - <dataset-viewer - class="mt-1" - *ngFor="let dataset of filteredDataEntry | searchResultPagination : currentPage : hitsPerPage" - (saveToFavourite)="saveToFavourite(dataset)" - (removeFromFavourite)="removeFromFavourite(dataset)" - (showPreviewDataset)="onShowPreviewDataset($event)" - [dataset]="dataset" - [isFaved]="favedDataentries$ | async | datasetIsFaved : dataset"> - <div regionTagsContainer> - - <!-- TODO may want to separate the region badge into a separate component --> - <span - *ngFor="let region of dataset.parcellationRegion" - class="badge badge-secondary mr-1 mw-100"> - <span [ngStyle]="{backgroundColor:(region | regionBackgroundToRgbPipe)}" class="dot"> - - </span> - <span class="d-inline-block mw-100 overflow-hidden text-truncate"> - {{ region.name }} - </span> - </span> - </div> - </dataset-viewer> - </div> - - <pagination-component - (paginationChange)="currentPage = $event" - [hitsPerPage]="hitsPerPage" - [total]="filteredDataEntry.length" - [currentPage]="currentPage"> - </pagination-component> - </div> + <div class="h-100 w-100"> + <cdk-virtual-scroll-viewport + class="h-100" + itemSize="50"> + <single-dataset-view + *cdkVirtualFor="let dataset of filteredDataEntry" + (click)="showFocusedDataset(dataset)" + class="m-2" + [kgSchema]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[0]" + [kgId]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[1]" + [prefetched]="dataset" + [simpleMode]="true" + [ripple]="true"> + </single-dataset-view> + </cdk-virtual-scroll-viewport> </div> + + </ng-container> +</ng-template> + +<ng-template #filePreviewTemplate> + <preview-component + *ngIf="filePreviewName" + [datasetName]="filePreviewName"> + + </preview-component> </ng-template> +<ng-template #detailDataset> + + <single-dataset-view + [prefetched]="focusedDataset"> + + </single-dataset-view> +</ng-template> + +<ng-template #history> + <mat-list> + <mat-list-item *ngFor="let item of (history$ | async)"> + Viewed {{ item.file.name }} + </mat-list-item> + </mat-list> +</ng-template> \ No newline at end of file diff --git a/src/ui/databrowserModule/datasetViewer/datasetViewer.component.ts b/src/ui/databrowserModule/datasetViewer/datasetViewer.component.ts deleted file mode 100644 index aea172ffcc434d3fef4bc11c479ef2333a0e448c..0000000000000000000000000000000000000000 --- a/src/ui/databrowserModule/datasetViewer/datasetViewer.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core"; -import { DataEntry } from "src/services/stateStore.service"; - -@Component({ - selector : 'dataset-viewer', - templateUrl : './datasetViewer.template.html', - styleUrls : ['./datasetViewer.style.css'] -}) - -export class DatasetViewerComponent{ - @Input() dataset : DataEntry - @Input() isFaved: boolean - - @Output() showPreviewDataset: EventEmitter<{datasetName:string, event:MouseEvent}> = new EventEmitter() - @ViewChild('kgrRef', {read:ElementRef}) kgrRef: ElementRef - - previewDataset(event:MouseEvent){ - if (!this.dataset.preview) return - this.showPreviewDataset.emit({ - event, - datasetName: this.dataset.name - }) - event.stopPropagation() - } - - clickMainCard(event:MouseEvent) { - if (this.kgrRef) this.kgrRef.nativeElement.click() - } - - get methods(): string[]{ - return this.dataset.activity.reduce((acc, act) => { - return acc.concat(act.methods) - }, []) - } - - get hasKgRef(): boolean{ - return this.kgReference.length > 0 - } - - get kgReference(): string[] { - return this.dataset.kgReference.map(ref => `https://doi.org/${ref}`) - } - - /** - * Dummy functions, the store.dispatch is the important function - */ - @Output() - saveToFavourite: EventEmitter<boolean> = new EventEmitter() - - @Output() - removeFromFavourite: EventEmitter<boolean> = new EventEmitter() - - saveToFav(){ - this.saveToFavourite.emit() - } - - removeFromFav(){ - this.removeFromFavourite.emit() - } -} \ No newline at end of file diff --git a/src/ui/databrowserModule/datasetViewer/datasetViewer.style.css b/src/ui/databrowserModule/datasetViewer/datasetViewer.style.css deleted file mode 100644 index 17836b855bb96876f0781e7a30e4974aca789d72..0000000000000000000000000000000000000000 --- a/src/ui/databrowserModule/datasetViewer/datasetViewer.style.css +++ /dev/null @@ -1,26 +0,0 @@ -flat-tree-component -{ - overflow: hidden; - white-space: nowrap; -} - -:host-context([darktheme="true"]) hr -{ - border-color: rgba(0, 0, 0, 0.5); -} - -.dataset-pill -{ - font-size:80%; -} - -.ds-container -{ - - background-color: rgba(128, 128, 128, 0.2); -} - -.preview-container -{ - flex: 0 0 2em; -} \ No newline at end of file diff --git a/src/ui/databrowserModule/datasetViewer/datasetViewer.template.html b/src/ui/databrowserModule/datasetViewer/datasetViewer.template.html deleted file mode 100644 index fc1a6fe78977bdd21750a3015741cf1f996efcf3..0000000000000000000000000000000000000000 --- a/src/ui/databrowserModule/datasetViewer/datasetViewer.template.html +++ /dev/null @@ -1,44 +0,0 @@ -<div class="d-flex"> - <div - *ngIf="dataset; else defaultDisplay" - (click)="clickMainCard($event)" - [ngClass]="{'muted': !hasKgRef}" - class="p-2 ds-container main-container" - [hoverable]="{translateY:-2,disable:!hasKgRef}"> - <ng-content select="[regionTagsContainer]"> - - </ng-content> - <div title> - {{ dataset.name }} - </div> - - <a - *ngFor="let kgr of kgReference" - class="btn btn-sm btn-link" - target="_blank" - [href]="kgr" - hidden - #kgrRef> - Show more info on this dataset <i class="fas fa-external-link-alt"></i> - </a> - </div> - - <div - *ngIf="dataset.preview" - (click)="previewDataset($event)" - class="ds-container ml-1 p-2 preview-container text-muted d-flex align-items-center" - [hoverable]="{translateY:-3}"> - <i class="fas fa-eye"></i> - </div> - - <div - (click)="isFaved ? removeFromFav() : saveToFav()" - [class]="(isFaved ? 'text-primary' : 'text-muted') + ' ds-container ml-1 p-2 preview-container d-flex align-items-center'" - [hoverable]="{translateY:-3}"> - <i class="fas fa-thumbtack"></i> - </div> -</div> - -<ng-template #defaultDisplay> - Nothing to display ... -</ng-template> diff --git a/src/ui/databrowserModule/fileviewer/dedicated/dedicated.component.ts b/src/ui/databrowserModule/fileviewer/dedicated/dedicated.component.ts index 218bb6d37bec99694a1e6fddec6fd306343a9021..ce27a423d9f547aa9323fce09d8a1bd3b88f9b49 100644 --- a/src/ui/databrowserModule/fileviewer/dedicated/dedicated.component.ts +++ b/src/ui/databrowserModule/fileviewer/dedicated/dedicated.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from "@angular/core"; import { ViewerPreviewFile } from "src/services/state/dataStore.store"; import { DatabrowserService } from "../../databrowser.service"; +import { KgSingleDatasetService } from "../../kgSingleDatasetService.service"; @Component({ selector : 'dedicated-viewer', @@ -14,21 +15,21 @@ export class DedicatedViewer{ @Input() previewFile : ViewerPreviewFile constructor( - private dbService:DatabrowserService, + private singleKgDsService:KgSingleDatasetService, ){ } get isShowing(){ - return this.dbService.ngLayers.has(this.previewFile.url) + return this.singleKgDsService.ngLayers.has(this.previewFile.url) } showDedicatedView(){ - this.dbService.showNewNgLayer({ url: this.previewFile.url }) + this.singleKgDsService.showNewNgLayer({ url: this.previewFile.url }) } removeDedicatedView(){ - this.dbService.removeNgLayer({ url: this.previewFile.url }) + this.singleKgDsService.removeNgLayer({ url: this.previewFile.url }) } click(event:MouseEvent){ diff --git a/src/ui/databrowserModule/fileviewer/fileviewer.component.ts b/src/ui/databrowserModule/fileviewer/fileviewer.component.ts index 2f5cc8224a750516dc10226062b8028d0e74cb28..95e4b9fcce7256a4d5ded0d174da8d1e00fbb522 100644 --- a/src/ui/databrowserModule/fileviewer/fileviewer.component.ts +++ b/src/ui/databrowserModule/fileviewer/fileviewer.component.ts @@ -1,9 +1,10 @@ -import { Component, Input, OnChanges, OnDestroy, ViewChild, ElementRef, OnInit, Output, EventEmitter } from '@angular/core' +import { Component, Input, OnChanges, OnDestroy, ViewChild, ElementRef, OnInit, Output, EventEmitter, Inject, Optional } from '@angular/core' import { DomSanitizer } from '@angular/platform-browser'; import { interval,from } from 'rxjs'; import { switchMap,take,retry } from 'rxjs/operators' import { ViewerPreviewFile } from 'src/services/state/dataStore.store'; +import { MAT_DIALOG_DATA } from '@angular/material'; @Component({ @@ -23,8 +24,10 @@ export class FileViewer implements OnChanges,OnDestroy,OnInit{ @ViewChild('childChart') childChart : ChartComponentInterface constructor( + @Optional() @Inject(MAT_DIALOG_DATA) data, private sanitizer:DomSanitizer ){ + if (data) this.previewFile = data.previewFile } private _downloadUrl : string diff --git a/src/ui/databrowserModule/fileviewer/line/line.chart.template.html b/src/ui/databrowserModule/fileviewer/line/line.chart.template.html index 84c6916deae37b8d72866e33e1f60cf089ad64af..760ae982d3d3379e70b5b50cd9089da8d66bd7d1 100644 --- a/src/ui/databrowserModule/fileviewer/line/line.chart.template.html +++ b/src/ui/databrowserModule/fileviewer/line/line.chart.template.html @@ -12,6 +12,7 @@ #canvas> </canvas> <div class="w-100 d-flex justify-content-end"> + <!-- TODO ngIF expression changed after checked --> <a hidden #DownloadLineChartLink class="outline-none" @@ -19,7 +20,7 @@ [href]="csvDataUrl" container="body" *ngIf="shapedLineChartDatasets" - matTooltip="Download line graph as csv"> + matTooltip="Download as csv"> <i class="fas fa-file-csv"></i> <!-- <i class="fas fa-file-csv"></i><span class="ml-2">Download line graph as csv</span>--> </a> diff --git a/src/ui/databrowserModule/kgSingleDatasetService.service.ts b/src/ui/databrowserModule/kgSingleDatasetService.service.ts index 551b2cf34d1b2c7f70b93e085371c910c0ef35bf..418704c4d12a965a51e84071870a7c0a35a35fd5 100644 --- a/src/ui/databrowserModule/kgSingleDatasetService.service.ts +++ b/src/ui/databrowserModule/kgSingleDatasetService.service.ts @@ -1,10 +1,42 @@ -import { Injectable } from "@angular/core"; +import { Injectable, TemplateRef, OnDestroy } from "@angular/core"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service" +import { Store, select } from "@ngrx/store"; +import { SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; +import { ViewerPreviewFile, DataEntry } from "src/services/state/dataStore.store"; +import { determinePreviewFileType, PREVIEW_FILE_TYPES } from "./preview/previewFileIcon.pipe"; +import { MatDialog } from "@angular/material"; +import { FileViewer } from "./fileviewer/fileviewer.component"; +import { ADD_NG_LAYER, REMOVE_NG_LAYER } from "src/services/stateStore.service"; +import { Subscription, Subject } from "rxjs"; @Injectable({ providedIn: 'root' }) -export class KgSingleDatasetService { +export class KgSingleDatasetService implements OnDestroy{ + + public previewingFile$: Subject<{file:ViewerPreviewFile, dataset: DataEntry}> = new Subject() + + private subscriptions: Subscription[] = [] + public ngLayers : Set<string> = new Set() + + constructor( + private constantService: AtlasViewerConstantsServices, + private store$: Store<any>, + private dialog: MatDialog + ) { + + this.subscriptions.push( + this.store$.pipe( + select('ngViewerState') + ).subscribe(layersInterface => { + this.ngLayers = new Set(layersInterface.layers.map(l => l.source.replace(/^nifti\:\/\//, ''))) + }) + ) - constructor(private constantService: AtlasViewerConstantsServices) { + } + + ngOnDestroy(){ + while (this.subscriptions.length > 0) { + this.subscriptions.pop().unsubscribe() + } } public getInfoFromKg({ kgId, kgSchema = 'minds/core/dataset/v1.0.0' }: Partial<KgQueryInterface>) { @@ -40,9 +72,61 @@ export class KgSingleDatasetService { anchor.href = url; anchor.click(); } + + public showPreviewList(template: TemplateRef<any>){ + this.store$.dispatch({ + type: SHOW_BOTTOM_SHEET, + bottomSheetTemplate: template + }) + } + + public previewFile(file:ViewerPreviewFile, dataset: DataEntry) { + this.previewingFile$.next({ + file, + dataset + }) + const type = determinePreviewFileType(file) + if (type === PREVIEW_FILE_TYPES.NIFTI) { + this.store$.dispatch({ + type: SHOW_BOTTOM_SHEET, + bottomSheetTemplate: null + }) + const { url } = file + this.showNewNgLayer({ url }) + return + } + this.dialog.open(FileViewer, { + data: { + previewFile: file + } + }) + } + + 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, + layer : { + name : url + } + }) + } } interface KgQueryInterface{ kgSchema: string kgId: string -} \ 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 3b98629ee72bc7cad5fe813ef8aaa846e2b03e50..df41aab07fc807183ce87f68846d37ea65578ec1 100644 --- a/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css +++ b/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css @@ -2,9 +2,3 @@ div { white-space: nowrap; } - -.clickable:hover -{ - color:#dbb556; - cursor:default; -} diff --git a/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html b/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html index 35b537a72ffe9207979e2777a3ee0815cf5e1dad..34052b616b6716623d81b0e249a108bc41eeee52 100644 --- a/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html +++ b/src/ui/databrowserModule/modalityPicker/modalityPicker.template.html @@ -1,18 +1,15 @@ -<div class="ws-initial"> - <div - *ngIf="checkedModality.length > 0" +<div> + <button (click)="clearAll()" - class="btn btn-sm btn-link "> - clear all - </div> + [disabled]="checkedModality.length === 0" + mat-raised-button> + Clear all + </button> </div> -<div - *ngFor="let datamodality of countedDataM" - (click)="toggleModality(datamodality)" - class="clickable"> - <i [ngClass]="datamodality.visible ? 'far fa-check-square' : 'text-muted far fa-square'"> - - </i> +<mat-checkbox + [checked]="datamodality.visible" + (change)="toggleModality(datamodality)" + *ngFor="let datamodality of countedDataM"> {{ datamodality.name }} <span class="text-muted">({{ datamodality.occurance }})</span> -</div> +</mat-checkbox> \ No newline at end of file diff --git a/src/ui/databrowserModule/preview/preview.component.ts b/src/ui/databrowserModule/preview/preview.component.ts index 19921de6402afa653c9e2b6ec68352df13e7dfdd..4748a11b5216dd053cd34e9a926d350654571414 100644 --- a/src/ui/databrowserModule/preview/preview.component.ts +++ b/src/ui/databrowserModule/preview/preview.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; import { DatabrowserService } from "../databrowser.service"; import { ViewerPreviewFile } from "src/services/state/dataStore.store"; @@ -10,7 +10,7 @@ const getRenderNodeFn = ({name : activeFileName = ''} = {}) => ({name = '', path @Component({ selector: 'preview-component', - templateUrl: './preview.template.html', + templateUrl: './previewList.template.html', styleUrls: [ './preview.style.css' ] @@ -18,6 +18,9 @@ const getRenderNodeFn = ({name : activeFileName = ''} = {}) => ({name = '', path export class PreviewComponent implements OnInit{ @Input() datasetName: string + @Output() previewFile: EventEmitter<ViewerPreviewFile> = new EventEmitter() + + public fetchCompleteFlag: boolean = false public previewFiles: ViewerPreviewFile[] = [] public activeFile: ViewerPreviewFile @@ -56,6 +59,9 @@ export class PreviewComponent implements OnInit{ .catch(e => { this.error = JSON.stringify(e) }) + .finally(() => { + this.fetchCompleteFlag = true + }) } } -} \ No newline at end of file +} diff --git a/src/ui/databrowserModule/preview/preview.style.css b/src/ui/databrowserModule/preview/preview.style.css index 23018c4cb9c1562bb329a8416a148bb819a503ed..627947e7c8a27910c5dc1007cdb650ca895791ea 100644 --- a/src/ui/databrowserModule/preview/preview.style.css +++ b/src/ui/databrowserModule/preview/preview.style.css @@ -1,9 +1,3 @@ -:host -{ - display: block; - width: 100%; -} - .readmore-wrapper { font-size: 80%; diff --git a/src/ui/databrowserModule/preview/previewFileIcon.pipe.ts b/src/ui/databrowserModule/preview/previewFileIcon.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..717147da325568c890f55b0b9240cd40ab12fa30 --- /dev/null +++ b/src/ui/databrowserModule/preview/previewFileIcon.pipe.ts @@ -0,0 +1,47 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { ViewerPreviewFile } from "src/services/state/dataStore.store"; + +@Pipe({ + name: 'previewFileIconPipe' +}) + +export class PreviewFileIconPipe implements PipeTransform{ + public transform(previewFile: ViewerPreviewFile):{fontSet: string, fontIcon:string}{ + const type = determinePreviewFileType(previewFile) + if (type === PREVIEW_FILE_TYPES.NIFTI) return { + fontSet: 'fas', + fontIcon: 'fa-brain' + } + + if (type === PREVIEW_FILE_TYPES.IMAGE) return { + fontSet: 'fas', + fontIcon: 'fa-image' + } + + if (type === PREVIEW_FILE_TYPES.CHART) return { + fontSet: 'far', + fontIcon: 'fa-chart-bar' + } + + return { + fontSet: 'fas', + fontIcon: 'fa-file' + } + } +} + +export const determinePreviewFileType = (previewFile: ViewerPreviewFile) => { + const { mimetype, data } = previewFile + const { chartType = null } = data || {} + if ( mimetype === 'application/nifti' ) return PREVIEW_FILE_TYPES.NIFTI + if ( /^image/.test(mimetype)) return PREVIEW_FILE_TYPES.IMAGE + if ( /application\/json/.test(mimetype) && (chartType === 'line' || chartType === 'radar')) return PREVIEW_FILE_TYPES.CHART + return PREVIEW_FILE_TYPES.OTHER +} + +export const PREVIEW_FILE_TYPES = { + NIFTI: 'NIFTI', + IMAGE: 'IMAGE', + CHART: 'CHART', + OTHER: 'OTHER' +} diff --git a/src/ui/databrowserModule/preview/previewFileType.pipe.ts b/src/ui/databrowserModule/preview/previewFileType.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bba2c39f45564cfe3f4ac1f8f00529c143297f3 --- /dev/null +++ b/src/ui/databrowserModule/preview/previewFileType.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { ViewerPreviewFile } from "src/services/state/dataStore.store"; +import { determinePreviewFileType } from "./previewFileIcon.pipe"; + +@Pipe({ + name: 'previewFileTypePipe' +}) + +export class PreviewFileTypePipe implements PipeTransform{ + public transform(file: ViewerPreviewFile): string{ + return determinePreviewFileType(file) + } +} \ No newline at end of file diff --git a/src/ui/databrowserModule/preview/previewList.template.html b/src/ui/databrowserModule/preview/previewList.template.html new file mode 100644 index 0000000000000000000000000000000000000000..e98b7e3017e9eb7deb7c0620ccd68469dd8dd028 --- /dev/null +++ b/src/ui/databrowserModule/preview/previewList.template.html @@ -0,0 +1,18 @@ +<mat-nav-list *ngIf="fetchCompleteFlag; else loadingPlaceholder"> + <h3 mat-subheader>Available Preview Files</h3> + <mat-list-item + *ngFor="let file of previewFiles" + (click)="previewFile.emit(file)"> + <mat-icon + [fontSet]="(file | previewFileIconPipe).fontSet" + [fontIcon]="(file | previewFileIconPipe).fontIcon" + matListIcon> + </mat-icon> + <h4 mat-line>{{ file.name }}</h4> + <p mat-line>mimetype: {{ file.mimetype }}</p> + </mat-list-item> +</mat-nav-list> + +<ng-template #loadingPlaceholder> + <div class="d-inline-block spinnerAnimationCircle"></div> loading previews ... +</ng-template> \ No newline at end of file diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.component.ts b/src/ui/databrowserModule/singleDataset/singleDataset.component.ts index a64124ee2bfaac3a695afc51df203aef7f0ec4c7..3cb41fc60b21bd13bbbc96690c699de92bb79d17 100644 --- a/src/ui/databrowserModule/singleDataset/singleDataset.component.ts +++ b/src/ui/databrowserModule/singleDataset/singleDataset.component.ts @@ -1,8 +1,10 @@ -import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core"; +import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, TemplateRef, Output, EventEmitter } from "@angular/core"; import { KgSingleDatasetService } from "../kgSingleDatasetService.service"; -import { Publication, File } from 'src/services/state/dataStore.store' +import { Publication, File, DataEntry, ViewerPreviewFile } from 'src/services/state/dataStore.store' import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; import { HumanReadableFileSizePipe } from "src/util/pipes/humanReadableFileSize.pipe"; +import { DatabrowserService } from "../databrowser.service"; +import { Observable } from "rxjs"; @Component({ selector: 'single-dataset-view', @@ -14,6 +16,8 @@ import { HumanReadableFileSizePipe } from "src/util/pipes/humanReadableFileSize. }) export class SingleDatasetView implements OnInit { + @Input() ripple: boolean = false + /** * the name/desc/publications are placeholder/fallback entries * while the actual data is being loaded from KG with kgSchema and kgId @@ -25,6 +29,12 @@ export class SingleDatasetView implements OnInit { @Input() kgSchema?: string @Input() kgId?: string + @Input() prefetched: any = null + @Input() simpleMode: boolean = false + + @Output() previewingFile: EventEmitter<ViewerPreviewFile> = new EventEmitter() + + public preview: boolean = false private humanReadableFileSizePipe: HumanReadableFileSizePipe = new HumanReadableFileSizePipe() /** @@ -43,31 +53,51 @@ export class SingleDatasetView implements OnInit { public fetchingSingleInfoInProgress = false public downloadInProgress = false + public favedDataentries$: Observable<DataEntry[]> + public dataset: any + constructor( + private dbService: DatabrowserService, private singleDatasetService: KgSingleDatasetService, private cdr: ChangeDetectorRef, private constantService: AtlasViewerConstantsServices - ){} + ){ + this.favedDataentries$ = this.dbService.favedDataentries$ + } ngOnInit() { - const { kgId, kgSchema } = this - + const { kgId, kgSchema, prefetched } = this + if ( prefetched ) { + const { name, description, kgReference, publications, files, preview, ...rest } = prefetched + this.name = name + this.description = description + this.kgReference = kgReference + this.publications = publications + this.files = files + this.preview = preview + + this.dataset = prefetched + return + } if (!kgSchema || !kgId) return this.fetchingSingleInfoInProgress = true this.singleDatasetService.getInfoFromKg({ kgId, kgSchema }) - .then(({ files, publications, name, description, kgReference}) => { + .then(json => { /** * TODO dataset specific */ + const { files, publications, name, description, kgReference} = json this.name = name this.description = description this.kgReference = kgReference this.publications = publications this.files = files + this.dataset = json + this.cdr.markForCheck() }) .catch(e => { @@ -75,6 +105,7 @@ export class SingleDatasetView implements OnInit { }) .finally(() => { this.fetchingSingleInfoInProgress = false + this.cdr.markForCheck() }) } @@ -82,10 +113,6 @@ export class SingleDatasetView implements OnInit { return this.kgSchema && this.kgId } - get appendedKgReferences() { - return this.kgReference.map(v => `https://doi.org/${v}`) - } - get numOfFiles(){ return this.files ? this.files.length @@ -103,19 +130,41 @@ export class SingleDatasetView implements OnInit { } get showFooter(){ - return (this.appendedKgReferences && this.appendedKgReferences.length > 0) + return (this.kgReference && this.kgReference.length > 0) || (this.publications && this.publications.length > 0) || (this.files && this.files.length > 0) } + toggleFav() { + this.dbService.toggleFav(this.dataset) + } + + showPreviewList(templateRef: TemplateRef<any>){ + this.singleDatasetService.showPreviewList(templateRef) + } + + handlePreviewFile(file: ViewerPreviewFile){ + this.previewingFile.emit(file) + this.singleDatasetService.previewFile(file, this.dataset) + } + + stop(event:Event){ + event.stopPropagation() + } + downloadZipFromKg() { this.downloadInProgress = true + this.cdr.markForCheck() + const { kgId, kgSchema } = this this.singleDatasetService.downloadZipFromKg({ kgId, kgSchema }, this.name) .catch(err => this.constantService.catchError(err)) - .finally(() => this.downloadInProgress = false) + .finally(() => { + this.downloadInProgress = false + this.cdr.markForCheck() + }) } } diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.template.html b/src/ui/databrowserModule/singleDataset/singleDataset.template.html index 2abde63a3f58b052f17081b80c24a7569b4578b0..8c41f9dfd842996879ecd012d8a6f9fdbe66f9b2 100644 --- a/src/ui/databrowserModule/singleDataset/singleDataset.template.html +++ b/src/ui/databrowserModule/singleDataset/singleDataset.template.html @@ -1,22 +1,82 @@ -<mat-card> +<mat-card mat-ripple [matRippleDisabled]="!ripple"> <!-- title --> - <mat-card-header> + <mat-card-header *ngIf="!simpleMode"> <mat-card-title> {{ name }} </mat-card-title> </mat-card-header> + + <mat-card-content *ngIf="simpleMode"> + <p> + {{ name }} + </p> + <mat-grid-list [cols]="kgReference.length + (preview ? 1 : 0) + (downloadEnabled ? 2 : 0)" rowHeight="4em"> + + <!-- explore --> + <mat-grid-tile *ngFor="let kgRef of kgReference"> + <a [href]="kgRef | doiParserPipe" + matTooltip="Explore" + (mousedown)="stop($event)" + (click)="stop($event)" + mat-icon-button + target="_blank"> + <i class="fas fa-external-link-alt"></i> + </a> + </mat-grid-tile> + + <!-- pin --> + <mat-grid-tile *ngIf="downloadEnabled"> + + <button + (mousedown)="stop($event)" + (click)="stop($event);toggleFav()" + matTooltip="Pin" + mat-icon-button + [color]="(favedDataentries$ | async | datasetIsFaved : dataset) ? 'primary' : 'basic'"> + <i class="fas fa-thumbtack"></i> + </button> + </mat-grid-tile> + + <!-- preview --> + <mat-grid-tile *ngIf="preview"> + <button + (mousedown)="stop($event)" + (click)="stop($event);showPreviewList(previewFilesListTemplate)" + matTooltip="Preview" + mat-icon-button> + <i class="far fa-eye"></i> + </button> + </mat-grid-tile> + + <!-- download --> + <mat-grid-tile *ngIf="downloadEnabled"> + <button + matTooltip="Download" + (mousedown)="stop($event)" + (click)="stop($event);downloadZipFromKg()" + [disabled]="downloadInProgress" + mat-icon-button> + <i class="ml-1 fas" [ngClass]="!downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> + </button> + </mat-grid-tile> + </mat-grid-list> + </mat-card-content> <!-- description --> - <mat-card-content> + <mat-card-content *ngIf="!simpleMode"> <p>{{ description }}</p> </mat-card-content> <!-- publications --> - <mat-card-content> + <mat-card-content *ngIf="!simpleMode"> <div class="d-block mb-2" *ngFor="let publication of publications"> - <a *ngIf="publication.doi; else plainText" [href]="publication.doi | doiParserPipe" + <a + *ngIf="publication.doi; else plainText" + (mousedown)="stop($event)" + (click)="stop($event)" + [href]="publication.doi | doiParserPipe" target="_blank"> {{ publication.cite }} </a> @@ -28,12 +88,12 @@ <!-- footer --> - <mat-card-actions> + <mat-card-actions *ngIf="!simpleMode"> <!-- explore --> - <a *ngFor="let kgRef of appendedKgReferences" + <a *ngFor="let kgRef of kgReference" class="m-2" - [href]="kgRef" + [href]="kgRef | doiParserPipe" target="_blank"> <button mat-raised-button @@ -43,6 +103,17 @@ </button> </a> + <!-- pin data --> + <button + (mousedown)="stop($event)" + (click)="stop($event);toggleFav()" + mat-button + color="primary" + [color]="(favedDataentries$ | async | datasetIsFaved : dataset) ? 'primary' : 'basic'"> + {{ (favedDataentries$ | async | datasetIsFaved : dataset) ? 'Unpin' : 'Pin' }} this dataset + <i class="fas fa-thumbtack"></i> + </button> + <!-- download --> <button @@ -59,4 +130,13 @@ <i class="ml-1 fas" [ngClass]="!downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> </button> </mat-card-actions> -</mat-card> \ No newline at end of file + + <mat-card-footer></mat-card-footer> +</mat-card> + +<ng-template #previewFilesListTemplate> + <preview-component + (previewFile)="handlePreviewFile($event)" + [datasetName]="name"> + </preview-component> +</ng-template> \ No newline at end of file diff --git a/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts b/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..446a22789f3d2a05b7c0602de10fd47ef4a3be3f --- /dev/null +++ b/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'getKgSchemaIdFromFullIdPipe' +}) + +export class GetKgSchemaIdFromFullIdPipe implements PipeTransform{ + public transform(fullId: string):[string, string]{ + if (!fullId) return [null, null] + const match = /([\w\-\.]*\/[\w\-\.]*\/[\w\-\.]*\/[\w\-\.]*)\/([\w\-\.]*)$/.exec(fullId) + if (!match) return [null, null] + return [match[1], match[2]] + } +} \ No newline at end of file diff --git a/src/ui/layerbrowser/layerbrowser.component.ts b/src/ui/layerbrowser/layerbrowser.component.ts index 6a99ffbaf392bc480aff27c810907c1a23df5ffb..3f825ba22d7526a5098acd2f90b1d4e88540ec59 100644 --- a/src/ui/layerbrowser/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerbrowser.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, Input, Pipe, PipeTransform } from "@angular/core"; +import { Component, OnDestroy, Input, Pipe, PipeTransform, Output, EventEmitter, OnInit } from "@angular/core"; import { NgLayerInterface } from "../../atlasViewer/atlasViewer.component"; import { Store, select } from "@ngrx/store"; import { ViewerStateInterface, isDefined, REMOVE_NG_LAYER, FORCE_SHOW_SEGMENT, safeFilter, getNgIds } from "../../services/stateStore.service"; @@ -15,7 +15,9 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta ] }) -export class LayerBrowser implements OnDestroy{ +export class LayerBrowser implements OnInit, OnDestroy{ + + @Output() nonBaseLayersChanged: EventEmitter<NgLayerInterface[]> = new EventEmitter() /** * TODO make untangle nglayernames and its dependency on ng @@ -109,6 +111,12 @@ export class LayerBrowser implements OnDestroy{ shareReplay(1) ) + } + + ngOnInit(){ + this.subscriptions.push( + this.nonBaseNgLayers$.subscribe(layers => this.nonBaseLayersChanged.emit(layers)) + ) this.subscriptions.push( this.forceShowSegment$.subscribe(state => this.forceShowSegmentCurrentState = state) ) diff --git a/src/ui/menuicons/menuicons.component.ts b/src/ui/menuicons/menuicons.component.ts index 8afb0b82fa8421a8361e476eeba8676ffe91a90f..e71f41c69ac23703b3301f757fe541322df6e6bb 100644 --- a/src/ui/menuicons/menuicons.component.ts +++ b/src/ui/menuicons/menuicons.component.ts @@ -4,6 +4,11 @@ import { Injector, ComponentFactory, ComponentFactoryResolver, + TemplateRef, + ViewChild, + OnInit, + OnDestroy, + AfterViewInit, } from "@angular/core"; import { WidgetServices } from "src/atlasViewer/widgetUnit/widgetService.service"; @@ -14,9 +19,15 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta import { DatabrowserService } from "../databrowserModule/databrowser.service"; import { PluginServices, PluginManifest } from "src/atlasViewer/atlasViewer.pluginService.service"; import { Store, select } from "@ngrx/store"; -import { Observable, combineLatest } from "rxjs"; +import { Observable, combineLatest, Subscription } from "rxjs"; import { map, shareReplay, startWith } from "rxjs/operators"; -import { ToastService } from "src/services/toastService.service"; +import { SHOW_SIDEBAR_TEMPLATE } from "src/services/state/uiState.store"; +import { LayerBrowser } from "../layerbrowser/layerbrowser.component"; +import { MatDialogRef, MatDialog } from "@angular/material"; +import { NgLayerInterface } from "src/atlasViewer/atlasViewer.component"; +import { DataEntry } from "src/services/stateStore.service"; +import { KgSingleDatasetService } from "../databrowserModule/kgSingleDatasetService.service"; +import { determinePreviewFileType, PREVIEW_FILE_TYPES } from "../databrowserModule/preview/previewFileIcon.pipe"; @Component({ selector: 'menu-icons', templateUrl: './menuicons.template.html', @@ -26,7 +37,10 @@ import { ToastService } from "src/services/toastService.service"; ] }) -export class MenuIconsBar{ +export class MenuIconsBar implements OnInit, OnDestroy { + + private layerBrowserDialogRef: MatDialogRef<any> + private subscriptions: Subscription[] = [] public badgetPosition: string = 'above before' @@ -56,14 +70,15 @@ export class MenuIconsBar{ public toolBtnClass$: Observable<string> public getKgSearchBtnCls$: Observable<[Set<WidgetUnit>, string]> - get darktheme(){ - return this.constantService.darktheme - } + public sidebarTemplate$: Observable<TemplateRef<any>> public selectedTemplate$: Observable<any> public selectedParcellation$: Observable<any> public selectedRegions$: Observable<any> + public getPluginBtnClass$: Observable<[Set<string>, Set<string>, string]> + public launchedPlugins$: Observable<string[]> + searchedItemsNumber = 0 searchLoading = false searchMenuFrozen = false @@ -71,6 +86,8 @@ export class MenuIconsBar{ showSearchMenu = false mouseHoversSearch = false + public fetchedDatasets: DataEntry[] = [] + constructor( private widgetServices:WidgetServices, private injector:Injector, @@ -79,7 +96,8 @@ export class MenuIconsBar{ cfr: ComponentFactoryResolver, public pluginServices:PluginServices, private store: Store<any>, - private toastService: ToastService + private dialog: MatDialog, + private singleDatasetService: KgSingleDatasetService ){ this.isMobile = this.constantService.mobile @@ -95,10 +113,10 @@ export class MenuIconsBar{ select('templateSelected') ) - this.selectedParcellation$ = store.pipe( - select('viewerState'), - select('parcellationSelected'), - ) + this.selectedParcellation$ = store.pipe( + select('viewerState'), + select('parcellationSelected'), + ) this.selectedRegions$ = store.pipe( select('viewerState'), @@ -140,6 +158,33 @@ export class MenuIconsBar{ this.widgetServices.minimisedWindow$, this.themedBtnClass$ ) + + this.sidebarTemplate$ = this.store.pipe( + select('uiState'), + select('sidebarTemplate') + ) + } + + ngOnInit(){ + /** + * on opening nifti volume, collapse side bar + */ + this.subscriptions.push( + this.singleDatasetService.previewingFile$.subscribe(({ file }) => { + if (determinePreviewFileType(file) === PREVIEW_FILE_TYPES.NIFTI) { + this.store.dispatch({ + type: SHOW_SIDEBAR_TEMPLATE, + sidebarTemplate: null + }) + } + }) + ) + } + + ngOnDestroy(){ + while(this.subscriptions.length > 0){ + this.subscriptions.pop().unsubscribe() + } } /** @@ -217,23 +262,27 @@ export class MenuIconsBar{ } } - get databrowserIsShowing() { - return this.dataBrowser !== null - } - - public closeWidget(event: MouseEvent, wu:WidgetUnit){ - event.stopPropagation() - this.widgetServices.exitWidget(wu) - } - - public renameKgSearchWidget(event:MouseEvent, wu: WidgetUnit) { - event.stopPropagation() + public showKgSearchSideNav(kgSearchTemplate: TemplateRef<any> = null){ + this.store.dispatch({ + type: SHOW_SIDEBAR_TEMPLATE, + sidebarTemplate: kgSearchTemplate + }) } - public favKgSearch(event: MouseEvent, wu: WidgetUnit) { - event.stopPropagation() + handleNonbaseLayerEvent(layers: NgLayerInterface[]){ + if (layers.length === 0) { + this.layerBrowserDialogRef && this.layerBrowserDialogRef.close() + this.layerBrowserDialogRef = null + return + } + if (this.layerBrowserDialogRef) return + this.layerBrowserDialogRef = this.dialog.open(LayerBrowser, { + hasBackdrop: false, + autoFocus: false, + position: { + top: '1em' + }, + disableClose: true + }) } - - public getPluginBtnClass$: Observable<[Set<string>, Set<string>, string]> - public launchedPlugins$: Observable<string[]> } diff --git a/src/ui/menuicons/menuicons.style.css b/src/ui/menuicons/menuicons.style.css index 8b00982ec9a521638c85199c96a16076639c60a9..cbaaac39d3a0c58e9422b81a73630fa49e7f36e1 100644 --- a/src/ui/menuicons/menuicons.style.css +++ b/src/ui/menuicons/menuicons.style.css @@ -1,26 +1,3 @@ -:host -{ - display: flex; - flex-direction: column; - align-items: flex-start; -} - -:host > * -{ - margin-top: 1em; - display:inline-block; -} - -:host >>> .tooltip-inner -{ - background-color: rgba(128, 128, 128, 0.5); -} - -:host >>> .tooltip.right .tooltip-arrow::before, -:host >>> .tooltip.right .tooltip-arrow -{ - border-right-color: rgba(128, 128, 128, 0.5); -} .soh-row > *:not(:first-child) { @@ -37,7 +14,13 @@ layer-browser max-width: 20em; } +:host > * +{ + pointer-events: all; +} - - - +[root] > * +{ + margin-top: 1em; + display:inline-block; +} diff --git a/src/ui/menuicons/menuicons.template.html b/src/ui/menuicons/menuicons.template.html index 86db4673a5e3a866f144c261c0e719067b74f0bc..aa83120e4a530204d41bc529c3a4680a3401fa34 100644 --- a/src/ui/menuicons/menuicons.template.html +++ b/src/ui/menuicons/menuicons.template.html @@ -1,204 +1,260 @@ -<logo-container *ngIf="!isMobile"> -</logo-container> - -<!-- hide icons when templates has yet been selected --> -<ng-template [ngIf]="selectedTemplate$ | async"> - - <!-- selected regions --> - <sleight-of-hand - [doNotClose]="viewerStateController.focused"> - - <!-- shown prior to mouse over --> - <div sleight-of-hand-front> - <button - [matBadge]="(selectedRegions$ | async).length > 0 ? (selectedRegions$ | async).length : null" - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - mat-icon-button - color="primary"> - <i class="fas fa-brain"></i> - </button> - </div> - - <!-- shown upon mouseover --> - <div - sleight-of-hand-back - class="d-flex flex-row align-items-center soh-row"> - - <!-- place holder icon --> - <button - [matBadge]="(selectedRegions$ | async).length > 0 ? (selectedRegions$ | async).length : null" - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - mat-icon-button - color="primary"> - <i class="fas fa-brain"></i> - </button> - - <div class="position-relative"> - - <div [class]="((darktheme$ | async) ? 'bg-dark' : 'bg-light') + ' position-absolute card'"> - <viewer-state-controller #viewerStateController></viewer-state-controller> - </div> - - <!-- invisible icon to keep height of the otherwise unstable flex block --> - <div class="invisible pe-none"> +<div class="w-0 ml-4 d-flex flex-column align-items-start" root> + <logo-container *ngIf="!isMobile"> + </logo-container> + + <!-- hide icons when templates has yet been selected --> + <ng-template [ngIf]="selectedTemplate$ | async"> + + <!-- TODO do not close expression changed after view checked --> + <!-- selected regions --> + <sleight-of-hand + [doNotClose]="viewerStateController.focused"> + + <!-- shown prior to mouse over --> + <div sleight-of-hand-front> + <button + [matBadge]="(selectedRegions$ | async).length > 0 ? (selectedRegions$ | async).length : null" + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + mat-icon-button + color="primary"> <i class="fas fa-brain"></i> - </div> + </button> </div> - - <ng-template #noBrainRegionSelected> - <small [class]="((darktheme$ | async) ? 'bg-dark text-light' : 'bg-light text-dark') + ' muted pl-2 pr-2 p-1 text-nowrap'"> - Double click any brain region to select it. - </small> - </ng-template> - </div> - </sleight-of-hand> - - <!-- layer browser --> - <sleight-of-hand> - <div sleight-of-hand-front> - <button - [matBadge]="layerBrowser && (layerBrowser.nonBaseNgLayers$ | async)?.length > 0 ? (layerBrowser.nonBaseNgLayers$ | async)?.length : null" - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - color="primary" - mat-icon-button> - <i class="fas fa-layer-group"></i> - </button> - </div> - <div - class="d-flex flex-row align-items-center soh-row" - sleight-of-hand-back> - - <button - [matBadge]="layerBrowser && (layerBrowser.nonBaseNgLayers$ | async)?.length > 0 ? (layerBrowser.nonBaseNgLayers$ | async)?.length : null" - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - color="primary" - mat-icon-button> - <i class="fas fa-layer-group"></i> - </button> - - <div class="position-relative d-flex align-items-center"> - - <div [ngClass]="{'invisible pe-none': (layerBrowser.nonBaseNgLayers$ | async).length === 0}" class="position-absolute"> - <mat-card> - <layer-browser #layerBrowser> - </layer-browser> - </mat-card> + + <!-- shown upon mouseover --> + <div + sleight-of-hand-back + class="d-flex flex-row align-items-center soh-row"> + + <!-- place holder icon --> + <button + [matBadge]="(selectedRegions$ | async).length > 0 ? (selectedRegions$ | async).length : null" + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + mat-icon-button + color="primary"> + <i class="fas fa-brain"></i> + </button> + + <div class="position-relative"> + + <div [class]="((darktheme$ | async) ? 'bg-dark' : 'bg-light') + ' position-absolute card'"> + <viewer-state-controller #viewerStateController></viewer-state-controller> + </div> + + <!-- invisible icon to keep height of the otherwise unstable flex block --> + <div class="invisible pe-none"> + <i class="fas fa-brain"></i> + </div> </div> - - <ng-container *ngIf="(layerBrowser.nonBaseNgLayers$ | async).length === 0" #noNonBaseNgLayerTemplate> - <small [class]="((darktheme$ | async) ? 'bg-dark text-light' : 'bg-light text-dark') + ' muted pl-2 pr-2 p-1 text-nowrap position-absolute'"> - No additional layers added + + <ng-template #noBrainRegionSelected> + <small [class]="((darktheme$ | async) ? 'bg-dark text-light' : 'bg-light text-dark') + ' muted pl-2 pr-2 p-1 text-nowrap'"> + Double click any brain region to select it. </small> - </ng-container> - - <!-- invisible button to prop up the size of parent block --> - <!-- otherwise, sibling block position will be wonky --> + </ng-template> + </div> + </sleight-of-hand> + + <!-- layer browser --> + <sleight-of-hand> + <div sleight-of-hand-front> <button + [matBadge]="layerBrowser && (layerBrowser.nonBaseNgLayers$ | async)?.length > 0 ? (layerBrowser.nonBaseNgLayers$ | async)?.length : null" + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" color="primary" - class="invisible pe-none" mat-icon-button> <i class="fas fa-layer-group"></i> </button> - </div> - - </div> - </sleight-of-hand> - - <!-- tools --> - <sleight-of-hand> - - <!-- shown icon prior to mouse over --> - <div sleight-of-hand-front> - <button - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - [matBadge]="(launchedPlugins$ | async)?.length > 0 ? (launchedPlugins$ | async)?.length : null" - mat-icon-button - color="primary"> - <i class="fas fa-tools"></i> - </button> - </div> - - <!-- shown after mouse over --> - <div - class="d-flex flex-row soh-row align-items-start" - sleight-of-hand-back> - - <!-- placeholder icon --> - <button - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - [matBadge]="(launchedPlugins$ | async)?.length > 0 ? (launchedPlugins$ | async)?.length : null" - mat-icon-button - color="primary"> - <i class="fas fa-tools"></i> - </button> - - <!-- render all fetched tools --> - <div class="d-flex flex-row soh-row"> - - <!-- add new tool btn --> + <div + class="d-flex flex-row align-items-center soh-row" + sleight-of-hand-back> + + <button + [matBadge]="layerBrowser && (layerBrowser.nonBaseNgLayers$ | async)?.length > 0 ? (layerBrowser.nonBaseNgLayers$ | async)?.length : null" + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + color="primary" + mat-icon-button> + <i class="fas fa-layer-group"></i> + </button> + + <div class="position-relative d-flex align-items-center"> + + <div [ngClass]="{'invisible pe-none': (layerBrowser.nonBaseNgLayers$ | async).length === 0}" class="position-absolute"> + <mat-card> + <layer-browser (nonBaseLayersChanged)="handleNonbaseLayerEvent($event)" #layerBrowser> + </layer-browser> + </mat-card> + </div> + + <ng-container *ngIf="(layerBrowser.nonBaseNgLayers$ | async).length === 0" #noNonBaseNgLayerTemplate> + <small [class]="((darktheme$ | async) ? 'bg-dark text-light' : 'bg-light text-dark') + ' muted pl-2 pr-2 p-1 text-nowrap position-absolute'"> + No additional layers added + </small> + </ng-container> + + <!-- invisible button to prop up the size of parent block --> + <!-- otherwise, sibling block position will be wonky --> + <button + color="primary" + class="invisible pe-none" + mat-icon-button> + <i class="fas fa-layer-group"></i> + </button> + + </div> + + </div> + </sleight-of-hand> + + <!-- tools --> + <sleight-of-hand> + + <!-- shown icon prior to mouse over --> + <div sleight-of-hand-front> <button - matTooltip="Add new plugin" - matTooltipPosition="below" + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + [matBadge]="(launchedPlugins$ | async)?.length > 0 ? (launchedPlugins$ | async)?.length : null" mat-icon-button color="primary"> - <i class="fas fa-plus"></i> + <i class="fas fa-tools"></i> </button> - + </div> + + <!-- shown after mouse over --> + <div + class="d-flex flex-row soh-row align-items-start" + sleight-of-hand-back> + + <!-- placeholder icon --> <button - *ngFor="let manifest of pluginServices.fetchedPluginManifests" - mat-mini-fab - matTooltipPosition="below" - [matTooltip]="manifest.displayName || manifest.name" - [color]="getPluginBtnClass$ | async | pluginBtnFabColorPipe : manifest.name" - (click)="clickPluginIcon(manifest)"> - {{ (manifest.displayName || manifest.name).slice(0, 1) }} + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + [matBadge]="(launchedPlugins$ | async)?.length > 0 ? (launchedPlugins$ | async)?.length : null" + mat-icon-button + color="primary"> + <i class="fas fa-tools"></i> </button> + + <!-- render all fetched tools --> + <div class="d-flex flex-row soh-row"> + + <!-- add new tool btn --> + <button + matTooltip="Add new plugin" + matTooltipPosition="below" + mat-icon-button + color="primary"> + <i class="fas fa-plus"></i> + </button> + + <button + *ngFor="let manifest of pluginServices.fetchedPluginManifests" + mat-mini-fab + matTooltipPosition="below" + [matTooltip]="manifest.displayName || manifest.name" + [color]="getPluginBtnClass$ | async | pluginBtnFabColorPipe : manifest.name" + (click)="clickPluginIcon(manifest)"> + {{ (manifest.displayName || manifest.name).slice(0, 1) }} + </button> + </div> </div> + </sleight-of-hand> + + <!-- kg search alt --> + <div class="d-flex align-item-start"> + <div *ngIf="(sidebarTemplate$ | async) === kgSearchDatasets; then alreadyOpened; else notYetOpened"> + </div> + <ng-template #alreadyOpened> + <button + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + [matBadge]="(fetchedDatasets && fetchedDatasets.length) ? fetchedDatasets.length : null" + matTooltip="Found datasets" + matTooltipPosition="after" + mat-mini-fab + (click)="showKgSearchSideNav()" + color="primary"> + <i class="fas fa-book"></i> + </button> + </ng-template> + <ng-template #notYetOpened> + <button + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + [matBadge]="(fetchedDatasets && fetchedDatasets.length) ? fetchedDatasets.length : null" + matTooltip="Found datasets" + matTooltipPosition="after" + mat-icon-button + (click)="showKgSearchSideNav(kgSearchDatasets)" + color="primary"> + <i class="fas fa-book"></i> + </button> + </ng-template> </div> - </sleight-of-hand> - - <!-- search kg --> -<!-- <sleight-of-hand>--> - <div class="d-flex align-items-start" (mouseenter)="mouseHoversSearch = true; showSearchMenu = true" (mouseleave)="mouseHoversSearch = false; hideSearchMenu()" style="z-index: 1" #SearchMenu> - <div sleight-of-hand-front> - <button - [matTooltip]="!searchedItemsNumber && 'Please select any region to get search results'" - matTooltipPosition="after" - mat-icon-button - color="primary" - [matBadgePosition]="badgetPosition" - matBadgeColor="accent" - [matBadge]="searchedItemsNumber? searchedItemsNumber : null"> - <i class="fas fa-search"></i> - </button> - </div> + <!-- search kg --> + <!-- <sleight-of-hand>--> + <div + class="d-flex align-items-start" + (mouseenter)="mouseHoversSearch = true; showSearchMenu = true" + (mouseleave)="mouseHoversSearch = false; hideSearchMenu()" + style="z-index: 1" + #SearchMenu> + <div sleight-of-hand-front> + <button + [matTooltip]="!searchedItemsNumber && 'Please select any region to get search results'" + matTooltipPosition="after" + mat-icon-button + color="primary" + [matBadgePosition]="badgetPosition" + matBadgeColor="accent" + [matBadge]="searchedItemsNumber? searchedItemsNumber : null"> + <i class="fas fa-search"></i> + </button> + </div> + <div *ngIf="(selectedRegions$ | async)?.length" - [hidden] = "!showSearchMenu" - class="position-absolute" - style="padding-left: 50px;"> + [hidden] = "!showSearchMenu" + class="position-absolute" + style="padding-left: 50px;"> <mat-card class="p-0"> <search-panel - elementOutClick - (outsideClick)="filePreviewModalClosed && $event? closeFrozenMenu() : null" - [selectedTemplate$]="selectedTemplate$" - [selectedParcellation$]="selectedParcellation$" - [selectedRegions$]="selectedRegions$" - [searchPanelPositionTop]="SearchMenu.offsetTop" - (searchedItemsNumber)="searchedItemsNumber = $event" - (searchLoading)="searchLoading = true" - (freezeSearchMenu)="searchMenuFrozen = $event" - (filePreviewModalClosed)="filePreviewModalClosed = $event" - (closeSearchMenu)="this.showSearchMenu = false"> + elementOutClick + (outsideClick)="filePreviewModalClosed && $event? closeFrozenMenu() : null" + [selectedTemplate$]="selectedTemplate$" + [selectedParcellation$]="selectedParcellation$" + [selectedRegions$]="selectedRegions$" + [searchPanelPositionTop]="SearchMenu.offsetTop" + (searchedItemsNumber)="searchedItemsNumber = $event" + (searchLoading)="searchLoading = true" + (freezeSearchMenu)="searchMenuFrozen = $event" + (filePreviewModalClosed)="filePreviewModalClosed = $event" + (closeSearchMenu)="this.showSearchMenu = false"> </search-panel> </mat-card> </div> </div> -</ng-template> + + </ng-template> +</div> + +<!-- needs the data browser to be rendered, but hidden, so that side templateref can be passed +this also bypass the necessity of doing a query --> +<div [hidden]="true"> + <ng-template [ngIf]="(selectedTemplate$ | async) && (selectedParcellation$ | async)" #kgSearchDatasets> + <data-browser + (dataentriesUpdated)="fetchedDatasets = $event" + [template]="selectedTemplate$ | async" + [regions]="selectedRegions$ | async" + [parcellation]="selectedParcellation$ | async" + #dataBrowser> + + </data-browser> + </ng-template> +</div> diff --git a/src/ui/searchItemPreview/searchItemPreview.component.ts b/src/ui/searchItemPreview/searchItemPreview.component.ts index dc77dcec318e54c62a4992e10c93d10174cf3a32..d652b8ed50b0b0a7271ab22bff746b5bcf81daaf 100644 --- a/src/ui/searchItemPreview/searchItemPreview.component.ts +++ b/src/ui/searchItemPreview/searchItemPreview.component.ts @@ -6,6 +6,7 @@ import {MatDialog} from "@angular/material"; import {CHANGE_NAVIGATION} from "src/services/state/viewerState.store"; import {Store} from "@ngrx/store"; import {ToastService} from "src/services/toastService.service"; +import { KgSingleDatasetService } from "../databrowserModule/kgSingleDatasetService.service"; @Component({ selector: 'search-item-preview-component', @@ -33,7 +34,7 @@ export class SearchItemPreviewComponent { constructor( private dbrService: DatabrowserService, public constantsService: AtlasViewerConstantsServices, - private dbService: DatabrowserService, + private singleDsService: KgSingleDatasetService, public dialog: MatDialog, private store: Store<any>, private toastService: ToastService, @@ -93,15 +94,15 @@ export class SearchItemPreviewComponent { niftiLayerIsShowing(previewFile){ - return this.dbService.ngLayers.has(previewFile.url) + return this.singleDsService.ngLayers.has(previewFile.url) } showDedicatedViewOnAtlasViewer(previewFile){ - this.dbService.showNewNgLayer({ url: previewFile.url }) + this.singleDsService.showNewNgLayer({ url: previewFile.url }) } removeDedicatedViewOnAtlasViewer(previewFile){ - this.dbService.removeNgLayer({ url: previewFile.url }) + this.singleDsService.removeNgLayer({ url: previewFile.url }) } navigateToRegion(region) { diff --git a/src/ui/sharedModules/angularMaterial.module.ts b/src/ui/sharedModules/angularMaterial.module.ts index fb7fada22730e0a1287df1edc55e9bc6be23df91..a78db023d0d1baaf078dd624c78cdc096eb91ca4 100644 --- a/src/ui/sharedModules/angularMaterial.module.ts +++ b/src/ui/sharedModules/angularMaterial.module.ts @@ -18,6 +18,9 @@ import { MatSlideToggleModule, MatRippleModule, MatSliderModule, + MatExpansionModule, + MatGridListModule, + MatIconModule } from '@angular/material'; import { NgModule } from '@angular/core'; @@ -44,7 +47,10 @@ import {DragDropModule} from "@angular/cdk/drag-drop"; MatSlideToggleModule, MatRippleModule, MatSliderModule, - DragDropModule + DragDropModule, + MatExpansionModule, + MatGridListModule, + MatIconModule ], exports: [ MatButtonModule, @@ -66,7 +72,10 @@ import {DragDropModule} from "@angular/cdk/drag-drop"; MatSlideToggleModule, MatRippleModule, MatSliderModule, - DragDropModule + DragDropModule, + MatExpansionModule, + MatGridListModule, + MatIconModule ], }) export class AngularMaterialModule { } diff --git a/src/ui/signinBanner/signinBanner.style.css b/src/ui/signinBanner/signinBanner.style.css index 1d53a9f509d6124afce0b454cc5826c211b4cfa6..03c30963c60253c4bc1e505f5de01bff0a2726ca 100644 --- a/src/ui/signinBanner/signinBanner.style.css +++ b/src/ui/signinBanner/signinBanner.style.css @@ -21,7 +21,6 @@ flex: 1 0 auto; } -region-hierarchy, dropdown-component { font-size:80%; @@ -70,3 +69,8 @@ dropdown-component outline: none; background-color: transparent; } + +:host > * +{ + pointer-events: all; +} \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 6e69259635457825d934e87e59cbe2d4f2c38617..8ffb79888772ddf52ac3cde962a44230500158df 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -5,7 +5,7 @@ import { NehubaViewerUnit } from "./nehubaContainer/nehubaViewer/nehubaViewer.co import { NehubaContainer } from "./nehubaContainer/nehubaContainer.component"; import { SplashScreen, GetTemplateImageSrcPipe, ImgSrcSetPipe } from "./nehubaContainer/splashScreen/splashScreen.component"; import { LayoutModule } from "../layouts/layout.module"; -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { FormsModule } from "@angular/forms"; import { GroupDatasetByRegion } from "../util/pipes/groupDataEntriesByRegion.pipe"; import { filterRegionDataEntries } from "../util/pipes/filterRegionDataEntries.pipe"; @@ -28,7 +28,6 @@ import { DownloadDirective } from "../util/directives/download.directive"; import { LogoContainer } from "./logoContainer/logoContainer.component"; import { TemplateParcellationCitationsContainer } from "./templateParcellationCitations/templateParcellationCitations.component"; import { MobileOverlay } from "./nehubaContainer/mobileOverlay/mobileOverlay.component"; -import { FilterNullPipe } from "../util/pipes/filterNull.pipe"; import { ShowToastDirective } from "../util/directives/showToast.directive"; import { HelpComponent } from "./help/help.component"; import { ConfigComponent } from './config/config.component' @@ -37,7 +36,6 @@ import { DatabrowserModule } from "./databrowserModule/databrowser.module"; import { SigninBanner } from "./signinBanner/signinBanner.components"; import { SigninModal } from "./signinModal/signinModal.component"; import { UtilModule } from "src/util/util.module"; -import { RegionHierarchy } from "./viewerStateController/regionHierachy/regionHierarchy.component"; import { FilterNameBySearch } from "./viewerStateController/regionHierachy/filterNameBySearch.pipe"; import { StatusCardComponent } from "./nehubaContainer/statusCard/statusCard.component"; import { CookieAgreement } from "./cookieAgreement/cookieAgreement.component"; @@ -56,7 +54,6 @@ import { GetFilenamePipe } from "src/util/pipes/getFilename.pipe"; import { GetFileExtension } from "src/util/pipes/getFileExt.pipe"; import { ViewerStateController } from "./viewerStateController/viewerState.component"; import { BinSavedRegionsSelectionPipe, SavedRegionsSelectionBtnDisabledPipe } from "./viewerStateController/viewerState.pipes"; -import { RegionTextSearchAutocomplete } from "./viewerStateController/regionSearch/regionSearch.component"; import { PluginBtnFabColorPipe } from "src/util/pipes/pluginBtnFabColor.pipe"; import { KgSearchBtnColorPipe } from "src/util/pipes/kgSearchBtnColor.pipe"; import { TemplateParcellationHasMoreInfo } from "src/util/pipes/templateParcellationHasMoreInfo.pipe"; @@ -75,7 +72,6 @@ import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; imports : [ HttpClientModule, FormsModule, - ReactiveFormsModule, LayoutModule, ComponentsModule, DatabrowserModule, @@ -101,7 +97,6 @@ import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; MenuIconsBar, SigninBanner, SigninModal, - RegionHierarchy, StatusCardComponent, CookieAgreement, KGToS, @@ -111,7 +106,6 @@ import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; SinglePanel, CurrentLayout, ViewerStateController, - RegionTextSearchAutocomplete, MaximmisePanelButton, SearchPanel, SearchItemPreviewComponent, @@ -126,7 +120,6 @@ import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; GetLayerNameFromDatasets, SortDataEntriesToRegion, SpatialLandmarksToDataBrowserItemPipe, - FilterNullPipe, FilterNameBySearch, AppendtooltipTextPipe, MobileControlNubStylePipe, diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts index 2fd52a746bd58437785353e315e68bcdf2b6f2a9..ea26648643a8418f7e941e0993f2a77748a5643e 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts @@ -211,4 +211,11 @@ export class RegionHierarchy implements OnInit, AfterViewInit{ public aggregatedRegionTree: any + public gotoRegion(region: any) { + this.doubleClickRegion.emit(region) + } + + public deselectRegion(region: any) { + this.singleClickRegion.emit(region) + } } \ No newline at end of file diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css index 372068bbf85ed6be2b7f1f2ae06fb467f7f4a701..483444c3dbeb5e533aed03e53833500513066fb0 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css @@ -5,7 +5,6 @@ div[treeContainer] z-index: 3; height:20em; - width: calc(100% + 4em); overflow-y:auto; overflow-x:hidden; @@ -13,10 +12,20 @@ div[treeContainer] background-color:rgba(12,12,12,0.8); */ } -div[hideScrollbarcontainer] +[selectedRegionsChipsContainer] { + flex: 0 0 20%; +} + +[hideScrollbarcontainer] +{ + flex: 1 1 0; overflow:hidden; - margin-top:2px; +} + +[hideScrollbarcontainer] [hideScrollbarInnerContainer] +{ + width: calc(100% + 8em); } input[type="text"] @@ -30,11 +39,6 @@ input[type="text"] width:20em; } -.tree-header -{ - flex: 0 0 auto; -} - .tree-body { flex: 1 1 auto; @@ -51,7 +55,8 @@ input[type="text"] flex: 0 0 auto; } -:host > [hideScrollbarContainer] +mat-chip-list >>> .mat-chip-list-wrapper { - flex: 1 1 0; + height: 100%; + width:110%; } \ No newline at end of file diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html index 5a55fdc8c328c231048001c9340687ebdf890f3b..308e53b6b37703598392c3a3eb79e7fc44e26109 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html @@ -9,13 +9,11 @@ autocomplete="off" [placeholder]="placeHolderText"/> </mat-form-field> - -<div hideScrollbarContainer> - <div - class="d-flex flex-column h-100" - treeContainer - #treeContainer> - <div class="tree-header d-inline-flex align-items-center"> + +<div class="d-flex flex-grow-1 flex-shrink-1"> + + <div class="d-flex flex-column" selectedRegionsChipsContainer> + <div class="flex-grow-0 flex-shrink-0 d-flex align-items-center justify-content-between"> <div> {{ selectedRegions.length }} {{ selectedRegions.length > 1 ? 'regions' : 'region' }} selected </div> @@ -26,18 +24,59 @@ clear all </div> </div> - + + <div hideScrollbarcontainer> + <mat-chip-list + class="w-100 flex-grow-1 flex-shrink-1"> + <cdk-virtual-scroll-viewport + hideScrollbarInnerContainer + class="h-100 p-4 overflow-x-hidden" + [itemSize]="32"> + <mat-chip + *cdkVirtualFor="let region of (selectedRegions | filterRowsByVisbilityPipe : null : filterTreeBySearch)" + class="w-90"> + + <span class="dot mr-1 flex-grow-0 flex-shrink-0" [ngStyle]="{backgroundColor: (region | regionBackgroundToRgbPipe)}"></span> + <span class="flex-grow-1 flex-shrink-1 text-truncate"> + {{ region.name }} + </span> + <button + *ngIf="region.position" + (click)="gotoRegion(region)" + mat-icon-button> + <i class="fas fa-map-marked-alt"></i> + </button> + <button + (click)="deselectRegion(region)" + mat-icon-button> + <i class="fas fa-trash"></i> + </button> + </mat-chip> + </cdk-virtual-scroll-viewport> + </mat-chip-list> + </div> + </div> + + <div hideScrollbarContainer> <div - *ngIf="parcellationSelected && parcellationSelected.regions as regions" - class="tree-body"> - <flat-tree-component - (treeNodeClick)="handleClickRegion($event)" - (totalRenderedListChanged)="handleTotalRenderedListChanged($event)" - [inputItem]="aggregatedRegionTree" - [renderNode]="displayTreeNode" - [searchFilter]="filterTreeBySearch"> - - </flat-tree-component> - </div> + class="d-flex flex-column h-100" + treeContainer + hideScrollbarInnerContainer + #treeContainer> + + <div + *ngIf="parcellationSelected && parcellationSelected.regions as regions" + class="tree-body"> + <flat-tree-component + (treeNodeClick)="handleClickRegion($event)" + (totalRenderedListChanged)="handleTotalRenderedListChanged($event)" + [inputItem]="aggregatedRegionTree" + [renderNode]="displayTreeNode" + [searchFilter]="filterTreeBySearch"> + + </flat-tree-component> + </div> + </div> </div> -</div> \ No newline at end of file +</div> + \ No newline at end of file diff --git a/src/ui/viewerStateController/viewerState.template.html b/src/ui/viewerStateController/viewerState.template.html index 5223f311c44afcf73e677e582ad57b3954aa78ad..8d80cc044262dc8f323fcd3a1827df0749a891e6 100644 --- a/src/ui/viewerStateController/viewerState.template.html +++ b/src/ui/viewerStateController/viewerState.template.html @@ -66,7 +66,7 @@ <!-- parcellation selection --> <mat-card-content class="d-inline-flex flex-nowrap"> - <mat-form-field + <mat-form-field *ngIf="templateSelected$ | async as templateSelected" class="d-inline-flex flex-nowrap"> <mat-label> @@ -141,7 +141,7 @@ [itemSize]="32"> <mat-chip *cdkVirtualFor="let region of (regionsSelected$ | async)" - class="w-100"> + class="w-90"> <span class="flex-grow-1 flex-shrink-1 text-truncate"> {{ region.name }} </span> diff --git a/src/ui/viewerStateController/viewerState.useEffect.ts b/src/ui/viewerStateController/viewerState.useEffect.ts index 4c8232e1c4509088bf1ac287bdd5057508505bef..70ab954ca1b0a817d6b6e3402dfe45dd7f211bff 100644 --- a/src/ui/viewerStateController/viewerState.useEffect.ts +++ b/src/ui/viewerStateController/viewerState.useEffect.ts @@ -27,6 +27,7 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ @Effect() selectParcellationWithName$: Observable<any> + @Effect() doubleClickOnHierarchy$: Observable<any> constructor( @@ -120,7 +121,37 @@ export class ViewerStateControllerUseEffect implements OnInit, OnDestroy{ ) this.doubleClickOnHierarchy$ = this.actions$.pipe( - ofType(VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY) + ofType(VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY), + map(action => { + const { payload = {} } = action as ViewerStateAction + const { region } = payload + if (!region) { + return { + type: GENERAL_ACTION_TYPES.ERROR, + payload: { + message: `Go to region: region not defined` + } + } + } + + const { position } = region + if (!position) { + return { + type: GENERAL_ACTION_TYPES.ERROR, + payload: { + message: `${region.name} - does not have a position defined` + } + } + } + + return { + type: CHANGE_NAVIGATION, + navigation: { + position, + animation: {} + } + } + }) ) this.singleClickOnHierarchy$ = this.actions$.pipe( diff --git a/src/util/util.module.ts b/src/util/util.module.ts index a83857d6198306aad9e955f7247b01baf29e4a2e..c19d9046cea5045834ad295ea449acc94dc091db 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -1,9 +1,15 @@ import { NgModule } from "@angular/core"; +import { FilterNullPipe } from "./pipes/filterNull.pipe"; +import { FilterRowsByVisbilityPipe } from "src/components/flatTree/filterRowsByVisibility.pipe"; @NgModule({ declarations: [ + FilterNullPipe, + FilterRowsByVisbilityPipe ], exports: [ + FilterNullPipe, + FilterRowsByVisbilityPipe ] })