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,115 @@
1
+ /**
2
+ * rgbImageViewer.js
3
+ *
4
+ * @class RgbImageViewer -- minimal pan/zoom viewer for a flat RGB quick-view
5
+ * datasource (config.image_kind === 'rgb', see main.js's init()). Kept as
6
+ * its own small class rather than a mode flag inside ImageViewer: that
7
+ * class unconditionally builds one grayscale transfer function per channel
8
+ * and a custom WebGL colorize pass (see its constructor) -- none of that
9
+ * applies to a flat image with imageData: [], so it's simpler and safer to
10
+ * hand OpenSeadragon its native single-image tile source directly and skip
11
+ * DataLayer/ChannelList/ViewerSidebar/gating entirely.
12
+ */
13
+ class RgbImageViewer {
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+
18
+ async init() {
19
+ this.viewer = OpenSeadragon({
20
+ id: "openseadragon",
21
+ prefixUrl: plexoraUrl("client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/"),
22
+ tileSources: {
23
+ type: "image",
24
+ url: plexoraUrl(`generated/rgb/${datasource}`),
25
+ },
26
+ minZoomImageRatio: 0.9,
27
+ visibilityRatio: 1,
28
+ homeFillsViewer: false,
29
+ showNavigator: false,
30
+ });
31
+ window.PlexoraStatus?.watchViewer(this.viewer);
32
+ const loader = document.getElementById("openseadragon_loader");
33
+ this.viewer.addHandler("open", () => {
34
+ if (loader) {
35
+ loader.style.display = "none";
36
+ }
37
+ });
38
+ this.initProjectLabel();
39
+ }
40
+
41
+ initProjectLabel() {
42
+ const wrapper = document.getElementById("openseadragon_wrapper");
43
+ if (!wrapper || document.getElementById("viewer_project_label")) return;
44
+ const label = document.createElement("div");
45
+ label.id = "viewer_project_label";
46
+ label.className = "viewer-project-label";
47
+ label.textContent = datasource || "";
48
+ wrapper.appendChild(label);
49
+ }
50
+
51
+ downloadCurrentView(format = "png") {
52
+ const canvas = this.viewer?.drawer?.canvas;
53
+ if (!canvas) return;
54
+
55
+ if (format === "pdf") {
56
+ const pdf = new jsPDF({
57
+ orientation: canvas.width >= canvas.height ? "landscape" : "portrait",
58
+ unit: "px",
59
+ format: [canvas.width, canvas.height],
60
+ });
61
+ pdf.addImage(canvas.toDataURL("image/png"), "PNG", 0, 0, canvas.width, canvas.height);
62
+ // No channels/scale bar on a flat RGB image -- the project label
63
+ // is the only overlay, drawn as real vector text/shape rather
64
+ // than baked into the image pixels (matches ImageViewer's PDF
65
+ // export, see imageViewer.js's drawProjectLabelVector).
66
+ this.drawProjectLabelVector(pdf);
67
+ pdf.save(`${datasource || "plexora"}_current_view.pdf`);
68
+ return;
69
+ }
70
+ const link = document.createElement("a");
71
+ link.download = `${datasource || "plexora"}_current_view.png`;
72
+ link.href = canvas.toDataURL("image/png");
73
+ document.body.appendChild(link);
74
+ link.click();
75
+ document.body.removeChild(link);
76
+ }
77
+
78
+ getOverlayRectInCanvasSpace(el) {
79
+ const canvasEl = this.viewer?.drawer?.canvas;
80
+ if (!canvasEl || !el) return null;
81
+ const canvasRect = canvasEl.getBoundingClientRect();
82
+ const elRect = el.getBoundingClientRect();
83
+ if (!canvasRect.width || !canvasRect.height) return null;
84
+ const scaleX = canvasEl.width / canvasRect.width;
85
+ const scaleY = canvasEl.height / canvasRect.height;
86
+ return {
87
+ x: (elRect.left - canvasRect.left) * scaleX,
88
+ y: (elRect.top - canvasRect.top) * scaleY,
89
+ width: elRect.width * scaleX,
90
+ height: elRect.height * scaleY,
91
+ scale: scaleX,
92
+ };
93
+ }
94
+
95
+ drawProjectLabelVector(pdf) {
96
+ const labelEl = document.getElementById("viewer_project_label");
97
+ if (!labelEl) return;
98
+ const rect = this.getOverlayRectInCanvasSpace(labelEl);
99
+ if (!rect || !rect.width) return;
100
+
101
+ pdf.saveGraphicsState();
102
+ pdf.setGState(new pdf.GState({ opacity: 0.86 }));
103
+ pdf.setFillColor(17, 24, 39);
104
+ pdf.roundedRect(rect.x, rect.y, rect.width, rect.height, 3 * rect.scale, 3 * rect.scale, "F");
105
+ pdf.restoreGraphicsState();
106
+
107
+ pdf.setFont("helvetica", "bold");
108
+ pdf.setFontSize(Math.max(6, 13 * rect.scale));
109
+ pdf.setTextColor(241, 245, 249);
110
+ pdf.text(labelEl.textContent || "", rect.x + rect.width / 2, rect.y + rect.height / 2, {
111
+ align: "center",
112
+ baseline: "middle",
113
+ });
114
+ }
115
+ }
@@ -0,0 +1,411 @@
1
+ /**
2
+ * @class SearchableSelect - lightweight typeahead combobox replacing a plain <select>.
3
+ * Mounted into a container element; filters its option list live as the user types.
4
+ *
5
+ * Two shapes, because two different jobs:
6
+ *
7
+ * trigger: "input" (the default) -- the field IS the combobox. It shows the
8
+ * current value, and typing over it filters. Right where the control is
9
+ * the only thing on its line and the value is what you want to read.
10
+ *
11
+ * trigger: "button" -- a button showing the current value, with the search
12
+ * field inside the dropdown it opens. Narrower, because it is sized to
13
+ * its label rather than to a text field, so it can share a line with
14
+ * other controls; and the search is where somebody looking for a search
15
+ * box would look, which a field that doubles as the value display is
16
+ * not -- there is nothing about it that says it can be typed into.
17
+ *
18
+ * Both filter the same list, report through the same onChange, and are reached
19
+ * the same way from the keyboard.
20
+ */
21
+ class SearchableSelect {
22
+ constructor(mount, options = {}) {
23
+ this.mount = mount;
24
+ this.options = [...(options.options || [])];
25
+ this.value = options.value || "";
26
+ this.placeholder = options.placeholder || "Search…";
27
+ //: What the search field inside a button-triggered menu says. The
28
+ //: trigger's own label is the current value, so the two cannot share
29
+ //: one string.
30
+ this.searchPlaceholder = options.searchPlaceholder || this.placeholder;
31
+ //: Shown on a button trigger that has no value yet.
32
+ this.emptyLabel = options.emptyLabel || this.placeholder;
33
+ this.describeOption = options.describeOption || (() => "");
34
+ // Optional per-option status dot (e.g. "this marker already has a
35
+ // gate set") -- separate from describeOption's always-visible text
36
+ // hint, since this is a lightweight visual cue whose detail only
37
+ // needs to show up on hover (title attribute), not inline text.
38
+ this.getIndicator = options.getIndicator || null;
39
+ // What an exhausted filter says. Defaulted to the marker wording this
40
+ // widget was written for, so the channel rows and the gate picker read
41
+ // as they always did.
42
+ this.emptyText = options.emptyText || "No markers match";
43
+ // For callers whose visible <label> cannot use `for` -- this mounts a
44
+ // generated input, so there is no stable id to point one at.
45
+ this.ariaLabel = options.ariaLabel || "";
46
+ this.onChange = options.onChange || (() => {});
47
+ this.trigger = options.trigger === "button" ? "button" : "input";
48
+ this.filtered = [...this.options];
49
+ this.activeIndex = -1;
50
+ this.isOpen = false;
51
+ this.render();
52
+ }
53
+
54
+ render() {
55
+ this.mount.innerHTML = "";
56
+ this.mount.classList.add("marker-combobox");
57
+
58
+ this.menu = document.createElement("div");
59
+ this.menu.className = "marker-combobox-menu";
60
+ this.menu.hidden = true;
61
+ // Appended to <body> (a positioning "portal"), not this.mount: a disabled/dimmed
62
+ // ancestor row has opacity < 1, which creates its own CSS stacking context and
63
+ // would trap this menu's z-index behind the *next* row otherwise.
64
+ document.body.appendChild(this.menu);
65
+
66
+ if (this.trigger === "button") {
67
+ this.renderButtonTrigger();
68
+ } else {
69
+ this.renderInputTrigger();
70
+ }
71
+
72
+ // `field` is whatever the typing goes into and `anchor` is what the
73
+ // menu hangs off. They are the same element for an input trigger and
74
+ // deliberately different for a button one, which is the whole of the
75
+ // difference between the two shapes.
76
+ this.field.addEventListener("input", () => this.filter(this.field.value));
77
+ this.field.addEventListener("keydown", (event) => this.handleKeydown(event));
78
+ this.field.addEventListener("blur", () => {
79
+ window.setTimeout(() => {
80
+ const active = document.activeElement;
81
+ if (active === this.field || this.menu?.contains(active)
82
+ || active === this.anchor) {
83
+ return;
84
+ }
85
+ this.close();
86
+ if (this.trigger === "input") this.field.value = this.value;
87
+ }, 120);
88
+ });
89
+ }
90
+
91
+ /** The classic shape: one field that is both the value and the search. */
92
+ renderInputTrigger() {
93
+ this.input = document.createElement("input");
94
+ this.input.type = "text";
95
+ this.input.className = "sidebar-select marker-combobox-input";
96
+ this.input.autocomplete = "off";
97
+ this.input.spellcheck = false;
98
+ this.input.placeholder = this.placeholder;
99
+ this.input.setAttribute("role", "combobox");
100
+ this.input.setAttribute("aria-expanded", "false");
101
+ this.input.setAttribute("aria-autocomplete", "list");
102
+ if (this.ariaLabel) this.input.setAttribute("aria-label", this.ariaLabel);
103
+ this.input.value = this.value;
104
+ this.mount.appendChild(this.input);
105
+
106
+ this.input.addEventListener("focus", () => this.open(true));
107
+ this.input.addEventListener("click", () => this.open(true));
108
+
109
+ this.anchor = this.input;
110
+ this.field = this.input;
111
+ this.menu.setAttribute("role", "listbox");
112
+ this.list = this.menu;
113
+ }
114
+
115
+ /** The compact shape: a button, with the search inside the dropdown. */
116
+ renderButtonTrigger() {
117
+ this.button = document.createElement("button");
118
+ this.button.type = "button";
119
+ this.button.className = "sidebar-select marker-combobox-trigger";
120
+ this.button.setAttribute("aria-haspopup", "listbox");
121
+ this.button.setAttribute("aria-expanded", "false");
122
+ if (this.ariaLabel) {
123
+ this.button.setAttribute("aria-label", this.ariaLabel);
124
+ this.button.title = this.ariaLabel;
125
+ }
126
+
127
+ this.buttonLabel = document.createElement("span");
128
+ this.buttonLabel.className = "marker-combobox-trigger-label";
129
+ const caret = document.createElement("span");
130
+ caret.className = "fas fa-chevron-down marker-combobox-caret";
131
+ caret.setAttribute("aria-hidden", "true");
132
+ this.button.append(this.buttonLabel, caret);
133
+ this.button.addEventListener("click", () => this.toggle());
134
+ this.mount.appendChild(this.button);
135
+
136
+ this.menu.classList.add("has-search");
137
+ const searchRow = document.createElement("div");
138
+ searchRow.className = "marker-combobox-searchbox";
139
+ const icon = document.createElement("span");
140
+ icon.className = "fas fa-magnifying-glass";
141
+ icon.setAttribute("aria-hidden", "true");
142
+ this.field = document.createElement("input");
143
+ this.field.type = "text";
144
+ this.field.className = "marker-combobox-search";
145
+ this.field.autocomplete = "off";
146
+ this.field.spellcheck = false;
147
+ this.field.placeholder = this.searchPlaceholder;
148
+ this.field.setAttribute("role", "combobox");
149
+ this.field.setAttribute("aria-expanded", "true");
150
+ this.field.setAttribute("aria-autocomplete", "list");
151
+ this.field.setAttribute("aria-label", this.ariaLabel || this.searchPlaceholder);
152
+ searchRow.append(icon, this.field);
153
+
154
+ this.list = document.createElement("div");
155
+ this.list.className = "marker-combobox-list";
156
+ this.list.setAttribute("role", "listbox");
157
+ this.menu.append(searchRow, this.list);
158
+
159
+ this.anchor = this.button;
160
+ this.paintTrigger();
161
+ }
162
+
163
+ /** The button's label: the current value, or what to pick if there is none. */
164
+ paintTrigger() {
165
+ if (!this.buttonLabel) return;
166
+ this.buttonLabel.textContent = this.value || this.emptyLabel;
167
+ this.buttonLabel.classList.toggle("is-placeholder", !this.value);
168
+ // The value can be far wider than the button, which truncates it -- so
169
+ // the full text is on the title, where the label alone would leave a
170
+ // column name unreadable in a narrow sidebar.
171
+ this.button.title = this.value
172
+ ? (this.ariaLabel ? `${this.ariaLabel}: ${this.value}` : this.value)
173
+ : (this.ariaLabel || this.emptyLabel);
174
+ }
175
+
176
+ setOptions(names) {
177
+ this.options = [...names];
178
+ if (this.isOpen) {
179
+ this.filter(this.field.value);
180
+ }
181
+ }
182
+
183
+ setValue(name) {
184
+ this.value = name || "";
185
+ if (this.trigger === "button") {
186
+ this.paintTrigger();
187
+ } else {
188
+ this.field.value = this.value;
189
+ }
190
+ }
191
+
192
+ filter(query) {
193
+ const q = query.trim().toLowerCase();
194
+ this.filtered = q ? this.options.filter((name) => name.toLowerCase().includes(q)) : [...this.options];
195
+ this.activeIndex = this.filtered.length ? 0 : -1;
196
+ this.renderMenu();
197
+ this.open(false);
198
+ }
199
+
200
+ toggle() {
201
+ if (this.isOpen) {
202
+ this.close();
203
+ } else {
204
+ this.open(true);
205
+ }
206
+ }
207
+
208
+ open(reset) {
209
+ if (reset) {
210
+ this.filtered = [...this.options];
211
+ this.activeIndex = Math.max(this.options.indexOf(this.value), 0);
212
+ this.renderMenu();
213
+ if (this.trigger === "button") {
214
+ // Opened empty rather than seeded with the current value: this
215
+ // field is a search, not the value, and a query already in it
216
+ // would show one match and look like the list.
217
+ this.field.value = "";
218
+ this.field.focus();
219
+ } else {
220
+ this.field.select();
221
+ }
222
+ }
223
+ this.isOpen = true;
224
+ this.positionMenu();
225
+ this.menu.hidden = false;
226
+ requestAnimationFrame(() => this.menu.classList.add("is-open"));
227
+ this.anchor.setAttribute("aria-expanded", "true");
228
+ this.bindDismissListeners();
229
+ }
230
+
231
+ positionMenu() {
232
+ const rect = this.anchor.getBoundingClientRect();
233
+ this.menu.style.left = `${rect.left}px`;
234
+ this.menu.style.top = `${rect.bottom + 4}px`;
235
+ // A button trigger is sized to its label, which can be narrower than
236
+ // the option names it is choosing between -- so the menu takes the
237
+ // wider of the two rather than inheriting a width nothing fits in.
238
+ this.menu.style.width = this.trigger === "button"
239
+ ? `${Math.max(rect.width, 240)}px`
240
+ : `${rect.width}px`;
241
+ }
242
+
243
+ close() {
244
+ this.isOpen = false;
245
+ if (!this.menu) return;
246
+ this.menu.classList.remove("is-open");
247
+ this.anchor.setAttribute("aria-expanded", "false");
248
+ window.setTimeout(() => {
249
+ // The menu may have been destroyed inside this delay -- closing is
250
+ // the first thing destroy() does, and the blur handler also lands
251
+ // here on its own timer.
252
+ if (!this.isOpen && this.menu) this.menu.hidden = true;
253
+ }, 150);
254
+ this.unbindDismissListeners();
255
+ }
256
+
257
+ bindDismissListeners() {
258
+ if (this._dismissScroll) return;
259
+ // Follows the trigger rather than shutting on any scroll at all.
260
+ //
261
+ // Closing was the cheap way to stop a body-portaled menu lingering at a
262
+ // stale position, and the cure was worse: a wheel the option list could
263
+ // not absorb -- at either end of it, or over the search box, which does
264
+ // not scroll -- reached the panel behind, so the menu being read
265
+ // vanished while the page moved under it. Repositioning keeps the
266
+ // promise (the menu is never somewhere its trigger is not) without
267
+ // taking the menu away from someone using it.
268
+ //
269
+ // Scrolling the menu's own list is not a page scroll and is ignored.
270
+ this._dismissScroll = (event) => {
271
+ if (event.target === this.menu || this.menu.contains(event.target)) return;
272
+ this.positionMenu();
273
+ // Scrolled out of view is the one case with nothing left to follow.
274
+ const rect = this.anchor.getBoundingClientRect();
275
+ const limit = window.innerHeight || document.documentElement?.clientHeight;
276
+ if (limit && (rect.bottom < 0 || rect.top > limit)) this.close();
277
+ };
278
+ this._dismissResize = () => this.close();
279
+ // A button trigger takes focus itself, so its menu cannot rely on the
280
+ // field's blur alone -- clicking anywhere else on the page has to shut
281
+ // it, the same way every other popover in the viewer behaves.
282
+ this._dismissClick = (event) => {
283
+ if (!this.isOpen) return;
284
+ if (this.menu.contains(event.target) || this.mount.contains(event.target)) return;
285
+ this.close();
286
+ };
287
+ window.addEventListener("scroll", this._dismissScroll, true);
288
+ window.addEventListener("resize", this._dismissResize);
289
+ document.addEventListener("click", this._dismissClick);
290
+ }
291
+
292
+ unbindDismissListeners() {
293
+ if (!this._dismissScroll) return;
294
+ window.removeEventListener("scroll", this._dismissScroll, true);
295
+ window.removeEventListener("resize", this._dismissResize);
296
+ document.removeEventListener("click", this._dismissClick);
297
+ this._dismissScroll = null;
298
+ this._dismissResize = null;
299
+ this._dismissClick = null;
300
+ }
301
+
302
+ renderMenu() {
303
+ this.list.innerHTML = "";
304
+ if (!this.filtered.length) {
305
+ const empty = document.createElement("div");
306
+ empty.className = "marker-combobox-empty";
307
+ empty.textContent = this.emptyText;
308
+ this.list.appendChild(empty);
309
+ return;
310
+ }
311
+ this.filtered.forEach((name, index) => {
312
+ const option = document.createElement("div");
313
+ option.className = "marker-combobox-option";
314
+ option.setAttribute("role", "option");
315
+ option.dataset.value = name;
316
+ if (name === this.value) option.classList.add("is-selected");
317
+ if (index === this.activeIndex) option.classList.add("is-active");
318
+
319
+ if (this.getIndicator) {
320
+ const indicatorTitle = this.getIndicator(name);
321
+ if (indicatorTitle) {
322
+ const dot = document.createElement("span");
323
+ dot.className = "marker-combobox-option-indicator";
324
+ dot.title = indicatorTitle;
325
+ option.appendChild(dot);
326
+ }
327
+ }
328
+
329
+ const label = document.createElement("span");
330
+ label.className = "marker-combobox-option-label";
331
+ label.textContent = name;
332
+ option.appendChild(label);
333
+
334
+ const hint = this.describeOption(name);
335
+ if (hint) {
336
+ const tag = document.createElement("span");
337
+ tag.className = "marker-combobox-hint";
338
+ tag.textContent = hint;
339
+ option.appendChild(tag);
340
+ }
341
+
342
+ option.addEventListener("mousedown", (event) => {
343
+ event.preventDefault();
344
+ this.selectOption(name);
345
+ });
346
+ this.list.appendChild(option);
347
+ });
348
+ }
349
+
350
+ selectOption(name) {
351
+ this.setValue(name);
352
+ this.close();
353
+ // Focus goes back to what opened the menu, or a keyboard user is left
354
+ // on an element that is no longer visible.
355
+ if (this.trigger === "button") this.button.focus();
356
+ this.onChange(name);
357
+ }
358
+
359
+ handleKeydown(event) {
360
+ if (!this.isOpen && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
361
+ event.preventDefault();
362
+ this.open(true);
363
+ return;
364
+ }
365
+ if (!this.isOpen) return;
366
+ if (event.key === "ArrowDown") {
367
+ event.preventDefault();
368
+ this.activeIndex = Math.min(this.activeIndex + 1, this.filtered.length - 1);
369
+ this.renderMenu();
370
+ this.scrollActiveIntoView();
371
+ } else if (event.key === "ArrowUp") {
372
+ event.preventDefault();
373
+ this.activeIndex = Math.max(this.activeIndex - 1, 0);
374
+ this.renderMenu();
375
+ this.scrollActiveIntoView();
376
+ } else if (event.key === "Enter") {
377
+ event.preventDefault();
378
+ const active = this.filtered[this.activeIndex];
379
+ if (active) this.selectOption(active);
380
+ } else if (event.key === "Escape") {
381
+ this.close();
382
+ if (this.trigger === "button") {
383
+ this.button.focus();
384
+ } else {
385
+ this.field.value = this.value;
386
+ this.field.blur();
387
+ }
388
+ }
389
+ }
390
+
391
+ scrollActiveIntoView() {
392
+ const active = this.menu.querySelector(".marker-combobox-option.is-active");
393
+ if (active) active.scrollIntoView({ block: "nearest" });
394
+ }
395
+
396
+ /**
397
+ * Take this combobox off the page for good.
398
+ *
399
+ * The menu is portaled onto <body> (see render), so removing the element it
400
+ * was mounted into leaves the menu behind -- a panel that is torn down and
401
+ * rebuilt otherwise strands one orphan per rebuild, and a stale one can
402
+ * still be opened by keyboard.
403
+ */
404
+ destroy() {
405
+ this.close();
406
+ this.unbindDismissListeners();
407
+ this.menu?.remove();
408
+ this.menu = null;
409
+ this.list = null;
410
+ }
411
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared step-2 wait for segmentation-mask generation.
3
+ *
4
+ * Both import flows start the mask job when their first form page is submitted
5
+ * (see data_model.start_segmentation_job), so by the time the user finishes the
6
+ * second page the conversion is usually well underway or already done. This
7
+ * shows what is left of it rather than dropping the user into a viewer whose
8
+ * cell layer would pop in some seconds later.
9
+ *
10
+ * Loaded as a plain <script>, like the pages that use it, so it exports one
11
+ * global rather than an ES module binding.
12
+ */
13
+
14
+ /**
15
+ * Take over the page with a progress panel until the mask is ready, then go to
16
+ * `redirectUrl`.
17
+ *
18
+ * @param {Object} options
19
+ * @param {string} options.datasource Dataset name to poll for.
20
+ * @param {string} options.redirectUrl Where to go once the mask is ready.
21
+ * @param {number} [options.pollIntervalMs=1000]
22
+ */
23
+ function awaitSegmentationThenOpen(options) {
24
+ const datasource = options.datasource;
25
+ const redirectUrl = options.redirectUrl;
26
+ const pollIntervalMs = options.pollIntervalMs || 1000;
27
+
28
+ const panel = renderPanel();
29
+ let stopped = false;
30
+
31
+ function go() {
32
+ stopped = true;
33
+ window.location = redirectUrl;
34
+ }
35
+
36
+ function poll() {
37
+ if (stopped) return;
38
+ fetch(plexoraUrl('get_segmentation_status') + '?' + new URLSearchParams({datasource: datasource}))
39
+ .then(function (response) { return response.json(); })
40
+ .then(function (status) {
41
+ if (stopped) return;
42
+ if (!status || status.status === 'ready') {
43
+ panel.setProgress(100, 'Segmentation mask ready');
44
+ // Let the finished bar actually render before navigating,
45
+ // otherwise a fast conversion just looks like a flicker.
46
+ window.setTimeout(go, 350);
47
+ return;
48
+ }
49
+ if (status.status === 'error') {
50
+ panel.setError(status.error, go);
51
+ return;
52
+ }
53
+ panel.setProgress(
54
+ typeof status.progress === 'number' ? status.progress : null,
55
+ // The server's message says which kind of mask is being
56
+ // built and, when the supplied one could not be used
57
+ // as-is, which requirement it missed.
58
+ status.message || 'Converting segmentation mask'
59
+ );
60
+ window.setTimeout(poll, pollIntervalMs);
61
+ })
62
+ .catch(function () {
63
+ // A transient fetch failure shouldn't abandon a job that is
64
+ // still running server-side -- keep polling.
65
+ if (!stopped) window.setTimeout(poll, pollIntervalMs);
66
+ });
67
+ }
68
+
69
+ poll();
70
+ }
71
+
72
+ function renderPanel() {
73
+ const overlay = document.createElement('div');
74
+ overlay.className = 'segmentation-progress-overlay';
75
+ overlay.setAttribute('role', 'status');
76
+ overlay.setAttribute('aria-live', 'polite');
77
+ overlay.innerHTML = [
78
+ '<div class="segmentation-progress-card">',
79
+ ' <div class="segmentation-progress-title">Preparing segmentation mask</div>',
80
+ ' <div class="segmentation-progress-detail">Converting segmentation mask</div>',
81
+ ' <div class="segmentation-progress-track">',
82
+ ' <div class="segmentation-progress-fill is-indeterminate"></div>',
83
+ ' </div>',
84
+ ' <div class="segmentation-progress-actions"></div>',
85
+ '</div>',
86
+ ].join('');
87
+ document.body.appendChild(overlay);
88
+
89
+ const detail = overlay.querySelector('.segmentation-progress-detail');
90
+ const fill = overlay.querySelector('.segmentation-progress-fill');
91
+ const title = overlay.querySelector('.segmentation-progress-title');
92
+ const actions = overlay.querySelector('.segmentation-progress-actions');
93
+
94
+ return {
95
+ setProgress: function (percent, message) {
96
+ detail.textContent = message;
97
+ if (percent === null) {
98
+ fill.classList.add('is-indeterminate');
99
+ fill.style.width = '';
100
+ return;
101
+ }
102
+ fill.classList.remove('is-indeterminate');
103
+ fill.style.width = Math.max(0, Math.min(100, percent)) + '%';
104
+ },
105
+ setError: function (message, onSkip) {
106
+ title.textContent = 'Segmentation mask failed';
107
+ title.classList.add('has-error');
108
+ detail.textContent = message || 'The mask could not be converted.';
109
+ fill.classList.remove('is-indeterminate');
110
+ fill.classList.add('has-error');
111
+ fill.style.width = '100%';
112
+ // The rest of the import succeeded, so offer the viewer without a
113
+ // cell layer rather than leaving the user stuck on this panel.
114
+ const button = document.createElement('button');
115
+ button.type = 'button';
116
+ button.className = 'btn btn-secondary';
117
+ button.textContent = 'Open viewer without segmentation';
118
+ button.addEventListener('click', onSkip);
119
+ actions.appendChild(button);
120
+ },
121
+ };
122
+ }