From f5085e496e7d8686ee1d75ddff1f62012dfdb661 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Wed, 4 May 2022 10:07:11 +0200
Subject: [PATCH] feat: added plugin api for adding annotations chore: remove
 unused

---
 deploy/plugins/index.js                       |  35 ++++--
 src/api/service.ts                            |  66 ++++++++++-
 src/atlasComponents/sapi/type.ts              |   7 ++
 .../features/entry/entry.component.ts         |  92 ++++++++-------
 src/auth/auth.service.ts                      |   3 +-
 src/messaging/service.ts                      |  12 +-
 src/plugin/handshake.md                       |  22 ++++
 src/plugin/plugin.module.ts                   |   2 +
 .../pluginBanner/pluginBanner.component.ts    |  37 +++---
 .../pluginBanner/pluginBanner.template.html   |  11 +-
 .../pluginPortal/pluginPortal.component.ts    |  37 +++---
 src/plugin/request.md                         |  79 ++++++++++++-
 src/plugin/service.ts                         |   8 ++
 src/plugin/types.ts                           |   2 +-
 .../parseRouteToTmplParcReg.spec.ts           |  24 ----
 src/routerModule/parseRouteToTmplParcReg.ts   | 110 ------------------
 src/state/annotations/actions.ts              |   6 +-
 src/state/annotations/index.ts                |   2 +-
 src/state/annotations/selectors.ts            |  23 ++++
 src/state/annotations/store.ts                |  34 +++++-
 src/state/atlasSelection/selectors.ts         |   2 +-
 src/viewerModule/nehuba/annotation/effects.ts |  20 ++++
 src/viewerModule/nehuba/annotation/service.ts |  89 ++++++++++++++
 .../layerCtrl.service/layerCtrl.service.ts    |   3 +-
 src/viewerModule/nehuba/module.ts             |  15 ++-
 25 files changed, 491 insertions(+), 250 deletions(-)
 delete mode 100644 src/routerModule/parseRouteToTmplParcReg.spec.ts
 delete mode 100644 src/routerModule/parseRouteToTmplParcReg.ts
 create mode 100644 src/viewerModule/nehuba/annotation/effects.ts
 create mode 100644 src/viewerModule/nehuba/annotation/service.ts

diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js
index 8d6bee52d..a90197652 100644
--- a/deploy/plugins/index.js
+++ b/deploy/plugins/index.js
@@ -6,24 +6,26 @@
 const express = require('express')
 const lruStore = require('../lruStore')
 const got = require('got')
+const { URL } = require('url')
+const path = require("path")
 const router = express.Router()
-const DEV_PLUGINS = (() => {
+const V2_7_DEV_PLUGINS = (() => {
   try {
     return JSON.parse(
-      process.env.DEV_PLUGINS || `[]`
+      process.env.V2_7_DEV_PLUGINS || `[]`
     )
   } catch (e) {
     console.warn(`Parsing DEV_PLUGINS failed: ${e}`)
     return []
   }
 })()
-const PLUGIN_URLS = (process.env.PLUGIN_URLS && process.env.PLUGIN_URLS.split(';')) || []
-const STAGING_PLUGIN_URLS = (process.env.STAGING_PLUGIN_URLS && process.env.STAGING_PLUGIN_URLS.split(';')) || []
+const V2_7_PLUGIN_URLS = (process.env.V2_7_PLUGIN_URLS && process.env.V2_7_PLUGIN_URLS.split(';')) || []
+const V2_7_STAGING_PLUGIN_URLS = (process.env.V2_7_STAGING_PLUGIN_URLS && process.env.V2_7_STAGING_PLUGIN_URLS.split(';')) || []
 
 router.get('', (_req, res) => {
   return res.status(200).json([
-    ...PLUGIN_URLS,
-    ...STAGING_PLUGIN_URLS
+    ...V2_7_PLUGIN_URLS,
+    ...V2_7_STAGING_PLUGIN_URLS
   ])
 })
 
@@ -32,8 +34,8 @@ const getKey = url => `plugin:manifest-cache:${url}}`
 router.get('/manifests', async (_req, res) => {
 
   const allManifests = await Promise.all([
-    ...PLUGIN_URLS,
-    ...STAGING_PLUGIN_URLS
+    ...V2_7_PLUGIN_URLS,
+    ...V2_7_STAGING_PLUGIN_URLS
   ].map(async url => {
     const key = getKey(url)
     
@@ -47,14 +49,25 @@ router.get('/manifests', async (_req, res) => {
     } catch (e) {
       const resp = await got(url)
       const json = JSON.parse(resp.body)
+
+      const { iframeUrl, 'siibra-explorer': flag } = json
+      if (!flag) return null
+      if (!iframeUrl) return null
+      const u = new URL(url)
       
-      await store.set(key, JSON.stringify(json), { maxAge: 1000 * 60 * 60 })
-      return json
+      let replaceObj = {}
+      if (!/^https?:\/\//.test(iframeUrl)) {
+        u.pathname = path.resolve(path.dirname(u.pathname), iframeUrl)
+        replaceObj['iframeUrl'] = u.toString()
+      }
+      const returnObj = {...json, ...replaceObj}
+      await store.set(key, JSON.stringify(returnObj), { maxAge: 1000 * 60 * 60 })
+      return returnObj
     }
   }))
 
   res.status(200).json(
-    [...DEV_PLUGINS, ...allManifests.filter(v => !!v)]
+    [...V2_7_DEV_PLUGINS, ...allManifests.filter(v => !!v)]
   )
 })
 
diff --git a/src/api/service.ts b/src/api/service.ts
index 2427dcaa9..41b100f78 100644
--- a/src/api/service.ts
+++ b/src/api/service.ts
@@ -3,7 +3,8 @@ import { select, Store } from "@ngrx/store";
 import { Subject } from "rxjs";
 import { distinctUntilChanged, filter, map, take } from "rxjs/operators";
 import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel, OpenMINDSCoordinatePoint } from "src/atlasComponents/sapi";
-import { MainState, atlasSelection, userInteraction } from "src/state"
+import { SxplrCoordinatePointExtension } from "src/atlasComponents/sapi/type";
+import { MainState, atlasSelection, userInteraction, annotation } from "src/state"
 import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
 import { CANCELLABLE_DIALOG, CANCELLABLE_DIALOG_OPTS } from "src/util/interfaces";
 import { Booth, BoothResponder, createBroadcastingJsonRpcChannel, JRPCRequest, JRPCResp } from "./jsonrpc"
@@ -71,6 +72,27 @@ export type ApiBoothEvents = {
     }
     response: SapiRegionModel | OpenMINDSCoordinatePoint
   }
+  
+  addAnnotations: {
+    request: {
+      annotations: SxplrCoordinatePointExtension[]
+    }
+    response: 'OK'
+  }
+
+  rmAnnotations: {
+    request: {
+      annotations: AtId[]
+    }
+    response: 'OK'
+  }
+
+  exit: {
+    request: {
+      requests: JRPCRequest<keyof ApiBoothEvents, ApiBoothEvents[keyof ApiBoothEvents]['request']>[]
+    }
+    response: 'OK'
+  }
 
   cancelRequest: {
     request: {
@@ -225,7 +247,7 @@ export class ApiService implements BoothResponder<ApiBoothEvents>{
       this.broadcastCh.emit('allRegions', regions)
     })
   }
-  async onRequest(event: JRPCRequest<keyof ApiBoothEvents, null>): Promise<void | JRPCResp<ApiBoothEvents[keyof ApiBoothEvents]['response'], string>> {
+  async onRequest(event: JRPCRequest<keyof ApiBoothEvents, unknown>): Promise<void | JRPCResp<ApiBoothEvents[keyof ApiBoothEvents]['response'], string>> {
     /**
      * if id is not present, then it's a no-op
      */
@@ -386,6 +408,46 @@ export class ApiService implements BoothResponder<ApiBoothEvents>{
         }
       })
     }
