diff --git a/common/constants.js b/common/constants.js index 5054d2ede406c717c9c36669017e7fee0755433f..b37adb9a66e77d60f340b0a8cb2247136ea2ae4e 100644 --- a/common/constants.js +++ b/common/constants.js @@ -61,8 +61,8 @@ // Annotations USER_ANNOTATION_LIST: 'user annotations footer', - USER_ANNOTATION_IMPORT: 'user annotations import', - USER_ANNOTATION_EXPORT: 'user annotations export', + USER_ANNOTATION_IMPORT: 'Import annotations', + USER_ANNOTATION_EXPORT: 'Export all of my annotations', USER_ANNOTATION_EXPORT_SINGLE: 'Export annotation', USER_ANNOTATION_HIDE: 'user annotations hide', USER_ANNOTATION_DELETE: 'Delete annotation', diff --git a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.style.css b/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.style.css index 8d0266fe77710f71d9e50865274cd336aaf8cecc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.style.css +++ b/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.style.css @@ -1,6 +0,0 @@ -.header-container -{ - padding: 16px; - margin: -16px!important; - padding-top: 6rem; -} diff --git a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html b/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html index 697ef4010d189ef662977b2c2716363b6d4e53fe..4438e03e4e2b0b055211ffc49d7ab06bcc08c514 100644 --- a/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html +++ b/src/atlasComponents/databrowserModule/preview/previewCard/previewCard.template.html @@ -1,5 +1,5 @@ <mat-card class="mat-elevation-z4"> - <div class="header-container bg-50-grey-20"> + <div class="sidenav-cover-header-container bg-50-grey-20"> <mat-card-title> {{ singleDsView?.name || file.name || filename }} </mat-card-title> diff --git a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css index ccdddfd5e46b96c4f33db7ef7e6370dd1bd59f02..9105967131e061d7d2959065d49a71d716ee398c 100644 --- a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css +++ b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.style.css @@ -2,10 +2,3 @@ { position: relative; } - -.header-container -{ - padding: 16px; - margin: -16px!important; - padding-top: 6rem; -} diff --git a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html index 455219a1825c31e207e94e6ea46a36774c6871ad..2acd816673e51556d4b66eae36f8b3078472b485 100644 --- a/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html +++ b/src/atlasComponents/databrowserModule/singleDataset/sideNavView/sDsSideNavView.template.html @@ -9,7 +9,7 @@ </button> <mat-card class="mat-elevation-z4"> - <div class="header-container bg-50-grey-20"> + <div class="sidenav-cover-header-container bg-50-grey-20"> <mat-card-title> <ng-content select="[region-of-interest]"></ng-content> <div *ngIf="!fetchFlag; else isLoadingTmpl"> diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css index ee2db754e2ec22f48db4e1353f55895dd8aeb13a..34d152c481193d92a7b78b2fc6c0901f955556f9 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.style.css @@ -20,10 +20,3 @@ mat-icon font-size: 95%; line-height: normal; } - -.header-container -{ - padding: 16px; - margin: -16px!important; - padding-top: 6rem; -} diff --git a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html index ac243dc5cf13882cadeafd73c0ddb4ce3700fc8b..8f7b7adcbf4304301b7010e8f02d33f88f1777bc 100644 --- a/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/atlasComponents/parcellationRegion/regionMenu/regionMenu.template.html @@ -2,7 +2,7 @@ <!-- rgbDarkmode must be checked for strict equality to true/false as if rgb is undefined, rgbDarkmode will be null/undefined which is falsy --> - <div class="header-container" + <div class="sidenav-cover-header-container" [ngClass]="{'darktheme': rgbDarkmode === true, 'lighttheme': rgbDarkmode === false}" [style.backgroundColor]="rgbString"> <mat-card-title> diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index fd16cedee7b3320b858d9f92cf98a63f00faa6f4..59275a07d0ace42729e7864dd4e9d8a66c668510 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -1,10 +1,17 @@ -import {Component} from "@angular/core"; -import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; +import {Component, ViewChild} from "@angular/core"; import {ARIA_LABELS} from "common/constants"; import { ModularUserAnnotationToolService } from "../tools/service"; -import { TExportFormats } from "../tools/type"; +import { IAnnotationGeometry, TExportFormats } from "../tools/type"; import { ComponentStore } from "src/viewerModule/componentStore"; +import { map, startWith, tap } from "rxjs/operators"; +import { Observable } from "rxjs"; +import { TZipFileConfig } from "src/zipFilesOutput/type"; +import { TFileInputEvent } from "src/getFileInput/type"; +import { FileInputDirective } from "src/getFileInput/getFileInput.directive"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { unzip } from "src/zipFilesOutput/zipFilesOutput.directive"; +const README = 'EXAMPLE OF READ ME TEXT' @Component({ selector: 'annotation-list', @@ -18,13 +25,40 @@ export class AnnotationList { public ARIA_LABELS = ARIA_LABELS + + @ViewChild(FileInputDirective) + fileInput: FileInputDirective + public managedAnnotations$ = this.annotSvc.managedAnnotations$ + + public manAnnExists$ = this.managedAnnotations$.pipe( + map(arr => !!arr && arr.length > 0), + startWith(false) + ) + + public filesExport$: Observable<TZipFileConfig[]> = this.managedAnnotations$.pipe( + startWith([] as IAnnotationGeometry[]), + map(manAnns => { + const readme = { + filename: 'README.md', + filecontent: README, + } + const annotationSands = manAnns.map(ann => { + return { + filename: `${ann.id}.sands.json`, + filecontent: JSON.stringify(ann.toSands(), null, 2), + } + }) + return [ readme, ...annotationSands ] + }) + ) constructor( private annotSvc: ModularUserAnnotationToolService, + private snackbar: MatSnackBar, cStore: ComponentStore<{ useFormat: TExportFormats }>, ) { cStore.setState({ - useFormat: 'json' + useFormat: 'sands' }) } @@ -32,4 +66,67 @@ export class AnnotationList { toggleManagedAnnotationVisibility(id: string) { this.annotSvc.toggleAnnotationVisibilityById(id) } + + private parseAndAddAnnotation(input: string) { + const json = JSON.parse(input) + const annotation = this.annotSvc.parseAnnotationObject(json) + this.annotSvc.importAnnotation(annotation) + } + + async handleImportEvent(ev: TFileInputEvent<'text' | 'file'>){ + try { + const clearFileInputAndInform = () => { + if (this.fileInput) { + this.fileInput.clear() + } + this.snackbar.open('Annotation imported successfully!', 'Dismiss', { + duration: 3000 + }) + } + + if (ev.type === 'text') { + const input = (ev as TFileInputEvent<'text'>).payload.input + /** + * parse as json, and go through the parsers + */ + this.parseAndAddAnnotation(input) + clearFileInputAndInform() + return + } + if (ev.type === 'file') { + const files = (ev as TFileInputEvent<'file'>).payload.files + if (files.length === 0) throw new Error(`Need at least one file.`) + if (files.length > 1) throw new Error(`Parsing multiple files are not yet supported`) + const file = files[0] + const isJson = /\.json$/.test(file.name) + const isZip = /\.zip$/.test(file.name) + if (isZip) { + const files = await unzip(file) + const sands = files.filter(f => /\.json$/.test(f.filename)) + for (const sand of sands) { + this.parseAndAddAnnotation(sand.filecontent) + } + clearFileInputAndInform() + } + if (isJson) { + const reader = new FileReader() + reader.onload = evt => { + const out = evt.target.result + this.parseAndAddAnnotation(out as string) + clearFileInputAndInform() + } + reader.onerror = e => { throw e } + reader.readAsText(file, 'utf-8') + } + /** + * check if zip or json + */ + return + } + } catch (e) { + this.snackbar.open(`Error importing: ${e.toString()}`, 'Dismiss', { + duration: 3000 + }) + } + } } diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index fcfea4ae4a53a64ad1299f16c7ecf584ef461770..4cd227d10dd4d9a5db7a2e9f6b91f8427c4da849 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -1,60 +1,92 @@ -<!-- header --> - -<div class="d-flex"> - <h2 class="mat-h2 mt-4 text-muted"> - Annotations - </h2> -</div> - -<mat-divider></mat-divider> - - -<!-- list of annotations --> -<ng-template [ngIf]="managedAnnotations$ | async" [ngIfElse]="placeholderTmpl" let-managedAnnotations> - - <mat-accordion *ngIf="managedAnnotations.length > 0; else placeholderTmpl" - [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_LIST" - class="h-100 d-flex flex-column overflow-auto"> - - <!-- expansion panel --> - <mat-expansion-panel *ngFor="let managedAnnotation of managedAnnotations" - hideToggle> - - <mat-expansion-panel-header [ngClass]="{'highlight': managedAnnotation.highlighted }"> - <mat-panel-title class="d-flex align-items-center"> - - <!-- toggle visibility --> - <button - mat-icon-button - iav-stop="click" - (click)="toggleManagedAnnotationVisibility(managedAnnotation.id)"> - <i [ngClass]="(hiddenAnnotations$ | async | annotationVisiblePipe : managedAnnotation) ? 'fa-eye' : 'fa-eye-slash'" class="fas"></i> - </button> - <span class="flex-shrink-1 flex-grow-1" [ngClass]="{ 'text-muted': !managedAnnotation.name }"> - {{ managedAnnotation | singleAnnotationNamePipe : managedAnnotation.name }} - </span> - <i class="flex-shrink-0 flex-grow-0" [ngClass]="managedAnnotation | singleannotationClsIconPipe"></i> - </mat-panel-title> - </mat-expansion-panel-header> - - <!-- single annotation edit body --> - <ng-template matExpansionPanelContent> - <single-annotation-unit - [single-annotation-unit-annotation]="managedAnnotation"> - </single-annotation-unit> +<mat-card class="mat-elevantion-z4 h-100"> + <div class="sidenav-cover-header-container bg-50-grey-20"> + + <!-- title --> + <mat-card-title> + My Annotations + </mat-card-title> + + <!-- actions --> + <mat-card-subtitle> + <!-- import --> + <ng-template #importMessageTmpl> + Please select a annotation file to import. </ng-template> - </mat-expansion-panel> - </mat-accordion> -</ng-template> - -<!-- place holder when no annotations exist --> -<ng-template #placeholderTmpl> - <mat-card> - <span> - No annotations visible yet. - </span> - <span> - Start by adding an annotion, or import an existing annotation. - </span> - </mat-card> -</ng-template> \ No newline at end of file + + <button mat-icon-button + (file-input-directive)="handleImportEvent($event)" + [file-input-directive-title]="ARIA_LABELS.USER_ANNOTATION_IMPORT" + [file-input-directive-text]="true" + [file-input-directive-file]="true" + [file-input-directive-message]="importMessageTmpl" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_IMPORT" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_IMPORT"> + <i class="fas fa-folder-open"></i> + </button> + + <!-- export --> + <button mat-icon-button + [zip-files-output]="filesExport$ | async" + zip-files-output-zip-filename="exported_annotations.zip" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT" + [disabled]="!(manAnnExists$ | async)"> + <i class="fas fa-file-export"></i> + </button> + </mat-card-subtitle> + </div> + + <!-- content --> + <mat-card-content class="mt-4 ml-15px-n mr-15px-n pb-4"> + <!-- list of annotations --> + <ng-template [ngIf]="managedAnnotations$ | async" [ngIfElse]="placeholderTmpl" let-managedAnnotations> + + <mat-accordion *ngIf="managedAnnotations.length > 0; else placeholderTmpl" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_LIST" + class="h-100 d-flex flex-column overflow-auto"> + + <!-- expansion panel --> + <mat-expansion-panel *ngFor="let managedAnnotation of managedAnnotations" + hideToggle> + + <mat-expansion-panel-header [ngClass]="{'highlight': managedAnnotation.highlighted }"> + <mat-panel-title class="d-flex align-items-center"> + + <!-- toggle visibility --> + <button + mat-icon-button + iav-stop="click" + (click)="toggleManagedAnnotationVisibility(managedAnnotation.id)"> + <i [ngClass]="(hiddenAnnotations$ | async | annotationVisiblePipe : managedAnnotation) ? 'fa-eye' : 'fa-eye-slash'" class="fas"></i> + </button> + <span class="flex-shrink-1 flex-grow-1" [ngClass]="{ 'text-muted': !managedAnnotation.name }"> + {{ managedAnnotation | singleAnnotationNamePipe : managedAnnotation.name }} + </span> + <i class="flex-shrink-0 flex-grow-0" [ngClass]="managedAnnotation | singleannotationClsIconPipe"></i> + </mat-panel-title> + </mat-expansion-panel-header> + + <!-- single annotation edit body --> + <ng-template matExpansionPanelContent> + <div class="d-flex"> + + <!-- spacer for inset single-annotation-unit --> + <div class="w-3em flex-grow-0 flex-shrink-0"></div> + + <single-annotation-unit [single-annotation-unit-annotation]="managedAnnotation" + class="flex-grow-1 flex-shrink-1"> + </single-annotation-unit> + </div> + </ng-template> + </mat-expansion-panel> + </mat-accordion> + </ng-template> + + <!-- place holder when no annotations exist --> + <ng-template #placeholderTmpl> + <div class="p-4 text-muted"> + <p>Start by adding an annotation, or import existing annotations.</p> + </div> + </ng-template> + </mat-card-content> +</mat-card> diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index ee03a1d8d1a611f3857198ae4f6faee3cb7794f1..025e66892e489c2bc1ec828d0be0196b02944c5d 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -1,15 +1,16 @@ -import { Component } from "@angular/core"; +import { Component, Inject, OnDestroy, Optional } from "@angular/core"; import { Store } from "@ngrx/store"; import { ModularUserAnnotationToolService } from "../tools/service"; import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper"; import { ARIA_LABELS } from 'common/constants' +import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; @Component({ selector: 'annotating-tools-panel', templateUrl: './annotationMode.template.html', styleUrls: ['./annotationMode.style.css'] }) -export class AnnotationMode { +export class AnnotationMode implements OnDestroy{ public ARIA_LABELS = ARIA_LABELS @@ -21,11 +22,20 @@ export class AnnotationMode { onClick: Function }[] = [] + private onDestroyCb: Function[] = [] + constructor( private store$: Store<any>, private modularToolSvc: ModularUserAnnotationToolService, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, ) { this.moduleAnnotationTypes = this.modularToolSvc.moduleAnnotationTypes + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + const stopClickProp = () => false + register(stopClickProp) + this.onDestroyCb.push(() => deregister(stopClickProp)) + } } exitAnnotationMode(){ @@ -36,7 +46,10 @@ export class AnnotationMode { ) } deselectTools(){ - console.log('deselect tools') this.modularToolSvc.deselectTools() } + + ngOnDestroy(){ + while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + } } diff --git a/src/atlasComponents/userAnnotations/annotationService.service.ts b/src/atlasComponents/userAnnotations/annotationService.service.ts deleted file mode 100644 index 814bb01353e6485a64e30a50b2c9e9c4c9fc486a..0000000000000000000000000000000000000000 --- a/src/atlasComponents/userAnnotations/annotationService.service.ts +++ /dev/null @@ -1,363 +0,0 @@ -import {Inject, Injectable, Optional} from "@angular/core"; -import {viewerStateSetViewerMode} from "src/services/state/viewerState/actions"; -import {getUuid} from "src/util/fn"; -import {Store} from "@ngrx/store"; -import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; -import {AnnotationType, GroupedAnnotation, ViewerAnnotation} from "src/atlasComponents/userAnnotations/annotationInterfaces"; - -const USER_ANNOTATION_LAYER_NAME = 'user_annotations' -const USER_ANNOTATION_STORE_KEY = `user_landmarks_demo_2` - -const USER_ANNOTATION_LAYER_SPEC = { - "type": "annotation", - "tool": "annotateBoundingBox", - "name": USER_ANNOTATION_LAYER_NAME, - "annotationColor": "#ffee00", - "annotations": [], -} - -@Injectable() -export class AnnotationService { - - // Annotations to display on viewer - public pureAnnotationsForViewer: ViewerAnnotation[] = [] - - // Grouped annotations for user - public groupedAnnotations: GroupedAnnotation[] = [] - - // Filtered annotations with converted voxed to mm - public finalAnnotationList: GroupedAnnotation[] = [] - - public addedLayer: any - public ellipsoidMinRadius = 0.5 - public annotationFilter: 'all' | 'current' = 'current' - - public selectedTemplate: {name, id} - public voxelSize: any[] = [] - public selectedAtlas: {name, id} - public hoverAnnotation: {id: string, partIndex: number} - - public annotationTypes: AnnotationType[] = [ - {name: 'Cursor', class: 'fas fa-mouse-pointer', type: 'move', action: 'none'}, - {name: 'Point', class: 'fas fa-circle', type: 'singleCoordinate', action: 'paint'}, - {name: 'Line', class: 'fas fa-slash', type: 'doubleCoordinate', action: 'paint'}, - {name: 'Polygon', class: 'fas fa-draw-polygon', type: 'polygon', action: 'paint'}, - // {name: 'Bounding box', class: 'far fa-square', type: 'doubleCoordinate', action: 'paint'}, - // {name: 'Ellipsoid', class: 'fas fa-bullseye', type: 'doubleCoordinate', action: 'paint'}, - {name: 'Remove', class: 'fas fa-trash', type: 'remove', action: 'remove'}, - ] - - private get viewer(){ - return this.injectedViewer || (window as any).viewer - } - - constructor(private store$: Store<any>, - @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer - ) {} - - public disable = () => { - this.store$.dispatch(viewerStateSetViewerMode({payload: null})) - } - - public loadAnnotationLayer() { - if (!this.viewer) { - throw new Error(`viewer is not initialised`) - } - - const layer = this.viewer.layerSpecification.getLayer( - USER_ANNOTATION_LAYER_NAME, - USER_ANNOTATION_LAYER_SPEC - ) - - this.addedLayer = this.viewer.layerManager.addManagedLayer(layer) - } - - loadAnnotationsOnInit() { - const annotationsString = window.localStorage.getItem(USER_ANNOTATION_STORE_KEY) - const annotationList = JSON.parse(annotationsString) - - if (annotationList && annotationList.length) { - this.pureAnnotationsForViewer = annotationList.filter(a => a.atlas.id === this.selectedAtlas.id) - - this.groupedAnnotations = this.pureAnnotationsForViewer.filter(a => a.type !== 'polygon') - this.addPolygonsToGroupedAnnotations(this.pureAnnotationsForViewer.filter(a => a.type === 'polygon')) - this.refreshFinalAnnotationList() - this.pureAnnotationsForViewer.filter(a => a.annotationVisible && a.template.id === this.selectedTemplate.id) - .forEach(a => { - this.addAnnotationOnViewer(a) - }) - } - } - - getRadii(a, b): number[] { - const returnArray: number[] = [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])] - .map(n => n === 0? this.ellipsoidMinRadius : n) - return returnArray - } - - saveAnnotation({id = null, - position1 = null, - position2 = null, - name = null, - description = null, - type = null, - circular = null, - atlas = null, - template = null, - } = {}, store = true, backup = false) { - let annotation = { - id: id || getUuid(), - annotationVisible: true, - description, - name, - position1, - position2, - circular, - template: template || this.selectedTemplate, - atlas: this.selectedAtlas, - type: type.toLowerCase() - } - - const foundIndex = this.pureAnnotationsForViewer.findIndex(x => x.id === annotation.id) - - if (foundIndex >= 0) { - annotation = { - ...this.pureAnnotationsForViewer[foundIndex], - ...annotation - } - } - - this.addAnnotationOnViewer(annotation) - - if (backup) { - const i = this.saveEditList.findIndex((e) => e.id === annotation.id) - if (i < 0) {this.saveEditList.push(annotation) - } else {this.saveEditList[i] = annotation} - } - - if (store) { - this.storeAnnotation(annotation) - } - } - public saveEditList = [] - - public storeBackup() { - if (this.saveEditList.length) { - if (this.saveEditList[0].type === 'polygon') { - this.addPolygonsToGroupedAnnotations(this.saveEditList) - } - this.saveEditList.forEach(a => this.storeAnnotation(a)) - this.saveEditList = [] - } - } - - generateNameByType(type) { - const pointAnnotationNumber = this.pureAnnotationsForViewer - .filter(a => a.name && a.name.startsWith(type) && (+a.name.split(type)[1])) - .map(a => +a.name.split(type)[1]) - - return pointAnnotationNumber && pointAnnotationNumber.length? - `${type}${Math.max(...pointAnnotationNumber) + 1}` : `${type}1` - - } - - storeAnnotation(annotation) { - // give names by type + number - // if (!annotation.name && annotation.type !== 'polygon') { - // annotation.name = this.generateNameByType(annotation.type) - // } - - const foundIndex = this.pureAnnotationsForViewer.findIndex(x => x.id === annotation.id) - - if (foundIndex >= 0) { - annotation = { - ...this.pureAnnotationsForViewer[foundIndex], - ...annotation - } - this.pureAnnotationsForViewer[foundIndex] = annotation - } else { - this.pureAnnotationsForViewer.push(annotation) - } - - if(annotation.type !== 'polygon') { - const foundIndex = this.groupedAnnotations.findIndex(x => x.id === annotation.id) - - if (foundIndex >= 0) { - this.groupedAnnotations[foundIndex] = annotation - } else { - this.groupedAnnotations.push(annotation) - } - this.refreshFinalAnnotationList() - } - - this.storeToLocalStorage() - } - - addAnnotationOnViewer(annotation) { - const annotationLayer = this.viewer.layerManager.getLayerByName(USER_ANNOTATION_LAYER_NAME).layer - const annotations = annotationLayer.localAnnotations.toJSON() - - annotations.push({ - description: annotation.description? annotation.description : '', - id: annotation.id, - point: annotation.type === 'point'? annotation.position1 : null, - pointA: annotation.type === 'line' || annotation.type === 'bounding box' || annotation.type === 'polygon'? - annotation.position1 : null, - pointB: annotation.type === 'line' || annotation.type === 'bounding box' || annotation.type === 'polygon'? - annotation.position2 : null, - center: annotation.type === 'ellipsoid'? - annotation.position1 : null, - radii: annotation.type === 'ellipsoid'? - annotation.position2 : null, - type: annotation.type === 'bounding box'? 'axis_aligned_bounding_box' - : annotation.type === 'polygon'? 'line' - : annotation.type.toUpperCase() - }) - - annotationLayer.localAnnotations.restoreState(annotations) - } - - - removeAnnotation(id) { - this.removeAnnotationFromViewer(id) - this.pureAnnotationsForViewer = this.pureAnnotationsForViewer.filter(a => a.id !== id) - this.groupedAnnotations = this.groupedAnnotations.filter(a => a.id !== id.split('_')[0]) - this.refreshFinalAnnotationList() - this.storeToLocalStorage() - } - - storeToLocalStorage() { - window.localStorage.setItem(USER_ANNOTATION_STORE_KEY, JSON.stringify(this.pureAnnotationsForViewer)) - } - - removeAnnotationFromViewer(id) { - const annotationLayer = this.viewer.layerManager.getLayerByName(USER_ANNOTATION_LAYER_NAME)?.layer - if (annotationLayer) { - let annotations = annotationLayer.localAnnotations.toJSON() - annotations = annotations.filter(a => a.id !== id) - annotationLayer.localAnnotations.restoreState(annotations) - } - } - - addPolygonsToGroupedAnnotations(annotations) { - let transformed = [...annotations] - - for (let i = 0; i<annotations.length; i++) { - - const annotationId = annotations[i].id.split('_') - if (!transformed.find(t => t.id === annotationId[0])) { - const polygonAnnotations = annotations.filter(a => a.id.split('_')[0] === annotationId[0] - && a.id.split('_')[1]) - // clear polygonAnnotations - .map(a => { - if (a.annotations) { - a.annotations = a.annotations.map(an => { - return { - id: an.id, - position1: an.position1, - position2: an.position2 - } - }) - } - return a - }) - - const polygonPositions = polygonAnnotations.map((a, index) => { - return (index+1) !== polygonAnnotations.length? { - position: a.position2, - lines: [ - {id: a.id, point: 2}, - {id: polygonAnnotations[index+1].id, point: 1} - ] - } : a.position2.join() !== polygonAnnotations[0].position1.join()? { - position: a.position2, - lines: [ - {id: a.id, point: 2} - ] - } : null - }).filter(a => !!a) - polygonPositions.unshift({ - position: polygonAnnotations[0].position1, - lines: polygonAnnotations[0].position1.join() === [...polygonAnnotations].pop().position2.join()? - [{id: polygonAnnotations[0].id, point: 1}, {id: [...polygonAnnotations].pop().id, point: 2}] - : [{id: polygonAnnotations[0].id, point: 1}] - }) - - transformed = transformed.filter(a => a.id.split('_')[0] !== annotationId[0]) - - if (annotations[i].name === null) { - annotations[i].name = this.generateNameByType(annotations[i].type) - } - - transformed.push({ - id: annotationId[0], - name: annotations[i].name, - description: annotations[i].description, - type: 'polygon', - annotations: polygonAnnotations, - positions: polygonPositions, - circular: polygonAnnotations[0].position1.join() === [...polygonAnnotations].pop().position2.join(), - annotationVisible: annotations[i].annotationVisible, - template: annotations[i].template, - atlas: this.selectedAtlas - }) - } - - } - - transformed.forEach(tr=> { - const foundIndex = this.groupedAnnotations.findIndex(x => x.id === tr.id) - - if (foundIndex >= 0) { - this.groupedAnnotations[foundIndex] = tr - } else { - this.groupedAnnotations.push(tr) - } - this.refreshFinalAnnotationList() - }) - } - - refreshFinalAnnotationList(filter = null) { - if (filter) {this.annotationFilter = filter} - this.finalAnnotationList = this.groupedAnnotations - // Filter all/current template - .filter(a => this.annotationFilter === 'all' || a.template.id === this.selectedTemplate.id) - // convert to MM - .map(a => { - if (a.positions) { - a.positions = a.positions.map(p => { - return { - ...p, - position: this.voxelToMM(p.position) - } - }) - } else { - a.position1 = this.voxelToMM(a.position1) - a.position2 = a.position2 && this.voxelToMM(a.position2) - } - - a.dimension = 'mm' - return a - }) - } - - voxelToMM(r: number[]): number[] { - return r.map((r, i) => parseFloat((+r*this.voxelSize[i]/1e6).toFixed(3))) - } - - mmToVoxel(mm: number[]): any[] { - return mm.map((m, i) => +m*1e6/this.voxelSize[i]) - } - - getVoxelFromSpace = (spaceId: string) => { - return IAV_VOXEL_SIZES_NM[spaceId] - } -} - -export const IAV_VOXEL_SIZES_NM = { - 'minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9': [25000, 25000, 25000], - 'minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8': [39062.5, 39062.5, 39062.5], - 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588': [21166.666015625, 20000, 21166.666015625], - 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992': [1000000, 1000000, 1000000,], - 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': [1000000, 1000000, 1000000] -} diff --git a/src/atlasComponents/userAnnotations/directives/exportAnnotation.directive.ts b/src/atlasComponents/userAnnotations/directives/exportAnnotation.directive.ts deleted file mode 100644 index 13935dea1a1fe6dd4fa9dbb9654be7043f329e18..0000000000000000000000000000000000000000 --- a/src/atlasComponents/userAnnotations/directives/exportAnnotation.directive.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {Directive, HostListener, Input} from "@angular/core"; -import * as JSZip from "jszip"; - -@Directive({ - selector: '[export-annotations]' -}) -export class ExportAnnotation { - - @Input('export-annotations') input: any - - @HostListener('click') - onClick() { - this.exportAnnotations(this.input.annotations, this.input.sands) - } - - getSandsObj(position, template) { - return { - coordinates: { - value: position.map(p => +p), - unit: 'mm' - }, - coordinateSpace: { - fullName: template.name, - versionIdentifier: template.id - } - } - } - - exportAnnotations(annotations, sands = false) { - - const zip = new JSZip() - const zipFileName = `annotation - ${annotations[0].atlas.name}.zip` - - if (sands) { - annotations.forEach(a => { - zip.folder(a.name) - if (a.positions) { - a.positions.forEach(p => { - zip.folder(a.name).file(`${p.position}.json`, JSON.stringify(this.getSandsObj(p.position, a.template))) - }) - } else { - zip.folder(a.name).file(`${a.position1}.json`, JSON.stringify(this.getSandsObj(a.position1, a.template))) - if (a.position2) zip.folder(a.name).file(`${a.position1}.json`, JSON.stringify(this.getSandsObj(a.position2, a.template))) - } - }) - } else { - annotations.forEach(a => { - const fileName = a.name.replace(/[\\/:*?"<>|]/g, "").trim() - zip.file(`${fileName}.json`, JSON.stringify(a)) - }) - } - - - zip.file("README.txt", - `The annotation has been extracted from the atlas: "${annotations.map(a => a.atlas.name).filter((v, i, a) => a.indexOf(v) === i).join()}" - and template(s): "${annotations.map(a => a.template.name).filter((v, i, a) => a.indexOf(v) === i).join()}"`) - zip.generateAsync({ - type: "base64" - }).then(content => { - const link = document.createElement('a') - link.href = 'data:application/zip;base64,' + content - link.download = zipFileName - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - }) - } - -} diff --git a/src/atlasComponents/userAnnotations/directives/importAnnotation.directive.ts b/src/atlasComponents/userAnnotations/directives/importAnnotation.directive.ts deleted file mode 100644 index 052e1e8805b774746f46dbc18438a96af74020f3..0000000000000000000000000000000000000000 --- a/src/atlasComponents/userAnnotations/directives/importAnnotation.directive.ts +++ /dev/null @@ -1,74 +0,0 @@ -import {Directive, HostListener, Input} from "@angular/core"; -import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; - -@Directive({ - selector: '[import-annotations]' -}) -export class ImportAnnotation { - - @Input('import-annotations') input: any - - constructor(private ans: AnnotationService) {} - - @HostListener('change', ['$event.target']) - onClick(target: any) { - if (target.files.length) { - this.importFile(target.files[0]) - } - } - - importFile(file) { - const sands = this.input.sands || null - - const fileReader = new FileReader() - fileReader.readAsText(file, "UTF-8") - fileReader.onload = () => { - const fileData = JSON.parse(fileReader.result.toString()) - - if (sands) { - if (!fileData.coordinates || !fileData.coordinates.value || fileData.coordinates.value.length !== 3 - || !fileData.coordinateSpace || !fileData.coordinateSpace.fullName || !fileData.coordinateSpace.versionIdentifier) { - return - } - const position1 = this.ans.mmToVoxel(fileData.coordinates.value) - this.ans.saveAnnotation({position1, - template: { - name: fileData.coordinateSpace.fullName, - id: fileData.coordinateSpace.versionIdentifier - }, - type: 'point'}) - } else { - const {id, name, description, type, - atlas, template, positions, annotations} = fileData - - if (!id || !(fileData.position1 || positions) || !type) { - return - } - - if (fileData.type !== 'polygon') { - const position1 = this.ans.mmToVoxel(fileData.position1.split(',').map(Number)) - const position2 = fileData.position2 && this.ans.mmToVoxel(fileData.position2.split(',').map(Number)) - - this.ans.saveAnnotation({position1, position2, - name, description, type, atlas, template - }) - } else if (annotations) { - annotations.forEach(a => { - this.ans.saveAnnotation({ - id: a.id, - name, description, - position1: a.position1, - position2: a.position2, - type: 'polygon'}) - }) - this.ans.groupedAnnotations.push(fileData) - this.ans.refreshFinalAnnotationList() - } - - } - } - fileReader.onerror = (error) => { - console.warn(error) - } - } -} diff --git a/src/atlasComponents/userAnnotations/directives/keyListener.directive.ts b/src/atlasComponents/userAnnotations/directives/keyListener.directive.ts deleted file mode 100644 index 5d304fadbf4a99784da2671405d1f11f070c1983..0000000000000000000000000000000000000000 --- a/src/atlasComponents/userAnnotations/directives/keyListener.directive.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Directive, ElementRef, HostListener} from "@angular/core"; - -@Directive({ - selector: '[annotation-list-key-listener]' -}) -export class KeyListener { - - constructor(private elementRef: ElementRef) {} - - @HostListener('keydown', ['$event']) - onKeyDown(e) { - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' || e === 'Escape' || e.key === 'Enter') { - e.stopPropagation() - this.elementRef.nativeElement.blur() - } - } - -} diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index e3c764001977ef4907627a681d9e39342cbc722d..46f329bcb327691ae1bcd22384a6b80555028c3b 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -6,16 +6,14 @@ import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import {AnnotationMode} from "src/atlasComponents/userAnnotations/annotationMode/annotationMode.component"; import {AnnotationList} from "src/atlasComponents/userAnnotations/annotationList/annotationList.component"; -import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; import { UserAnnotationToolModule } from "./tools/module"; import {AnnotationSwitch} from "src/atlasComponents/userAnnotations/directives/annotationSwitch.directive"; -import {ExportAnnotation} from "src/atlasComponents/userAnnotations/directives/exportAnnotation.directive"; -import {ImportAnnotation} from "src/atlasComponents/userAnnotations/directives/importAnnotation.directive"; -import {KeyListener} from "src/atlasComponents/userAnnotations/directives/keyListener.directive"; import {CoordinateInputTextPipe} from "src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe"; import { UtilModule } from "src/util"; import { SingleAnnotationClsIconPipe, SingleAnnotationNamePipe, SingleAnnotationUnit } from "./singleAnnotationUnit/singleAnnotationUnit.component"; import { AnnotationVisiblePipe } from "./annotationVisible.pipe"; +import { FileInputModule } from "src/getFileInput/module"; +import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; @NgModule({ imports: [ @@ -27,23 +25,19 @@ import { AnnotationVisiblePipe } from "./annotationVisible.pipe"; AngularMaterialModule, UserAnnotationToolModule, UtilModule, + FileInputModule, + ZipFilesOutputModule, ], declarations: [ AnnotationMode, AnnotationList, AnnotationSwitch, - ImportAnnotation, - ExportAnnotation, - KeyListener, CoordinateInputTextPipe, SingleAnnotationUnit, SingleAnnotationNamePipe, SingleAnnotationClsIconPipe, AnnotationVisiblePipe, ], - providers: [ - AnnotationService - ], exports: [ AnnotationMode, AnnotationList, diff --git a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html index 9dfe60e8dcf6249b1ac16053e87c5c9bbaea8632..e03b3068ebc2360cc9e6a08c537c73c750c4ece9 100644 --- a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html +++ b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.template.html @@ -32,7 +32,7 @@ </mat-form-field> </form> -<mat-divider class="m-2"></mat-divider> +<mat-divider class="m-2 d-block position-relative"></mat-divider> <ng-template #editAnnotationVCRef> </ng-template> diff --git a/src/atlasComponents/userAnnotations/tools/delete.ts b/src/atlasComponents/userAnnotations/tools/delete.ts index f3141900788a0dbe1a512bc610db5ab51fee2323..ecf04a10d3e7dabed9b5094fe5cb59443fde0c3a 100644 --- a/src/atlasComponents/userAnnotations/tools/delete.ts +++ b/src/atlasComponents/userAnnotations/tools/delete.ts @@ -4,7 +4,7 @@ import { filter, switchMapTo, takeUntil, withLatestFrom } from "rxjs/operators"; import { Point } from "./point"; import { AbsToolClass, IAnnotationEvents, IAnnotationGeometry, IAnnotationTools, TAnnotationEvent, TCallbackFunction, TNgAnnotationPoint, TToolType } from "./type"; -export class ToolDelete extends AbsToolClass implements IAnnotationTools, OnDestroy { +export class ToolDelete extends AbsToolClass<Point> implements IAnnotationTools, OnDestroy { public subs: Subscription[] = [] toolType: TToolType = 'deletion' @@ -15,6 +15,9 @@ export class ToolDelete extends AbsToolClass implements IAnnotationTools, OnDest return [] } + // eslint-disable-next-line @typescript-eslint/no-empty-function + addAnnotation(){} + // eslint-disable-next-line @typescript-eslint/no-empty-function removeAnnotation(){} diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index 5df04a845515df59cab72a4d96e9ada3885a281a..6dcf2e1c436530a658cb9930655ebfe04ec5cccc 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -19,7 +19,7 @@ import { getUuid } from "src/util/fn"; type TLineJsonSpec = { '@type': 'siibra-ex/annotation/line' - points: TPointJsonSpec[] + points: (TPointJsonSpec|Point)[] } & TBaseAnnotationGeomtrySpec export class Line extends IAnnotationGeometry{ @@ -39,7 +39,7 @@ export class Line extends IAnnotationGeometry{ ? p : new Point({ id: `${this.id}_${getUuid()}`, - "@type": 'siibra-ex/annotatoin/point', + "@type": 'siibra-ex/annotation/point', space: this.space, ...p }) @@ -111,10 +111,54 @@ export class Line extends IAnnotationGeometry{ return new Line(json) } + static fromSANDS(json: TSandsLine): Line{ + const { + "@id": id, + "@type": type, + coordinateSpace, + coordinatesFrom, + coordinatesTo + } = json + if (type !== 'tmp/line') throw new Error(`cannot parse line from sands`) + const fromPt = coordinatesFrom.map(c => { + if (c.unit["@id"] !== 'id.link/mm') throw new Error(`Cannot parse unit`) + return c.value * 1e6 + }) + const toPoint = coordinatesTo.map(c => { + if (c.unit["@id"] !== 'id.link/mm') throw new Error(`Cannot parse unit`) + return c.value * 1e6 + }) + const line = new Line({ + id, + "@type": "siibra-ex/annotation/line", + points: [ + new Point({ + "@type": 'siibra-ex/annotation/point', + x: fromPt[0], + y: fromPt[1], + z: fromPt[2], + space: coordinateSpace + }), + new Point({ + '@type': "siibra-ex/annotation/point", + x: toPoint[0], + y: toPoint[1], + z: toPoint[2], + space: coordinateSpace + }) + ], + space: coordinateSpace + }) + return line + } + constructor(spec?: TLineJsonSpec){ super(spec) const { points = [] } = spec || {} - this.points = points.map(Point.fromJSON) + this.points = points.map(p => { + if (p instanceof Point) return p + return Point.fromJSON(p) + }) } public translate(x: number, y: number, z: number) { @@ -131,7 +175,7 @@ export class Line extends IAnnotationGeometry{ export const LINE_ICON_CLASS = 'fas fa-slash' -export class ToolLine extends AbsToolClass implements IAnnotationTools, OnDestroy { +export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, OnDestroy { static PREVIEW_ID='tool_line_preview' public name = 'Line' public toolType: TToolType = 'drawing' @@ -277,6 +321,14 @@ export class ToolLine extends AbsToolClass implements IAnnotationTools, OnDestro this.subs.forEach(s => s.unsubscribe()) } + addAnnotation(line: Line) { + const idx = this.managedAnnotations.findIndex(ann => ann.id === line.id) + if (idx >= 0) throw new Error(`Line annotation has already been added`) + this.managedAnnotations.push(line) + this.managedAnnotations$.next(this.managedAnnotations) + this.forceRefreshAnnotations$.next(null) + } + removeAnnotation(id: string){ const idx = this.managedAnnotations.findIndex(ann => ann.id === id) if (idx < 0) { diff --git a/src/atlasComponents/userAnnotations/tools/line/line.style.css b/src/atlasComponents/userAnnotations/tools/line/line.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3cf6a85959217c0d3217a794e9dd0a1a7cbc460a 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.style.css +++ b/src/atlasComponents/userAnnotations/tools/line/line.style.css @@ -0,0 +1,5 @@ +:host +{ + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/atlasComponents/userAnnotations/tools/line/line.template.html b/src/atlasComponents/userAnnotations/tools/line/line.template.html index ad1246b245e8acaf7799853eaa40439e5197f471..7b05ff79b4780f4d6858ec4ec2b6e51683fbd727 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.template.html +++ b/src/atlasComponents/userAnnotations/tools/line/line.template.html @@ -12,7 +12,7 @@ </mat-chip> </mat-chip-list> -<mat-divider class="m-2"></mat-divider> +<mat-divider class="m-2 d-block position-relative"></mat-divider> <!-- actions --> @@ -39,15 +39,6 @@ <div class="iv-custom-comp card text" iav-stop="click"> - <div class="d-flex"> - <button *ngFor="let format of viableFormats" - (click)="setFormat(format)" - mat-flat-button - [color]="useFormat === format ? 'primary' : ''"> - {{ format }} - </button> - </div> - <div class="iv-custom-comp text"> <mat-form-field> <mat-label> diff --git a/src/atlasComponents/userAnnotations/tools/module.ts b/src/atlasComponents/userAnnotations/tools/module.ts index 3576af312ea3851571ebc27f1ddac12947497252..cfc25b90f3e6262f80c777bb5942930372346c57 100644 --- a/src/atlasComponents/userAnnotations/tools/module.ts +++ b/src/atlasComponents/userAnnotations/tools/module.ts @@ -9,6 +9,12 @@ import { PolyUpdateCmp } from "./poly/poly.component"; import { ModularUserAnnotationToolService } from "./service"; import { ToFormattedStringPipe } from "./toFormattedString.pipe"; import { ANNOTATION_EVENT_INJ_TOKEN } from "./type"; +import {Line, ToolLine} from "src/atlasComponents/userAnnotations/tools/line"; + +import { Point, ToolPoint } from "./point"; +import { ToolSelect } from "./select"; +import { ToolDelete } from "./delete"; +import { Polygon, ToolPolygon } from "./poly"; @NgModule({ imports: [ @@ -38,6 +44,32 @@ import { ANNOTATION_EVENT_INJ_TOKEN } from "./type"; export class UserAnnotationToolModule { - // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor(_svc: ModularUserAnnotationToolService){} + constructor(svc: ModularUserAnnotationToolService){ + const selTool = svc.registerTool({ + toolCls: ToolSelect + }) + svc.defaultTool = selTool + + svc.registerTool({ + toolCls: ToolPoint, + target: Point, + editCmp: PointUpdateCmp, + }) + + svc.registerTool({ + toolCls: ToolLine, + target: Line, + editCmp: LineUpdateCmp, + }) + + svc.registerTool({ + toolCls: ToolPolygon, + target: Polygon, + editCmp: PolyUpdateCmp, + }) + + svc.registerTool({ + toolCls: ToolDelete + }) + } } diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 0b7d51d5e604dd3fbbf9a432436ea813bccd1884..233413921fd8b1317f098db99e02dd00023a98b1 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -7,7 +7,7 @@ export type TPointJsonSpec = { x: number y: number z: number - '@type': 'siibra-ex/annotatoin/point' + '@type': 'siibra-ex/annotation/point' } & TBaseAnnotationGeomtrySpec export class Point extends IAnnotationGeometry { @@ -31,7 +31,7 @@ export class Point extends IAnnotationGeometry { } toJSON(): TPointJsonSpec{ const { id, x, y, z, space, name, desc } = this - return { id, x, y, z, space, name, desc, '@type': 'siibra-ex/annotatoin/point' } + return { id, x, y, z, space, name, desc, '@type': 'siibra-ex/annotation/point' } } getNgAnnotationIds(){ @@ -48,6 +48,33 @@ export class Point extends IAnnotationGeometry { return new Point(json) } + static fromSANDS(sands: TSandsPoint): Point { + const { + "@id": id, + "@type": type, + coordinateSpace, + coordinates + } = sands + if (type === 'https://openminds.ebrains.eu/sands/CoordinatePoint') { + const parsedCoordinate = coordinates.map(coord => { + const { value, unit } = coord + if (unit["@id"] !== 'id.link/mm') throw new Error(`Unit does not parse`) + return value * 1e6 + }) + const point = new Point({ + id, + space: coordinateSpace, + "@type": 'siibra-ex/annotation/point', + x: parsedCoordinate[0], + y: parsedCoordinate[1], + z: parsedCoordinate[2], + }) + return point + } + + throw new Error(`cannot parse sands for points, @type mismatch`) + } + toString(){ return `${(this.x / 1e6).toFixed(2)}mm, ${(this.y / 1e6).toFixed(2)}mm, ${(this.z / 1e6).toFixed(2)}mm` } @@ -74,7 +101,7 @@ export class Point extends IAnnotationGeometry { export const POINT_ICON_CLASS='fas fa-circle' -export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestroy { +export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools, OnDestroy { static PREVIEW_ID='tool_point_preview' public name = 'Point' public toolType: TToolType = 'drawing' @@ -113,7 +140,7 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr const pt = new Point({ x, y, z, space, - '@type': 'siibra-ex/annotatoin/point' + '@type': 'siibra-ex/annotation/point' }) const { id } = pt pt.remove = () => this.removeAnnotation(id) @@ -135,7 +162,7 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr */ this.dragHoveredAnnotationsDelta$.subscribe(ev => { const { ann, deltaX, deltaY, deltaZ } = ev - const { pickedAnnotationId, pickedOffset } = ann.detail + const { pickedAnnotationId } = ann.detail const foundAnn = this.managedAnnotations.find(ann => ann.id === pickedAnnotationId) if (foundAnn) { foundAnn.translate(deltaX, deltaY, deltaZ) @@ -148,7 +175,6 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr merge( this.forceRefresh$, ).subscribe(() => { - console.log('emit... here?') let out: INgAnnotationTypes['point'][] = [] for (const managedAnn of this.managedAnnotations) { if (managedAnn.space['@id'] === this.space['@id']) { @@ -160,15 +186,27 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr ) } + addAnnotation(point: Point){ + const found = this.managedAnnotations.find(p => p.id === point.id) + if (found) throw new Error(`Point annotation already added`) + this.managedAnnotations.push(point) + this.managedAnnotations$.next(this.managedAnnotations) + this.forceRefresh$.next(null) + } + /** * @description remove managed annotation via id * @param id id of annotation */ removeAnnotation(id: string) { const idx = this.managedAnnotations.findIndex(ann => id === ann.id) - + if (idx < 0){ + throw new Error(`cannot find point idx ${idx}`) + return + } this.managedAnnotations.splice(idx, 1) this.managedAnnotations$.next(this.managedAnnotations) + this.forceRefresh$.next(null) } onMouseMoveRenderPreview(pos: [number, number, number]) { diff --git a/src/atlasComponents/userAnnotations/tools/point/point.template.html b/src/atlasComponents/userAnnotations/tools/point/point.template.html index 1de69ecc26e2e7837758892f0c2d165ab87ab3bc..2eeccd57ca5afa1f41e15e20be332b9f6c90a274 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.template.html +++ b/src/atlasComponents/userAnnotations/tools/point/point.template.html @@ -11,7 +11,7 @@ </mat-chip> </mat-chip-list> -<mat-divider class="m-2"></mat-divider> +<mat-divider class="m-2 d-block position-relative"></mat-divider> <!-- actions --> @@ -38,15 +38,6 @@ <div class="iv-custom-comp card text" iav-stop="click"> - <div class="d-flex"> - <button *ngFor="let format of viableFormats" - (click)="setFormat(format)" - mat-flat-button - [color]="useFormat === format ? 'primary' : ''"> - {{ format }} - </button> - </div> - <div class="iv-custom-comp text"> <mat-form-field> <mat-label> diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index ccd48017600d98b202028d69cb7b9f4670556ff6..8594e7213f2345698dd078dc02f0151ff6741a53 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -6,7 +6,7 @@ import { filter, switchMapTo, takeUntil, withLatestFrom } from "rxjs/operators"; import { getUuid } from "src/util/fn"; type TPolyJsonSpec = { - points: TPointJsonSpec[] + points: (TPointJsonSpec|Point)[] edges: [number, number][] '@type': 'siibra-ex/annotation/polyline' } & TBaseAnnotationGeomtrySpec @@ -49,7 +49,7 @@ export class Polygon extends IAnnotationGeometry{ : new Point({ id: `${this.id}_${getUuid()}`, space: this.space, - '@type': 'siibra-ex/annotatoin/point', + '@type': 'siibra-ex/annotation/point', ...p }) @@ -99,14 +99,11 @@ export class Polygon extends IAnnotationGeometry{ coordinateSpace: { '@id': this.space["@id"], }, - coordinatesPairs: this.edges.map(([ idx1, idx2 ]) => { - const { x: x1, y: y1, z: z1 } = this.points[idx1] - const { x: x2, y: y2, z: z2 } = this.points[idx2] - return [ - [getCoord(x1), getCoord(y1), getCoord(z1)], - [getCoord(x2), getCoord(y2), getCoord(z2)] - ] - }) + coordinates: this.points.map(p => { + const { x, y, z } = p + return [getCoord(x/1e6), getCoord(y/1e6), getCoord(z/1e6)] + }), + closed: true } } @@ -161,10 +158,54 @@ export class Polygon extends IAnnotationGeometry{ return new Polygon(json) } + static fromSANDS(sands: TSandsPolyLine): Polygon { + const { + "@id": id, + "@type": type, + coordinateSpace, + coordinates + } = sands + if (type === 'tmp/poly') { + const points: Point[] = [] + const edges: [number, number][] = [] + for (const coordinate of coordinates) { + const parsedValue = coordinate.map(c => { + if (c.unit["@id"] !== 'id.link/mm') throw new Error(`Unit does not parse`) + return c.value * 1e6 + }) + const p = new Point({ + space: coordinateSpace, + x: parsedValue[0], + y: parsedValue[1], + z: parsedValue[2], + "@type": "siibra-ex/annotation/point" + }) + const newIdx = points.push(p) + if (newIdx > 1) { + edges.push([ newIdx - 2, newIdx - 1 ]) + } + } + + const poly = new Polygon({ + id, + "@type": 'siibra-ex/annotation/polyline', + space: coordinateSpace, + points, + edges + }) + return poly + } + + throw new Error(`cannot import sands`) + } + constructor(spec?: TPolyJsonSpec){ super(spec) const { points = [], edges = [] } = spec || {} - this.points = points.map(Point.fromJSON) + this.points = points.map(p => { + if (p instanceof Point) return p + return Point.fromJSON(p) + }) this.edges = edges } @@ -182,7 +223,7 @@ export class Polygon extends IAnnotationGeometry{ export const POLY_ICON_CLASS = 'fas fa-draw-polygon' -export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDestroy { +export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationTools, OnDestroy { static PREVIEW_ID='tool_poly_preview' public name = 'polygon' @@ -292,7 +333,6 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes const { id } = this.selectedPoly this.selectedPoly.remove = () => this.removeAnnotation(id) this.managedAnnotations.push(this.selectedPoly) - this.managedAnnotations$.next(this.managedAnnotations) } else { if (ann.detail) { @@ -318,6 +358,11 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes this.lastAddedPoint ) this.lastAddedPoint = addedPoint + + /** + * always emit new annotation onclick + */ + this.managedAnnotations$.next(this.managedAnnotations) }), /** @@ -372,6 +417,14 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes ) } + addAnnotation(poly: Polygon){ + const idx = this.managedAnnotations.findIndex(ann => ann.id === poly.id) + if (idx >= 0) throw new Error(`Polygon already added.`) + this.managedAnnotations.push(poly) + this.managedAnnotations$.next(this.managedAnnotations) + this.forceRefreshAnnotations$.next(null) + } + removeAnnotation(id: string) { const idx = this.managedAnnotations.findIndex(ann => ann.id === id) if (idx < 0) { diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html index 9340263fb28b89db4df5750b8b46170f85875822..07fa279961fce101fff2075de6ce0eb5588d022f 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html @@ -12,7 +12,7 @@ </mat-chip> </mat-chip-list> -<mat-divider class="m-2"></mat-divider> +<mat-divider class="m-2 d-block position-relative"></mat-divider> <!-- actions --> @@ -39,15 +39,6 @@ <div class="iv-custom-comp card text" iav-stop="click"> - <div class="d-flex"> - <button *ngFor="let format of viableFormats" - (click)="setFormat(format)" - mat-flat-button - [color]="useFormat === format ? 'primary' : ''"> - {{ format }} - </button> - </div> - <div class="iv-custom-comp text"> <mat-form-field> <mat-label> diff --git a/src/atlasComponents/userAnnotations/tools/select.ts b/src/atlasComponents/userAnnotations/tools/select.ts index ea2bea2b5b54ee8fcd3e81a582b497c82650bfab..2c10e4fe0a46c725a77666d5f621d3101c82a3b0 100644 --- a/src/atlasComponents/userAnnotations/tools/select.ts +++ b/src/atlasComponents/userAnnotations/tools/select.ts @@ -4,7 +4,7 @@ import { filter } from 'rxjs/operators' import { Point } from "./point"; import { AbsToolClass, IAnnotationEvents, IAnnotationGeometry, IAnnotationTools, TAnnotationEvent, TCallbackFunction, TNgAnnotationPoint, TToolType } from "./type"; -export class ToolSelect extends AbsToolClass implements IAnnotationTools, OnDestroy { +export class ToolSelect extends AbsToolClass<Point> implements IAnnotationTools, OnDestroy { public subs: Subscription[] = [] toolType: TToolType = 'selecting' @@ -15,6 +15,9 @@ export class ToolSelect extends AbsToolClass implements IAnnotationTools, OnDest return [] } + // eslint-disable-next-line @typescript-eslint/no-empty-function + addAnnotation(){} + // eslint-disable-next-line @typescript-eslint/no-empty-function removeAnnotation(){} diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index 9cc72aa5ad507cda981c4a9ada6fda416ecebab4..d80d959f8a5da6134f14343892949be7a6ac8da1 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -7,16 +7,12 @@ import { map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators"; import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; -import { Polygon, ToolPolygon } from "./poly"; -import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TExportFormats, TCallbackFunction } from "./type"; +import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TExportFormats, TCallbackFunction, TSandsPolyLine, TSandsPoint, TSandsLine } from "./type"; import { switchMapWaitFor } from "src/util/fn"; -import {Line, ToolLine} from "src/atlasComponents/userAnnotations/tools/line"; -import { PolyUpdateCmp } from './poly/poly.component' -import { Point, ToolPoint } from "./point"; -import { PointUpdateCmp } from "./point/point.component"; -import { LineUpdateCmp } from "./line/line.component"; -import { ToolSelect } from "./select"; -import { ToolDelete } from "./delete"; +import { Polygon } from "./poly"; +import { Line } from "./line"; +import { Point } from "./point"; + const IAV_VOXEL_SIZES_NM = { 'minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9': [25000, 25000, 25000], @@ -96,7 +92,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private registeredTools: { name: string iconClass: string - toolInsance: AbsToolClass + toolInstance: AbsToolClass<any> target?: ClassInterface<IAnnotationGeometry> editCmp?: ClassInterface<any> onDestoryCallBack: () => void @@ -125,13 +121,13 @@ export class ModularUserAnnotationToolService implements OnDestroy{ * editCmp?: ClassInterface<any> * }} arg */ - private registerTool<T extends AbsToolClass>(arg: { + public registerTool<T extends AbsToolClass<any>>(arg: { toolCls: ClassInterface<T> target?: ClassInterface<IAnnotationGeometry> editCmp?: ClassInterface<any> - }): AbsToolClass{ + }): AbsToolClass<any>{ const { toolCls: Cls, target, editCmp } = arg - const newTool = new Cls(this.annotnEvSubj, arg => this.handleToolCallback(arg)) as AbsToolClass & { ngOnDestroy?: Function } + const newTool = new Cls(this.annotnEvSubj, arg => this.handleToolCallback(arg)) as T & { ngOnDestroy?: Function } const { name, iconClass, onMouseMoveRenderPreview } = newTool this.moduleAnnotationTypes.push({ @@ -178,7 +174,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ iconClass, target, editCmp, - toolInsance: newTool, + toolInstance: newTool, onDestoryCallBack: () => { newTool.ngOnDestroy && newTool.ngOnDestroy() this.managedAnnotationsStream$.next({ @@ -214,33 +210,6 @@ export class ModularUserAnnotationToolService implements OnDestroy{ @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, ){ - const selTool = this.registerTool({ - toolCls: ToolSelect - }) - this.defaultTool = selTool - - this.registerTool({ - toolCls: ToolPoint, - target: Point, - editCmp: PointUpdateCmp, - }) - - this.registerTool({ - toolCls: ToolLine, - target: Line, - editCmp: LineUpdateCmp, - }) - - this.registerTool({ - toolCls: ToolPolygon, - target: Polygon, - editCmp: PolyUpdateCmp, - }) - - this.registerTool({ - toolCls: ToolDelete - }) - /** * listen to mouse event on nehubaViewer, and emit as TAnnotationEvent */ @@ -369,9 +338,9 @@ export class ModularUserAnnotationToolService implements OnDestroy{ console.warn(`cannot find tool ${selectedToolName}`) return } - const { toolInsance } = selectedTool - const previewNgAnnotation = toolInsance.onMouseMoveRenderPreview - ? toolInsance.onMouseMoveRenderPreview([ngMouseEvent.x, ngMouseEvent.y, ngMouseEvent.z]) + const { toolInstance } = selectedTool + const previewNgAnnotation = toolInstance.onMouseMoveRenderPreview + ? toolInstance.onMouseMoveRenderPreview([ngMouseEvent.x, ngMouseEvent.y, ngMouseEvent.z]) : [] if (this.previewNgAnnIds.length !== previewNgAnnotation.length) { @@ -548,7 +517,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ } } - private defaultTool: AbsToolClass + public defaultTool: AbsToolClass<any> public deselectTools(){ this.activeToolName = null @@ -560,6 +529,29 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) } + parseAnnotationObject(json: TSandsPolyLine | TSandsPoint | TSandsLine): IAnnotationGeometry{ + if (json['@type'] === 'tmp/poly') { + return Polygon.fromSANDS(json) + } + if (json['@type'] === 'tmp/line') { + return Line.fromSANDS(json) + } + if (json['@type'] === 'https://openminds.ebrains.eu/sands/CoordinatePoint') { + return Point.fromSANDS(json) + } + throw new Error(`cannot parse annotation object`) + } + + importAnnotation(annotationObj: IAnnotationGeometry){ + for (const tool of this.registeredTools) { + const { toolInstance, target } = tool + if (!!target && annotationObj instanceof target) { + toolInstance.addAnnotation(annotationObj) + return + } + } + } + ngOnDestroy(){ while(this.subscription.length > 0) this.subscription.pop().unsubscribe() } diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index e8be6598144c0d30b90a4f883bd81f254d67d6d2..3f1ccf1f1294a0c5d855c577c185f6b6b2d11f7d 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -8,13 +8,14 @@ import { getUuid } from "src/util/fn" * TODO perhaps split into drawing subclass/utility subclass */ -export abstract class AbsToolClass { +export abstract class AbsToolClass<T extends IAnnotationGeometry> { public abstract name: string public abstract iconClass: string + public abstract addAnnotation(annotation: T): void public abstract removeAnnotation(id: string): void - public abstract managedAnnotations$: Observable<IAnnotationGeometry[]> + public abstract managedAnnotations$: Observable<T[]> abstract subs: Subscription[] protected space: TBaseAnnotationGeomtrySpec['space'] @@ -202,7 +203,8 @@ type TSandsQValue = { type TSandsCoord = [TSandsQValue, TSandsQValue] | [TSandsQValue, TSandsQValue, TSandsQValue] export type TSandsPolyLine = { - coordinatesPairs: [TSandsCoord, TSandsCoord][] + coordinates: TSandsCoord[] + closed: boolean coordinateSpace: { '@id': string } diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 7fd34675da7020d4ca8230a6965a9c6cf6113b19..dbe633e67022d391826067b9d7d0004e627548bf 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -87,7 +87,7 @@ export class AtlasViewerAPIServices implements OnDestroy{ private s: Subscription[] = [] - private onMouseClick(ev: any) { + private onMouseClick(ev: any): boolean { const { rs, spec } = this.getNextUserRegionSelectHandler() || {} if (!!rs) { @@ -116,10 +116,11 @@ export class AtlasViewerAPIServices implements OnDestroy{ mousePositionReal = floatArr && Array.from(floatArr).map((val: number) => val / 1e6) }) } - return rs({ + rs({ type: spec.type, payload: mousePositionReal }) + return false } /** @@ -129,10 +130,11 @@ export class AtlasViewerAPIServices implements OnDestroy{ if (!!moSegments && Array.isArray(moSegments) && moSegments.length > 0) { this.popUserRegionSelectHandler() - return rs({ + rs({ type: spec.type, payload: moSegments }) + return false } } } else { @@ -142,7 +144,8 @@ export class AtlasViewerAPIServices implements OnDestroy{ */ if (!!moSegments && Array.isArray(moSegments) && moSegments.length > 0) { this.popUserRegionSelectHandler() - return rs(moSegments[0]) + rs(moSegments[0]) + return false } } } diff --git a/src/util/directives/dragDrop.directive.ts b/src/dragDropFile/dragDrop.directive.ts similarity index 93% rename from src/util/directives/dragDrop.directive.ts rename to src/dragDropFile/dragDrop.directive.ts index 4f2689b7bfa41878098fa1c6d87c15c45c20e7be..0427836c4a369ddf3fd7ced09358639d206cdcf1 100644 --- a/src/util/directives/dragDrop.directive.ts +++ b/src/dragDropFile/dragDrop.directive.ts @@ -4,15 +4,16 @@ import { debounceTime, map, scan, switchMap } from "rxjs/operators"; import {MatSnackBar, MatSnackBarRef, SimpleSnackBar} from "@angular/material/snack-bar"; @Directive({ - selector: '[drag-drop]', + selector: '[drag-drop-file]', + exportAs: 'dragDropFile' }) -export class DragDropDirective implements OnInit, OnDestroy { +export class DragDropFileDirective implements OnInit, OnDestroy { @Input() public snackText: string - @Output('drag-drop') + @Output('drag-drop-file') public dragDropOnDrop: EventEmitter<File[]> = new EventEmitter() @HostBinding('style.transition') diff --git a/src/dragDropFile/module.ts b/src/dragDropFile/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3b77d44dc37541f55f62beed2ab213af43c4d77 --- /dev/null +++ b/src/dragDropFile/module.ts @@ -0,0 +1,17 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { DragDropFileDirective } from "./dragDrop.directive"; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + DragDropFileDirective + ], + exports: [ + DragDropFileDirective + ] +}) + +export class DragDropFileModule{} \ No newline at end of file diff --git a/src/getFileInput/fileInputModal/fileInputModal.component.ts b/src/getFileInput/fileInputModal/fileInputModal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b402c272261e01cf13047a0d9c3c65776ada7850 --- /dev/null +++ b/src/getFileInput/fileInputModal/fileInputModal.component.ts @@ -0,0 +1,119 @@ +import { Component, ElementRef, EventEmitter, Inject, Input, Optional, TemplateRef, ViewChild } from "@angular/core"; +import { MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { IFileInputConfig, TFileInputEvent } from "../type"; + +const FILEINPUT_DEFAULT_LABEL = 'File input' + +@Component({ + selector: 'file-input-modal', + templateUrl: './fileInputModal.template.html', + styleUrls: [ + './fileInputModal.style.css' + ] +}) + +export class FileInputModal implements IFileInputConfig{ + + @Input('file-input-directive-title') + title = 'Import' + + @Input('file-input-directive-text') + allowText = false + + @Input('file-input-directive-file') + allowFile = true + + @Input('file-input-directive-message') + messageTmpl: TemplateRef<any> + + @ViewChild('fileInput', { read: ElementRef }) + private fileInputEl: ElementRef<HTMLInputElement> + + constructor( + @Optional() @Inject(MAT_DIALOG_DATA) data: IFileInputConfig + ){ + if (data) { + const { allowFile, allowText, messageTmpl, title } = data + this.allowFile = allowFile + this.allowText = allowText + this.messageTmpl = messageTmpl + this.title = title || this.title + } + } + + public hasInput = false + + private _textInput = '' + set textInput(val: string) { + this._textInput = val + this.checkImportable() + } + get textInput(){ + return this._textInput + } + + public fileInputLabel: string = FILEINPUT_DEFAULT_LABEL + public hasFileInput = false + + private _fileInput: File + set fileInput(val: File){ + this._fileInput = val + this.hasFileInput = !!this.fileInput + this.fileInputLabel = this.hasFileInput + ? this._fileInput.name + : FILEINPUT_DEFAULT_LABEL + + this.checkImportable() + } + get fileInput(){ + return this._fileInput + } + handleFileInputChange(ev: InputEvent){ + const target = ev.target as HTMLInputElement + this.fileInput = target.files[0] + } + + handleFileDrop(files: File[]){ + this.fileInput = files[0] + } + + public importable = false + checkImportable(){ + if (this._textInput.length > 0) { + this.importable = true + return + } + if (this.hasFileInput){ + this.importable = true + return + } + this.importable = false + } + + clear(){ + this.textInput = '' + this.fileInput = null + } + + public evtEmitter = new EventEmitter<TFileInputEvent<'text' | 'file'>>() + + runImport(){ + if (this._textInput !== '') { + this.evtEmitter.emit({ + type: 'text', + payload: { + input: this._textInput + } + }) + return + } + if (this.hasFileInput) { + const files = [this.fileInput] + this.evtEmitter.emit({ + type: 'file', + payload: { files } + }) + return + } + } +} \ No newline at end of file diff --git a/src/getFileInput/fileInputModal/fileInputModal.style.css b/src/getFileInput/fileInputModal/fileInputModal.style.css new file mode 100644 index 0000000000000000000000000000000000000000..3326ccd216c00e08bf82bdf311be32f86cefae46 --- /dev/null +++ b/src/getFileInput/fileInputModal/fileInputModal.style.css @@ -0,0 +1,22 @@ +.file-input-label-container +{ + box-sizing: border-box; + padding: 1rem; +} + +.file-input-label +{ + display: flex; + align-items: center; + justify-content: center; + border-radius: 1rem; + border: 2px dashed rgba(128,128,128,0.4); + opacity: 0.7; + transition: opacity 200ms ease-in-out; +} + +.file-input-label:hover +{ + cursor: pointer; + opacity: 1.0; +} \ No newline at end of file diff --git a/src/getFileInput/fileInputModal/fileInputModal.template.html b/src/getFileInput/fileInputModal/fileInputModal.template.html new file mode 100644 index 0000000000000000000000000000000000000000..4fdc365d5169260206961acf60c0e117f29ce582 --- /dev/null +++ b/src/getFileInput/fileInputModal/fileInputModal.template.html @@ -0,0 +1,55 @@ +<h2 mat-dialog-title> + {{ title }} +</h2> +<mat-dialog-content> + <ng-template [ngIf]="messageTmpl" [ngIfElse]="defaultMessageTmpl" [ngTemplateOutlet]="messageTmpl"> + </ng-template> + + <ng-template #defaultMessageTmpl> + Please select a file to import. + </ng-template> + + <div class="w-100 d-flex"> + + <!-- text input --> + <mat-form-field *ngIf="allowText" + class="flex-grow-1 flex-shrink-1 w-0"> + <mat-label> + Text Input + </mat-label> + <textarea matInput + [(ngModel)]="textInput" + rows="5" + placeholder="Text Input"></textarea> + </mat-form-field> + + <!-- file-input --> + <div class="file-input-label-container flex-grow-1 flex-shrink-1 w-0 position-relative" + (drag-drop-file)="handleFileDrop($event)"> + <label for="file-input" class="file-input-label w-100 h-100"> + <i [ngClass]="hasFileInput ? 'fa-file' : 'fa-folder-open'" class="fas"></i> + <span class="ml-2"> + {{ fileInputLabel }} + </span> + </label> + <input (change)="handleFileInputChange($event)" + type="file" + class="position-absolute left-0 top-0 w-0 h-0 invisible" + name="file-input" + id="file-input" + #fileInput> + </div> + </div> +</mat-dialog-content> + +<mat-dialog-actions align="end"> + <button mat-raised-button + (click)="runImport()" + [disabled]="!importable" + color="primary"> + Import + </button> + <button mat-button mat-dialog-close> + Close + </button> +</mat-dialog-actions> diff --git a/src/getFileInput/getFileInput.directive.ts b/src/getFileInput/getFileInput.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1158cdac9de4598957793bec83f2c90761edb2f --- /dev/null +++ b/src/getFileInput/getFileInput.directive.ts @@ -0,0 +1,68 @@ +import { Directive, EventEmitter, HostListener, Input, Output, TemplateRef } from "@angular/core"; +import { MatDialog, MatDialogRef } from "@angular/material/dialog"; +import { FileInputModal } from "./fileInputModal/fileInputModal.component"; +import { IFileInputConfig, TFileInputEvent } from "./type"; + +@Directive({ + selector: '[file-input-directive]', + exportAs: 'fileInputDirective' +}) + +export class FileInputDirective implements IFileInputConfig{ + + @Input('file-input-directive-title') + title = 'Import' + + @Input('file-input-directive-text') + allowText = false + + @Input('file-input-directive-file') + allowFile = true + + @Input('file-input-directive-message') + messageTmpl: TemplateRef<any> + + @Output('file-input-directive') + evtEmitter = new EventEmitter<TFileInputEvent<'text' | 'file'>>() + + private dialogRef: MatDialogRef<FileInputModal> + + @HostListener('click') + handleClick(){ + const { title, allowText, allowFile, messageTmpl } = this + this.dialogRef = this.dialog.open(FileInputModal, { + width: '65vw', + data: { + allowText, + allowFile, + title, + messageTmpl, + } + }) + const evSub = this.dialogRef.componentInstance.evtEmitter.subscribe( + (ev: TFileInputEvent<"text" | "file">) => this.evtEmitter.emit(ev) + ) + this.dialogRef.afterClosed().subscribe(() => { + this.dialogRef = null + evSub.unsubscribe() + }) + } + + constructor( + private dialog: MatDialog + ){ + + } + + clear(){ + if (this.dialogRef) { + this.dialogRef.componentInstance.clear() + } + } + + dismiss(){ + if (this.dialogRef) { + this.dialogRef.close() + } + } +} \ No newline at end of file diff --git a/src/getFileInput/index.ts b/src/getFileInput/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/getFileInput/module.ts b/src/getFileInput/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..77610abdb95357e856f20d9d59a13ee98ee515ed --- /dev/null +++ b/src/getFileInput/module.ts @@ -0,0 +1,30 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FileInputDirective } from "./getFileInput.directive"; +import { MatDialogModule } from '@angular/material/dialog'; +import { FileInputModal } from "./fileInputModal/fileInputModal.component"; +import { FormsModule } from "@angular/forms"; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { DragDropFileModule } from "src/dragDropFile/module"; + +@NgModule({ + imports: [ + CommonModule, + MatDialogModule, + FormsModule, + MatInputModule, + MatButtonModule, + DragDropFileModule, + ], + declarations: [ + FileInputDirective, + FileInputModal, + ], + exports: [ + FileInputDirective, + FileInputModal, + ], +}) + +export class FileInputModule{} \ No newline at end of file diff --git a/src/getFileInput/type.ts b/src/getFileInput/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b95f06956c8dc9b9dc64798f9a5df1e8d7e3faa --- /dev/null +++ b/src/getFileInput/type.ts @@ -0,0 +1,18 @@ +import { TemplateRef } from "@angular/core"; + +export interface IFileInputConfig { + title: string + allowText: boolean + allowFile: boolean + messageTmpl?: TemplateRef<any> +} + +export type TFileInput = { + text: { input: string } + file: { files: File[] } +} + +export type TFileInputEvent<Evt extends keyof TFileInput> = { + type: Evt + payload: TFileInput[Evt] +} \ No newline at end of file diff --git a/src/main.module.ts b/src/main.module.ts index 90c7080be8366e2e1e3ecc7f4784c77f6321fbb1..3d47b99f720a655bc63cc94f3f11d08caec175ab 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -29,7 +29,6 @@ import { UIService } from "./services/uiService.service"; import { DatabrowserModule, OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, DataBrowserFeatureStore, GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, DatabrowserService } from "src/atlasComponents/databrowserModule"; import { ViewerStateControllerUseEffect } from "src/state"; import { DockedContainerDirective } from "./util/directives/dockedContainer.directive"; -import { DragDropDirective } from "./util/directives/dragDrop.directive"; import { FloatingContainerDirective } from "./util/directives/floatingContainer.directive"; import { FloatingMouseContextualContainerDirective } from "./util/directives/floatingMouseContextualContainer.directive"; import { NewViewerDisctinctViewToLayer } from "./util/pipes/newViewerDistinctViewToLayer.pipe"; @@ -134,7 +133,6 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> { DockedContainerDirective, FloatingContainerDirective, FloatingMouseContextualContainerDirective, - DragDropDirective, /* pipes */ GetNamesPipe, diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index e98f09941fcc99b2a721c892167f806adf681137..d55624cb78aa1a54e114edcea645ede26c4c56a0 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -843,3 +843,10 @@ mat-list.sm mat-list-item { color: inherit!important; } + +.sidenav-cover-header-container +{ + padding: 16px; + margin: -16px!important; + padding-top: 6rem; +} \ No newline at end of file diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 5ae2b7d4c0e22f7713fabe456b59f735c34ab882..23e6e4b16bad4e0a8ffce83c9fba2c2c536816c0 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -11,12 +11,12 @@ [hasBackdrop]="false"> <mat-drawer #annotationDrawer [mode]="'push'" + [autoFocus]="false" [disableClose]="true" - class="box-shadow-none border-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> + class="p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> <annotation-list></annotation-list> </mat-drawer> - <mat-drawer-content class="visible position-relative pe-none"> <iav-layout-fourcorners> diff --git a/src/zipFilesOutput/module.ts b/src/zipFilesOutput/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..59e58e22483dfc9a2509fbee93ea5331ed60f8f2 --- /dev/null +++ b/src/zipFilesOutput/module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ZipFilesOutput } from "./zipFilesOutput.directive"; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + ZipFilesOutput + ], + exports: [ + ZipFilesOutput + ] +}) + +export class ZipFilesOutputModule{} + +export { TZipFileConfig } from './type' \ No newline at end of file diff --git a/src/zipFilesOutput/type.ts b/src/zipFilesOutput/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5e986b36848cd8cea94cad3e4d54dbd17fa495e --- /dev/null +++ b/src/zipFilesOutput/type.ts @@ -0,0 +1,5 @@ +export type TZipFileConfig = { + filename: string + filecontent: string + base64?: boolean +} \ No newline at end of file diff --git a/src/zipFilesOutput/zipFilesOutput.directive.ts b/src/zipFilesOutput/zipFilesOutput.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcf70bddfaf8e3e84ab97f887ae93ae8e95fcfc6 --- /dev/null +++ b/src/zipFilesOutput/zipFilesOutput.directive.ts @@ -0,0 +1,55 @@ +import { Directive, HostListener, Inject, Input } from "@angular/core"; +import { TZipFileConfig } from "./type"; +import * as JSZip from "jszip"; +import { DOCUMENT } from "@angular/common"; + +@Directive({ + selector: '[zip-files-output]', + exportAs: 'zipFilesOutput' +}) + +export class ZipFilesOutput { + @Input('zip-files-output') + zipFiles: TZipFileConfig[] = [] + + @Input('zip-files-output-zip-filename') + zipFilename = 'archive.zip' + + @HostListener('click') + async onClick(){ + const zip = new JSZip() + for (const zipFile of this.zipFiles) { + const { filecontent, filename, base64 } = zipFile + zip.file(filename, filecontent, { base64 }) + } + const blob = await zip.generateAsync({ type: 'blob' }) + const anchor = this.doc.createElement('a') + anchor.href = URL.createObjectURL(blob) + anchor.download = this.zipFilename + + this.doc.body.appendChild(anchor) + anchor.click() + this.doc.body.removeChild(anchor) + URL.revokeObjectURL(anchor.href) + } + constructor( + @Inject(DOCUMENT) private doc: Document + ){ + + } +} + +export async function unzip(file: File): Promise<TZipFileConfig[]>{ + const zip = new JSZip() + const loadedAsync = await zip.loadAsync(file) + + const out: TZipFileConfig[] = [] + for (const filename in loadedAsync.files) { + const filecontent = await loadedAsync.files[filename].async('string') + out.push({ + filename, + filecontent + }) + } + return out +}