diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index e5e89a7790a51c3b1bce7558fb78339c5f9dc62e..094c5b3035c7835fef28ba1ee6c9f6326f3fd945 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -91,7 +91,7 @@ <div resizeSliver> <span - *ngIf = "isMobile" + *ngIf = "isMobile || true" class = "tabContainer" (click) = "toggleSidePanel('menuBrowser')" [ngClass] = "{'active-tab' : (sidePanelView$ | async) === 'menuBrowser'}" > diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index dec754ca211e5cba31b93b7ae89189566d67ab59..96f68781bdfb562b982f9809a96de6181d06e15d 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -224,4 +224,24 @@ markdown-dom pre code border: 0.2em rgba(128,128,128,0.2) solid; border-top: 0.2em rgba(128,128,128,0.99) solid; animation: spinning 700ms linear infinite running; +} + +.theme-controlled.btn, +.theme-controlled.btn, +.theme-controlled.btn +{ + border-radius:0; + border: none; +} + +[darktheme="true"] .theme-controlled.btn.btn-default +{ + background-color: rgba(50, 50 , 50, 1.0); + color: rgba(230, 230, 230, 1.0); +} + +[darktheme="true"] .theme-controlled.btn.btn-default.active +{ + background-color: rgba(70, 70 , 70, 1.0); + color: rgba(255, 255, 255, 1.0); } \ No newline at end of file diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1435c56ec723cabacf6431c434df7a527f7a5174 --- /dev/null +++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts @@ -0,0 +1,185 @@ +import { Component, Input, Output,EventEmitter, ElementRef, ViewChild, AfterViewInit, ChangeDetectionStrategy, OnDestroy } from "@angular/core"; +import { fromEvent, Subject, Observable, merge, concat, of, combineLatest } from "rxjs"; +import { map, switchMap, takeUntil, filter, scan, take } from "rxjs/operators"; + +@Component({ + selector : 'mobile-overlay', + templateUrl : './mobileOverlay.template.html', + styleUrls : [ + './mobileOverlay.style.css' + ], + styles : [ + ` +div.active > span:before +{ + content: '\u2022'; + width: 1em; + display: inline-block; + background:none; +} +div:not(.active) > span:before +{ + content : ' '; + width : 1em; + display: inline-block; +} + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class MobileOverlay implements AfterViewInit, OnDestroy{ + @Input() tunableProperties : string [] = [] + @Output() deltaValue : EventEmitter<{delta:number, selectedProp : string}> = new EventEmitter() + @ViewChild('initiator', {read: ElementRef}) initiator : ElementRef + @ViewChild('mobileMenuContainer', {read: ElementRef}) menuContainer : ElementRef + + private _onDestroySubject : Subject<boolean> = new Subject() + + private _focusedProperties : string + get focusedProperty(){ + return this._focusedProperties + ? this._focusedProperties + : this.tunableProperties[0] + } + + public showScreen$ : Observable<boolean> + public showProperties$ : Observable<boolean> + private _drag$ : Observable<any> + + ngOnDestroy(){ + this._onDestroySubject.next(true) + this._onDestroySubject.complete() + } + + ngAfterViewInit(){ + + this.showScreen$ = merge( + fromEvent(this.initiator.nativeElement, 'touchstart'), + fromEvent(this.initiator.nativeElement, 'touchend'), + ).pipe( + map((ev:TouchEvent) => ev.touches.length === 1) + ) + + this._drag$ = fromEvent(this.initiator.nativeElement, 'touchmove').pipe( + takeUntil(fromEvent(this.initiator.nativeElement, 'touchend').pipe( + filter((ev:TouchEvent) => ev.touches.length === 0) + )), + map((ev:TouchEvent) => (ev.preventDefault(), ev.stopPropagation(), ev)), + filter((ev:TouchEvent) => ev.touches.length === 1), + scan((acc,curr) => acc.length < 2 + ? acc.concat(curr) + : acc.slice(1).concat(curr), []), + filter(ev => ev.length === 2) + ) + + this.showProperties$ = fromEvent(this.initiator.nativeElement, 'touchstart').pipe( + switchMap(() => concat( + this._drag$.pipe( + map(double => ({ + deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, + deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY + })), + scan((acc, _curr) => acc), + map(v => v.deltaY ** 2 > v.deltaX ** 2) + ), + of(false) + ) + ) + ) + + fromEvent(this.initiator.nativeElement, 'touchstart').pipe( + switchMap(() => this._drag$.pipe( + map(double => ({ + deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, + deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY + })), + scan((acc, curr:any) => ({ + pass: acc.pass === null + ? curr.deltaX ** 2 > curr.deltaY ** 2 + : acc.pass, + delta: curr.deltaX + }), { + pass: null, + delta : null + }), + filter(ev => ev.pass), + map(ev => ev.delta) + )), + takeUntil(this._onDestroySubject) + ).subscribe(ev => this.deltaValue.emit({ + delta : ev, + selectedProp : this.focusedProperty + })) + + const offsetObs$ = fromEvent(this.initiator.nativeElement, 'touchstart').pipe( + switchMap(() => concat( + this._drag$.pipe( + scan((acc,curr) => [acc[0], curr[1]]), + map(double => ({ + deltaX : double[1].touches[0].screenX - double[0].touches[0].screenX, + deltaY : double[1].touches[0].screenY - double[0].touches[0].screenY + })), + ) + )) + ) + combineLatest( + this.showProperties$, + offsetObs$ + ).pipe( + filter(v => v[0]), + map(v => v[1]), + takeUntil(this._onDestroySubject) + ).subscribe(v => this.scrollHeight = v.deltaY) + + this.showProperties$.pipe( + takeUntil(this._onDestroySubject), + filter(v => !v) + ).subscribe(() => { + if(this.focusItemIndex >= 0){ + this._focusedProperties = this.tunableProperties[this.focusItemIndex] + } + this.scrollHeight = 0 + }) + } + + scrollHeight : number = 0 + + get defaultY(){ + return this.tunableProperties.findIndex(p => p === this.focusedProperty) * this.menuCellHeight + } + + get menuTransform(){ + return `translate(0px, ${this.menuYTranslate}px)` + } + + get menuYTranslate(){ + return this.menuContainer + ? Math.max( + Math.min( + this.defaultY + this.scrollHeight, + this.menuContainerHeight / 2 - this.menuCellHeight / 2 + ), + - this.menuContainerHeight / 2 + this.menuCellHeight / 2 + ) + : this.defaultY + } + + get menuContainerHeight(){ + return this.menuContainer + ? this.menuContainer.nativeElement.offsetHeight + : 0 + } + + get menuCellHeight(){ + return this.tunableProperties.length > 0 + ? this.menuContainerHeight / this.tunableProperties.length + : 0 + } + + get focusItemIndex():number{ + return this.menuContainer + ? Math.floor((this.menuContainerHeight / 2 - this.menuYTranslate) / this.menuCellHeight) + : -1 + } +} \ No newline at end of file diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css new file mode 100644 index 0000000000000000000000000000000000000000..87cdcdb74678417815d754680476c1dedd767f37 --- /dev/null +++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.style.css @@ -0,0 +1,66 @@ +:host +{ + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + z-index: 9999; + + pointer-events: none; +} + +:host [screen] +{ + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + + display: flex; + justify-content: center; + align-items: center; + + color : black; + background-color: rgba(255, 255, 255, 0.5); +} + +:host-context([darktheme="true"]) [screen] +{ + color : white; + background-color: rgba(0, 0, 0, 0.5); +} + +[mobileMenuContainer] +{ + z-index: 9999; + transform: translate(0, 155px); +} + +.scrollFocus:after +{ + content: ' '; + position:absolute; + width:100%; + height:100%; + top: 0; + left: 0; + + background-color: rgba(0, 0, 128, 0.2); +} + +:host-context([darktheme="true"]) .scrollFocus:after +{ + background-color: rgba(128, 128, 200, 0.2); +} + +[guide] +{ + z-index:9999; +} + +:host-context([darktheme="true"]) +{ + /* color: white; */ +} \ No newline at end of file diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html new file mode 100644 index 0000000000000000000000000000000000000000..541b687d8831e900e346db53607c2f14d812a67c --- /dev/null +++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.template.html @@ -0,0 +1,18 @@ +<div screen *ngIf = "showProperties$ | async"> + <div [style.transform] = "menuTransform" class = "btn-group-vertical" role = "group" mobileMenuContainer #mobileMenuContainer> + <div + *ngFor = "let p of tunableProperties; let i = index" + [ngClass] = "{'active' : p === focusedProperty, 'scrollFocus' : i === focusItemIndex}" + class = "btn btn-default theme-controlled"> + <span> + {{ p }} + </span> + </div> + </div> +</div> +<ng-content *ngIf = "showScreen$ | async" select = "[guide]" guide> +</ng-content> +<div #initiator> + <ng-content select="[initiator]"> + </ng-content> +</div> \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 65187a2d6b0b1a46607f33179d7ddc55af8ca6c8..133dcf08a529a664cb5ab5c31ce1370e62e1b432 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -24,8 +24,6 @@ export class NehubaContainer implements OnInit, OnDestroy{ @ViewChild('[pos01]',{read:ElementRef}) topright : ElementRef @ViewChild('[pos10]',{read:ElementRef}) bottomleft : ElementRef @ViewChild('[pos11]',{read:ElementRef}) bottomright : ElementRef - @ViewChild('mobileObliqueCtrl', {read:ElementRef}) mobileObliqueCtrl : ElementRef - @ViewChild('mobileObliqueScreen', {read:ElementRef}) mobileObliqueScreen : ElementRef private nehubaViewerFactory : ComponentFactory<NehubaViewerUnit> @@ -552,50 +550,69 @@ export class NehubaContainer implements OnInit, OnDestroy{ } // datasetViewerRegistry : Set<string> = new Set() - public showObliqueScreen : Observable<boolean> + public showObliqueScreen$ : Observable<boolean> + public showObliqueSelection$ : Observable<boolean> + public showObliqueRotate$ : Observable<boolean> ngAfterViewInit(){ - if(this.isMobile){ - - this.showObliqueScreen = merge( - fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchstart'), - fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchend') - ).pipe( - map((ev:TouchEvent) => ev.touches.length === 1) - ) - - fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchstart').pipe( - switchMap(() => fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchmove').pipe( - takeUntil(fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchend').pipe( - filter((ev:TouchEvent) => ev.touches.length === 0) - )), - map((ev:TouchEvent) => (ev.preventDefault(), ev.stopPropagation(), ev)), - filter((ev:TouchEvent) => ev.touches.length === 1), - map((ev:TouchEvent) => [ev.touches[0].screenX, ev.touches[0].screenY] ), - - /* TODO reduce boiler plate, export */ - scan((acc,curr) => acc.length < 2 - ? acc.concat([curr]) - : acc.slice(1).concat([curr]), []), - filter(isdouble => isdouble.length === 2), - map(double => ({ - deltaX : double[1][0] - double[0][0], - deltaY : double[1][1] - double[0][1] - })) - )) - ) - .subscribe(({deltaX, deltaY}) => { - const {navigationState} = this.nehubaViewer.nehubaViewer.ngviewer - navigationState.pose.rotateRelative(this.nehubaViewer.vec3([0, 1, 0]), -deltaX / 4.0 * Math.PI / 180.0) - navigationState.pose.rotateRelative(this.nehubaViewer.vec3([1, 0, 0]), deltaY / 4.0 * Math.PI / 180.0) - }) - } + // if(this.isMobile){ + + // this.showObliqueScreen$ = merge( + // fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchstart'), + // fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchend') + // ).pipe( + // map((ev:TouchEvent) => ev.touches.length === 1) + // ) + + // fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchstart').pipe( + // switchMap(() => fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchmove').pipe( + // takeUntil(fromEvent(this.mobileObliqueCtrl.nativeElement, 'touchend').pipe( + // filter((ev:TouchEvent) => ev.touches.length === 0) + // )), + // map((ev:TouchEvent) => (ev.preventDefault(), ev.stopPropagation(), ev)), + // filter((ev:TouchEvent) => ev.touches.length === 1), + // map((ev:TouchEvent) => [ev.touches[0].screenX, ev.touches[0].screenY] ), + + // /* TODO reduce boiler plate, export */ + // scan((acc,curr) => acc.length < 2 + // ? acc.concat([curr]) + // : acc.slice(1).concat([curr]), []), + // filter(isdouble => isdouble.length === 2), + // map(double => ({ + // deltaX : double[1][0] - double[0][0], + // deltaY : double[1][1] - double[0][1] + // })) + // )) + // ) + // .subscribe(({deltaX, deltaY}) => { + + + + // }) + // } } ngOnDestroy(){ this.subscriptions.forEach(s=>s.unsubscribe()) } + get tunableMobileProperties(){ + return ['Oblique Rotate X', 'Oblique Rotate Y', 'Oblique Rotate Z'] + } + + handleMobileOverlayEvent(obj:any){ + const {delta, selectedProp} = obj + + const idx = this.tunableMobileProperties.findIndex(p => p === selectedProp) + idx === 0 + ? this.nehubaViewer.obliqueRotateX(delta) + : idx === 1 + ? this.nehubaViewer.obliqueRotateY(delta) + : idx === 2 + ? this.nehubaViewer.obliqueRotateZ(delta) + : console.warn('could not oblique rotate') + } + returnTruePos(quadrant:number,data:any){ const pos = quadrant > 2 ? [0,0,0] : diff --git a/src/ui/nehubaContainer/nehubaContainer.style.css b/src/ui/nehubaContainer/nehubaContainer.style.css index 6b12b3bb969276b7c57575f3b413cc1bac293bb5..13e9fe21bb986f3031f73b4ccbd69237d45369e3 100644 --- a/src/ui/nehubaContainer/nehubaContainer.style.css +++ b/src/ui/nehubaContainer/nehubaContainer.style.css @@ -167,4 +167,14 @@ div[mobileObliqueScreen] background-color:rgba(128,128,128,0.2); transition: all 0.5s linear; pointer-events: all; +} + +div[mobileObliqueGuide] +{ + position : absolute; + top: 40%; + width: 100%; + display:flex; + flex-direction: column; + align-items: center; } \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 9153ddd3d74ea7b1171c657744ff229479a9d7b5..b3158ad21a3fc38e1c2bc82db6547274eb9d457d 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -86,9 +86,7 @@ </div> </div> -<layout-floating-container - *ngIf = "viewerLoaded"> - +<layout-floating-container *ngIf = "viewerLoaded"> <div statusCard> @@ -130,12 +128,19 @@ </div> </div> </layout-floating-container> -<layout-floating-container *ngIf = "isMobile" [show] = "viewerLoaded"> - - <div *ngIf = "showObliqueScreen | async" #mobileObliqueScreen mobileObliqueScreen> - +<mobile-overlay + *ngIf = "isMobile && viewerLoaded" + [tunableProperties] = "tunableMobileProperties" + (deltaValue) = "handleMobileOverlayEvent($event)"> + <div mobileObliqueGuide guide> + <div> + <i class="glyphicon glyphicon-resize-vertical"></i> oblique mode + </div> + <div> + <i class="glyphicon glyphicon-resize-horizontal"></i> rotate slice + </div> </div> - <div mobileObliqueCtrl #mobileObliqueCtrl> + <div mobileObliqueCtrl initiator> <i class="glyphicon glyphicon-globe"></i> </div> -</layout-floating-container> \ No newline at end of file +</mobile-overlay> \ No newline at end of file diff --git a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts index 608c1b758250fc67d7607a19a44391c3c93e4285..4f2baf2f003492710399b52a59def3478a51a7d5 100644 --- a/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts +++ b/src/ui/nehubaContainer/nehubaViewer/nehubaViewer.component.ts @@ -344,6 +344,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ this.nehubaViewer.ngviewer.navigationState.zoomBy(factor)) ) } + ngOnDestroy(){ this._s$.forEach(_s$=>{ if(_s$) _s$.unsubscribe() @@ -385,6 +386,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ layerObj[key] == l[key]) } + // TODO single landmark for user landmark public addUserLandmarks(landmarks:any[]){ if(!this.nehubaViewer) return @@ -406,6 +408,7 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ this.loadLayer(_) } + // TODO single landmark for user landmark public removeUserLandmarks(){ if(!this.nehubaViewer) return @@ -535,6 +538,18 @@ export class NehubaViewerUnit implements AfterViewInit,OnDestroy{ this.nehubaViewer.setPosition( this.vec3(position) , positionReal ? true : false ) } + public obliqueRotateX(amount:number){ + this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 1, 0]), -amount / 4.0 * Math.PI / 180.0) + } + + public obliqueRotateY(amount:number){ + this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([1, 0, 0]), amount / 4.0 * Math.PI / 180.0) + } + + public obliqueRotateZ(amount:number){ + this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 0, 1]), amount / 4.0 * Math.PI / 180.0) + } + private updateColorMap(arrayIdx:number[]){ const set = new Set(arrayIdx) const newColorMap = new Map( diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 9ac37e377aaf8afe762007bfd387a4cb55b632e6..b48c6e7de35192d8f489cc3b0ad08d48a6c24678 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -36,6 +36,7 @@ import { SpatialLandmarksToDataBrowserItemPipe } from "../util/pipes/spatialLand 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"; @NgModule({ @@ -67,6 +68,7 @@ import { TemplateParcellationCitationsContainer } from "./templateParcellationCi DatasetViewerComponent, LogoContainer, TemplateParcellationCitationsContainer, + MobileOverlay, /* pipes */ GroupDatasetByRegion, @@ -104,7 +106,8 @@ import { TemplateParcellationCitationsContainer } from "./templateParcellationCi FileViewer, LogoContainer, DatasetViewerComponent, - TemplateParcellationCitationsContainer + TemplateParcellationCitationsContainer, + MobileOverlay, ] })