+    case 'addAnnotations': {
+      const { annotations } = event.params as ApiBoothEvents['addAnnotations']['request']
+      const ann = annotations as (annotation.Annotation<'openminds'>)[]
+      this.store.dispatch(
+        annotation.actions.addAnnotations({
+          annotations: ann
+        })
+      )
+      if (event.id) {
+        return {
+          jsonrpc: '2.0',
+          id: event.id,
+          result: 'OK'
+        }
+      }
+      break
+    }
+    case 'rmAnnotations': {
+      const { annotations } = event.params as ApiBoothEvents['rmAnnotations']['request']
+      this.store.dispatch(
+        annotation.actions.rmAnnotations({
+          annotations
+        })
+      )
+      if (event.id){
+        return {
+          jsonrpc: '2.0',
+          id: event.id,
+          result: 'OK'
+        }
+      }
+      break
+    }
+    case 'exit': {
+      const { requests } = event.params as ApiBoothEvents['exit']['request']
+      for (const req of requests) {
+        await this.onRequest(req)
+      }
+      break
+    }
     case 'cancelRequest': {
       const { id } = event.params as ApiBoothEvents['cancelRequest']['request']
       const idx = this.requestUserQueue.findIndex(q => q.id === id)
diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts
index 27574a962..1796c54d0 100644
--- a/src/atlasComponents/sapi/type.ts
+++ b/src/atlasComponents/sapi/type.ts
@@ -16,6 +16,13 @@ export type SapiSpaceModel = components["schemas"]["SapiSpaceModel"]
 export type SapiParcellationModel = components["schemas"]["SapiParcellationModel"]
 export type SapiRegionModel = components["schemas"]["siibra__openminds__SANDS__v3__atlas__parcellationEntityVersion__Model"]
 export type OpenMINDSCoordinatePoint = components['schemas']['siibra__openminds__SANDS__v3__miscellaneous__coordinatePoint__Model']
+export type SxplrCoordinatePointExtension = {
+  openminds: OpenMINDSCoordinatePoint
+  name: string
+  description: string
+  color: string
+  '@id': string // should match the id of opendminds specs
+}
 
 export type SapiRegionMapInfoModel = components["schemas"]["NiiMetadataModel"]
 export type SapiVOIDataResponse = components["schemas"]["VOIDataModel"]
diff --git a/src/atlasComponents/sapiViews/features/entry/entry.component.ts b/src/atlasComponents/sapiViews/features/entry/entry.component.ts
index 53b27fb30..691af83bd 100644
--- a/src/atlasComponents/sapiViews/features/entry/entry.component.ts
+++ b/src/atlasComponents/sapiViews/features/entry/entry.component.ts
@@ -1,9 +1,9 @@
 import { Component, Input, OnDestroy } from "@angular/core";
 import { Store } from "@ngrx/store";
-import { AnnotationLayer, TNgAnnotationPoint } from "src/atlasComponents/annotations";
+import { TNgAnnotationPoint } from "src/atlasComponents/annotations";
 import { SapiFeatureModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel, CLEANED_IEEG_DATASET_TYPE } from "src/atlasComponents/sapi";
 import { IeegOnFocusEvent, ContactPoint, Electrode, Session, IeegOnDefocusEvent } from "../ieeg";
-import { atlasSelection, annotation, atlasAppearance } from "src/state"
+import { atlasSelection, annotation } from "src/state"
 
 @Component({
   selector: 'sxplr-sapiviews-features-entry',
@@ -39,11 +39,8 @@ export class FeatureEntryCmp implements OnDestroy{
     ieeg: CLEANED_IEEG_DATASET_TYPE
   }
 
-  static readonly IEEG_ANNOTATION_LAYER_RED = `ieeg-annotation-layer-red`
-  static readonly IEEG_ANNOTATION_LAYER_WHITE = `ieeg-annotation-layer-white`
-  private ieegRedAnnLayer: AnnotationLayer
-  private ieegWhiteAnnLayer: AnnotationLayer
-  
+  private addedAnnotations: annotation.UnionAnnotation[] = []
+
   ieegOnFocus(ev: IeegOnFocusEvent){
     if (ev.contactPoint) {
       /**
@@ -63,30 +60,51 @@ export class FeatureEntryCmp implements OnDestroy{
       /**
        * 
        */
-      if (!this.ieegRedAnnLayer) {
-        this.ieegRedAnnLayer = new AnnotationLayer(FeatureEntryCmp.IEEG_ANNOTATION_LAYER_RED, "#ff0000")
-      }
-      if (!this.ieegWhiteAnnLayer) {
-        this.ieegWhiteAnnLayer = new AnnotationLayer(FeatureEntryCmp.IEEG_ANNOTATION_LAYER_WHITE, "#ffffff")
-      }
       const allInRoiPoints: TNgAnnotationPoint[] = this.getPointsFromSession(ev.session, true)
       const allNonInRoiPoints: TNgAnnotationPoint[] = this.getPointsFromSession(ev.session, false)
+      const annotationsToBeAdded: annotation.UnionAnnotation[] = []
       for (const pt of allInRoiPoints) {
-        this.ieegRedAnnLayer.addAnnotation(pt)
+        annotationsToBeAdded.push({
+          "@id": pt.id,
+          color: annotation.AnnotationColor.RED,
+          openminds: {
+            "@id": pt.id,
+            "@type": "https://openminds.ebrains.eu/sands/CoordinatePoint",
+            coordinateSpace: {
+              "@id": this.space["@id"]
+            },
+            coordinates: pt.point.map(v => {
+              return {
+                value: v / 1e6
+              }
+            })
+          },
+          name: pt.description || "Untitled"
+        })
       }
       for (const pt of allNonInRoiPoints) {
-        this.ieegWhiteAnnLayer.addAnnotation(pt)
+        annotationsToBeAdded.push({
+          "@id": pt.id,
+          color: annotation.AnnotationColor.WHITE,
+          openminds: {
+            "@id": pt.id,
+            "@type": "https://openminds.ebrains.eu/sands/CoordinatePoint",
+            coordinateSpace: {
+              "@id": this.space["@id"]
+            },
+            coordinates: pt.point.map(v => {
+              return {
+                value: v / 1e6
+              }
+            })
+          },
+          name: pt.description || "Untitled"
+        })
       }
+      this.addedAnnotations = annotationsToBeAdded
       this.store.dispatch(
         annotation.actions.addAnnotations({
-          annotations: [...allInRoiPoints, ...allNonInRoiPoints].map(p => {
-            return { "@id": p.id }
-          })
-        })
-      )
-      this.store.dispatch(
-        atlasAppearance.actions.setOctantRemoval({
-          flag: false
+          annotations: annotationsToBeAdded
         })
       )
     }
@@ -103,31 +121,15 @@ export class FeatureEntryCmp implements OnDestroy{
           })
         })
       )
