diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index a1175088db8d56f6f0a879ef5c2e40b439db917e..9abbd42776480899103ff4b228616cccd1fd7f80 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -20,7 +20,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 { ToastService } from "src/services/toastService.service"; +import { LocalFileService } from "src/services/localFile.service"; /** * TODO @@ -85,6 +85,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[] @@ -109,9 +110,13 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { private modalService: BsModalService, private databrowserService: DatabrowserService, private dispatcher$: ActionsSubject, - private toastService: ToastService, - 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)), @@ -284,6 +289,9 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) ) + /** + * TODO deprecated + */ this.subscriptions.push( this.ngLayerNames$.pipe( concatMap(data => this.constantsService.loadExportNehubaPromise.then(data)) @@ -386,6 +394,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) } @@ -399,7 +411,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 @@ -425,6 +437,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)) @@ -450,12 +465,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 = [ @@ -465,17 +474,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 @@ -492,6 +502,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({ @@ -508,6 +521,26 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { }) } + /** + * TODO deprecated + */ + + toggleSidePanel(panelName:string){ + this.store.dispatch({ + type : TOGGLE_SIDE_PANEL, + focusedSidePanel :panelName + }) + } + + /** + * TODO deprecated + */ + panelAnimationEnd(){ + if( this.nehubaContainer && this.nehubaContainer.nehubaViewer && this.nehubaContainer.nehubaViewer.nehubaViewer ) { + this.nehubaContainer.nehubaViewer.nehubaViewer.redraw() + } + } + closeMenuWithSwipe(documentToSwipe: ElementRef) { if (documentToSwipe && documentToSwipe.nativeElement) { diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 429869cf77846acea64457a1b8cdbf52c32b4ad5..d77828f903cc1b0856653f37d146e698c21426e5 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -122,7 +122,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 8506cf25f696ab09fbe69dd4647c8d650b5dc6de..118d5123f19854425acaa5a8f65eec612c08a584 100644 --- a/src/atlasViewer/atlasViewer.urlService.service.ts +++ b/src/atlasViewer/atlasViewer.urlService.service.ts @@ -229,7 +229,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 : { @@ -326,7 +326,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 */ @@ -337,7 +341,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 1fb66d55c46ea77a249b850a7a349d681d4528f2..336718e137af62d3ec860a5c80cc6018876b6810 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -42,6 +42,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"; @NgModule({ imports : [ @@ -91,6 +93,7 @@ import { UseEffects } from "./services/effect/effect"; PluginFactoryDirective, FloatingMouseContextualContainerDirective, FixedMouseContextualContainerDirective, + DragDropDirective, /* pipes */ GetNamesPipe, @@ -113,6 +116,7 @@ import { UseEffects } from "./services/effect/effect"; ToastService, AtlasWorkerService, AuthService, + LocalFileService, /** * TODO 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 b63e2dfaa5a37e5caa567efebe4bf1c135e00a4b..707c71a085245706c04898b9b2935b34432b3b0a 100644 --- a/src/ui/sharedModules/angularMaterial.module.ts +++ b/src/ui/sharedModules/angularMaterial.module.ts @@ -4,12 +4,13 @@ import { MatSidenavModule, MatCardModule, MatTabsModule, - MatTooltipModule + MatTooltipModule, + MatSnackBarModule } from '@angular/material'; import { NgModule } from '@angular/core'; @NgModule({ - imports: [MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], - exports: [MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], + imports: [MatSnackBarModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], + exports: [MatSnackBarModule, MatButtonModule, MatCheckboxModule, MatSidenavModule, MatCardModule, MatTabsModule, MatTooltipModule], }) export class AngularMaterialModule { } \ No newline at end of file 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