From 033a43bf8aefc10099eab9225af55e7b13544ec3 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Tue, 28 Jun 2022 17:46:27 +0200
Subject: [PATCH] feat: allow custom layer shader to be updated chore: removed
 deprecated, unused vars/files

---
 src/plugin_examples/README.md                 | 118 -----
 src/plugin_examples/migrationGuide.md         |  51 ---
 src/plugin_examples/plugin1/manifest.json     |  18 -
 src/plugin_examples/plugin_api.md             | 416 ------------------
 .../routeStateTransform.service.ts            |   7 +-
 src/routerModule/router.service.ts            |   5 +-
 src/state/plugins/actions.ts                  |   9 -
 src/state/plugins/effects.ts                  |  51 ++-
 src/state/plugins/store.ts                    |  23 +-
 .../layerCtrl.service/layerCtrl.service.ts    |  38 +-
 .../layerCtrl.service/layerCtrl.util.ts       |   3 +
 .../nehubaViewer/nehubaViewer.component.ts    |  11 +
 12 files changed, 81 insertions(+), 669 deletions(-)
 delete mode 100644 src/plugin_examples/README.md
 delete mode 100644 src/plugin_examples/migrationGuide.md
 delete mode 100644 src/plugin_examples/plugin1/manifest.json
 delete mode 100644 src/plugin_examples/plugin_api.md

