From 3f5390da0e4ad7aec4166f0b126340e279887b00 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Sun, 20 Jun 2021 16:51:55 +0200 Subject: [PATCH] refactor context menu --- .../coordinateInputText.pipe.ts | 8 -- .../directives/annotationSwitch.directive.ts | 75 +++++++++++++++++++ .../filterAnnotationBySpace.pipe.ts | 13 ++++ src/atlasComponents/userAnnotations/module.ts | 22 +++--- .../userAnnotations/tools/line.ts | 17 +++-- .../userAnnotations/tools/module.ts | 4 +- .../userAnnotations/tools/poly.ts | 2 +- .../userAnnotations/tools/service.ts | 16 +++- .../userAnnotations/tools/type.ts | 6 ++ src/contextMenuModule/service.ts | 14 +++- .../viewerCmp/viewerCmp.component.ts | 43 ++++++++--- .../viewerCmp/viewerCmp.template.html | 52 +++++++------ 12 files changed, 206 insertions(+), 66 deletions(-) delete mode 100644 src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts create mode 100644 src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts diff --git a/src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts b/src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts deleted file mode 100644 index 99e09c852..000000000 --- a/src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe.ts +++ /dev/null @@ -1,8 +0,0 @@ -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/directives/annotationSwitch.directive.ts b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts index 817af1139..0aeb8ac79 100644 --- a/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts +++ b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts @@ -5,6 +5,10 @@ import { Store } from "@ngrx/store"; import { TContextArg } from "src/viewerModule/viewer.interface"; import { TContextMenuReg } from "src/contextMenuModule"; import { CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util"; +import { ModularUserAnnotationToolService } from "../tools/service"; +import { IAnnotationGeometry } from "../tools/type"; +import { retry } from 'common/util' +import { MatSnackBar } from "@angular/material/snack-bar"; @Directive({ selector: '[annotation-switch]' @@ -15,15 +19,44 @@ export class AnnotationSwitch implements OnDestroy{ constructor( private store$: Store<any>, + private svc: ModularUserAnnotationToolService, + private snackbar: MatSnackBar, @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>> ) { + const sub = this.svc.managedAnnotations$.subscribe(manAnn => this.manangedAnnotations = manAnn) + this.onDestroyCb.push( + () => sub.unsubscribe() + ) + + + const loadAnn = async () => { + try { + const anns = await this.getAnnotation() + for (const ann of anns) { + this.svc.importAnnotation(ann) + } + } catch (e) { + this.snackbar.open(`Error loading annotation from storage: ${e.toString()}`, 'Dismiss', { + duration: 3000 + }) + } + } + loadAnn() } ngOnDestroy(){ while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()() } + /** + * TODO move annotation storage/retrival to more logical location + */ + @HostListener('window:beforeunload') + onPageHide(){ + this.storeAnnotation(this.manangedAnnotations) + } + @HostListener('click') onClick() { this.store$.dispatch( @@ -32,4 +65,46 @@ export class AnnotationSwitch implements OnDestroy{ }) ) } + + private manangedAnnotations = [] + private localstoragekey = 'userAnnotationKey' + private storeAnnotation(anns: IAnnotationGeometry[]){ + const arr = [] + for (const ann of anns) { + const json = ann.toJSON() + arr.push(json) + } + const stringifiedJSON = JSON.stringify(arr) + const { pako } = (window as any).export_nehuba + const compressed = pako.deflate(stringifiedJSON) + let out = '' + for (const num of compressed) { + out += String.fromCharCode(num) + } + const encoded = btoa(out) + window.localStorage.setItem(this.localstoragekey, encoded) + } + private async getAnnotation(): Promise<IAnnotationGeometry[]>{ + const encoded = window.localStorage.getItem(this.localstoragekey) + if (!encoded) return [] + const bin = atob(encoded) + + await retry(() => { + if (!!(window as any).export_nehuba) return true + else throw new Error(`export nehuba not yet ready`) + }, { + timeout: 1000, + retries: 10 + }) + + const { pako } = (window as any).export_nehuba + const decoded = pako.inflate(bin, { to: 'string' }) + const arr = JSON.parse(decoded) + const out: IAnnotationGeometry[] = [] + for (const obj of arr) { + const geometry = this.svc.parseAnnotationObject(obj) + out.push(geometry) + } + return out + } } diff --git a/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts b/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts new file mode 100644 index 000000000..69506ed32 --- /dev/null +++ b/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { IAnnotationGeometry } from "./tools/type"; + +@Pipe({ + name: 'filterAnnotationsBySpace', + pure: true +}) + +export class FilterAnnotationsBySpace implements PipeTransform{ + public transform(annotations: IAnnotationGeometry[], space: { '@id': string }): IAnnotationGeometry[]{ + return annotations.filter(ann => ann.space["@id"] === space["@id"]) + } +} \ No newline at end of file diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts index 46f329bcb..efeb89eda 100644 --- a/src/atlasComponents/userAnnotations/module.ts +++ b/src/atlasComponents/userAnnotations/module.ts @@ -1,24 +1,22 @@ -import {NgModule} from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {DatabrowserModule} from "src/atlasComponents/databrowserModule"; -import {AngularMaterialModule} from "src/ui/sharedModules/angularMaterial.module"; -import {FormsModule, ReactiveFormsModule} from "@angular/forms"; -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 { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +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 { UserAnnotationToolModule } from "./tools/module"; -import {AnnotationSwitch} from "src/atlasComponents/userAnnotations/directives/annotationSwitch.directive"; -import {CoordinateInputTextPipe} from "src/atlasComponents/userAnnotations/annotationList/coordinateInputText.pipe"; +import { AnnotationSwitch } from "src/atlasComponents/userAnnotations/directives/annotationSwitch.directive"; import { UtilModule } from "src/util"; import { SingleAnnotationClsIconPipe, SingleAnnotationNamePipe, SingleAnnotationUnit } from "./singleAnnotationUnit/singleAnnotationUnit.component"; import { AnnotationVisiblePipe } from "./annotationVisible.pipe"; import { FileInputModule } from "src/getFileInput/module"; import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; +import { FilterAnnotationsBySpace } from "./filterAnnotationBySpace.pipe"; @NgModule({ imports: [ CommonModule, - DatabrowserModule, BrowserAnimationsModule, FormsModule, ReactiveFormsModule, @@ -32,11 +30,11 @@ import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; AnnotationMode, AnnotationList, AnnotationSwitch, - CoordinateInputTextPipe, SingleAnnotationUnit, SingleAnnotationNamePipe, SingleAnnotationClsIconPipe, AnnotationVisiblePipe, + FilterAnnotationsBySpace, ], exports: [ AnnotationMode, diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts index 6dcf2e1c4..c1ac7f8a4 100644 --- a/src/atlasComponents/userAnnotations/tools/line.ts +++ b/src/atlasComponents/userAnnotations/tools/line.ts @@ -17,7 +17,7 @@ import { merge, Observable, Subject, Subscription } from "rxjs"; import { filter, switchMapTo, takeUntil } from "rxjs/operators"; import { getUuid } from "src/util/fn"; -type TLineJsonSpec = { +export type TLineJsonSpec = { '@type': 'siibra-ex/annotation/line' points: (TPointJsonSpec|Point)[] } & TBaseAnnotationGeomtrySpec @@ -102,9 +102,16 @@ export class Line extends IAnnotationGeometry{ } - toJSON(){ - const { id, points } = this - return { id, points } + toJSON(): TLineJsonSpec{ + const { id, name, desc, points, space } = this + return { + id, + name, + desc, + points: points.map(p => p.toJSON()), + space, + '@type': 'siibra-ex/annotation/line' + } } static fromJSON(json: TLineJsonSpec){ @@ -266,7 +273,7 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On this.selectedLine.addLinePoints(crd) this.selectedLine = null - + this.managedAnnotations$.next(this.managedAnnotations) if (this.callback) { this.callback({ type: 'paintingEnd' }) } diff --git a/src/atlasComponents/userAnnotations/tools/module.ts b/src/atlasComponents/userAnnotations/tools/module.ts index cfc25b90f..e7a3a0402 100644 --- a/src/atlasComponents/userAnnotations/tools/module.ts +++ b/src/atlasComponents/userAnnotations/tools/module.ts @@ -8,8 +8,8 @@ import { PointUpdateCmp } from "./point/point.component"; import { PolyUpdateCmp } from "./poly/poly.component"; import { ModularUserAnnotationToolService } from "./service"; import { ToFormattedStringPipe } from "./toFormattedString.pipe"; -import { ANNOTATION_EVENT_INJ_TOKEN } from "./type"; -import {Line, ToolLine} from "src/atlasComponents/userAnnotations/tools/line"; +import { ANNOTATION_EVENT_INJ_TOKEN, } from "./type"; +import { Line, ToolLine } from "src/atlasComponents/userAnnotations/tools/line"; import { Point, ToolPoint } from "./point"; import { ToolSelect } from "./select"; diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts index 8594e7213..4dc49ee30 100644 --- a/src/atlasComponents/userAnnotations/tools/poly.ts +++ b/src/atlasComponents/userAnnotations/tools/poly.ts @@ -5,7 +5,7 @@ import { merge, Observable, Subject, Subscription } from "rxjs"; import { filter, switchMapTo, takeUntil, withLatestFrom } from "rxjs/operators"; import { getUuid } from "src/util/fn"; -type TPolyJsonSpec = { +export type TPolyJsonSpec = { points: (TPointJsonSpec|Point)[] edges: [number, number][] '@type': 'siibra-ex/annotation/polyline' diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts index d80d959f8..b6d4e8143 100644 --- a/src/atlasComponents/userAnnotations/tools/service.ts +++ b/src/atlasComponents/userAnnotations/tools/service.ts @@ -7,7 +7,7 @@ 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"; -import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TExportFormats, TCallbackFunction, TSandsPolyLine, TSandsPoint, TSandsLine } from "./type"; +import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson } from "./type"; import { switchMapWaitFor } from "src/util/fn"; import { Polygon } from "./poly"; import { Line } from "./line"; @@ -87,6 +87,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ private managedAnnotations: IAnnotationGeometry[] = [] public managedAnnotations$ = this.managedAnnotationsStream$.pipe( scanCollapse(), + shareReplay(1), ) private registeredTools: { @@ -374,11 +375,11 @@ export class ModularUserAnnotationToolService implements OnDestroy{ const managedAnnotationUpdate$ = combineLatest([ this.forcedAnnotationRefresh$, this.ngAnnotations$.pipe( + scanCollapse(), switchMap(switchMapWaitFor({ condition: () => !!this.ngAnnotationLayer, leading: true })), - scanCollapse(), ) ]).pipe( map(([_, ngAnnos]) => ngAnnos), @@ -529,7 +530,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{ }) } - parseAnnotationObject(json: TSandsPolyLine | TSandsPoint | TSandsLine): IAnnotationGeometry{ + parseAnnotationObject(json: TSands | TGeometryJson): IAnnotationGeometry{ if (json['@type'] === 'tmp/poly') { return Polygon.fromSANDS(json) } @@ -539,6 +540,15 @@ export class ModularUserAnnotationToolService implements OnDestroy{ if (json['@type'] === 'https://openminds.ebrains.eu/sands/CoordinatePoint') { return Point.fromSANDS(json) } + if (json['@type'] === 'siibra-ex/annotation/point') { + return Point.fromJSON(json) + } + if (json['@type'] === 'siibra-ex/annotation/line') { + return Line.fromJSON(json) + } + if (json['@type'] === 'siibra-ex/annotation/polyline') { + return Polygon.fromJSON(json) + } throw new Error(`cannot parse annotation object`) } diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts index 3f1ccf1f1..2355942cc 100644 --- a/src/atlasComponents/userAnnotations/tools/type.ts +++ b/src/atlasComponents/userAnnotations/tools/type.ts @@ -2,6 +2,9 @@ import { InjectionToken } from "@angular/core" import { merge, Observable, of, Subject, Subscription } from "rxjs" import { filter, map, mapTo, pairwise, switchMap, switchMapTo, takeUntil, withLatestFrom } from 'rxjs/operators' import { getUuid } from "src/util/fn" +import { TLineJsonSpec } from "./line" +import { TPointJsonSpec } from "./point" +import { TPolyJsonSpec } from "./poly" /** * base class to be extended by all annotation tools @@ -202,6 +205,9 @@ type TSandsQValue = { } type TSandsCoord = [TSandsQValue, TSandsQValue] | [TSandsQValue, TSandsQValue, TSandsQValue] +export type TGeometryJson = TPointJsonSpec | TLineJsonSpec | TPolyJsonSpec +export type TSands = TSandsPolyLine | TSandsLine | TSandsPoint + export type TSandsPolyLine = { coordinates: TSandsCoord[] closed: boolean diff --git a/src/contextMenuModule/service.ts b/src/contextMenuModule/service.ts index ce521f62d..be34051f9 100644 --- a/src/contextMenuModule/service.ts +++ b/src/contextMenuModule/service.ts @@ -4,11 +4,23 @@ import { Injectable, TemplateRef, ViewContainerRef } from "@angular/core" import { ReplaySubject, Subject, Subscription } from "rxjs" import { RegDeregController } from "src/util/regDereg.base" -type TTmplRef = { +type TTmpl = { tmpl: TemplateRef<any> data: any } +type TSimple = { + data: { + message: string + iconClass?: string + } +} + +type TTmplRef = (TTmpl | TSimple) & { + order?: number + onClick?: Function +} + type CtxMenuInterArg<T> = { context: T append: (arg: TTmplRef) => void diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index e6983e8ff..cc6316f18 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -197,6 +197,9 @@ export class ViewerCmp implements OnDestroy { @ViewChild('viewerStatusCtxMenu', { read: TemplateRef }) private viewerStatusCtxMenu: TemplateRef<any> + @ViewChild('viewerStatusRegionCtxMenu', { read: TemplateRef }) + private viewerStatusRegionCtxMenu: TemplateRef<any> + public context: TContextArg<TSupportedViewers> private templateSelected: any private getRegionFromlabelIndexId: Function @@ -247,8 +250,25 @@ export class ViewerCmp implements OnDestroy { ngAfterViewInit(){ const cb: TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>> = ({ append, context }) => { - let hoveredRegions = [] + /** + * first append general viewer info + */ + append({ + tmpl: this.viewerStatusCtxMenu, + data: { + context, + metadata: { + template: this.templateSelected, + } + }, + order: 0 + }) + + /** + * check hovered region + */ + let hoveredRegions = [] if (context.viewerType === 'nehuba') { hoveredRegions = (context as TContextArg<'nehuba'>).payload.nehuba.reduce( (acc, curr) => acc.concat( @@ -272,16 +292,17 @@ export class ViewerCmp implements OnDestroy { hoveredRegions = (context as TContextArg<'threeSurfer'>).payload._mouseoverRegion } - append({ - tmpl: this.viewerStatusCtxMenu, - data: { - context, - metadata: { - template: this.templateSelected, - hoveredRegions - } - } - }) + if (hoveredRegions.length > 0) { + append({ + tmpl: this.viewerStatusRegionCtxMenu, + data: { + context, + metadata: { hoveredRegions } + }, + order: 5 + }) + } + return true } this.viewerModuleSvc.register(cb) diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 5f39905cf..4d51060ef 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -1060,11 +1060,14 @@ <!-- context menu template --> <ng-template #viewerCtxMenuTmpl let-tmplRefs="tmplRefs"> - <mat-card class="p-0" + <mat-card class="p-0 d-flex flex-column" [iav-key-listener]="[{type: 'keydown', target: 'document', capture: true}]" (iav-key-event)="disposeCtxMenu()" (iav-outsideClick)="disposeCtxMenu()"> - <mat-card-content *ngFor="let tmplRef of tmplRefs"> + <mat-card-content *ngFor="let tmplRef of tmplRefs" + class="m-0" + [ngStyle]="{order: tmplRef.order || 0}"> + <mat-divider></mat-divider> <!-- template provided --> <ng-template [ngIf]="tmplRef.tmpl" @@ -1077,6 +1080,8 @@ <ng-template #fallbackTmpl> {{ tmplRef.data.message || 'test' }} </ng-template> + + <mat-divider></mat-divider> </mat-card-content> </mat-card> </ng-template> @@ -1124,29 +1129,30 @@ DEFAULT </ng-container> </ng-container> + </mat-list> +</ng-template> - <!-- hovered ROIs --> - <ng-template [ngIf]="data.metadata.hoveredRegions.length > 0"> - <mat-divider></mat-divider> - - <mat-list-item *ngFor="let hoveredR of data.metadata.hoveredRegions"> - <span mat-line> - {{ hoveredR.displayName || hoveredR.name }} - </span> - <span mat-line class="text-muted"> - <i class="fas fa-brain"></i> - <span> - Brain region - </span> +<ng-template #viewerStatusRegionCtxMenu let-data> + <!-- hovered ROIs --> + <mat-list> + <mat-list-item *ngFor="let hoveredR of data.metadata.hoveredRegions; let first = first"> + <mat-divider class="top-0" *ngIf="!first"></mat-divider> + <span mat-line> + {{ hoveredR.displayName || hoveredR.name }} + </span> + <span mat-line class="text-muted"> + <i class="fas fa-brain"></i> + <span> + Brain region </span> + </span> - <!-- lookup region --> - <button mat-icon-button - (click)="selectRoi(hoveredR)" - ctx-menu-dismiss> - <i class="fas fa-search"></i> - </button> - </mat-list-item> - </ng-template> + <!-- lookup region --> + <button mat-icon-button + (click)="selectRoi(hoveredR)" + ctx-menu-dismiss> + <i class="fas fa-search"></i> + </button> + </mat-list-item> </mat-list> </ng-template> -- GitLab