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,177 @@
1
+ /**
2
+ * RoiApi - this plugin's own HTTP client.
3
+ *
4
+ * A plugin owns the addresses of its own routes. Core's DataLayer must never
5
+ * learn them: that is what made gating privileged in the way the plugin API
6
+ * exists to rule out, and it is why `plugins/roi/...` appears in this file and
7
+ * nowhere in plexora/client/src/ (asserted by tests/test_datalayer_requests.py).
8
+ *
9
+ * Unlike GatingApi, nothing here swallows its own errors. That is deliberate.
10
+ * Gating's methods each catch and `console.log`, which is exactly what hid
11
+ * `saveGatingList` failing outright for every call it ever made -- and for
12
+ * annotations the stakes are higher, because a save that quietly does nothing
13
+ * means the user's regions exist only in a tab they are about to close. So a
14
+ * transport failure propagates to RoiStore, which keeps the geometry, retries,
15
+ * and says so on the panel.
16
+ *
17
+ * Requests return `{ok, status, data}` rather than throwing on a non-2xx: the
18
+ * status IS the answer for two of them. 409 means somebody else saved first and
19
+ * the user has work worth keeping; 422 means the image changed underneath. Both
20
+ * need handling, not an exception.
21
+ */
22
+ class RoiApi {
23
+
24
+ constructor(ctx) {
25
+ this.url = ctx.url;
26
+ this.datasource = ctx.datasource;
27
+ }
28
+
29
+ /** Build the response shape every method returns. */
30
+ async _read(response) {
31
+ let data = {};
32
+ try {
33
+ data = await response.json();
34
+ } catch (e) {
35
+ // A body that is not JSON is a server fault, not an answer. Keep the
36
+ // status, which is the part still worth acting on.
37
+ data = { error: "the server sent a response that could not be read" };
38
+ }
39
+ return { ok: response.ok, status: response.status, data };
40
+ }
41
+
42
+ async getState() {
43
+ const response = await fetch(this.url('plugins/roi/api/state') + '?' + new URLSearchParams({
44
+ datasource: this.datasource
45
+ }));
46
+ return this._read(response);
47
+ }
48
+
49
+ async postOperations(baseRevision, operations) {
50
+ const response = await fetch(this.url('plugins/roi/api/operations'), {
51
+ method: 'POST',
52
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({
54
+ datasource: this.datasource,
55
+ base_revision: baseRevision,
56
+ operations: operations
57
+ })
58
+ });
59
+ return this._read(response);
60
+ }
61
+
62
+ async importGeojson(document, baseRevision, acceptDimensionMismatch) {
63
+ const response = await fetch(this.url('plugins/roi/api/import'), {
64
+ method: 'POST',
65
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({
67
+ datasource: this.datasource,
68
+ base_revision: baseRevision,
69
+ document: document,
70
+ accept_dimension_mismatch: Boolean(acceptDimensionMismatch)
71
+ })
72
+ });
73
+ return this._read(response);
74
+ }
75
+
76
+ /**
77
+ * Download the server's export.
78
+ *
79
+ * Fetched into a Blob rather than navigated to, so a failure is visible
80
+ * here as a status rather than as the browser replacing the viewer with an
81
+ * error page -- and so this method is one the request probe can actually
82
+ * observe.
83
+ */
84
+ async downloadExport() {
85
+ const response = await fetch(this.url('plugins/roi/api/export.geojson') + '?' + new URLSearchParams({
86
+ datasource: this.datasource
87
+ }));
88
+ if (!response.ok) return this._read(response);
89
+ const blob = await response.blob();
90
+ RoiApi.saveBlob(blob, `${this.datasource}_rois.geojson`);
91
+ return { ok: true, status: response.status, data: {} };
92
+ }
93
+
94
+ /**
95
+ * Where this project's regions can be written, and under what name.
96
+ *
97
+ * One call rather than one per fact: the panel needs the kind, the default
98
+ * name, the names already taken in the user's file and the one this project
99
+ * last saved to, all at the same moment, and none of them is useful alone.
100
+ */
101
+ async destination() {
102
+ const response = await fetch(this.url('plugins/roi/api/adapters/destination') + '?' + new URLSearchParams({
103
+ datasource: this.datasource
104
+ }));
105
+ return this._read(response);
106
+ }
107
+
108
+ async saveToAnndata(key, replace) {
109
+ const response = await fetch(this.url('plugins/roi/api/adapters/anndata'), {
110
+ method: 'POST',
111
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({
113
+ datasource: this.datasource,
114
+ key: key,
115
+ // Never defaulted to true here. The server refuses an existing
116
+ // key without it, which is the guard that stops one annotation
117
+ // pass landing on another -- this flag is the user's answer to
118
+ // being asked, not a convenience.
119
+ replace: Boolean(replace)
120
+ })
121
+ });
122
+ return this._read(response);
123
+ }
124
+
125
+ /**
126
+ * Annotate the project's cells with the regions they fall inside.
127
+ *
128
+ * The other direction from the saves above: those put the polygons in the
129
+ * user's file, this puts two columns on their rows. `name` is the same
130
+ * destination name the saves take, because the columns are derived from it
131
+ * -- one name to keep track of, not two.
132
+ */
133
+ async mapToCells(name, replace) {
134
+ const response = await fetch(this.url('plugins/roi/api/map_to_cells'), {
135
+ method: 'POST',
136
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({
138
+ datasource: this.datasource,
139
+ name: name,
140
+ // Never defaulted to true, for the same reason the AnnData save
141
+ // does not: the server refuses an existing column without it,
142
+ // and that refusal is the guard against one mapping pass
143
+ // silently erasing another.
144
+ replace: Boolean(replace)
145
+ })
146
+ });
147
+ return this._read(response);
148
+ }
149
+
150
+ async saveToSpatialdata(elementName) {
151
+ const response = await fetch(this.url('plugins/roi/api/adapters/spatialdata'), {
152
+ method: 'POST',
153
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ datasource: this.datasource, element_name: elementName })
155
+ });
156
+ return this._read(response);
157
+ }
158
+
159
+ /**
160
+ * Hand the browser a file to save.
161
+ *
162
+ * Also the emergency path: RoiStore builds the same document from local
163
+ * state when the server is unreachable, so a user whose saves are failing
164
+ * can still get their work out of the tab.
165
+ */
166
+ static saveBlob(blob, filename) {
167
+ const href = URL.createObjectURL(blob);
168
+ const anchor = document.createElement("a");
169
+ anchor.href = href;
170
+ anchor.download = filename;
171
+ document.body.appendChild(anchor);
172
+ anchor.click();
173
+ anchor.remove();
174
+ // Deferred: revoking synchronously races the download in Safari.
175
+ setTimeout(() => URL.revokeObjectURL(href), 10_000);
176
+ }
177
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * roiFigureBridge.js - what ROI tells a figure about itself.
3
+ *
4
+ * Two DOM events, and no import in either direction. Figure Builder does not
5
+ * know this plugin exists and this plugin does not know Figure Builder does;
6
+ * the same arrangement `plexora:roi-hover` already uses between ROI and Cell
7
+ * Explorer, for the same reason -- a build with one and not the other has to
8
+ * work, and both of these listeners simply never fire when nobody dispatches.
9
+ *
10
+ * plexora:figure-capture-state "describe what you are drawing"
11
+ * plexora:figure-restore-state "here is what you were drawing"
12
+ *
13
+ * ## What is restored, and what deliberately is not
14
+ *
15
+ * ROI's category visibility is PERSISTED -- it lives in the annotation document
16
+ * beside the shapes. Restoring it would therefore be an edit to the user's
17
+ * annotations, made because they looked at a figure panel. That is precisely
18
+ * what a figure must not do: a panel edit may not silently rewrite the
19
+ * project's own state.
20
+ *
21
+ * So the restore applies only what is transient -- whether the overlay is drawn
22
+ * at all -- and REPORTS the rest. When the categories on screen no longer match
23
+ * the ones the panel was captured with, that comes back as "partial" and the
24
+ * figure says so. The panel's own preview is unaffected either way: it is a
25
+ * raster of what was captured, and export re-renders from the recorded state
26
+ * rather than from the live plugin.
27
+ */
28
+ (function () {
29
+ const PLUGIN_NAME = "roi";
30
+
31
+ /** This plugin's live controller, or null when its tool is not open. */
32
+ function controller() {
33
+ return window.__plexora?.plugins?.get(PLUGIN_NAME)?.sidebarController || null;
34
+ }
35
+
36
+ function version() {
37
+ return window.__plexora?.plugins?.get(PLUGIN_NAME)?.definition?.version || "";
38
+ }
39
+
40
+ /**
41
+ * What is being drawn, as a blob only this plugin reads back.
42
+ *
43
+ * The legend is computed HERE, now, and stored with the panel -- so a figure
44
+ * exported on a machine where this plugin is not installed still carries the
45
+ * right labels and colours. Recomputing it at export time from a plugin
46
+ * whose palette has since changed would produce a legend that disagrees with
47
+ * the panel above it.
48
+ */
49
+ function describe() {
50
+ const roi = controller();
51
+ const store = roi?.store;
52
+ if (!store) return null;
53
+
54
+ const categories = {};
55
+ const legend = [];
56
+ for (const category of store.sortedCategories ? store.sortedCategories() : store.categories) {
57
+ const visible = category.visible !== false;
58
+ categories[category.id] = visible;
59
+ if (visible) {
60
+ legend.push({ kind: "categorical", label: category.label, color: category.color });
61
+ }
62
+ }
63
+ return {
64
+ state: {
65
+ overlay_visible: Boolean(roi.renderer?.enabled),
66
+ categories: categories,
67
+ feature_count: store.features ? store.features.length : 0,
68
+ },
69
+ legend: legend,
70
+ };
71
+ }
72
+
73
+ window.addEventListener("plexora:figure-capture-state", (event) => {
74
+ const described = describe();
75
+ if (!described) return; // tool not open: contribute nothing, say nothing
76
+ event.detail?.contribute?.(PLUGIN_NAME, {
77
+ version: version(),
78
+ state: described.state,
79
+ legend: described.legend,
80
+ });
81
+ });
82
+
83
+ window.addEventListener("plexora:figure-restore-state", (event) => {
84
+ const wanted = event.detail?.plugins?.[PLUGIN_NAME];
85
+ const report = event.detail?.report;
86
+ if (!wanted) return;
87
+
88
+ const roi = controller();
89
+ if (!roi) return; // not open; Figure Builder records "skipped" for us
90
+
91
+ const state = wanted.state || {};
92
+ // Transient and ours to set: whether the overlay draws at all.
93
+ roi.renderer?.setEnabled?.(state.overlay_visible !== false);
94
+
95
+ // Persisted and NOT ours to overwrite. Reported instead, so the figure
96
+ // can tell the user which part of the panel the live viewer is not
97
+ // reproducing -- rather than either lying about it or quietly editing
98
+ // their annotations.
99
+ const current = roi.store?.categories || [];
100
+ const differs = current.some((category) =>
101
+ state.categories
102
+ && Object.prototype.hasOwnProperty.call(state.categories, category.id)
103
+ && (category.visible !== false) !== state.categories[category.id]);
104
+
105
+ report?.(PLUGIN_NAME, differs ? "partial" : "ok");
106
+ });
107
+ })();
@@ -0,0 +1,350 @@
1
+ /**
2
+ * roiGeometry.js - polygon arithmetic, and nothing else.
3
+ *
4
+ * Pure functions over plain arrays: no DOM, no viewer, no state. That is what
5
+ * lets the hard parts -- simplification, hit-testing, self-intersection -- be
6
+ * tested directly (tests/js/roi_geometry_probe.mjs) instead of only through a
7
+ * browser nobody can run in CI.
8
+ *
9
+ * Every coordinate here is a FULL-RESOLUTION IMAGE PIXEL. Screen pixels never
10
+ * reach this file; roiTools converts at the edge. Tolerances are therefore
11
+ * passed in already divided by the current zoom, which is why several functions
12
+ * take an epsilon rather than assuming one -- "within 6 pixels of the cursor"
13
+ * is a different number of image pixels at every zoom level.
14
+ */
15
+ const RoiGeometry = (function () {
16
+
17
+ /** Drop points that repeat their predecessor within `epsilon`.
18
+ *
19
+ * A pointer stream is full of these: a stationary mouse still fires move
20
+ * events, and two identical positions in a row make a zero-length segment
21
+ * that every downstream algorithm has to special-case. */
22
+ function dedupe(points, epsilon = 0) {
23
+ const out = [];
24
+ for (const point of points) {
25
+ const last = out[out.length - 1];
26
+ if (last && Math.abs(point[0] - last[0]) <= epsilon
27
+ && Math.abs(point[1] - last[1]) <= epsilon) continue;
28
+ out.push([point[0], point[1]]);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ /** How many genuinely different positions a ring has, ignoring a closing
34
+ * duplicate. Three is the minimum that encloses anything. */
35
+ function distinctCount(points) {
36
+ const seen = new Set();
37
+ for (const [x, y] of points) seen.add(`${x},${y}`);
38
+ return seen.size;
39
+ }
40
+
41
+ function closeRing(points) {
42
+ if (!points.length) return points;
43
+ const first = points[0];
44
+ const last = points[points.length - 1];
45
+ if (first[0] === last[0] && first[1] === last[1]) return points;
46
+ return [...points, [first[0], first[1]]];
47
+ }
48
+
49
+ /** A ring without its closing duplicate -- the form the editor works in,
50
+ * where every position is a vertex the user can drag. */
51
+ function openRing(points) {
52
+ if (points.length > 1) {
53
+ const first = points[0];
54
+ const last = points[points.length - 1];
55
+ if (first[0] === last[0] && first[1] === last[1]) return points.slice(0, -1);
56
+ }
57
+ return points;
58
+ }
59
+
60
+ /** Twice the signed area (the shoelace sum). Sign gives winding; magnitude
61
+ * is what the tiny-shape guard measures. */
62
+ function signedArea(points) {
63
+ const ring = openRing(points);
64
+ let sum = 0;
65
+ for (let i = 0; i < ring.length; i++) {
66
+ const [x1, y1] = ring[i];
67
+ const [x2, y2] = ring[(i + 1) % ring.length];
68
+ sum += x1 * y2 - x2 * y1;
69
+ }
70
+ return sum / 2;
71
+ }
72
+
73
+ function area(points) {
74
+ return Math.abs(signedArea(points));
75
+ }
76
+
77
+ // -- geometry objects ------------------------------------------------
78
+
79
+ function polygonFrom(points) {
80
+ return { type: "Polygon", coordinates: [closeRing(points)] };
81
+ }
82
+
83
+ /** Every ring of a Polygon or MultiPolygon, outer and holes alike. */
84
+ function rings(geometry) {
85
+ if (!geometry || !geometry.coordinates) return [];
86
+ if (geometry.type === "MultiPolygon") {
87
+ return geometry.coordinates.flat();
88
+ }
89
+ return geometry.coordinates;
90
+ }
91
+
92
+ /** The polygons of a geometry, each as its own list of rings. */
93
+ function polygons(geometry) {
94
+ if (!geometry || !geometry.coordinates) return [];
95
+ return geometry.type === "MultiPolygon" ? geometry.coordinates : [geometry.coordinates];
96
+ }
97
+
98
+ function vertexCount(geometry) {
99
+ return rings(geometry).reduce((total, ring) => total + ring.length, 0);
100
+ }
101
+
102
+ /**
103
+ * Whether the editor can move this shape's individual vertices.
104
+ *
105
+ * Only a plain single-ring polygon. Holes and MultiPolygons are preserved
106
+ * exactly as imported and can be selected, renamed, recategorized, moved as
107
+ * a whole and deleted -- they just cannot have their vertices dragged,
108
+ * because "drag a vertex" is ambiguous once there is more than one ring and
109
+ * guessing wrong silently rewrites somebody's imported contour.
110
+ */
111
+ function isVertexEditable(geometry) {
112
+ return Boolean(geometry) && geometry.type === "Polygon"
113
+ && Array.isArray(geometry.coordinates) && geometry.coordinates.length === 1;
114
+ }
115
+
116
+ function bounds(geometry) {
117
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
118
+ for (const ring of rings(geometry)) {
119
+ for (const [x, y] of ring) {
120
+ if (x < minX) minX = x;
121
+ if (y < minY) minY = y;
122
+ if (x > maxX) maxX = x;
123
+ if (y > maxY) maxY = y;
124
+ }
125
+ }
126
+ if (minX === Infinity) return null;
127
+ return { minX, minY, maxX, maxY };
128
+ }
129
+
130
+ function translate(geometry, dx, dy) {
131
+ const move = (ring) => ring.map(([x, y]) => [x + dx, y + dy]);
132
+ if (geometry.type === "MultiPolygon") {
133
+ return {
134
+ type: "MultiPolygon",
135
+ coordinates: geometry.coordinates.map((polygon) => polygon.map(move)),
136
+ };
137
+ }
138
+ return { type: "Polygon", coordinates: geometry.coordinates.map(move) };
139
+ }
140
+
141
+ // -- hit testing -----------------------------------------------------
142
+
143
+ /**
144
+ * Even-odd ray casting, which handles holes for free: a point inside an
145
+ * interior ring crosses the outer boundary once and the hole once, an even
146
+ * number, and so reads as outside -- which it is.
147
+ */
148
+ function containsPoint(geometry, x, y) {
149
+ for (const polygon of polygons(geometry)) {
150
+ let inside = false;
151
+ for (const ring of polygon) {
152
+ const points = openRing(ring);
153
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
154
+ const [xi, yi] = points[i];
155
+ const [xj, yj] = points[j];
156
+ if ((yi > y) !== (yj > y)
157
+ && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
158
+ inside = !inside;
159
+ }
160
+ }
161
+ }
162
+ if (inside) return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ function distanceToSegment(px, py, ax, ay, bx, by) {
168
+ const dx = bx - ax;
169
+ const dy = by - ay;
170
+ const lengthSquared = dx * dx + dy * dy;
171
+ let t = lengthSquared === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lengthSquared;
172
+ t = Math.max(0, Math.min(1, t));
173
+ const cx = ax + t * dx;
174
+ const cy = ay + t * dy;
175
+ return Math.hypot(px - cx, py - cy);
176
+ }
177
+
178
+ /** Shortest distance from a point to any edge -- what makes a thin sliver
179
+ * or a shape the cursor is just outside of still clickable. */
180
+ function distanceToBoundary(geometry, x, y) {
181
+ let best = Infinity;
182
+ for (const ring of rings(geometry)) {
183
+ const points = closeRing(ring);
184
+ for (let i = 0; i + 1 < points.length; i++) {
185
+ const distance = distanceToSegment(
186
+ x, y, points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]);
187
+ if (distance < best) best = distance;
188
+ }
189
+ }
190
+ return best;
191
+ }
192
+
193
+ /** The vertex nearest (x, y) within `tolerance`, or null.
194
+ *
195
+ * Reported as {ring, index} so a caller can put it back where it came from;
196
+ * only single-ring polygons are offered for editing (see isVertexEditable),
197
+ * but the index is ring-qualified anyway so that stays true if that changes. */
198
+ function nearestVertex(geometry, x, y, tolerance) {
199
+ let best = null;
200
+ const list = rings(geometry);
201
+ for (let r = 0; r < list.length; r++) {
202
+ const points = openRing(list[r]);
203
+ for (let i = 0; i < points.length; i++) {
204
+ const distance = Math.hypot(points[i][0] - x, points[i][1] - y);
205
+ if (distance <= tolerance && (!best || distance < best.distance)) {
206
+ best = { ring: r, index: i, distance };
207
+ }
208
+ }
209
+ }
210
+ return best;
211
+ }
212
+
213
+ // -- simplification --------------------------------------------------
214
+
215
+ /**
216
+ * Ramer-Douglas-Peucker.
217
+ *
218
+ * Freehand drawing produces one point per pointer event -- thousands for a
219
+ * single region, most of them describing sub-pixel wobble in a straight
220
+ * line. Stored raw they make every redraw, every hit test and every save
221
+ * proportional to how slowly the user moved the mouse.
222
+ *
223
+ * Run ONCE, when the stroke is completed, and never again. Simplification
224
+ * is lossy, so re-running it on each subsequent edit would walk the shape
225
+ * away from what was drawn a little at a time -- a shape nobody touched
226
+ * would keep changing.
227
+ *
228
+ * Iterative rather than recursive: a 100,000-point trace recurses deeply
229
+ * enough to blow the stack, and losing the whole shape to a RangeError at
230
+ * the moment of completion is the worst possible time for it.
231
+ */
232
+ function simplify(points, epsilon) {
233
+ if (points.length <= 2 || !(epsilon > 0)) return points.slice();
234
+
235
+ const keep = new Uint8Array(points.length);
236
+ keep[0] = 1;
237
+ keep[points.length - 1] = 1;
238
+
239
+ const stack = [[0, points.length - 1]];
240
+ while (stack.length) {
241
+ const [first, last] = stack.pop();
242
+ if (last <= first + 1) continue;
243
+
244
+ let farthest = -1;
245
+ let farthestDistance = 0;
246
+ for (let i = first + 1; i < last; i++) {
247
+ const distance = distanceToSegment(
248
+ points[i][0], points[i][1],
249
+ points[first][0], points[first][1],
250
+ points[last][0], points[last][1]);
251
+ if (distance > farthestDistance) {
252
+ farthestDistance = distance;
253
+ farthest = i;
254
+ }
255
+ }
256
+
257
+ if (farthestDistance > epsilon && farthest > 0) {
258
+ keep[farthest] = 1;
259
+ stack.push([first, farthest], [farthest, last]);
260
+ }
261
+ }
262
+
263
+ const out = [];
264
+ for (let i = 0; i < points.length; i++) if (keep[i]) out.push(points[i]);
265
+ return out;
266
+ }
267
+
268
+ // -- validity --------------------------------------------------------
269
+
270
+ /** Above this, the O(n^2) pair check below is skipped rather than run.
271
+ *
272
+ * The check is advisory -- it decides how a shape is outlined, not whether
273
+ * it is stored -- so declining to answer for a very large geometry is a
274
+ * fair trade against freezing the tab for several seconds mid-draw.
275
+ * Freehand strokes are simplified before this runs, so in practice the
276
+ * shapes that reach it are well under the limit; imported million-vertex
277
+ * contours are the case this protects against. */
278
+ const MAX_INTERSECTION_CHECK = 3000;
279
+
280
+ /**
281
+ * Does this ring cross itself?
282
+ *
283
+ * Reported, never repaired. `make_valid` and its equivalents turn a bow-tie
284
+ * into two triangles, which is a different annotation from the one the user
285
+ * drew -- and doing that invisibly, to a shape somebody is going to publish
286
+ * a measurement from, is not a repair. The renderer draws the outline
287
+ * differently and the user decides.
288
+ */
289
+ function selfIntersects(ring) {
290
+ const points = openRing(ring);
291
+ const n = points.length;
292
+ if (n < 4 || n > MAX_INTERSECTION_CHECK) return false;
293
+
294
+ for (let i = 0; i < n; i++) {
295
+ const a1 = points[i];
296
+ const a2 = points[(i + 1) % n];
297
+ for (let j = i + 1; j < n; j++) {
298
+ // Segments sharing an endpoint always "touch"; only genuinely
299
+ // disjoint pairs count as a crossing.
300
+ if (j === i || (j + 1) % n === i || j === (i + 1) % n) continue;
301
+ const b1 = points[j];
302
+ const b2 = points[(j + 1) % n];
303
+ if (segmentsCross(a1, a2, b1, b2)) return true;
304
+ }
305
+ }
306
+ return false;
307
+ }
308
+
309
+ function orientation(p, q, r) {
310
+ const value = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
311
+ if (value === 0) return 0;
312
+ return value > 0 ? 1 : 2;
313
+ }
314
+
315
+ function onSegment(p, q, r) {
316
+ return q[0] <= Math.max(p[0], r[0]) && q[0] >= Math.min(p[0], r[0])
317
+ && q[1] <= Math.max(p[1], r[1]) && q[1] >= Math.min(p[1], r[1]);
318
+ }
319
+
320
+ function segmentsCross(p1, q1, p2, q2) {
321
+ const o1 = orientation(p1, q1, p2);
322
+ const o2 = orientation(p1, q1, q2);
323
+ const o3 = orientation(p2, q2, p1);
324
+ const o4 = orientation(p2, q2, q1);
325
+ if (o1 !== o2 && o3 !== o4) return true;
326
+ if (o1 === 0 && onSegment(p1, p2, q1)) return true;
327
+ if (o2 === 0 && onSegment(p1, q2, q1)) return true;
328
+ if (o3 === 0 && onSegment(p2, p1, q2)) return true;
329
+ if (o4 === 0 && onSegment(p2, q1, q2)) return true;
330
+ return false;
331
+ }
332
+
333
+ /** Keep a point inside the image. Drawing tools clamp as the pointer moves,
334
+ * so a shape cannot be given coordinates that name no pixel; imported
335
+ * geometry is never clamped, because a region that genuinely continues off
336
+ * the edge of the slide is a real thing to have recorded. */
337
+ function clamp(point, width, height) {
338
+ const x = width ? Math.min(Math.max(point[0], 0), width) : point[0];
339
+ const y = height ? Math.min(Math.max(point[1], 0), height) : point[1];
340
+ return [x, y];
341
+ }
342
+
343
+ return Object.freeze({
344
+ dedupe, distinctCount, closeRing, openRing, signedArea, area,
345
+ polygonFrom, rings, polygons, vertexCount, isVertexEditable, bounds, translate,
346
+ containsPoint, distanceToSegment, distanceToBoundary, nearestVertex,
347
+ simplify, selfIntersects, segmentsCross, clamp,
348
+ MAX_INTERSECTION_CHECK,
349
+ });
350
+ })();