diff --git a/src/plugin_examples/README.md b/src/plugin_examples/README.md
deleted file mode 100644
index 7f967539c..000000000
--- a/src/plugin_examples/README.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# Plugin README
-
-A plugin needs to contain three files. 
-- Manifest JSON
-- template HTML
-- script JS
-
-
-These files need to be served by GET requests over HTTP with appropriate CORS header. 
-
----
-
-## Manifest JSON
-
-The manifest JSON file describes the metadata associated with the plugin. 
-
-```json
-{
-  "name":"fzj.xg.helloWorld",
-  "displayName": "Hello World - my first plugin",
-  "templateURL":"http://LINK-TO-YOUR-PLUGIN-TEMPLATE/template.html",
-  "scriptURL":"http://LINK-TO-YOUR-PLUGIN-SCRIPT/script.js",
-  "initState":{
-    "key1": "value1",
-    "key2" : {
-      "nestedKey1" : "nestedValue1"
-    }
-  },
-  "initStateUrl": "http://LINK-TO-PLUGIN-STATE",
-  "persistency": false,
-
-  "description": "Human readable description of the plugin.",
-  "desc": "Same as description. If both present, description takes more priority.",
-  "homepage": "https://HOMEPAGE-URL-TO-YOUR-PLUGIN/doc.html",
-  "authors": "Author <author@example.com>, Author2 <author2@example.org>"
-}
-```
-*NB* 
-- Plugin name must be unique globally. To prevent plugin name clashing, please adhere to the convention of naming your package **AFFILIATION.AUTHORNAME.PACKAGENAME\[.VERSION\]**. 
-- the `initState` object and `initStateUrl` will be available prior to the evaluation of `script.js`, and will populate the objects `interactiveViewer.pluginControl[MANIFEST.name].initState` and `interactiveViewer.pluginControl[MANIFEST.name].initStateUrl` respectively. 
-
----
-
-## Template HTML
-
-The template HTML file describes the HTML view that will be rendered in the widget.
-
-
-```html
-<form>
-  <div class = "input-group">
-    <span class = "input-group-addon">Area 1</span>
-    <input type = "text" id = "fzj.xg.helloWorld.area1" name = "fzj.xg.helloWorld.area1" class = "form-control" placeholder="Select a region" value = "">
-  </div>
-
-  <div class = "input-group">
-    <span class = "input-group-addon">Area 2</span>
-    <input type = "text" id = "fzj.xg.helloWorld.area2" name = "fzj.xg.helloWorld.area2" class = "form-control" placeholder="Select a region" value = "">
-  </div>
-
-  <hr class = "col-md-10">
-
-  <div class = "col-md-12">
-    Select genes of interest:
-  </div>
-  <div class = "input-group">
-    <input type = "text" id = "fzj.xg.helloWorld.genes" name = "fzj.xg.helloWorld.genes" class = "form-control" placeholder = "Genes of interest ...">
-    <span class = "input-group-btn">
-      <button id = "fzj.xg.helloWorld.addgenes" name = "fzj.xg.helloWorld.addgenes" class = "btn btn-default" type = "button">Add</button>
-    </span>
-  </div>
-
-  <hr class = "col-md-10">
-
-  <button id = "fzj.xg.helloWorld.submit" name = "fzj.xg.helloWorld.submit" type = "button" class = "btn btn-default btn-block">Submit</button>
-
-  <hr class = "col-md-10">
-
-  <div class = "col-md-12" id = "fzj.xg.helloWorld.result">
-
-  </div>
-</form>
-```
-
-*NB*
-- *bootstrap 3.3.6* css is already included for templating.
-- keep in mind of the widget width restriction (400px) when crafting the template
-- whilst there are no vertical limits on the widget, contents can be rendered outside the viewport. Consider setting the *max-height* attribute.
-- your template and script will interact with each other likely via *element id*. As a result, it is highly recommended that unique id's are used. Please adhere to the convention: **AFFILIATION.AUTHOR.PACKAGENAME.ELEMENTID** 
-
----
-
-## Script JS
-
-The script will always be appended **after** the rendering of the template. 
-
-```javascript
-(()=>{
-  /* your code here */
-
-  if(interactiveViewer.pluginControl['fzj.xg.helloWorld'].initState){
-    /* init plugin with initState */
-  }
-  
-  const submitButton = document.getElemenById('fzj.xg.helloWorld.submit')
-  submitButton.addEventListener('click',(ev)=>{
-    console.log('submit button was clicked')
-  })
-})()
-```
-*NB*
-- JS is loaded and executed **before** the attachment of DOM (template). This is to allow webcomponents have a chance to be loaded. If your script needs the DOM to be attached, use a `setTimeout` callback to delay script execution.
-- ensure the script is scoped locally, instead of poisoning the global scope
-- for every observable subscription, call *unsubscribe()* in the *onShutdown* callback
-- some frameworks such as *jquery2*, *jquery3*, *react/reactdom* and *webcomponents* can be loaded via *interactiveViewer.pluinControl.loadExternalLibraries([LIBRARY_NAME_1, LIBRARY_NAME_2])*. if the libraries are loaded, remember to hook *interactiveViewer.pluginControl.unloadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])* in the *onShutdown* callback
-- when/if using webcomponents, please be aware that the `connectedCallback()` and `disconnectedCallback()` will be called everytime user toggle between *floating* and *docked* modes. 
-- when user navigate to a new template all existing widgets will be destroyed, unless the `persistency` is set to `true` in `manifest.json`.
-- for a list of APIs, see [plugin_api.md](plugin_api.md)
diff --git a/src/plugin_examples/migrationGuide.md b/src/plugin_examples/migrationGuide.md
deleted file mode 100644
index fcd5e040b..000000000
--- a/src/plugin_examples/migrationGuide.md
+++ /dev/null
@@ -1,51 +0,0 @@
-Plugin Migration Guide (v0.1.0 => v0.2.0)
-======
-Plugin APIs have changed drastically from v0.1.0 to v0.2.0. Here is a list of plugin API from v0.1.0, and how it has changed moving to v0.2.0.
-
-**n.b.** `webcomponents-lite.js` is no longer included by default. You will need to request it explicitly with `window.interactiveViewer.pluginControl.loadExternalLibraries()` and unload it once you are done.
-
----
-
-- ~~*window.nehubaUI*~~ removed
-  - ~~*metadata*~~ => **window.interactiveViewer.metadata**
-    - ~~*selectedTemplate* : nullable Object~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead
-    - ~~*availableTemplates* : Array of TemplateDescriptors (empty array if no templates are available)~~ => **window.interactiveViewer.metadata.loadedTemplates**
-    - ~~*selectedParcellation* : nullable Object~~ removed. use **window.interactiveViewer.metadata.selectedParcellationBSubject** instead
-    - ~~*selectedRegions* : Array of Object (empty array if no regions are selected)~~ removed. use **window.interactiveViewer.metadata.selectedRegionsBSubject** instead
-
-- ~~window.pluginControl['YOURPLUGINNAME'] *nb: may be undefined if yourpluginname is incorrect*~~ => **window.interactiveViewer.pluginControl[YOURPLUGINNAME]**
-  - blink(sec?:number) : Function that causes the floating widget to blink, attempt to grab user attention
-  - ~~pushMessage(message:string) : Function that pushes a message that are displayed as a popover if the widget is minimised. No effect if the widget is not miniminised.~~ removed
-  - shutdown() : Function that causes the widget to shutdown dynamically. (triggers onShutdown callback)
-  - onShutdown(callback) : Attaches a callback function, which is called when the plugin is shutdown.
-  
-- ~~*window.viewerHandle*~~ => **window.interactiveViewer.viewerHandle**
-  - ~~*loadTemplate(TemplateDescriptor)* : Function that loads a new template~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead
-  - ~~*onViewerInit(callback)* : Functional that allows a callback function to be called just before a nehuba viewer is initialised~~ removed
-  - ~~*afterViewerInit(callback)* : Function that allows a callback function to be called just after a nehuba viewer is initialised~~ removed
-  - ~~*onViewerDestroy(callback)* : Function that allows a callback function be called just before a nehuba viewer is destroyed~~ removed
-  - ~~*onParcellationLoading(callback)* : Function that allows a callback function to be called just before a parcellation is selected~~ removed
-  - ~~*afterParcellationLoading(callback)* : Function that allows a callback function to be called just after a parcellation is selected~~ removed
-  - *setNavigationLoc(loc,realSpace?)* : Function that teleports to loc : number[3]. Optional argument to determine if the loc is in realspace (default) or voxelspace.
-  - ~~*setNavigationOrientation(ori)* : Function that teleports to ori : number[4]. (Does not work currently)~~ => **setNavigationOri(ori)** (still non-functional)
-  - *moveToNavigationLoc(loc,realSpace?)* : same as *setNavigationLoc(loc,realSpace?)*, except moves to target location over 500ms.
-  - *showSegment(id)* : Function that selectes a segment in the viewer and UI. 
-  - *hideSegment(id)* : Function that deselects a segment in the viewer and UI.
-  - *showAllSegments()* : Function that selects all segments.
-  - *hideAllSegments()* : Function that deselects all segments.
-  - *loadLayer(layerObject)* : Function that loads a custom neuroglancer compatible layer into the viewer (e.g. precomputed, NIFTI, etc). Does not influence UI. 
-  - *mouseEvent* RxJs Observable. Read more at [rxjs doc](http://reactivex.io/rxjs/)
-    - *mouseEvent.filter(filterFn:({eventName : String, event: Event})=>boolean)* returns an Observable. Filters the event stream according to the filter function.
-    - *mouseEvent.map(mapFn:({eventName : String, event: Event})=>any)* returns an Observable. Map the event stream according to the map function.
-    - *mouseEvent.subscribe(callback:({eventName : String , event : Event})=>void)* returns an Subscriber instance. Call *Subscriber.unsubscribe()* when done to avoid memory leak. 
-  - *mouseOverNehuba* RxJs Observable. Read more at [rxjs doc](http://reactivex.io/rxjs)
-    - *mouseOverNehuba.filter* && *mouseOvernehuba.map* see above
-    - *mouseOverNehuba.subscribe(callback:({nehubaOutput : any, foundRegion : any})=>void)*
-
-- ~~*window.uiHandle*~~ => **window.interactiveViewer.uiHandle**
-  - ~~*onTemplateSelection(callback)* : Function that allows a callback function to be called just after user clicks to navigate to a new template, before *selectedTemplate* is updated~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead
-  - ~~*afterTemplateSelection(callback)* : Function that allows a callback function to be called after the template selection process is complete, and *selectedTemplate* is updated~~ removed
-  - ~~*onParcellationSelection(callback)* : Function that attach a callback function to user selecting a different parcellation~~ removed. use **window.interactiveViewer.metadata.selectedParcellationBSubject** instead.
-  - ~~*afterParcellationSelection(callback)* : Function that attach a callback function to be called after the parcellation selection process is complete and *selectedParcellation* is updated.~~ removed
-  - *modalControl*
-    - ~~*getModalHandler()* : Function returning a handler to change/show/hide/listen to a Modal.~~ removed
\ No newline at end of file
diff --git a/src/plugin_examples/plugin1/manifest.json b/src/plugin_examples/plugin1/manifest.json
deleted file mode 100644
index 0813f787c..000000000
--- a/src/plugin_examples/plugin1/manifest.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name":"fzj.xg.exmaple.0_0_1",
-  "displayName": "Example Plugin (v0.0.1)",
-  "templateURL": "http://HOSTNAME/test.html",
-  "scriptURL": "http://HOSTNAME/script.js",
-  "initState": {
-    "key1": "val1",
-    "key2": {
-      "key21": "val21"
-    }
-  },
-  "initStateUrl": "http://HOSTNAME/state?id=007",
-  "persistency": false,
-  "description": "description of example plugin",
-  "desc": "desc of example plugin",
-  "homepage": "http://HOSTNAME/home.html",
-  "authors": "Xiaoyun Gui <x.gui@fz-juelich.de>"
-}
\ No newline at end of file
diff --git a/src/plugin_examples/plugin_api.md b/src/plugin_examples/plugin_api.md
deleted file mode 100644
index 6954f15b5..000000000
--- a/src/plugin_examples/plugin_api.md
+++ /dev/null
@@ -1,416 +0,0 @@
-# Plugin APIs
-
-## window.interactiveViewer
-
-### metadata
-
-#### selectedTemplateBSubject
-
-BehaviourSubject that emits a TemplateDescriptor object whenever a template is selected. Emits null onInit.
-
-#### selectedParcellationBSubject
-
-BehaviourSubject that emits a ParcellationDescriptor object whenever a parcellation is selected. n.b. selecting a new template automatically select the first available parcellation. Emits null onInit.
-
-#### selectedRegionsBSubject
-
-BehaviourSubject that emits an Array of RegionDescriptor objects whenever the list of selected regions changes. Emits empty array onInit.
-
-#### loadedTemplates
-
-Array of TemplateDescriptor objects. Loaded asynchronously onInit.
-
-#### layersRegionLabelIndexMap
-
-Map of layer name to Map of labelIndex (used by neuroglancer and nehuba) to the corresponding RegionDescriptor object.
-
-### viewerHandle
-
-> **nb** `viewerHandle` may be undefined at any time (user be yet to select an atlas, user could have unloaded an atlas. ...etc)
-
-#### setNavigationLoc(coordinates, realspace?:boolean)
-
-Function that teleports the navigation state to coordinates : [x:number,y:number,z:number]. Optional arg determine if the set of coordinates is in realspace (default) or voxelspace.
-
-#### moveToNavigationLoc(coordinates,realspace?:boolean)
-
-same as *setNavigationLoc(coordinates,realspace?)*, except the action is carried out over 500ms.
-
-#### setNavigationOri(ori)
-
-(NYI) Function that sets the orientation state of the viewer.
-
-#### moveToNavigationOri(ori)
-
-(NYI) same as *setNavigationOri*, except the action is carried out over 500ms.
-
-#### showSegment(labelIndex)
-
-Function that shows a specific segment. Will trigger *selectedRegionsBSubject*.
-
-#### hideSegment(labelIndex)
-
-Function that hides a specific segment. Will trigger *selectRegionsBSubject*
-
-#### showAllSegments()
-
-Function that shows all segments. Will trigger *selectRegionsBSubject*
-
-#### hideAllSegments()
-Function that hides all segments. Will trigger *selectRegionBSubject*
-
-#### getLayersSegmentColourMap()
-
-Call to get Map of layer name to Map of label index to colour map
-
-#### applyLayersColourMap
-
-Function that applies a custom colour map.
-
-#### loadLayer(layerObject)
-
-Function that loads *ManagedLayersWithSpecification* directly to neuroglancer. Returns the values of the object successfully added. **n.b.** advanced feature, will likely break other functionalities. **n.b.** if the layer name is already taken, the layer will not be added.
-  
-```javascript
-const obj = {
-  'advanced layer' : {
-    type : 'image',
-    source : 'nifti://http://example.com/data/nifti.nii',
-  },
-  'advanced layer 2' : {
-    type : 'mesh',
-    source : 'vtk://http://example.com/data/vtk.vtk'
-  }
-}
-const returnValue = window.interactiveViewer.viewerHandle.loadLayer(obj)
-/* loads two layers, an image nifti layer and a mesh vtk layer */
-
-console.log(returnValue)
-/* prints
-
-[{ 
-  type : 'image', 
-  source : 'nifti...' 
-},
-{
-  type : 'mesh',
-  source : 'vtk...'
-}] 
-*/
-```
-
-#### removeLayer(layerObject)
-
-Function that removes *ManagedLayersWithSpecification*, returns an array of the names of the layers removed. 
-
-**n.b.** advanced feature. may break other functionalities.
-
-```js
-const obj = {
-  'name' : /^PMap/
-}
-const returnValue = window.interactiveViewer.viewerHandle.removeLayer(obj)
-
-console.log(returnValue)
-/* prints
-['PMap 001','PMap 002']
-*/
-```
-
-#### add3DLandmarks(landmarks)
-
-adds landmarks to both the perspective view and slice view. 
-
-_input_
-
-| input | type | desc |
-| --- | --- | --- |
-| landmarks | array | an array of `landmarks` to be rendered by the viewer |
-
-A landmark object consist of the following keys:
-
-| key | type | desc | required |
-| --- | --- | --- | --- |
-| id | string | id of the landmark | yes |
-| name | string | name of the landmark | |
-| position | [number, number, number] | position (in mm) | yes |
-| color | [number, number, number] | rgb of the landmark | |
-
-
-```js
-const landmarks = [{
-  id : `fzj-xg-jugex-1`,
-  position : [0,0,0]
-},{
-  id : `fzj-xg-jugex-2`,
-  position : [22,27,-1],
-  color: [255, 0, 255]
-}]
-window.interactiveViewer.viewerHandle.add3DLandmarks(landmarks)
-
-/* adds landmarks in perspective view and slice view */
-```
-
-#### remove3DLandmarks(IDs)
-
-removes the landmarks by their IDs
-
-```js
-window.interactiveViewer.viewerHandle
-  .remove3DLandmarks(['fzj-xg-jugex-1', 'fzj-xg-jugex-2'])
-/* removes the landmarks added above */
-```
-
-#### setLayerVisibility(layerObject, visible)
-
-Function that sets the visibility of a layer. Returns the names of all the layers that are affected as an Array of string.
-
-```js
-const obj = {
-  'type' : 'segmentation'
-}
-
-window.interactiveViewer.viewerHandle.setLayerVisibility(obj,false)
-
-/* turns off all the segmentation layers */
-```
-
-#### mouseEvent
-
-Subject that emits an object shaped `{ eventName : string, event: event }` when a user triggers a mouse event on the viewer. 
-
-
-#### mouseOverNehuba
-
-**deprecating** use mouseOverNehubaUI instead
-
-BehaviourSubject that emits an object shaped `{ nehubaOutput : number | null, foundRegion : RegionDescriptor | null }`
-
-#### mouseOverNehubaUI
-
-`Observable<{ landmark, segments, customLandmark }>`.
-
-**nb** it is a known issue that if customLandmarks are destroyed/created while user mouse over the custom landmark this observable will emit `{ customLandmark: null }`
-
-### uiHandle
-
-#### getModalHandler()
-
-returns a modalHandler object, which has the following methods/properties:
-
-##### hide()
-
-Dynamically hides the modal
-
-##### show()
-
-Shows the modal
-
-##### title
-
-title of the modal (String)
-
-##### body
-
-body of the modal shown (String)
-
-##### footer
-
-footer of the modal (String)
-
-##### dismissable
-
-whether the modal is dismissable on click backdrop/esc key (Boolean)
-
-*n.b. if true, users will not be able to interact with the viewer unless you specifically call `handler.hide()`*
-
-#### launchNewWidget(manifest)
-
-returns a Promise. expects a JSON object, with the same key value as a plugin manifest. the *name* key must be unique, or the promise will be rejected. 
-
-#### getUserInput(config)
-
-returns a Promise, resolves when user confirms, rejects when user cancels. expects config object object with the following structure:
-
-```javascript
-const config = {
-  "title": "Title of the modal", // default: "Message"
-  "message":"Message to be seen by the user.", // default: ""
-  "placeholder": "Start typing here", // default: "Type your response here"
-  "defaultValue": "42" // default: ""
-  "iconClass":"fas fa-save" // default fas fa-save, set to falsy value to disable
-}
-```
-
-#### getUserConfirmation(config)
-
-returns a Promise, resolves when user confirms, rejects when user cancels. expects config object object with the following structure:
-
-```javascript
-const config = {
-  "title": "Title of the modal", // default: "Message"
-  "message":"Message to be seen by the user." // default: ""
-}
-```
-
-#### getUserToSelectARegion(message)
-
-**To be deprecated**
-
-_input_
-
-| input | type | desc |
-| --- | --- | --- |
-| message | `string` | human readable message displayed to the user | 
-| spec.type | `'POINT'` `'PARCELLATION_REGION'` **default** | type of region to be returned. |
-
-_returns_
-
-`Promise`, resolves to return array of region clicked, rejects with error object `{ userInitiated: boolean }`
-
-Requests user to select a region of interest. Resolving to the region selected by the user. Rejects if either user cancels by pressing `Esc` or `Cancel`, or by developer calling `cancelPromise`
-
-#### getUserToSelectRoi(message, spec)
-
-_input_
-
-| input | type | desc |
-| --- | --- | --- |
-| message | `string` | human readable message displayed to the user | 
-| spec.type | `POINT` `PARCELLATION_REGION` | type of ROI to be returned. |
-
-_returns_
-
-`Promise`
-
-**resolves**: return `{ type, payload }`. `type` is the same as `spec.type`, and `payload` differs depend on the type requested:
-
-| type | payload | example |
-| --- | --- | --- |
-| `POINT` | array of number in mm | `[12.2, 10.1, -0.3]` |
-| `PARCELLATION_REGOIN` | non empty array of region selected | `[{ "layer": { "name" : " viewer specific layer name " }, "segment": {} }]` |
-
-**rejects**: with error object `{ userInitiated: boolean }`
-
-Requests user to select a region of interest. If the `spec.type` input is missing, it is assumed to be `'PARCELLATION_REGION'`. Resolving to the region selected by the user. Rejects if either user cancels by pressing `Esc` or `Cancel`, or by developer calling `cancelPromise`
-
-#### cancelPromise(promise)
-
-returns `void`
-  
-_input_ 
-
-| input | type | desc |
-| --- | --- | --- |
-| promise | `Promise` | Reference to the __exact__ promise returned by `uiHnandle` methods |
-
-Cancel the request to select a parcellation region.
-
-_usage example_
-
-```javascript
-
-(() => {
-  const pr = interactive.uiHandle.getUserToSelectARegion(`webJuGEx would like you to select a region`)
-
-  pr.then(region => {  })
-    .catch(console.warn)
-
-  /*
-    * do NOT do 
-    * 
-    * const pr = interactive.uiHandle.getUserToSelectARegion(`webJuGEx would like you to select a region`)
-    *   .then(region => {  })
-    *   -catch(console.warn)
-    * 
-    * the promise passed to `cancelPromise` must be the exact promise returned.
-    * by chaining then/catch, a new reference is returned
-    */
-
-  setTimeout(() => {
-    try {
-      interactive.uiHandle.cancelPromise(pr)
-    } catch (e) {
-      // if the promise has been fulfilled (by resolving or user cancel), cancelPromise will throw
-    }
-  }, 5000)
-})()
-```
-
-### pluginControl
-
-#### loadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])
-
-Function that loads external libraries. Pass the name of the libraries as an Array of string, and returns a Promise. When promise resolves, the libraries are loaded.
-
-**n.b.** while unlikely, there is a possibility that multiple requests to load external libraries in quick succession can cause the promise to resolve before the library is actually loaded. 
-
-```js
-const currentlySupportedLibraries = ['jquery@2','jquery@3','webcomponentsLite@1.1.0','react@16','reactdom@16','vue@2.5.16']
-
-window.interactivewViewer.loadExternalLibraries(currentlySupportedLibraries)
-  .then(() => {
-    /* loaded */
-  })
-  .catch(e=>console.warn(e))
-
-```
-
-####  unloadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])
-
-unloading the libraries (should be called on shutdown).
-
-#### *[PLUGINNAME]*
-
-returns a plugin handler. This would be how to interface with the plugins.
-
-##### blink()
-
-Function that causes the floating widget to blink, attempt to grab user attention (silently fails if called on startup).
-
-##### setProgressIndicator(val:number|null)
-
-Set the progress of the plugin. Useful for giving user feedbacks on the progress of a long running process. Call the function with null to unset the progress.
-
-##### shutdown()
-
-Function that causes the widget to shutdown dynamically. (triggers onShutdown callback, silently fails if called on startup)
-
-##### onShutdown(callback)
-
-Attaches a callback function, which is called when the plugin is shutdown.
-
-##### initState
-
-passed from `manifest.json`. Useful for setting initial state of the plugin. Can be any JSON valid value (array, object, string).
-
-##### initStateUrl
-
-passed from `manifest.json`. Useful for setting initial state of the plugin.  Can be any JSON valid value (array, object, string).
-
-##### setInitManifestUrl(url|null)
-
-set/unset the url for a manifest json that will be fetched on atlas viewer startup. the argument should be a valid URL, has necessary CORS header, and returns a valid manifest json file. null will unset the search param. Useful for passing/preserving state. If called multiple times, the last one will take effect.
-
-```js
-const pluginHandler = window.interactiveViewer.pluginControl[PLUGINNAME]
-
-const subscription = window.interactiveViewer.metadata.selectedTemplateBSubject.subscribe(template=>console.log(template))
-
-fetch(`http://YOUR_BACKEND.com/API_ENDPOINT`)
-  .then(data=>pluginHandler.blink(20))
-
-pluginHandler.onShutdown(()=>{
-  subscription.unsubscribe()
-})
-```
-
-------
-
-## window.nehubaViewer
-
-nehuba object, exposed if developer would like to use it
-
-## window.viewer
-
-neuroglancer object, exposed if developer would like to use it
\ No newline at end of file
diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts
index 10f003638..dc7da45d8 100644
--- a/src/routerModule/routeStateTransform.service.ts
+++ b/src/routerModule/routeStateTransform.service.ts
@@ -146,8 +146,11 @@ export class RouteStateTransformSvc {
     const pluginStates = fullPath.queryParams['pl']
     if (pluginStates) {
       try {
-        const arrPluginStates = JSON.parse(pluginStates)
-        returnState["[state.plugins]"].initManifests = arrPluginStates.map(url => [plugins.INIT_MANIFEST_SRC, url] as [string, string])
+        const arrPluginStates: string[] = JSON.parse(pluginStates)
+        if (arrPluginStates.length > 1) throw new Error(`can only initialise one plugin at a time`)
+        returnState["[state.plugins]"].initManifests = {
+          [plugins.INIT_MANIFEST_SRC]: arrPluginStates
+        }
       } catch (e) {
         /**
          * parsing plugin error
diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts
index 3500cc3dc..3359055f7 100644
--- a/src/routerModule/router.service.ts
+++ b/src/routerModule/router.service.ts
@@ -10,9 +10,8 @@ import { scan } from 'rxjs/operators'
 import { RouteStateTransformSvc } from "./routeStateTransform.service";
 import { SAPI } from "src/atlasComponents/sapi";
 import { generalActions } from "src/state";
-/**
- * http://localhost:8080/#/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-290/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIy..0.14gY0~.14gY0..1LSm
- */
+
+
 @Injectable({
   providedIn: 'root'
 })
diff --git a/src/state/plugins/actions.ts b/src/state/plugins/actions.ts
index 5fe4fcd15..759c75230 100644
--- a/src/state/plugins/actions.ts
+++ b/src/state/plugins/actions.ts
@@ -7,12 +7,3 @@ export const clearInitManifests = createAction(
     nameSpace: string
   }>()
 )
-
-export const setInitMan = createAction(
-  `${nameSpace} setInitMan`,
-  props<{
-    nameSpace: string
-    url: string
-    internal?: boolean
-  }>()
-)
diff --git a/src/state/plugins/effects.ts b/src/state/plugins/effects.ts
index c27a74669..5f147b9b1 100644
--- a/src/state/plugins/effects.ts
+++ b/src/state/plugins/effects.ts
@@ -6,9 +6,8 @@ import * as constants from "./const"
 import * as selectors from "./selectors"
 import * as actions from "./actions"
 import { DialogService } from "src/services/dialogService.service";
-import { of } from "rxjs";
-import { HttpClient } from "@angular/common/http";
-import { getHttpHeader } from "src/util/constants"
+import { NEVER, of } from "rxjs";
+import { PluginService } from "src/plugin/service";
 
 @Injectable()
 export class Effects{
@@ -16,27 +15,33 @@ export class Effects{
   initMan = this.store.pipe(
     select(selectors.initManfests),
     map(initMan => initMan[constants.INIT_MANIFEST_SRC]),
-    filter(val => !!val),
+    filter(val => val && val.length > 0),
   )
 
+  private pendingList = new Set<string>()
+  private launchedList = new Set<string>()
+  private banList = new Set<string>()
+
   initManLaunch = createEffect(() => this.initMan.pipe(
-    switchMap(val => 
-      this.dialogSvc
-        .getUserConfirm({
-          message: `This URL is trying to open a plugin from ${val}. Proceed?`
-        })
-        .then(() => 
-          this.http.get(val, {
-            headers: getHttpHeader(),
-            responseType: 'json'
-          }).toPromise()
-        )
-        .then(json => {
-          /**
-           * TODO fix init plugin launch
-           * at that time, also restore effects.spec.ts test
-           */
-        })
+    switchMap(val => of(...val)),
+    switchMap(
+      url => {
+        if (this.pendingList.has(url)) return NEVER
+        if (this.launchedList.has(url)) return NEVER
+        if (this.banList.has(url)) return NEVER
+        this.pendingList.add(url)
+        return this.dialogSvc
+          .getUserConfirm({
+            message: `This URL is trying to open a plugin from ${url}. Proceed?`
+          })
+          .then(() => {
+            this.launchedList.add(url)
+            return this.svc.launchPlugin(url)
+          })
+          .finally(() => {
+            this.pendingList.delete(url)
+          })
+      }
     ),
     catchError(() => of(null))
   ), { dispatch: false })
@@ -52,8 +57,8 @@ export class Effects{
   constructor(
     private store: Store,
     private dialogSvc: DialogService,
-    private http: HttpClient,
+    private svc: PluginService,
   ){
     
   }
-}
\ No newline at end of file
+}
diff --git a/src/state/plugins/store.ts b/src/state/plugins/store.ts
index 83bb211ec..7283a77a4 100644
--- a/src/state/plugins/store.ts
+++ b/src/state/plugins/store.ts
@@ -1,9 +1,8 @@
 import { createReducer, on } from "@ngrx/store";
 import * as actions from "./actions"
-import { INIT_MANIFEST_SRC } from "./const"
 
 export type PluginStore = {
-  initManifests: Record<string, string>
+  initManifests: Record<string, string[]>
 }
 
 export const defaultState: PluginStore = {
@@ -15,8 +14,8 @@ export const reducer = createReducer(
   on(
     actions.clearInitManifests,
     (state, { nameSpace }) => {
-      if (!state[nameSpace]) return state
-      const newMan: Record<string, string> = {}
+      if (!state.initManifests[nameSpace]) return state
+      const newMan: Record<string, string[]> = {}
       const { initManifests } = state
       for (const key in initManifests) {
         if (key === nameSpace) continue
@@ -28,20 +27,4 @@ export const reducer = createReducer(
       }
     }
   ),
-  on(
-    actions.setInitMan,
-    (state, { nameSpace, url, internal }) => {
-      if (!internal) {
-        if (nameSpace === INIT_MANIFEST_SRC) return state
-      }
-      const { initManifests } = state
-      return {
-        ...state,
-        initManifests: {
-          ...initManifests,
-          [nameSpace]: url
-        }
-      }
-    }
-  )
 )
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index 5980c76d6..136c0d0a3 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -14,6 +14,8 @@ import { ColorMapCustomLayer } from "src/state/atlasAppearance";
 import { SapiRegionModel } from "src/atlasComponents/sapi";
 import { AnnotationLayer } from "src/atlasComponents/annotations";
 import { PMAP_LAYER_NAME } from "../constants"
+import { EnumColorMapName, mapKeyColorMap } from "src/util/colorMaps";
+import { getShader } from "src/util/constants";
 
 export const BACKUP_COLOR = {
   red: 255,
@@ -275,15 +277,20 @@ 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 getUpdatedCustomLayer(isSameLayer: (o: atlasAppearance.NgLayerCustomLayer, n: atlasAppearance.NgLayerCustomLayer) => boolean){
+    return 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(n => oldCustomLayers.some(o => o.id === n.id && !isSameLayer(o, n)))
+      }),
+      filter(arr => arr.length > 0),
+    )
+  }
+
+  private updateCustomLayerTransparency$ = this.getUpdatedCustomLayer((o, n) => o.opacity === n.opacity)
+  private updateCustomLayerColorMap$ = this.getUpdatedCustomLayer((o, n) => o.shader === n.shader)
 
   private ngLayers$ = this.customLayers$.pipe(
     map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]),
@@ -346,6 +353,19 @@ export class NehubaLayerControlService implements OnDestroy{
         } as TNgLayerCtrl<'setLayerTransparency'>
       })
     ),
+    this.updateCustomLayerColorMap$.pipe(
+      map(layers => {
+        const payload: Record<string, string> = {}
+        for (const layer of layers) {
+          const shader = layer.shader ?? getShader()
+          payload[layer.id] = shader
+        }
+        return {
+          type: 'updateShader',
+          payload
+        } as TNgLayerCtrl<'updateShader'>
+      })
+    ),
     this.manualNgLayersControl$,
   ).pipe(
   )
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
index d5a743d67..1fa9f52ff 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
@@ -51,6 +51,9 @@ export interface INgLayerCtrl {
   setLayerTransparency: {
     [key: string]: number
   }
+  updateShader: {
+    [key: string]: string
+  }
 }
 
 export type TNgLayerCtrl<T extends keyof INgLayerCtrl> = {
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index cfd902f1a..b1f70202a 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -349,6 +349,12 @@ export class NehubaViewerUnit implements OnDestroy {
                 this.setLayerTransparency(key, p.payload[key])
               }
             }
+            if (message.type === "updateShader") {
+              const p = message as TNgLayerCtrl<'updateShader'>
+              for (const key in p.payload) {
+                this.setLayerShader(key, p.payload[key])
+              }
+            }
           }
         })
       )
@@ -796,6 +802,11 @@ export class NehubaViewerUnit implements OnDestroy {
     if (layer.layer.opacity) layer.layer.opacity.restoreState(alpha)
   }
 
+  private setLayerShader(layerName: string, shader: string) {
+    const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
+    if (layer?.layer?.fragmentMain) layer.layer.fragmentMain.restoreState(shader)
+  }
+
   public setMeshTransparency(flag: boolean){
 
     /**
-- 
GitLab