diff --git a/common/constants.js b/common/constants.js index 0b589f5d8ea3f7b47badb8e1eb1a80c9a4754661..e9693fcf1c8a21e001c6bd539597e8cf7746110f 100644 --- a/common/constants.js +++ b/common/constants.js @@ -57,7 +57,19 @@ ADDITIONAL_VOLUME_CONTROL: 'Additional volumes control', //Viewer mode - VIEWER_MODE_ANNOTATING: 'annotating' + VIEWER_MODE_ANNOTATING: 'annotating', + + // Annotations + USER_ANNOTATION_VIEWER: 'user annotations viewer', + USER_ANNOTATION_HEADER: 'user annotations header', + USER_ANNOTATION_LIST: 'user annotations footer', + USER_ANNOTATION_IMPORT: 'user annotations import', + USER_ANNOTATION_EXPORT: 'user annotations export', + USER_ANNOTATION_EXPORT_SINGLE: 'user annotations export single', + USER_ANNOTATION_HIDE: 'user annotations hide', + USER_ANNOTATION_DELETE: 'user annotations delete', + + } exports.IDS = { @@ -82,11 +94,6 @@ 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/annotationInterfaces.ts b/src/atlasComponents/userAnnotations/annotationInterfaces.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d631452fa5d9eb02168ee5a078faf7148f651f5 --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationInterfaces.ts @@ -0,0 +1,48 @@ + +export interface ViewerAnnotation { + id: string + position1: number[] + position2: number[] + name: string + description: string + type: string + circular: boolean + atlas: {name: string, id: string} + template: {name: string, id: string} + annotationVisible: boolean +} + +export interface GroupedAnnotation { + id: string + position1?: number[] + position2?: number[] + annotations?: PolygonAnnotations[] + positions?: PolygonPositions[] + dimension?: string + + name: string + description: string + type: string + circular?: boolean + atlas: {name: string, id: string} + template: {name: string, id: string} + annotationVisible: boolean +} + +export interface PolygonAnnotations { + id: string + position1: number[] + position2: number[] +} + +export interface PolygonPositions { + position: number[] + lines: {id: string, point: number}[] +} + +export interface AnnotationType { + name: string + class: string + type: string + action: string +} diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index b963c590821d9d1d32975d31d91f4aedfe848d00..23c6557a043660af2298a50a31792900d462224b 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -2,6 +2,7 @@ import {Component} from "@angular/core"; import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; import {viewerStateChangeNavigation} from "src/services/state/viewerState/actions"; import {Store} from "@ngrx/store"; +import {ARIA_LABELS} from "common/constants"; @Component({ selector: 'annotation-list', @@ -10,12 +11,18 @@ import {Store} from "@ngrx/store"; }) export class AnnotationList { + public ARIA_LABELS = ARIA_LABELS public identifier = (index: number, item: any) => item.id constructor(private store$: Store<any>, public ans: AnnotationService) {} toggleAnnotationVisibility(annotation) { if (annotation.type === 'polygon') { + // ToDo Change when mainList will be groupedAnnotations + const annotationIndex = this.ans.groupedAnnotations.findIndex(a => a.id === annotation.id) + this.ans.groupedAnnotations[annotationIndex].annotationVisible = !this.ans.groupedAnnotations[annotationIndex].annotationVisible + this.ans.refreshFinalAnnotationList() + this.ans.pureAnnotationsForViewer.filter(an => an.id.split('_')[0] === annotation.id.split('_')[0]) .forEach(a => this.toggleVisibility(a)) } else { @@ -23,7 +30,7 @@ export class AnnotationList { } } - toggleVisibility(annotation) { + private toggleVisibility = (annotation) => { const annotationIndex = this.ans.pureAnnotationsForViewer.findIndex(a => a.id === annotation.id) if (this.ans.pureAnnotationsForViewer[annotationIndex].annotationVisible) { @@ -45,73 +52,86 @@ export class AnnotationList { } } - navigate(position) { - position = position.split(',').map(p => +p * 1e6) - this.store$.dispatch( - viewerStateChangeNavigation({ - navigation: { - position, - positionReal: true - }, - }) - ) + navigate(position: number[]) { + // Convert to nm before navigate + position = position.map(p => +p * 1e6) + + if (position && position.length === 3) { + this.store$.dispatch( + viewerStateChangeNavigation({ + navigation: { + position, + positionReal: true + }, + }) + ) + } } - 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))) { + saveAnnotation(annotation) { + if (annotation.type !== 'polygon') { + + // Convert to Number Array + if (annotation.position1 && (typeof annotation.position1 === 'string')) { + annotation.position1 = this.positionToNumberArray(annotation.position1) + } + if (annotation.position2 && (typeof annotation.position2 === 'string')) { + annotation.position2 = this.positionToNumberArray(annotation.position2) + } + + // Return if positions are valid + if (annotation.position1.length !== 3 || !annotation.position1.every(e => !isNaN(e)) + || (annotation.position2 && (annotation.position2.length !== 3 || !annotation.position2.every(e => !isNaN(e))))) { return } else { - annotation.position1 = this.ans.mmToVoxel(annotation.position1.split(',')).join() - annotation.position2 = annotation.position2 && this.ans.mmToVoxel(annotation.position2.split(',')).join() + // Convert to Voxel + annotation.position1 = this.ans.mmToVoxel(annotation.position1) + annotation.position2 = annotation.position2 && this.ans.mmToVoxel(annotation.position2) } + // Save annotation this.ans.saveAnnotation(annotation) } else { - if (!annotation.name) { - annotation.name = this.ans.giveNameByType('polygon') - } + + // if (!annotation.name) { + // annotation.name = this.ans.generateNameByType('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) + //ToDo Change when main list will be groupedAnnotations 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.refreshFinalAnnotationList() } } + positionToNumberArray(position) { + return position.split(',').map(n => parseFloat(n)) + } + savePolygonPosition(id, position, inputVal) { - inputVal = inputVal.replace(/\s/g, '') - if (inputVal.split(',').length !== 3 || !inputVal.split(',').every(e => !!e)) { + inputVal = this.positionToNumberArray(inputVal) + + if (inputVal.length !== 3 || !inputVal.every(e => !isNaN(e))) { return } else { - inputVal = this.ans.mmToVoxel(inputVal.split(',')).join() + inputVal = this.ans.mmToVoxel(inputVal) } position.lines.forEach(l => { if (l.point === 2) { const annotation = this.ans.pureAnnotationsForViewer.find(a => a.id === l.id) annotation.position2 = inputVal - this.saveAnnotation(annotation, true) + this.ans.saveAnnotation(annotation, true) } else { const annotation = this.ans.pureAnnotationsForViewer.find(a => a.id === l.id) annotation.position1 = inputVal - this.saveAnnotation(annotation, true) + this.ans.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 858e3ab3d6d5664377f6aa62ff310ac7566bf276..1f57a5f2d7a2155d5fb5c398d7fb079ee9337258 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.style.css @@ -1,29 +1,7 @@ -.annotation-content { - max-width: 300px; - min-width: 300px; -} - .inactive-filter { color: #bababa; } -.selecting-height { - max-height: 100px; -} - -input, textarea { - background: none; - border: none; -} - -input:focus, textarea:focus { - border-bottom: 2px solid; -} - -.annotation-name-input { - width: 180px; -} - :host-context([darktheme="true"]) .hovering-header { background-color: #737373; } diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index c8fd56c4ff37d22775c4a8e22f5a45a4b00ac706..e977ff16b072e3b88f143b259b64a65e366e2e1c 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -1,16 +1,16 @@ -<div aria-label="user annotations viewer" class="annotation-content w-100"> +<div [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_VIEWER" class="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 class="overflow-hidden mt-3 mr-2 ml-2"> + <div [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_HEADER" class="d-flex justify-content-between"> <div> <button class="mr-1 ml-1" mat-icon-button - aria-label="Import annotation" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_IMPORT" 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/> + <input type="file" #importInput [import-annotations]="{sands: false}" hidden/> + <input type="file" #importInputSands [import-annotations]="{sands: true}" hidden/> <mat-menu #importMenu="matMenu"> <button mat-menu-item (click)="importInputSands.click()"> SANDS format @@ -21,28 +21,28 @@ </mat-menu> <button class="mr-1 ml-1" mat-icon-button - aria-label="Export annotation" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT" 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)"> + <button mat-menu-item [export-annotations]="{annotations: ans.finalAnnotationList, sands: true}"> SANDS format </button> - <button mat-menu-item (click)="ans.exportAnnotations(ans.finalAnnotationList)"> + <button mat-menu-item [export-annotations]="{annotations: ans.finalAnnotationList, sands: false}"> 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.refreshAnnotationFilter('all')"> + <small [ngClass]="[ans.annotationFilter !== 'all'? 'text-muted' : '']" + class="cursor-pointer" (click)="ans.refreshFinalAnnotationList('all')"> All landmarks </small> - <small [ngClass]="[ans.annotationFilter !== 'current'? 'inactive-filter' : '']" - class="cursor-pointer" (click)="ans.refreshAnnotationFilter('current')"> + <small [ngClass]="[ans.annotationFilter !== 'current'? 'text-muted' : '']" + class="cursor-pointer" (click)="ans.refreshFinalAnnotationList('current')"> Current template </small> </div> @@ -50,27 +50,22 @@ </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 #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-accordion [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_LIST" + class="h-100 d-flex flex-column overflow-auto"> + <mat-expansion-panel hideToggle + *ngFor="let annotation of ans.finalAnnotationList; let i = index; trackBy: identifier"> + <mat-expansion-panel-header + [ngClass]="ans.hoverAnnotation?.id.includes(annotation.id) && 'highlight'"> <mat-panel-title> <small class="cursor-pointer mr-3 ml-1 d-flex align-items-center" - aria-label="Hide annotation" - [matTooltip]="annotation.annotationVisible? 'Hide' : 'Show'" - (click)="$event.stopPropagation(); toggleAnnotationVisibility(annotation)"> + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_HIDE" + [matTooltip]="annotation.annotationVisible? 'Hide' : 'Show'" + (click)="$event.stopPropagation(); 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> </small> - <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)"/> + <div class="color-inherit">{{annotation.name}}</div> </mat-panel-title> <mat-panel-description class="w-100 d-flex align-items-center justify-content-end" @@ -83,82 +78,102 @@ </mat-panel-description> - </mat-expansion-panel-header> - <div> - <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)" + <div class="d-flex flex-column"> + <mat-form-field class="w-100"> + <input matInput class="font-italic color-inherit" + placeholder="Name" + [(ngModel)]="annotation.name" + (keydown.space)="$event.stopPropagation();" + annotation-list-key-listener (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> + </mat-form-field> + + <p class="mt-2 mb-4">{{annotation.template.name}}</p> + + <div *ngIf="annotation.type !== 'polygon'" + class="w-100 d-flex align-items-center justify-content-between mt-2"> + <mat-form-field class="w-100"> + <input matInput class="w-100 font-italic color-inherit flex-grow-1" + placeholder="Position 1" + [ngModel]="(annotation.position1 | coordinateInputText)" + (ngModelChange)="annotation.position1 = positionToNumberArray($event)" + annotation-list-key-listener + (keyup)="this.saveAnnotation(annotation)"/> + </mat-form-field> + <small class="d-flex align-items-center flex-grow-0"> <i + class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" + (click)="navigate(annotation.position1)"></i></small> </div> - <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 *ngIf="annotation.type !== 'polygon' && annotation.position2" + class="w-100 d-flex align-items-center justify-content-between mb-2"> + <mat-form-field class="w-100"> + <input matInput + placeholder="Position 2" + class="w-100 font-italic d-flex align-items-center color-inherit" + [ngModel]="(annotation.position2 | coordinateInputText)" + (ngModelChange)="annotation.position2 = positionToNumberArray($event)" + annotation-list-key-listener + (keyup)="this.saveAnnotation(annotation)"/> + </mat-form-field> + <small class="d-flex align-items-center"> <i + class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" + (click)="navigate(annotation.position2)"></i></small> </div> <div *ngIf="annotation.type === 'polygon'"> - <div *ngFor="let position of annotation.positions" 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" - [value]="position?.position" - #polygonPositionInput - (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 *ngFor="let position of annotation.positions; let positionIndex = index" + class="w-100 d-flex align-items-center justify-content-between mb-2"> + <mat-form-field class="w-100"> + <input matInput + [placeholder]="'Position' + (+positionIndex + 1)" + class="w-100 font-italic d-flex align-items-center color-inherit" + [value]="(position?.position | coordinateInputText)" + #polygonPositionInput + annotation-list-key-listener + (keyup)="this.savePolygonPosition(annotation.id, position, polygonPositionInput.value)"/> + </mat-form-field> + <small class="d-flex align-items-center"> <i + class="fas fa-map-marked-alt mr-2 ml-2 cursor-pointer" + (click)="navigate(position?.position)"></i></small> </div> </div> <div class="w-100"> - <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> + <mat-form-field class="w-100"> + <textarea matInput class="w-100 color-inherit" + placeholder="Description" + cdkTextareaAutosize + #autosize="cdkTextareaAutosize" + cdkAutosizeMinRows="1" cdkAutosizeMaxRows="5" + [(ngModel)]="annotation.description" + annotation-list-key-listener + (keyup)="this.saveAnnotation(annotation)"> + </textarea></mat-form-field> </div> - <div class="d-flex align-items-center justify-content-end w-100"> <button class="mr-1 ml-1" mat-icon-button - aria-label="Export single annotation" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" 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)"> + <button mat-menu-item [export-annotations]="{annotations: [annotation], sands: true}"> SANDS format </button> - <button mat-menu-item (click)="ans.exportAnnotations([annotation])"> + <button mat-menu-item [export-annotations]="{annotations: [annotation], sands: false}"> Siibra explorer format </button> </mat-menu> <button class="mr-1 ml-1" mat-icon-button - aria-label="Delete annotation" + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_DELETE" (click)="removeAnnotation(annotation)"> <i class="fas fa-trash"></i> </button> diff --git a/src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts b/src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..99e09c8526f6ee1d04cbb484905ced845ae9ac7c --- /dev/null +++ b/src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts @@ -0,0 +1,8 @@ +import {Pipe, PipeTransform} from "@angular/core"; + +@Pipe({ name: 'coordinateInputText'}) +export class CoordinateInputTextPipe implements PipeTransform { + transform(coordinate: number[]) { + return coordinate.map(c => `${c.toFixed(3) }mm`).join(', ') + } +} diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index 6b70b7516a5dd21c036fc1dc42c3acd20c6eb51f..9913f392863ffc173e79488ad4b6003309a976dc 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -25,12 +25,12 @@ export class AnnotationMode implements OnInit, OnDestroy { public moduleAnnotationTypes: {instance: { name: string, iconClass: string }, onClick: Function} [] = [] public selectedType = 0 - public position1: string - public position2: string + public position1: number[] + public position2: number[] public editingAnnotationId: string public selecting = 'position1' - public mousePos: any[] + public mousePos: number[] public navState: any = {} private hoverAnnotation$: Observable<{id: string, partIndex: number}> @@ -154,8 +154,8 @@ export class AnnotationMode implements OnInit, OnDestroy { let hovering: any let hoveringType: string let hoveringName: string - let hoveringPosition1: [] - let hoveringPosition2: [] + let hoveringPosition1: number[] + let hoveringPosition2: number[] let draggingStartPosition: any[] let hoveringPolygonAnnotations: any[] let dragging = false @@ -167,8 +167,8 @@ export class AnnotationMode implements OnInit, OnDestroy { draggingStartPosition = this.mousePos 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 + hoveringPosition1 = hoveringAnnotation.position1 + hoveringPosition2 = hoveringAnnotation.position2 ? hoveringAnnotation.position2 : null hoveringType = this.ans.pureAnnotationsForViewer.find(a => a.id === hovering.id)?.type if (hoveringAnnotation.type === 'polygon') { hoveringPolygonAnnotations = this.ans.pureAnnotationsForViewer.filter(a => a.id.split('_')[0] === hovering.id.split('_')[0]) @@ -205,37 +205,37 @@ export class AnnotationMode implements OnInit, OnDestroy { const dragRange = this.mousePos.map((mp, i) => mp - +draggingStartPosition[i]) if (hoveringType === 'point') { - this.ans.saveAnnotation({id: hovering.id, position1: this.mousePos.join(), name: hoveringName, type: hoveringType}, false, true) + this.ans.saveAnnotation({id: hovering.id, position1: this.mousePos, 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(), + position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]), + position2: hoveringPosition2.map((hp, i) => +hp + dragRange[i]), name: hoveringName, type: hoveringType}, false, true) } else if (hovering.partIndex === 1) { this.ans.saveAnnotation({id: hovering.id, - position1: this.mousePos.join(), - position2: hoveringPosition2.join(), + position1: this.mousePos, + position2: hoveringPosition2, name: hoveringName, type: hoveringType}, false, true) } else if (hovering.partIndex === 2) { this.ans.saveAnnotation({id: hovering.id, - position1: hoveringPosition1.join(), - position2: this.mousePos.join(), + position1: hoveringPosition1, + position2: this.mousePos, name: hoveringName, type: hoveringType}, false, true) } } 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(), + position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]), + position2: hoveringPosition2.map((hp, i) => +hp + dragRange[i]), name: hoveringName, type: hoveringType}, false, true) } else if (hoveringType === 'ellipsoid') { this.ans.saveAnnotation({id: hovering.id, - position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]).join(), - position2: hoveringPosition2.join(), + position1: hoveringPosition1.map((hp, i) => +hp + dragRange[i]), + position2: hoveringPosition2, name: hoveringName, type: hoveringType}, false, true) } else if (hoveringType === 'polygon') { @@ -243,8 +243,8 @@ export class AnnotationMode implements OnInit, OnDestroy { 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(), + position1: pa.position1.map((hp, i) => +hp + dragRange[i]), + position2: pa.position2.map((hp, i) => +hp + dragRange[i]), name: pa.name, description: pa.description, type: pa.type @@ -256,20 +256,20 @@ export class AnnotationMode implements OnInit, OnDestroy { 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()) + samePos1 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position1.join() === hoveringPosition2.join()) + samePos2 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position2.join() === hoveringPosition2.join()) this.ans.saveAnnotation({id: hovering.id, - position1: hoveringPosition1.join(), - position2: this.mousePos.join(), + position1: hoveringPosition1, + position2: this.mousePos, 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()) + samePos1 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position1.join() === hoveringPosition1.join()) + samePos2 = hoveringPolygonAnnotations.filter(hp => hp.id !== hovering.id && hp.position2.join() === hoveringPosition1.join()) this.ans.saveAnnotation({id: hovering.id, - position1: this.mousePos.join(), - position2: hoveringPosition2.join(), + position1: this.mousePos, + position2: hoveringPosition2, name, description, type: hoveringType}, true, false) @@ -277,7 +277,7 @@ export class AnnotationMode implements OnInit, OnDestroy { } samePos1.forEach(a => { this.ans.saveAnnotation({id: a.id, - position1: this.mousePos.join(), + position1: this.mousePos, position2: a.position2, name, description, @@ -287,7 +287,7 @@ export class AnnotationMode implements OnInit, OnDestroy { samePos2.forEach(a => { this.ans.saveAnnotation({id: a.id, position1: a.position1, - position2: this.mousePos.join(), + position2: this.mousePos, name, description, type: a.type}, true, false) @@ -309,14 +309,12 @@ export class AnnotationMode implements OnInit, OnDestroy { .subscribe(floatArr => { this.mousePos = floatArr && floatArr if (this.selecting === 'position1' && this.mousePos) { - this.position1 = this.mousePos.join() + this.position1 = this.mousePos } else if (this.selecting === 'position2' && this.mousePos) { if (this.ans.annotationTypes[this.selectedType].name === 'Ellipsoid') { - this.position2 = [ - this.ans.getRadii(this.position1.split(','), this.mousePos), - ].join() + this.position2 = this.ans.getRadii(this.position1, this.mousePos) } else { - this.position2 = this.mousePos.join() + this.position2 = this.mousePos } if (this.position1 @@ -356,6 +354,7 @@ export class AnnotationMode implements OnInit, OnDestroy { } this.ans.storeBackup() + this.changeToDefaultTool() this.editingAnnotationId = null this.selecting = 'position1' @@ -384,18 +383,9 @@ export class AnnotationMode implements OnInit, OnDestroy { } this.ans.voxelSize = this.ans.getVoxelFromSpace(tmpl.fullId) + this.ans.loadAnnotationsOnInit() // 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.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) - }) - } + }) ) @@ -438,17 +428,19 @@ export class AnnotationMode implements OnInit, OnDestroy { } else { this.ans.removeAnnotation(this.ans.hoverAnnotation.id) } + this.changeToDefaultTool() } // 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, + this.ans.saveAnnotation({name: this.ans.generateNameByType(this.ans.annotationTypes[this.selectedType].name), + position1: this.position1, type: this.ans.annotationTypes[this.selectedType].name}) + this.changeToDefaultTool() } 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) { if (this.ans.annotationTypes[this.selectedType].type === 'polygon') { this.ans.saveAnnotation({id: this.editingAnnotationId, @@ -460,9 +452,11 @@ export class AnnotationMode implements OnInit, OnDestroy { this.editingAnnotationId = splitEditingAnnotationId[0] + '_' + (+splitEditingAnnotationId[1]+1) } else { this.ans.saveAnnotation({id: this.editingAnnotationId, + name: this.ans.generateNameByType(this.ans.annotationTypes[this.selectedType].name), position1: this.position1, position2: this.position2, type: this.ans.annotationTypes[this.selectedType].name}) + this.changeToDefaultTool() this.editingAnnotationId = null this.selecting = 'position1' } @@ -470,6 +464,10 @@ export class AnnotationMode implements OnInit, OnDestroy { } } + changeToDefaultTool() { + this.selectedType = 0 + } + public selectAnnotationType = (typeIndex) => { this.selectedType = typeIndex this.editingAnnotationId = null diff --git a/src/atlasComponents/userAnnotations/annotationService.service.ts b/src/atlasComponents/userAnnotations/annotationService.service.ts index 9390907c56df96715c64401b92a9b34b2b3c48a8..4f4e25739abb619b89e3ee44b7b45b73c2d4dc0a 100644 --- a/src/atlasComponents/userAnnotations/annotationService.service.ts +++ b/src/atlasComponents/userAnnotations/annotationService.service.ts @@ -6,11 +6,15 @@ import {Store} from "@ngrx/store"; import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; import * as JSZip from 'jszip'; import { Observable } from "rxjs"; +import {AnnotationType, GroupedAnnotation, ViewerAnnotation} from "src/atlasComponents/userAnnotations/annotationInterfaces"; + +const USER_ANNOTATION_LAYER_NAME = 'user_annotations' +const USER_ANNOTATION_STORE_KEY = `user_landmarks_demo_2` const USER_ANNOTATION_LAYER_SPEC = { "type": "annotation", "tool": "annotateBoundingBox", - "name": CONST.USER_ANNOTATION_LAYER_NAME, + "name": USER_ANNOTATION_LAYER_NAME, "annotationColor": "#ffee00", "annotations": [], } @@ -21,13 +25,13 @@ export class AnnotationService { public moduleAnnotationTypes: {instance: {name: string, iconClass: string, toolSelected$: Observable<boolean>}, onClick: Function}[] = [] // Annotations to display on viewer - public pureAnnotationsForViewer = [] + public pureAnnotationsForViewer: ViewerAnnotation[] = [] // Grouped annotations for user - public groupedAnnotations = [] + public groupedAnnotations: GroupedAnnotation[] = [] // Filtered annotations with converted voxed to mm - public finalAnnotationList = [] + public finalAnnotationList: GroupedAnnotation[] = [] public addedLayer: any public ellipsoidMinRadius = 0.5 @@ -38,7 +42,7 @@ export class AnnotationService { public selectedAtlas: {name, id} public hoverAnnotation: {id: string, partIndex: number} - public annotationTypes = [ + public annotationTypes: AnnotationType[] = [ {name: 'Cursor', class: 'fas fa-mouse-pointer', type: 'move', action: 'none'}, {name: 'Point', class: 'fas fa-circle', type: 'singleCoordinate', action: 'paint'}, {name: 'Line', class: 'fas fa-slash', type: 'doubleCoordinate', action: 'paint'}, @@ -66,17 +70,32 @@ export class AnnotationService { } const layer = this.viewer.layerSpecification.getLayer( - CONST.USER_ANNOTATION_LAYER_NAME, + USER_ANNOTATION_LAYER_NAME, USER_ANNOTATION_LAYER_SPEC ) this.addedLayer = this.viewer.layerManager.addManagedLayer(layer) + } + loadAnnotationsOnInit() { + const annotationsString = window.localStorage.getItem(USER_ANNOTATION_STORE_KEY) + const annotationList = JSON.parse(annotationsString) + if (annotationList && annotationList.length) { + this.pureAnnotationsForViewer = annotationList.filter(a => a.atlas.id === this.selectedAtlas.id) + + this.groupedAnnotations = this.pureAnnotationsForViewer.filter(a => a.type !== 'polygon') + this.addPolygonsToGroupedAnnotations(this.pureAnnotationsForViewer.filter(a => a.type === 'polygon')) + this.refreshFinalAnnotationList() + this.pureAnnotationsForViewer.filter(a => a.annotationVisible && a.template.id === this.selectedTemplate.id) + .forEach(a => { + this.addAnnotationOnViewer(a) + }) + } } - getRadii(a, b) { - const returnArray = [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])] + getRadii(a, b): number[] { + const returnArray: number[] = [Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]), Math.abs(b[2] - a[2])] .map(n => n === 0? this.ellipsoidMinRadius : n) return returnArray } @@ -137,7 +156,7 @@ export class AnnotationService { } } - giveNameByType(type) { + generateNameByType(type) { const pointAnnotationNumber = this.pureAnnotationsForViewer .filter(a => a.name && a.name.startsWith(type) && (+a.name.split(type)[1])) .map(a => +a.name.split(type)[1]) @@ -149,9 +168,9 @@ export class AnnotationService { storeAnnotation(annotation) { // give names by type + number - if (!annotation.name && annotation.type !== 'polygon') { - annotation.name = this.giveNameByType(annotation.type) - } + // if (!annotation.name && annotation.type !== 'polygon') { + // annotation.name = this.generateNameByType(annotation.type) + // } const foundIndex = this.pureAnnotationsForViewer.findIndex(x => x.id === annotation.id) @@ -173,31 +192,28 @@ export class AnnotationService { } else { this.groupedAnnotations.push(annotation) } - this.refreshAnnotationFilter() + this.refreshFinalAnnotationList() } this.storeToLocalStorage() } addAnnotationOnViewer(annotation) { - const annotationLayer = this.viewer.layerManager.getLayerByName(CONST.USER_ANNOTATION_LAYER_NAME).layer + const annotationLayer = this.viewer.layerManager.getLayerByName(USER_ANNOTATION_LAYER_NAME).layer const annotations = annotationLayer.localAnnotations.toJSON() - 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, + point: annotation.type === 'point'? annotation.position1 : null, pointA: annotation.type === 'line' || annotation.type === 'bounding box' || annotation.type === 'polygon'? - position1Voxel : null, + annotation.position1 : null, pointB: annotation.type === 'line' || annotation.type === 'bounding box' || annotation.type === 'polygon'? - position2Voxel : null, + annotation.position2 : null, center: annotation.type === 'ellipsoid'? - position1Voxel : null, + annotation.position1 : null, radii: annotation.type === 'ellipsoid'? - position2Voxel : null, + annotation.position2 : null, type: annotation.type === 'bounding box'? 'axis_aligned_bounding_box' : annotation.type === 'polygon'? 'line' : annotation.type.toUpperCase() @@ -211,16 +227,16 @@ export class AnnotationService { this.removeAnnotationFromViewer(id) this.pureAnnotationsForViewer = this.pureAnnotationsForViewer.filter(a => a.id !== id) this.groupedAnnotations = this.groupedAnnotations.filter(a => a.id !== id.split('_')[0]) - this.refreshAnnotationFilter() + this.refreshFinalAnnotationList() this.storeToLocalStorage() } storeToLocalStorage() { - window.localStorage.setItem(CONST.USER_ANNOTATION_STORE_KEY, JSON.stringify(this.pureAnnotationsForViewer)) + window.localStorage.setItem(USER_ANNOTATION_STORE_KEY, JSON.stringify(this.pureAnnotationsForViewer)) } removeAnnotationFromViewer(id) { - const annotationLayer = this.viewer.layerManager.getLayerByName(CONST.USER_ANNOTATION_LAYER_NAME)?.layer + 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) @@ -237,6 +253,19 @@ export class AnnotationService { if (!transformed.find(t => t.id === annotationId[0])) { const polygonAnnotations = annotations.filter(a => a.id.split('_')[0] === annotationId[0] && a.id.split('_')[1]) + // clear polygonAnnotations + .map(a => { + if (a.annotations) { + a.annotations = a.annotations.map(an => { + return { + id: an.id, + position1: an.position1, + position2: an.position2 + } + }) + } + return a + }) const polygonPositions = polygonAnnotations.map((a, index) => { return (index+1) !== polygonAnnotations.length? { @@ -245,7 +274,7 @@ export class AnnotationService { {id: a.id, point: 2}, {id: polygonAnnotations[index+1].id, point: 1} ] - } : a.position2 !== polygonAnnotations[0].position1? { + } : a.position2.join() !== polygonAnnotations[0].position1.join()? { position: a.position2, lines: [ {id: a.id, point: 2} @@ -254,15 +283,15 @@ export class AnnotationService { }).filter(a => !!a) polygonPositions.unshift({ position: polygonAnnotations[0].position1, - lines: polygonAnnotations[0].position1 === [...polygonAnnotations].pop().position2? + lines: polygonAnnotations[0].position1.join() === [...polygonAnnotations].pop().position2.join()? [{id: polygonAnnotations[0].id, point: 1}, {id: [...polygonAnnotations].pop().id, point: 2}] : [{id: polygonAnnotations[0].id, point: 1}] }) transformed = transformed.filter(a => a.id.split('_')[0] !== annotationId[0]) - if (!annotations[i].name) { - annotations[i].name = this.giveNameByType(annotations[i].type) + if (annotations[i].name === null) { + annotations[i].name = this.generateNameByType(annotations[i].type) } transformed.push({ @@ -272,7 +301,7 @@ export class AnnotationService { type: 'polygon', annotations: polygonAnnotations, positions: polygonPositions, - circular: polygonAnnotations[0].position1 === [...polygonAnnotations].pop().position2, + circular: polygonAnnotations[0].position1.join() === [...polygonAnnotations].pop().position2.join(), annotationVisible: annotations[i].annotationVisible, template: annotations[i].template, atlas: this.selectedAtlas @@ -289,11 +318,11 @@ export class AnnotationService { } else { this.groupedAnnotations.push(tr) } - this.refreshAnnotationFilter() + this.refreshFinalAnnotationList() }) } - refreshAnnotationFilter(filter = null) { + refreshFinalAnnotationList(filter = null) { if (filter) {this.annotationFilter = filter} this.finalAnnotationList = this.groupedAnnotations // Filter all/current template @@ -304,155 +333,32 @@ export class AnnotationService { a.positions = a.positions.map(p => { return { ...p, - position: this.voxelToMM(p.position.split(',')).join() + position: this.voxelToMM(p.position) } }) } else { - a.position1 = this.voxelToMM(a.position1.split(',')).join() - a.position2 = a.position2 && this.voxelToMM(a.position2.split(',')).join() + a.position1 = this.voxelToMM(a.position1) + a.position2 = a.position2 && this.voxelToMM(a.position2) } 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[] { + voxelToMM(r: number[]): number[] { return r.map((r, i) => parseFloat((+r*this.voxelSize[i]/1e6).toFixed(3))) } - mmToVoxel(mm): any[] { + mmToVoxel(mm: number[]): any[] { return mm.map((m, i) => +m*1e6/this.voxelSize[i]) } getVoxelFromSpace = (spaceId: string) => { return IAV_VOXEL_SIZES_NM[spaceId] } - - 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], diff --git a/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ff35b28af30ae4b60199457850ac572aad38020 --- /dev/null +++ b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts @@ -0,0 +1,24 @@ +import {Directive, HostListener} from "@angular/core"; +import {viewerStateSetViewerMode} from "src/services/state/viewerState/actions"; +import {ARIA_LABELS} from "common/constants"; +import {Store} from "@ngrx/store"; + +@Directive({ + selector: '[annotation-switch]' +}) +export class AnnotationSwitch { + + + constructor(private store$: Store<any>) { + + } + + @HostListener('click') + onClick() { + this.setAnnotatingMode() + } + + private setAnnotatingMode() { + this.store$.dispatch(viewerStateSetViewerMode({payload: ARIA_LABELS.VIEWER_MODE_ANNOTATING})) + } +} diff --git a/src/atlasComponents/userAnnotations/directives/exportAnnotation.directive.ts b/src/atlasComponents/userAnnotations/directives/exportAnnotation.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..13935dea1a1fe6dd4fa9dbb9654be7043f329e18 --- /dev/null +++ b/src/atlasComponents/userAnnotations/directives/exportAnnotation.directive.ts @@ -0,0 +1,69 @@ +import {Directive, HostListener, Input} from "@angular/core"; +import * as JSZip from "jszip"; + +@Directive({ + selector: '[export-annotations]' +}) +export class ExportAnnotation { + + @Input('export-annotations') input: any + + @HostListener('click') + onClick() { + this.exportAnnotations(this.input.annotations, this.input.sands) + } + + getSandsObj(position, template) { + return { + coordinates: { + value: position.map(p => +p), + unit: 'mm' + }, + coordinateSpace: { + fullName: template.name, + versionIdentifier: template.id + } + } + } + + exportAnnotations(annotations, sands = false) { + + const zip = new JSZip() + const zipFileName = `annotation - ${annotations[0].atlas.name}.zip` + + if (sands) { + annotations.forEach(a => { + zip.folder(a.name) + if (a.positions) { + a.positions.forEach(p => { + zip.folder(a.name).file(`${p.position}.json`, JSON.stringify(this.getSandsObj(p.position, a.template))) + }) + } else { + zip.folder(a.name).file(`${a.position1}.json`, JSON.stringify(this.getSandsObj(a.position1, a.template))) + if (a.position2) zip.folder(a.name).file(`${a.position1}.json`, JSON.stringify(this.getSandsObj(a.position2, a.template))) + } + }) + } else { + annotations.forEach(a => { + const fileName = a.name.replace(/[\\/:*?"<>|]/g, "").trim() + zip.file(`${fileName}.json`, JSON.stringify(a)) + }) + } + + + zip.file("README.txt", + `The annotation has been extracted from the atlas: "${annotations.map(a => a.atlas.name).filter((v, i, a) => a.indexOf(v) === i).join()}" + and template(s): "${annotations.map(a => a.template.name).filter((v, i, a) => a.indexOf(v) === i).join()}"`) + zip.generateAsync({ + type: "base64" + }).then(content => { + const link = document.createElement('a') + link.href = 'data:application/zip;base64,' + content + link.download = zipFileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + }) + } + +} diff --git a/src/atlasComponents/userAnnotations/directives/importAnnotation.directive.ts b/src/atlasComponents/userAnnotations/directives/importAnnotation.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..052e1e8805b774746f46dbc18438a96af74020f3 --- /dev/null +++ b/src/atlasComponents/userAnnotations/directives/importAnnotation.directive.ts @@ -0,0 +1,74 @@ +import {Directive, HostListener, Input} from "@angular/core"; +import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; + +@Directive({ + selector: '[import-annotations]' +}) +export class ImportAnnotation { + + @Input('import-annotations') input: any + + constructor(private ans: AnnotationService) {} + + @HostListener('change', ['$event.target']) + onClick(target: any) { + if (target.files.length) { + this.importFile(target.files[0]) + } + } + + importFile(file) { + const sands = this.input.sands || null + + const fileReader = new FileReader() + fileReader.readAsText(file, "UTF-8") + fileReader.onload = () => { + const fileData = JSON.parse(fileReader.result.toString()) + + if (sands) { + if (!fileData.coordinates || !fileData.coordinates.value || fileData.coordinates.value.length !== 3 + || !fileData.coordinateSpace || !fileData.coordinateSpace.fullName || !fileData.coordinateSpace.versionIdentifier) { + return + } + const position1 = this.ans.mmToVoxel(fileData.coordinates.value) + this.ans.saveAnnotation({position1, + template: { + name: fileData.coordinateSpace.fullName, + id: fileData.coordinateSpace.versionIdentifier + }, + type: 'point'}) + } else { + const {id, name, description, type, + atlas, template, positions, annotations} = fileData + + if (!id || !(fileData.position1 || positions) || !type) { + return + } + + if (fileData.type !== 'polygon') { + const position1 = this.ans.mmToVoxel(fileData.position1.split(',').map(Number)) + const position2 = fileData.position2 && this.ans.mmToVoxel(fileData.position2.split(',').map(Number)) + + this.ans.saveAnnotation({position1, position2, + name, description, type, atlas, template + }) + } else if (annotations) { + annotations.forEach(a => { + this.ans.saveAnnotation({ + id: a.id, + name, description, + position1: a.position1, + position2: a.position2, + type: 'polygon'}) + }) + this.ans.groupedAnnotations.push(fileData) + this.ans.refreshFinalAnnotationList() + } + + } + } + fileReader.onerror = (error) => { + console.warn(error) + } + } +} diff --git a/src/atlasComponents/userAnnotations/directives/keyListener.directive.ts b/src/atlasComponents/userAnnotations/directives/keyListener.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d304fadbf4a99784da2671405d1f11f070c1983 --- /dev/null +++ b/src/atlasComponents/userAnnotations/directives/keyListener.directive.ts @@ -0,0 +1,18 @@ +import {Directive, ElementRef, HostListener} from "@angular/core"; + +@Directive({ + selector: '[annotation-list-key-listener]' +}) +export class KeyListener { + + constructor(private elementRef: ElementRef) {} + + @HostListener('keydown', ['$event']) + onKeyDown(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' || e === 'Escape' || e.key === 'Enter') { + e.stopPropagation() + this.elementRef.nativeElement.blur() + } + } + +} diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index 8a712e78e54ea6aa12d0e9f28ecabcfb9e7ff0d4..88b1e8e83dccec82cceabd7adcbcb7d899703aa0 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -9,6 +9,11 @@ import {AnnotationList} from "src/atlasComponents/userAnnotations/annotationList import {AnnotationService} from "src/atlasComponents/userAnnotations/annotationService.service"; import {AnnotationMessage} from "src/atlasComponents/userAnnotations/annotationMessage/annotationMessage.component"; import { UserAnnotationToolModule } from "./tools/module"; +import {AnnotationSwitch} from "src/atlasComponents/userAnnotations/directives/annotationSwitch.directive"; +import {ExportAnnotation} from "src/atlasComponents/userAnnotations/directives/exportAnnotation.directive"; +import {ImportAnnotation} from "src/atlasComponents/userAnnotations/directives/importAnnotation.directive"; +import {KeyListener} from "src/atlasComponents/userAnnotations/directives/keyListener.directive"; +import {CoordinateInputTextPipe} from "src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe"; @NgModule({ imports: [ @@ -23,7 +28,12 @@ import { UserAnnotationToolModule } from "./tools/module"; declarations: [ AnnotationMode, AnnotationList, - AnnotationMessage + AnnotationMessage, + AnnotationSwitch, + ImportAnnotation, + ExportAnnotation, + KeyListener, + CoordinateInputTextPipe ], providers: [ AnnotationService @@ -31,7 +41,8 @@ import { UserAnnotationToolModule } from "./tools/module"; exports: [ AnnotationMode, AnnotationList, - AnnotationMessage + AnnotationMessage, + AnnotationSwitch ] }) diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts index 46ca24930bca133c3d86594ee1d21f66efcdae36..649c4cdd2f0bb2ae00f9c4d297fe9e00183a4dfa 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts +++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts @@ -92,10 +92,6 @@ 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 0578434894262ce6752754e081e237186dee6ca8..89939c69623085628dcbdea50a19edca1bf1d3ef 100644 --- a/src/ui/topMenu/topMenuCmp/topMenu.template.html +++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html @@ -153,7 +153,7 @@ </button> <button mat-menu-item [disabled]="!viewerLoaded" - (click)="setAnnotatingMode()" + annotation-switch [matTooltip]="annotateTooltipText"> <mat-icon fontSet="fas" fontIcon="fa-pencil-ruler"> </mat-icon> diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index f10fa73405f9917e818e474c1382bc301c2b82a5..293bddb07b00c9378f1e1702561e83d311c2cf12 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -136,8 +136,9 @@ export class ViewerCmp implements OnDestroy { ) public viewerMode: string - public hideUi$ = this.store$.pipe( + public hideUi$: Observable<boolean> = this.store$.pipe( select(viewerStateViewerModeSelector), + map(h => h === ARIA_LABELS.VIEWER_MODE_ANNOTATING), distinctUntilChanged(), ) diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 6fa739408aed2b0423d6931b56b50bd61f05a50d..6e2d0bcdab4f197299c340b59f799a8a50feedaa 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -7,11 +7,11 @@ <!-- Annotation mode --> - <div *ngIf="(hideUi$ | async) === ARIA_LABELS.VIEWER_MODE_ANNOTATING"> + <div *ngIf="hideUi$ | async"> <mat-drawer-container class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible" [hasBackdrop]="false"> - <mat-drawer #drawer [mode]="'push'" [disableClose]="true" class="pe-all"> + <mat-drawer #drawer [mode]="'push'" [disableClose]="true" class="box-shadow-none border-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> <annotation-list></annotation-list> </mat-drawer> <mat-drawer-content class="visible position-relative pe-none"> @@ -20,11 +20,9 @@ <!-- 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'}"> + <div (click)="drawer.toggle()" class="mt-3"> + <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; + context: {matColor: 'primary',fontIcon: 'fa-list',tooltip: 'Annotation list'}"> </ng-container> </div> </div>