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