From 5e7505a65c4e91927b3dd7b6b1569fc5be05bd2c Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Tue, 31 May 2022 10:57:26 +0200
Subject: [PATCH] feat: allow plugin to add and remove custom layer bugfix:
 allow layer transparency to be modified via ng custom layer

---
 deploy/plugins/index.js                       | 66 ++++++++++---------
 src/api/service.ts                            | 46 ++++++++++++-
 src/plugin/README.md                          | 20 ++++++
 src/plugin/request.md                         | 45 +++++++++++++
 src/plugin/tsUtil.js                          |  1 +
 src/state/atlasSelection/effects.ts           |  4 +-
 .../layerCtrl.service/layerCtrl.service.ts    | 25 ++++++-
 .../nehubaViewer/nehubaViewer.component.ts    | 10 ++-
 8 files changed, 181 insertions(+), 36 deletions(-)

diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js
index e7d2aafd6..6089e2fe9 100644
--- a/deploy/plugins/index.js
+++ b/deploy/plugins/index.js
@@ -34,38 +34,42 @@ const getKey = url => `plugin:manifest-cache:${url}`
 
 router.get('/manifests', async (_req, res) => {
 
-  const allManifests = await Promise.all([
-    ...V2_7_PLUGIN_URLS,
-    ...V2_7_STAGING_PLUGIN_URLS
-  ].map(async url => {
-    const key = getKey(url)
-    
-    await lruStore._initPr
-    const { store } = lruStore
+  const allManifests = await Promise.all(
+    [...V2_7_PLUGIN_URLS, ...V2_7_STAGING_PLUGIN_URLS].map(async url =>
+      race(
+        async () => {
+          const key = getKey(url)
+          
+          await lruStore._initPr
+          const { store } = lruStore
+          
+          try {
+            const storedManifest = await store.get(key)
+            if (storedManifest) return JSON.parse(storedManifest)
+            else throw `not found`
+          } catch (e) {
+            const resp = await got(url)
+            const json = JSON.parse(resp.body)
     
-    try {
-      const storedManifest = await store.get(key)
-      if (storedManifest) return JSON.parse(storedManifest)
-      else throw `not found`
-    } 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)
-      
-      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
-    }
-  }))
+            const { iframeUrl, 'siibra-explorer': flag } = json
+            if (!flag) return null
+            if (!iframeUrl) return null
+            const u = new URL(url)
+            
+            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
+          }
+        },
+        { timeout: 1000 }
+      )
+    )
+  )
 
   res.status(200).json(
     [...V2_7_DEV_PLUGINS, ...allManifests.filter(v => !!v)]
diff --git a/src/api/service.ts b/src/api/service.ts
index 41b100f78..67269b960 100644
--- a/src/api/service.ts
+++ b/src/api/service.ts
@@ -4,7 +4,7 @@ import { Subject } from "rxjs";
 import { distinctUntilChanged, filter, map, take } from "rxjs/operators";
 import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel, OpenMINDSCoordinatePoint } from "src/atlasComponents/sapi";
 import { SxplrCoordinatePointExtension } from "src/atlasComponents/sapi/type";
-import { MainState, atlasSelection, userInteraction, annotation } from "src/state"
+import { MainState, atlasSelection, userInteraction, annotation, atlasAppearance } 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"
@@ -13,6 +13,8 @@ export type NAMESPACE_TYPE = "sxplr"
 export const namespace: NAMESPACE_TYPE = "sxplr"
 const nameSpaceRegex = new RegExp(`^${namespace}`)
 
+type AddableLayer = atlasAppearance.NgLayerCustomLayer
+
 type AtId = {
   "@id": string
 }
@@ -87,6 +89,27 @@ export type ApiBoothEvents = {
     response: 'OK'
   }
 
