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,466 @@
1
+ /**
2
+ * FigureCaptureDock - the whole of Figure Builder's in-viewer chrome.
3
+ *
4
+ * There is no sidebar panel any more. Figure Builder started with one because
5
+ * every plugin has one, and it was the wrong home for all of it: capturing is
6
+ * something you do WHILE looking at the image, and a panel three hundred pixels
7
+ * to the left is a glance away from the thing being judged. Splitting the
8
+ * controls between the two -- capture on the image, "which figure?" in the
9
+ * sidebar -- was worse than either, because the two halves of one decision were
10
+ * in two places. So everything is here: the mode toggle, the instructions, the
11
+ * session's captures, which figure they go into, the way through to the canvas,
12
+ * the borrowed-viewer session that reopening a panel starts, and the way out.
13
+ *
14
+ * Mounted as a sibling of #openseadragon inside #openseadragon_wrapper -- the
15
+ * placement miniMap.js documents: OSD binds its mouse tracker to
16
+ * #openseadragon, so clicks on this chrome never reach the viewer and nothing
17
+ * here needs stopPropagation. Top-right because core's own furniture is
18
+ * elsewhere: project label top-left, overview lens bottom-left, channel legend
19
+ * bottom-right. The legend is the one that matters -- it is in the same column
20
+ * as this, so the dock measures the room above it and caps itself there rather
21
+ * than growing down through it as the strip fills up. See roomFor().
22
+ *
23
+ * Everything here is built in JavaScript rather than rendered into a panel slot
24
+ * for one reason: core has no slot over the image, and adding one to
25
+ * index.html for a single plugin would put a plugin-shaped hole in a template
26
+ * every build renders. What a plugin creates it may also style, so the classes
27
+ * are all `fb-` and tests/test_plugin_css_boundary.py stays satisfied.
28
+ */
29
+ class FigureCaptureDock {
30
+
31
+ /** The one-key shortcut. C for capture; nothing in core or in the other
32
+ * plugins binds a bare letter, which is checked in the probe rather than
33
+ * assumed. */
34
+ static get SHORTCUT() { return "c"; }
35
+
36
+ /** Whether the strip is open. A property of this browser and this person's
37
+ * habits, like the current figure -- not of the figure or the project. */
38
+ static get STORAGE_KEY() { return "plexora.figure_builder.stripOpen"; }
39
+
40
+ /** The dock's inset from the top of the viewer, matching `top` in the
41
+ * stylesheet; and the clearance it keeps from whatever is below it. */
42
+ static get MARGIN() { return 12; }
43
+ static get GAP() { return 10; }
44
+
45
+ /**
46
+ * The least the dock may be squeezed to, in pixels.
47
+ *
48
+ * On a short window under a tall channel legend there is not enough room
49
+ * for both, and something has to give. It is this: about the orb, the
50
+ * strip's header and a glimpse of the list. A dock squeezed below that is
51
+ * one the user cannot capture with at all, which is worse than a dock that
52
+ * overlaps a legend in the one case where nothing else fits.
53
+ */
54
+ static get MIN_HEIGHT() { return 168; }
55
+
56
+ /** Core's channel legend: bottom-right of the same wrapper the dock hangs
57
+ * in, and the only thing the dock can collide with. Read for its geometry
58
+ * and never touched -- it is core's element, and this is a plugin. */
59
+ static get OBSTACLE_ID() { return "viewer_channel_legend"; }
60
+
61
+ /**
62
+ * How tall the dock may be.
63
+ *
64
+ * The strip grows with every capture, and left to itself it grew straight
65
+ * down into the channel legend -- which is `pointer-events: none`, so the
66
+ * captures underneath it stayed clickable and the two just illegibly
67
+ * overlapped. Rather than a fixed cap (wrong on every window that is not
68
+ * the one it was chosen on), the dock takes the room that is actually free
69
+ * above whatever occupies the corner below it.
70
+ *
71
+ * Pure so the arithmetic can be checked without a browser.
72
+ *
73
+ * @param {number} hostHeight the viewer wrapper's height.
74
+ * @param {?number} obstacleTop the top of the thing below, in the same
75
+ * coordinates, or null when there is nothing there.
76
+ */
77
+ static roomFor(hostHeight, obstacleTop) {
78
+ const margin = FigureCaptureDock.MARGIN;
79
+ const floor = (obstacleTop === null || obstacleTop === undefined)
80
+ ? hostHeight - margin
81
+ : obstacleTop - FigureCaptureDock.GAP;
82
+ return Math.max(FigureCaptureDock.MIN_HEIGHT, Math.round(floor - margin));
83
+ }
84
+
85
+ /**
86
+ * Is this keystroke the capture shortcut?
87
+ *
88
+ * A bare letter, so the guards are the whole of it: no modifier chord (Cmd-C
89
+ * is copy and always will be), and nothing while the user is typing --
90
+ * otherwise naming a figure "cell cores" toggles capture mode four times.
91
+ */
92
+ static isShortcut(event) {
93
+ if (!event || event.metaKey || event.ctrlKey || event.altKey) return false;
94
+ if (typeof event.key !== "string") return false;
95
+ if (event.key.toLowerCase() !== FigureCaptureDock.SHORTCUT) return false;
96
+ return !FigureCaptureTool.isTyping();
97
+ }
98
+
99
+ /**
100
+ * @param {object} handlers onToggleCapture, onSelectCapture(id),
101
+ * onRemoveCapture(id), onNewFigure, onChooseFigure, onOpenCanvas,
102
+ * onUpdatePanel, onCancelEdit, onClose.
103
+ */
104
+ constructor(handlers) {
105
+ this.handlers = handlers || {};
106
+ this.root = null;
107
+ this.open = this.readOpen();
108
+ this._onKeyDown = (event) => {
109
+ if (!FigureCaptureDock.isShortcut(event)) return;
110
+ event.preventDefault();
111
+ this.handlers.onToggleCapture?.();
112
+ };
113
+ this._onResize = () => this.layout();
114
+ //: Watches the legend, which changes height whenever a channel is
115
+ //: turned on or off -- a plugin cannot hear core's channel events, and
116
+ //: the box changing is the thing that actually matters here.
117
+ this._watch = null;
118
+ }
119
+
120
+ // -- lifecycle -------------------------------------------------------
121
+
122
+ mount() {
123
+ const host = this.host();
124
+ if (this.root || !host) return false;
125
+
126
+ const root = document.createElement("div");
127
+ root.className = "fb-dock";
128
+ root.innerHTML = `
129
+ <div class="fb-dock-head">
130
+ <div class="fb-dock-caption" data-role="caption"></div>
131
+ <button class="fb-orb" type="button" data-role="orb"
132
+ aria-pressed="false" title="Capture mode (C)">
133
+ <span class="fas fa-crop-simple" aria-hidden="true"></span>
134
+ <span class="fb-visually-hidden">Capture mode</span>
135
+ </button>
136
+ <button class="fb-dock-close" type="button" data-role="close"
137
+ title="Close Figure Builder">
138
+ <span class="fas fa-xmark" aria-hidden="true"></span>
139
+ <span class="fb-visually-hidden">Close Figure Builder</span>
140
+ </button>
141
+ </div>
142
+
143
+ <p class="fb-dock-error" data-role="error" hidden></p>
144
+
145
+ <section class="fb-strip" data-role="strip">
146
+ <button class="fb-strip-toggle" type="button" data-role="collapse"
147
+ aria-expanded="true">
148
+ <span class="fas fa-chevron-down fb-strip-chevron" aria-hidden="true"></span>
149
+ <span class="fb-strip-title">Captures</span>
150
+ <span class="fb-strip-count" data-role="count">0</span>
151
+ </button>
152
+ <div class="fb-strip-body">
153
+ <div class="fb-strip-items" data-role="items"></div>
154
+ <p class="fb-strip-empty" data-role="empty">Nothing captured yet.</p>
155
+ <div class="fb-strip-foot">
156
+ <span class="fb-strip-label">Capture into</span>
157
+ <div class="fb-strip-figure-row">
158
+ <button class="fb-strip-figure" type="button" data-role="choose"
159
+ title="Open a figure, or start one"></button>
160
+ <button class="fb-strip-add" type="button" data-role="new"
161
+ title="New figure">
162
+ <span class="fas fa-plus" aria-hidden="true"></span>
163
+ <span class="fb-visually-hidden">New figure</span>
164
+ </button>
165
+ </div>
166
+ <span class="fb-strip-meta" data-role="meta"></span>
167
+ <button class="fb-strip-go" type="button" data-role="canvas"></button>
168
+ </div>
169
+ </div>
170
+ </section>
171
+
172
+ <section class="fb-dock-edit" data-role="edit" hidden>
173
+ <div class="fb-dock-edit-head">
174
+ <span class="fas fa-arrow-rotate-left" aria-hidden="true"></span>
175
+ <span>Editing <strong data-role="editLabel"></strong></span>
176
+ </div>
177
+ <p class="fb-dock-edit-body">The viewer is showing this panel's captured
178
+ view, and the outline on the image is where its edges are. Adjust the
179
+ channels, contrast and overlays as usual, drag the outline to reframe,
180
+ then update the panel &mdash; or cancel to put the viewer back. Your
181
+ project's own saved channels are not touched either way.</p>
182
+ <div data-role="editNotes"></div>
183
+ <div class="fb-dock-edit-actions">
184
+ <button class="fb-strip-go" type="button" data-role="editUpdate">Update panel</button>
185
+ <button class="fb-strip-figure" type="button" data-role="editCancel">Cancel</button>
186
+ </div>
187
+ </section>`;
188
+
189
+ // Delegated: the items are rebuilt on every capture, so handlers bound
190
+ // to them would be rebound continuously and leak the ones replaced.
191
+ root.addEventListener("click", (event) => this.clicked(event));
192
+ host.appendChild(root);
193
+ this.root = root;
194
+ this.applyOpen();
195
+ this.layout();
196
+
197
+ // On the document, not on the dock: a shortcut that only worked while
198
+ // the pointer was over a 150px strip would be a shortcut nobody found.
199
+ document.addEventListener("keydown", this._onKeyDown);
200
+ window.addEventListener("resize", this._onResize);
201
+ this.watchObstacle();
202
+ return true;
203
+ }
204
+
205
+ unmount() {
206
+ document.removeEventListener("keydown", this._onKeyDown);
207
+ window.removeEventListener("resize", this._onResize);
208
+ this._watch?.disconnect();
209
+ this._watch = null;
210
+ this.root?.remove();
211
+ this.root = null;
212
+ }
213
+
214
+ destroy() {
215
+ this.unmount();
216
+ }
217
+
218
+ get mounted() {
219
+ return Boolean(this.root);
220
+ }
221
+
222
+ // -- how much room there is ------------------------------------------
223
+
224
+ /** The wrapper the dock and the legend both hang in. */
225
+ host() {
226
+ return document.getElementById("openseadragon_wrapper");
227
+ }
228
+
229
+ obstacle() {
230
+ return document.getElementById(FigureCaptureDock.OBSTACLE_ID);
231
+ }
232
+
233
+ /**
234
+ * Give the dock the height that is actually free above the legend.
235
+ *
236
+ * A max-height rather than a height: the dock is still only as tall as what
237
+ * is in it, and this is the ceiling the strip's scroll starts at.
238
+ */
239
+ layout() {
240
+ const host = this.host();
241
+ if (!this.root || !host?.getBoundingClientRect) return;
242
+ const bounds = host.getBoundingClientRect();
243
+
244
+ const legend = this.obstacle();
245
+ const box = legend?.getBoundingClientRect?.();
246
+ // A legend with no height is one core has emptied or hidden, and a hidden
247
+ // obstacle is not one: measuring it anyway would cap the dock against a
248
+ // rectangle nobody can see.
249
+ const top = box && box.height ? box.top - bounds.top : null;
250
+
251
+ this.root.style.maxHeight = FigureCaptureDock.roomFor(bounds.height, top) + "px";
252
+ }
253
+
254
+ /**
255
+ * Re-measure when the legend's box changes.
256
+ *
257
+ * Through a ResizeObserver rather than by listening to core: the legend is
258
+ * redrawn on channel changes and on colour changes, and binding to those
259
+ * events would be a plugin reaching into core's event vocabulary for a fact
260
+ * it can observe directly. Guarded, because the probes' synthetic window
261
+ * does not have one and neither does every browser this may meet.
262
+ */
263
+ watchObstacle() {
264
+ const legend = this.obstacle();
265
+ if (this._watch || !legend || typeof ResizeObserver !== "function") return;
266
+ this._watch = new ResizeObserver(() => this.layout());
267
+ this._watch.observe(legend);
268
+ }
269
+
270
+ el(role) {
271
+ return this.root?.querySelector(`[data-role="${role}"]`) || null;
272
+ }
273
+
274
+ // -- events ----------------------------------------------------------
275
+
276
+ clicked(event) {
277
+ const target = event.target.closest("[data-role]");
278
+ const role = target?.dataset.role;
279
+ const id = () => event.target.closest("[data-capture-id]")?.dataset.captureId;
280
+ if (role === "orb") {
281
+ this.handlers.onToggleCapture?.();
282
+ } else if (role === "close") {
283
+ this.handlers.onClose?.();
284
+ } else if (role === "collapse") {
285
+ this.setOpen(!this.open);
286
+ } else if (role === "remove") {
287
+ // Nested inside the item, so `closest` finds this one first --
288
+ // which is what stops discarding a capture from also selecting it
289
+ // and flying the viewer to a region that is about to be gone.
290
+ const captureId = id();
291
+ if (captureId) this.handlers.onRemoveCapture?.(captureId);
292
+ } else if (role === "select") {
293
+ const captureId = id();
294
+ if (captureId) this.handlers.onSelectCapture?.(captureId);
295
+ } else if (role === "new") {
296
+ this.handlers.onNewFigure?.();
297
+ } else if (role === "choose") {
298
+ this.handlers.onChooseFigure?.();
299
+ } else if (role === "canvas") {
300
+ this.handlers.onOpenCanvas?.();
301
+ } else if (role === "editUpdate") {
302
+ this.handlers.onUpdatePanel?.();
303
+ } else if (role === "editCancel") {
304
+ this.handlers.onCancelEdit?.();
305
+ }
306
+ }
307
+
308
+ readOpen() {
309
+ try {
310
+ return window.localStorage.getItem(FigureCaptureDock.STORAGE_KEY) !== "0";
311
+ } catch (error) {
312
+ // Private-browsing modes throw rather than answering. Defaulting to
313
+ // open is the recoverable direction: a strip nobody wanted is one
314
+ // click away from gone, a strip nobody can find is not.
315
+ return true;
316
+ }
317
+ }
318
+
319
+ setOpen(open) {
320
+ this.open = Boolean(open);
321
+ try {
322
+ window.localStorage.setItem(FigureCaptureDock.STORAGE_KEY, this.open ? "1" : "0");
323
+ } catch (error) {
324
+ /* see readOpen */
325
+ }
326
+ this.applyOpen();
327
+ }
328
+
329
+ applyOpen() {
330
+ if (!this.root) return;
331
+ this.root.classList.toggle("is-collapsed", !this.open);
332
+ this.el("collapse")?.setAttribute("aria-expanded", String(this.open));
333
+ }
334
+
335
+ // -- rendering -------------------------------------------------------
336
+
337
+ /**
338
+ * @param {object} state armed, figureTitle (null when no figure
339
+ * is open), meta, error, editing ({label, notes} or null), selected
340
+ * and captures: [{id, url, caption, pending}].
341
+ */
342
+ render(state) {
343
+ if (!this.root) return;
344
+ const armed = Boolean(state.armed);
345
+ const editing = state.editing || null;
346
+
347
+ this.root.classList.toggle("is-armed", armed);
348
+ // The capture half stands down while the viewer is showing a panel's
349
+ // borrowed scene: a capture taken then would be a panel of somebody
350
+ // else's view, and nothing on screen would say so.
351
+ this.root.classList.toggle("is-editing", Boolean(editing));
352
+
353
+ const orb = this.el("orb");
354
+ if (orb) {
355
+ orb.setAttribute("aria-pressed", String(armed));
356
+ orb.setAttribute("title", armed ? "Leave capture mode (C)" : "Capture mode (C)");
357
+ }
358
+
359
+ // The instructions the sidebar used to carry, kept because they are the
360
+ // only place the modifiers are written down -- but only while the mode
361
+ // they describe is on, and never as a block that covers the image.
362
+ const caption = this.el("caption");
363
+ if (caption) {
364
+ // Both letters are read off the classes that own them rather than
365
+ // written out here, so a key that moves cannot leave the only
366
+ // written record of it pointing at the old one.
367
+ const shoot = FigureCaptureTool.SHOOT_KEY.toUpperCase();
368
+ const mode = FigureCaptureDock.SHORTCUT.toUpperCase();
369
+ caption.innerHTML = armed
370
+ ? 'Drag the frame to move it, drag the image to redraw it. '
371
+ + '<kbd>Shift</kbd> square · <kbd>Space</kbd> pan · '
372
+ + `<kbd>${shoot}</kbd> capture · <kbd>Esc</kbd> exit`
373
+ : `Capture <kbd>${mode}</kbd>`;
374
+ }
375
+
376
+ const error = this.el("error");
377
+ if (error) {
378
+ error.textContent = state.error || "";
379
+ error.hidden = !state.error;
380
+ }
381
+
382
+ const strip = this.el("strip");
383
+ if (strip) strip.hidden = Boolean(editing);
384
+ const edit = this.el("edit");
385
+ if (edit) edit.hidden = !editing;
386
+ if (editing) this.renderEdit(editing);
387
+
388
+ const captures = state.captures || [];
389
+ const count = this.el("count");
390
+ if (count) count.textContent = String(captures.length);
391
+ const empty = this.el("empty");
392
+ if (empty) empty.hidden = captures.length > 0;
393
+
394
+ const items = this.el("items");
395
+ if (items) {
396
+ items.innerHTML = captures.map((capture, index) => {
397
+ const escape = FigureSchema.escapeHtml;
398
+ const classes = ["fb-strip-item"];
399
+ if (capture.pending) classes.push("is-pending");
400
+ if (capture.id === state.selected) classes.push("is-selected");
401
+ const title = escape(capture.caption || "")
402
+ + (capture.pending ? " — not in a figure yet" : "");
403
+ const image = capture.url
404
+ ? `<img src="${escape(capture.url)}" alt="" draggable="false">`
405
+ : '<span class="fb-strip-pending"></span>';
406
+ // The whole item selects; the button inside it discards. The
407
+ // number matches the label on the box out on the image.
408
+ return `<figure class="${classes.join(" ")}" data-role="select"
409
+ data-capture-id="${escape(capture.id)}"
410
+ title="${title} — click to go back and aim the shutter at it again">
411
+ ${image}
412
+ <span class="fb-strip-index">${index + 1}</span>
413
+ <button class="fb-strip-remove" type="button" data-role="remove"
414
+ title="Discard this capture">
415
+ <span class="fas fa-xmark" aria-hidden="true"></span>
416
+ <span class="fb-visually-hidden">Discard</span>
417
+ </button>
418
+ </figure>`;
419
+ }).join("");
420
+ }
421
+
422
+ // Never a placeholder that reads like a name: "Untitled figure" is what
423
+ // an unnamed figure is called, and showing it when there is no figure at
424
+ // all would tell the user their captures are somewhere they are not.
425
+ const choose = this.el("choose");
426
+ if (choose) {
427
+ choose.textContent = state.figureTitle || "Not chosen yet";
428
+ choose.classList.toggle("is-empty", !state.figureTitle);
429
+ }
430
+
431
+ const meta = this.el("meta");
432
+ if (meta) {
433
+ meta.textContent = state.meta || "";
434
+ meta.hidden = !state.meta;
435
+ }
436
+
437
+ // One label, because there is now one thing it does: the canvas is a
438
+ // page of its own and this leaves the viewer for it. It used to toggle a
439
+ // pane beside the image and say "Hide canvas" on the way back, which
440
+ // made the same button mean two things depending on a state nothing
441
+ // else on screen showed. The arrow says the page is going to change.
442
+ const go = this.el("canvas");
443
+ if (go) {
444
+ go.innerHTML = 'Figure Canvas '
445
+ + '<span class="fas fa-arrow-up-right-from-square" aria-hidden="true"></span>';
446
+ go.title = "Open the Figure Canvas — leaves the viewer";
447
+ }
448
+
449
+ // The strip has just changed height, and the legend may have changed
450
+ // too since the last look -- on a browser without a ResizeObserver this
451
+ // is the only thing that would notice.
452
+ this.watchObstacle();
453
+ this.layout();
454
+ }
455
+
456
+ renderEdit(editing) {
457
+ const label = this.el("editLabel");
458
+ if (label) label.textContent = editing.label || "this panel";
459
+ const notes = this.el("editNotes");
460
+ if (notes) {
461
+ notes.innerHTML = (editing.notes || [])
462
+ .map((note) => `<p class="fb-dock-edit-note">${FigureSchema.escapeHtml(note)}</p>`)
463
+ .join("");
464
+ }
465
+ }
466
+ }