diff --git a/common/constants.js b/common/constants.js index 5a7066902a523a337cd27ab40065d8a64db0df31..b39325bc3a05a782e7178d68b976c1f933d86832 100644 --- a/common/constants.js +++ b/common/constants.js @@ -66,7 +66,7 @@ USER_ANNOTATION_EXPORT: 'user annotations export', USER_ANNOTATION_EXPORT_SINGLE: 'Export annotation', USER_ANNOTATION_HIDE: 'user annotations hide', - USER_ANNOTATION_DELETE: 'user annotations delete', + USER_ANNOTATION_DELETE: 'Delete annotation', GOTO_ANNOTATION_ROI: 'Navigate to annotation location of interest' } diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts index 1f99934f9fe1c3aff22631b302a882864ef01b00..7321b4de4dc86a52db6aebec572bd6735118c673 100644 --- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts +++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts @@ -1,7 +1,7 @@ import {Component, HostListener, Inject, OnDestroy, OnInit, Optional} from "@angular/core"; import {select, Store} from "@ngrx/store"; -import {ARIA_LABELS, CONST} from "common/constants"; -import { merge, Observable, Subscription} from "rxjs"; +import { ARIA_LABELS } from "common/constants"; +import { Observable, Subscription} from "rxjs"; import {getUuid} from "src/util/fn"; import {VIEWER_INJECTION_TOKEN} from "src/ui/layerbrowser/layerDetail/layerDetail.component"; import {buffer, debounceTime, distinctUntilChanged, filter, map, switchMapTo, take, takeUntil, tap} from "rxjs/operators"; diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index e74cbf1b6054b7bbee6bd5492e6d9e0b71ed8ae9..5df04a845515df59cab72a4d96e9ada3885a281a 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -190,6 +190,13 @@ export class ToolLine extends AbsToolClass implements IAnnotationTools, OnDestro * on end tool select */ toolDeselect$.subscribe(() => { + /** + * if line is not completed by tool deselect + * it only has a single point, and should be removed + */ + if (this.selectedLine) { + this.removeAnnotation(this.selectedLine.id) + } this.selectedLine = null }), /** @@ -206,12 +213,13 @@ export class ToolLine extends AbsToolClass implements IAnnotationTools, OnDestro "@type": 'siibra-ex/annotation/line', points: [] }) + const { id } = this.selectedLine + this.selectedLine.remove = () => this.removeAnnotation(id) this.selectedLine.addLinePoints(crd) this.managedAnnotations.push(this.selectedLine) this.managedAnnotations$.next(this.managedAnnotations) } else { - // ToDo Tool Should Be Deselected. this.selectedLine.addLinePoints(crd) this.selectedLine = null diff --git a/src/atlasComponents/userAnnotations/tools/line/line.component.ts b/src/atlasComponents/userAnnotations/tools/line/line.component.ts index e99984bbb5bd5d22b19022c60b76aaa22d939d87..82289d20a86efdd15d98c649d807dd4436d7fa72 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.component.ts +++ b/src/atlasComponents/userAnnotations/tools/line/line.component.ts @@ -1,7 +1,6 @@ import { Component, ElementRef, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; import { MatSnackBar } from "@angular/material/snack-bar"; import { Store } from "@ngrx/store"; -import { Subscription } from "rxjs"; import { Line, LINE_ICON_CLASS } from "../line"; import { ToolCmpBase } from "../toolCmp.base"; import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from "../type"; @@ -24,31 +23,19 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ public updateAnnotation: Line public ARIA_LABELS = ARIA_LABELS - - public annotationLabel = 'Line' public LINE_ICON_CLASS = LINE_ICON_CLASS @ViewChild('copyTarget', { read: ElementRef, static: false }) copyTarget: ElementRef - public useFormat: TExportFormats = 'json' - private sub: Subscription[] = [] - constructor( private store: Store<any>, snackbar: MatSnackBar, clipboard: Clipboard, - private cStore: ComponentStore<{ useFormat: TExportFormats }>, + cStore: ComponentStore<{ useFormat: TExportFormats }>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ - super(clipboard, snackbar) - if (this.cStore) { - this.sub.push( - this.cStore.select(store => store.useFormat).subscribe((val: TExportFormats) => { - this.useFormat = val - }) - ) - } + super(clipboard, snackbar, cStore) if (updateAnnotation) { if (updateAnnotation instanceof Line) { @@ -57,16 +44,6 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ } } - public viableFormats: TExportFormats[] = ['json', 'sands'] - - setFormat(format: TExportFormats){ - if (this.cStore) { - this.cStore.setState({ - useFormat: format - }) - } - } - ngOnDestroy(){ while (this.sub.length > 0) this.sub.pop().unsubscribe() } @@ -79,6 +56,22 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ if (!this.updateAnnotation && !roi) { throw new Error(`updateAnnotation undefined`) } + + if (roi && roi instanceof Point) { + const { x, y, z } = roi + + this.store.dispatch( + viewerStateChangeNavigation({ + navigation: { + position: [x, y, z], + positionReal: true, + animation: {} + } + }) + ) + return + } + if (this.updateAnnotation.points.length < 1) { this.snackbar.open('No points added to polygon yet.', 'Dismiss', { duration: 3000 @@ -98,18 +91,7 @@ export class LineUpdateCmp extends ToolCmpBase implements OnDestroy{ ) } - gotoPoint(point: Point){ - if (!point) throw new Error(`Point is not defined.`) - const { x, y, z } = point - - this.store.dispatch( - viewerStateChangeNavigation({ - navigation: { - position: [x, y, z], - positionReal: true, - animation: {} - } - }) - ) + remove(){ + this.updateAnnotation?.remove() } } diff --git a/src/atlasComponents/userAnnotations/tools/line/line.template.html b/src/atlasComponents/userAnnotations/tools/line/line.template.html index d65b06ef8c1a6e4c3ed767fcc055fed689b3813f..ad1246b245e8acaf7799853eaa40439e5197f471 100644 --- a/src/atlasComponents/userAnnotations/tools/line/line.template.html +++ b/src/atlasComponents/userAnnotations/tools/line/line.template.html @@ -1,4 +1,4 @@ -<!-- actions --> +<!-- summary of geometry --> <span class="m-2 text-muted"> Endpoints @@ -6,13 +6,16 @@ <mat-chip-list> <mat-chip *ngFor="let point of (updateAnnotation?.points || []) ; let i = index" - (click)="gotoPoint(point)"> + (click)="gotoRoi(point)" + [matTooltip]="point"> {{ i }} </mat-chip> </mat-chip-list> <mat-divider class="m-2"></mat-divider> +<!-- actions --> + <div class="d-flex"> <!-- export --> @@ -22,6 +25,14 @@ [matMenuTriggerFor]="exportMenu"> <i class="fas fa-file-export"></i> </button> + + <!-- delete --> + <button mat-icon-button + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_DELETE" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_DELETE" + (click)="remove()"> + <i class="fas fa-trash"></i> + </button> </div> <mat-menu #exportMenu="matMenu" xPosition="before"> diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts index 713c3c6cc4c6c315eb0ed478b4e33f04c37f5e18..2d9d9fc96fcb9fd7d6a4339ea7b15fc84d36152a 100644 --- a/src/atlasComponents/userAnnotations/tools/point.ts +++ b/src/atlasComponents/userAnnotations/tools/point.ts @@ -109,14 +109,20 @@ export class ToolPoint extends AbsToolClass implements IAnnotationTools, OnDestr toolSelThenClick$.subscribe(ev => { const {x, y, z} = ev.detail.ngMouseEvent const { space } = this - this.managedAnnotations.push( - new Point({ - x, y, z, - space, - '@type': 'siibra-ex/annotatoin/point' - }) - ) + const pt = new Point({ + x, y, z, + space, + '@type': 'siibra-ex/annotatoin/point' + }) + const { id } = pt + pt.remove = () => this.removeAnnotation(id) + this.managedAnnotations.push(pt) this.managedAnnotations$.next(this.managedAnnotations) + + /** + * deselect on selecting a point + */ + this.callback({ type: 'paintingEnd' }) }), /** diff --git a/src/atlasComponents/userAnnotations/tools/point/point.component.ts b/src/atlasComponents/userAnnotations/tools/point/point.component.ts index 9e08437b5b803a17486bf84d6fc0ee0a80598cf3..8dd44c0d7ed9c6b8e8d42310fbb0b99b3fa252d4 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.component.ts +++ b/src/atlasComponents/userAnnotations/tools/point/point.component.ts @@ -6,7 +6,8 @@ import { Clipboard } from "@angular/cdk/clipboard"; import { ToolCmpBase } from "../toolCmp.base"; import { Store } from "@ngrx/store"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; -import { Subscription } from "rxjs"; +import { ComponentStore } from "src/viewerModule/componentStore"; +import { ARIA_LABELS } from 'common/constants' @Component({ selector: 'point-update-cmp', @@ -18,20 +19,12 @@ import { Subscription } from "rxjs"; export class PointUpdateCmp extends ToolCmpBase implements OnDestroy{ + public ARIA_LABELS = ARIA_LABELS public POINT_ICON_CLASS = POINT_ICON_CLASS @Input('update-annotation') updateAnnotation: Point - @Input('annotation-label') - annotationLabel = 'Point' - - @Input('show-copy-button') - showCopyBtn = true - - private sub: Subscription[] = [] - public useFormat: TExportFormats = 'string' - @ViewChild('copyTarget', { read: ElementRef, static: false }) copyTarget: ElementRef @@ -39,9 +32,10 @@ export class PointUpdateCmp extends ToolCmpBase implements OnDestroy{ private store: Store<any>, snackbar: MatSnackBar, clipboard: Clipboard, + cStore: ComponentStore<{ useFormat: TExportFormats }>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ - super(clipboard, snackbar) + super(clipboard, snackbar, cStore) if (updateAnnotation) { if (updateAnnotation instanceof Point) { @@ -75,4 +69,7 @@ export class PointUpdateCmp extends ToolCmpBase implements OnDestroy{ ) } + remove(){ + this.updateAnnotation?.remove() + } } diff --git a/src/atlasComponents/userAnnotations/tools/point/point.template.html b/src/atlasComponents/userAnnotations/tools/point/point.template.html index c5fdf78056fa36c8011f50ce97e2c14d27715951..1de69ecc26e2e7837758892f0c2d165ab87ab3bc 100644 --- a/src/atlasComponents/userAnnotations/tools/point/point.template.html +++ b/src/atlasComponents/userAnnotations/tools/point/point.template.html @@ -1,32 +1,72 @@ -<div class="d-flex align-items center"> - <button mat-icon-button - class="flex-grow-0 flex-shrink-0" - [attr.aria-label]="ARIA_LABELS.GOTO_ANNOTATION_ROI" +<!-- summary of geometry --> + +<span class="m-2 text-muted"> + Point +</span> + +<mat-chip-list> + <mat-chip [matTooltip]="updateAnnotation" (click)="gotoRoi()"> - <i [class]="POINT_ICON_CLASS"></i> + 0 + </mat-chip> +</mat-chip-list> + +<mat-divider class="m-2"></mat-divider> + +<!-- actions --> + +<div class="d-flex"> + + <!-- export --> + <button mat-icon-button + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [matMenuTriggerFor]="exportMenu"> + <i class="fas fa-file-export"></i> </button> - <div class="flex-grow-1 flex-shrink-1"> - <mat-form-field class="w-100"> - <mat-label> - {{ annotationLabel }} - </mat-label> - - <textarea matInput - disabled="true" - #copyTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> - - <!-- copy to clipboard button --> - <button mat-icon-button - matSuffix - iav-stop="click" - aria-label="Copy to clipboard" - matTooltip="Copy to clipboard." - (click)="copyToClipboard(copyValue)" - *ngIf="showCopyBtn && !!copyTarget" - color="basic"> - <i class="fas fa-copy"></i> + <!-- delete --> + <button mat-icon-button + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_DELETE" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_DELETE" + (click)="remove()"> + <i class="fas fa-trash"></i> + </button> +</div> + +<mat-menu #exportMenu="matMenu" xPosition="before"> + <div class="iv-custom-comp card text" + iav-stop="click"> + + <div class="d-flex"> + <button *ngFor="let format of viableFormats" + (click)="setFormat(format)" + mat-flat-button + [color]="useFormat === format ? 'primary' : ''"> + {{ format }} </button> - </mat-form-field> + </div> + + <div class="iv-custom-comp text"> + <mat-form-field> + <mat-label> + {{ useFormat }} + </mat-label> + <textarea + disabled="true" + matInput + #exportTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> + + <button mat-icon-button + matSuffix + iav-stop="click" + aria-label="Copy to clipboard" + matTooltip="Copy to clipboard." + (click)="copyToClipboard(exportTarget.value)" + color="basic"> + <i class="fas fa-copy"></i> + </button> + </mat-form-field> + </div> </div> -</div> +</mat-menu> diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index 42780797a569d548d7f6f73d466a88328f068118..ccd48017600d98b202028d69cb7b9f4670556ff6 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -1,4 +1,4 @@ -import { IAnnotationTools, IAnnotationGeometry, TAnnotationEvent, IAnnotationEvents, AbsToolClass, INgAnnotationTypes, TNgAnnotationEv, TToolType, TBaseAnnotationGeomtrySpec, TSandsPolyLine, getCoord } from "./type"; +import { IAnnotationTools, IAnnotationGeometry, TAnnotationEvent, IAnnotationEvents, AbsToolClass, INgAnnotationTypes, TNgAnnotationEv, TToolType, TBaseAnnotationGeomtrySpec, TSandsPolyLine, getCoord, TCallbackFunction } from "./type"; import { Point, TPointJsonSpec } from './point' import { OnDestroy } from "@angular/core"; import { merge, Observable, Subject, Subscription } from "rxjs"; @@ -219,9 +219,10 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes } constructor( - annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>> + annotationEv$: Observable<TAnnotationEvent<keyof IAnnotationEvents>>, + callback: TCallbackFunction ){ - super(annotationEv$) + super(annotationEv$, callback) this.init() const toolDeselect$ = this.toolSelected$.pipe( filter(flag => !flag) @@ -242,6 +243,34 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes * on end tool select */ toolDeselect$.subscribe(() => { + + /** + * cleanup poly on tool deselect + */ + if (this.selectedPoly) { + + const { edges, points } = this.selectedPoly + /** + * check if closed. if not close, close it + */ + if (edges.length > 0) { + + if (edges[edges.length - 1].every(v => v !== 0)) { + this.selectedPoly.addPoint( + points[0], + points[points.length - 1] + ) + } + } + + /** + * if edges < 3, discard poly + */ + if (edges.length < 3) { + this.removeAnnotation(this.selectedPoly.id) + } + } + this.selectedPoly = null this.lastAddedPoint = null }), @@ -260,19 +289,32 @@ export class ToolPolygon extends AbsToolClass implements IAnnotationTools, OnDes space: this.space, '@type': 'siibra-ex/annotation/polyline' }) + const { id } = this.selectedPoly + this.selectedPoly.remove = () => this.removeAnnotation(id) this.managedAnnotations.push(this.selectedPoly) this.managedAnnotations$.next(this.managedAnnotations) - } + } else { - let existingPoint: Point - if (ann.detail) { - const { pickedAnnotationId, pickedOffset } = ann.detail - const out = this.selectedPoly.parseNgAnnotationObj(pickedAnnotationId, pickedOffset) - existingPoint = out?.point + if (ann.detail) { + const { pickedAnnotationId, pickedOffset } = ann.detail + const out = this.selectedPoly.parseNgAnnotationObj(pickedAnnotationId, pickedOffset) + const isFirstPoint = out?.point === this.selectedPoly.points[0] + if (isFirstPoint) { + this.selectedPoly.addPoint( + this.selectedPoly.points[0], + this.lastAddedPoint + ) + this.callback({ + type: 'paintingEnd', + }) + return + } + } + } const addedPoint = this.selectedPoly.addPoint( - existingPoint || mouseev.detail.ngMouseEvent, + mouseev.detail.ngMouseEvent, this.lastAddedPoint ) this.lastAddedPoint = addedPoint diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts index 3621c2d25717a64aa8b3cb85aaafd491dc5bb43b..885c8b41c2292737e681f4a156a9f93ab77631f4 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.component.ts @@ -6,7 +6,9 @@ import { IAnnotationGeometry, TExportFormats, UDPATE_ANNOTATION_TOKEN } from ".. import { Clipboard } from "@angular/cdk/clipboard"; import { viewerStateChangeNavigation } from "src/services/state/viewerState/actions"; import { Store } from "@ngrx/store"; -import { Subscription } from "rxjs"; +import { Point } from "../point"; +import { ARIA_LABELS } from 'common/constants' +import { ComponentStore } from "src/viewerModule/componentStore"; @Component({ selector: 'poly-update-cmp', @@ -20,22 +22,29 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ @Input('update-annotation') public updateAnnotation: Polygon - public annotationLabel = 'Polygon' + public ARIA_LABELS = ARIA_LABELS public POLY_ICON_CLASS = POLY_ICON_CLASS @ViewChild('copyTarget', { read: ElementRef, static: false }) copyTarget: ElementRef public useFormat: TExportFormats = 'string' - private sub: Subscription[] = [] constructor( private store: Store<any>, snackbar: MatSnackBar, clipboard: Clipboard, + cStore: ComponentStore<{ useFormat: TExportFormats }>, @Optional() @Inject(UDPATE_ANNOTATION_TOKEN) updateAnnotation: IAnnotationGeometry, ){ - super(clipboard, snackbar) + super(clipboard, snackbar, cStore) + if (this.cStore) { + this.sub.push( + this.cStore.select(store => store.useFormat).subscribe((val: TExportFormats) => { + this.useFormat = val + }) + ) + } if (updateAnnotation) { if (updateAnnotation instanceof Polygon) { @@ -52,10 +61,26 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ return this.copyTarget && this.copyTarget.nativeElement.value } - gotoRoi(){ + gotoRoi(roi?: IAnnotationGeometry){ if (!this.updateAnnotation) { throw new Error(`updateAnnotation undefined`) } + + if (roi && roi instanceof Point) { + const { x, y, z } = roi + + this.store.dispatch( + viewerStateChangeNavigation({ + navigation: { + position: [x, y, z], + positionReal: true, + animation: {} + } + }) + ) + return + } + if (this.updateAnnotation.points.length < 1) { this.snackbar.open('No points added to polygon yet.', 'Dismiss', { duration: 3000 @@ -74,4 +99,8 @@ export class PolyUpdateCmp extends ToolCmpBase implements OnDestroy{ }) ) } + + remove(){ + this.updateAnnotation?.remove() + } } diff --git a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html index 52dab491ec667660bf73c783f24d6b97dff5c93c..9340263fb28b89db4df5750b8b46170f85875822 100644 --- a/src/atlasComponents/userAnnotations/tools/poly/poly.template.html +++ b/src/atlasComponents/userAnnotations/tools/poly/poly.template.html @@ -1,37 +1,73 @@ -<div class="d-flex align-items-center"> +<!-- summary of geometry --> + +<span class="m-2 text-muted"> + Vertices +</span> + +<mat-chip-list> + <mat-chip *ngFor="let point of (updateAnnotation?.points || []); let i = index" + (click)="gotoRoi(point)" + [matTooltip]="point"> + {{ i }} + </mat-chip> +</mat-chip-list> + +<mat-divider class="m-2"></mat-divider> + +<!-- actions --> + +<div class="d-flex"> + + <!-- export --> <button mat-icon-button - class="flex-grow-0 flex-shrink-0" - [attr.aria-label]="ARIA_LABELS.GOTO_ANNOTATION_ROI" - (click)="gotoRoi()"> - <i [class]="POLY_ICON_CLASS"></i> + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" + [matMenuTriggerFor]="exportMenu"> + <i class="fas fa-file-export"></i> </button> - - <div class="flex-grow-1 flex-shrink-1"> - <mat-form-field class="w-100"> - <mat-label> - {{ annotationLabel }} - </mat-label> - <textarea matInput - disabled="true" - #copyTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> - - <button mat-icon-button - matSuffix - iav-stop="click" - aria-label="Copy to clipboard" - matTooltip="Copy to clipboard." - (click)="copyToClipboard(copyValue)" - color="basic"> - <i class="fas fa-copy"></i> - </button> - </mat-form-field> - </div> + <!-- delete --> + <button mat-icon-button + [attr.aria-label]="ARIA_LABELS.USER_ANNOTATION_DELETE" + [matTooltip]="ARIA_LABELS.USER_ANNOTATION_DELETE" + (click)="remove()"> + <i class="fas fa-trash"></i> + </button> </div> -<point-update-cmp - *ngFor="let point of (updateAnnotation?.points || []) ; let i = index" - class="pl-4" - [annotation-label]="'Point ' + ( i + 1 )" - [update-annotation]="point"> -</point-update-cmp> +<mat-menu #exportMenu="matMenu" xPosition="before"> + <div class="iv-custom-comp card text" + iav-stop="click"> + + <div class="d-flex"> + <button *ngFor="let format of viableFormats" + (click)="setFormat(format)" + mat-flat-button + [color]="useFormat === format ? 'primary' : ''"> + {{ format }} + </button> + </div> + + <div class="iv-custom-comp text"> + <mat-form-field> + <mat-label> + {{ useFormat }} + </mat-label> + <textarea + disabled="true" + matInput + #exportTarget>{{ updateAnnotation.updateSignal$ | async | toFormattedStringPipe : updateAnnotation : useFormat }}</textarea> + + <button mat-icon-button + matSuffix + iav-stop="click" + aria-label="Copy to clipboard" + matTooltip="Copy to clipboard." + (click)="copyToClipboard(exportTarget.value)" + color="basic"> + <i class="fas fa-copy"></i> + </button> + </mat-form-field> + </div> + </div> +</mat-menu> diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index a48049574a75e15fadc82f9b01a9126ea3968a3b..76d41728a1c1dd9479b17dc943aa501d64f5c46b 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -3,7 +3,7 @@ import { ARIA_LABELS } from 'common/constants' import { Inject, Optional } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs"; -import { map, switchMap, filter } from "rxjs/operators"; +import { map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators"; import { viewerStateSelectedTemplatePureSelector, viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors"; import { NehubaViewerUnit } from "src/viewerModule/nehuba"; import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util"; @@ -374,21 +374,35 @@ export class ModularUserAnnotationToolService implements OnDestroy{ ) /** - * on tool managed annotations update, update annotations + * on tool managed annotations update */ + const managedAnnotationUpdate$ = combineLatest([ + this.forcedAnnotationRefresh$, + this.ngAnnotations$.pipe( + switchMap(switchMapWaitFor({ + condition: () => !!this.ngAnnotationLayer, + leading: true + })), + scanCollapse(), + ) + ]).pipe( + map(([_, ngAnnos]) => ngAnnos), + shareReplay(1), + ) this.subscription.push( - combineLatest([ - this.forcedAnnotationRefresh$, - this.ngAnnotations$.pipe( - switchMap(switchMapWaitFor({ - condition: () => !!this.ngAnnotationLayer, - leading: true - })), - scanCollapse(), - ) - ]).pipe( - map(([_, ngAnnos]) => ngAnnos), - ).subscribe(arr => { + // delete removed annotations + managedAnnotationUpdate$.pipe( + pairwise(), + filter(([ oldAnn, newAnn ]) => newAnn.length < oldAnn.length), + ).subscribe(([ oldAnn, newAnn ]) => { + const newAnnIdSet = new Set(newAnn.map(ann => ann.id)) + const outs = oldAnn.filter(ann => !newAnnIdSet.has(ann.id)) + for (const out of outs){ + this.deleteNgAnnotationById(out.id) + } + }), + //update annotations + managedAnnotationUpdate$.subscribe(arr => { const ignoreNgAnnIdsSet = new Set<string>() for (const hiddenAnnot of this.hiddenAnnotations) { const ids = hiddenAnnot.getNgAnnotationIds() diff --git a/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts b/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts index 102128f26fc77e7f552d77f917a7d759c9168ac3..586ef70c11e40c2aa43a3d04af6518d1210d8956 100644 --- a/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts +++ b/src/atlasComponents/userAnnotations/tools/toolCmp.base.ts @@ -1,15 +1,40 @@ import { MatSnackBar } from "@angular/material/snack-bar" import { Clipboard } from "@angular/cdk/clipboard"; import { ARIA_LABELS } from 'common/constants' +import { ComponentStore } from "src/viewerModule/componentStore"; +import { TExportFormats } from "./type"; +import { Subscription } from "rxjs"; export abstract class ToolCmpBase { public ARIA_LABELS = ARIA_LABELS + + public viableFormats: TExportFormats[] = ['json', 'sands'] + public useFormat: TExportFormats = 'json' + + protected sub: Subscription[] = [] constructor( protected clipboard: Clipboard, - protected snackbar: MatSnackBar, + protected snackbar: MatSnackBar, + protected cStore: ComponentStore<{ useFormat: TExportFormats }>, ){ + if (this.cStore) { + this.sub.push( + this.cStore.select(store => store.useFormat).subscribe((val: TExportFormats) => { + this.useFormat = val + }) + ) + } + } + + setFormat(format: TExportFormats){ + if (this.cStore) { + this.cStore.setState({ + useFormat: format + }) + } } + copyToClipboard(value: string){ const success = this.clipboard.copy(`${value}`) this.snackbar.open( @@ -23,4 +48,9 @@ export abstract class ToolCmpBase { * Intention of navigating to ROI */ abstract gotoRoi(): void + + /** + * Intention to remove + */ + abstract remove(): void } diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index 06f715bcbbeb6342f73228b49b89f12da0438265..65edbdd41c5d164c89a417459fa3341925e3d483 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -243,6 +243,9 @@ export abstract class IAnnotationGeometry { abstract toString(): string abstract toSands(): ISandsAnnotation[keyof ISandsAnnotation] + public remove() { + throw new Error(`The remove method needs to be overwritten by the tool manager`) + } public updateSignal$ = new Subject() constructor(spec?: TBaseAnnotationGeomtrySpec){