From 56475b56fc10f5aa5ffc54ddee3ea93fcd4f87e0 Mon Sep 17 00:00:00 2001 From: fsdavid <daviti1@mail.com> Date: Tue, 18 May 2021 15:20:38 +0200 Subject: [PATCH] Adding annotation --- common/constants.js | 11 +- .../annotationList.component.ts | 53 +++ .../annotationList/annotationList.style.css | 12 + .../annotationList.template.html | 100 +++++ .../annotationMode.component.ts | 363 ++++++++++++++++++ .../annotationMode/annotationMode.style.css | 7 + .../annotationMode.template.html | 26 ++ .../annotationService.service.ts | 163 ++++++++ .../editAnnotation.component.ts | 249 ++---------- .../editAnnotation/editAnnotation.style.css | 7 - .../editAnnotation.template.html | 71 +--- .../groupAnnotationPolygons.pipe.ts | 31 ++ src/atlasComponents/userAnnotations/index.ts | 3 +- src/atlasComponents/userAnnotations/module.ts | 15 +- .../userAnnotationsCmp.components.ts | 204 ---------- .../userAnnotationsCmp.style.css | 29 -- .../userAnnotationsCmp.template.html | 110 ------ .../atlasViewer.apiService.service.ts | 15 +- .../state/viewerState.store.helper.ts | 10 +- src/services/state/viewerState.store.ts | 24 +- src/services/state/viewerState/actions.ts | 9 +- src/services/state/viewerState/selectors.ts | 9 +- .../topMenu/topMenuCmp/topMenu.components.ts | 8 +- .../topMenu/topMenuCmp/topMenu.template.html | 43 +-- src/viewerModule/module.ts | 2 + .../viewerCmp/viewerCmp.component.ts | 22 +- .../viewerCmp/viewerCmp.template.html | 69 +++- 27 files changed, 963 insertions(+), 702 deletions(-) create mode 100644 src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts create mode 100644 src/atlasComponents/userAnnotations/annotationList/annotationList.style.css create mode 100644 src/atlasComponents/userAnnotations/annotationList/annotationList.template.html create mode 100644 src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts create mode 100644 src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css create mode 100644 src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html create mode 100644 src/atlasComponents/userAnnotations/annotationService.service.ts delete mode 100644 src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css create mode 100644 src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts delete mode 100644 src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts delete mode 100644 src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css delete mode 100644 src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.template.html diff --git a/common/constants.js b/common/constants.js index 97fcd1559..0b589f5d8 100644 --- a/common/constants.js +++ b/common/constants.js @@ -54,7 +54,10 @@ // additional volumes TOGGLE_SHOW_LAYER_CONTROL: `Show layer control`, - ADDITIONAL_VOLUME_CONTROL: 'Additional volumes control' + ADDITIONAL_VOLUME_CONTROL: 'Additional volumes control', + + //Viewer mode + VIEWER_MODE_ANNOTATING: 'annotating' } exports.IDS = { @@ -78,6 +81,12 @@ RECEPTOR_FP_CAPTION: `The receptor densities are visualized as fingerprints (fp), which provide the mean density and standard deviation for each of the analyzed receptor types, averaged across samples.`, RECEPTOR_PR_CAPTION: `For a single tissue sample, an exemplary density distribution for a single receptor from the pial surface to the border between layer VI and the white matter.`, RECEPTOR_AR_CAPTION: `An exemplary density distribution of a single receptor for one laminar cross-section in a single tissue sample.`, + + // Annotatins + + // Annotations + USER_ANNOTATION_LAYER_NAME: 'user_annotations', + USER_ANNOTATION_STORE_KEY: `user_landmarks_demo_1` } exports.QUICKTOUR_DESC ={ diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts new file mode 100644 index 000000000..3184287ef --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -0,0 +1,53 @@ +import {Component} from "@angular/core"; +import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; + +@Component({ + selector: 'annotation-list', + templateUrl: './annotationList.template.html', + styleUrls: ['./annotationList.style.css'] +}) +export class AnnotationList { + + public annotationFilter: 'all' | 'current' = 'all' + public editing = -1 + + get annotationsToShow() { + return this.ans.annotations + .filter(a => (a.type !== 'polygon' || +a.id.split('_')[1] === 0) + && (this.annotationFilter === 'all' || a.templateName === this.ans.selectedTemplate)) + } + + constructor(public ans: AnnotationService) {} + + toggleAnnotationVisibility(annotation) { + if (annotation.type === 'polygon') { + this.ans.annotations.filter(an => an.id.split('_')[0] === annotation.id.split('_')[0]) + .forEach(a => this.toggleVisibility(a)) + } else { + this.toggleVisibility(annotation) + } + } + + toggleVisibility(annotation) { + const annotationIndex = this.ans.annotations.findIndex(a => a.id === annotation.id) + + if (this.ans.annotations[annotationIndex].annotationVisible) { + this.ans.removeAnnotationFromViewer(annotation.id) + this.ans.annotations[annotationIndex].annotationVisible = false + } else { + this.ans.addAnnotationOnViewer(this.ans.annotations[annotationIndex]) + this.ans.annotations[annotationIndex].annotationVisible = true + } + this.ans.storeToLocalStorage() + } + + removeAnnotation(annotation) { + if (annotation.type === 'polygon') { + this.ans.annotations.filter(an => an.id.split('_')[0] === annotation.id.split('_')[0]) + .forEach(a => this.ans.removeAnnotation(a.id)) + } else { + this.ans.removeAnnotation(annotation.id) + } + } + +} diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css b/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css new file mode 100644 index 000000000..b405d8a9e --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css @@ -0,0 +1,12 @@ +.annotation-content { + max-width: 500px; + min-width: 300px; +} + +.inactive-filter { + color: #bababa; +} + +.selecting-height { + max-height: 100px; +} diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html new file mode 100644 index 000000000..39677b3ed --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -0,0 +1,100 @@ +<div aria-label="user annotations viewer" class="annotation-content w-100"> + + <div aria-label="user annotations header" class="overflow-hidden mt-3 mr-2 ml-2"> + <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]="[annotationFilter !== 'all'? 'inactive-filter' : '']" + class="cursor-pointer" (click)="annotationFilter = 'all'"> + All landmarks + </small> + <small [ngClass]="[annotationFilter !== 'current'? 'inactive-filter' : '']" + class="cursor-pointer" (click)="annotationFilter = 'current'"> + Current template + </small> + </div> + </div> + </div> + <mat-divider class="mt-2 mb-2"></mat-divider> + + + <mat-accordion aria-label="user annotations list" + class="h-100 d-flex flex-column overflow-auto"> +<!-- <mat-expansion-panel hideToggle *ngFor="let annotation of ans.annotations | groupAnnotationPolygons; let i = index;">--> + <mat-expansion-panel hideToggle *ngFor="let annotation of annotationsToShow; let i = index;"> + <mat-expansion-panel-header> + + <mat-panel-title> + <div class="d-flex flex-column align-items-center m-0"> + <small class="font-italic">{{annotation.position1}}</small> + <small class="font-italic" *ngIf="annotation.position2"> {{annotation.position2}}</small> + </div> + </mat-panel-title> + + <mat-panel-description class="w-100 d-flex align-items-center justify-content-end" + [matTooltip]="annotation.type"> + <span class="mr-2">{{annotation.name}}</span> + <small><i [ngClass]="annotation.type === 'line'? 'fas fa-slash' + : annotation.type === 'bounding box'? 'far fa-square' + : annotation.type === 'ellipsoid'? 'fas fa-bullseye' + : annotation.type === 'polygonParent' || annotation.type === 'polygon'? 'fas fa-draw-polygon' + : 'fas fa-circle'"></i></small> + </mat-panel-description> + + + + </mat-expansion-panel-header> + + + <div> + <div *ngIf="annotation.description">{{annotation.description}}</div> + + <small>{{annotation.templateName}}</small> + + <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)"> + <i class="fas far fa-check-circle" *ngIf="annotation.annotationVisible; else notVisible"></i> + <ng-template #notVisible><i class="far fa-circle"></i></ng-template> + </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 + (click)="editing = i" + 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)"> + <i class="fas fa-trash"></i> + </button> + </div> + + <edit-annotation [annotation]="annotation" *ngIf="editing === i" (finished)="editing = -1"> + </edit-annotation> + </div> + + </mat-expansion-panel> + + </mat-accordion> + +</div> diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts new file mode 100644 index 000000000..c2368a8d4 --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -0,0 +1,363 @@ +import {Component, HostListener, Inject, OnDestroy, OnInit, Optional} from "@angular/core"; +import {select, Store} from "@ngrx/store"; +import {CONST} from "common/constants"; +import {Observable, Subscription} from "rxjs"; +import {getUuid} from "src/util/fn"; +import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; +import {buffer, debounceTime, distinctUntilChanged, filter, map, switchMapTo, take, takeUntil, tap} from "rxjs/operators"; +import {viewerStateNavigationStateSelector, viewerStateSelectedTemplateSelector} from "src/services/state/viewerState/selectors"; +import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; + +@Component({ + selector: 'annotating-mode', + templateUrl: './annotationMode.template.html', + styleUrls: ['./annotationMode.style.css'] +}) +export class AnnotationMode implements OnInit, OnDestroy { + + public selectedType = 0 + + public position1: string + public position2: string + public editingAnnotationId: string + + public selecting = 'position1' + public mousePos + public navState: any + + private hoverAnnotation$: Observable<{id: string, partIndex: number}> + public hoverAnnotation: {id: string, partIndex: number} + private onDestroyCb: Function[] = [] + public subscriptions: Subscription[] = [] + + //ToDo remove + public dark = true + + private get viewer(){ + return this.injectedViewer || (window as any).viewer + } + + get nehubaViewer() { + return (window as any).nehubaViewer + } + get interactiveViewer() { + return (window as any).interactiveViewer + } + + constructor( + private store$: Store<any>, + public ans: AnnotationService, + @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer, + ) {} + + ngOnInit(): void { + // Load annotation layer on init + this.ans.loadAnnotationLayer() + + this.hoverAnnotation$ = new Observable<{id: string, partIndex: number}>(obs => { + const mouseState = this.viewer.mouseState + const cb: () => void = mouseState.changed.add(() => { + if (mouseState.active && mouseState.pickedAnnotationLayer === this.ans.addedLayer.layer.annotationLayerState.value) { + obs.next({ + id: mouseState.pickedAnnotationId, + partIndex: mouseState.pickedOffset + }) + } else { + obs.next(null) + } + }) + this.onDestroyCb.push(() => { + cb() + obs.complete() + }) + }).pipe( + distinctUntilChanged((o, n) => { + if (o === n) return true + return `${o?.id || ''}${o?.partIndex || ''}` === `${n?.id || ''}${n?.partIndex || ''}` + }) + ) + + this.subscriptions.push(this.hoverAnnotation$.subscribe(ha => { + this.hoverAnnotation = ha + })) + + const mouseDown$ = this.interactiveViewer.viewerHandle.mouseEvent.pipe( + filter((e: any) => e.eventName === 'mousedown') + ) + const mouseUp$ = this.interactiveViewer.viewerHandle.mouseEvent.pipe( + filter((e: any) => e.eventName === 'mouseup') + ) + const mouseMove$ = this.interactiveViewer.viewerHandle.mouseEvent.pipe( + filter((e: any) => e.eventName === 'mousemove') + ) + + // Trigger mouse click on viewer (avoid dragging) + this.subscriptions.push( + mouseDown$.pipe( + switchMapTo( + mouseUp$.pipe( + takeUntil(mouseMove$), + ), + ), + ).subscribe(event => { + setTimeout(() => { + this.mouseClick() + }) + }) + ) + + // Dragging - edit hovering annotations while dragging + let hovering + let hoveringType + let hoveringPosition1 + let hoveringPosition2 + let draggingStartPosition + let hoveringPolygonAnnotations + this.subscriptions.push( + mouseDown$.pipe( + tap(() => { + hovering = this.hoverAnnotation + if (hovering) { + draggingStartPosition = this.mousePos + const hoveringAnnotation = this.ans.annotations.find(a => a.id === this.hoverAnnotation.id) + if (hoveringAnnotation) { + hoveringPosition1 = hoveringAnnotation.position1.split(',') + hoveringPosition2 = hoveringAnnotation.position2 ? hoveringAnnotation.position2.split(',') : null + hoveringType = this.ans.annotations.find(a => a.id === hovering.id)?.type + if (hoveringAnnotation.type === 'polygon') { + hoveringPolygonAnnotations = this.ans.annotations.filter(a => a.id.split('_')[0] === hovering.id.split('_')[0]) + } + hoveringType = this.ans.annotations.find(a => a.id === hovering.id)?.type + } + } + }), + switchMapTo( + mouseMove$.pipe( + takeUntil(mouseUp$), + ), + ), + ).subscribe(event => { + if (hovering && this.selecting !== 'position2') { + // keep navigation while dragging + this.interactiveViewer.viewerHandle.setNavigationLoc(this.navState, false) + // make changes to annotations by type + // - when line is hovered move full annotation - + // - when line point is hovered move only point + if (this.mousePos) { + const dragRange = this.mousePos.map((mp, i) => mp - +draggingStartPosition[i]) + + if (hoveringType === 'point') { + this.ans.saveAnnotation({id: hovering.id, position1: this.mousePos.join(), type: hoveringType}) + } else if (hoveringType === 'line') { + if (hovering.partIndex === 0) { + + this.ans.saveAnnotation({id: hovering.id, + position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]).join(), + position2: hoveringPosition2.map((hp, i) => +hp + dragRange[i]).join(), + type: hoveringType}) + } else if (hovering.partIndex === 1) { + this.ans.saveAnnotation({id: hovering.id, + position1: this.mousePos.join(), + position2: hoveringPosition2.join(), + type: hoveringType}) + } else if (hovering.partIndex === 2) { + this.ans.saveAnnotation({id: hovering.id, + position1: hoveringPosition1.join(), + position2: this.mousePos.join(), + type: hoveringType}) + } + } else if (hoveringType === 'bounding box') { + this.ans.saveAnnotation({id: hovering.id, + position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]).join(), + position2: hoveringPosition2.map((hp, i) => +hp + dragRange[i]).join(), + type: hoveringType}) + } else if (hoveringType === 'ellipsoid') { + this.ans.saveAnnotation({id: hovering.id, + position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]).join(), + position2: hoveringPosition2.join(), + type: hoveringType}) + } else if (hoveringType === 'polygon') { + if (hovering.partIndex === 0) { + hoveringPolygonAnnotations.forEach(pa => { + this.ans.saveAnnotation({ + id: pa.id, + position1: pa.position1.split(',').map((hp, i) => +hp + dragRange[i]).join(), + position2: pa.position2.split(',').map((hp, i) => +hp + dragRange[i]).join(), + type: pa.type + }) + }) + } else if (hovering.partIndex === 2) { + this.ans.saveAnnotation({id: hovering.id, + position1: hoveringPosition1.join(), + position2: this.mousePos.join(), + type: hoveringType}) + + const hoveringPolygonNum = +hovering.id.split('_')[1] + const nextPolygon = hoveringPolygonAnnotations.find(hpa => +hpa.id.split('_')[1] === hoveringPolygonNum + 1) + if (nextPolygon) { + this.ans.saveAnnotation({id: nextPolygon.id, + position1: this.mousePos.join(), + position2: nextPolygon.position2, + type: nextPolygon.type}) + } + + } else if (hovering.partIndex === 1) { + this.ans.saveAnnotation({id: hovering.id, + position1: this.mousePos.join(), + position2: hoveringPosition2.join(), + type: hoveringType}) + + const hoveringPolygonNum = +hovering.id.split('_')[1] + const prevPolygon = hoveringPolygonAnnotations.find(hpa => +hpa.id.split('_')[1] === hoveringPolygonNum - 1) || null + if (prevPolygon) { + this.ans.saveAnnotation({id: prevPolygon.id, + position1: prevPolygon.position1, + position2: this.mousePos.join(), + type: prevPolygon.type}) + } + } + } + } + } + + }) + ) + + this.subscriptions.push( + this.nehubaViewer.mousePosition.inVoxels + .subscribe(floatArr => { + this.mousePos = floatArr && floatArr + + if (this.selecting === 'position1' && this.mousePos) { + this.position1 = this.mousePos.join() + } else if (this.selecting === 'position2' && this.mousePos) { + if (this.ans.annotationTypes[this.selectedType].name === 'Ellipsoid') { + this.position2 = [ + this.ans.getRadii(this.position1.split(',').map(n => +n), this.mousePos), + ].join() + } else { + this.position2 = this.mousePos.join() + } + + if (this.position1 + && (this.ans.annotationTypes[this.selectedType].type === 'doubleCoordinate' + || this.ans.annotationTypes[this.selectedType].type === 'polygon') + && this.position2) { + if (!this.editingAnnotationId) { + this.editingAnnotationId = getUuid() + if (this.ans.annotationTypes[this.selectedType].type === 'polygon') { + this.editingAnnotationId += '_0' + } + } + this.ans.saveAnnotation({id: this.editingAnnotationId, position1: this.position1, + position2: this.position2, + type: this.ans.annotationTypes[this.selectedType].name}) + } + } + }), + + // Double click - end creating polygon + mouseUp$.pipe( + buffer(mouseUp$.pipe(debounceTime(250))), + map((list: any) => list.length), + filter(x => x === 2) + ).subscribe(() => { + + if (this.ans.annotationTypes[this.selectedType].type === 'polygon') { + this.ans.removeAnnotation(this.editingAnnotationId) + const splitEditingAnnotationId = this.editingAnnotationId.split('_') + const prevAnnotation = this.ans.annotations.find(a => a.id === `${splitEditingAnnotationId[0]}_${+splitEditingAnnotationId[1] - 1}`) + if (prevAnnotation.id) this.ans.removeAnnotation(prevAnnotation.id) + this.editingAnnotationId = null + this.selecting = 'position1' + } + }), + + this.store$.pipe( + select(viewerStateNavigationStateSelector), + ).subscribe(nav => { + this.navState = nav.position.map(np => np/1e6) + }), + this.store$.pipe( + select(viewerStateSelectedTemplateSelector), + take(1) + ).subscribe(tmpl => { + this.ans.selectedTemplate = tmpl.name + this.dark = tmpl.useTheme === 'dark' + + // Set get annotations from the local storage and add them to the viewer + if (window.localStorage.getItem(CONST.USER_ANNOTATION_STORE_KEY) && window.localStorage.getItem(CONST.USER_ANNOTATION_STORE_KEY).length) { + const annotationsString = window.localStorage.getItem(CONST.USER_ANNOTATION_STORE_KEY) + this.ans.annotations = JSON.parse(annotationsString) + this.ans.annotations.filter(a => a.annotationVisible && a.templateName === this.ans.selectedTemplate).forEach(a => this.ans.addAnnotationOnViewer(a)) + } + }) + ) + + } + + ngOnDestroy(): void { + while (this.onDestroyCb.length > 0) this.onDestroyCb.pop()() + this.subscriptions.forEach(s => s.unsubscribe()) + if (this.ans.addedLayer) { + this.viewer.layerManager.removeManagedLayer(this.ans.addedLayer) + } + } + + @HostListener('document:keydown.escape', ['$event']) onKeydownHandler(event: KeyboardEvent) { + if (this.selecting === 'position2' && this.mousePos) { + this.ans.removeAnnotation(this.editingAnnotationId) + this.editingAnnotationId = null + this.selecting = 'position1' + } + } + + mouseClick() { + // Remove annotation + if (this.ans.annotationTypes[this.selectedType].type === 'remove' && this.hoverAnnotation) { + const hoveringAnnotationObj = this.ans.annotations.find(a => a.id === this.hoverAnnotation.id) + if (hoveringAnnotationObj.type === 'polygon') { + const polygonAnnotations = this.ans.annotations.filter(a => a.id.split('_')[0] === hoveringAnnotationObj.id.split('_')[0]) + polygonAnnotations.forEach(pa => this.ans.removeAnnotation(pa.id)) + } else { + this.ans.removeAnnotation(this.hoverAnnotation.id) + } + } + // save annotation by selected annotation type + if (this.selecting === 'position1' && this.position1) { + if (this.ans.annotationTypes[this.selectedType].type === 'singleCoordinate') { + this.ans.saveAnnotation({position1: this.position1, + position2: this.position2, + type: this.ans.annotationTypes[this.selectedType].name}) + } else if (this.ans.annotationTypes[this.selectedType].type === 'doubleCoordinate' + || this.ans.annotationTypes[this.selectedType].type === 'polygon') { + this.selecting = 'position2' + } + + } else if (this.selecting === 'position2' && this.position2 && this.mousePos) { + this.ans.saveAnnotation({id: this.editingAnnotationId, + position1: this.position1, + position2: this.position2, + type: this.ans.annotationTypes[this.selectedType].name}) + if (this.ans.annotationTypes[this.selectedType].type === 'polygon') { + this.position1 = this.position2 + const splitEditingAnnotationId = this.editingAnnotationId.split('_') + this.editingAnnotationId = splitEditingAnnotationId[0] + '_' + (+splitEditingAnnotationId[1]+1) + } else { + this.editingAnnotationId = null + this.selecting = 'position1' + } + + } + } + + public selectAnnotationType = (typeIndex) => { + this.selectedType = typeIndex + this.editingAnnotationId = null + this.mousePos = null + this.position2 = null + this.position1 = null + this.selecting = 'position1' + } + +} diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css new file mode 100644 index 000000000..2ff0c0d71 --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css @@ -0,0 +1,7 @@ +.annotation-toolbar { + z-index: 100; + width: 40px; +} +.annotation-toolbar div { + +} diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html new file mode 100644 index 000000000..022ae8cb0 --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html @@ -0,0 +1,26 @@ +<div class="pe-all d-flex flex-column justify-content-content annotation-toolbar"> + + <div class="d-flex flex-column flex-grow-0 panel-default" + [ngStyle]="{backgroundColor: dark? '#424242' : 'white', + color: dark? 'antiquewhite' : '#424242'}" + style="margin-bottom: 20px;"> + <button *ngFor="let type of ans.annotationTypes; let i = index" + mat-icon-button + type="button" + (click)="selectAnnotationType(i)" + class="mb-2 mt-2" + [matTooltip]="type.name" + [color]="selectedType === i? 'primary' : 'secondary'"> + <i [ngClass]="type.class"></i> + </button> + <button + mat-icon-button + (click)="ans.disable()" + type="button" + class="mb-2 mt-2" + matTooltip="Disable annotating" + color="warn"> + <i class="fas fa-times"></i> + </button> + </div> +</div> diff --git a/src/atlasComponents/userAnnotations/annotationService.service.ts b/src/atlasComponents/userAnnotations/annotationService.service.ts new file mode 100644 index 000000000..517f723b6 --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationService.service.ts @@ -0,0 +1,163 @@ +import {ApplicationRef, ChangeDetectorRef, Inject, Injectable, OnDestroy, Optional} from "@angular/core"; +import {CONST} from "common/constants"; +import {viewerStateSetViewerMode} from "src/services/state/viewerState/actions"; +import {Subscription} from "rxjs"; +import {getUuid} from "src/util/fn"; +import {Store} from "@ngrx/store"; +import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; +import {TemplateCoordinatesTransformation} from "src/services/templateCoordinatesTransformation.service"; + +const USER_ANNOTATION_LAYER_SPEC = { + "type": "annotation", + "tool": "annotateBoundingBox", + "name": CONST.USER_ANNOTATION_LAYER_NAME, + "annotationColor": "#ffee00", + "annotations": [], +} + +@Injectable() +export class AnnotationService implements OnDestroy { + + public annotations = [] + public addedLayer: any + public ellipsoidMinRadius = 0.5 + + public selectedTemplate: string + public subscriptions: Subscription[] = [] + + public annotationTypes = [ + {name: 'Cursor', class: 'fas fa-mouse-pointer', type: 'move'}, + {name: 'Point', class: 'fas fa-circle', type: 'singleCoordinate'}, + {name: 'Line', class: 'fas fa-slash', type: 'doubleCoordinate'}, + {name: 'Polygon', class: 'fas fa-draw-polygon', type: 'polygon'}, + {name: 'Bounding box', class: 'far fa-square', type: 'doubleCoordinate'}, + {name: 'Ellipsoid', class: 'fas fa-bullseye', type: 'doubleCoordinate'}, + {name: 'Remove', class: 'fas fa-trash', type: '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( + CONST.USER_ANNOTATION_LAYER_NAME, + USER_ANNOTATION_LAYER_SPEC + ) + + this.addedLayer = this.viewer.layerManager.addManagedLayer(layer) + + + } + + 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 + } + + saveAnnotation({id = null, + position1 = null, //this.position1, + position2 = null, //this.position2, + type = null, //this.annotationTypes[this.selectedType].name, + description = null, + name = null, + templateName = null, + } = {}) { + let annotation = { + id: id || getUuid(), + annotationVisible: true, + description, + name, + position1, + position2, + templateName: templateName || this.selectedTemplate, + type: type.toLowerCase() + } + + const foundIndex = this.annotations.findIndex(x => x.id === annotation.id) + + if (foundIndex >= 0) { + annotation = { + ...this.annotations[foundIndex], + ...annotation + } + this.annotations[foundIndex] = annotation + } else { + this.annotations.push(annotation) + } + this.addAnnotationOnViewer(annotation) + this.storeToLocalStorage() + } + + addAnnotationOnViewer(annotation) { + const annotationLayer = this.viewer.layerManager.getLayerByName(CONST.USER_ANNOTATION_LAYER_NAME).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? 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' || annotation.type === 'polygon'? + position1Voxel : null, + pointB: annotation.type === 'line' || annotation.type === 'bounding box' || annotation.type === 'polygon'? + position2Voxel : null, + center: annotation.type === 'ellipsoid'? + position1Voxel : null, + radii: annotation.type === 'ellipsoid'? + position2Voxel : 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.annotations = this.annotations.filter(a => a.id !== id) + this.storeToLocalStorage() + } + + storeToLocalStorage() { + // ToDo temporary solution - because impure pipe stucks + + + window.localStorage.setItem(CONST.USER_ANNOTATION_STORE_KEY, JSON.stringify(this.annotations)) + } + + removeAnnotationFromViewer(id) { + const annotationLayer = this.viewer.layerManager.getLayerByName(CONST.USER_ANNOTATION_LAYER_NAME)?.layer + if (annotationLayer) { + let annotations = annotationLayer.localAnnotations.toJSON() + annotations = annotations.filter(a => a.id !== id) + annotationLayer.localAnnotations.restoreState(annotations) + } + } + + ngOnDestroy(){ + this.subscriptions.forEach(s => s.unsubscribe()) + } +} diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts index f797b1e85..981b3ee90 100644 --- a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts +++ b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts @@ -1,276 +1,81 @@ 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 { Observable, Subscription } from "rxjs"; -import { filter } from "rxjs/operators"; -import { select, Store } from "@ngrx/store"; -import { viewerStateSelectedTemplateSelector } from "src/services/state/viewerState/selectors"; -import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; +import { Subscription } from "rxjs"; +import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; @Component({ selector: 'edit-annotation', templateUrl: './editAnnotation.template.html', - styleUrls: ['./editAnnotation.style.css'], animations: [ - // doesn't do anything? 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 + @Input() annotation: any - @Output() saveAnnotation: EventEmitter<any> = new EventEmitter() - @Output() selectingOutput: EventEmitter<any> = new EventEmitter() - @Output() editingMode: EventEmitter<any> = new EventEmitter() + @Output() finished: 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 (window as any).viewer - } - constructor( private formBuilder: FormBuilder, - private changeDetectionRef: ChangeDetectorRef, - private store: Store<any>, + public ans: AnnotationService ) { 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}, { + id: [{value: 'null'}], + position1: [{value: ''}], + position2: [{value: ''}], + name: [{value: ''}, { validators: [Validators.maxLength(200)] }], - description: [{value: '', disabled: this.loading}, { + description: [{value: ''}, { validators: [Validators.maxLength(1000)] }], templateName: [{value: ''}], - type: [{value: 'point'}], - annotationVisible: [true] + type: [{value: ''}], + annotationVisible: [{value: 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() - } + this.annotationForm.controls.id.setValue(this.annotation.id) + this.annotationForm.controls.position1.setValue(this.annotation.position1) + this.annotationForm.controls.position2.setValue(this.annotation.position2) + this.annotationForm.controls.name.setValue(this.annotation.name) + this.annotationForm.controls.description.setValue(this.annotation.description) + this.annotationForm.controls.templateName.setValue(this.annotation.templateName) + this.annotationForm.controls.type.setValue(this.annotation.type) + this.annotationForm.controls.annotationVisible.setValue(this.annotation.annotationVisible) } - 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 - if (annotationLayer) { - 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.ans.saveAnnotation(this.annotationForm.value) this.cancelEditing() } } cancelEditing() { - if (this.showOnFocus) { - this.showFull = false - this.editingMode.emit(this.showFull) - } + this.finished.emit() this.resetForm() } @@ -279,17 +84,7 @@ export class EditAnnotationComponent implements OnInit, OnDestroy { 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) }) diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css deleted file mode 100644 index fa79116ce..000000000 --- a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.style.css +++ /dev/null @@ -1,7 +0,0 @@ -.selectedType { - -} - -.short-input { - max-width: 200px; -} diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html index 515ffb62c..a52351f57 100644 --- a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html +++ b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html @@ -4,70 +4,39 @@ (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" + <div 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> - + <mat-form-field class="w-100"> + <mat-label>Name</mat-label> + <input name="name" + formControlName="name" + matInput> + </mat-form-field> <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 *ngIf="selectedType.name !== 'Ellipsoid'">position {{selectedType.twoCoordinates && ' 1'}}</mat-label> - <mat-label *ngIf="selectedType.name === 'Ellipsoid'">center (vox)</mat-label> + <mat-form-field class="w-100 annotation-editing-body"> + <mat-label *ngIf="annotation.type !== 'ellipsoid'">Position {{annotation.position2 && ' 1'}}</mat-label> + <mat-label *ngIf="annotation.type === 'ellipsoid'">Center (vox)</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="selecting === 'position1'" class="fas fa-crosshairs" style="margin-left: -20px;"></i> - <i *ngIf="position1Selected && !(cursorOut && selecting === 'position1')" class="fas fa-check-circle" style="margin-left: -20px;"></i> + <input type="text" name="position1" + placeholder="0,0,0" + class="pr-4" + formControlName="position1" + matInput> </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 *ngIf="selectedType.name !== 'Ellipsoid'">position 2 (vox)</mat-label> - <mat-label *ngIf="selectedType.name === 'Ellipsoid'">radii</mat-label> + <div class="d-flex flex-column align-items-center w-100" *ngIf="annotation.position2"> + <mat-form-field class="w-100 annotation-editing-body"> + <mat-label *ngIf="annotation.type !== 'ellipsoid'">Position 2 (vox)</mat-label> + <mat-label *ngIf="annotation.type === 'ellipsoid'">Radii</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="selecting === 'position2'" class="fas fa-crosshairs" style="margin-left: -20px;"></i> - <i *ngIf="position2Selected && !(cursorOut && selecting === 'position2')" class="fas fa-check-circle" style="margin-left: -20px;"></i> + formControlName="position2" matInput> </mat-form-field> - <small *ngIf="cursorOut && selecting === 'position2'" style="margin-top: -20px;">selecting</small> </div> </div> diff --git a/src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts b/src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts new file mode 100644 index 000000000..0b85117b8 --- /dev/null +++ b/src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts @@ -0,0 +1,31 @@ +import {Pipe, PipeTransform} from "@angular/core"; + +@Pipe({ name: 'groupAnnotationPolygons'}) +export class GroupAnnotationPolygons implements PipeTransform { + + transform(annotations: any[]) { + + // let transformed = [...annotations] + + // for (let i = 0; i<annotations.length; i++) { + // if (annotations[i].type === 'polygon') { + // 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]) + // + // transformed = transformed.filter(a => a.id.split('_')[0] !== annotationId[0]) + // + // transformed.push({ + // id: annotationId[0], + // type: 'polygonParent', + // annotations: polygonAnnotations, + // templateName: annotations[i].templateName + // }) + // } + // } + // } + // return transformed + + return annotations.filter(a => a.type !== 'polygon' || +a.id.split('_')[1] === 0) + } +} diff --git a/src/atlasComponents/userAnnotations/index.ts b/src/atlasComponents/userAnnotations/index.ts index 8e75162e0..6da0cda7d 100644 --- a/src/atlasComponents/userAnnotations/index.ts +++ b/src/atlasComponents/userAnnotations/index.ts @@ -1,2 +1,3 @@ -export { UserAnnotationsComponent } from "./userAnnotationsCmp/userAnnotationsCmp.components"; +export { AnnotationMode } from "./annotationMode/annotationMode.component"; +export { AnnotationList } from "./annotationList/annotationList.component"; export { UserAnnotationsModule } from "./module"; diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index 942cab632..e54f72cee 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -2,10 +2,13 @@ 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"; +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 {GroupAnnotationPolygons} from "src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe"; @NgModule({ imports: [ @@ -17,11 +20,17 @@ import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; AngularMaterialModule, ], declarations: [ - UserAnnotationsComponent, EditAnnotationComponent, + AnnotationMode, + AnnotationList, + GroupAnnotationPolygons + ], + providers: [ + AnnotationService ], exports: [ - UserAnnotationsComponent, + AnnotationMode, + AnnotationList ] }) diff --git a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts deleted file mode 100644 index 9e19c5f2d..000000000 --- a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.components.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { Component, EventEmitter, OnDestroy, OnInit, Output} from "@angular/core"; -import { Observable, Subscription } from "rxjs"; -import { distinctUntilChanged } from "rxjs/operators"; -import { getUuid } from 'src/util/fn' - -const USER_ANNOTATION_LAYER_NAME = 'USER_ANNOTATION_LAYER_NAME' -const USER_ANNOTATION_LAYER_SPEC = { - "type": "annotation", - "tool": "annotateBoundingBox", - "name": USER_ANNOTATION_LAYER_NAME, - "annotationColor": "#ffee00", - "annotations": [], -} -const USER_ANNOTATION_STORE_KEY = `user_landmarks_demo_1` - -@Component({ - selector: 'user-annotations', - templateUrl: './userAnnotationsCmp.template.html', - styleUrls: ['./userAnnotationsCmp.style.css'] -}) -export class UserAnnotationsComponent implements OnInit, OnDestroy { - - public landmarkFilter: 'all' | 'current' = 'all' - public cursorOut = false - public selecting: string - public editingMode = false - public minimized = false - - public hovering = -1 - public expanded = -1 - - public annotations = [] - private hoverAnnotation$: Observable<{id: string, partIndex: number}> - - @Output() close: EventEmitter<any> = new EventEmitter() - - private subscription: Subscription[] = [] - private onDestroyCb: (() => void )[] = [] - - private get viewer(){ - return (window as any).viewer - } - - ngOnDestroy(): void { - while(this.onDestroyCb.length) this.onDestroyCb.pop()() - while(this.subscription.length) this.subscription.pop().unsubscribe() - - if (!this.viewer) { - throw new Error(`this.viewer is undefined`) - } - const annotationLayer = this.viewer.layerManager.getLayerByName(USER_ANNOTATION_LAYER_NAME) - if (annotationLayer) { - this.viewer.layerManager.removeManagedLayer(annotationLayer) - } - } - - ngOnInit(): void { - this.loadAnnotationLayer() - - if (window.localStorage.getItem(USER_ANNOTATION_STORE_KEY) && window.localStorage.getItem(USER_ANNOTATION_STORE_KEY).length) { - const annotationsString = window.localStorage.getItem(USER_ANNOTATION_STORE_KEY) - this.annotations = JSON.parse(annotationsString) - this.annotations.filter(a => a.annotationVisible).forEach(a => this.addAnnotationOnViewer(a)) - } - } - - 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 - ) - - const addedLayer = this.viewer.layerManager.addManagedLayer(layer) - - this.hoverAnnotation$ = new Observable<{id: string, partIndex: number}>(obs => { - const mouseState = this.viewer.mouseState - const cb: () => void = mouseState.changed.add(() => { - if (mouseState.active && mouseState.pickedAnnotationLayer === addedLayer.layer.annotationLayerState.value) { - obs.next({ - id: mouseState.pickedAnnotationId, - partIndex: mouseState.pickedOffset - }) - } else { - obs.next(null) - } - }) - this.onDestroyCb.push(() => { - cb() - obs.complete() - }) - }).pipe( - distinctUntilChanged((o, n) => { - if (o === n) return true - return `${o?.id || ''}${o?.partIndex || ''}` === `${n?.id || ''}${n?.partIndex || ''}` - }) - ) - } - - saveAnnotation(annotation) { - if (!annotation.id) { - annotation.id = getUuid() - } - - 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_ANNOTATION_LAYER_NAME).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(USER_ANNOTATION_STORE_KEY, JSON.stringify(this.annotations)) - } - - 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) - } - } - - // navigate(coord) { - // this.store.dispatch( - // viewerStateChangeNavigation({ - // navigation: { - // position: coord, - // animation: {}, - // } - // }) - // ) - // } -} diff --git a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css b/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css deleted file mode 100644 index ed1848825..000000000 --- a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.style.css +++ /dev/null @@ -1,29 +0,0 @@ -.annotation-content { - max-width: 500px; -} - -.inactive-filter { - color: #bababa; -} - -.selecting-height { - max-height: 100px; -} - -.annotation-list { - max-height: 400px; -} - -.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 deleted file mode 100644 index 8a79dfdd8..000000000 --- a/src/atlasComponents/userAnnotations/userAnnotationsCmp/userAnnotationsCmp.template.html +++ /dev/null @@ -1,110 +0,0 @@ -<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? 'mat-elevation-z6' : hovering === i? 'mat-elevation-z8' : 'mat-elevation-z2'" - (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/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index e7253b284..0833ddd51 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -6,7 +6,10 @@ import { Observable, Subject, Subscription, from, race, of, } from "rxjs"; import { distinctUntilChanged, map, filter, startWith, switchMap, catchError, mapTo, take } from "rxjs/operators"; import { DialogService } from "src/services/dialogService.service"; import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"; -import { viewerStateFetchedTemplatesSelector } from "src/services/state/viewerState/selectors"; +import { + viewerStateFetchedTemplatesSelector, + viewerStateViewerModeSelector +} from "src/services/state/viewerState/selectors"; import { getLabelIndexMap, getMultiNgIdsRegionsLabelIndexMap, @@ -17,6 +20,7 @@ import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { FRAGMENT_EMIT_RED } from "src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component"; import { IPluginManifest, PluginServices } from "src/plugin"; import { ILoadMesh } from 'src/messaging/types' +import {ARIA_LABELS} from "common/constants"; declare let window @@ -84,6 +88,15 @@ export class AtlasViewerAPIServices implements OnDestroy{ private s: Subscription[] = [] private onMouseClick(ev: any): boolean{ + + // If annotation mode is on, avoid region selection + let viewerMode + this.store.pipe( + select(viewerStateViewerModeSelector), + take(1) + ).subscribe(vm => viewerMode = vm) + if (viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) return false + const { rs, spec } = this.getNextUserRegionSelectHandler() || {} if (!!rs) { diff --git a/src/services/state/viewerState.store.helper.ts b/src/services/state/viewerState.store.helper.ts index c636776f3..bf5f69fb8 100644 --- a/src/services/state/viewerState.store.helper.ts +++ b/src/services/state/viewerState.store.helper.ts @@ -22,6 +22,7 @@ import { viewerStateToggleLayer, viewerStateToggleRegionSelect, viewerStateSelectRegionWithIdDeprecated, + viewerStateSetViewerMode, viewerStateDblClickOnViewer, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, @@ -46,6 +47,7 @@ export { viewerStateToggleLayer, viewerStateToggleRegionSelect, viewerStateSelectRegionWithIdDeprecated, + viewerStateSetViewerMode, viewerStateDblClickOnViewer, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, @@ -143,7 +145,7 @@ export class ViewerStateHelperEffect{ private store$: Store<any>, private actions$: Actions ){ - + } } @@ -180,7 +182,7 @@ export function isNewerThan(arr: IHasVersion[], srcObj: IHasId, compObj: IHasId) while (currPreviousId) { it += 1 if (it>100) throw new Error(`iteration excced 100, did you include a loop?`) - + const curr = arr.find(v => v['@version']['@this'] === currPreviousId) if (!curr) throw new Error(`GenNewerVersions error, version id ${currPreviousId} not found`) currPreviousId = curr['@version'][ flag ? '@next' : '@previous' ] @@ -196,8 +198,8 @@ export function isNewerThan(arr: IHasVersion[], srcObj: IHasId, compObj: IHasId) for (const obj of GenNewerVersions(false)) { if (obj['@version']['@this'] === compObj['@id']) { return true - } + } } - + throw new Error(`isNewerThan error, neither srcObj nor compObj exist in array`) } diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index ef175c5b6..f28fc4109 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -10,7 +10,7 @@ import { LoggingService } from 'src/logging'; import { IavRootStoreInterface } from '../stateStore.service'; import { GENERAL_ACTION_TYPES } from '../stateStore.service' import { CLOSE_SIDE_PANEL } from './uiState.store'; -import { +import { viewerStateSetSelectedRegions, viewerStateSetConnectivityRegion, viewerStateSelectParcellation, @@ -24,7 +24,12 @@ import { viewerStateNewViewer } from './viewerState.store.helper'; import { cvtNehubaConfigToNavigationObj } from 'src/state'; -import { actionSelectLandmarks, viewerStateChangeNavigation, viewerStateNehubaLayerchanged } from './viewerState/actions'; +import { + viewerStateChangeNavigation, + viewerStateNehubaLayerchanged, + viewerStateSetViewerMode, + actionSelectLandmarks +} from './viewerState/actions'; import { serialiseParcellationRegion } from "common/util" export interface StateInterface { @@ -34,6 +39,8 @@ export interface StateInterface { parcellationSelected: any | null regionsSelected: any[] + viewerMode: string + landmarksSelected: any[] userLandmarks: IUserLandmark[] @@ -75,6 +82,7 @@ export const defaultState: StateInterface = { fetchedTemplates : [], loadedNgLayers: [], regionsSelected: [], + viewerMode: null, userLandmarks: [], dedicatedView: null, navigation: null, @@ -163,6 +171,12 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part regionsSelected: selectRegions, } } + case SET_VIEWER_MODE: { + return { + ...prevState, + viewerMode: action.payload + } + } case DESELECT_LANDMARKS : { return { ...prevState, @@ -223,7 +237,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part return { ...prevState, overwrittenColorMap: action.payload || '', - } + } default : return prevState } @@ -250,6 +264,10 @@ export const CHANGE_NAVIGATION = viewerStateChangeNavigation.type export const SELECT_PARCELLATION = viewerStateSelectParcellation.type +export const DESELECT_REGIONS = `DESELECT_REGIONS` +export const SELECT_REGIONS_WITH_ID = viewerStateSelectRegionWithIdDeprecated.type +export const SET_VIEWER_MODE = viewerStateSetViewerMode.type +export const SELECT_LANDMARKS = `SELECT_LANDMARKS` export const SELECT_REGIONS = viewerStateSetSelectedRegions.type export const DESELECT_LANDMARKS = `DESELECT_LANDMARKS` export const USER_LANDMARKS = `USER_LANDMARKS` diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts index 7ef58b79f..74d44f9b8 100644 --- a/src/services/state/viewerState/actions.ts +++ b/src/services/state/viewerState/actions.ts @@ -3,7 +3,7 @@ import { IRegion } from './constants' export const viewerStateNewViewer = createAction( `[viewerState] newViewer`, - props<{ + props<{ selectTemplate: any selectParcellation: any navigation?: any @@ -67,7 +67,7 @@ export const viewerStateSelectParcellation = createAction( ) export const viewerStateSelectTemplateWithName = createAction( - `[viewerState] selectTemplateWithName`, + `[viewerState] selectTemplateWithName`, props<{ payload: { name: string } }>() ) @@ -91,6 +91,11 @@ export const viewerStateSelectRegionWithIdDeprecated = createAction( props<{ selectRegionIds: string[] }>() ) +export const viewerStateSetViewerMode = createAction( + `[viewerState] setViewerMode`, + props<{payload: string}>() +) + export const viewerStateDblClickOnViewer = createAction( `[viewerState] dblClickOnViewer`, props<{ payload: { segments: any, landmark: any, userLandmark: any } }>() diff --git a/src/services/state/viewerState/selectors.ts b/src/services/state/viewerState/selectors.ts index 3e13df34b..cb0ac92e8 100644 --- a/src/services/state/viewerState/selectors.ts +++ b/src/services/state/viewerState/selectors.ts @@ -18,7 +18,7 @@ const flattenFetchedTemplatesIntoParcellationsReducer = (acc, curr) => { useTheme: curr['useTheme'] } }) - + return acc.concat( parcelations ) } @@ -91,6 +91,11 @@ export const viewerStateSelectorNavigation = createSelector( viewerState => viewerState['navigation'] ) +export const viewerStateViewerModeSelector = createSelector( + state => state['viewerState'], + viewerState => viewerState['viewerMode'] +) + export const viewerStateGetOverlayingAdditionalParcellations = createSelector( state => state[viewerStateHelperStoreName], state => state['viewerState'], @@ -205,7 +210,7 @@ export const viewerStateSelectedTemplateFullInfoSelector = createSelector( darktheme: (fullTemplateInfo || {}).useTheme === 'dark' } }) - } + } ) export const viewerStateContextedSelectedRegionsSelector = createSelector( diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index a15328676..46ca24930 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -11,7 +11,8 @@ import { AuthService } from "src/auth"; import { IavRootStoreInterface, IDataEntry } from "src/services/stateStore.service"; import { MatDialog, MatDialogConfig, MatDialogRef } from "@angular/material/dialog"; import { MatBottomSheet } from "@angular/material/bottom-sheet"; -import { CONST, QUICKTOUR_DESC } from 'common/constants' +import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' +import {viewerStateSetViewerMode} from "src/services/state/viewerState/actions"; import { IQuickTourData } from "src/ui/quickTour/constrants"; @Component({ @@ -50,6 +51,7 @@ export class TopMenuCmp { public pluginTooltipText: string = `Plugins and Tools` public screenshotTooltipText: string = 'Take screenshot' + public annotateTooltipText: string = 'Start annotating' public quickTourData: IQuickTourData = { description: QUICKTOUR_DESC.TOP_MENU, @@ -90,6 +92,10 @@ export class TopMenuCmp { } } + setAnnotatingMode() { + this.store$.dispatch(viewerStateSetViewerMode({payload: ARIA_LABELS.VIEWER_MODE_ANNOTATING,})) + } + private keyListenerConfigBase = { type: 'keydown', stop: true, diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html index aec870d1a..773d7e82b 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -26,12 +26,6 @@ </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"> @@ -66,10 +60,6 @@ <ng-container *ngTemplateOutlet="pinnedDatasetBtnTmpl"> </ng-container> - <!-- user annotations --> - <ng-container *ngTemplateOutlet="userAnnotationsBtnTmpl"> - </ng-container> - <!-- help one pager --> <ng-container *ngTemplateOutlet="helpBtnTmpl"> </ng-container> @@ -133,22 +123,6 @@ </div> </ng-template> -<!-- User annotations btn --> -<ng-template #userAnnotationsBtnTmpl> - <div class="btnWrapper" - (click)="bottomSheet.open(userAnnotations, {hasBackdrop: false, disableClose: true})" - 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" @@ -177,6 +151,16 @@ Screenshot </span> </button> + <button mat-menu-item + [disabled]="!viewerLoaded" + (click)="setAnnotatingMode()" + [matTooltip]="annotateTooltipText"> + <mat-icon fontSet="fas" fontIcon="fa-pencil-ruler"> + </mat-icon> + <span> + Annotate + </span> + </button> <plugin-banner></plugin-banner> </mat-menu> @@ -286,10 +270,3 @@ </mat-list-item> </mat-list> </ng-template> - - - -<ng-template #userAnnotations> - <user-annotations (close)="bottomSheet.dismiss()" style="max-width: 600px"> - </user-annotations> -</ng-template> diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index d31ef339f..beaaff406 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -19,6 +19,7 @@ import { NehubaModule } from "./nehuba"; import { ThreeSurferModule } from "./threeSurfer"; import { RegionAccordionTooltipTextPipe } from "./util/regionAccordionTooltipText.pipe"; import { ViewerCmp } from "./viewerCmp/viewerCmp.component"; +import {UserAnnotationsModule} from "src/atlasComponents/userAnnotations"; import {QuickTourModule} from "src/ui/quickTour/module"; @NgModule({ @@ -38,6 +39,7 @@ import {QuickTourModule} from "src/ui/quickTour/module"; AtlasCmptConnModule, ComponentsModule, BSFeatureModule, + UserAnnotationsModule, QuickTourModule, ContextMenuModule, ], diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index aa6d2af84..ffffe0d10 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -3,7 +3,15 @@ import { select, Store } from "@ngrx/store"; import { combineLatest, Observable, Subject, Subscription } from "rxjs"; import { distinctUntilChanged, filter, map, startWith } from "rxjs/operators"; import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; -import { viewerStateContextedSelectedRegionsSelector, viewerStateGetOverlayingAdditionalParcellations, viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector, viewerStateStandAloneVolumes } from "src/services/state/viewerState/selectors" +import { + viewerStateContextedSelectedRegionsSelector, + viewerStateGetOverlayingAdditionalParcellations, + viewerStateParcVersionSelector, + viewerStateSelectedParcellationSelector, + viewerStateSelectedTemplateSelector, + viewerStateStandAloneVolumes, + viewerStateViewerModeSelector +} from "src/services/state/viewerState/selectors" import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants' import { ngViewerActionClearView } from "src/services/state/ngViewerState/actions"; import { ngViewerSelectorClearViewEntries } from "src/services/state/ngViewerState/selectors"; @@ -87,7 +95,7 @@ export class ViewerCmp implements OnDestroy { @ViewChild('sideNavFullLeftSwitch', { static: true }) private sidenavLeftSwitch: SwitchDirective - + public quickTourRegionSearch: IQuickTourData = { order: 7, description: QUICKTOUR_DESC.REGION_SEARCH, @@ -127,6 +135,12 @@ export class ViewerCmp implements OnDestroy { map(v => v.length > 0) ) + public viewerMode: string + public viewerMode$ = this.store$.pipe( + select(viewerStateViewerModeSelector), + distinctUntilChanged(), + ) + public useViewer$: Observable<TSupportedViewers | 'notsupported'> = combineLatest([ this.templateSelected$, this.isStandaloneVolumes$, @@ -233,7 +247,7 @@ export class ViewerCmp implements OnDestroy { ngAfterViewInit(){ const cb = (context: TContextArg<'nehuba' | 'threeSurfer'>) => { let hoveredRegions = [] - + if (context.viewerType === 'nehuba') { hoveredRegions = (context as TContextArg<'nehuba'>).payload.nehuba.reduce( (acc, curr) => acc.concat( @@ -256,7 +270,7 @@ export class ViewerCmp implements OnDestroy { if (context.viewerType === 'threeSurfer') { hoveredRegions = (context as TContextArg<'threeSurfer'>).payload._mouseoverRegion } - + return { tmpl: this.viewerStatusCtxMenu, data: { diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 22b8b3f22..352b73b71 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -5,8 +5,39 @@ <layout-floating-container [zIndex]="10"> + + <!-- Annotation mode --> + <div *ngIf="(viewerMode$ | async) === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> + + <mat-drawer-container class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" + [hasBackdrop]="false"> + <mat-drawer #drawer [mode]="'push'" class="pe-all"> + <annotation-list></annotation-list> + </mat-drawer> + <mat-drawer-content class="visible position-relative pe-none"> + <!-- pullable tab top right corner --> + <div iavLayoutFourCornersTopLeft class="d-flex flex-column flex-nowrap w-100"> + + <!-- top left --> + <div class="flex-grow-1 d-flex flex-nowrap mb-2"> + <div (click)="drawer.toggle()" class="tab-toggle-container"> + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: { + matColor: 'primary', + fontIcon: 'fa-list', + tooltip: 'Annotation list'}"> + </ng-container> + </div> + </div> + <annotating-mode #annotationMode> + </annotating-mode> + </div> + + </mat-drawer-content> + </mat-drawer-container> + </div> <!-- top drawer --> <mat-drawer-container + [hidden]="viewerMode$ | async" [iav-switch-initstate]="false" iav-switch #sideNavTopSwitch="iavSwitch" @@ -190,7 +221,7 @@ </mat-drawer> <!-- main-content --> - <mat-drawer-content class="visible position-relative"> + <mat-drawer-content class="visible position-relative" [hidden]="viewerMode$ | async"> <iav-layout-fourcorners [iav-layout-fourcorners-cnr-cntr-ngclass]="{'w-100': true}"> @@ -599,33 +630,33 @@ </ng-template> </ng-template> - <ng-template #tabTmpl_defaultTmpl - let-matColor="matColor" - let-fontIcon="fontIcon" - let-customColor="customColor" - let-customColorDarkmode="customColorDarkmode" - let-tooltip="tooltip"> - <!-- (click)="sideNavMasterSwitch.toggle()" --> - <button mat-raised-button - [attr.aria-label]="ARIA_LABELS.TOGGLE_SIDE_PANEL" - [matTooltip]="tooltip" - class="pe-all tab-toggle" - [ngClass]="{ + +</ng-template> +<ng-template #tabTmpl_defaultTmpl + let-matColor="matColor" + let-fontIcon="fontIcon" + let-customColor="customColor" + let-customColorDarkmode="customColorDarkmode" + let-tooltip="tooltip"> + <!-- (click)="sideNavMasterSwitch.toggle()" --> + <button mat-raised-button + [attr.aria-label]="ARIA_LABELS.TOGGLE_SIDE_PANEL" + [matTooltip]="tooltip" + class="pe-all tab-toggle" + [ngClass]="{ 'darktheme': customColorDarkmode === true, 'lighttheme': customColorDarkmode === false }" - [style.backgroundColor]="customColor" + [style.backgroundColor]="customColor" - [color]="(!customColor && matColor) ? matColor : null"> + [color]="(!customColor && matColor) ? matColor : null"> <span [ngClass]="{'iv-custom-comp text': !!customColor}"> <i class="fas" [ngClass]="fontIcon || 'fa-question'"></i> </span> - </button> - </ng-template> + </button> </ng-template> - <!-- region sidenav tmpl --> <ng-template #sidenavRegionTmpl> @@ -1078,7 +1109,7 @@ <!-- hovered ROIs --> <ng-template [ngIf]="data.metadata.hoveredRegions.length > 0"> <mat-divider></mat-divider> - + <mat-list-item *ngFor="let hoveredR of data.metadata.hoveredRegions"> <span mat-line> {{ hoveredR.displayName || hoveredR.name }} -- GitLab