From d1a20be77aeb2f7b58c598937199462dd8c85053 Mon Sep 17 00:00:00 2001 From: fsdavid <daviti1@mail.com> Date: Tue, 1 Jun 2021 03:06:51 +0200 Subject: [PATCH] User annotation fixes --- package.json | 1 + .../annotationList.component.ts | 121 ++++---- .../annotationList/annotationList.style.css | 10 + .../annotationList.template.html | 81 +++-- .../annotationMessage.component.ts | 21 ++ .../annotationMode.component.ts | 142 ++++++--- .../annotationMode/annotationMode.style.css | 6 - .../annotationMode.template.html | 23 +- .../annotationService.service.ts | 289 ++++++++++++++---- .../editAnnotation.component.ts | 118 ------- .../editAnnotation.template.html | 60 ---- .../groupAnnotationPolygons.pipe.ts | 31 -- src/atlasComponents/userAnnotations/index.ts | 1 + src/atlasComponents/userAnnotations/module.ts | 9 +- .../atlasViewer.apiService.service.ts | 20 +- src/services/state/viewerState.store.ts | 4 +- .../viewerCmp/viewerCmp.component.ts | 2 +- .../viewerCmp/viewerCmp.template.html | 11 +- 18 files changed, 502 insertions(+), 448 deletions(-) create mode 100644 src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component.ts delete mode 100644 src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts delete mode 100644 src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html delete mode 100644 src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts diff --git a/package.json b/package.json index 142c217a9..bea7e83f4 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@types/node": "12.12.39", "export-nehuba": "0.0.12", "hbp-connectivity-component": "^0.3.18", + "jszip": "^3.6.0", "zone.js": "^0.10.2" } } diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index fbc8b0d6f..b963c5908 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -10,61 +10,13 @@ import {Store} from "@ngrx/store"; }) export class AnnotationList { - public editing = -1 - - get annotationsToShow() { - return this.ans.annotations - // .filter(a => this.annotationFilter === 'all' || a.templateName === this.ans.selectedTemplate) - // .filter(a => (a.type !== 'polygon' || +a.id.split('_')[1] === 0) - - // let transformed = [...this.ans.annotations] - // - // for (let i = 0; i<this.ans.annotations.length; i++) { - // if (this.ans.annotations[i].type === 'polygon') { - // const annotationId = this.ans.annotations[i].id.split('_') - // if (!transformed.find(t => t.id === annotationId[0])) { - // const polygonAnnotations = this.ans.annotations.filter(a => a.id.split('_')[0] === annotationId[0] - // && a.id.split('_')[1]) - // - // const polygonPositions = polygonAnnotations.map((a, i) => { - // return i-1 !== polygonAnnotations.length? { - // position: a.position1, - // lines: [ - // {id: a.id, point: 2}, - // {id: polygonAnnotations[i+1], point: 1} - // ] - // } : polygonAnnotations[i].position2 !== polygonAnnotations[0].position1? { - // position: a.position2, - // lines: [ - // {id: a.id, point: 2} - // ] - // } : null - // }) - // - // transformed = transformed.filter(a => a.id.split('_')[0] !== annotationId[0]) - // - // transformed.push({ - // id: annotationId[0], - // name: this.ans.annotations[i].name, - // type: 'polygon', - // annotations: polygonAnnotations, - // positions: polygonPositions, - // annotationVisible: this.ans.annotations[i].annotationVisible, - // templateName: this.ans.annotations[i].templateName - // }) - // } - // } - // } - // return transformed - } - - public identifyer = (index: number, item: any) => item.id + public identifier = (index: number, item: any) => item.id constructor(private store$: Store<any>, public ans: AnnotationService) {} toggleAnnotationVisibility(annotation) { if (annotation.type === 'polygon') { - this.ans.annotations.filter(an => an.id.split('_')[0] === annotation.id.split('_')[0]) + this.ans.pureAnnotationsForViewer.filter(an => an.id.split('_')[0] === annotation.id.split('_')[0]) .forEach(a => this.toggleVisibility(a)) } else { this.toggleVisibility(annotation) @@ -72,21 +24,21 @@ export class AnnotationList { } toggleVisibility(annotation) { - const annotationIndex = this.ans.annotations.findIndex(a => a.id === annotation.id) + const annotationIndex = this.ans.pureAnnotationsForViewer.findIndex(a => a.id === annotation.id) - if (this.ans.annotations[annotationIndex].annotationVisible) { + if (this.ans.pureAnnotationsForViewer[annotationIndex].annotationVisible) { this.ans.removeAnnotationFromViewer(annotation.id) - this.ans.annotations[annotationIndex].annotationVisible = false + this.ans.pureAnnotationsForViewer[annotationIndex].annotationVisible = false } else { - this.ans.addAnnotationOnViewer(this.ans.annotations[annotationIndex]) - this.ans.annotations[annotationIndex].annotationVisible = true + this.ans.addAnnotationOnViewer(this.ans.pureAnnotationsForViewer[annotationIndex]) + this.ans.pureAnnotationsForViewer[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]) + this.ans.pureAnnotationsForViewer.filter(an => an.id.split('_')[0] === annotation.id.split('_')[0]) .forEach(a => this.ans.removeAnnotation(a.id)) } else { this.ans.removeAnnotation(annotation.id) @@ -94,7 +46,6 @@ export class AnnotationList { } navigate(position) { - //ToDo change for real position for all templates position = position.split(',').map(p => +p * 1e6) this.store$.dispatch( viewerStateChangeNavigation({ @@ -106,27 +57,61 @@ export class AnnotationList { ) } - saveAnnotation(annotation) { - if (annotation.position1.split(',').length !== 3 || !annotation.position1.split(',').every(e => !!e) - || ((annotation.position2 - && annotation.position2.split(',').length !== 3) || !annotation.position1.split(',').every(e => !!e))) { - return + saveAnnotation(annotation, singlePolygon = false) { + if (annotation.type !== 'polygon' || singlePolygon) { + annotation.position1 = annotation.position1.replace(/\s/g, '') + annotation.position2 = annotation.position2 && annotation.position2.replace(/\s/g, '') + if (annotation.position1.split(',').length !== 3 || !annotation.position1.split(',').every(e => !!e) + || ((annotation.position2 + && annotation.position2.split(',').length !== 3) || !annotation.position1.split(',').every(e => !!e))) { + return + } else { + annotation.position1 = this.ans.mmToVoxel(annotation.position1.split(',')).join() + annotation.position2 = annotation.position2 && this.ans.mmToVoxel(annotation.position2.split(',')).join() + } + this.ans.saveAnnotation(annotation) + } else { + if (!annotation.name) { + annotation.name = this.ans.giveNameByType('polygon') + } + + const toUpdateFirstAnnotation = this.ans.pureAnnotationsForViewer.find(a => a.id === `${annotation.id}_0`) + toUpdateFirstAnnotation.name = annotation.name + toUpdateFirstAnnotation.description = annotation.description + this.ans.saveAnnotation(toUpdateFirstAnnotation) + + const toUpdate = this.ans.groupedAnnotations.findIndex(a => a.id === annotation.id) + this.ans.groupedAnnotations[toUpdate].name = annotation.name + this.ans.groupedAnnotations[toUpdate].description = annotation.description + this.ans.refreshAnnotationFilter() + } - this.ans.saveAnnotation(annotation) } - savePolygonPosition(position, inputVal) { + savePolygonPosition(id, position, inputVal) { + inputVal = inputVal.replace(/\s/g, '') + if (inputVal.split(',').length !== 3 || !inputVal.split(',').every(e => !!e)) { + return + } else { + inputVal = this.ans.mmToVoxel(inputVal.split(',')).join() + } position.lines.forEach(l => { if (l.point === 2) { - const annotation = this.ans.annotations.find(a => a.id === l.id) + const annotation = this.ans.pureAnnotationsForViewer.find(a => a.id === l.id) annotation.position2 = inputVal - this.saveAnnotation(annotation) + this.saveAnnotation(annotation, true) } else { - const annotation = this.ans.annotations.find(a => a.id === l.id) + const annotation = this.ans.pureAnnotationsForViewer.find(a => a.id === l.id) annotation.position1 = inputVal - this.saveAnnotation(annotation) + this.saveAnnotation(annotation, true) } }) } + submitInput(e, area) { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + area.blur() + } + } + } diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css b/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css index 5d6cede95..858e3ab3d 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css @@ -20,3 +20,13 @@ input:focus, textarea:focus { border-bottom: 2px solid; } +.annotation-name-input { + width: 180px; +} + +:host-context([darktheme="true"]) .hovering-header { + background-color: #737373; +} +:host-context([darktheme="false"]) .hovering-header { + background-color: rgb(245, 245, 245); +} diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index 731f821a8..c8fd56c4f 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -4,25 +4,45 @@ <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"> + aria-label="Import annotation" + matTooltip="Import JSON" + [matMenuTriggerFor]="importMenu"> <i class="fas fa-file-import"></i> </button> + <input type="file" #importInput (change)="$event.target.files.length && ans.importFile( $event.target.files[0])" hidden/> + <input type="file" #importInputSands (change)="$event.target.files.length && ans.importFile($event.target.files[0], true)" hidden/> + <mat-menu #importMenu="matMenu"> + <button mat-menu-item (click)="importInputSands.click()"> + SANDS format + </button> + <button mat-menu-item (click)="importInput.click()"> + Siibra explorer format + </button> + </mat-menu> + <button class="mr-1 ml-1" mat-icon-button - disabled - aria-label="Export user annotation" - matTooltip="Export"> + aria-label="Export annotation" + matTooltip="Export" + [matMenuTriggerFor]="exportAllMenu"> <i class="fas fa-file-export"></i> </button> + <mat-menu #exportAllMenu="matMenu"> + <button mat-menu-item (click)="ans.exportAnnotations(ans.finalAnnotationList, true)"> + SANDS format + </button> + <button mat-menu-item (click)="ans.exportAnnotations(ans.finalAnnotationList)"> + Siibra explorer format + </button> + </mat-menu> + </div> <div class="d-flex flex-column"> <small [ngClass]="[ans.annotationFilter !== 'all'? 'inactive-filter' : '']" - class="cursor-pointer" (click)="ans.changeAnnotationFilter('all')"> + class="cursor-pointer" (click)="ans.refreshAnnotationFilter('all')"> All landmarks </small> <small [ngClass]="[ans.annotationFilter !== 'current'? 'inactive-filter' : '']" - class="cursor-pointer" (click)="ans.changeAnnotationFilter('current')"> + class="cursor-pointer" (click)="ans.refreshAnnotationFilter('current')"> Current template </small> </div> @@ -32,9 +52,8 @@ <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 #expansion hideToggle *ngFor="let annotation of ans.displayAnnotations; let i = index; trackBy: identifyer"> - <mat-expansion-panel-header> + <mat-expansion-panel #expansion hideToggle *ngFor="let annotation of ans.finalAnnotationList; let i = index; trackBy: identifier"> + <mat-expansion-panel-header [ngClass]="ans.hoverAnnotation?.id.includes(annotation.id) && 'hovering-header'"> <mat-panel-title> <small class="cursor-pointer mr-3 ml-1 d-flex align-items-center" @@ -44,11 +63,14 @@ <i class="fas far fa-check-circle" *ngIf="annotation.annotationVisible; else notVisible"></i> <ng-template #notVisible><i class="far fa-circle"></i></ng-template> </small> - <input class="font-italic outline-none color-inherit" + <input #annotationNameRef class="font-italic outline-none color-inherit annotation-name-input" (click)="expansion.expanded? $event.stopPropagation() : null" [(ngModel)]="annotation.name" + (keydown.escape)="$event.stopPropagation(); annotationNameRef.blur();" + (keydown.space)="$event.stopPropagation();" + (keydown.enter)="$event.stopPropagation(); annotationNameRef.blur();" + (keydown)="submitInput($event, annotationNameRef)" (keyup)="this.saveAnnotation(annotation)"/> -<!-- <span class="mr-2">{{annotation.name}}</span>--> </mat-panel-title> <mat-panel-description class="w-100 d-flex align-items-center justify-content-end" @@ -66,11 +88,15 @@ <div> - <small class="mt-2 mb-2">{{annotation.templateName}}</small> + <small class="mt-2 mb-2">{{annotation.template.name}}</small> <div *ngIf="annotation.type !== 'polygon'" class="w-100 d-flex align-items-center justify-content-between mt-2"> <input class="w-100 font-italic outline-none color-inherit" + #position1Ref [(ngModel)]="annotation.position1" + (keyup.escape)="position1Ref.blur(); $event.stopPropagation();" + (keyup.enter)="position1Ref.blur(); $event.stopPropagation();" + (keydown)="submitInput($event, position1Ref)" (keyup)="this.saveAnnotation(annotation)"/> <small class="d-flex align-items-center">mm <i class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" (click)="navigate(annotation.position1)"></i></small> </div> @@ -78,6 +104,10 @@ <div *ngIf="annotation.type !== 'polygon' && annotation.position2" class="w-100 d-flex align-items-center justify-content-between mb-2"> <input class="w-100 font-italic d-flex align-items-center outline-none color-inherit" [(ngModel)]="annotation.position2" + #position2Ref + (keyup.escape)="position2Ref.blur(); $event.stopPropagation();" + (keyup.enter)="position2Ref.blur(); $event.stopPropagation();" + (keydown)="submitInput($event, position2Ref)" (keyup)="this.saveAnnotation(annotation)"/> <small class="d-flex align-items-center">mm <i class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" (click)="navigate(annotation.position2)"></i></small> </div> @@ -87,7 +117,10 @@ <input class="w-100 font-italic d-flex align-items-center outline-none color-inherit" [value]="position?.position" #polygonPositionInput - (keyup)="this.savePolygonPosition(position, polygonPositionInput.value)"/> + (keyup.escape)="polygonPositionInput.blur(); $event.stopPropagation();" + (keyup.enter)="polygonPositionInput.blur(); $event.stopPropagation();" + (keydown)="submitInput($event, polygonPositionInput)" + (keyup)="this.savePolygonPosition(annotation.id, position, polygonPositionInput.value)"/> <small class="d-flex align-items-center">mm <i class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" (click)="navigate(position?.position)"></i></small> </div> </div> @@ -97,10 +130,13 @@ <textarea class="w-100 outline-none color-inherit" placeholder="Add description" cdkTextareaAutosize + #descriptionTextAreaRef #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="1" cdkAutosizeMaxRows="5" [(ngModel)]="annotation.description" + (keyup.escape)="descriptionTextAreaRef.blur(); $event.stopPropagation();" + (keydown)="submitInput($event, descriptionTextAreaRef)" (keyup)="this.saveAnnotation(annotation)"></textarea> </div> @@ -108,10 +144,19 @@ <div class="d-flex align-items-center justify-content-end w-100"> <button class="mr-1 ml-1" mat-icon-button - disabled - aria-label="Export single annotation"> + aria-label="Export single annotation" + matTooltip="Export" + [matMenuTriggerFor]="exportSingleMenu"> <i class="fas fa-file-export"></i> </button> + <mat-menu #exportSingleMenu="matMenu"> + <button mat-menu-item (click)="ans.exportAnnotations([annotation], true)"> + SANDS format + </button> + <button mat-menu-item (click)="ans.exportAnnotations([annotation])"> + Siibra explorer format + </button> + </mat-menu> <button class="mr-1 ml-1" mat-icon-button aria-label="Delete annotation" (click)="removeAnnotation(annotation)"> @@ -119,8 +164,6 @@ </button> </div> - <edit-annotation [annotation]="annotation" *ngIf="editing === i" (finished)="editing = -1"> - </edit-annotation> </div> </mat-expansion-panel> diff --git a/src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component.ts b/src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component.ts new file mode 100644 index 000000000..cf1c198e0 --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component.ts @@ -0,0 +1,21 @@ +import {Component} from "@angular/core"; +import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; + +@Component({ + selector: 'annotation-message', + template: `<mat-card class="pe-all position-absolute d-flex align-items-center annotation-mode-message-panel"> + <mat-panel-title>Annotation mode</mat-panel-title> + <button mat-icon-button + color="warn" + (click)="ans.disable()" + type="button" + class="mb-2 mt-2" + matTooltip="Exit annotation mode"> + <i class="fas fa-times"></i> + </button> + </mat-card>`, + styles: [`.annotation-mode-message-panel {height: 30px;top: 20px;right: 0;}`] +}) +export class AnnotationMessage { + constructor(public ans: AnnotationService) {} +} diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index 6605fb2c1..0803ac641 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -1,16 +1,19 @@ import {Component, HostListener, Inject, OnDestroy, OnInit, Optional} from "@angular/core"; import {select, Store} from "@ngrx/store"; import {ARIA_LABELS, CONST} from "common/constants"; -import {Observable, Subscription} from "rxjs"; +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 { + viewerStateGetSelectedAtlas, viewerStateNavigationStateSelector, - viewerStateSelectedTemplateSelector, - viewerStateViewerModeSelector + viewerStateSelectedTemplateSelector, viewerStateViewerModeSelector, } from "src/services/state/viewerState/selectors"; import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; +import {NehubaViewerUnit} from "src/viewerModule/nehuba"; +import {NEHUBA_INSTANCE_INJTKN} from "src/viewerModule/nehuba/util"; +import {CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor} from "src/util"; @Component({ selector: 'annotating-mode', @@ -26,11 +29,11 @@ export class AnnotationMode implements OnInit, OnDestroy { public editingAnnotationId: string public selecting = 'position1' - public mousePos - public navState: any + public mousePos: any[] + public navState: any = {} private hoverAnnotation$: Observable<{id: string, partIndex: number}> - public hoverAnnotation: {id: string, partIndex: number} + // public hoverAnnotation: {id: string, partIndex: number} private onDestroyCb: Function[] = [] public subscriptions: Subscription[] = [] @@ -38,9 +41,15 @@ export class AnnotationMode implements OnInit, OnDestroy { return this.injectedViewer || (window as any).viewer } - get nehubaViewer() { - return (window as any).nehubaViewer + private _nehubaViewer: NehubaViewerUnit; + + get nehubaViewer(){ + return this._nehubaViewer + } + set nehubaViewer(v: NehubaViewerUnit) { + this._nehubaViewer = v } + get interactiveViewer() { return (window as any).interactiveViewer } @@ -49,7 +58,33 @@ export class AnnotationMode implements OnInit, OnDestroy { private store$: Store<any>, public ans: AnnotationService, @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer, - ) {} + @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) nehubaViewer$: Observable<NehubaViewerUnit>, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor + ) { + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + const onMouseClick = this.onMouseClick.bind(this) + register(onMouseClick) + this.onDestroyCb.push(() => deregister(onMouseClick)) + } + + if (nehubaViewer$) { + this.subscriptions.push( + nehubaViewer$.subscribe( + viewer => this.nehubaViewer = viewer + ) + ) + } + } + + onMouseClick(e: any) { + let viewerMode: string + this.store$.pipe( + select(viewerStateViewerModeSelector), + take(1) + ).subscribe(vm => viewerMode = vm) + if (viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) return false + } ngOnInit(): void { // Load annotation layer on init @@ -79,7 +114,7 @@ export class AnnotationMode implements OnInit, OnDestroy { ) this.subscriptions.push(this.hoverAnnotation$.subscribe(ha => { - this.hoverAnnotation = ha + this.ans.hoverAnnotation = ha })) const mouseDown$ = this.interactiveViewer.viewerHandle.mouseEvent.pipe( @@ -119,25 +154,24 @@ export class AnnotationMode implements OnInit, OnDestroy { let hoveringName: string let hoveringPosition1: [] let hoveringPosition2: [] - let draggingStartPosition: [] + let draggingStartPosition: any[] let hoveringPolygonAnnotations: any[] let dragging = false this.subscriptions.push( mouseDown$.pipe( tap(() => { - hovering = this.hoverAnnotation + hovering = this.ans.hoverAnnotation if (hovering) { draggingStartPosition = this.mousePos - const hoveringAnnotation = this.ans.annotations.find(a => a.id === this.hoverAnnotation.id) + const hoveringAnnotation = this.ans.pureAnnotationsForViewer.find(a => a.id === this.ans.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 + hoveringType = this.ans.pureAnnotationsForViewer.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]) + hoveringPolygonAnnotations = this.ans.pureAnnotationsForViewer.filter(a => a.id.split('_')[0] === hovering.id.split('_')[0]) } - hoveringType = this.ans.annotations.find(a => a.id === hovering.id)?.type - hoveringName = this.ans.annotations.find(a => a.id === hovering.id)?.name + hoveringName = this.ans.pureAnnotationsForViewer.find(a => a.id === hovering.id)?.name } } }), @@ -156,7 +190,12 @@ export class AnnotationMode implements OnInit, OnDestroy { if (hovering && this.selecting !== 'position2') { dragging = true // keep navigation while dragging - this.interactiveViewer.viewerHandle.setNavigationLoc(this.navState) + // this.interactiveViewer.viewerHandle.setNavigationLoc(this.navState) + this.nehubaViewer.setNavigationState({ + position: this.navState.position, + perspectiveOrientation: this.navState.perspectiveOrientation, + positionReal: true + }) // make changes to annotations by type // - when line is hovered move full annotation - // - when line point is hovered move only point @@ -167,7 +206,6 @@ export class AnnotationMode implements OnInit, OnDestroy { this.ans.saveAnnotation({id: hovering.id, position1: this.mousePos.join(), name: hoveringName, type: hoveringType}, false, true) } 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(), @@ -205,46 +243,57 @@ export class AnnotationMode implements OnInit, OnDestroy { 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(), - name: hoveringName, + name: pa.name, + description: pa.description, type: pa.type }, false, true) }) } else { let samePos1: any[] let samePos2: any[] + const name = hoveringPolygonAnnotations[0].name + const description = hoveringPolygonAnnotations[0].description if (hovering.partIndex === 2) { samePos1 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position1 === hoveringPosition2.join()) samePos2 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position2 === hoveringPosition2.join()) this.ans.saveAnnotation({id: hovering.id, position1: hoveringPosition1.join(), position2: this.mousePos.join(), - name: hoveringName, - type: hoveringType}, false, true) + name, + description, + type: hoveringType}, true, false) } else if (hovering.partIndex === 1) { samePos1 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position1 === hoveringPosition1.join()) samePos2 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position2 === hoveringPosition1.join()) this.ans.saveAnnotation({id: hovering.id, position1: this.mousePos.join(), position2: hoveringPosition2.join(), - name: hoveringName, - type: hoveringType}, false, true) + name, + description, + type: hoveringType}, true, false) } samePos1.forEach(a => { this.ans.saveAnnotation({id: a.id, position1: this.mousePos.join(), position2: a.position2, - name: hoveringName, - type: a.type}, false, true) + name, + description, + type: a.type}, true, false) }) samePos2.forEach(a => { this.ans.saveAnnotation({id: a.id, position1: a.position1, position2: this.mousePos.join(), - name: hoveringName, - type: a.type}, false, true) + name, + description, + type: a.type}, true, false) }) + + this.ans.addPolygonsToGroupedAnnotations( + this.ans.pureAnnotationsForViewer.filter(a => a.id.split('_')[0] === hovering.id.split('_')[0]) + ) } } } @@ -254,10 +303,9 @@ export class AnnotationMode implements OnInit, OnDestroy { ) this.subscriptions.push( - this.nehubaViewer.mousePosition.inVoxels + this.nehubaViewer.mousePosInVoxel$ .subscribe(floatArr => { this.mousePos = floatArr && floatArr - if (this.selecting === 'position1' && this.mousePos) { this.position1 = this.mousePos.join() } else if (this.selecting === 'position2' && this.mousePos) { @@ -315,22 +363,33 @@ export class AnnotationMode implements OnInit, OnDestroy { this.store$.pipe( select(viewerStateNavigationStateSelector), ).subscribe(nav => { - this.navState = nav.position + this.navState.position = nav.position + this.navState.perspectiveOrientation = nav.perspectiveOrientation }), + this.store$.pipe( + select(viewerStateGetSelectedAtlas) + ).subscribe(atlas => { + this.ans.selectedAtlas = {name: atlas.name, id: atlas['@id']} + }), + this.store$.pipe( select(viewerStateSelectedTemplateSelector), take(1) ).subscribe(tmpl => { - this.ans.selectedTemplate = tmpl.name - this.ans.darkTheme = tmpl.useTheme === 'dark' + this.ans.selectedTemplate = { + name: tmpl.name, + id: tmpl['@id'] + } + this.ans.voxelSize = this.ans.getVoxelFromSpace(tmpl.fullId) // 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.displayAnnotations = this.ans.annotations.filter(a => a.type !== 'polygon') - this.ans.addPolygonsToDisplayAnnotations(this.ans.annotations.filter(a => a.type === 'polygon')) - this.ans.annotations.filter(a => a.annotationVisible && a.templateName === this.ans.selectedTemplate) + this.ans.pureAnnotationsForViewer = JSON.parse(annotationsString).filter(a => a.atlas.id === this.ans.selectedAtlas.id) + this.ans.groupedAnnotations = this.ans.pureAnnotationsForViewer.filter(a => a.type !== 'polygon') + this.ans.addPolygonsToGroupedAnnotations(this.ans.pureAnnotationsForViewer.filter(a => a.type === 'polygon')) + this.ans.refreshAnnotationFilter() + this.ans.pureAnnotationsForViewer.filter(a => a.annotationVisible && a.template.id === this.ans.selectedTemplate.id) .forEach(a => { this.ans.addAnnotationOnViewer(a) }) @@ -369,20 +428,19 @@ export class AnnotationMode implements OnInit, OnDestroy { 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 (this.ans.annotationTypes[this.selectedType].type === 'remove' && this.ans.hoverAnnotation) { + const hoveringAnnotationObj = this.ans.pureAnnotationsForViewer.find(a => a.id === this.ans.hoverAnnotation.id) if (hoveringAnnotationObj.type === 'polygon') { - const polygonAnnotations = this.ans.annotations.filter(a => a.id.split('_')[0] === hoveringAnnotationObj.id.split('_')[0]) + const polygonAnnotations = this.ans.pureAnnotationsForViewer.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) + this.ans.removeAnnotation(this.ans.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') { diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css index 02e6997f4..89e110ea0 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.style.css @@ -5,9 +5,3 @@ .annotation-toolbar-content { margin-bottom: 20px; } -.annotation-mode-message-panel { - height: 30px; - top: 20px; - right: 0; - background-color: rgba(66,65,65, 0.3); -} diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html index ed046f989..a67bdb554 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html @@ -1,9 +1,9 @@ <div class="pe-all d-flex flex-column justify-content-content annotation-toolbar"> - <div class="d-flex flex-column flex-grow-0 panel-default annotation-toolbar-content" - [ngStyle]="{backgroundColor: ans.darkTheme? '#424242' : 'white', - color: ans.darkTheme? 'antiquewhite' : '#424242'}"> - <button *ngFor="let type of ans.annotationTypes; let i = index" + <mat-card class="d-flex flex-column flex-grow-0 panel-default annotation-toolbar-content p-0"> + + <div *ngFor="let type of ans.annotationTypes; let i = index; let last = last"> + <button mat-icon-button type="button" (click)="selectAnnotationType(i)" @@ -12,6 +12,8 @@ [color]="selectedType === i? 'primary' : 'secondary'"> <i [ngClass]="type.class"></i> </button> + <mat-divider *ngIf="last || type.action !== ans.annotationTypes[i+1].action"></mat-divider> + </div> <button mat-icon-button (click)="ans.disable()" @@ -21,16 +23,5 @@ color="warn"> <i class="fas fa-times"></i> </button> - </div> -</div> - -<div class="pe-all position-absolute d-flex align-items-center p-3 annotation-mode-message-panel"> - <span>Annotation mode</span> - <button mat-icon-button - (click)="ans.disable()" - type="button" - class="mb-2 mt-2" - matTooltip="Exit annotation mode"> - <i class="fas fa-times"></i> - </button> + </mat-card> </div> diff --git a/src/atlasComponents/userAnnotations/annotationService.service.ts b/src/atlasComponents/userAnnotations/annotationService.service.ts index 6e361e0b1..7dc46abea 100644 --- a/src/atlasComponents/userAnnotations/annotationService.service.ts +++ b/src/atlasComponents/userAnnotations/annotationService.service.ts @@ -1,11 +1,14 @@ -import {ApplicationRef, ChangeDetectorRef, Inject, Injectable, OnDestroy, Optional} from "@angular/core"; -import {CONST} from "common/constants"; +import {Inject, Injectable, OnDestroy, Optional} from "@angular/core"; +import {CONST, ARIA_LABELS} 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 {select, Store} from "@ngrx/store"; import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; -import {TemplateCoordinatesTransformation} from "src/services/templateCoordinatesTransformation.service"; +import {CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor} from "src/util"; +import {viewerStateViewerModeSelector} from "src/services/state/viewerState/selectors"; +import {take} from "rxjs/operators"; +import * as JSZip from 'jszip'; const USER_ANNOTATION_LAYER_SPEC = { "type": "annotation", @@ -16,26 +19,34 @@ const USER_ANNOTATION_LAYER_SPEC = { } @Injectable() -export class AnnotationService implements OnDestroy { +export class AnnotationService { + + // Annotations to display on viewer + public pureAnnotationsForViewer = [] + + // Grouped annotations for user + public groupedAnnotations = [] + + // Filtered annotations with converted voxed to mm + public finalAnnotationList = [] - public annotations = [] - public displayAnnotations = [] public addedLayer: any public ellipsoidMinRadius = 0.5 public annotationFilter: 'all' | 'current' = 'current' - public selectedTemplate: string - public darkTheme = false - public subscriptions: Subscription[] = [] + public selectedTemplate: {name, id} + public voxelSize: any[] = [] + public selectedAtlas: {name, id} + public hoverAnnotation: {id: string, partIndex: number} 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'}, + {name: 'Cursor', class: 'fas fa-mouse-pointer', type: 'move', action: 'none'}, + {name: 'Point', class: 'fas fa-circle', type: 'singleCoordinate', action: 'paint'}, + {name: 'Line', class: 'fas fa-slash', type: 'doubleCoordinate', action: 'paint'}, + {name: 'Polygon', class: 'fas fa-draw-polygon', type: 'polygon', action: 'paint'}, + // {name: 'Bounding box', class: 'far fa-square', type: 'doubleCoordinate', action: 'paint'}, + // {name: 'Ellipsoid', class: 'fas fa-bullseye', type: 'doubleCoordinate', action: 'paint'}, + {name: 'Remove', class: 'fas fa-trash', type: 'remove', action: 'remove'}, ] private get viewer(){ @@ -43,7 +54,8 @@ export class AnnotationService implements OnDestroy { } constructor(private store$: Store<any>, - @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer) {} + @Optional() @Inject(VIEWER_INJECTION_TOKEN) private injectedViewer + ) {} public disable = () => { this.store$.dispatch(viewerStateSetViewerMode({payload: null})) @@ -71,12 +83,14 @@ export class AnnotationService implements OnDestroy { } saveAnnotation({id = null, - position1 = null, //this.position1, - position2 = null, //this.position2, - type = null, //this.annotationTypes[this.selectedType].name, - description = null, + position1 = null, + position2 = null, name = null, - templateName = null, + description = null, + type = null, + circular = null, + atlas = null, + template = null, } = {}, store = true, backup = false) { let annotation = { id: id || getUuid(), @@ -85,15 +99,17 @@ export class AnnotationService implements OnDestroy { name, position1, position2, - templateName: templateName || this.selectedTemplate, + circular, + template: template || this.selectedTemplate, + atlas: this.selectedAtlas, type: type.toLowerCase() } - const foundIndex = this.annotations.findIndex(x => x.id === annotation.id) + const foundIndex = this.pureAnnotationsForViewer.findIndex(x => x.id === annotation.id) if (foundIndex >= 0) { annotation = { - ...this.annotations[foundIndex], + ...this.pureAnnotationsForViewer[foundIndex], ...annotation } } @@ -115,7 +131,7 @@ export class AnnotationService implements OnDestroy { public storeBackup() { if (this.saveEditList.length) { if (this.saveEditList[0].type === 'polygon') { - this.addPolygonsToDisplayAnnotations(this.saveEditList) + this.addPolygonsToGroupedAnnotations(this.saveEditList) } this.saveEditList.forEach(a => this.storeAnnotation(a)) this.saveEditList = [] @@ -123,7 +139,7 @@ export class AnnotationService implements OnDestroy { } giveNameByType(type) { - const pointAnnotationNumber = this.annotations + const pointAnnotationNumber = this.pureAnnotationsForViewer .filter(a => a.name && a.name.startsWith(type) && (+a.name.split(type)[1])) .map(a => +a.name.split(type)[1]) @@ -134,30 +150,31 @@ export class AnnotationService implements OnDestroy { storeAnnotation(annotation) { // give names by type + number - if (!annotation.name) { + if (!annotation.name && annotation.type !== 'polygon') { annotation.name = this.giveNameByType(annotation.type) } - const foundIndex = this.annotations.findIndex(x => x.id === annotation.id) + const foundIndex = this.pureAnnotationsForViewer.findIndex(x => x.id === annotation.id) if (foundIndex >= 0) { annotation = { - ...this.annotations[foundIndex], + ...this.pureAnnotationsForViewer[foundIndex], ...annotation } - this.annotations[foundIndex] = annotation + this.pureAnnotationsForViewer[foundIndex] = annotation } else { - this.annotations.push(annotation) + this.pureAnnotationsForViewer.push(annotation) } if(annotation.type !== 'polygon') { - const foundIndex = this.displayAnnotations.findIndex(x => x.id === annotation.id) + const foundIndex = this.groupedAnnotations.findIndex(x => x.id === annotation.id) if (foundIndex >= 0) { - this.displayAnnotations[foundIndex] = annotation + this.groupedAnnotations[foundIndex] = annotation } else { - this.displayAnnotations.push(annotation) + this.groupedAnnotations.push(annotation) } + this.refreshAnnotationFilter() } this.storeToLocalStorage() @@ -167,12 +184,6 @@ export class AnnotationService implements OnDestroy { 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(',') : '' @@ -199,16 +210,14 @@ export class AnnotationService implements OnDestroy { removeAnnotation(id) { this.removeAnnotationFromViewer(id) - this.annotations = this.annotations.filter(a => a.id !== id) - this.displayAnnotations = this.annotations.filter(a => a.id !== id) + this.pureAnnotationsForViewer = this.pureAnnotationsForViewer.filter(a => a.id !== id) + this.groupedAnnotations = this.groupedAnnotations.filter(a => a.id !== id.split('_')[0]) + this.refreshAnnotationFilter() this.storeToLocalStorage() } storeToLocalStorage() { - // ToDo temporary solution - because impure pipe stucks - - - window.localStorage.setItem(CONST.USER_ANNOTATION_STORE_KEY, JSON.stringify(this.annotations)) + window.localStorage.setItem(CONST.USER_ANNOTATION_STORE_KEY, JSON.stringify(this.pureAnnotationsForViewer)) } removeAnnotationFromViewer(id) { @@ -220,7 +229,7 @@ export class AnnotationService implements OnDestroy { } } - addPolygonsToDisplayAnnotations(annotations) { + addPolygonsToGroupedAnnotations(annotations) { let transformed = [...annotations] for (let i = 0; i<annotations.length; i++) { @@ -260,34 +269,196 @@ export class AnnotationService implements OnDestroy { transformed.push({ id: annotationId[0], name: annotations[i].name, + description: annotations[i].description, type: 'polygon', annotations: polygonAnnotations, positions: polygonPositions, + circular: polygonAnnotations[0].position1 === [...polygonAnnotations].pop().position2, annotationVisible: annotations[i].annotationVisible, - templateName: annotations[i].templateName + template: annotations[i].template, + atlas: this.selectedAtlas }) } } transformed.forEach(tr=> { - const foundIndex = this.displayAnnotations.findIndex(x => x.id === tr.id) + const foundIndex = this.groupedAnnotations.findIndex(x => x.id === tr.id) if (foundIndex >= 0) { - this.displayAnnotations[foundIndex] = tr + this.groupedAnnotations[foundIndex] = tr } else { - this.displayAnnotations.push(tr) + this.groupedAnnotations.push(tr) } + this.refreshAnnotationFilter() }) } - changeAnnotationFilter(filter) { - this.annotationFilter = filter - this.displayAnnotations = this.displayAnnotations - .filter(a => this.annotationFilter === 'all' || a.templateName === this.selectedTemplate) + refreshAnnotationFilter(filter = null) { + if (filter) {this.annotationFilter = filter} + this.finalAnnotationList = this.groupedAnnotations + // Filter all/current template + .filter(a => this.annotationFilter === 'all' || a.template.id === this.selectedTemplate.id) + // convert to MM + .map(a => { + if (a.positions) { + a.positions = a.positions.map(p => { + return { + ...p, + position: this.voxelToMM(p.position.split(',')).join() + } + }) + } else { + a.position1 = this.voxelToMM(a.position1.split(',')).join() + a.position2 = a.position2 && this.voxelToMM(a.position2.split(',')).join() + } + + a.dimension = 'mm' + return a + }) + // clear polygonAnnotations + .map(a => { + if (a.annotations) { + a.annotations = a.annotations.map(an => { + return { + id: an.id, + position1: an.position1, + position2: an.position2 + } + }) + } + + return a + }) + } + + voxelToMM(r): any[] { + return r.map((r, i) => parseFloat((+r*this.voxelSize[i]/1e6).toFixed(3))) + } + + mmToVoxel(mm): any[] { + return mm.map((m, i) => +m*1e6/this.voxelSize[i]) + } + + getVoxelFromSpace = (spaceId: string) => { + return IAV_VOXEL_SIZES_NM[spaceId] } - ngOnDestroy(){ - this.subscriptions.forEach(s => s.unsubscribe()) + getSandsObj(position, template) { + return { + coordinates: { + value: position.split(',').map(p => +p), + unit: 'mm' + }, + coordinateSpace: { + fullName: template.name, + versionIdentifier: template.id + } + } } + + exportAnnotations(annotations: any[], sands = false) { + const zip = new JSZip() + const zipFileName = `annotation - ${annotations[0].atlas.name}.zip` + + + if (sands) { + annotations.forEach(a => { + zip.folder(a.name) + if (a.positions) { + a.positions.forEach(p => { + zip.folder(a.name).file(`${p.position}.json`, JSON.stringify(this.getSandsObj(p.position, a.template))) + }) + } else { + zip.folder(a.name).file(`${a.position1}.json`, JSON.stringify(this.getSandsObj(a.position1, a.template))) + if (a.position2) zip.folder(a.name).file(`${a.position1}.json`, JSON.stringify(this.getSandsObj(a.position2, a.template))) + } + }) + } else { + annotations.forEach(a => { + const fileName = a.name.replace(/[\\/:*?"<>|]/g, "").trim() + zip.file(`${fileName}.json`, JSON.stringify(a)) + }) + } + + + zip.file("README.txt", + `The annotation has been extracted from the atlas: "${annotations.map(a => a.atlas.name).filter((v, i, a) => a.indexOf(v) === i).join()}" + and template(s): "${annotations.map(a => a.template.name).filter((v, i, a) => a.indexOf(v) === i).join()}"`) + zip.generateAsync({ + type: "base64" + }).then(content => { + const link = document.createElement('a') + link.href = 'data:application/zip;base64,' + content + link.download = zipFileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + }) + } + + + importFile(file, sands = false) { + const fileReader = new FileReader() + fileReader.readAsText(file, "UTF-8") + fileReader.onload = () => { + const fileData = JSON.parse(fileReader.result.toString()) + + if (sands) { + if (!fileData.coordinates || !fileData.coordinates.value || fileData.coordinates.value.length !== 3 + || !fileData.coordinateSpace || !fileData.coordinateSpace.fullName || !fileData.coordinateSpace.versionIdentifier) { + return + } + const position1 = this.mmToVoxel(fileData.coordinates.value).join() + this.saveAnnotation({position1, + template: { + name: fileData.coordinateSpace.fullName, + id: fileData.coordinateSpace.versionIdentifier + }, + type: 'point'}) + } else { + const {id, name, description, type, + atlas, template, positions, annotations} = fileData + + if (!id || !(fileData.position1 || positions) || !type) { + return + } + + if (fileData.type !== 'polygon') { + const position1 = this.mmToVoxel(fileData.position1.split(',')).join() + const position2 = fileData.position2 && this.mmToVoxel(fileData.position2.split(',')).join() + + this.saveAnnotation({position1, position2, + name, description, type, atlas, template + }) + } else if (annotations) { + annotations.forEach(a => { + this.saveAnnotation({ + id: a.id, + name, description, + position1: a.position1, + position2: a.position2, + type: 'polygon'}) + }) + this.groupedAnnotations.push(fileData) + this.refreshAnnotationFilter() + } + + } + } + fileReader.onerror = (error) => { + console.warn(error) + } + } + +} + + + +export const IAV_VOXEL_SIZES_NM = { + 'minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9': [25000, 25000, 25000], + 'minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8': [39062.5, 39062.5, 39062.5], + 'minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588': [21166.666015625, 20000, 21166.666015625], + 'minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992': [1000000, 1000000, 1000000,], + 'minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2': [1000000, 1000000, 1000000] } diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts deleted file mode 100644 index e758630b3..000000000 --- a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.component.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - Component, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output -} from "@angular/core"; -import { FormBuilder, FormGroup, Validators } from "@angular/forms"; -import { animate, style, transition, trigger } from "@angular/animations"; -import { Subscription } from "rxjs"; -import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; -import {viewerStateChangeNavigation} from "src/services/state/viewerState/actions"; -import {Store} from "@ngrx/store"; - -@Component({ - selector: 'edit-annotation', - templateUrl: './editAnnotation.template.html', - animations: [ - trigger( - 'enterAnimation', [ - transition(':enter', [ - style({transform: 'translateY(100%)', opacity: 0}), - animate('100ms', style({transform: 'translateY(0)', opacity: 1})) - ]), - ] - ) - ], -}) -export class EditAnnotationComponent implements OnInit, OnDestroy { - - @Input() annotation: any - - @Output() finished: EventEmitter<any> = new EventEmitter() - - public annotationForm: FormGroup - - public subscriptions: Subscription[] = [] - - constructor( - private formBuilder: FormBuilder, - public ans: AnnotationService, - private store$: Store<any>, - ) { - this.annotationForm = this.formBuilder.group({ - id: [{value: 'null'}], - position1: [{value: ''}], - position2: [{value: ''}], - name: [{value: ''}, { - validators: [Validators.maxLength(200)] - }], - description: [{value: ''}, { - validators: [Validators.maxLength(1000)] - }], - templateName: [{value: ''}], - type: [{value: ''}], - annotationVisible: [{value: true}] - }) - } - - ngOnInit() { - 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) - } - - - submitForm() { - if (this.annotationForm.valid) { - if (this.annotationForm.controls.position1.value.split(',').length !== 3 || - (this.annotationForm.controls.position2.value - && this.annotationForm.controls.position2.value.split(',').length !== 3)) { - return - } - this.ans.saveAnnotation(this.annotationForm.value) - this.cancelEditing() - } - } - - cancelEditing() { - this.finished.emit() - this.resetForm() - } - - resetForm() { - this.annotationForm.reset() - this.annotationForm.markAsPristine() - this.annotationForm.markAsUntouched() - - this.annotationForm.controls.annotationVisible.setValue(true) - Object.keys(this.annotationForm.controls).forEach(key => { - this.annotationForm.get(key).setErrors(null) - }) - } - - ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()) - } - - navigate(position) { - //ToDo change for real position for all templates - position = position.split(',').map(p => +p * 1e6) - this.store$.dispatch( - viewerStateChangeNavigation({ - navigation: { - position, - positionReal: true - }, - }) - ) - } - -} diff --git a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html b/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html deleted file mode 100644 index d423d5415..000000000 --- a/src/atlasComponents/userAnnotations/editAnnotation/editAnnotation.template.html +++ /dev/null @@ -1,60 +0,0 @@ -<form class="annotation-form d-flex flex-column align-items-center" - autocomplete="off" - [formGroup]="annotationForm" - (keydown.enter)="!annotationForm.invalid? submitForm() : null" - (ngSubmit)="submitForm()"> - - - <div class="w-100" - [@enterAnimation]> - <mat-form-field class="w-100"> - <mat-label>Name</mat-label> - <input name="name" - formControlName="name" - (keyup)="submitForm()" - matInput> - </mat-form-field> - <div class="d-flex flex-column align-items-center position-relative w-100"> - <mat-form-field class="w-100 annotation-editing-body"> - <mat-label *ngIf="annotation.type !== 'ellipsoid'">Position {{annotation.position2 && ' 1'}} (vox)</mat-label> - <mat-label *ngIf="annotation.type === 'ellipsoid'">Center (vox)</mat-label> - <div> - <input type="text" name="position1" - placeholder="0,0,0mm" - class="pr-4" - formControlName="position1" - (keyup)="submitForm()" - matInput> - </div> - </mat-form-field> - <small class="position-absolute" style="bottom: 20px; right: 0">mm <i class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" (click)="navigate(annotation.position1)"></i></small> - </div> - - <div class="d-flex flex-column align-items-center w-100 position-relative" *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,0mm" - formControlName="position2" - (keyup)="submitForm()" - matInput> - </mat-form-field> - <small class="position-absolute" style="bottom: 20px; right: 0">mm <i class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" (click)="navigate(annotation.position2)"></i></small> - - </div> - - <mat-form-field class="w-100 annotation-editing-body"> - <mat-label>description</mat-label> - <textarea [matTextareaAutosize]="true" - [matAutosizeMinRows]="1" - [matAutosizeMaxRows]="5" - name="description" - (keyup)="submitForm()" - 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/groupAnnotationPolygons.pipe.ts b/src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts deleted file mode 100644 index 0b85117b8..000000000 --- a/src/atlasComponents/userAnnotations/groupAnnotationPolygons.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 6da0cda7d..d5c0a00d0 100644 --- a/src/atlasComponents/userAnnotations/index.ts +++ b/src/atlasComponents/userAnnotations/index.ts @@ -1,3 +1,4 @@ export { AnnotationMode } from "./annotationMode/annotationMode.component"; export { AnnotationList } from "./annotationList/annotationList.component"; +export { AnnotationMessage } from "./annotationMessage/annotationMessage.component"; export { UserAnnotationsModule } from "./module"; diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index e54f72cee..c9e2aabec 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -3,12 +3,11 @@ import {CommonModule} from "@angular/common"; import {DatabrowserModule} from "src/atlasComponents/databrowserModule"; import {AngularMaterialModule} from "src/ui/sharedModules/angularMaterial.module"; 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"; +import {AnnotationMessage} from "src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component"; @NgModule({ imports: [ @@ -20,17 +19,17 @@ import {GroupAnnotationPolygons} from "src/atlasComponents/userAnnotations/group AngularMaterialModule, ], declarations: [ - EditAnnotationComponent, AnnotationMode, AnnotationList, - GroupAnnotationPolygons + AnnotationMessage ], providers: [ AnnotationService ], exports: [ AnnotationMode, - AnnotationList + AnnotationList, + AnnotationMessage ] }) diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index 0833ddd51..7fd34675d 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -87,16 +87,7 @@ 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 - + private onMouseClick(ev: any) { const { rs, spec } = this.getNextUserRegionSelectHandler() || {} if (!!rs) { @@ -125,11 +116,10 @@ export class AtlasViewerAPIServices implements OnDestroy{ mousePositionReal = floatArr && Array.from(floatArr).map((val: number) => val / 1e6) }) } - rs({ + return rs({ type: spec.type, payload: mousePositionReal }) - return false } /** @@ -139,11 +129,10 @@ export class AtlasViewerAPIServices implements OnDestroy{ if (!!moSegments && Array.isArray(moSegments) && moSegments.length > 0) { this.popUserRegionSelectHandler() - rs({ + return rs({ type: spec.type, payload: moSegments }) - return false } } } else { @@ -153,8 +142,7 @@ export class AtlasViewerAPIServices implements OnDestroy{ */ if (!!moSegments && Array.isArray(moSegments) && moSegments.length > 0) { this.popUserRegionSelectHandler() - rs(moSegments[0]) - return false + return rs(moSegments[0]) } } } diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index f28fc4109..ed2422c70 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -171,7 +171,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part regionsSelected: selectRegions, } } - case SET_VIEWER_MODE: { + case viewerStateSetViewerMode.type: { return { ...prevState, viewerMode: action.payload @@ -266,7 +266,7 @@ 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 SET_VIEWER_MODE = viewerStateSetViewerMode.type export const SELECT_LANDMARKS = `SELECT_LANDMARKS` export const SELECT_REGIONS = viewerStateSetSelectedRegions.type export const DESELECT_LANDMARKS = `DESELECT_LANDMARKS` diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index ffffe0d10..f10fa7340 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -136,7 +136,7 @@ export class ViewerCmp implements OnDestroy { ) public viewerMode: string - public viewerMode$ = this.store$.pipe( + public hideUi$ = this.store$.pipe( select(viewerStateViewerModeSelector), distinctUntilChanged(), ) diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 352b73b71..6fa739408 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -7,16 +7,16 @@ <!-- Annotation mode --> - <div *ngIf="(viewerMode$ | async) === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> + <div *ngIf="(hideUi$ | 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"> + <mat-drawer #drawer [mode]="'push'" [disableClose]="true" 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"> + <div class="d-flex flex-column flex-nowrap w-100"> <!-- top left --> <div class="flex-grow-1 d-flex flex-nowrap mb-2"> @@ -34,10 +34,11 @@ </mat-drawer-content> </mat-drawer-container> + <annotation-message></annotation-message> </div> <!-- top drawer --> <mat-drawer-container - [hidden]="viewerMode$ | async" + [hidden]="hideUi$ | async" [iav-switch-initstate]="false" iav-switch #sideNavTopSwitch="iavSwitch" @@ -221,7 +222,7 @@ </mat-drawer> <!-- main-content --> - <mat-drawer-content class="visible position-relative" [hidden]="viewerMode$ | async"> + <mat-drawer-content class="visible position-relative" [hidden]="hideUi$ | async"> <iav-layout-fourcorners [iav-layout-fourcorners-cnr-cntr-ngclass]="{'w-100': true}"> -- GitLab