-
-      this.store.dispatch(
-        atlasAppearance.actions.setOctantRemoval({
-          flag: true
-        })
-      )
-
-      if (this.ieegRedAnnLayer) {
-        for (const pt of allInRoiPoints) {
-          this.ieegRedAnnLayer.removeAnnotation(pt)
-        }
-      }
-      
-      if (this.ieegWhiteAnnLayer) {
-        for (const pt of allNonInRoiPoints) {
-          this.ieegWhiteAnnLayer.removeAnnotation(pt)
-        }
-      }
-      
-
     }
   }
+
   ngOnDestroy(): void {
-    if (this.ieegRedAnnLayer) this.ieegRedAnnLayer.dispose()
-    if (this.ieegWhiteAnnLayer) this.ieegWhiteAnnLayer.dispose()
+    this.store.dispatch(
+      annotation.actions.rmAnnotations({
+        annotations: this.addedAnnotations
+      })
+    )
   }
 
   constructor(
diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts
index 487fb0d5e..e2067301c 100644
--- a/src/auth/auth.service.ts
+++ b/src/auth/auth.service.ts
@@ -2,6 +2,7 @@ import { HttpClient } from "@angular/common/http";
 import { Injectable, OnDestroy } from "@angular/core";
 import { Observable, of, Subscription } from "rxjs";
 import { catchError, map, shareReplay } from "rxjs/operators";
+import { environment } from "src/environments/environment"
 
 const IV_REDIRECT_TOKEN = `IV_REDIRECT_TOKEN`
 
@@ -37,7 +38,7 @@ export class AuthService implements OnDestroy {
   constructor(
     private httpClient: HttpClient,
   ) {
-    this.user$ = this.httpClient.get<TUserRouteResp>(`user`).pipe(
+    this.user$ = this.httpClient.get<TUserRouteResp>(`${environment.BACKEND_URL || ''}user`).pipe(
       map(json => {
         if (json.error) {
           throw new Error(json.message || 'User not loggedin.')
diff --git a/src/messaging/service.ts b/src/messaging/service.ts
index d3649cdf3..d810a7fce 100644
--- a/src/messaging/service.ts
+++ b/src/messaging/service.ts
@@ -71,11 +71,13 @@ export class MessagingService {
           result
         }, origin)
       } catch (error) {
-        src.postMessage({
-          id,
-          jsonrpc: '2.0',
-          error
-        }, origin)
+        if (src) {
+          src.postMessage({
+            id,
+            jsonrpc: '2.0',
+            error
+          }, origin)
+        }
       }
     })
 
diff --git a/src/plugin/handshake.md b/src/plugin/handshake.md
index e69de29bb..a55a97d23 100644
--- a/src/plugin/handshake.md
+++ b/src/plugin/handshake.md
@@ -0,0 +1,22 @@
+# Handshake API
+
+Handshake messages are meant for siibra-explorer to probe if the plugin is alive and well (and also a way for the plugin to check if siibra-explorer is responsive)
+
+<!-- the API reference below are auto generated by generateTypes.js  -->
+<!-- do not edit, as the edit will be overwritten by the auto generation -->
+
+## API
+### `sxplr.init`
+
+- request
+
+  ```ts
+  null
+  ```
+
+- response
+
+  ```ts
+  {"name": string}
+  ```
+
diff --git a/src/plugin/plugin.module.ts b/src/plugin/plugin.module.ts
index 08d54d85d..ea1ec730a 100644
--- a/src/plugin/plugin.module.ts
+++ b/src/plugin/plugin.module.ts
@@ -1,4 +1,5 @@
 import { CommonModule, DOCUMENT } from "@angular/common";
+import { HttpClientModule } from "@angular/common/http";
 import { NgModule } from "@angular/core";
 import { LoggingModule } from "src/logging";
 import { AngularMaterialModule } from "src/sharedModules";
@@ -15,6 +16,7 @@ import { PluginPortal } from "./pluginPortal/pluginPortal.component";
     LoggingModule,
     UtilModule,
     AngularMaterialModule,
+    HttpClientModule,
   ],
   declarations: [
     PluginBannerUI,
diff --git a/src/plugin/pluginBanner/pluginBanner.component.ts b/src/plugin/pluginBanner/pluginBanner.component.ts
index e6dac94af..df682e4cf 100644
--- a/src/plugin/pluginBanner/pluginBanner.component.ts
+++ b/src/plugin/pluginBanner/pluginBanner.component.ts
@@ -1,9 +1,10 @@
-import { Component, ViewChild, TemplateRef } from "@angular/core";
+import { Component, TemplateRef } from "@angular/core";
 import { MatDialog } from "@angular/material/dialog";
 import { environment } from 'src/environments/environment';
-import { MatSnackBar } from "@angular/material/snack-bar";
 import { PluginService } from "../service";
 import { PluginManifest } from "../types";
+import { combineLatest, Observable, Subject } from "rxjs";
+import { map, scan, startWith } from "rxjs/operators";
 
 @Component({
   selector : 'plugin-banner',
@@ -17,17 +18,14 @@ export class PluginBannerUI {
 
   EXPERIMENTAL_FEATURE_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG
 
-  pluginManifests: PluginManifest[] = []
-
   constructor(
     private svc: PluginService,
     private matDialog: MatDialog,
-    private matSnackbar: MatSnackBar,
   ) {
   }
 
   public launchPlugin(plugin: PluginManifest) {
-    this.svc.launchPlugin(plugin.url)
+    this.svc.launchPlugin(plugin.iframeUrl)
   }
 
   public showTmpl(tmpl: TemplateRef<any>){
@@ -36,12 +34,25 @@ export class PluginBannerUI {
     })
   }
 
-  public loadingThirdpartyPlugin = false
-  public async addThirdPartyPlugin(manifestUrl: string) {
-    this.matSnackbar.open(`Adding third party plugin is current unavailable.`)
-  }
-
-  test(){
-    this.svc.launchPlugin('http://localhost:8000')
+  private thirdpartyPlugin$: Subject<{name: 'Added Plugin', iframeUrl: string}> = new Subject()
+
+  availablePlugins$: Observable<{
+    name: string
+    iframeUrl: string
+  }[]> = combineLatest([
+    this.svc.pluginManifests$,
+    this.thirdpartyPlugin$.pipe(
+      scan((acc, curr) => acc.concat(curr), []),
+      startWith([])
+    ),
+  ]).pipe(
+    map(([builtIn, thirdParty]) => [...builtIn, ...thirdParty])
+  )
+
+  public addThirdPartyPlugin(iframeUrl: string) {
+    this.thirdpartyPlugin$.next({
+      name: 'Added Plugin',
+      iframeUrl
+    })
   }
 }
diff --git a/src/plugin/pluginBanner/pluginBanner.template.html b/src/plugin/pluginBanner/pluginBanner.template.html
index 80ffd873c..4f9ef3ae2 100644
--- a/src/plugin/pluginBanner/pluginBanner.template.html
+++ b/src/plugin/pluginBanner/pluginBanner.template.html
@@ -1,16 +1,12 @@
 <mat-action-list>
   <button mat-menu-item
-    *ngFor="let plugin of pluginManifests"
+    *ngFor="let plugin of availablePlugins$ | async"
     (click)="launchPlugin(plugin)">
     <span>
       {{ plugin.name }}
     </span>
   </button>
 
-  <button *ngIf="EXPERIMENTAL_FEATURE_FLAG" mat-menu-item (click)="test()">
-    test
-  </button>
-
   <button mat-menu-item *ngIf="EXPERIMENTAL_FEATURE_FLAG"
     (click)="showTmpl(thirdPartyPluginTmpl)">
     <span>
@@ -28,9 +24,9 @@
     <form>
       <mat-form-field class="d-block">
         <mat-label>
-          manifest.json URL
+          iframe index.html URL
         </mat-label>
-        <input type="text" matInput placeholder="https://example.com/manifest.json" #urlInput>
+        <input type="text" matInput placeholder="https://example.com/index.html" #urlInput>
       </mat-form-field>
     </form>
   </mat-dialog-content>
@@ -38,7 +34,6 @@
   <mat-dialog-actions align="end">
     <button (click)="addThirdPartyPlugin(urlInput.value)"
       mat-raised-button
-      [disabled]="loadingThirdpartyPlugin"
       color="primary">
       Load
     </button>
diff --git a/src/plugin/pluginPortal/pluginPortal.component.ts b/src/plugin/pluginPortal/pluginPortal.component.ts
index ce0da6086..7d5b4d421 100644
--- a/src/plugin/pluginPortal/pluginPortal.component.ts
+++ b/src/plugin/pluginPortal/pluginPortal.component.ts
@@ -109,25 +109,28 @@ export class PluginPortal implements AfterViewInit, OnDestroy, ListenerChannel{
        * listening to plugin requests
        * only start after boothVisitor is defined
        */
-      this.sub.push(
-        fromEvent<MessageEvent>(window, 'message').pipe(
-          startWith(null as MessageEvent),
-          filter(msg => !!this.boothVisitor && msg.data.jsonrpc === "2.0" && !!msg.data.method)
-        ).subscribe(async msg => {
-          try {
-            const result = await this.boothVisitor.request(msg.data)
+      const sub = fromEvent<MessageEvent>(window, 'message').pipe(
+        startWith(null as MessageEvent),
+        filter(msg => !!this.boothVisitor && msg.data.jsonrpc === "2.0" && !!msg.data.method && msg.origin === this.origin)
+      ).subscribe(async msg => {
+        try {
+          if (msg.data.method === `${namespace}.exit`) {
+            sub.unsubscribe()
+          }
+          const result = await this.boothVisitor.request(msg.data)
+          if (!!result) {
             this.childWindow.postMessage(result, this.origin)
-          } catch (e) {
-            this.childWindow.postMessage({
-              id: msg.data.id,
-              error: {
-                code: -32603,
-                message: e.toString()
-              }
-            }, this.origin)
           }
-        })
-      )
+        } catch (e) {
+          this.childWindow.postMessage({
+            id: msg.data.id,
+            error: {
+              code: -32603,
+              message: e.toString()
+            }
+          }, this.origin)
+        }
+      })
     }
   }
   notify(payload: JRPCRequest<keyof BroadCastingApiEvents, BroadCastingApiEvents[keyof BroadCastingApiEvents]>) {
diff --git a/src/plugin/request.md b/src/plugin/request.md
index 6260c2ae2..a2575863a 100644
--- a/src/plugin/request.md
+++ b/src/plugin/request.md
@@ -1,6 +1,38 @@
 # Request API
 
-TBD
+Request  messages are sent when the plugin requests siibra-explorer to do something on its behalf.
+
+Be it request the user to select a region, a point, navigate to a specific location etc. 
+
+> :warning: Please note that `beforeunload` window event does not fire on iframe windows. Plugins should do whatever cleanup it needs, then send the message `sxplr.exit`. 
+
+```javascript
+
+let parentWindow
+window.addEventListener('message', ev => {
+  const { source, data, origin } = msg
+  const { id, method, params, result, error } = data
+
+  if (method === "sxplr.init") {
+    parentWindow = source
+  }
+})
+
+window.addEventListener('pagehide', () => {
+
+  // do cleanup
+  // n.b. since iframe unload usually do not trigger DOM events
+  // one will need to manually trigger destroying any apps manually
+
+  parentWindow.postMessage({
+    jsonrpc: '2.0',
+    method: `sxplr.exit`,
+    params: {
+      requests: [] // any remaining requests to be carried out
+    }
+  })
+})
+```
 
 <!-- the API reference below are auto generated by generateTypes.js  -->
 <!-- do not edit, as the edit will be overwritten by the auto generation -->
@@ -126,6 +158,51 @@ TBD
   ```
 
 
+### `sxplr.addAnnotations`
+
+- request
+
+  ```ts
+  {"annotations": SxplrCoordinatePointExtension[]}
+  ```
+
+- response
+
+  ```ts
+  'OK'
+  ```
+
+
+### `sxplr.rmAnnotations`
+
+- request
+
+  ```ts
+  {"annotations": AtId[]}
+  ```
+
+- response
+
+  ```ts
+  'OK'
+  ```
+
+
+### `sxplr.exit`
+
+- request
+
+  ```ts
+  {"requests": JRPCRequest[]}
+  ```
+
+- response
+
+  ```ts
+  'OK'
+  ```
+
+
 ### `sxplr.cancelRequest`
 
 - request
diff --git a/src/plugin/service.ts b/src/plugin/service.ts
index 97a72e542..4f5a5e74f 100644
--- a/src/plugin/service.ts
+++ b/src/plugin/service.ts
@@ -1,9 +1,11 @@
+import { HttpClient } from "@angular/common/http";
 import { Injectable, Injector, NgZone } from "@angular/core";
 import { WIDGET_PORTAL_TOKEN } from "src/widget/constants";
 import { WidgetService } from "src/widget/service";
 import { WidgetPortal } from "src/widget/widgetPortal/widgetPortal.component";
 import { setPluginSrc } from "./const";
 import { PluginPortal } from "./pluginPortal/pluginPortal.component";
+import { environment } from "src/environments/environment"
 
 @Injectable({
   providedIn: 'root'
@@ -16,8 +18,14 @@ export class PluginService {
     private wSvc: WidgetService,
     private injector: Injector,
     private zone: NgZone,
+    private http: HttpClient
   ){}
 
+  pluginManifests$ = this.http.get<{
+    'siibra-explorer': true
+    name: string
+    iframeUrl: string
+  }[]>(`${environment.BACKEND_URL || ''}plugins/manifests`)
 
   async launchPlugin(htmlSrc: string){
     if (this.loadedPlugins.includes(htmlSrc)) return
diff --git a/src/plugin/types.ts b/src/plugin/types.ts
index 83ba2e312..f51aa57d9 100644
--- a/src/plugin/types.ts
+++ b/src/plugin/types.ts
@@ -1,4 +1,4 @@
 export type PluginManifest = {
   name: string
-  url: string
+  iframeUrl: string
 }
diff --git a/src/routerModule/parseRouteToTmplParcReg.spec.ts b/src/routerModule/parseRouteToTmplParcReg.spec.ts
deleted file mode 100644
index d81c9491c..000000000
--- a/src/routerModule/parseRouteToTmplParcReg.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { parseSearchParamForTemplateParcellationRegion } from './parseRouteToTmplParcReg'
-
-const url = `/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2/p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-26/@:0.0.0.-W000..2_ZG29.-ASCS.2-8jM2._aAY3..BSR0..0.1w4W0~.0..1jtG`
-
-const fakeState = {
-}
-const fakeTmpl = {
-  '@id': 'foobar'
-}
-const fakeParc = {
-  '@id': 'buzbaz'
-}
-const fakeRegions = [{
-  ngId: 'foo',
-  labelIndex: 152
-}]
-
-describe('parseRouteToTmplParcReg.ts', () => {
-  describe('> parseSearchParamForTemplateParcellationRegion', () => {
-    it('> parses selected region properly', () => {
-      
-    })
-  })
-})
\ No newline at end of file
diff --git a/src/routerModule/parseRouteToTmplParcReg.ts b/src/routerModule/parseRouteToTmplParcReg.ts
deleted file mode 100644
index ee4d505a1..000000000
--- a/src/routerModule/parseRouteToTmplParcReg.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import { decodeToNumber, separator } from './cipher'
-
-import {
-  TUrlAtlas,
-  TUrlPathObj,
-} from './type'
-import { UrlTree } from '@angular/router'
-import { serializeSegment } from "src/viewerModule/nehuba/util"
-
-
-export const PARSE_ERROR = {
-  TEMPALTE_NOT_SET: 'TEMPALTE_NOT_SET',
-  TEMPLATE_NOT_FOUND: 'TEMPLATE_NOT_FOUND',
-  PARCELLATION_NOT_UPDATED: 'PARCELLATION_NOT_UPDATED',
-}
-
-export const encodeId = (id: string) => id && id.replace(/\//g, ':')
-export const decodeId = (codedId: string) => codedId && codedId.replace(/:/g, '/')
-
-export function parseSearchParamForTemplateParcellationRegion(obj: TUrlPathObj<string[], TUrlAtlas<string[]>>, fullPath: UrlTree, state: any, warnCb: (arg: string) => void) {
-
-  /**
-   * TODO if search param of either template or parcellation is incorrect, wrong things are searched
-   */
-  
-
-  const templateSelected = (() => {
-    const { fetchedTemplates } = state.viewerState
-
-    const searchedId = obj.t && decodeId(obj.t[0])
-
-    if (!searchedId) return null
-    const templateToLoad = fetchedTemplates.find(template => (template['@id'] || template['fullId']) === searchedId)
-    if (!templateToLoad) { throw new Error(PARSE_ERROR.TEMPLATE_NOT_FOUND) }
-    return templateToLoad
-  })()
-
-  const parcellationSelected = (() => {
-    if (!templateSelected) return null
-    const searchedId = obj.p && decodeId(obj.p[0])
-
-    const parcellationToLoad = templateSelected.parcellations.find(parcellation => (parcellation['@id'] || parcellation['fullId']) === searchedId)
-    if (!parcellationToLoad) { 
-      warnCb(`parcellation with id ${searchedId} not found... load the first parc instead`)
-    }
-    return parcellationToLoad || templateSelected.parcellations[0]
-  })()
-
-  /* selected regions */
-
-  const regionsSelected = (() => {
-    if (!parcellationSelected) return []
-
-    // TODO deprecate. Fallback (defaultNgId) (should) already exist
-    // if (!viewerState.parcellationSelected.updated) throw new Error(PARCELLATION_NOT_UPDATED)
-
-    
-    /**
-     * either or both parcellationToLoad and .regions maybe empty
-     */
-
-    try {
-      const json = obj.r
-        ? { [obj.r[0]]: obj.r[1] }
-        : JSON.parse(fullPath.queryParams['cRegionsSelected'] || '{}')
-
-      const selectRegionIds = []
-
-      for (const ngId in json) {
-        const val = json[ngId]
-        const labelIndicies = val.split(separator).map(n => {
-          try {
-            return decodeToNumber(n)
-          } catch (e) {
-            /**
-             * TODO poisonsed encoded char, send error message
-             */
-            return null
-          }
-        }).filter(v => !!v)
-        for (const labelIndex of labelIndicies) {
-          selectRegionIds.push( serializeSegment(ngId, labelIndex) )
-        }
-      }
-      return [] 
-      // selectRegionIds
-      //   .map(labelIndexId => {
-      //     const region = getRegionFromlabelIndexId({ labelIndexId })
-      //     if (!region) {
-      //       // cb && cb({ type: ID_ERROR, message: `region with id ${labelIndexId} not found, and will be ignored.` })
-      //     }
-      //     return region
-      //   })
-      //   .filter(r => !!r)
-
-    } catch (e) {
-      /**
-       * parsing cRegionSelected error
-       */
-      // cb && cb({ type: DECODE_CIPHER_ERROR, message: `parsing cRegionSelected error ${e.toString()}` })
-    }
-    return []
-  })()
-
-  return {
-    templateSelected,
-    parcellationSelected,
-    regionsSelected,
-  }
-}
diff --git a/src/state/annotations/actions.ts b/src/state/annotations/actions.ts
index daaddd8ce..8527aaaf3 100644
--- a/src/state/annotations/actions.ts
+++ b/src/state/annotations/actions.ts
@@ -1,6 +1,6 @@
 import { createAction, props } from "@ngrx/store"
 import { nameSpace } from "./const"
-import { Annotation } from "./store"
+import { UnionAnnotation } from "./store"
 
 export const clearAllAnnotations = createAction(
   `${nameSpace} clearAllAnnotations`
@@ -9,13 +9,13 @@ export const clearAllAnnotations = createAction(
 export const rmAnnotations = createAction(
   `${nameSpace} rmAnnotations`,
   props<{
-    annotations: Annotation[]
+    annotations: {'@id': string }[]
   }>()
 )
 
 export const addAnnotations = createAction(
   `${nameSpace} addAnnotations`,
   props<{
-    annotations: Annotation[]
+    annotations: UnionAnnotation[]
   }>()
 )
diff --git a/src/state/annotations/index.ts b/src/state/annotations/index.ts
index 4ad0f0680..13a9be8ec 100644
--- a/src/state/annotations/index.ts
+++ b/src/state/annotations/index.ts
@@ -1,4 +1,4 @@
 export * as actions from "./actions"
-export { Annotation, AnnotationState, reducer, defaultState } from "./store"
+export { Annotation, AnnotationState, reducer, defaultState, TypesOfDetailedAnnotations, UnionAnnotation, AnnotationColor } from "./store"
 export { nameSpace } from "./const"
 export * as selectors from "./selectors"
diff --git a/src/state/annotations/selectors.ts b/src/state/annotations/selectors.ts
index abbb3444f..d44a9e211 100644
--- a/src/state/annotations/selectors.ts
+++ b/src/state/annotations/selectors.ts
@@ -1,6 +1,8 @@
 import { createSelector } from "@ngrx/store"
 import { nameSpace } from "./const"
 import { Annotation, AnnotationState } from "./store"
+import { selectors as atlasSelectionSelectors } from "../atlasSelection"
+import { annotation } from ".."
 
 const selectStore = state => state[nameSpace] as AnnotationState
 
@@ -8,3 +10,24 @@ export const annotations = createSelector(
   selectStore,
   state => state.annotations
 )
+
+export const spaceFilteredAnnotations = createSelector(
+  selectStore,
+  atlasSelectionSelectors.selectStore,
+  (annState, atlasSelState) => annState.annotations.filter(ann => {
+    const spaceId = atlasSelState.selectedTemplate['@id']
+    if (ann['openminds']) {
+      return (ann as Annotation<'openminds'>).openminds.coordinateSpace['@id'] === spaceId
+    }
+
+    if (ann['line']) {
+      return (ann as Annotation<'line'>).line.pointA.coordinateSpace['@id'] === spaceId
+        && (ann as Annotation<'line'>).line.pointB.coordinateSpace['@id'] === spaceId
+    }
+
+    if (ann['box']) {
+      return (ann as Annotation<'box'>).box.pointA.coordinateSpace['@id'] === spaceId
+        &&  (ann as Annotation<'box'>).box.pointB.coordinateSpace['@id'] === spaceId
+    }
+  })
+)
diff --git a/src/state/annotations/store.ts b/src/state/annotations/store.ts
index 113220b47..21198ff81 100644
--- a/src/state/annotations/store.ts
+++ b/src/state/annotations/store.ts
@@ -1,12 +1,40 @@
 import { createReducer, on } from "@ngrx/store"
+import { OpenMINDSCoordinatePoint } from "src/atlasComponents/sapi"
 import * as actions from "./actions"
 
-export type Annotation = {
-  "@id": string
+type Line = {
+  pointA: OpenMINDSCoordinatePoint
+  pointB: OpenMINDSCoordinatePoint
+}
+
+type BBox = {
+  pointA: OpenMINDSCoordinatePoint
+  pointB: OpenMINDSCoordinatePoint
+}
+
+export type TypesOfDetailedAnnotations = {
+  openminds: OpenMINDSCoordinatePoint
+  line: Line
+  box: BBox
 }
 
+export enum AnnotationColor {
+  WHITE="WHITE",
+  RED="RED",
+  BLUE="BLUE",
+}
+
+export type Annotation<T extends keyof TypesOfDetailedAnnotations> = {
+  "@id": string
+  name: string
+  description?: string
+  color?: AnnotationColor
+} & { [key in T] : TypesOfDetailedAnnotations[T]}
+
+export type UnionAnnotation = Annotation<'openminds'> | Annotation<'line'> | Annotation<'box'>
+
 export type AnnotationState = {
-  annotations: Annotation[]
+  annotations: UnionAnnotation[]
 }
 
 export const defaultState: AnnotationState = {
diff --git a/src/state/atlasSelection/selectors.ts b/src/state/atlasSelection/selectors.ts
index 93f075350..ad3079a56 100644
--- a/src/state/atlasSelection/selectors.ts
+++ b/src/state/atlasSelection/selectors.ts
@@ -3,7 +3,7 @@ import { nameSpace, AtlasSelectionState } from "./const"
 
 export const viewerStateHelperStoreName = 'viewerStateHelper'
 
-const selectStore = (state: any) => state[nameSpace] as AtlasSelectionState
+export const selectStore = (state: any) => state[nameSpace] as AtlasSelectionState
 
 export const selectedAtlas = createSelector(
   selectStore,
diff --git a/src/viewerModule/nehuba/annotation/effects.ts b/src/viewerModule/nehuba/annotation/effects.ts
new file mode 100644
index 000000000..b4a6de67c
--- /dev/null
+++ b/src/viewerModule/nehuba/annotation/effects.ts
@@ -0,0 +1,20 @@
+import { Injectable } from "@angular/core";
+import { createEffect } from "@ngrx/effects";
+import { select, Store } from "@ngrx/store";
+import { map } from "rxjs/operators";
+import { annotation, atlasAppearance } from "src/state"
+
+@Injectable()
+export class NgAnnotationEffects{
+  constructor(private store: Store){}
+
+  onAnnotationHideQuadrant = createEffect(() => this.store.pipe(
+    select(annotation.selectors.spaceFilteredAnnotations),
+    map(arr => {
+      const spaceFilteredAnnotationExists = arr.length > 0
+      return atlasAppearance.actions.setOctantRemoval({
+        flag: !spaceFilteredAnnotationExists
+      })
+    }),
+  ))
+}
diff --git a/src/viewerModule/nehuba/annotation/service.ts b/src/viewerModule/nehuba/annotation/service.ts
new file mode 100644
index 000000000..0bfd9f926
--- /dev/null
+++ b/src/viewerModule/nehuba/annotation/service.ts
@@ -0,0 +1,89 @@
+import { Injectable, OnDestroy } from "@angular/core";
+import { select, Store } from "@ngrx/store";
+import { Subscription } from "rxjs";
+import { filter, map, pairwise, startWith } from "rxjs/operators";
+import { AnnotationLayer, TNgAnnotationAABBox, TNgAnnotationLine, TNgAnnotationPoint } from "src/atlasComponents/annotations";
+import { annotation } from "src/state";
+
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class NgAnnotationService implements OnDestroy {
+
+  private subs: Subscription[]  = []
+  static ANNOTATION_LAYER_NAME = 'whale-annotation-layer'
+  static INIT_LAYERS: Set<string> = new Set<string>()
+  static COLOR_MAP = new Map<keyof typeof annotation.AnnotationColor, string>([
+    [annotation.AnnotationColor.WHITE, "#ffffff"],
+    [annotation.AnnotationColor.BLUE, "#00ff00"],
+    [annotation.AnnotationColor.RED, "#ff0000"],
+  ])
+
+  static GET_ANN_LAYER(ann: annotation.UnionAnnotation): AnnotationLayer {
+    const color = ann.color || annotation.AnnotationColor.WHITE
+    const layerName = `${NgAnnotationService.ANNOTATION_LAYER_NAME}-${annotation.AnnotationColor[color]}`
+
+    const annLayer = AnnotationLayer.Get(layerName, NgAnnotationService.COLOR_MAP.get(color) || "#ffffff")
+    NgAnnotationService.INIT_LAYERS.add(layerName)
+    return annLayer
+  }
+
+  ngOnDestroy(): void {
+    NgAnnotationService.INIT_LAYERS.forEach(layername => {
+      const layer = AnnotationLayer.Get(layername, '#ffffff')
+      layer.dispose()
+    })
+    while(this.subs.length) this.subs.pop().unsubscribe()
+  }
+
+  constructor(
+    private store: Store
+  ){
+    
+    this.subs.push(
+      this.store.pipe(
+        select(annotation.selectors.spaceFilteredAnnotations)
+      ).pipe(
+        startWith<annotation.UnionAnnotation[]>([]),
+        pairwise(),
+        map(([prevAnnotations, currAnnotations]) => {
+          const prevAnnotationIds = new Set(prevAnnotations.map(ann => ann["@id"]))
+          const currAnnotationIds = new Set(currAnnotations.map(ann => ann["@id"]))
+          const newAnnotations = currAnnotations.filter(ann => !prevAnnotationIds.has(ann["@id"]))
+          const expiredAnnotations = prevAnnotations.filter(ann => !currAnnotationIds.has(ann["@id"]))
+          return {
+            newAnnotations,
+            expiredAnnotations
+          }
+        }),
+        filter(({ newAnnotations, expiredAnnotations }) => newAnnotations.length > 0 || expiredAnnotations.length > 0)
+      ).subscribe(({ newAnnotations, expiredAnnotations }) => {
+        
+        for (const ann of expiredAnnotations) {
+          const annLayer = NgAnnotationService.GET_ANN_LAYER(ann)
+          annLayer.removeAnnotation({
+            id: ann["@id"]
+          })
+        }
+        for (const ann of newAnnotations) {
+          let annotation: TNgAnnotationPoint | TNgAnnotationLine | TNgAnnotationAABBox
+          let annLayer: AnnotationLayer
+          if (!!ann['openminds']) {
+            annLayer = NgAnnotationService.GET_ANN_LAYER(ann)
+            annotation = {
+              id: ann["@id"],
+              description: ann.description,
+              type: 'point',
+              point: (ann as annotation.Annotation<'openminds'>).openminds.coordinates.map(coord => coord.value * 1e6) as [number, number, number]
+            } as TNgAnnotationPoint
+          }
+
+          if (annotation && annLayer) annLayer.addAnnotation(annotation)
+          else console.warn(`annotation or annotation layer was not initialized.`)
+        }
+      })
+    )
+  }
+}
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index 1fda36377..85f67513d 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -12,6 +12,7 @@ import { LayerCtrlEffects } from "./layerCtrl.effects";
 import { arrayEqual } from "src/util/array";
 import { ColorMapCustomLayer } from "src/state/atlasAppearance";
 import { SapiRegionModel } from "src/atlasComponents/sapi";
+import { AnnotationLayer } from "src/atlasComponents/annotations";
 
 export const BACKUP_COLOR = {
   red: 255,
@@ -359,6 +360,6 @@ export class NehubaLayerControlService implements OnDestroy{
       )
     )
   ]).pipe(
-    map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName])
+    map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName, ...AnnotationLayer.Map.keys()])
   )
 }
diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts
index fe4025538..5ba4843f6 100644
--- a/src/viewerModule/nehuba/module.ts
+++ b/src/viewerModule/nehuba/module.ts
@@ -26,6 +26,8 @@ import { NgLayerCtrlCmp } from "./ngLayerCtl/ngLayerCtrl.component";
 import { EffectsModule } from "@ngrx/effects";
 import { MeshEffects } from "./mesh.effects/mesh.effects";
 import { NehubaLayoutOverlayModule } from "./layoutOverlay";
+import { NgAnnotationService } from "./annotation/service";
+import { NgAnnotationEffects } from "./annotation/effects";
 
 @NgModule({
   imports: [
@@ -51,7 +53,8 @@ import { NehubaLayoutOverlayModule } from "./layoutOverlay";
       reducer
     ),
     EffectsModule.forFeature([
-      MeshEffects
+      MeshEffects,
+      NgAnnotationEffects,
     ]),
     QuickTourModule,
     NehubaLayoutOverlayModule,
@@ -81,11 +84,17 @@ import { NehubaLayoutOverlayModule } from "./layoutOverlay";
     {
       provide: NEHUBA_INSTANCE_INJTKN,
       useValue: new BehaviorSubject(null)
-    }
+    },
+    NgAnnotationService
   ],
   schemas: [
     CUSTOM_ELEMENTS_SCHEMA
   ]
 })
 
-export class NehubaModule{}
+export class NehubaModule{
+
+  constructor(_svc: NgAnnotationService){
+
+  }
+}
-- 
GitLab