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,335 @@
1
+ /**
2
+ * projectEdit.js -- editing an existing project.
3
+ *
4
+ * The page is generated from the project record, not from a fixed form: a
5
+ * section renders only when `project.has` says it applies. A project with just
6
+ * an image shows an image summary and two empty slots, not a wall of
7
+ * coordinate pickers it has no columns for; an AnnData project shows its read
8
+ * spec and never the CSV column classifier.
9
+ *
10
+ * The image is deliberately the one thing that cannot change — everything a
11
+ * project derives (channel names, the tile pyramid, the mask's geometry) is
12
+ * keyed to it, so swapping it is a new project rather than an edit.
13
+ *
14
+ * Saving posts only what changed. The server merges, so a field this page
15
+ * never renders can never be cleared by saving.
16
+ */
17
+ (function () {
18
+ function ready(fn) {
19
+ if (document.readyState === "loading") {
20
+ document.addEventListener("DOMContentLoaded", fn);
21
+ } else {
22
+ fn();
23
+ }
24
+ }
25
+
26
+ ready(() => {
27
+ const root = document.getElementById("project-edit");
28
+ if (!root) return;
29
+
30
+ const project = JSON.parse(root.dataset.project);
31
+ const needs = root.dataset.needs ? JSON.parse(root.dataset.needs) : null;
32
+ const state = { roles: {}, columns: null };
33
+ let classifier = null;
34
+ let coordinates = null;
35
+
36
+ // Mirrors requirementsModal.js's option value for "only one image" --
37
+ // both surfaces translate it into the payload's `single_image` flag
38
+ // and neither ever sends it as it stands.
39
+ const SINGLE_IMAGE = "\u0000single-image";
40
+ // And the cell id's "number the rows" choice -- see requirementsModal.js
41
+ // for why it is an option rather than the wording on the blank.
42
+ const ROW_NUMBER = "\u0000row-number";
43
+
44
+ const maskInput = document.getElementById("edit_segmentation");
45
+ if (maskInput) {
46
+ // Nothing else on this page wires a Browse button: the upload
47
+ // page's script is what attaches [data-browse-target], and it does
48
+ // not load here -- so both buttons sat there doing nothing.
49
+ const maskBrowse = maskInput.parentElement
50
+ .querySelector(".browse-button");
51
+ if (typeof attachBrowseButton === "function") {
52
+ attachBrowseButton(maskBrowse, maskInput,
53
+ { mode: "file", filter: "image" });
54
+ }
55
+ }
56
+
57
+ // -- Data file ------------------------------------------------------
58
+ // The shared control (dataSourceField.js), not a bare input: a .zarr
59
+ // store with several tables, or a table spanning several images, needs
60
+ // narrowing before it can be read at all, and swapping one in here used
61
+ // to post a path the server could only refuse.
62
+ const dataMount = document.getElementById("edit_data_field");
63
+ const dataField = dataMount && window.PlexoraDataSourceField.mount(dataMount, {
64
+ id: "edit_data",
65
+ value: project.data?.src || "",
66
+ hint: dataMount.dataset.hint,
67
+ });
68
+
69
+ // -- Column classifier (CSV only) ---------------------------------
70
+ const classifierMount = document.getElementById("edit_classifier");
71
+ if (classifierMount && project.has.columns) {
72
+ classifier = PlexoraColumnClassifier.mount(classifierMount, {
73
+ markers: project.columns.markers,
74
+ metadata: project.columns.metadata,
75
+ roles: project.roles,
76
+ roleLabels: project.roleLabels,
77
+ onChange: () => {
78
+ const value = classifier.value();
79
+ state.columns = { markers: value.markers, metadata: value.metadata };
80
+ Object.assign(state.roles, value.roles);
81
+ },
82
+ });
83
+ }
84
+
85
+ // -- Role selects (AnnData / SpatialData) --------------------------
86
+ // The classifier already owns roles for CSV, so these render only for
87
+ // the formats whose columns are not user-classified. Options, current
88
+ // answer and blank-option wording all come from the server, which is
89
+ // what keeps this identical to the requirements modal -- for these
90
+ // formats the question is about the file's own obs columns and the
91
+ // answer goes into the read spec, and neither surface knows that.
92
+ const rolesMount = document.getElementById("edit_roles");
93
+ if (rolesMount && !project.has.columns && project.has.data) {
94
+ const candidates = project.roleColumns || [];
95
+
96
+ function configField(labelText) {
97
+ const field = document.createElement("div");
98
+ field.className = "config-field";
99
+ const label = document.createElement("label");
100
+ label.textContent = labelText;
101
+ field.appendChild(label);
102
+ rolesMount.appendChild(field);
103
+ return field;
104
+ }
105
+
106
+ // x/y are not column roles for these formats -- an obsm array holds
107
+ // both axes, so they are one question with its own control. Shared
108
+ // with the requirements modal (coordinateField.js) so the two
109
+ // surfaces ask and post the same thing.
110
+ const coordinateOptions = project.coordinateOptions;
111
+ const asksCoordinates = !!coordinateOptions
112
+ && Object.keys(coordinateOptions).length > 0;
113
+ const roles = (project.editableRoles || []).filter(
114
+ (role) => !(asksCoordinates && (role === "x" || role === "y")));
115
+
116
+ roles.forEach((role) => {
117
+ const field = configField(project.roleLabels[role]);
118
+ const select = document.createElement("select");
119
+ select.className = "form-select";
120
+ select.append(new Option("—", ""));
121
+ if (role === "image_id") {
122
+ // The other answer, and the only true one for data with no
123
+ // such column. See the same option in requirementsModal.js.
124
+ select.append(new Option("This data has only one image",
125
+ SINGLE_IMAGE, false, project.singleImage));
126
+ }
127
+ // Likewise for the cell id: an option, not the wording on the
128
+ // blank, so choosing it is something the user did.
129
+ const fallback = (project.roleDefaults || {})[role];
130
+ if (fallback) {
131
+ select.append(new Option(fallback, ROW_NUMBER, false,
132
+ project.rowNumberIds));
133
+ }
134
+ const current = (project.roleAnswers || {})[role];
135
+ candidates.forEach((name) => {
136
+ select.append(new Option(name, name, false, current === name));
137
+ });
138
+ select.addEventListener("change", () => {
139
+ if (role === "image_id") {
140
+ state.single_image = select.value === SINGLE_IMAGE;
141
+ state.roles[role] =
142
+ select.value === SINGLE_IMAGE ? null : (select.value || null);
143
+ return;
144
+ }
145
+ if (fallback) {
146
+ state.row_number_ids = select.value === ROW_NUMBER;
147
+ state.roles[role] =
148
+ select.value === ROW_NUMBER ? null : (select.value || null);
149
+ return;
150
+ }
151
+ state.roles[role] = select.value || null;
152
+ });
153
+ field.appendChild(select);
154
+ });
155
+
156
+ if (asksCoordinates) {
157
+ const field = configField("Cell coordinates");
158
+ coordinates = window.PlexoraCoordinateField.mount(field, {
159
+ ...coordinateOptions,
160
+ name: "edit-coordinates",
161
+ onChange: (value) => { state.coordinates = value; },
162
+ });
163
+ }
164
+ }
165
+
166
+ // -- Cell layer ----------------------------------------------------
167
+ const cellLayerInput = document.getElementById("edit_cell_layer");
168
+
169
+ // -- Expression matrix ---------------------------------------------
170
+ const featuresInput = document.getElementById("edit_features_layer");
171
+ const featuresLogInput = document.getElementById("edit_features_log");
172
+
173
+ // -- Highlight what a tool is waiting for --------------------------
174
+ // Reached from the Tools menu without JavaScript: the same requirement
175
+ // list the modal would have rendered, pointed at the fields that
176
+ // answer it. `confirm` counts as well as `missing` -- a tool opened for
177
+ // the first time on a well-named table has nothing missing and still
178
+ // wants its guesses looked at.
179
+ const outstanding = [...(needs?.missing || []), ...(needs?.confirm || [])];
180
+ if (needs && outstanding.length) {
181
+ const banner = document.getElementById("edit_needs");
182
+ banner.hidden = false;
183
+ banner.textContent = needs.missing.length
184
+ ? `${needs.label} needs: ${needs.missing.map((r) => r.label).join(", ")}.`
185
+ : `Check what ${needs.label} will use, then save.`;
186
+ const first = outstanding[0];
187
+ const target = first.kind === "segmentation" ? maskInput
188
+ : first.kind === "data" ? dataMount
189
+ : first.kind === "features" ? (featuresInput || featuresLogInput)
190
+ : classifierMount || rolesMount;
191
+ target?.scrollIntoView({ block: "center", behavior: "smooth" });
192
+ target?.classList.add("field-wanted");
193
+ }
194
+
195
+ // -- Save ----------------------------------------------------------
196
+ const save = document.getElementById("edit_save");
197
+ const error = document.getElementById("edit_error");
198
+
199
+ /**
200
+ * The requirement keys this page put in front of the user, in the
201
+ * server's vocabulary (plexora/api/plugin.py). Derived from what was
202
+ * rendered rather than hardcoded, so a section that does not apply to
203
+ * this project never claims to have asked about it.
204
+ */
205
+ function renderedKeys() {
206
+ const keys = [];
207
+ if (classifierMount && project.has.columns) keys.push("markers");
208
+ if (project.has.features) keys.push("features");
209
+ const roleFields = classifier ? Object.keys(project.roleLabels)
210
+ : (project.editableRoles || []);
211
+ keys.push(...roleFields.map((role) => `role:${role}`));
212
+ // Its own key, because for these formats it is its own question --
213
+ // and role:x/role:y are never asked here, so confirming them would
214
+ // claim the user was shown something the page did not render.
215
+ if (coordinates) {
216
+ keys.push("coordinates");
217
+ }
218
+ return keys;
219
+ }
220
+
221
+ save.addEventListener("click", async () => {
222
+ save.disabled = true;
223
+ error.hidden = true;
224
+
225
+ // A data file the user has not finished describing -- a store
226
+ // whose table is still unpicked -- is not something to post. The
227
+ // server would refuse it by asking for a choice, which reads as
228
+ // nonsense with the control offering that choice right there.
229
+ const waiting = dataField?.blocking();
230
+ if (waiting) {
231
+ error.textContent = waiting;
232
+ error.hidden = false;
233
+ save.disabled = false;
234
+ return;
235
+ }
236
+
237
+ const task = window.PlexoraStatus?.begin("Saving project");
238
+
239
+ const payload = {};
240
+ // Only send a path field when its value differs from what is
241
+ // stored. Sending it unchanged would be harmless but would make
242
+ // the server re-read the file and rebuild the mask pyramid, which
243
+ // is minutes of work for a no-op save.
244
+ if (maskInput && maskInput.value.trim() !== (project.segmentation.src || "")) {
245
+ payload.segmentation = maskInput.value.trim();
246
+ }
247
+ // The data file carries three companions when it changes -- which
248
+ // table inside the store, and which image inside the table. They
249
+ // go only with a changed path, since that is the only thing they
250
+ // describe.
251
+ const data = dataField?.value();
252
+ if (data && data.data !== (project.data?.src || "")) {
253
+ payload.data = data.data;
254
+ if (data.table) payload.table = data.table;
255
+ if (data.subset_column) {
256
+ payload.subset_column = data.subset_column;
257
+ payload.subset_value = data.subset_value;
258
+ }
259
+ }
260
+ if (Object.keys(state.roles).length) payload.roles = state.roles;
261
+ if (state.columns) payload.columns = state.columns;
262
+ if (state.single_image !== undefined) {
263
+ payload.single_image = state.single_image;
264
+ }
265
+ if (state.row_number_ids !== undefined) {
266
+ payload.row_number_ids = state.row_number_ids;
267
+ }
268
+ // Same rule as the path fields above: only when it differs. The
269
+ // control seeds itself from the stored spec at mount, so sending it
270
+ // unconditionally would re-read the file on every no-op save.
271
+ const chosen = state.coordinates;
272
+ const stored = (project.coordinateOptions || {}).current || {};
273
+ if (chosen && JSON.stringify(chosen) !== JSON.stringify(stored)) {
274
+ payload.coordinates = chosen;
275
+ }
276
+ // The same rule as every other field here, and it was the only one
277
+ // not following it: sending the select's value unconditionally
278
+ // turned a default into an override on every save, including saves
279
+ // that were about something else entirely. "" is a real value to
280
+ // send -- it clears an override -- so this compares rather than
281
+ // testing for truth.
282
+ if (cellLayerInput
283
+ && cellLayerInput.value !== (cellLayerInput.dataset.stored || "")) {
284
+ payload.cellLayer = cellLayerInput.value;
285
+ }
286
+ // Sent only when they differ: the server re-reads the file for
287
+ // either, and saving an unchanged form should stay free.
288
+ if (featuresInput && featuresInput.value !== project.featureSource) {
289
+ payload.features_layer = featuresInput.value;
290
+ }
291
+ if (featuresLogInput && featuresLogInput.checked !== Boolean(project.featureLog)) {
292
+ payload.features_log = featuresLogInput.checked;
293
+ }
294
+ // Saving is the user looking at these values and accepting them,
295
+ // which is the same act the requirements modal asks for -- so a
296
+ // tool opened afterwards must not ask about them again. Sent as the
297
+ // keys this page rendered, whether or not their value changed.
298
+ payload.confirm = renderedKeys();
299
+
300
+ try {
301
+ const response = await fetch(
302
+ plexoraUrl(`project/${encodeURIComponent(project.name)}`),
303
+ {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify(payload),
307
+ },
308
+ );
309
+ const result = await response.json();
310
+ if (!response.ok || !result.success) {
311
+ throw new Error(result.error || "Could not save the project");
312
+ }
313
+ task?.done();
314
+
315
+ const viewerUrl = plexoraUrl(encodeURIComponent(project.name));
316
+ if (result.segmentation_pending) {
317
+ awaitSegmentationThenOpen({
318
+ datasource: project.name,
319
+ redirectUrl: viewerUrl,
320
+ });
321
+ } else {
322
+ window.location.href = viewerUrl;
323
+ }
324
+ } catch (e) {
325
+ // The old save reported failure as HTTP 200 with no message and
326
+ // the client navigated away regardless. Say what went wrong and
327
+ // stay put.
328
+ task?.fail("Save failed");
329
+ error.textContent = e.message;
330
+ error.hidden = false;
331
+ save.disabled = false;
332
+ }
333
+ });
334
+ });
335
+ })();
@@ -0,0 +1,136 @@
1
+ /**
2
+ * quickViewLanding.js -- wires the home-page (index.html's {% else %}
3
+ * branch, shown when no datasource is loaded yet). The full path to a
4
+ * local image is always POSTed to /quick_view -- never the file's bytes,
5
+ * so huge OME-TIFFs load instantly instead of being copied over HTTP.
6
+ *
7
+ * Clicking the dropzone asks the *server* (which runs on the same machine,
8
+ * launched from a terminal or Jupyter) to pop a native OS file dialog and
9
+ * hand back the real path -- see /browse_path and
10
+ * server/utils/native_dialog.py. That only works when there's a real
11
+ * desktop session for the dialog to appear on, so the manual path input is
12
+ * a soft fallback, hidden until the browse call actually fails.
13
+ */
14
+ (function () {
15
+ const dropzone = document.getElementById("quick_view_dropzone");
16
+ const pathFallback = document.getElementById("quick_view_path_fallback");
17
+ const pathInput = document.getElementById("quick_view_path_input");
18
+ const loadButton = document.getElementById("quick_view_path_load");
19
+ const status = document.getElementById("quick_view_status");
20
+
21
+ if (!dropzone) {
22
+ return;
23
+ }
24
+
25
+ function setStatus(message, isError) {
26
+ if (!message) {
27
+ status.hidden = true;
28
+ status.textContent = "";
29
+ status.classList.remove("error");
30
+ return;
31
+ }
32
+ status.hidden = false;
33
+ status.textContent = message;
34
+ status.classList.toggle("error", !!isError);
35
+ }
36
+
37
+ function setBusy(busy) {
38
+ dropzone.style.pointerEvents = busy ? "none" : "";
39
+ loadButton.disabled = busy || !pathInput.value.trim();
40
+ }
41
+
42
+ async function submitQuickView(path) {
43
+ setBusy(true);
44
+ setStatus("Loading " + path.split(/[\\/]/).pop() + "...", false);
45
+ try {
46
+ const response = await fetch(plexoraUrl("quick_view"), {
47
+ method: "POST",
48
+ headers: {"Content-Type": "application/json"},
49
+ body: JSON.stringify({path: path}),
50
+ });
51
+ const result = await response.json();
52
+ if (!response.ok || !result.success) {
53
+ throw new Error(result.error || "Could not load that image.");
54
+ }
55
+ window.location.href = result.redirect;
56
+ } catch (error) {
57
+ setBusy(false);
58
+ setStatus(error.message || "Could not load that image.", true);
59
+ }
60
+ }
61
+
62
+ // Reveals the manual path input -- used only once the native dialog has
63
+ // failed to produce a full path on its own.
64
+ function showPathFallback(message) {
65
+ pathFallback.hidden = false;
66
+ setStatus(message || null, !!message);
67
+ pathInput.focus();
68
+ pathInput.dispatchEvent(new Event("input"));
69
+ }
70
+
71
+ async function browseForImage() {
72
+ setStatus("Opening file browser...", false);
73
+ await browseForPath({
74
+ mode: "file",
75
+ filter: "image",
76
+ onPicked: (path) => {
77
+ setStatus(null);
78
+ submitQuickView(path);
79
+ },
80
+ onUnavailable: () => {
81
+ // Soft fallback: no desktop session for a dialog to appear on
82
+ // (headless/remote server, no display, tkinter missing, ...).
83
+ showPathFallback("Automatic browsing isn't available here -- paste the full path instead.");
84
+ },
85
+ });
86
+ }
87
+
88
+ dropzone.addEventListener("click", () => browseForImage());
89
+ dropzone.addEventListener("keydown", (event) => {
90
+ if (event.key === "Enter" || event.key === " ") {
91
+ event.preventDefault();
92
+ browseForImage();
93
+ }
94
+ });
95
+
96
+ let validationRequestId = 0;
97
+ pathInput.addEventListener("input", async () => {
98
+ const path = pathInput.value.trim();
99
+ pathInput.classList.remove("is-invalid");
100
+ loadButton.disabled = true;
101
+ if (!path) {
102
+ return;
103
+ }
104
+ const requestId = ++validationRequestId;
105
+ try {
106
+ const response = await fetch(plexoraUrl("check_file_existence"), {
107
+ method: "POST",
108
+ headers: {"Content-Type": "application/json"},
109
+ body: JSON.stringify({path: path}),
110
+ });
111
+ const exists = await response.json();
112
+ if (requestId !== validationRequestId) {
113
+ return;
114
+ }
115
+ pathInput.classList.toggle("is-invalid", !exists);
116
+ loadButton.disabled = !exists;
117
+ } catch (error) {
118
+ if (requestId === validationRequestId) {
119
+ pathInput.classList.add("is-invalid");
120
+ }
121
+ }
122
+ });
123
+
124
+ pathInput.addEventListener("keydown", (event) => {
125
+ if (event.key === "Enter" && !loadButton.disabled) {
126
+ submitQuickView(pathInput.value.trim());
127
+ }
128
+ });
129
+
130
+ loadButton.addEventListener("click", () => {
131
+ const path = pathInput.value.trim();
132
+ if (path) {
133
+ submitQuickView(path);
134
+ }
135
+ });
136
+ })();