diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..aefd5ef3457d3909d03676498875246d2bb714de --- /dev/null +++ b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts @@ -0,0 +1,297 @@ +import { + AfterViewInit, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnDestroy, + OnInit, + Optional, + Output +} from "@angular/core"; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {animate, style, transition, trigger} from "@angular/animations"; +import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; +import {Subscription} from "rxjs"; +import {filter, map} from "rxjs/operators"; +import {select, Store} from "@ngrx/store"; +import {viewerStateSelectedTemplateSelector} from "src/services/state/viewerState/selectors"; + +@Component({ + selector: 'edit-annotation', + templateUrl: './editAnnotation.template.html', + styleUrls: ['./editAnnotation.style.css'], + animations: [ + trigger( + 'enterAnimation', [ + transition(':enter', [ + style({transform: 'translateY(100%)', opacity: 0}), + animate('100ms', style({transform: 'translateY(0)', opacity: 1})) + ]), + // transition(':leave', [ + // style({transform: 'translateY(0)', opacity: 1}), + // animate('100ms', style({transform: 'translateY(100%)', opacity: 0})) + // ]) + ] + ) + ], +}) +export class EditAnnotationComponent implements OnInit, OnDestroy { + + @Input() editingAnnotation: any + @Input() showOnFocus = false + @Input() cursorOut = false + + @Output() saveAnnotation: EventEmitter<any> = new EventEmitter() + @Output() selectingOutput: EventEmitter<any> = new EventEmitter() + @Output() editingMode: EventEmitter<any> = new EventEmitter() + + public selecting: string + public position1Selected = false + public position2Selected = false + + public showFull = false + public annotationForm: FormGroup + public loading = false + public mousePos + + public selectedTemplate: string + public ellipsoidMinRadius = 0.2 + + // public voxelSize + + public subscriptions: Subscription[] = [] + + public annotationTypes = [{name: 'Point', class: 'fas fa-circle'}, + {name: 'Line', class: 'fas fa-slash', twoCoordinates: true}, + {name: 'Bounding box', class: 'far fa-square', twoCoordinates: true}, + {name: 'Ellipsoid', class: 'fas fa-bullseye', twoCoordinates: true}] + public selectedType: any + + get nehubaViewer() { + return (window as any).nehubaViewer + } + get interactiveViewer() { + return (window as any).interactiveViewer + } + private get viewer(){ + return this.injectedViewer || (window as any).viewer + } + + constructor(private formBuilder: FormBuilder, + private changeDetectionRef: ChangeDetectorRef, + private store: Store<any>, + @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer) { + this.annotationForm = this.formBuilder.group({ + id: [{value: null, disabled: true}], + position1: [{value: '', disabled: this.loading}], + position2: [{value: '', disabled: this.loading}], + name: [{value: '', disabled: this.loading}, { + validators: [Validators.maxLength(200)] + }], + description: [{value: '', disabled: this.loading}, { + validators: [Validators.maxLength(1000)] + }], + templateName: [{value: ''}], + type: [{value: 'point'}], + annotationVisible: [true] + }) + } + + ngOnInit() { + this.selectType(this.annotationTypes[0]) + + this.subscriptions.push( + this.nehubaViewer.mousePosition.inVoxels + .subscribe(floatArr => { + // this.mousePos = floatArr && Array.from(floatArr).map((val: number) => val / 1e6) + this.mousePos = floatArr && floatArr + + if (this.selecting === 'position1' && this.mousePos) { + this.annotationForm.controls.position1.setValue(this.mousePos.join()) + } else if (this.selecting === 'position2' && this.mousePos) { + if (this.annotationForm.controls.type.value === 'ellipsoid') { + this.annotationForm.controls.position2.setValue([ + this.getRadii(this.annotationForm.controls.position1.value.split(',').map(n => +n), this.mousePos), + ].join()) + } else { + this.annotationForm.controls.position2.setValue(this.mousePos.join()) + } + + if (this.annotationForm.controls.position1.value + && this.selectedType.twoCoordinates + && this.annotationForm.controls.position2.value) { + this.setAnnotation() + } + } + }), + + this.interactiveViewer.viewerHandle.mouseEvent.pipe( + filter((e: any) => e.eventName === 'click') + ).subscribe(() => { + if (this.selecting === 'position1' && this.annotationForm.controls.position1.value) { + this.selectPosition1() + if (this.selectedType.twoCoordinates) { + this.changeSelectingPoint('position2') + } else { + this.changeSelectingPoint('') + } + this.changeDetectionRef.detectChanges() + } else if (this.selecting === 'position2' && this.mousePos) { + this.selectPosition2() + } + }), + this.store.pipe( + select(viewerStateSelectedTemplateSelector) + ).subscribe(tmpl => { + this.annotationForm.controls.templateName.setValue(tmpl.name) + this.selectedTemplate = tmpl.name + }) + ) + + // this.voxelSize = this.nehubaViewer.config.dataset.initialNgState.navigation.pose.position.voxelSize + } + + changeSelectingPoint(selecting) { + this.selecting = selecting + this.selectingOutput.emit(selecting) + } + + selectPosition1() { + this.position1Selected = true + if (!this.selectedType.twoCoordinates) { + this.changeSelectingPoint('') + this.setAnnotation() + } + + } + + selectPosition2() { + this.position2Selected = true + this.changeSelectingPoint('') + this.changeDetectionRef.detectChanges() + + if (this.position1Selected) { + this.changeSelectingPoint('') + this.setAnnotation() + } + } + + position1CursorOut() { + if (this.annotationForm.controls.position1.value && !this.cursorOut) { + this.selectPosition1() + } + } + position2CursorOut() { + if (this.annotationForm.controls.position2.value && !this.cursorOut) { + this.selectPosition2() + } + } + + selectType(type) { + this.selectedType = type + this.annotationForm.controls.type.setValue(type.name.toLowerCase()) + this.annotationForm.controls.position1.setValue('') + this.annotationForm.controls.position2.setValue('') + if (!this.showOnFocus || this.showFull) this.changeSelectingPoint('position1') + this.position1Selected = false + this.position2Selected = false + } + + focusInName() { + if (this.showOnFocus) { + if (!this.showFull) { + this.changeSelectingPoint('position1') + } + this.showFull = true + this.editingMode.emit(this.showFull) + } + } + + setAnnotation() { + const annotationLayer = this.viewer.layerManager.getLayerByName('user_annotations').layer + const annotations = annotationLayer.localAnnotations.toJSON() + + // ToDo Still some error with the logic + // const position1Voxel = this.annotationForm.controls.position1.value.split(',') + // .map((r, i) => r/this.voxelSize[i]) + // const position2Voxel = this.annotationForm.controls.position2.value.split(',') + // .map((r, i) => r/this.voxelSize[i]) + + const position1Voxel = this.annotationForm.controls.position1.value.split(',') + const position2Voxel = this.annotationForm.controls.position2.value.split(',') + + annotations.push({ + description: this.annotationForm.controls.description.value? this.annotationForm.controls.description.value : '', + id: 'adding', + point: this.annotationForm.controls.type.value === 'point'? this.annotationForm.controls.position1.value.split(',') : null, + pointA: this.annotationForm.controls.type.value === 'line' || this.annotationForm.controls.type.value === 'bounding box'? + position1Voxel : null, + pointB: this.annotationForm.controls.type.value === 'line' || this.annotationForm.controls.type.value === 'bounding box'? + position2Voxel : null, + center: this.annotationForm.controls.type.value === 'ellipsoid'? + position1Voxel : null, + radii: this.annotationForm.controls.type.value === 'ellipsoid'? + position2Voxel : null, + type: this.annotationForm.controls.type.value !== 'bounding box'? + this.annotationForm.controls.type.value.toUpperCase() : 'axis_aligned_bounding_box' + }) + + annotationLayer.localAnnotations.restoreState(annotations) + } + + getRadii(a, b) { + const returnArray = [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 + } + + removeLoadingAnnotation() { + const annotationLayer = this.viewer.layerManager.getLayerByName('user_annotations').layer + const annotations = annotationLayer.localAnnotations.toJSON() + annotationLayer.localAnnotations.restoreState(annotations.filter(a => a.id !== 'adding')) + } + + submitForm() { + if (this.annotationForm.valid) { + // this.annotationForm.controls.annotationVisible.setValue('true') + this.saveAnnotation.emit(this.annotationForm.value) + this.cancelEditing() + } + } + + cancelEditing() { + if (this.showOnFocus) { + this.showFull = false + this.editingMode.emit(this.showFull) + } + this.resetForm() + } + + resetForm() { + this.annotationForm.reset() + this.annotationForm.markAsPristine() + this.annotationForm.markAsUntouched() + + // Set form defaults + this.annotationForm.controls.annotationVisible.setValue(true) + this.annotationForm.controls.templateName.setValue(this.selectedTemplate) + this.annotationForm.controls.templateName.setValue(this.selectedTemplate) + + this.position1Selected = false + this.position1Selected = false + this.selectType(this.annotationTypes[0]) + this.removeLoadingAnnotation() + this.changeSelectingPoint('') + + Object.keys(this.annotationForm.controls).forEach(key => { + this.annotationForm.get(key).setErrors(null) + }) + } + + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()) + } + +} diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css new file mode 100644 index 0000000000000000000000000000000000000000..fa79116cedefbfe88f92b7e1e2c7069ccc2f03dc --- /dev/null +++ b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css @@ -0,0 +1,7 @@ +.selectedType { + +} + +.short-input { + max-width: 200px; +} diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html new file mode 100644 index 0000000000000000000000000000000000000000..e31169200e644ac88df4f22f23e1d04b01fcae22 --- /dev/null +++ b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html @@ -0,0 +1,86 @@ +<form class="annotation-form d-flex flex-column align-items-center" + autocomplete="off" + [formGroup]="annotationForm" + (keydown.enter)="!annotationForm.invalid? submitForm() : null" + (ngSubmit)="submitForm()"> + + <mat-form-field *ngIf="!(showFull && cursorOut && selecting)" + [ngClass]="!showOnFocus || showFull? 'w-100' : 'short-input'"> + <mat-label>{{!showOnFocus || showFull ? 'name' : '+ Annotate'}}</mat-label> + <input (focusin)="focusInName()" + name="name" + formControlName="name" + matInput> + </mat-form-field> + + <div *ngIf="!showOnFocus || showFull" + class="w-100" + [@enterAnimation]> + + <div class="w-100 d-flex justify-content-center"> + <div class="d-flex flex-column align-items-center"> + <small *ngIf="!(showFull && cursorOut && selecting)">Annotation type</small> + <div> + <button *ngFor="let type of annotationTypes" + mat-icon-button + type="button" + [matTooltip]="type.name" + [color]="type.name === selectedType.name? 'primary' : null" + (click)="selectType(type)"> + <i [ngClass]="type.class"></i> + </button> + </div> + </div> + </div> + + <div class="d-flex w-100"> + <div class="d-flex flex-column align-items-center w-100"> + <mat-form-field class="w-100 annotation-editing-body" + [ngStyle]="{border: cursorOut && selecting === 'position1'? '2px solid' : null}"> + <mat-label>position {{selectedType.twoCoordinates && ' 1'}} in mm (curr. voxel)</mat-label> + <div> + <input type="text" name="position1" + placeholder="0,0,0" + class="pr-4" + formControlName="position1" matInput + (focusin)="position1Selected = false; changeSelectingPoint('position1')" + (focusout)="position1CursorOut()"> + <i *ngIf="cursorOut && selecting === 'position1'" class="fas fa-edit" style="margin-left: -20px;"></i> + <i *ngIf="position1Selected && !(cursorOut && selecting === 'position1')" class="fas fa-check-circle" style="margin-left: -20px;"></i> + </div> + </mat-form-field> + <small *ngIf="cursorOut && selecting === 'position1'" style="margin-top: -20px;">selecting</small> + </div> + + <div class="d-flex flex-column align-items-center w-100" *ngIf="selectedType.twoCoordinates"> + <mat-form-field class="w-100 annotation-editing-body" + [ngStyle]="{border: cursorOut && selecting === 'position2'? '2px solid' : null}"> + <mat-label>position 2 in mm (curr. voxel)</mat-label> + <input type="text" name="position2" + class="pr-4" + placeholder="0,0,0" + formControlName="position2" matInput + (focusin)="position2Selected = false; changeSelectingPoint('position2')" + (focusout)="position2CursorOut()"> + <i *ngIf="cursorOut && selecting === 'position2'" class="fas fa-edit" style="margin-left: -20px;"></i> + <i *ngIf="position2Selected && !(cursorOut && selecting === 'position2')" class="fas fa-check-circle" style="margin-left: -20px;"></i> + </mat-form-field> + <small *ngIf="cursorOut && selecting === 'position2'" style="margin-top: -20px;">selecting</small> + </div> + </div> + + + <mat-form-field class="w-100 annotation-editing-body"> + <mat-label>description</mat-label> + <textarea [matTextareaAutosize]="true" + [matAutosizeMinRows]="1" + [matAutosizeMaxRows]="5" + name="description" + formControlName="description" matInput></textarea> + </mat-form-field> + <div class="w-100 d-flex justify-content-end"> + <button type="button" (click)="cancelEditing()" mat-button>Cancel</button> + <button type="submit" mat-raised-button color="primary">Save</button> + </div> + </div> +</form> diff --git a/src/atlasComponents/userAnnotations/index.ts b/src/atlasComponents/userAnnotations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e75162e0b75d94c90974aa916492d2fc5033f4f --- /dev/null +++ b/src/atlasComponents/userAnnotations/index.ts @@ -0,0 +1,2 @@ +export { UserAnnotationsComponent } from "./userAnnotationsCmp/userAnnotationsCmp.components"; +export { UserAnnotationsModule } from "./module"; diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..942cab632a45e25780bd56daf77a7c129b979b12 --- /dev/null +++ b/src/atlasComponents/userAnnotations/module.ts @@ -0,0 +1,28 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {DatabrowserModule} from "src/atlasComponents/databrowserModule"; +import {AngularMaterialModule} from "src/ui/sharedModules/angularMaterial.module"; +import {UserAnnotationsComponent} from "src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {EditAnnotationComponent} from "src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; + +@NgModule({ + imports: [ + CommonModule, + DatabrowserModule, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule, + AngularMaterialModule, + ], + declarations: [ + UserAnnotationsComponent, + EditAnnotationComponent, + ], + exports: [ + UserAnnotationsComponent, + ] +}) + +export class UserAnnotationsModule{} diff --git a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e41f4ab1e5cbcfb61c96b57af53bf674569199e --- /dev/null +++ b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts @@ -0,0 +1,173 @@ +import {AfterViewInit, Component, EventEmitter, Inject, OnDestroy, OnInit, Optional, Output} from "@angular/core"; +import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; +import {Store} from "@ngrx/store"; + +@Component({ + selector: 'user-annotations', + templateUrl: './userAnnotationsCmp.template.html', + styleUrls: ['./userAnnotationsCmp.style.css'] +}) +export class UserAnnotationsComponent implements OnInit, OnDestroy { + + public userAnnotationLayerName = 'user_annotations' + public landmarkFilter: 'all' | 'current' = 'all' + public cursorOut = false + public selecting: string + public editingMode = false + public minimized = false + public nameInLocalStorage = 'user_landmarks_demo_1' + + public hovering = -1 + public expanded = -1 + + public annotations = [] + + @Output() close: EventEmitter<any> = new EventEmitter() + + constructor(@Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer, + private store: Store<any>) { + } + + private get viewer(){ + return this.injectedViewer || (window as any).viewer + } + + ngOnDestroy(): void { + this.viewer?.layerManager.removeManagedLayer( + this.viewer.layerManager.getLayerByName('user_annotations')) + } + + ngOnInit(): void { + this.loadAnnotationLayer() + + if (window.localStorage.getItem(this.nameInLocalStorage) && window.localStorage.getItem(this.nameInLocalStorage).length) { + const annotationsString = window.localStorage.getItem(this.nameInLocalStorage) + this.annotations = JSON.parse(annotationsString) + this.annotations.filter(a => a.annotationVisible).forEach(a => this.addAnnotationOnViewer(a)) + } + } + + public loadAnnotationLayer() { + return Object.keys(this.annotationLayerObj) + .filter(key => + /* if the layer exists, it will not be loaded */ + !this.viewer?.layerManager.getLayerByName(key)) + .map(key => { + this.viewer?.layerManager.addManagedLayer( + this.viewer.layerSpecification.getLayer(key, this.annotationLayerObj[key])) + return this.annotationLayerObj[key] + }) + } + + saveAnnotation(annotation) { + if (!annotation.id) { + annotation.id = this.randomId + } + + const foundIndex = this.annotations.findIndex(x => x.id === annotation.id) + if (foundIndex >= 0) { + this.annotations[foundIndex] = annotation + } else { + this.annotations.push(annotation) + } + + if (annotation.annotationVisible) { + this.addAnnotationOnViewer(annotation) + } + this.storeToLocalStorage() + } + + addAnnotationOnViewer(annotation) { + const annotationLayer = this.viewer.layerManager.getLayerByName('user_annotations').layer + const annotations = annotationLayer.localAnnotations.toJSON() + + // ToDo Still some error with the logic + // const position1Voxel = this.annotationForm.controls.position1.value.split(',') + // .map((r, i) => r/this.voxelSize[i]) + // const position2Voxel = this.annotationForm.controls.position2.value.split(',') + // .map((r, i) => r/this.voxelSize[i]) + + const position1Voxel = annotation.position1.split(',') + const position2Voxel = annotation.position2.split(',') + + annotations.push({ + description: annotation.description? annotation.description : '', + id: annotation.id, + point: annotation.type === 'point'? annotation.position1.split(',') : null, + pointA: annotation.type === 'line' || annotation.type === 'bounding box'? + position1Voxel : null, + pointB: annotation.type === 'line' || annotation.type === 'bounding box'? + position2Voxel : null, + center: annotation.type === 'ellipsoid'? + position1Voxel : null, + radii: annotation.type === 'ellipsoid'? + position2Voxel : null, + type: annotation.type !== 'bounding box'? + annotation.type.toUpperCase() : 'axis_aligned_bounding_box' + }) + + annotationLayer.localAnnotations.restoreState(annotations) + } + + toggleAnnotationView(i) { + if (this.expanded === i) { + this.expanded = -1 + } else { + this.expanded = i + } + } + + toggleAnnotationVisibility(id) { + const annotationIndex = this.annotations.findIndex(a => a.id === id) + + if (this.annotations[annotationIndex].annotationVisible) { + this.removeAnnotationFromViewer(id) + this.annotations[annotationIndex].annotationVisible = false + } else { + this.addAnnotationOnViewer(this.annotations[annotationIndex]) + this.annotations[annotationIndex].annotationVisible = true + } + this.storeToLocalStorage() + } + + removeAnnotation(id) { + this.removeAnnotationFromViewer(id) + this.annotations = this.annotations.filter(a => a.id !== id) + this.expanded = -1 + this.storeToLocalStorage() + } + + storeToLocalStorage() { + window.localStorage.setItem(this.nameInLocalStorage, JSON.stringify(this.annotations)) + } + + removeAnnotationFromViewer(id) { + const annotationLayer = this.viewer.layerManager.getLayerByName('user_annotations').layer + let annotations = annotationLayer.localAnnotations.toJSON() + annotations = annotations.filter(a => a.id !== id) + annotationLayer.localAnnotations.restoreState(annotations) + } + + // navigate(coord) { + // this.store.dispatch( + // viewerStateChangeNavigation({ + // navigation: { + // position: coord, + // animation: {}, + // } + // }) + // ) + // } + + public annotationLayerObj = {"user_annotations": { + "type": "annotation", + "tool": "annotateBoundingBox", + "name": this.userAnnotationLayerName, + "annotationColor": "#ffee00", + "annotations": [], + }} + + get randomId() { + return Math.random().toString(36).substr(2, 9) + } +} diff --git a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css new file mode 100644 index 0000000000000000000000000000000000000000..8fcc6ad32e64fc6092be6ee3fc81aaea37c49798 --- /dev/null +++ b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css @@ -0,0 +1,45 @@ +.annotation-content { + max-width: 500px; +} + +.inactive-filter { + color: #bababa; +} + +.selecting-height { + max-height: 100px; +} + +.annotation-list { + max-height: 400px; +} + +.anno-item { + background: #424242; + box-shadow: 5px 5px 10px #383838, + -5px -5px 10px #4c4c4c; +} +.hovering-border { + background: linear-gradient(145deg, #3b3b3b, #474747); + box-shadow: 5px 5px 10px #383838, + -5px -5px 10px #4c4c4c; +} +.selected-border { + background: #424242; + box-shadow: inset 5px 5px 10px #383838, + inset -5px -5px 10px #4c4c4c; +} + +.minimize-icon { + position: absolute; + top: 10px; + right: 10px; +} + +.minimized { + max-height: 30px; +} + +.minimize-row { + min-height: 30px; +} diff --git a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.template.html b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.template.html new file mode 100644 index 0000000000000000000000000000000000000000..f2dac52e65b6d6c195fa148f689cf8384cda0536 --- /dev/null +++ b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.template.html @@ -0,0 +1,110 @@ +<div class="w-100 d-flex justify-content-center position-relative overflow-hidden" + (mouseleave)="cursorOut = true" (mouseenter)="cursorOut=false" + [ngClass]="selecting && cursorOut? 'selecting-height' : minimized? 'minimized' : ''"> + <div class="minimize-icon" *ngIf="!editingMode"> + <i *ngIf="minimized" class="fas fa-plus cursor-pointer" (click)="minimized=false"></i> + <i *ngIf="!minimized" class="fas fa-minus cursor-pointer" (click)="minimized=true"></i> + <i class="ml-3 fas fa-times cursor-pointer" (click)="close.emit()"></i> + </div> + <div aria-label="user annotations viewer" class="annotation-content w-100"> + <div class="minimize-row d-flex align-items-center" *ngIf="minimized" (click)="minimized=false"> + {{this.annotations.length}} - Annotations + </div> + + <div aria-label="user annotations header" class="overflow-hidden"> + <edit-annotation [cursorOut]="cursorOut" + [showOnFocus]="true" + (saveAnnotation)="saveAnnotation($event)" + (editingMode)="editingMode = $event" + (selectingOutput)="this.selecting = $event"></edit-annotation> + </div> + + <div *ngIf="!editingMode" + aria-label="user annotations list" + class="annotation-list d-flex flex-column overflow-auto mb-2"> + <div *ngFor="let annotation of annotations; let i = index;" + [ngClass]="expanded === i? 'selected-border' : hovering === i? 'hovering-border' : 'anno-item'" + (mouseenter)="hovering = i" + (mouseleave)="hovering = -1" + class="p-2 pr-4 pl-4 m-2"> + <div class="d-flex align-items-center justify-content-between" + (click)="toggleAnnotationView(i)"> + <div class="d-flex align-items-center"> + <small class="mr-4"><i [ngClass]="annotation.type === 'line'? 'fas fa-slash' + : annotation.type === 'bounding box'? 'far fa-square' + : annotation.type === 'ellipsoid'? 'fas fa-bullseye' : 'fas fa-circle'"></i></small> + <div> + <div *ngIf="annotation.name">{{annotation.name}}</div> + <div matCardSubtitle class="m-0" *ngIf="annotation.templateName">{{annotation.templateName}}</div> + <div matCardSubtitle class="d-flex align-items-center m-0"> + <small class="font-italic">{{annotation.position1}}</small> + <small class="font-italic" *ngIf="annotation.position2"> • {{annotation.position2}}</small> + </div> + </div> + </div> + <div> + <button class="mr-1 ml-1" mat-icon-button + aria-label="Toggle annotation view"> + <i class="fas" [ngClass]="expanded === i? 'fa-arrow-up' : 'fa-arrow-down'"></i> + </button> + </div> + </div> + <div *ngIf="expanded === i"> + <div *ngIf="annotation.description">{{annotation.description}}</div> + <div class="d-flex align-items-center justify-content-center w-100"> + <button class="mr-1 ml-1" mat-icon-button + aria-label="Hide annotation" + [matTooltip]="annotation.annotationVisible? 'Hide' : 'Show'" + (click)="toggleAnnotationVisibility(annotation.id)"> + <i class="far fa-circle" *ngIf="!annotation.annotationVisible"></i> + <i class="fas far fa-check-circle" *ngIf="annotation.annotationVisible"></i> + </button> + <button class="mr-1 ml-1" mat-icon-button + disabled + aria-label="Export single annotation"> + <i class="fas fa-file-export"></i> + </button> + <button class="mr-1 ml-1" mat-icon-button + disabled + aria-label="Edit annotation"> + <i class="fas fa-edit"></i> + </button> + <button class="mr-1 ml-1" mat-icon-button + aria-label="Delete annotation" + (click)="removeAnnotation(annotation.id)"> + <i class="fas fa-trash"></i> + </button> + </div> + </div> + </div> + </div> + + <mat-divider class="mt-2 mb-2"></mat-divider> + <div arial-label="user annotations footer" class="d-flex justify-content-between"> + <div> + <button class="mr-1 ml-1" mat-icon-button + disabled + aria-label="Import user annotation" + matTooltip="Import"> + <i class="fas fa-file-import"></i> + </button> + <button class="mr-1 ml-1" mat-icon-button + disabled + aria-label="Export user annotation" + matTooltip="Export"> + <i class="fas fa-file-export"></i> + </button> + </div> + <div class="d-flex flex-column"> + <small [ngClass]="[landmarkFilter !== 'all'? 'inactive-filter' : '']" + class="cursor-pointer" (click)="landmarkFilter = 'all'"> + All landmarks + </small> + <small [ngClass]="[landmarkFilter !== 'current'? 'inactive-filter' : '']" + class="cursor-pointer" (click)="landmarkFilter = 'current'"> + Current template + </small> + </div> + </div> + </div> +</div> diff --git a/src/ui/topMenu/module.ts b/src/ui/topMenu/module.ts index 8ae3bbf7341f50320612c758bd319527860a982a..97f253f6bae0bb4599f869b3c8ffa5e999044545 100644 --- a/src/ui/topMenu/module.ts +++ b/src/ui/topMenu/module.ts @@ -13,6 +13,7 @@ import { KgTosModule } from "../kgtos/module"; import { ScreenshotModule } from "../screenshot"; import { AngularMaterialModule } from "../sharedModules/angularMaterial.module"; import { TopMenuCmp } from "./topMenuCmp/topMenu.components"; +import {UserAnnotationsModule} from "src/atlasComponents/userAnnotations"; @NgModule({ imports: [ @@ -29,6 +30,7 @@ import { TopMenuCmp } from "./topMenuCmp/topMenu.components"; PluginModule, AuthModule, ScreenshotModule, + UserAnnotationsModule ], declarations: [ TopMenuCmp diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index e76a092a2be712b7493517e82d783eab05128f66..63633128ab43e68b8dc665398900f9a8a2c3d252 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -26,6 +26,12 @@ </ng-container> </div> + <!-- user annotations --> + <div iav-fab-speed-dial-child> + <ng-container *ngTemplateOutlet="userAnnotationsBtnTmpl"> + </ng-container> + </div> + <!-- help one pager --> <div iav-fab-speed-dial-child> <ng-container *ngTemplateOutlet="helpBtnTmpl"> @@ -57,6 +63,10 @@ <ng-container *ngTemplateOutlet="pinnedDatasetBtnTmpl"> </ng-container> + <!-- user annotations --> + <ng-container *ngTemplateOutlet="userAnnotationsBtnTmpl"> + </ng-container> + <!-- help one pager --> <ng-container *ngTemplateOutlet="helpBtnTmpl"> </ng-container> @@ -120,6 +130,22 @@ </div> </ng-template> +<!-- User annotations btn --> +<ng-template #userAnnotationsBtnTmpl> + <div class="btnWrapper" + (click)="bottomSheet.open(userAnnotations, {hasBackdrop: false})" + matTooltip="My annotations"> + <iav-dynamic-mat-button + [attr.pinned-datasets-length]="(favDataEntries$ | async)?.length" + [iav-dynamic-mat-button-style]="matBtnStyle" + [iav-dynamic-mat-button-color]="matBtnColor" + iav-dynamic-mat-button-aria-label="Annotations"> + + <i class="fas fa-pencil-ruler"></i> + </iav-dynamic-mat-button> + </div> +</ng-template> + <ng-template #helpBtnTmpl> <div class="btnWrapper" @@ -257,3 +283,10 @@ </mat-list-item> </mat-list> </ng-template> + + + +<ng-template #userAnnotations> + <user-annotations (close)="bottomSheet.dismiss()" style="max-width: 600px"> + </user-annotations> +</ng-template>