diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 5c4d9fdc7e88bf8dfb19879e01d5ad6da0fa6b61..1cabf4fea761c7c24d20fff42d21f526cea73558 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -59,9 +59,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { darktheme: boolean = false @HostBinding('attr.ismobile') - get isMobile(){ - return this.constantsService.mobile - } + public ismobile: boolean = false meetsRequirement: boolean = true @@ -302,6 +300,10 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) } + this.subscriptions.push( + this.constantsService.useMobileUI$.subscribe(bool => this.ismobile = bool) + ) + this.subscriptions.push( this.snackbarMessage$.pipe( // angular material issue @@ -324,7 +326,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.subscriptions.push( this.showHelp$.subscribe(() => { this.helpDialogRef = this.matDialog.open(this.helpComponent, { - autoFocus: false + autoFocus: false, + panelClass: ['col-12','col-sm-12','col-md-8','col-lg-6','col-xl-4'] }) }) ) @@ -472,18 +475,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { return false } - if(this.constantsService.mobile){ - /** - * TODO change to snack bar in future - */ - - // this.modalService.show(ModalUnit,{ - // initialState: { - // title: this.constantsService.mobileWarningHeader, - // body: this.constantsService.mobileWarning - // } - // }) - } return true } diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts index 2101b00b43d11fec66b81ed9050a3f9d939f56b6..a31da6c69cb8e42ef432c99688439c00493eb66d 100644 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ b/src/atlasViewer/atlasViewer.constantService.service.ts @@ -1,9 +1,8 @@ -import { Injectable } from "@angular/core"; +import { Injectable, OnDestroy } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { ViewerStateInterface } from "../services/stateStore.service"; -import { Subject, Observable } from "rxjs"; -import { ACTION_TYPES, ViewerConfiguration } from 'src/services/state/viewerConfig.store' -import { map, shareReplay, filter } from "rxjs/operators"; +import { Subject, Observable, Subscription } from "rxjs"; +import { map, shareReplay, filter, tap } from "rxjs/operators"; import { SNACKBAR_MESSAGE } from "src/services/state/uiState.store"; export const CM_THRESHOLD = `0.05` @@ -13,11 +12,12 @@ export const CM_MATLAB_JET = `float r;if( x < 0.7 ){r = 4.0 * x - 1.5;} else {r providedIn : 'root' }) -export class AtlasViewerConstantsServices{ +export class AtlasViewerConstantsServices implements OnDestroy { public darktheme: boolean = false public darktheme$: Observable<boolean> - public mobile: boolean + + public useMobileUI$: Observable<boolean> public loadExportNehubaPromise : Promise<boolean> public getActiveColorMapFragmentMain = ():string=>`void main(){float x = toNormalized(getDataValue());${CM_MATLAB_JET}if(x>${CM_THRESHOLD}){emitRGB(vec3(r,g,b));}else{emitTransparent();}}` @@ -73,16 +73,6 @@ export class AtlasViewerConstantsServices{ public templateUrls = Array(100) - private _templateUrls = [ - // 'res/json/infant.json', - 'res/json/bigbrain.json', - 'res/json/colin.json', - 'res/json/MNI152.json', - 'res/json/waxholmRatV2_0.json', - 'res/json/allenMouse.json', - // 'res/json/test.json' - ] - /* to be provided by KG in future */ private _mapArray : [string,string[]][] = [ [ 'JuBrain Cytoarchitectonic Atlas' , @@ -190,12 +180,9 @@ Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float ['h', 'show help'], ['?', 'show help'], ['o', 'toggle perspective/orthographic'] - ] - get showHelpGeneralMap() { - return this.mobile - ? this.showHelpGeneralMobile - : this.showHelpGeneralDesktop - } + ] + + public showHelpGeneralMap = this.showHelpGeneralDesktop private showHelpSliceViewMobile = [ ['drag', 'pan'] @@ -204,11 +191,8 @@ Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float ['drag', 'pan'], ['shift + drag', 'oblique slice'] ] - get showHelpSliceViewMap() { - return this.mobile - ? this.showHelpSliceViewMobile - : this.showHelpSliceViewDesktop - } + + public showHelpSliceViewMap = this.showHelpSliceViewDesktop private showHelpPerspectiveMobile = [ ['drag', 'change perspective view'] @@ -217,11 +201,7 @@ Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float private showHelpPerspectiveDesktop = [ ['drag', 'change perspective view'] ] - get showHelpPerspectiveViewMap() { - return this.mobile - ? this.showHelpPerspectiveMobile - : this.showHelpPerspectiveDesktop - } + public showHelpPerspectiveViewMap = this.showHelpPerspectiveDesktop get showHelpSupportText() { return `Did you encounter an issue? @@ -241,17 +221,10 @@ Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float private repoUrl = `https://github.com/HumanBrainProject/interactive-viewer` constructor( - private store : Store<ViewerStateInterface> + private store$ : Store<ViewerStateInterface> ){ - const ua = window && window.navigator && window.navigator.userAgent - ? window.navigator.userAgent - : '' - - /* https://stackoverflow.com/a/25394023/6059235 */ - this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(ua) - - this.darktheme$ = this.store.pipe( + this.darktheme$ = this.store$.pipe( select('viewerState'), select('templateSelected'), filter(v => !!v), @@ -259,21 +232,37 @@ Interactive atlas viewer requires **webgl2.0**, and the \`EXT_color_buffer_float shareReplay(1) ) - /** - * set gpu limit if user is on mobile - */ - if (this.mobile) { - this.store.dispatch({ - type: ACTION_TYPES.UPDATE_CONFIG, - config: { - gpuLimit: 2e8 - } as Partial<ViewerConfiguration> - }) + this.useMobileUI$ = this.store$.pipe( + select('viewerConfigState'), + select('useMobileUI'), + shareReplay(1) + ) + + this.subscriptions.push( + this.useMobileUI$.subscribe(bool => { + if (bool) { + this.showHelpSliceViewMap = this.showHelpSliceViewMobile + this.showHelpGeneralMap = this.showHelpGeneralMobile + this.showHelpPerspectiveViewMap = this.showHelpPerspectiveMobile + } else { + this.showHelpSliceViewMap = this.showHelpSliceViewDesktop + this.showHelpGeneralMap = this.showHelpGeneralDesktop + this.showHelpPerspectiveViewMap = this.showHelpPerspectiveDesktop + } + }) + ) + } + + private subscriptions: Subscription[] = [] + + ngOnDestroy(){ + while(this.subscriptions.length > 0) { + this.subscriptions.pop().unsubscribe() } } catchError(e: Error | string){ - this.store.dispatch({ + this.store$.dispatch({ type: SNACKBAR_MESSAGE, snackbarMessage: e.toString() }) @@ -406,4 +395,4 @@ export const decodeToNumber: (encodedString:string, option?: B64EncodingOption) const castedFloat = new Float32Array(intArray.buffer) return castedFloat[0] } -} \ No newline at end of file +} diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 50b135a328207030803278026f2ea2f09fce1278..f8e201915ac1ca601a0af44a66aa6121c9bf0e72 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -6,7 +6,7 @@ <ng-template #helpComponent> <h2 mat-dialog-title>About Interactive Viewer</h2> - <mat-dialog-content class="h-90vh w-50vw"> + <mat-dialog-content> <mat-tab-group> <mat-tab label="Help"> <help-component> @@ -26,7 +26,7 @@ <mat-dialog-actions class="justify-content-center"> <button - mat-stroked-button + mat-flat-button (click)="closeModal('help')" cdkFocusInitial> close diff --git a/src/atlasViewer/widgetUnit/widgetService.service.ts b/src/atlasViewer/widgetUnit/widgetService.service.ts index 7f1cc48355967bb9bfec97c60f3ee3713cb62ee0..a976376cb96c424d48f1cfb5dbe630f3a71dbde5 100644 --- a/src/atlasViewer/widgetUnit/widgetService.service.ts +++ b/src/atlasViewer/widgetUnit/widgetService.service.ts @@ -1,4 +1,4 @@ -import { ComponentRef, ComponentFactory, Injectable, ViewContainerRef, ComponentFactoryResolver, Injector } from "@angular/core"; +import { ComponentRef, ComponentFactory, Injectable, ViewContainerRef, ComponentFactoryResolver, Injector, OnDestroy } from "@angular/core"; import { WidgetUnit } from "./widgetUnit.component"; import { AtlasViewerConstantsServices } from "../atlasViewer.constantService.service"; import { Subscription, BehaviorSubject } from "rxjs"; @@ -7,7 +7,7 @@ import { Subscription, BehaviorSubject } from "rxjs"; providedIn : 'root' }) -export class WidgetServices{ +export class WidgetServices implements OnDestroy{ public floatingContainer : ViewContainerRef public dockedContainer : ViewContainerRef @@ -28,6 +28,20 @@ export class WidgetServices{ ){ this.widgetUnitFactory = this.cfr.resolveComponentFactory(WidgetUnit) this.minimisedWindow$ = new BehaviorSubject(this.minimisedWindow) + + this.subscriptions.push( + this.constantServce.useMobileUI$.subscribe(bool => this.useMobileUI = bool) + ) + } + + private subscriptions: Subscription[] = [] + + public useMobileUI: boolean = false + + ngOnDestroy(){ + while(this.subscriptions.length > 0) { + this.subscriptions.pop().unsubscribe() + } } clearAllWidgets(){ @@ -64,7 +78,7 @@ export class WidgetServices{ const component = this.widgetUnitFactory.create(this.injector) const _option = getOption(options) - if(this.constantServce.mobile){ + if(this.useMobileUI){ _option.state = 'docked' } diff --git a/src/atlasViewer/widgetUnit/widgetUnit.component.ts b/src/atlasViewer/widgetUnit/widgetUnit.component.ts index 3f731d2665272b7694df307600fcf8c77b70ce06..5eb6e8d63f1e930930c6f716912f0e5a5f938a6d 100644 --- a/src/atlasViewer/widgetUnit/widgetUnit.component.ts +++ b/src/atlasViewer/widgetUnit/widgetUnit.component.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, ViewContainerRef,ComponentRef, HostBinding, HostListener, Output, EventEmitter, Input, ElementRef, OnInit, OnDestroy } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef,ComponentRef, HostBinding, HostListener, Output, EventEmitter, Input, OnInit, OnDestroy } from "@angular/core"; import { WidgetServices } from "./widgetService.service"; import { AtlasViewerConstantsServices } from "../atlasViewer.constantService.service"; @@ -30,6 +30,12 @@ export class WidgetUnit implements OnInit, OnDestroy{ isMinimised$: Observable<boolean> + public useMobileUI$: Observable<boolean> + + public hoverableConfig = { + translateY: -1 + } + /** * Timed alternates of blinkOn property should result in attention grabbing blink behaviour */ @@ -96,10 +102,10 @@ export class WidgetUnit implements OnInit, OnDestroy{ private subscriptions: Subscription[] = [] public id: string - constructor( - private constantsService: AtlasViewerConstantsServices - ){ + constructor(private constantsService: AtlasViewerConstantsServices){ this.id = Date.now().toString() + + this.useMobileUI$ = this.constantsService.useMobileUI$ } ngOnInit(){ @@ -177,8 +183,4 @@ export class WidgetUnit implements OnInit, OnDestroy{ } position : [number,number] = [400,100] - - get isMobile(){ - return this.constantsService.mobile - } } \ No newline at end of file diff --git a/src/atlasViewer/widgetUnit/widgetUnit.template.html b/src/atlasViewer/widgetUnit/widgetUnit.template.html index 84a9b851949d66beed27043783c46130c0832157..de35f7f2a2ee8c0c57dcb50db74ce0521dfdce62 100644 --- a/src/atlasViewer/widgetUnit/widgetUnit.template.html +++ b/src/atlasViewer/widgetUnit/widgetUnit.template.html @@ -18,24 +18,28 @@ </div> <div icons> <i - *ngIf="!isMobile" + *ngIf="useMobileUI$ | async" (click)="widgetServices.minimise(this)" class="fas fa-window-minimize" - [hoverable] ="{translateY: -1}"> + [hoverable] ="hoverableConfig"> </i> - <i *ngIf = "canBeDocked && state === 'floating' && !isMobile" - (click) = "dock($event)" - class = "fas fa-window-minimize" - hoverable></i> - <i *ngIf = "state === 'docked' && !isMobile" - (click) = "undock($event)" - class = "fas fa-window-restore" - hoverable></i> - <i *ngIf = "exitable" - (click) = "exit($event)" - class = "fas fa-times" - [hoverable] ="{translateY: -1}"></i> + + <ng-container *ngIf="!(useMobileUI$ | async)"> + <i *ngIf="canBeDocked && state === 'floating'" + (click)="dock($event)" + class="fas fa-window-minimize" + [hoverable]="hoverableConfig"></i> + <i *ngIf="state === 'docked'" + (click)="undock($event)" + class="fas fa-window-restore" + [hoverable]="hoverableConfig"></i> + </ng-container> + + <i *ngIf="exitable" + (click)="exit($event)" + class="fas fa-times" + [hoverable] ="hoverableConfig"></i> </div> <progress-bar [progress]="progressIndicator" *ngIf="showProgress" progressBar> diff --git a/src/main.module.ts b/src/main.module.ts index 63606d089c53d9771961fea68ac174718a3fadbd..5ff36abd47c75578875e104d477360f7490c74e2 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -32,7 +32,6 @@ import { FloatingContainerDirective } from "./util/directives/floatingContainer. import { PluginFactoryDirective } from "./util/directives/pluginFactory.directive"; import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive"; import { AuthService } from "./services/auth.service"; -import { ViewerConfiguration, LOCAL_STORAGE_CONST } from "./services/state/viewerConfig.store"; import { FixedMouseContextualContainerDirective } from "./util/directives/FixedMouseContextualContainerDirective.directive"; import { DatabrowserService } from "./ui/databrowserModule/databrowser.service"; import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; @@ -147,7 +146,6 @@ export class MainModule{ constructor( authServce: AuthService, - store: Store<ViewerConfiguration>, /** * instantiate singleton @@ -157,15 +155,5 @@ export class MainModule{ dbSerivce: DatabrowserService ){ authServce.authReloadState() - store.pipe( - select('viewerConfigState') - ).subscribe(({ gpuLimit, animation }) => { - if (gpuLimit) { - window.localStorage.setItem(LOCAL_STORAGE_CONST.GPU_LIMIT, gpuLimit.toString()) - } - if (typeof animation !== 'undefined' && animation !== null) { - window.localStorage.setItem(LOCAL_STORAGE_CONST.ANIMATION, animation.toString()) - } - }) } } diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 29e8a1512e127ab5fe69f6684cc6f98adb7e1a2d..9cf78dfc850529cc07f5353100fc1a7a941d0263 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -488,7 +488,7 @@ bs-modal-backdrop.modal-backdrop cursor: pointer; } -cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper +cdk-virtual-scroll-viewport:not(.cdk-virtual-scroll-orientation-horizontal) > .cdk-virtual-scroll-content-wrapper { width: 100%; } @@ -614,3 +614,18 @@ mat-icon[fontset="far"] { max-width: none!important; } + +.min-h-2 +{ + min-height: 1rem; +} + +.min-h-4 +{ + min-height: 2rem; +} + +.min-h-8 +{ + min-height: 4rem; +} diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index 7ca39242e804e69dda8fe397ac6139965d0b39c8..c3f24dcc1bef93ff1078a96b48a863b596c78de1 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -260,8 +260,12 @@ export class NgViewerUseEffect implements OnDestroy{ }) ) - this.toggleMaximiseCycleMessage$ = this.toggleMaximiseMode$.pipe( - filter(() => !this.constantService.mobile), + this.toggleMaximiseCycleMessage$ = combineLatest( + this.toggleMaximiseMode$, + this.constantService.useMobileUI$ + ).pipe( + filter(([_, useMobileUI]) => !useMobileUI), + map(([toggleMaximiseMode, _]) => toggleMaximiseMode), filter(({ payload }) => payload.panelMode && payload.panelMode === SINGLE_PANEL), mapTo({ type: SNACKBAR_MESSAGE, diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index 80cfaaef4a9019c5ffbbfa095bcce079ff092564..b58f86c33349151322501584766e2cc3984f9ddb 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -1,8 +1,7 @@ import { Action } from '@ngrx/store' import { TemplateRef } from '@angular/core'; -const agreedCookieKey = 'agreed-cokies' -const aggredKgTosKey = 'agreed-kg-tos' +import { LOCAL_STORAGE_CONST, COOKIE_VERSION, KG_TOS_VERSION } from 'src/util/constants' const defaultState : UIStateInterface = { mouseOverSegments: [], @@ -18,8 +17,8 @@ const defaultState : UIStateInterface = { /** * replace with server side logic (?) */ - agreedCookies: localStorage.getItem(agreedCookieKey) === 'agreed', - agreedKgTos: localStorage.getItem(aggredKgTosKey) === 'agreed' + agreedCookies: localStorage.getItem(LOCAL_STORAGE_CONST.AGREE_COOKIE) === COOKIE_VERSION, + agreedKgTos: localStorage.getItem(LOCAL_STORAGE_CONST.AGREE_KG_TOS) === KG_TOS_VERSION } export function uiState(state:UIStateInterface = defaultState,action:UIAction){ @@ -72,7 +71,7 @@ export function uiState(state:UIStateInterface = defaultState,action:UIAction){ /** * TODO replace with server side logic */ - localStorage.setItem(agreedCookieKey, 'agreed') + localStorage.setItem(LOCAL_STORAGE_CONST.AGREE_COOKIE, COOKIE_VERSION) return { ...state, agreedCookies: true @@ -81,7 +80,7 @@ export function uiState(state:UIStateInterface = defaultState,action:UIAction){ /** * TODO replace with server side logic */ - localStorage.setItem(aggredKgTosKey, 'agreed') + localStorage.setItem(LOCAL_STORAGE_CONST.AGREE_KG_TOS, KG_TOS_VERSION) return { ...state, agreedKgTos: true diff --git a/src/services/state/userConfigState.store.ts b/src/services/state/userConfigState.store.ts index a4af6c3420f635d37fe38c79cbaf40b0c5534598..ffbb5b214132bfccc972aee8cf9d3a4a2e85ea78 100644 --- a/src/services/state/userConfigState.store.ts +++ b/src/services/state/userConfigState.store.ts @@ -6,6 +6,8 @@ import { shareReplay, withLatestFrom, map, distinctUntilChanged, filter, take, t import { generateLabelIndexId, recursiveFindRegionWithLabelIndexId } from "../stateStore.service"; import { SELECT_REGIONS, NEWVIEWER, SELECT_PARCELLATION } from "./viewerState.store"; import { DialogService } from "../dialogService.service"; +import { VIEWER_CONFIG_ACTION_TYPES } from "./viewerConfig.store"; +import { LOCAL_STORAGE_CONST } from "src/util//constants"; interface UserConfigState{ savedRegionsSelection: RegionSelection[] @@ -256,6 +258,35 @@ export class UserConfigStateUseEffect implements OnDestroy{ }) ) + this.subscriptions.push( + this.store$.pipe( + select('viewerConfigState') + ).subscribe(({ gpuLimit, animation }) => { + + if (gpuLimit) { + window.localStorage.setItem(LOCAL_STORAGE_CONST.GPU_LIMIT, gpuLimit.toString()) + } + if (typeof animation !== 'undefined' && animation !== null) { + window.localStorage.setItem(LOCAL_STORAGE_CONST.ANIMATION, animation.toString()) + } + }) + ) + + this.subscriptions.push( + this.actions$.pipe( + + ofType(VIEWER_CONFIG_ACTION_TYPES.SET_MOBILE_UI), + map((action: any) => { + const { payload } = action + const { useMobileUI } = payload + return useMobileUI + }), + filter(bool => bool !== null) + ).subscribe((bool: boolean) => { + window.localStorage.setItem(LOCAL_STORAGE_CONST.MOBILE_UI, JSON.stringify(bool)) + }) + ) + this.subscriptions.push( this.actions$.pipe( ofType(ACTION_TYPES.UPDATE_REGIONS_SELECTIONS) @@ -275,11 +306,11 @@ export class UserConfigStateUseEffect implements OnDestroy{ /** * TODO save server side on per user basis */ - window.localStorage.setItem(LOCAL_STORAGE_KEY.SAVED_REGION_SELECTIONS, JSON.stringify(simpleSRSs)) + window.localStorage.setItem(LOCAL_STORAGE_CONST.SAVED_REGION_SELECTIONS, JSON.stringify(simpleSRSs)) }) ) - const savedSRSsString = window.localStorage.getItem(LOCAL_STORAGE_KEY.SAVED_REGION_SELECTIONS) + const savedSRSsString = window.localStorage.getItem(LOCAL_STORAGE_CONST.SAVED_REGION_SELECTIONS) const savedSRSs:SimpleRegionSelection[] = savedSRSsString && JSON.parse(savedSRSsString) this.restoreSRSsFromStorage$ = viewerState$.pipe( @@ -334,7 +365,3 @@ export class UserConfigStateUseEffect implements OnDestroy{ @Effect() public restoreSRSsFromStorage$: Observable<any> } - -const LOCAL_STORAGE_KEY = { - SAVED_REGION_SELECTIONS: 'fzj.xg.iv.SAVED_REGION_SELECTIONS' -} \ No newline at end of file diff --git a/src/services/state/viewerConfig.store.ts b/src/services/state/viewerConfig.store.ts index e6bf3881472833ec2f2adbf939ecbf41f21ee993..6f5579c39822824bef4f0f79d3f5238ad16afc09 100644 --- a/src/services/state/viewerConfig.store.ts +++ b/src/services/state/viewerConfig.store.ts @@ -1,8 +1,10 @@ import { Action } from "@ngrx/store"; +import { LOCAL_STORAGE_CONST } from "src/util/constants"; export interface ViewerConfiguration{ gpuLimit: number animation: boolean + useMobileUI: boolean } interface ViewerConfigurationAction extends Action{ @@ -20,31 +22,54 @@ export const CONFIG_CONSTANTS = { defaultAnimation: true } -export const ACTION_TYPES = { +const ACTION_TYPES = { SET_ANIMATION: `SET_ANIMATION`, UPDATE_CONFIG: `UPDATE_CONFIG`, - CHANGE_GPU_LIMIT: `CHANGE_GPU_LIMIT` -} - -export const LOCAL_STORAGE_CONST = { - GPU_LIMIT: 'iv-gpulimit', - ANIMATION: 'iv-animationFlag' + CHANGE_GPU_LIMIT: `CHANGE_GPU_LIMIT`, + SET_MOBILE_UI: 'SET_MOBILE_UI' } +// get gpu limit const lsGpuLimit = localStorage.getItem(LOCAL_STORAGE_CONST.GPU_LIMIT) const lsAnimationFlag = localStorage.getItem(LOCAL_STORAGE_CONST.ANIMATION) const gpuLimit = lsGpuLimit && !isNaN(Number(lsGpuLimit)) ? Number(lsGpuLimit) : CONFIG_CONSTANTS.defaultGpuLimit +// get animation flag const animation = lsAnimationFlag && lsAnimationFlag === 'true' ? true : lsAnimationFlag === 'false' ? false : CONFIG_CONSTANTS.defaultAnimation -export function viewerConfigState(prevState:ViewerConfiguration = {animation, gpuLimit}, action:ViewerConfigurationAction) { +// get mobile ui setting +// UA sniff only if not useMobileUI not explicitly set +const getIsMobile = () => { + const ua = window && window.navigator && window.navigator.userAgent + ? window.navigator.userAgent + : '' + + /* https://stackoverflow.com/a/25394023/6059235 */ + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(ua) +} +const useMobileUIStroageValue = window.localStorage.getItem(LOCAL_STORAGE_CONST.MOBILE_UI) + +const onLoadViewerconfig: ViewerConfiguration = { + animation, + gpuLimit, + useMobileUI: (useMobileUIStroageValue && useMobileUIStroageValue === 'true') || getIsMobile() +} + +export function viewerConfigState(prevState:ViewerConfiguration = onLoadViewerconfig, action:ViewerConfigurationAction) { switch (action.type) { + case ACTION_TYPES.SET_MOBILE_UI: + const { payload } = action + const { useMobileUI } = payload + return { + ...prevState, + useMobileUI + } case ACTION_TYPES.UPDATE_CONFIG: return { ...prevState, @@ -61,7 +86,8 @@ export function viewerConfigState(prevState:ViewerConfiguration = {animation, gp ...prevState, gpuLimit: newGpuLimit } - default: - return prevState + default: return prevState } -} \ No newline at end of file +} + +export const VIEWER_CONFIG_ACTION_TYPES = ACTION_TYPES \ No newline at end of file diff --git a/src/ui/config/config.component.ts b/src/ui/config/config.component.ts index 8f5083ee1affacc11e13e910888e1ec04ecc7be2..a52647cbd8bf4e6ee62f07fd732d2f72651fb9d0 100644 --- a/src/ui/config/config.component.ts +++ b/src/ui/config/config.component.ts @@ -1,14 +1,16 @@ import { Component, OnInit, OnDestroy } from '@angular/core' import { Store, select } from '@ngrx/store'; -import { ViewerConfiguration, ACTION_TYPES } from 'src/services/state/viewerConfig.store' +import { ViewerConfiguration, VIEWER_CONFIG_ACTION_TYPES } from 'src/services/state/viewerConfig.store' import { Observable, Subscription, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, startWith, debounceTime } from 'rxjs/operators'; +import { map, distinctUntilChanged, startWith, debounceTime, tap } from 'rxjs/operators'; import { MatSlideToggleChange, MatSliderChange } from '@angular/material'; import { NG_VIEWER_ACTION_TYPES, SUPPORTED_PANEL_MODES } from 'src/services/state/ngViewerState.store'; import { isIdentityQuat } from '../nehubaContainer/util'; +import { AtlasViewerConstantsServices } from 'src/atlasViewer/atlasViewer.constantService.service'; const GPU_TOOLTIP = `GPU TOOLTIP` const ANIMATION_TOOLTIP = `ANIMATION_TOOLTIP` +const MOBILE_UI_TOOLTIP = `MOBILE_UI_TOOLTIP` const ROOT_TEXT_ORDER : [string, string, string, string] = ['Coronal', 'Sagittal', 'Axial', '3D'] const OBLIQUE_ROOT_TEXT_ORDER : [string, string, string, string] = ['Slice View 1', 'Slice View 2', 'Slice View 3', '3D'] @@ -24,6 +26,7 @@ export class ConfigComponent implements OnInit, OnDestroy{ public GPU_TOOLTIP = GPU_TOOLTIP public ANIMATION_TOOLTIP = ANIMATION_TOOLTIP + public MOBILE_UI_TOOLTIP = MOBILE_UI_TOOLTIP public supportedPanelModes = SUPPORTED_PANEL_MODES /** @@ -31,6 +34,7 @@ export class ConfigComponent implements OnInit, OnDestroy{ */ public gpuLimit$: Observable<number> + public useMobileUI$: Observable<boolean> public animationFlag$: Observable<boolean> private subscriptions: Subscription[] = [] @@ -45,7 +49,13 @@ export class ConfigComponent implements OnInit, OnDestroy{ private viewerObliqueRotated$: Observable<boolean> - constructor(private store: Store<ViewerConfiguration>) { + constructor( + private store: Store<ViewerConfiguration>, + private constantService: AtlasViewerConstantsServices + ) { + + this.useMobileUI$ = this.constantService.useMobileUI$ + this.gpuLimit$ = this.store.pipe( select('viewerConfigState'), map((config:ViewerConfiguration) => config.gpuLimit), @@ -100,10 +110,20 @@ export class ConfigComponent implements OnInit, OnDestroy{ this.subscriptions.forEach(s => s.unsubscribe()) } + public toggleMobileUI(ev: MatSlideToggleChange){ + const { checked } = ev + this.store.dispatch({ + type: VIEWER_CONFIG_ACTION_TYPES.SET_MOBILE_UI, + payload: { + useMobileUI: checked + } + }) + } + public toggleAnimationFlag(ev: MatSlideToggleChange ){ const { checked } = ev this.store.dispatch({ - type: ACTION_TYPES.UPDATE_CONFIG, + type: VIEWER_CONFIG_ACTION_TYPES.UPDATE_CONFIG, config: { animation: checked } @@ -112,7 +132,7 @@ export class ConfigComponent implements OnInit, OnDestroy{ public handleMatSliderChange(ev:MatSliderChange){ this.store.dispatch({ - type: ACTION_TYPES.UPDATE_CONFIG, + type: VIEWER_CONFIG_ACTION_TYPES.UPDATE_CONFIG, config: { gpuLimit: ev.value * 1e6 } diff --git a/src/ui/config/config.template.html b/src/ui/config/config.template.html index 91f0dde64b0b2dc1d442f4bdacc616c284ad1237..d5a271152ebeabc0e7419d4346c38059344a0fad 100644 --- a/src/ui/config/config.template.html +++ b/src/ui/config/config.template.html @@ -1,5 +1,4 @@ <mat-tab-group> - <!-- viewer preference --> <mat-tab label="Viewer Preference"> @@ -154,39 +153,53 @@ <!-- hard ware --> <mat-tab label="Hardware"> - <div class="d-flex align-items-center"> - <mat-slide-toggle - [checked]="animationFlag$ | async" - (change)="toggleAnimationFlag($event)"> - Enable Animation - </mat-slide-toggle> - <small [matTooltip]="ANIMATION_TOOLTIP" class="ml-2 fas fa-question"></small> - </div> - <div class="d-flex flex-row align-items-center justify-content start"> - <label - class="m-0 d-inline-block flex-grow-0 flex-shrink-0" - for="gpuLimitSlider"> - GPU Limit - <small [matTooltip]="GPU_TOOLTIP" class="ml-2 fas fa-question"></small> - </label> - <mat-slider - class="flex-grow-1 flex-shrink-1 ml-2 mr-2" - id="gpuLimitSlider" - name="gpuLimitSlider" - thumbLabel="true" - min="100" - max="1000" - [step]="stepSize" - (change)="handleMatSliderChange($event)" - [value]="gpuLimit$ | async"> - </mat-slider> - <span class="d-inline-block flex-grow-0 flex-shrink-0 w-10em"> - {{ gpuLimit$ | async }} MB - </span> - </div> + <!-- wrapper + margin control --> + <div class="m-4"> - - </mat-tab> + <!-- use mobile UI --> + <div class="d-flex mb-2 align-items-center"> + <mat-slide-toggle + [checked]="useMobileUI$ | async" + (change)="toggleMobileUI($event)"> + Enable Mobile UI + </mat-slide-toggle> + <small iav-stop="click mousedown mouseup" [matTooltip]="MOBILE_UI_TOOLTIP" class="ml-2 fas fa-question"></small> + </div> + <!-- animation toggle --> + <div class="d-flex mb-2 align-items-center"> + <mat-slide-toggle + [checked]="animationFlag$ | async" + (change)="toggleAnimationFlag($event)"> + Enable Animation + </mat-slide-toggle> + <small iav-stop="click mousedown mouseup" [matTooltip]="ANIMATION_TOOLTIP" class="ml-2 fas fa-question"></small> + </div> + + <!-- GPU limit --> + <div class="d-flex flex-row align-items-center justify-content start"> + <label + class="m-0 d-inline-block flex-grow-0 flex-shrink-0" + for="gpuLimitSlider"> + GPU Limit + <small iav-stop="click mousedown mouseup" [matTooltip]="GPU_TOOLTIP" class="ml-2 fas fa-question"></small> + </label> + <mat-slider + class="flex-grow-1 flex-shrink-1 ml-2 mr-2" + id="gpuLimitSlider" + name="gpuLimitSlider" + thumbLabel="true" + min="100" + max="1000" + [step]="stepSize" + (change)="handleMatSliderChange($event)" + [value]="gpuLimit$ | async"> + </mat-slider> + <span class="d-inline-block flex-grow-0 flex-shrink-0 w-10em"> + {{ gpuLimit$ | async }} MB + </span> + </div> + </div> + </mat-tab> </mat-tab-group> diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index a535940ce7c9892fbb44f2226f126823f590a960..93b0103ea4026b8b3ddecdb2c9681fdfaae31113 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common"; import { DataBrowser } from "./databrowser/databrowser.component"; import { ComponentsModule } from "src/components/components.module"; import { ModalityPicker } from "./modalityPicker/modalityPicker.component"; -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { FormsModule } from "@angular/forms"; import { PathToNestedChildren } from "./util/pathToNestedChildren.pipe"; import { CopyPropertyPipe } from "./util/copyProperty.pipe"; import { FilterDataEntriesbyMethods } from "./util/filterDataEntriesByMethods.pipe"; @@ -26,21 +26,18 @@ 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"; import { SingleDatasetListView } from "./singleDataset/listView/singleDatasetListView.component"; -import { CurrentlySelectedRegions } from "../viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component"; @NgModule({ imports:[ ChartsModule, CommonModule, ComponentsModule, - ReactiveFormsModule, ScrollingModule, FormsModule, UtilModule, @@ -58,9 +55,6 @@ import { CurrentlySelectedRegions } from "../viewerStateController/currentlySele DedicatedViewer, SingleDatasetView, SingleDatasetListView, - RegionTextSearchAutocomplete, - RegionHierarchy, - CurrentlySelectedRegions, /** * pipes @@ -85,9 +79,6 @@ import { CurrentlySelectedRegions } from "../viewerStateController/currentlySele ModalityPicker, FilterDataEntriesbyMethods, FileViewer, - RegionTextSearchAutocomplete, - RegionHierarchy, - CurrentlySelectedRegions, ], entryComponents:[ DataBrowser, diff --git a/src/ui/databrowserModule/databrowser.useEffect.ts b/src/ui/databrowserModule/databrowser.useEffect.ts index 11341c3553f45dac39b68441afe299041018d129..44b5ebb5e792c27b72d8ee61f6776a87ad81325e 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.ts @@ -3,9 +3,25 @@ import { Store, select } from "@ngrx/store"; import { Actions, ofType, Effect } from "@ngrx/effects"; import { DATASETS_ACTIONS_TYPES, DataEntry } from "src/services/state/dataStore.store"; import { Observable, of, from, merge, Subscription } from "rxjs"; -import { withLatestFrom, map, catchError, filter, switchMap, scan, share, switchMapTo, shareReplay } from "rxjs/operators"; +import { withLatestFrom, map, catchError, filter, switchMap, scan } from "rxjs/operators"; import { KgSingleDatasetService } from "./kgSingleDatasetService.service"; import { getIdFromDataEntry } from "./databrowser.service"; +import { LOCAL_STORAGE_CONST } from "src/util/constants"; + +const savedFav$ = of(window.localStorage.getItem(LOCAL_STORAGE_CONST.FAV_DATASET)).pipe( + map(string => JSON.parse(string)), + map(arr => { + if (arr.every(item => item.id )) return arr + throw new Error('Not every item has id and/or name defined') + }), + catchError(err => { + /** + * TODO emit proper error + * possibly wipe corrupted local stoage here? + */ + return of(null) + }) +) @Injectable({ providedIn: 'root' @@ -79,11 +95,8 @@ export class DataBrowserUseEffect implements OnDestroy{ this.subscriptions.push( - merge( - this.favDataset$, - this.unfavDataset$ - ).pipe( - switchMapTo(this.favDataEntries$) + this.favDataEntries$.pipe( + filter(v => !!v) ).subscribe(favDataEntries => { /** * only store the minimal data in localstorage/db, hydrate when needed @@ -99,20 +112,7 @@ export class DataBrowserUseEffect implements OnDestroy{ }) ) - this.savedFav$ = of(window.localStorage.getItem(LOCAL_STORAGE_CONST.FAV_DATASET)).pipe( - map(string => JSON.parse(string)), - map(arr => { - if (arr.every(item => item.id )) return arr - throw new Error('Not every item has id and/or name defined') - }), - catchError(err => { - /** - * TODO emit proper error - * possibly wipe corrupted local stoage here? - */ - return of(null) - }) - ) + this.savedFav$ = savedFav$ this.onInitGetFav$ = this.savedFav$.pipe( filter(v => !!v), @@ -160,7 +160,3 @@ export class DataBrowserUseEffect implements OnDestroy{ @Effect() public toggleDataset$: Observable<any> } - -const LOCAL_STORAGE_CONST = { - FAV_DATASET: 'fzj.xg.iv.FAV_DATASET' -} diff --git a/src/ui/databrowserModule/databrowser/databrowser.component.ts b/src/ui/databrowserModule/databrowser/databrowser.component.ts index 368a12762c5e888cf0b0752447610ccdd96c601a..dedf8ebdc47f9d11c3e21e1ebb5646e0ed526f40 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.component.ts +++ b/src/ui/databrowserModule/databrowser/databrowser.component.ts @@ -196,7 +196,7 @@ export class DataBrowser implements OnChanges, OnDestroy,OnInit{ this.clearAll() } - trackbyFn(index:number, dataset:DataEntry) { + trackByFn(index:number, dataset:DataEntry){ return dataset.id } } diff --git a/src/ui/databrowserModule/databrowser/databrowser.template.html b/src/ui/databrowserModule/databrowser/databrowser.template.html index e8e62e687e0d75f710bfde3a40c89f3d090ffeff..e0df240574c738d685d75d1f40089dc1d3098e6d 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.template.html +++ b/src/ui/databrowserModule/databrowser/databrowser.template.html @@ -84,7 +84,8 @@ <cdk-virtual-scroll-viewport class="h-100" autosize> - <div class="virtual-scroll-element" *cdkVirtualFor="let dataset of filteredDataEntry; trackBy: trackbyFn; templateCacheSize: 0; let index = index"> + <div class="virtual-scroll-element" + *cdkVirtualFor="let dataset of filteredDataEntry; trackBy: trackByFn; templateCacheSize: 0; let index = index"> <!-- divider, show if not first --> <mat-divider *ngIf="index !== 0"></mat-divider> diff --git a/src/ui/layerbrowser/layerbrowser.component.ts b/src/ui/layerbrowser/layerbrowser.component.ts index 12d95fac85ce65326a387356eb00945f29ee87b4..7fefc4b163d5a452c757f4c55e8013de8096d160 100644 --- a/src/ui/layerbrowser/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerbrowser.component.ts @@ -3,7 +3,7 @@ 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"; import { Subscription, Observable, combineLatest } from "rxjs"; -import { filter, map, shareReplay, distinctUntilChanged } from "rxjs/operators"; +import { filter, map, shareReplay, distinctUntilChanged, throttleTime, debounceTime } from "rxjs/operators"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; @Component({ @@ -119,7 +119,11 @@ export class LayerBrowser implements OnInit, OnDestroy{ ngOnInit(){ this.subscriptions.push( - this.nonBaseNgLayers$.subscribe(layers => this.nonBaseLayersChanged.emit(layers)) + this.nonBaseNgLayers$.pipe( + // on switching template, non base layer will fire + // debounce to ensure that the non base layer is indeed an extra layer + debounceTime(160) + ).subscribe(layers => this.nonBaseLayersChanged.emit(layers)) ) this.subscriptions.push( this.forceShowSegment$.subscribe(state => this.forceShowSegmentCurrentState = state) @@ -208,10 +212,6 @@ export class LayerBrowser implements OnInit, OnDestroy{ : 'red' } - get isMobile(){ - return this.constantsService.mobile - } - public matTooltipPosition: string = 'below' } diff --git a/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.template.html b/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.template.html index 02d7cdeaa40ea2c519cca54fcd0c49d6eab4e365..62e7289790c7ab96f694036a9ec94706fa0dff50 100644 --- a/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.template.html +++ b/src/ui/nehubaContainer/maximisePanelButton/maximisePanelButton.template.html @@ -2,9 +2,9 @@ [matTooltip]="(isMaximised$ | async) ? 'Restore four panel view' : 'Maximise this panel'" mat-icon-button color="primary"> - <i *ngIf="isMaximised$ | async; else expandIconTemplate" class="fas fa-compress-arrows-alt"></i> + <i *ngIf="isMaximised$ | async; else expandIconTemplate" class="fas fa-compress"></i> </button> <ng-template #expandIconTemplate> - <i class="fas fa-expand-arrows-alt"></i> + <i class="fas fa-expand"></i> </ng-template> \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 5aabb59ed9ce68f43f06bf516291138cd4863274..714686a777d5863bf4789677027d821b97170dfc 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -163,6 +163,9 @@ export class NehubaContainer implements OnInit, OnDestroy{ public bottomSheet: MatBottomSheet, private kgSingleDataset: KgSingleDatasetService ){ + + this.useMobileUI$ = this.constantService.useMobileUI$ + this.favDataEntries$ = this.store.pipe( select('dataStore'), select('favDataEntries') @@ -445,9 +448,7 @@ export class NehubaContainer implements OnInit, OnDestroy{ ) } - get isMobile(){ - return this.constantService.mobile - } + public useMobileUI$: Observable<boolean> private removeExistingPanels() { const element = this.nehubaViewer.nehubaViewer.ngviewer.layout.container.componentValue.element as HTMLElement diff --git a/src/ui/nehubaContainer/nehubaContainer.style.css b/src/ui/nehubaContainer/nehubaContainer.style.css index 160d701048c08d585d06629e40fbeb3ca00dec3e..5023c2001888243b2a8e68e9838fb926d59adc2d 100644 --- a/src/ui/nehubaContainer/nehubaContainer.style.css +++ b/src/ui/nehubaContainer/nehubaContainer.style.css @@ -184,7 +184,7 @@ maximise-panel-button transform 250ms ease-in-out; position: absolute; - top: 0; + bottom: 0; right: 0; } @@ -202,11 +202,3 @@ maximise-panel-button:hover, opacity: 1.0 !important; pointer-events: all !important; } - - -maximise-panel-button.touch-top.touch-right.onHover, -maximise-panel-button.touch-top.touch-right:hover, -:host-context(:not([ismobile="true"])) maximise-panel-button.touch-top.touch-right -{ - transform: translate(-3em, 3em) -} diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 8d15e1474c1542e58e985bff89db8081267559a4..414c0bf23b77661cb23c9a808bd609dadb36449a 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -1,7 +1,7 @@ <ng-template #container> </ng-template> -<ui-splashscreen (contextmenu)="$event.stopPropagation();" *ngIf="!viewerLoaded"> +<ui-splashscreen iav-stop="mousedown mouseup touchstart touchmove touchend" (contextmenu)="$event.stopPropagation();" *ngIf="!viewerLoaded"> </ui-splashscreen> <!-- spatial landmarks overlay --> @@ -22,10 +22,10 @@ </div> </current-layout> -<layout-floating-container *ngIf="viewerLoaded && !isMobile"> +<layout-floating-container *ngIf="viewerLoaded"> <!-- tmp fab --> - <div class="m-3 load-fav-dataentries-fab position-absolute pe-all"> + <div class="mb-5 mr-3 load-fav-dataentries-fab position-absolute pe-all"> <button (click)="bottomSheet.open(savedDatasets)" [matBadge]="(favDataEntries$ | async)?.length > 0 ? (favDataEntries$ | async)?.length : null " @@ -41,8 +41,9 @@ <!-- StatusCard container--> <ui-status-card + *ngIf="!(useMobileUI$ | async)" [selectedTemplate]="selectedTemplate" - [isMobile]="isMobile" + [isMobile]="useMobileUI$ | async" [onHoverSegmentName]="onHoverSegmentName$ | async" [nehubaViewer]="nehubaViewer"> </ui-status-card> @@ -53,7 +54,7 @@ <!-- mobile nub, allowing for ooblique slicing in mobile --> <mobile-overlay - *ngIf="isMobile && viewerLoaded" + *ngIf="(useMobileUI$ | async) && viewerLoaded" [tunableProperties]="tunableMobileProperties" (deltaValue)="handleMobileOverlayEvent($event)"> <div class="base" delta> @@ -76,6 +77,7 @@ <div (contextmenu)="$event.stopPropagation(); $event.preventDefault();" [ngStyle]="panelMode$ | async | mobileControlNubStylePipe" + *ngIf="(panelMode$ | async) !== 'SINGLE_PANEL'" mobileObliqueCtrl initiator> <button mat-mini-fab color="primary"> @@ -193,7 +195,9 @@ </mat-card> <!-- render all fav dataset as mat list --> + <!-- TODO maybe use virtual scroll here? --> <mat-list-item + mat-ripple class="align-items-center" *ngFor="let ds of (favDataEntries$ | async)" role="listitem"> diff --git a/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts b/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts index 22fd3655e72c0553f62a90716301cbac92f26723..9f7e287a841bae5eeb7822d2ab632b38008d114f 100644 --- a/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts +++ b/src/ui/nehubaContainer/splashScreen/splashScreen.component.ts @@ -73,10 +73,6 @@ export class SplashScreen implements AfterViewInit{ get totalTemplates(){ return this.constanceService.templateUrls.length } - - get isMobile(){ - return this.constantsService.mobile - } } @Pipe({ diff --git a/src/ui/searchSideNav/searchSideNav.component.ts b/src/ui/searchSideNav/searchSideNav.component.ts index 61a5b169e355295b9941a8802a39a10fdf1f73a5..21ee2294c33fc2db0ceefac48dd27e7e5d6db6f3 100644 --- a/src/ui/searchSideNav/searchSideNav.component.ts +++ b/src/ui/searchSideNav/searchSideNav.component.ts @@ -6,6 +6,7 @@ import { Observable, Subscription } from "rxjs"; import { Store, select } from "@ngrx/store"; import { map, startWith, scan, filter, mapTo } from "rxjs/operators"; import { VIEWERSTATE_ACTION_TYPES } from "../viewerStateController/viewerState.base"; +import { trackRegionBy } from '../viewerStateController/regionHierachy/regionHierarchy.component' @Component({ selector: 'search-side-nav', @@ -64,7 +65,7 @@ export class SearchSideNav implements OnInit, OnDestroy { return } if (this.layerBrowserDialogRef) return - + this.dismiss.emit(true) this.layerBrowserDialogRef = this.dialog.open(LayerBrowser, { hasBackdrop: false, @@ -82,4 +83,6 @@ export class SearchSideNav implements OnInit, OnDestroy { payload: { region } }) } + + trackByFn = trackRegionBy } \ No newline at end of file diff --git a/src/ui/searchSideNav/searchSideNav.style.css b/src/ui/searchSideNav/searchSideNav.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..484b4a0181300ea85a44957a71e8cc6750c7f30b 100644 --- a/src/ui/searchSideNav/searchSideNav.style.css +++ b/src/ui/searchSideNav/searchSideNav.style.css @@ -0,0 +1,4 @@ +.region-wrapper +{ + height:78px; +} diff --git a/src/ui/searchSideNav/searchSideNav.template.html b/src/ui/searchSideNav/searchSideNav.template.html index 5d2e5d647f94b712291860d4fe906c68c81acd96..03c542ae012382565c680676d08825f4697f3348 100644 --- a/src/ui/searchSideNav/searchSideNav.template.html +++ b/src/ui/searchSideNav/searchSideNav.template.html @@ -66,13 +66,14 @@ <!-- multi region --> <ng-template #multiRegionTemplate> - <cdk-virtual-scroll-viewport class="h-100" autosize> - <div *cdkVirtualFor="let region of regionsSelected; let index = index"> + <cdk-virtual-scroll-viewport class="h-100" itemSize="78"> + <div *cdkVirtualFor="let region of regionsSelected; trackBy: trackByFn ; let index = index" + class="region-wrapper d-flex flex-column" > <!-- divider if index !== 0 --> - <mat-divider *ngIf="index !== 0"></mat-divider> + <mat-divider class="flex-grow-0 flex-shrink-0" *ngIf="index !== 0"></mat-divider> <!-- selected brain region --> - <div class="pt-2 pb-2 d-flex flex-row align-items-center flex-nowrap"> + <div class="flex-grow-1 flex-shrink-1 pt-2 pb-2 d-flex flex-row align-items-center flex-nowrap"> <i class="fas fa-brain font-2x mr-2"></i> <span class="font-weight-bold"> diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts index abd1eb3397f1c1a3e7ada69d3ead390f29bfbed7..97a23101bf3b107bb8dfa74ef68e5d0fb3db5d8b 100644 --- a/src/ui/signinBanner/signinBanner.components.ts +++ b/src/ui/signinBanner/signinBanner.components.ts @@ -18,14 +18,11 @@ export class SigninBanner{ @Input() darktheme: boolean - public isMobile: boolean - constructor( private constantService: AtlasViewerConstantsServices, private authService: AuthService, private dialog: MatDialog ){ - this.isMobile = this.constantService.mobile } /** diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html index a47e6e9fe192a93d6d8ce4950580d0e8433c11a0..cf0945cfcf66166e2c434b53ad7f02d91ebd8094 100644 --- a/src/ui/signinBanner/signinBanner.template.html +++ b/src/ui/signinBanner/signinBanner.template.html @@ -1,6 +1,4 @@ -<div - class="d-flex" - [ngClass]="{ 'flex-column w-100 align-items-stretch' : isMobile}" > +<div class="d-flex"> <!-- help btn --> <div class="btnWrapper"> diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 817b0a2eaaf2686fa325341f7024ad75e08d4e7d..adf230be016e508378d737106634ffb63c09b6f3 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 "src/layouts/layout.module"; -import { FormsModule } from "@angular/forms"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { GroupDatasetByRegion } from "src/util/pipes/groupDataEntriesByRegion.pipe"; import { filterRegionDataEntries } from "src/util/pipes/filterRegionDataEntries.pipe"; @@ -68,10 +68,16 @@ import {ElementOutClickDirective} from "src/util/directives/elementOutClick.dire import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; import { SearchSideNav } from "./searchSideNav/searchSideNav.component"; +import { RegionHierarchy } from './viewerStateController/regionHierachy/regionHierarchy.component' +import { CurrentlySelectedRegions } from './viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component' +import { RegionTextSearchAutocomplete } from "./viewerStateController/regionSearch/regionSearch.component"; +import { RegionsListView } from "./viewerStateController/regionsListView/simpleRegionsListView/regionListView.component"; + @NgModule({ imports : [ HttpClientModule, FormsModule, + ReactiveFormsModule, LayoutModule, ComponentsModule, DatabrowserModule, @@ -106,9 +112,12 @@ import { SearchSideNav } from "./searchSideNav/searchSideNav.component"; CurrentLayout, ViewerStateController, ViewerStateMini, - + RegionHierarchy, + CurrentlySelectedRegions, MaximmisePanelButton, SearchSideNav, + RegionTextSearchAutocomplete, + RegionsListView, /* pipes */ GroupDatasetByRegion, diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts index ea26648643a8418f7e941e0993f2a77748a5643e..6a059daa3e7790bd06e5b6796545813187b7cd3a 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.component.ts @@ -34,6 +34,9 @@ const getFilterTreeBySearch = (pipe:FilterNameBySearch, searchTerm:string) => (n export class RegionHierarchy implements OnInit, AfterViewInit{ + @Input() + public useMobileUI: boolean = false + @Input() public selectedRegions: any[] = [] @@ -218,4 +221,8 @@ export class RegionHierarchy implements OnInit, AfterViewInit{ public deselectRegion(region: any) { this.singleClickRegion.emit(region) } +} + +export function trackRegionBy(index: number, region: any){ + return region.labelIndex || region.id } \ 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 62181c1198ba15da064d26ce0385738fc5177fb8..121dea1c590f60f7f835b717aadebcc8cfed38a2 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css @@ -12,14 +12,18 @@ div[treeContainer] background-color:rgba(12,12,12,0.8); */ } -[selectedRegionsChipsContainer] +.flex-basis-20-pc { - flex: 0 0 20%; + flex-basis: 20%; +} + +.flex-basis-auto +{ + flex-basis: auto; } [hideScrollbarcontainer] { - flex: 1 1 0; overflow:hidden; } diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html index 0e4d9475b667ca3c8e83d301dba95a988413d456..6f85fde79957f9953b4c710a3e070620a78d8168 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html @@ -10,30 +10,58 @@ [placeholder]="placeHolderText"/> </mat-form-field> -<div class="d-flex flex-grow-1 flex-shrink-1"> +<ng-template #noRegionSelected> + No region selected +</ng-template> - <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> - <div - (click)="clearRegions($event)" - [ngClass]="{ 'invisible': selectedRegions.length === 0 }" - class="btn btn-link"> +<ng-template #regionSelectedText> + <span class="text-muted"> + <ng-template [ngIf]="selectedRegions.length > 0" [ngIfElse]="noRegionSelected"> + {{ (selectedRegions | filterRowsByVisbilityPipe : null : filterTreeBySearch).length }} / {{ selectedRegions.length }} + </ng-template> + </span> +</ng-template> + +<div + [ngClass]="{'flex-column': useMobileUI, 'flex-row': !useMobileUI}" + class="d-flex flex-grow-1 flex-shrink-1"> + + <!-- selected regions --> + <div + [ngClass]="{'flex-basis-20-pc': !useMobileUI, 'flex-basis-auto': useMobileUI}" + class="d-flex flex-column flex-grow-0 flex-shrink-0"> + + <div class="flex-grow-0 flex-shrink-0 d-flex flex-row align-items-center"> + + <button mat-button + *ngIf="selectedRegions.length > 0" + (click)="clearRegions($event)"> clear all - </div> + </button> + + <span class="m-1"> + <ng-container *ngTemplateOutlet="regionSelectedText"> + </ng-container> + </span> </div> - <div hideScrollbarcontainer> - <currently-selected-regions - class="d-block h-100" - hideScrollbarInnerContainer> - </currently-selected-regions> + <mat-divider></mat-divider> + + <div *ngIf="(selectedRegions | filterRowsByVisbilityPipe : null : filterTreeBySearch).length > 0" + class="mt-2 min-h-8" + hideScrollbarcontainer> + <regions-list-view class="d-block h-100" + (gotoRegion)="gotoRegion($event)" + (deselectRegion)="deselectRegion($event)" + [horizontal]="useMobileUI" + [regionsSelected]="selectedRegions | filterRowsByVisbilityPipe : null : filterTreeBySearch"> + + </regions-list-view> </div> </div> - <div hideScrollbarContainer> + <!-- region tree --> + <div class="flex-grow-1 flex-shrink-1" hideScrollbarContainer> <div class="d-flex flex-column h-100" treeContainer diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index 35b70d8177cb2d451e32a8c193b118df78afdc6f..e32fa8cb4e4e58760e373c2857d37b454f423584 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -7,6 +7,7 @@ import { FormControl } from "@angular/forms"; import { MatAutocompleteSelectedEvent, MatDialog } from "@angular/material"; import { ADD_TO_REGIONS_SELECTION_WITH_IDS, SELECT_REGIONS } from "src/services/state/viewerState.store"; import { VIEWERSTATE_ACTION_TYPES } from "../viewerState.base"; +import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase()) @@ -24,12 +25,18 @@ export class RegionTextSearchAutocomplete{ @Input() public showAutoComplete: boolean = true @ViewChild('autoTrigger', {read: ElementRef}) autoTrigger: ElementRef - @ViewChild('regionHierarchy', {read:TemplateRef}) regionHierarchyTemplate: TemplateRef<any> + @ViewChild('regionHierarchyDialog', {read:TemplateRef}) regionHierarchyDialogTemplate: TemplateRef<any> + + public useMobileUI$: Observable<boolean> + constructor( private store$: Store<any>, private dialog: MatDialog, + private constantService: AtlasViewerConstantsServices ){ + this.useMobileUI$ = this.constantService.useMobileUI$ + const viewerState$ = this.store$.pipe( select('viewerState'), shareReplay(1) @@ -131,8 +138,9 @@ export class RegionTextSearchAutocomplete{ } showHierarchy(event:MouseEvent){ - const dialog = this.dialog.open(this.regionHierarchyTemplate, { - height: '90vh', + // mat-card-content has a max height of 65vh + const dialog = this.dialog.open(this.regionHierarchyDialogTemplate, { + height: '65vh', width: '90vw' }) diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.template.html b/src/ui/viewerStateController/regionSearch/regionSearch.template.html index bf67e5428d58ba6ca82719f2427e4c937421342d..e08a8c7f53b3c30a17b75b1bc14f5e017905178a 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.template.html +++ b/src/ui/viewerStateController/regionSearch/regionSearch.template.html @@ -36,8 +36,24 @@ </button> </div> +<ng-template #regionHierarchyDialog> + <div class="h-100 d-flex flex-column"> + <mat-dialog-content class="flex-grow-1 flex-shrink-1"> + <ng-container *ngTemplateOutlet="regionHierarchy"> + </ng-container> + </mat-dialog-content> + + <mat-dialog-actions class="justify-content-center"> + <button mat-dialog-close mat-flat-button> + close + </button> + </mat-dialog-actions> + </div> +</ng-template> + <ng-template #regionHierarchy> <region-hierarchy + [useMobileUI]="useMobileUI$ | async" [selectedRegions]="regionsSelected$ | async | filterNull" (singleClickRegion)="handleRegionClick({ mode: 'single', region: $event })" (doubleClickRegion)="handleRegionClick({ mode: 'double', region: $event })" diff --git a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts b/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts similarity index 92% rename from src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts rename to src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts index 812b734b2ad558a0c51354321f304bba0773ea02..7b02be507c2ad53a7c7ddfeea986bdef57aab2c7 100644 --- a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts +++ b/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component.ts @@ -3,7 +3,7 @@ import { Store, select } from "@ngrx/store"; import { Observable } from "rxjs"; import { distinctUntilChanged, startWith } from "rxjs/operators"; import { DESELECT_REGIONS } from "src/services/state/viewerState.store"; -import { VIEWERSTATE_ACTION_TYPES } from "../viewerState.base"; +import { VIEWERSTATE_ACTION_TYPES } from "src/ui/viewerStateController/viewerState.base"; @Component({ selector: 'currently-selected-regions', diff --git a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.style.css b/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.style.css similarity index 100% rename from src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.style.css rename to src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.style.css diff --git a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.template.html b/src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.template.html similarity index 100% rename from src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.template.html rename to src/ui/viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.template.html diff --git a/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.component.ts b/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a964af89956e988fff8fc2b7230c784e002ab3a --- /dev/null +++ b/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.component.ts @@ -0,0 +1,18 @@ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from "@angular/core"; + +@Component({ + selector: 'regions-list-view', + templateUrl: './regionListView.template.html', + styleUrls: [ + './regionListView.style.css' + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class RegionsListView{ + @Input() horizontal: boolean = false + + @Input() regionsSelected: any[] = [] + @Output() deselectRegion: EventEmitter<any> = new EventEmitter() + @Output() gotoRegion: EventEmitter<any> = new EventEmitter() +} \ No newline at end of file diff --git a/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.style.css b/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.style.css new file mode 100644 index 0000000000000000000000000000000000000000..fda82d0bacf99f1f7d596212fe2d15051e9d5354 --- /dev/null +++ b/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.style.css @@ -0,0 +1,20 @@ +mat-chip-list >>> .mat-chip-list-wrapper +{ + height: 100%; +} +cdk-virtual-scroll-viewport.cdk-virtual-scroll-orientation-horizontal +{ + overflow-y: hidden; +} +cdk-virtual-scroll-viewport.cdk-virtual-scroll-orientation-horizontal >>> .cdk-virtual-scroll-content-wrapper +{ + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + + +cdk-virtual-scroll-viewport.cdk-virtual-scroll-orientation-horizontal mat-chip +{ + flex: 0 0 200px; +} \ No newline at end of file diff --git a/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.template.html b/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.template.html new file mode 100644 index 0000000000000000000000000000000000000000..9fff5b6a62291bde1090d91dfc9dad4f2efa13aa --- /dev/null +++ b/src/ui/viewerStateController/regionsListView/simpleRegionsListView/regionListView.template.html @@ -0,0 +1,29 @@ +<mat-chip-list class="p-1 d-block h-100 w-100"> + <cdk-virtual-scroll-viewport + [orientation]="horizontal ? 'horizontal' : 'vertical'" + class="w-100 h-100" + minBufferPx="1000" + maxBufferPx="1500" + itemSize="200"> + + <mat-chip *cdkVirtualFor="let region of regionsSelected" + [ngClass]="{'w-100': !horizontal }"> + <span class="flex-grow-1 flex-shrink-1 text-truncate"> + {{ region.name }} + </span> + <button + *ngIf="region.position" + iav-stop="mousedown click" + (click)="gotoRegion.emit(region)" + mat-icon-button> + <i class="fas fa-map-marked-alt"></i> + </button> + <button + iav-stop="mousedown click" + (click)="deselectRegion.emit(region)" + mat-icon-button> + <i class="fas fa-trash"></i> + </button> + </mat-chip> + </cdk-virtual-scroll-viewport> +</mat-chip-list> \ No newline at end of file diff --git a/src/ui/viewerStateController/viewerStateCMini/viewerStateMini.template.html b/src/ui/viewerStateController/viewerStateCMini/viewerStateMini.template.html index 8dda9dbdcc13e04538bb93fb6ca640262e22d96c..420457e17890c693e79ebfc9a14f1fdc7b3dc5ee 100644 --- a/src/ui/viewerStateController/viewerStateCMini/viewerStateMini.template.html +++ b/src/ui/viewerStateController/viewerStateCMini/viewerStateMini.template.html @@ -4,4 +4,14 @@ <br> <span *ngIf="parcellationSelected$ | async as parcellationSelected"> {{ parcellationSelected.name }} -</span> \ No newline at end of file +</span> + +<ng-container *ngIf="regionsSelected$ | async as regionsSelected"> + <ng-container *ngIf="regionsSelected.length > 0"> + + <br> + <span> + {{ regionsSelected.length }} region{{ regionsSelected.length > 1 ? 's' : '' }} selected + </span> + </ng-container> +</ng-container> \ No newline at end of file diff --git a/src/util/constants.ts b/src/util/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1612bcb4b9294ee210508e448e5859952ed789a --- /dev/null +++ b/src/util/constants.ts @@ -0,0 +1,13 @@ +export const LOCAL_STORAGE_CONST = { + GPU_LIMIT: 'fzj.xg.iv.GPU_LIMIT', + ANIMATION: 'fzj.xg.iv.ANIMATION_FLAG', + SAVED_REGION_SELECTIONS: 'fzj.xg.iv.SAVED_REGION_SELECTIONS', + MOBILE_UI: 'fzj.xg.iv.MOBILE_UI', + AGREE_COOKIE: 'fzj.xg.iv.AGREE_COOKIE', + AGREE_KG_TOS: 'fzj.xg.iv.AGREE_KG_TOS', + + FAV_DATASET: 'fzj.xg.iv.FAV_DATASET' +} + +export const COOKIE_VERSION = '0.3.0' +export const KG_TOS_VERSION = '0.3.0' diff --git a/src/util/directives/stopPropagation.directive.ts b/src/util/directives/stopPropagation.directive.ts index e211a566812bed435159a7334c3eac0f3b30a0f2..a3cc308fae91e1f6cf8cf14389d7bdfb044e7b7a 100644 --- a/src/util/directives/stopPropagation.directive.ts +++ b/src/util/directives/stopPropagation.directive.ts @@ -5,7 +5,10 @@ const VALID_EVENTNAMES = new Set([ 'mouseup', 'click', 'mouseenter', - 'mouseleave' + 'mouseleave', + 'touchstart', + 'touchmove', + 'touchend' ]) const stopPropagation = ev => ev.stopPropagation()