+  loadLayers: {
+    request: {
+      layers: AddableLayer[]
+    }
+    response: 'OK'
+  }
+
+  updateLayers: {
+    request: {
+      layers: AddableLayer[]
+    }
+    response: 'OK'
+  }
+
+  removeLayers: {
+    request: {
+      layers: {id: string}[]
+    }
+    response: 'OK'
+  }
+
   exit: {
     request: {
       requests: JRPCRequest<keyof ApiBoothEvents, ApiBoothEvents[keyof ApiBoothEvents]['request']>[]
@@ -441,6 +464,27 @@ export class ApiService implements BoothResponder<ApiBoothEvents>{
       }
       break
     }
+    case 'loadLayers':
+    case 'updateLayers': {
+      const { layers } = event.params as ApiBoothEvents['loadLayers']['request'] | ApiBoothEvents['updateLayers']['request']
+      for (const layer of layers) {
+        this.store.dispatch(
+          atlasAppearance.actions.addCustomLayer({
+            customLayer: layer
+          })
+        )
+      }
+      break
+    }
+    case 'removeLayers': {
+      const { layers } = event.params as ApiBoothEvents['removeLayers']['request']
+      for (const layer of layers) {
+        this.store.dispatch(
+          atlasAppearance.actions.removeCustomLayer(layer)
+        )
+      }
+      break
+    }
     case 'exit': {
       const { requests } = event.params as ApiBoothEvents['exit']['request']
       for (const req of requests) {
diff --git a/src/plugin/README.md b/src/plugin/README.md
index 260bd7db5..8656ab315 100644
--- a/src/plugin/README.md
+++ b/src/plugin/README.md
@@ -6,6 +6,26 @@ siibra-explorer provides a plugin system, which allow a third party application
 
 ## Quickstart
 
+### manifest
+
+The plugin need to expose a manifest json file. The manifest file needs to have the following properties:
+
+```json
+{
+  "iframeUrl": "<iframeUrl>",
+  "name": "<name>",
+  "siibra-explorer": "<siibra-explorer>"
+}
+```
+
+| property | required | desc | 
+| --- | --- | --- |
+| `iframeUrl` | true | points to the html where the iframe is located. If does not start with `https?://`, siibra-explorer will try to resolve it relative to the absolute path of manifest. |
+| `name` | true | name of the plugin | 
+| `siibra-explorer` | true | the version siibra-explorer this plugin is targetting. Should be >= 2.7.0. n.b. currently this entry is partially implemented, and any truthy value is sufficient.
+ |
+
+
 <!-- TBD -->
 
 ## Architecture
diff --git a/src/plugin/request.md b/src/plugin/request.md
index a2575863a..487c1344f 100644
--- a/src/plugin/request.md
+++ b/src/plugin/request.md
@@ -188,6 +188,51 @@ window.addEventListener('pagehide', () => {
   ```
 
 
+### `sxplr.loadLayers`
+
+- request
+
+  ```ts
+  {"layers": AddableLayer[]}
+  ```
+
+- response
+
+  ```ts
+  'OK'
+  ```
+
+
+### `sxplr.updateLayers`
+
+- request
+
+  ```ts
+  {"layers": AddableLayer[]}
+  ```
+
+- response
+
+  ```ts
+  'OK'
+  ```
+
+
+### `sxplr.removeLayers`
+
+- request
+
+  ```ts
+  {"layers": {"id": string}}
+  ```
+
+- response
+
+  ```ts
+  'OK'
+  ```
+
+
 ### `sxplr.exit`
 
 - request
diff --git a/src/plugin/tsUtil.js b/src/plugin/tsUtil.js
index b27c5c973..ea0040ad8 100644
--- a/src/plugin/tsUtil.js
+++ b/src/plugin/tsUtil.js
@@ -14,6 +14,7 @@ function getTypeText(node){
       return node.typeName.text
     }
     case "ArrayType": {
+      if (!node.elementType.typeName) return getTypeText(node.elementType)
       return `${node.elementType.typeName.text}[]`
     }
     case "TypeLiteral": {
diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts
index 0ef6018a2..e134e7d4b 100644
--- a/src/state/atlasSelection/effects.ts
+++ b/src/state/atlasSelection/effects.ts
@@ -50,8 +50,8 @@ export class Effect {
       )
     },
     ({ current, previous }) => {
-      const prevSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(previous.template["@id"])
-      const currSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(current.template["@id"])
+      const prevSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(previous?.template?.["@id"])
+      const currSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(current?.template?.["@id"])
       /**
        * if either space name is undefined, return default state for navigation
        */
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index 7cd16eb37..ba8b1d0ad 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -1,7 +1,7 @@
 import { Injectable, OnDestroy } from "@angular/core";
 import { select, Store } from "@ngrx/store";
 import { combineLatest, merge, Observable, Subject, Subscription } from "rxjs";
-import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
+import { debounceTime, distinctUntilChanged, filter, map, pairwise, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
 import { IColorMap, INgLayerCtrl, TNgLayerCtrl } from "./layerCtrl.util";
 import { SAPIRegion } from "src/atlasComponents/sapi/core";
 import { getParcNgId } from "../config.service"
@@ -276,6 +276,16 @@ export class NehubaLayerControlService implements OnDestroy{
 
   private ngLayersRegister: atlasAppearance.NgLayerCustomLayer[] = []
 
+  private updateCustomLayerTransparency$ = this.store$.pipe(
+    select(atlasAppearance.selectors.customLayers),
+    map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
+    pairwise(),
+    map(([ oldCustomLayers, newCustomLayers ]) => {
+      return newCustomLayers.filter(({ id, opacity }) => oldCustomLayers.some(({ id: oldId, opacity: oldOpacity }) => oldId === id && oldOpacity !== opacity))
+    }),
+    filter(arr => arr.length > 0)
+  )
+
   private ngLayers$ = this.customLayers$.pipe(
     map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
     distinctUntilChanged(
@@ -324,6 +334,19 @@ export class NehubaLayerControlService implements OnDestroy{
         } as TNgLayerCtrl<'remove'>
       })
     ),
+    this.updateCustomLayerTransparency$.pipe(
+      map(layers => {
+        const payload: Record<string, number> = {}
+        for (const layer of layers) {
+          const opacity = layer.opacity ?? 0.8
+          payload[layer.id] = opacity
+        }
+        return {
+          type: 'setLayerTransparency',
+          payload
+        } as TNgLayerCtrl<'setLayerTransparency'>
+      })
+    ),
     this.manualNgLayersControl$,
   ).pipe(
   )
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index 633778743..cfd902f1a 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -785,7 +785,15 @@ export class NehubaViewerUnit implements OnDestroy {
   private setLayerTransparency(layerName: string, alpha: number) {
     const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
     if (!layer) return
-    layer.layer.displayState.objectAlpha.restoreState(alpha)
+
+    /**
+     * for segmentation layer
+     */
+    if (layer.layer.displayState) layer.layer.displayState.objectAlpha.restoreState(alpha)
+    /**
+     * for image layer
+     */
+    if (layer.layer.opacity) layer.layer.opacity.restoreState(alpha)
   }
 
   public setMeshTransparency(flag: boolean){
-- 
GitLab