diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index c2b66ac5731e0d3805b5d54eb27ad21743cbaf9c..f58775aa9923e70c4e83ee5496e3cb2c21705335 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -16,6 +16,7 @@ import { FixedMouseContextualContainerDirective } from "src/util/directives/Fixe import { DatabrowserService } from "src/ui/databrowserModule/databrowser.service"; import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS } from "src/services/state/uiState.store"; import { TabsetComponent } from "ngx-bootstrap/tabs"; +import { LocalFileService } from "src/services/localFile.service"; import { MatDialog, MatDialogRef } from "@angular/material"; /** @@ -81,6 +82,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { /* handlers for nglayer */ /** * TODO make untangle nglayernames and its dependency on ng + * TODO deprecated */ public ngLayerNames$ : Observable<any> public ngLayers : NgLayerInterface[] @@ -105,8 +107,13 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { private matDialog: MatDialog, private databrowserService: DatabrowserService, private dispatcher$: ActionsSubject, - private rd: Renderer2 + private rd: Renderer2, + public localFileService: LocalFileService ) { + + /** + * TODO deprecated + */ this.ngLayerNames$ = this.store.pipe( select('viewerState'), filter(state => isDefined(state) && isDefined(state.templateSelected)), @@ -278,6 +285,9 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) ) + /** + * TODO deprecated + */ this.subscriptions.push( this.ngLayerNames$.pipe( concatMap(data => this.constantsService.loadExportNehubaPromise.then(data)) @@ -376,6 +386,10 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { map(([_flag, onhoverLandmark]) => onhoverLandmark || []) ) + /** + * TODO clean up code + * do not do this imperatively + */ this.closeMenuWithSwipe(this.mobileSideNav) } @@ -389,7 +403,7 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { /** * perhaps move this to constructor? */ - meetsRequirements() { + meetsRequirements():boolean { const canvas = document.createElement('canvas') const gl = canvas.getContext('webgl2') as WebGLRenderingContext @@ -419,6 +433,9 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { return true } + /** + * TODO deprecated + */ ngLayersChangeHandler(){ this.ngLayers = (window['viewer'].layerManager.managedLayers as any[]) // .filter(obj => obj.sourceUrl && /precomputed|nifti/.test(obj.sourceUrl)) @@ -444,12 +461,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) } - panelAnimationEnd(){ - if( this.nehubaContainer && this.nehubaContainer.nehubaViewer && this.nehubaContainer.nehubaViewer.nehubaViewer ) { - this.nehubaContainer.nehubaViewer.nehubaViewer.redraw() - } - } - nehubaClickHandler(event:MouseEvent){ if (!this.rClContextualMenu) return this.rClContextualMenu.mousePos = [ @@ -459,17 +470,18 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.rClContextualMenu.show() } - toggleSidePanel(panelName:string){ - this.store.dispatch({ - type : TOGGLE_SIDE_PANEL, - focusedSidePanel :panelName - }) - } - private selectedTemplate: any searchRegion(regions:any[]){ this.rClContextualMenu.hide() + + /** + * TODO move this to somewhere that makes sense, not in atlas viewer (? perhaps) + */ this.databrowserService.queryData({ regions, parcellation: this.selectedParcellation, template: this.selectedTemplate }) + + /** + * TODO clean up code. do not do this imperically + */ if (this.isMobile) { this.store.dispatch({ type : OPEN_SIDE_PANEL @@ -486,6 +498,9 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @HostBinding('attr.version') public _version : string = VERSION + /** + * TODO deprecated + */ changeMenuState({open, close}:{open?:boolean, close?:boolean} = {}) { if (open) { return this.store.dispatch({ diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index f8d3522472ea1c736ea56850db0091c8b536d69a..d22eae5adfd3f8b045326761fc9d7bc2033e233b 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -149,7 +149,7 @@ <!-- atlas template --> <ng-template #viewerBody> - <div class="atlas-container" helpdirective> + <div class="atlas-container" (drag-drop)="localFileService.handleFileDrop($event)" helpdirective> <ui-nehuba-container (contextmenu)="nehubaClickHandler($event)"> </ui-nehuba-container> diff --git a/src/atlasViewer/atlasViewer.urlService.service.ts b/src/atlasViewer/atlasViewer.urlService.service.ts index 1008c7a50e424d52c4c1bc32fc072163d8bd852e..72a5ed8886d1d34c518ec48a674d32b9c9c9d388 100644 --- a/src/atlasViewer/atlasViewer.urlService.service.ts +++ b/src/atlasViewer/atlasViewer.urlService.service.ts @@ -249,7 +249,7 @@ export class AtlasViewerURLService{ const niftiLayers = searchparams.get('niftiLayers') if(niftiLayers){ const layers = niftiLayers.split('__') - /* */ + layers.forEach(layer => this.store.dispatch({ type : ADD_NG_LAYER, layer : { @@ -346,7 +346,11 @@ export class AtlasViewerURLService{ return _ }) ), - this.additionalNgLayers$, + this.additionalNgLayers$.pipe( + map(layers => layers + .map(layer => layer.name) + .filter(layername => !/^blob\:/.test(layername))) + ), this.pluginState$ ).pipe( /* TODO fix encoding of nifti path. if path has double underscore, this encoding will fail */ @@ -357,7 +361,7 @@ export class AtlasViewerURLService{ ? Array.from(pluginState.initManifests.values()).filter(v => v !== null).join('__') : null, niftiLayers : niftiLayers.length > 0 - ? niftiLayers.map(layer => layer.name).join('__') + ? niftiLayers.join('__') : null } }) diff --git a/src/main.module.ts b/src/main.module.ts index b557d69b45e03f6990b3ea808e6470ba210462e1..ad4bd2ea47da47dd16e7e149f1d0f0c07f507951 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -40,6 +40,8 @@ import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; import {HttpClientModule} from "@angular/common/http"; import { EffectsModule } from "@ngrx/effects"; import { UseEffects } from "./services/effect/effect"; +import { DragDropDirective } from "./util/directives/dragDrop.directive"; +import { LocalFileService } from "./services/localFile.service"; import { DataBrowserUseEffect } from "./ui/databrowserModule/databrowser.useEffect"; import { DialogService } from "./services/dialogService.service"; import { DialogComponent } from "./components/dialog/dialog.component"; @@ -101,6 +103,7 @@ import 'hammerjs' PluginFactoryDirective, FloatingMouseContextualContainerDirective, FixedMouseContextualContainerDirective, + DragDropDirective, /* pipes */ GetNamesPipe, @@ -124,6 +127,7 @@ import 'hammerjs' ToastService, AtlasWorkerService, AuthService, + LocalFileService, DialogService, /** diff --git a/src/services/localFile.service.ts b/src/services/localFile.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d38450012e3d7a8e4ea680f483c27c52a4ea4013 --- /dev/null +++ b/src/services/localFile.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from "@angular/core"; +import { MatSnackBar } from "@angular/material"; +import { DatabrowserService } from "src/ui/databrowserModule/databrowser.service"; + +/** + * experimental service handling local user files such as nifti and gifti + */ + +@Injectable({ + providedIn: 'root' +}) + +export class LocalFileService { + public SUPPORTED_EXT = SUPPORTED_EXT + private supportedExtSet = new Set(SUPPORTED_EXT) + + constructor( + private snackBar: MatSnackBar, + private dbService: DatabrowserService + ){ + + } + + private niiUrl + + public handleFileDrop(files: File[]){ + try { + this.validateDrop(files) + for (const file of files) { + const ext = this.getExtension(file.name) + switch (ext) { + case NII: { + this.handleNiiFile(file) + break; + } + default: + throw new Error(`File ${file.name} does not have a file handler`) + } + } + } catch (e) { + this.snackBar.open(e, `Dismiss`, { + duration: 5000 + }) + console.error(e) + } + } + + private getExtension(filename:string) { + const match = /(\.\w*?)$/i.exec(filename) + return (match && match[1]) || '' + } + + private validateDrop(files: File[]){ + if (files.length !== 1) { + throw new Error('Interactive atlas viewer currently only supports drag and drop of one file at a time') + } + for (const file of files) { + const ext = this.getExtension(file.name) + if (!this.supportedExtSet.has(ext)) { + throw new Error(`File ${file.name}${ext === '' ? ' ' : (' with extension ' + ext)} cannot be loaded. The supported extensions are: ${this.SUPPORTED_EXT.join(', ')}`) + } + } + } + + private handleNiiFile(file: File){ + + if (this.niiUrl) { + URL.revokeObjectURL(this.niiUrl) + } + this.niiUrl = URL.createObjectURL(file) + this.dbService.showNewNgLayer({ + url: this.niiUrl + }) + + this.showLocalWarning() + } + + private showLocalWarning() { + this.snackBar.open(`Warning: sharing URL will not share the loaded local file`, 'Dismiss', { + duration: 5000 + }) + } +} + +const NII = `.nii` +const GII = '.gii' + +const SUPPORTED_EXT = [ + NII, + GII +] \ No newline at end of file diff --git a/src/ui/sharedModules/angularMaterial.module.ts b/src/ui/sharedModules/angularMaterial.module.ts index 9967e80d64279a7ef9e9b760a1c784593926c3b3..22ab6307004196d3b182682848014717adb67a8c 100644 --- a/src/ui/sharedModules/angularMaterial.module.ts +++ b/src/ui/sharedModules/angularMaterial.module.ts @@ -5,6 +5,7 @@ import { MatCardModule, MatTabsModule, MatTooltipModule, + MatSnackBarModule, MatBadgeModule, MatDividerModule, MatSelectModule, @@ -27,6 +28,7 @@ import { NgModule } from '@angular/core'; @NgModule({ imports: [ MatButtonModule, + MatSnackBarModule, MatCheckboxModule, MatSidenavModule, MatCardModule, @@ -47,6 +49,7 @@ import { NgModule } from '@angular/core'; exports: [ MatButtonModule, MatCheckboxModule, + MatSnackBarModule, MatSidenavModule, MatCardModule, MatTabsModule, diff --git a/src/util/directives/dragDrop.directive.ts b/src/util/directives/dragDrop.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..83895d5f0c4b81e472eb3e6c714697037986e109 --- /dev/null +++ b/src/util/directives/dragDrop.directive.ts @@ -0,0 +1,89 @@ +import { Directive, Input, Output, EventEmitter, HostListener, ElementRef, OnInit, OnDestroy, HostBinding } from "@angular/core"; +import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from "@angular/material"; +import { Observable, fromEvent, merge, Subscription, of, from } from "rxjs"; +import { map, scan, distinctUntilChanged, debounceTime, tap, switchMap, takeUntil } from "rxjs/operators"; + +@Directive({ + selector: '[drag-drop]' +}) + +export class DragDropDirective implements OnInit, OnDestroy{ + + @Input() + snackText: string + + @Output('drag-drop') + dragDropOnDrop: EventEmitter<File[]> = new EventEmitter() + + @HostBinding('style.transition') + transition = `opacity 300ms ease-in` + + @HostBinding('style.opacity') + opacity = null + + public snackbarRef: MatSnackBarRef<SimpleSnackBar> + + private dragover$: Observable<boolean> + + @HostListener('dragover', ['$event']) + ondragover(ev:DragEvent){ + ev.preventDefault() + } + + @HostListener('drop', ['$event']) + ondrop(ev:DragEvent) { + ev.preventDefault() + this.reset() + + this.dragDropOnDrop.emit(Array.from(ev.dataTransfer.files)) + } + + reset(){ + if (this.snackbarRef) { + this.snackbarRef.dismiss() + } + this.opacity = null + } + + private subscriptions: Subscription[] = [] + + ngOnInit(){ + this.subscriptions.push( + this.dragover$.pipe( + debounceTime(16) + ).subscribe(flag => { + if (flag) { + this.snackbarRef = this.snackBar.open(this.snackText || `Drop file(s) here.`) + this.opacity = 0.2 + } else { + this.reset() + } + }) + ) + } + + ngOnDestroy(){ + while(this.subscriptions.length > 0) { + this.subscriptions.pop().unsubscribe() + } + } + + constructor(private snackBar: MatSnackBar, private el:ElementRef){ + this.dragover$ = merge( + of(null), + fromEvent(this.el.nativeElement, 'drop') + ).pipe( + switchMap(() => merge( + fromEvent(this.el.nativeElement, 'dragenter').pipe( + map(() => 1) + ), + fromEvent(this.el.nativeElement, 'dragleave').pipe( + map(() => -1) + ) + ).pipe( + scan((acc, curr) => acc + curr, 0), + map(val => val > 0) + )) + ) + } +} \ No newline at end of file