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,369 @@
1
+ /**
2
+ * FigureCaptureBoxes - where the captures came from, still on the image.
3
+ *
4
+ * A capture used to leave no mark. The shutter fired, a thumbnail appeared in
5
+ * the strip, and the region it came from was gone -- so building a row of
6
+ * comparable panels meant remembering four places on a slide by eye. These are
7
+ * those four places, drawn as thin outlines that stay on the image for the whole
8
+ * session.
9
+ *
10
+ * ## Held in IMAGE pixels, which is the opposite of the viewfinder
11
+ *
12
+ * figureCaptureTool's frame is held in SCREEN pixels on purpose: it is a
13
+ * viewfinder, and panning the image under it is how you aim it. A capture box is
14
+ * the other thing entirely -- it is a record of a region OF THE IMAGE, so it has
15
+ * to travel with the image, growing as you zoom in and sliding as you pan. The
16
+ * two live in the same corner of the same screen and mean opposite things, and
17
+ * getting either one backwards produces something that looks fine until the
18
+ * moment it matters.
19
+ *
20
+ * Both are reprojected through the SAME tool instance rather than through a
21
+ * second copy of the arithmetic, so a frame re-aimed at a box (which is what
22
+ * selecting a capture does) lands exactly on it. Two copies would agree until
23
+ * the day one of them was fixed.
24
+ *
25
+ * ## The interior is not a target
26
+ *
27
+ * A box can cover most of the viewer. If the whole rectangle were clickable, a
28
+ * session's captures would gradually cover the image in dead zones where panning
29
+ * and every other tool's drag stopped working. So the element itself is
30
+ * `pointer-events: none` and only four thin bands along its edges -- plus the
31
+ * label, once it is showing -- take clicks. Everything inside a capture box
32
+ * behaves exactly like the image, because it is the image.
33
+ *
34
+ * ## Selecting one does not restore what it looked like
35
+ *
36
+ * Centring on a box puts the viewer back over that FIELD and touches nothing
37
+ * about the rendering: not a channel, not a window, not an overlay. That is
38
+ * deliberate and it is the feature -- go back to a region, change the channels,
39
+ * and capture it again to get a second panel of the same field under a different
40
+ * rendering, in pixel-level concordance with the first. Restoring the captured
41
+ * scene here would make that impossible to do by accident and hard to do on
42
+ * purpose. Reopening a panel's full scene is a different action, lives on the
43
+ * canvas, and says so (see the sidebar controller's editPanel).
44
+ */
45
+ class FigureCaptureBoxes {
46
+
47
+ /** How thick the clickable band along each edge is, in CSS pixels. Wider
48
+ * than the 1px outline it straddles, because a 1px hit target is a target
49
+ * nobody hits. */
50
+ static get EDGE() { return 9; }
51
+
52
+ /**
53
+ * How much viewer a region gets when you go back to it, as a multiple of
54
+ * the region itself. Two, so the capture fills about half the window.
55
+ *
56
+ * Filling the window edge to edge answers "what was in this capture" and
57
+ * nothing else: where it sits, what surrounds it, and whether the next one
58
+ * should be a little to the left are all off screen, and arriving that
59
+ * close is a lurch out of whatever the user was looking at. Half leaves a
60
+ * margin of context on every side, and it leaves the capture's own outline
61
+ * visible -- which is what tells the user they have arrived somewhere they
62
+ * have already been.
63
+ */
64
+ static get CONTEXT() { return 2; }
65
+
66
+ /**
67
+ * How long the viewer has to be QUIET before it counts as having arrived,
68
+ * and the longest the arrival will wait for a viewer that never is.
69
+ *
70
+ * `animation-finish` is not the end of the movement. OpenSeadragon raises
71
+ * it when the springs reach their target and then pulls the viewport back
72
+ * inside its constraints, which is a second, smaller movement afterwards.
73
+ * A capture near an edge of the slide provokes it, and so does any capture
74
+ * whose context rectangle does not share the viewer's aspect ratio -- which
75
+ * is most of them. See centerOn().
76
+ */
77
+ static get SETTLE_MS() { return 120; }
78
+ static get SETTLE_LIMIT_MS() { return 900; }
79
+
80
+ /** The viewer events that mean the picture is still moving. `resize` counts:
81
+ * the canvas opening beside the viewer moves the region on screen just as
82
+ * surely as a pan does. */
83
+ static get MOTION_EVENTS() { return ["animation", "animation-finish", "resize"]; }
84
+
85
+ /**
86
+ * The region, grown about its own centre.
87
+ *
88
+ * Pure and separate so the framing can be checked without a viewer. About
89
+ * the CENTRE rather than the top-left, or going back to a capture would put
90
+ * it in the corner of the window instead of the middle of it.
91
+ */
92
+ static contextRect(rect, grow) {
93
+ const factor = grow || FigureCaptureBoxes.CONTEXT;
94
+ const w = rect.w * factor;
95
+ const h = rect.h * factor;
96
+ return {
97
+ x: rect.x + (rect.w - w) / 2,
98
+ y: rect.y + (rect.h - h) / 2,
99
+ w: w,
100
+ h: h,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Where a captured region lands on screen, or null when it lands nowhere.
106
+ *
107
+ * Pure, and separate from the drawing, because "is this box on screen at
108
+ * all" is the question asked most often -- once per box per animation frame
109
+ * -- and the answer for a user zoomed into one corner of a slide is usually
110
+ * no. Boxes that are nowhere are not drawn at all rather than drawn far
111
+ * outside the viewer: #openseadragon_wrapper does not clip, so an undrawn
112
+ * box is the only kind that cannot end up over the sidebar.
113
+ */
114
+ static placement(screenRect, bounds) {
115
+ if (!screenRect || !bounds || !(bounds.width > 0) || !(bounds.height > 0)) return null;
116
+ const width = Math.max(1, screenRect.width);
117
+ const height = Math.max(1, screenRect.height);
118
+ const left = screenRect.x;
119
+ const top = screenRect.y;
120
+ if (left + width <= 0 || top + height <= 0) return null;
121
+ if (left >= bounds.width || top >= bounds.height) return null;
122
+ return {
123
+ left: Math.round(left),
124
+ top: Math.round(top),
125
+ width: Math.round(width),
126
+ height: Math.round(height),
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @param {object} ctx the plugin context.
132
+ * @param {object} options tool (the FigureCaptureTool whose coordinate
133
+ * frame these share) and onSelect(captureId).
134
+ */
135
+ constructor(ctx, options) {
136
+ this.ctx = ctx;
137
+ this.viewer = ctx.viewer?.viewer || null;
138
+ this.tool = (options && options.tool) || null;
139
+ this.onSelect = (options && options.onSelect) || (() => {});
140
+
141
+ //: [{id, rect}] with rect in FULL-RESOLUTION image pixels.
142
+ this.boxes = [];
143
+ this.selected = null;
144
+ this.root = null;
145
+ this._nodes = new Map();
146
+
147
+ this._onViewport = () => this.paint();
148
+ this._handlers = [];
149
+ }
150
+
151
+ get mounted() {
152
+ return Boolean(this.root);
153
+ }
154
+
155
+ // -- lifecycle -------------------------------------------------------
156
+
157
+ /** A sibling of #openseadragon inside #openseadragon_wrapper -- miniMap's
158
+ * placement, and the same reason: OSD binds its mouse tracker to
159
+ * #openseadragon, so a click on an edge band here never reaches the viewer
160
+ * underneath and nothing needs stopPropagation. */
161
+ mount() {
162
+ const host = document.getElementById("openseadragon_wrapper");
163
+ if (this.root || !host || !this.viewer) return false;
164
+
165
+ const root = document.createElement("div");
166
+ root.className = "fb-boxes";
167
+ // Delegated, because the nodes are rebuilt whenever the capture list
168
+ // changes and per-node handlers would be rebound on every shutter.
169
+ // Events from the edge bands still bubble here: pointer-events on an
170
+ // ancestor decides whether IT is hit, not whether its children's
171
+ // events pass through it.
172
+ root.addEventListener("click", (event) => {
173
+ const id = event.target.closest?.("[data-capture-id]")?.dataset.captureId;
174
+ if (id) this.onSelect(id);
175
+ });
176
+ root.addEventListener("pointerover", (event) => this.hover(event, true));
177
+ root.addEventListener("pointerout", (event) => this.hover(event, false));
178
+ host.appendChild(root);
179
+ this.root = root;
180
+
181
+ const on = (name, fn) => {
182
+ this.viewer.addHandler(name, fn);
183
+ this._handlers.push([name, fn]);
184
+ };
185
+ // The trio miniMap.js uses: every frame of a pan or zoom, the frame it
186
+ // settles on, and a viewer that changed size -- which is what opening
187
+ // the figure canvas beside it does.
188
+ on("animation", this._onViewport);
189
+ on("animation-finish", this._onViewport);
190
+ on("resize", this._onViewport);
191
+ on("open", this._onViewport);
192
+ window.addEventListener("resize", this._onViewport);
193
+
194
+ this.paint();
195
+ return true;
196
+ }
197
+
198
+ unmount() {
199
+ for (const [name, fn] of this._handlers) this.viewer?.removeHandler?.(name, fn);
200
+ this._handlers = [];
201
+ window.removeEventListener("resize", this._onViewport);
202
+ this.root?.remove();
203
+ this.root = null;
204
+ this._nodes = new Map();
205
+ }
206
+
207
+ destroy() {
208
+ this.unmount();
209
+ this.boxes = [];
210
+ this.selected = null;
211
+ }
212
+
213
+ hover(event, on) {
214
+ const box = event.target.closest?.(".fb-box");
215
+ if (!box) return;
216
+ // pointerout fires when the pointer moves between two edge bands of the
217
+ // SAME box, which happens at every corner -- and toggling the highlight
218
+ // off and straight back on is a flicker on a 120ms transition. Where
219
+ // the pointer went is relatedTarget.
220
+ if (!on && box.contains?.(event.relatedTarget)) return;
221
+ box.classList.toggle("is-hover", on);
222
+ }
223
+
224
+ // -- what there is to draw -------------------------------------------
225
+
226
+ /** @param {Array} boxes [{id, rect}], rect in full-resolution image px. */
227
+ setBoxes(boxes) {
228
+ this.boxes = (boxes || []).filter((box) => box && box.id && box.rect);
229
+ this.paint();
230
+ }
231
+
232
+ setSelected(id) {
233
+ this.selected = id || null;
234
+ this.paint();
235
+ }
236
+
237
+ // -- drawing ---------------------------------------------------------
238
+
239
+ node(id) {
240
+ let node = this._nodes.get(id);
241
+ if (node && node.isConnected !== false) return node;
242
+ node = document.createElement("div");
243
+ node.className = "fb-box";
244
+ node.dataset.captureId = id;
245
+ node.innerHTML = ["n", "e", "s", "w"].map((side) =>
246
+ `<span class="fb-box-edge fb-box-${side}"></span>`).join("")
247
+ + '<span class="fb-box-tag"></span>';
248
+ this.root.appendChild(node);
249
+ this._nodes.set(id, node);
250
+ return node;
251
+ }
252
+
253
+ /**
254
+ * Put every box where the image is now.
255
+ *
256
+ * Called once per animation frame during a pan, so it does the cheap thing:
257
+ * the nodes are reused and only their geometry is written. A session has
258
+ * tens of captures, not thousands.
259
+ */
260
+ paint() {
261
+ if (!this.root || !this.tool) return;
262
+ const bounds = this.tool.bounds();
263
+ const offset = this.tool.offset();
264
+
265
+ // The container is the viewer's own drawing area, and it clips: a box
266
+ // whose left edge is on screen and whose right edge is a mile off it
267
+ // would otherwise be a div a mile wide, laid over whatever is beside
268
+ // the viewer.
269
+ this.root.style.left = offset.x + "px";
270
+ this.root.style.top = offset.y + "px";
271
+ this.root.style.width = bounds.width + "px";
272
+ this.root.style.height = bounds.height + "px";
273
+
274
+ const live = new Set();
275
+ this.boxes.forEach((box, index) => {
276
+ const placement = FigureCaptureBoxes.placement(
277
+ this.tool.toScreenRect(box.rect), bounds);
278
+ if (!placement) return;
279
+ live.add(box.id);
280
+ const node = this.node(box.id);
281
+ node.style.left = placement.left + "px";
282
+ node.style.top = placement.top + "px";
283
+ node.style.width = placement.width + "px";
284
+ node.style.height = placement.height + "px";
285
+ node.classList.toggle("is-selected", box.id === this.selected);
286
+ const tag = node.querySelector(".fb-box-tag");
287
+ // Newest first in the strip, so the numbers read the same way in
288
+ // both places -- a box labelled 1 and a thumbnail labelled 1 that
289
+ // are different captures is worse than no numbers at all.
290
+ if (tag) tag.textContent = String(index + 1);
291
+ });
292
+
293
+ // Anything not placed this pass is off screen or gone from the strip.
294
+ for (const [id, node] of Array.from(this._nodes.entries())) {
295
+ if (live.has(id)) continue;
296
+ node.remove();
297
+ this._nodes.delete(id);
298
+ }
299
+ }
300
+
301
+ // -- going back to one -----------------------------------------------
302
+
303
+ /**
304
+ * Put the viewer back over a captured region.
305
+ *
306
+ * Over it with room around it -- CONTEXT times its own size -- rather than
307
+ * filling the window with it. The rendering is not touched; see the class
308
+ * comment.
309
+ *
310
+ * `done` is called once the viewer has STOPPED, which is not the same as
311
+ * once it says it has finished. The caller uses it to land the viewfinder
312
+ * on this exact region and to lock the shutter onto it, and that lock is
313
+ * what the selection is: taking it a few pixels off the region means the
314
+ * next movement -- OpenSeadragon's own constraint correction, arriving
315
+ * after `animation-finish` -- breaks it immediately, and the capture the
316
+ * user just clicked goes unselected before they can see it was selected.
317
+ *
318
+ * So this waits for quiet rather than for an event: every `animation`
319
+ * restarts a short timer, and the arrival is the timer surviving. The first
320
+ * wait is started here rather than by an event, because a fitBounds onto
321
+ * where the viewer already is raises no animation at all; and a viewer that
322
+ * never goes quiet gives up at SETTLE_LIMIT_MS rather than stranding the
323
+ * selection forever.
324
+ */
325
+ centerOn(rect, done) {
326
+ const item = this.viewer?.world?.getItemAt?.(0);
327
+ const viewport = this.viewer?.viewport;
328
+ if (!item || !viewport || !rect) {
329
+ done?.();
330
+ return false;
331
+ }
332
+ const scale = 2 ** (this.ctx.config?.extraZoomLevels || 0);
333
+ const framed = FigureCaptureBoxes.contextRect(rect);
334
+ viewport.fitBounds(item.imageToViewportRectangle(new OpenSeadragon.Rect(
335
+ framed.x * scale, framed.y * scale, framed.w * scale, framed.h * scale)));
336
+
337
+ if (done) this.whenStill(done);
338
+ return true;
339
+ }
340
+
341
+ /** Call `done` once the viewer has been still for SETTLE_MS. */
342
+ whenStill(done) {
343
+ let settled = false;
344
+ let quiet = null;
345
+ let limit = null;
346
+
347
+ const settle = () => {
348
+ if (settled) return;
349
+ settled = true;
350
+ clearTimeout(quiet);
351
+ clearTimeout(limit);
352
+ for (const name of FigureCaptureBoxes.MOTION_EVENTS) {
353
+ this.viewer.removeHandler?.(name, restart);
354
+ }
355
+ done();
356
+ };
357
+ const restart = () => {
358
+ if (settled) return;
359
+ clearTimeout(quiet);
360
+ quiet = setTimeout(settle, FigureCaptureBoxes.SETTLE_MS);
361
+ };
362
+
363
+ for (const name of FigureCaptureBoxes.MOTION_EVENTS) {
364
+ this.viewer.addHandler?.(name, restart);
365
+ }
366
+ restart();
367
+ limit = setTimeout(settle, FigureCaptureBoxes.SETTLE_LIMIT_MS);
368
+ }
369
+ }