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