plexora 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. plexora-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,704 @@
1
+ /**
2
+ * main.js Initializes main client/interface setup, and distributes events to respective views
3
+ */
4
+
5
+ //EVENTHANDLER
6
+ const eventHandler = new SimpleEventHandler(d3.select("body").node());
7
+ const datasource = flaskVariables.datasource;
8
+
9
+ //VIEWS
10
+ const __plexora = window.__plexora = {
11
+ dataLayer: null,
12
+ channelList: null,
13
+ seaDragonViewer: null,
14
+ viewerSidebar: null,
15
+ // Activated plugins by name: { definition, instance, sidebarController,
16
+ // cleanups }. Replaces the single csv_gatingList slot -- core no longer has
17
+ // a place that can only hold one plugin, or that names a particular one.
18
+ plugins: new Map()
19
+ }
20
+
21
+ //DATA MANAGEMENT
22
+ const dataSrcIndex = 0;
23
+
24
+ //CHANNELS
25
+ const imageChannels = {}; // lookup table between channel id and channel name (for image viewer)
26
+ const imageChannelsIdx = {};
27
+
28
+ //OTHER SETTINGS
29
+ document.getElementById("openseadragon").addEventListener("contextmenu", (event) => event.preventDefault()); //Disable right clicking on element
30
+
31
+ //LOAD DATA
32
+ // Data prevent caching on the config file, as it may have been modified
33
+ window.__plexoraReady = d3.json(`${plexoraUrl("config")}?t=${Date.now()}`).then(function (config) {
34
+ return init(config[datasource]);
35
+ });
36
+
37
+ //INITS
38
+
39
+ /**
40
+ * Init all views.
41
+ *
42
+ * @param conf - The configuration json file
43
+ */
44
+ async function init(config) {
45
+ // Flat RGB quick-view datasource (no channels, no gating, no feature
46
+ // table worth loading) -- hand off to the minimal pan/zoom-only viewer
47
+ // and skip DataLayer/ChannelList/ViewerSidebar/module setup entirely.
48
+ if (config.image_kind === "rgb") {
49
+ const rgbViewer = new RgbImageViewer(config);
50
+ __plexora.seaDragonViewer = rgbViewer;
51
+ await rgbViewer.init();
52
+ return;
53
+ }
54
+
55
+ //maximum selections
56
+ config.maxSelections = 4;
57
+ config.extraZoomLevels = 0;
58
+ if (Array.isArray(config.imageData)) {
59
+ config.imageData.forEach(function (channel) {
60
+ if (channel.src && channel.src.startsWith("/")) {
61
+ channel.src = plexoraUrl(channel.src);
62
+ }
63
+ });
64
+ }
65
+ //channel information
66
+ for (let idx = 0; idx < config["imageData"].length; idx++) {
67
+ imageChannels[config["imageData"][idx].fullname] = idx;
68
+ let name = config["imageData"][idx].name;
69
+ if (name !== "Area") {
70
+ imageChannelsIdx[idx] = name;
71
+ }
72
+ }
73
+
74
+ //initialize metadata
75
+ const dataLayer = new DataLayer(config, imageChannels);
76
+ const numericData = new NumericData(config, dataLayer);
77
+ const columns = await dataLayer.getChannelNames(true);
78
+ let imgMetadata = null;
79
+ try {
80
+ imgMetadata = await dataLayer.getMetadata();
81
+ } catch (error) {
82
+ console.error("Error getting metadata:", error);
83
+ }
84
+
85
+ //Create channel panels
86
+ channelList = new ChannelList(config, columns, dataLayer, eventHandler);
87
+ __plexora.channelList = channelList;
88
+ __plexora.dataLayer = dataLayer;
89
+
90
+ // Plugins whose scripts this page loaded (see the server's Plugin.scripts).
91
+ // Any number may be registered; each is activated below.
92
+ const pluginDefs = window.Plexora?.plugins?.all() ?? [];
93
+
94
+ //Create image viewer
95
+ const imageArgs = [imgMetadata, numericData, eventHandler];
96
+ const seaDragonViewer = new ImageViewer(config, dataLayer, ...imageArgs);
97
+ __plexora.seaDragonViewer = seaDragonViewer;
98
+ const viewerManager = new ViewerManager(seaDragonViewer, channelList);
99
+
100
+ // Core viewer toggles (Centroids/HD/Outlines) -- unconditional, independent of
101
+ // whichever tool (if any) is active, so they work on a plain base viewer too.
102
+ const viewerControls = new ViewerControls(seaDragonViewer, config, eventHandler);
103
+ __plexora.viewerControls = viewerControls;
104
+ viewerControls.init();
105
+
106
+ // Fire the database description request without awaiting it here --
107
+ // ImageViewer.init() below never reads dd (only WebGL/OSD setup), so
108
+ // there's no reason to serialize tile-viewer construction behind this
109
+ // network round trip. Only channelList.init(dd) and each plugin's own
110
+ // init(dd) below actually need dd, and they run after it resolves anyway.
111
+ const ddPromise = dataLayer.getDatabaseDescription();
112
+
113
+ const imageInit = [viewerManager, channelList, null, [], []];
114
+ const [dd] = await Promise.all([ddPromise, dataLayer.init(), seaDragonViewer.init(...imageInit)]);
115
+ __plexora.databaseDescription = dd;
116
+ // The dataset handed to every plugin -- same shape as the server's
117
+ // plexora.api dataset, so a plugin reads roles rather than column names.
118
+ __plexora.dataset = PlexoraDataset.build(config, imageChannels, dd);
119
+ channelList.init(dd);
120
+
121
+ /**
122
+ * Re-fetch what the server says this project's numbers are.
123
+ *
124
+ * Both of the things a tool is drawn from -- the read spec in `config` and
125
+ * the per-column statistics in `dd` -- are fetched once at page load. That
126
+ * is fine right up until the user changes which matrix is read, or turns
127
+ * the log transform on, from the requirements modal: the server re-reads
128
+ * the table and clears its own caches, but the page is still holding the
129
+ * ranges and histograms of the matrix it is no longer reading. Thresholding
130
+ * then opens on a panel of X values with a read spec pointing at a layer.
131
+ *
132
+ * Both `config` and `dd` are updated in place rather than replaced. Each is
133
+ * held by reference by things that outlive this call -- `config` by the
134
+ * viewer, the channel list and ViewerControls; `dd` by ChannelList and
135
+ * ViewerSidebar, which took it at boot (channelList.init(dd) /
136
+ * viewerSidebar.init(dd)) and read their ranges and histograms straight out
137
+ * of it ever after. Handing them a new object leaves them on the old one:
138
+ * rebinding only __plexora's reference is what left Thresholding drawing a
139
+ * log-valued slider over an X-valued histogram, since the gating panel
140
+ * reads the sidebar's copy.
141
+ */
142
+ __plexora.refreshDataset = async function refreshDataset() {
143
+ const fresh = await d3.json(`${plexoraUrl("config")}?t=${Date.now()}`);
144
+ const entry = fresh?.[datasource];
145
+ if (!entry) return;
146
+ // Only the read spec: attaching a mask also rewrites imageData, and
147
+ // adopting a new channel list mid-session would shift every index the
148
+ // tile path and the channel sliders are keyed on.
149
+ config.dataset = entry.dataset;
150
+ const description = await dataLayer.getDatabaseDescription();
151
+ for (const [column, stats] of Object.entries(description || {})) {
152
+ // Merged into the existing entry, not assigned over it: a channel's
153
+ // image_min/image_max/image_histogram and its quantization window
154
+ // are fetched lazily on activation (ChannelList.ensureChannelStats)
155
+ // and live in these same entries. Only which numbers the feature
156
+ // table holds has changed -- the image has not -- so they stay.
157
+ dd[column] = { ...dd[column], ...stats };
158
+ }
159
+ __plexora.dataset = PlexoraDataset.build(config, imageChannels, dd);
160
+ };
161
+
162
+ // The three calls toolLoader makes while painting a card, defined HERE
163
+ // rather than beside activatePlugin below: a page opened with ?tool=<name>
164
+ // reports its already-live tool through registerLoaded partway down this
165
+ // function, which goes through show() and reaches all three. Assigned later,
166
+ // they would be undefined at exactly that moment -- and every call site
167
+ // guards with `?.`, so the boot path would silently skip them instead of
168
+ // failing.
169
+
170
+ /**
171
+ * Point the shared controls at a tool the user has just switched to.
172
+ *
173
+ * Every layer that was visible STAYS visible -- that is the whole point of
174
+ * layers. What moves is which one the Cells control, the opacity slider,
175
+ * picking and the gate flows act on.
176
+ *
177
+ * Only for plugins that declared ownsCellLayer: a tool that draws its own
178
+ * overlay (ROI) must not take the active layer off the one that legitimately
179
+ * holds it merely by being looked at. The controls are re-synced either way,
180
+ * because opening ROI does not change what they should show.
181
+ */
182
+ __plexora.setActiveTool = function setActiveTool(name) {
183
+ const record = name ? __plexora.plugins.get(name) : null;
184
+ if (record?.instance && record.definition?.ownsCellLayer) {
185
+ seaDragonViewer.setActiveCellLayer(name);
186
+ }
187
+ viewerControls.syncToActiveLayer();
188
+ return seaDragonViewer.cellLayerOwner === name;
189
+ };
190
+
191
+ /**
192
+ * Draw a tool's layer, or stop drawing it, without unloading the tool.
193
+ *
194
+ * Returns whether this plugin has a layer at all -- a tool that draws its
195
+ * own overlay answers false, and its card's toggle is its controller's to
196
+ * honour (see the onVisibilityChange hook in pluginRegistry.js).
197
+ */
198
+ __plexora.setToolLayerVisible = function setToolLayerVisible(name, visible) {
199
+ const record = __plexora.plugins.get(name);
200
+ if (!record?.definition?.ownsCellLayer) return false;
201
+ // Only on a real change, and only then: the mask item and the point
202
+ // overlay are shared, so whether they are needed at all is a question
203
+ // about the whole stack rather than about this one layer.
204
+ if (seaDragonViewer.setCellLayerVisible(name, visible)) {
205
+ viewerControls.refreshLayerSurfaces();
206
+ }
207
+ return true;
208
+ };
209
+
210
+ /** Restack the cell layers, bottom of the sidebar order first. */
211
+ __plexora.setToolLayerOrder = function setToolLayerOrder(names) {
212
+ return seaDragonViewer.setCellLayerOrder(names);
213
+ };
214
+
215
+ // Instantiate plugins after the viewer exists but before tile loading gets
216
+ // going, matching the order the single-module path used to run in.
217
+ for (const definition of pluginDefs) {
218
+ activatePluginInstance(definition, dd);
219
+ }
220
+
221
+ //EVENT HANDLING
222
+
223
+ /**
224
+ * Listen to Color Transfer Change Events and forwards it to respective views.
225
+ *
226
+ * @param d - The color map object
227
+ */
228
+ const actionColorTransferChange = (d) => {
229
+ //map to full name
230
+ d.name = dataLayer.getFullChannelName(d.name);
231
+ // d3.select('body').style('cursor', 'progress');
232
+ seaDragonViewer.updateChannelColors(d.name, d.color, d.type);
233
+ // d3.select('body').style('cursor', 'default');
234
+ };
235
+ eventHandler.bind(ChannelList.events.COLOR_TRANSFER_CHANGE, actionColorTransferChange);
236
+
237
+ /**
238
+ * Listen to Render Mode Events and forwards it to respective views.
239
+ *
240
+ * @param d - The render mode object
241
+ */
242
+ const actionRenderingModeChange = (d) => {
243
+ seaDragonViewer.updateRenderingMode(d);
244
+ };
245
+ eventHandler.bind(ImageViewer.events.renderingMode, actionRenderingModeChange);
246
+
247
+ /**
248
+ * Listen to Channels set for Rendering and forwards it to respective views.
249
+ *
250
+ * @param d - The channel package object
251
+ */
252
+ const actionChannelsToRenderChange = (d) => {
253
+ //map to full name
254
+ d.name = dataLayer.getFullChannelName(d.name);
255
+
256
+ //send to image viewer
257
+ const action = ["remove", "add"][+d.status];
258
+ seaDragonViewer.updateActiveChannels(d.name, action);
259
+ // The tiles this kicks off are reported by the status indicator via
260
+ // OSD's fully-loaded-change (see appStatus.js watchViewer). The
261
+ // cursor:progress that used to bracket this call was set and cleared
262
+ // in the same synchronous turn, so it was never actually on screen
263
+ // while the work it described was running.
264
+ };
265
+ eventHandler.bind(ChannelList.events.CHANNELS_CHANGE, actionChannelsToRenderChange);
266
+
267
+ /**
268
+ * Listen to regional or single cell selection.
269
+ *
270
+ * @param d - The selections
271
+ */
272
+ const actionImageClickedMultiSel = (d) => {
273
+ // Nothing to select against without a feature table, or without a
274
+ // cell id naming the column that identifies a row.
275
+ const idField = __plexora.dataset?.schema?.cellId;
276
+ if (!idField) return;
277
+ const task = window.PlexoraStatus?.begin("Selecting");
278
+ try {
279
+ // add newly clicked item to selection
280
+ if (!Array.isArray(d.item)) {
281
+ dataLayer.addToCurrentSelection(d.item, true, d.clearPriors);
282
+ const picked = [d.item[idField]];
283
+ updateSeaDragonSelection({ picked });
284
+ } else {
285
+ dataLayer.addAllToCurrentSelection(d.item);
286
+ const picked = d.item.map(i => i[idField]);
287
+ updateSeaDragonSelection({ picked });
288
+ }
289
+ } finally {
290
+ task?.done();
291
+ }
292
+ };
293
+ eventHandler.bind(ImageViewer.events.imageClickedMultiSel, actionImageClickedMultiSel);
294
+
295
+ /**
296
+ * Listen to Channel Select Click Events.
297
+ *
298
+ * @param sels - The selected/deselected channels
299
+ */
300
+ const channelSelect = async (sels) => {
301
+ updateSeaDragonSelection();
302
+ let channelCells = await dataLayer.getChannelCellIds(sels);
303
+ dataLayer.addAllToCurrentSelection(channelCells);
304
+ };
305
+ eventHandler.bind(ChannelList.events.CHANNEL_SELECT, channelSelect);
306
+
307
+ /**
308
+ * Listens to and updates based on selection changes (specific for seadragon).
309
+ *
310
+ * @param props - may contain cell id
311
+ */
312
+ function updateSeaDragonSelection(props = {}) {
313
+ if ("picked" in props) {
314
+ seaDragonViewer.pickedIds = props.picked;
315
+ }
316
+ seaDragonViewer.forceRepaint();
317
+ }
318
+
319
+ /**
320
+ * Ranges one layer's plugin wants drawn, or {} when there is none.
321
+ *
322
+ * @param name - whose layer to ask. Null means the ACTIVE one, which is what
323
+ * core's own paths want; a plugin's gate flow passes its own name so a
324
+ * tool the user has switched away from still gates its OWN cells rather
325
+ * than whichever layer happens to be active.
326
+ *
327
+ * Core asks the viewer rather than reading a named plugin's state, so these
328
+ * paths work for any plugin -- or none.
329
+ */
330
+ function cellFilterFor(name = null) {
331
+ const provider = name
332
+ ? seaDragonViewer.getCellLayer(name)?.provider
333
+ : seaDragonViewer.cellLayer;
334
+ return provider?.getColorCodedRanges?.() ?? {};
335
+ }
336
+
337
+ function currentCellFilter() {
338
+ return cellFilterFor(null);
339
+ }
340
+
341
+ let centroidGateRequest = 0;
342
+ const updateCentroidsForGate = async (name = null) => {
343
+ if (!seaDragonViewer.shouldDrawCentroids()) return;
344
+ const requestId = ++centroidGateRequest;
345
+ seaDragonViewer.setLoading(true);
346
+ try {
347
+ await seaDragonViewer.ensureCentroidsReady(false);
348
+ if (requestId === centroidGateRequest) {
349
+ seaDragonViewer.updateCentroidFilter(cellFilterFor(name), true);
350
+ }
351
+ } finally {
352
+ seaDragonViewer.setLoading(false);
353
+ }
354
+ };
355
+ let segmentationGateRequest = 0;
356
+ const updateSegmentationForGate = async (showSpinner = true, name = undefined) => {
357
+ if (!seaDragonViewer.viewerManagerVMain?.sel_outlines) return;
358
+ const requestId = ++segmentationGateRequest;
359
+ await seaDragonViewer.updateSegmentationFilter(
360
+ cellFilterFor(name ?? null), showSpinner, name);
361
+ if (requestId !== segmentationGateRequest) {
362
+ seaDragonViewer.forceRepaint();
363
+ }
364
+ };
365
+ // Runs on every move tick, as fast as the network+render round trip allows, rather than
366
+ // waiting on a fixed interval: if a request is still in flight when the next tick arrives,
367
+ // it's marked pending and replayed immediately (with the latest gate values) as soon as the
368
+ // in-flight one finishes, so the mask keeps following the handle continuously while dragging.
369
+ let segmentationGateRunning = false;
370
+ let segmentationGatePending = false;
371
+ const runSegmentationGate = async (showSpinner, name = undefined) => {
372
+ if (!seaDragonViewer.viewerManagerVMain?.sel_outlines) return;
373
+ if (segmentationGateRunning) {
374
+ segmentationGatePending = true;
375
+ return;
376
+ }
377
+ segmentationGateRunning = true;
378
+ try {
379
+ await updateSegmentationForGate(showSpinner, name);
380
+ while (segmentationGatePending) {
381
+ segmentationGatePending = false;
382
+ await updateSegmentationForGate(false, name);
383
+ }
384
+ } finally {
385
+ segmentationGateRunning = false;
386
+ }
387
+ };
388
+
389
+ eventHandler.bind(ChannelList.events.BRUSH_MOVE, (d) => {
390
+ const fullName = dataLayer.getFullChannelName(d.name);
391
+ seaDragonViewer.updateChannelRange(fullName, d.dataRange[0], d.dataRange[1]);
392
+ });
393
+
394
+ /**
395
+ * Reset the (core) channel list to its initial values. Add-on modules hook their own
396
+ * reset behavior onto this same event via bindEvents() below.
397
+ */
398
+ const reset_lists = () => {
399
+ channelList.resetChannelList();
400
+ seaDragonViewer.forceRepaint();
401
+ };
402
+ eventHandler.bind(ChannelList.events.RESET_LISTS, reset_lists);
403
+
404
+ for (const definition of pluginDefs) {
405
+ bindPluginEvents(definition);
406
+ }
407
+
408
+ if (typeof ViewerSidebar !== "undefined" && document.getElementById("viewer_sidebar")) {
409
+ const viewerSidebar = new ViewerSidebar(config, columns, dataLayer, eventHandler, channelList);
410
+ __plexora.viewerSidebar = viewerSidebar;
411
+ for (const definition of pluginDefs) {
412
+ const controller = createPluginSidebar(definition, viewerSidebar);
413
+ if (!controller) continue;
414
+ viewerSidebar.registerModule(controller);
415
+ // Tell toolLoader.js this tool is already live (rendered server-side via
416
+ // a direct/bookmarked ?tool= link) so its close button and any later
417
+ // Tools-menu click work off the real state instead of re-fetching and
418
+ // re-activating a plugin that is already registered.
419
+ if (window.PlexoraToolLoader) {
420
+ const slotIds = Array.from(
421
+ document.querySelectorAll(`[data-tool-mount="${definition.name}"]`)
422
+ ).map((el) => el.id);
423
+ window.PlexoraToolLoader.registerLoaded(definition.name, slotIds, controller);
424
+ }
425
+ }
426
+ await viewerSidebar.init(dd);
427
+ }
428
+
429
+ // Segmentation-mask processing (pyramid/outline generation) runs in a
430
+ // background job on upload -- see data_model.start_segmentation_job -- so
431
+ // the viewer can open before it's done. Poll until it's ready (or errors
432
+ // out), then take the finished mask on in place.
433
+ //
434
+ // This used to call location.reload(). On a large mask that job runs for
435
+ // minutes, so the reload landed well into a working session: the viewer
436
+ // went blank and came back at the default viewport with every channel,
437
+ // tool, gate and pan/zoom gone, and nothing on screen said why. It is also
438
+ // avoidable -- see adoptSegmentation below for why nothing needs rebuilding.
439
+ //
440
+ // Each reading is announced as well as acted on. A plugin that is waiting
441
+ // for this mask rather than drawing a substitute (viewerControls'
442
+ // enableCellLayer) has to be able to say how far along it is, and the poll
443
+ // that already knows is the only thing that should be asking the server.
444
+ if (config.segmentation_status === 'pending') {
445
+ const announce = (what, detail) => window.dispatchEvent(
446
+ new CustomEvent(`plexora:segmentation-${what}`, { detail }));
447
+ //: Consecutive polls that came back with nothing. getSegmentationStatus
448
+ //: swallows its own errors and returns undefined, so this is the only
449
+ //: way to tell a dead server from a job that is simply still running.
450
+ let silent = 0;
451
+ const pollSegmentationStatus = async () => {
452
+ const status = await dataLayer.getSegmentationStatus();
453
+ if (status?.status === 'ready') {
454
+ adoptSegmentation(status.segmentation);
455
+ announce('ready', { segmentation: status.segmentation });
456
+ return;
457
+ }
458
+ if (status?.status === 'error') {
459
+ announce('failed', { error: status.error || '' });
460
+ return;
461
+ }
462
+ if (status?.status === 'pending') {
463
+ silent = 0;
464
+ announce('progress', {
465
+ progress: typeof status.progress === 'number' ? status.progress : null,
466
+ message: status.message || '',
467
+ });
468
+ } else if ((silent += 1) > 10) {
469
+ // Long enough that no ordinary hiccup reaches it, short enough
470
+ // that a panel waiting on this loop is not left waiting on a
471
+ // server that is never going to answer.
472
+ announce('failed', { error: '' });
473
+ return;
474
+ }
475
+ // One bad answer must not abandon a job that is still running
476
+ // server-side, which is why the count above has to run out first.
477
+ window.setTimeout(pollSegmentationStatus, 1500);
478
+ };
479
+ // Asked straight away rather than after a first interval: something may
480
+ // be showing a progress bar with nothing in it until this answers.
481
+ pollSegmentationStatus();
482
+ }
483
+
484
+ /**
485
+ * Take on the mask the background job just finished, without a reload.
486
+ *
487
+ * Nothing about the page is stale except one fact. The "Area" placeholder
488
+ * channel is inserted into imageData when the mask is *attached* (see
489
+ * import_routes.attach_segmentation), not when its pyramid lands, so no
490
+ * channel index shifts here and nothing keyed off one has to be rebuilt.
491
+ * The only thing that changes is config.segmentation going from null to a
492
+ * path -- and the label layer is loaded lazily from that, on demand, by
493
+ * viewerManager.load_label_image().
494
+ *
495
+ * @param path the derived pyramid the job produced.
496
+ */
497
+ function adoptSegmentation(path) {
498
+ if (!path || config.segmentation) return;
499
+ config.segmentation = path;
500
+ config.segmentation_status = 'ready';
501
+ // Both were set when the viewer opened and found no mask to fetch;
502
+ // clearing them is what lets the layer be requested now. `noLabel` also
503
+ // short-circuits ensureSegmentationReady(), so it has to go first.
504
+ seaDragonViewer.noLabel = false;
505
+ if (seaDragonViewer.viewerManagerVMain) {
506
+ seaDragonViewer.viewerManagerVMain.labelLayerRequested = false;
507
+ }
508
+
509
+ // Outlines and Filled were disabled while there was no mask to draw,
510
+ // and now there is one. Done before the swap below so the control is
511
+ // right either way -- a user who is drawing nothing on purpose still
512
+ // gains the option they were previously not offered.
513
+ viewerControls.refreshAvailability();
514
+
515
+ // A tool asked for this mask and was told to wait rather than given
516
+ // centroids instead (viewerControls.enableCellLayer). Nothing is drawing
517
+ // yet, so this is the moment the layer it asked for actually turns on.
518
+ if (seaDragonViewer.cellLayerAwaitingMask) {
519
+ seaDragonViewer.cellLayerAwaitingMask = false;
520
+ viewerControls.selectMode(
521
+ viewerControls.maskMode(viewerControls.ownerMaskPreference()));
522
+ return;
523
+ }
524
+
525
+ // Swap the drawing over only when what is showing is the fallback this
526
+ // very absence caused. A user who chose Centroids themselves gets to
527
+ // keep them, and a viewer drawing nothing was drawing nothing on
528
+ // purpose -- neither is a state a finished background job should
529
+ // overrule.
530
+ if (viewerControls.mode !== 'centroids') return;
531
+ if (!seaDragonViewer.centroidsFromFallback) return;
532
+ // Drawn the way whatever holds the cell layer asks for, not always as
533
+ // outlines. A project that gained its mask from the edit page reaches
534
+ // this path rather than enableCellLayer's, and hardcoding outlines here
535
+ // meant the tool's own preference was honoured on every later page load
536
+ // and never on the one where the mask actually arrived.
537
+ viewerControls.selectMode(
538
+ viewerControls.maskMode(viewerControls.ownerMaskPreference()));
539
+ }
540
+
541
+ /**
542
+ * Record of one activated plugin. Cleanups let a plugin release globals it
543
+ * registered (window listeners, timers) when it is torn down -- the old
544
+ * single-module world never needed this because the one module lived for
545
+ * the life of the page.
546
+ */
547
+ function pluginRecord(definition) {
548
+ let record = __plexora.plugins.get(definition.name);
549
+ if (!record) {
550
+ record = { definition, instance: null, sidebarController: null, cleanups: [] };
551
+ __plexora.plugins.set(definition.name, record);
552
+ }
553
+ return record;
554
+ }
555
+
556
+ /** The context every plugin hook receives. */
557
+ function pluginContext(definition, extra = {}) {
558
+ const record = pluginRecord(definition);
559
+ return {
560
+ // What this plugin is given: image data always, plus segmentation
561
+ // and a feature table when the project has them.
562
+ dataset: __plexora.dataset,
563
+ config,
564
+ columns,
565
+ dataLayer,
566
+ eventHandler,
567
+ channelList,
568
+ viewer: seaDragonViewer,
569
+ datasource,
570
+ // Core event names, so a plugin can hook core behaviour without
571
+ // reaching for a concrete core class off window.
572
+ coreEvents: ChannelList.events,
573
+ url: plexoraUrl,
574
+ // Ask the user for something this plugin declared in its Requires
575
+ // but the project has not got. Resolves true once it is recorded --
576
+ // centrally, so another plugin needing the same thing finds it
577
+ // already answered. A plugin should never build its own "type a
578
+ // column name" input; declare the requirement and call this.
579
+ requirements: {
580
+ require: (keys) => window.PlexoraRequirements.require(
581
+ datasource, definition.name, keys),
582
+ },
583
+ onCleanup: (fn) => record.cleanups.push(fn),
584
+ instance: record.instance,
585
+ ...extra,
586
+ };
587
+ }
588
+
589
+ function activatePluginInstance(definition, databaseDescription) {
590
+ const record = pluginRecord(definition);
591
+ record.instance = definition.createInstance
592
+ ? definition.createInstance(pluginContext(definition))
593
+ : null;
594
+
595
+ // Only a plugin that says it colours cells gets a layer. Registering is
596
+ // explicit because it is what puts a card in the sidebar and a pass in
597
+ // the compositor -- see ImageViewer.registerCellLayer.
598
+ if (record.instance && definition.ownsCellLayer) {
599
+ record.instance.pluginName = definition.name;
600
+ // Carried on the provider, so core can ask the viewer who holds the
601
+ // layer instead of learning plugin names. The paths that turn the
602
+ // mask on without a plugin activating -- a pyramid finishing
603
+ // conversion mid-session -- read it from there.
604
+ record.instance.preferredCellMode = definition.preferredCellMode || null;
605
+ seaDragonViewer.registerCellLayer(definition.name, record.instance, {
606
+ supportedModes: definition.supportedCellModes || null,
607
+ });
608
+ // The shared Cells control follows whichever layer is active, and
609
+ // this one just became it.
610
+ viewerControls.syncToActiveLayer();
611
+ // Nothing is drawn over the image until something needs it -- this
612
+ // is that moment. WHICH layer is the project's recorded choice, not
613
+ // this plugin's: the next plugin to register must get the same
614
+ // answer. HOW the mask is drawn -- filled or outlines -- is the
615
+ // plugin's, because it depends on what it is showing. See
616
+ // viewerControls.enableCellLayer.
617
+ viewerControls.enableCellLayer(definition.preferredCellMode, definition.name);
618
+ }
619
+ if (record.instance?.init) {
620
+ record.instance.init(databaseDescription, seaDragonViewer);
621
+ }
622
+ return record.instance;
623
+ }
624
+
625
+ function bindPluginEvents(definition) {
626
+ if (!definition.bindEvents) return;
627
+ // The two gate actions are bound to THIS plugin's own layer. A plugin
628
+ // that gates cells while another tool is the active one must move its
629
+ // own cells, not the active layer's -- passing the name here is what
630
+ // keeps a plugin from having to know it exists.
631
+ definition.bindEvents(pluginContext(definition, {
632
+ seaDragonViewer,
633
+ moduleInstance: pluginRecord(definition).instance,
634
+ updateSeaDragonSelection,
635
+ updateCentroidsForGate: () => updateCentroidsForGate(definition.name),
636
+ runSegmentationGate: (showSpinner) => runSegmentationGate(showSpinner, definition.name),
637
+ }));
638
+ }
639
+
640
+ function createPluginSidebar(definition, sidebar) {
641
+ if (!definition.createSidebarController) return null;
642
+ const record = pluginRecord(definition);
643
+ const controller = definition.createSidebarController(pluginContext(definition, {
644
+ sidebar,
645
+ moduleInstance: record.instance,
646
+ }));
647
+ record.sidebarController = controller || null;
648
+ return controller || null;
649
+ }
650
+
651
+ /**
652
+ * Activate a plugin discovered after boot (see toolLoader.js), once the
653
+ * viewer, dataLayer and ViewerSidebar are already up. The boot path above
654
+ * is left running its own sequence rather than routed through here: its
655
+ * timing is load-order sensitive (instance.init() must precede tile
656
+ * loading), and both paths end in the same registered state.
657
+ */
658
+ __plexora.activatePlugin = async function activatePlugin(definition) {
659
+ const instance = activatePluginInstance(definition, __plexora.databaseDescription);
660
+ bindPluginEvents(definition);
661
+
662
+ let sidebarController = null;
663
+ if (__plexora.viewerSidebar) {
664
+ sidebarController = createPluginSidebar(definition, __plexora.viewerSidebar);
665
+ if (sidebarController) {
666
+ await __plexora.viewerSidebar.registerModuleLate(sidebarController);
667
+ }
668
+ }
669
+ return { moduleInstance: instance, instance, sidebarController };
670
+ };
671
+
672
+ /**
673
+ * Tear a plugin down: run its cleanups, drop its cell layer, unhook its
674
+ * sidebar controller, and forget it. Without this a deactivated plugin
675
+ * leaves window-level listeners behind and a dead controller still receiving
676
+ * the sidebar's lifecycle calls -- neither of which could happen while a
677
+ * plugin lived for the life of the page.
678
+ */
679
+ __plexora.deactivatePlugin = function deactivatePlugin(name) {
680
+ const record = __plexora.plugins.get(name);
681
+ if (!record) return false;
682
+ for (const fn of record.cleanups) {
683
+ try {
684
+ fn();
685
+ } catch (error) {
686
+ console.error(`Plexora: cleanup failed for plugin "${name}"`, error);
687
+ }
688
+ }
689
+ try {
690
+ record.definition.destroy?.();
691
+ } catch (error) {
692
+ console.error(`Plexora: destroy() failed for plugin "${name}"`, error);
693
+ }
694
+ if (record.sidebarController) {
695
+ __plexora.viewerSidebar?.unregisterModule?.(record.sidebarController);
696
+ }
697
+ if (seaDragonViewer.unregisterCellLayer(name)) {
698
+ viewerControls.refreshLayerSurfaces();
699
+ }
700
+ __plexora.plugins.delete(name);
701
+ viewerControls.syncToActiveLayer();
702
+ return true;
703
+ };
704
+ }