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,126 @@
1
+ #
2
+ # Smallest enclosing circle - Library (Python)
3
+ #
4
+ # Copyright (c) 2020 Project Nayuki
5
+ # https://www.nayuki.io/page/smallest-enclosing-circle
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this program (see COPYING.txt and COPYING.LESSER.txt).
19
+ # If not, see <http://www.gnu.org/licenses/>.
20
+ #
21
+
22
+ import math, random
23
+
24
+
25
+ # Data conventions: A point is a pair of floats (x, y). A circle is a triple of floats (center x, center y, radius).
26
+
27
+ # Returns the smallest circle that encloses all the given points. Runs in expected O(n) time, randomized.
28
+ # Input: A sequence of pairs of floats or ints, e.g. [(0,5), (3.1,-2.7)].
29
+ # Output: A triple of floats representing a circle.
30
+ # Note: If 0 points are given, None is returned. If 1 point is given, a circle of radius 0 is returned.
31
+ #
32
+ # Initially: No boundary points known
33
+ def make_circle(points):
34
+ # Convert to float and randomize order
35
+ shuffled = [(float(x), float(y)) for (x, y) in points]
36
+ random.shuffle(shuffled)
37
+
38
+ # Progressively add points to circle or recompute circle
39
+ c = None
40
+ for (i, p) in enumerate(shuffled):
41
+ if c is None or not is_in_circle(c, p):
42
+ c = _make_circle_one_point(shuffled[ : i + 1], p)
43
+ return c
44
+
45
+
46
+ # One boundary point known
47
+ def _make_circle_one_point(points, p):
48
+ c = (p[0], p[1], 0.0)
49
+ for (i, q) in enumerate(points):
50
+ if not is_in_circle(c, q):
51
+ if c[2] == 0.0:
52
+ c = make_diameter(p, q)
53
+ else:
54
+ c = _make_circle_two_points(points[ : i + 1], p, q)
55
+ return c
56
+
57
+
58
+ # Two boundary points known
59
+ def _make_circle_two_points(points, p, q):
60
+ circ = make_diameter(p, q)
61
+ left = None
62
+ right = None
63
+ px, py = p
64
+ qx, qy = q
65
+
66
+ # For each point not in the two-point circle
67
+ for r in points:
68
+ if is_in_circle(circ, r):
69
+ continue
70
+
71
+ # Form a circumcircle and classify it on left or right side
72
+ cross = _cross_product(px, py, qx, qy, r[0], r[1])
73
+ c = make_circumcircle(p, q, r)
74
+ if c is None:
75
+ continue
76
+ elif cross > 0.0 and (left is None or _cross_product(px, py, qx, qy, c[0], c[1]) > _cross_product(px, py, qx, qy, left[0], left[1])):
77
+ left = c
78
+ elif cross < 0.0 and (right is None or _cross_product(px, py, qx, qy, c[0], c[1]) < _cross_product(px, py, qx, qy, right[0], right[1])):
79
+ right = c
80
+
81
+ # Select which circle to return
82
+ if left is None and right is None:
83
+ return circ
84
+ elif left is None:
85
+ return right
86
+ elif right is None:
87
+ return left
88
+ else:
89
+ return left if (left[2] <= right[2]) else right
90
+
91
+
92
+ def make_diameter(a, b):
93
+ cx = (a[0] + b[0]) / 2
94
+ cy = (a[1] + b[1]) / 2
95
+ r0 = math.hypot(cx - a[0], cy - a[1])
96
+ r1 = math.hypot(cx - b[0], cy - b[1])
97
+ return (cx, cy, max(r0, r1))
98
+
99
+
100
+ def make_circumcircle(a, b, c):
101
+ # Mathematical algorithm from Wikipedia: Circumscribed circle
102
+ ox = (min(a[0], b[0], c[0]) + max(a[0], b[0], c[0])) / 2
103
+ oy = (min(a[1], b[1], c[1]) + max(a[1], b[1], c[1])) / 2
104
+ ax = a[0] - ox; ay = a[1] - oy
105
+ bx = b[0] - ox; by = b[1] - oy
106
+ cx = c[0] - ox; cy = c[1] - oy
107
+ d = (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) * 2.0
108
+ if d == 0.0:
109
+ return None
110
+ x = ox + ((ax*ax + ay*ay) * (by - cy) + (bx*bx + by*by) * (cy - ay) + (cx*cx + cy*cy) * (ay - by)) / d
111
+ y = oy + ((ax*ax + ay*ay) * (cx - bx) + (bx*bx + by*by) * (ax - cx) + (cx*cx + cy*cy) * (bx - ax)) / d
112
+ ra = math.hypot(x - a[0], y - a[1])
113
+ rb = math.hypot(x - b[0], y - b[1])
114
+ rc = math.hypot(x - c[0], y - c[1])
115
+ return (x, y, max(ra, rb, rc))
116
+
117
+
118
+ _MULTIPLICATIVE_EPSILON = 1 + 1e-14
119
+
120
+ def is_in_circle(c, p):
121
+ return c is not None and math.hypot(p[0] - c[0], p[1] - c[1]) <= c[2] * _MULTIPLICATIVE_EPSILON
122
+
123
+
124
+ # Returns twice the signed area of the triangle defined by (x0, y0), (x1, y1), (x2, y2).
125
+ def _cross_product(x0, y0, x1, y1, x2, y2):
126
+ return (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0)
@@ -0,0 +1,372 @@
1
+ import collections
2
+ import dataclasses
3
+ import fractions
4
+ import io
5
+ import os
6
+ import re
7
+ import reprlib
8
+ import struct
9
+ from typing import List, Any
10
+ import warnings
11
+
12
+
13
+ datatype_formats = {
14
+ 1: "B", # BYTE
15
+ 2: "s", # ASCII
16
+ 3: "H", # SHORT
17
+ 4: "I", # LONG
18
+ 5: "I", # RATIONAL (pairs)
19
+ 6: "b", # SBYTE
20
+ 7: "B", # UNDEFINED
21
+ 8: "h", # SSHORT
22
+ 9: "i", # SLONG
23
+ 10: "i", # SRATIONAL (pairs)
24
+ 11: "f", # FLOAT
25
+ 12: "d", # DOUBLE
26
+ 13: "I", # IFD
27
+ 16: "Q", # LONG8
28
+ 17: "q", # SLONG8
29
+ 18: "Q", # IFD8
30
+ }
31
+ rational_datatypes = {5, 10}
32
+
33
+
34
+ class TiffSurgeon:
35
+ """Read, manipulate and write IFDs in BigTIFF files."""
36
+
37
+ def __init__(self, path, *, writeable=False, encoding=None):
38
+ self.path = path
39
+ self.writeable = writeable
40
+ self.encoding = encoding
41
+ self.endian = ""
42
+ self.ifds = None
43
+ self.file = open(self.path, "r+b" if self.writeable else "rb")
44
+ self._validate()
45
+
46
+ def _validate(self):
47
+ signature = self.read("2s")
48
+ signature = signature.decode("ascii", errors="ignore")
49
+ if signature == "II":
50
+ self.endian = "<"
51
+ elif signature == "MM":
52
+ self.endian = ">"
53
+ else:
54
+ raise FormatError(f"Not a TIFF file (signature is '{signature}').")
55
+ self.version = self.read("H")
56
+ if self.version == 42:
57
+ self.offset_size = 4
58
+ self.offset_format = "I"
59
+ self.ntags_size = 2
60
+ self.ntags_format = "H"
61
+ self.tag_size = 12
62
+ self.first_ifd_offset_offset = 4
63
+ self.first_ifd_offset = self.read("I")
64
+ elif self.version == 43:
65
+ offset_size, reserved, first_ifd_offset = self.read("H H Q")
66
+ if offset_size != 8 or reserved != 0:
67
+ raise FormatError(
68
+ f"Malformed BigTIFF: offset_size={offset_size}"
69
+ f" reserved={reserved}"
70
+ )
71
+ self.offset_size = 8
72
+ self.offset_format = "Q"
73
+ self.ntags_size = 8
74
+ self.ntags_format = "Q"
75
+ self.tag_size = 20
76
+ self.first_ifd_offset_offset = 8
77
+ self.first_ifd_offset = first_ifd_offset
78
+ else:
79
+ raise FormatError(f"Unknown TIFF version: {self.version}")
80
+
81
+ def read(self, fmt, *, file=None):
82
+ if file is None:
83
+ file = self.file
84
+ endian = self.endian or "="
85
+ size = struct.calcsize(endian + fmt)
86
+ raw = file.read(size)
87
+ value = self.unpack(fmt, raw)
88
+ return value
89
+
90
+ def write(self, fmt, *values):
91
+ if not self.writeable:
92
+ raise ValueError("File is opened as read-only.")
93
+ raw = self.pack(fmt, *values)
94
+ self.file.write(raw)
95
+
96
+ def unpack(self, fmt, raw):
97
+ assert self.endian or re.match(r"\d+s", fmt), \
98
+ "can't unpack non-string before endianness is detected"
99
+ fmt = self.endian + fmt
100
+ size = struct.calcsize(fmt)
101
+ values = struct.unpack(fmt, raw[:size])
102
+ if len(values) == 1:
103
+ return values[0]
104
+ else:
105
+ return values
106
+
107
+ def pack(self, fmt, *values):
108
+ assert self.endian, "can't pack without endian set"
109
+ fmt = self.endian + fmt
110
+ raw = struct.pack(fmt, *values)
111
+ return raw
112
+
113
+ @property
114
+ def tag_format(self):
115
+ return f"H H {self.offset_format} {self.offset_size}s"
116
+
117
+ def read_ifds(self):
118
+ ifds = [self.read_ifd(self.first_ifd_offset)]
119
+ while ifds[-1].offset_next:
120
+ ifds.append(self.read_ifd(ifds[-1].offset_next))
121
+ self.ifds = ifds
122
+
123
+ def read_ifd(self, offset):
124
+ self.file.seek(offset)
125
+ num_tags = self.read(self.ntags_format)
126
+ buf = io.BytesIO(self.file.read(num_tags * self.tag_size))
127
+ offset_next = self.read(self.offset_format)
128
+ offset_range = range(offset, self.file.tell())
129
+ try:
130
+ tags = TagSet([self.read_tag(buf) for i in range(num_tags)])
131
+ except FormatError as e:
132
+ raise FormatError(f"IFD at offset {offset}, {e}") from None
133
+ ifd = Ifd(tags, offset, offset_next, offset_range)
134
+ return ifd
135
+
136
+ def read_tag(self, buf):
137
+ tag = Tag(*self.read(self.tag_format, file=buf))
138
+ value, offset_range = self.tag_value(tag)
139
+ tag = dataclasses.replace(tag, value=value, offset_range=offset_range)
140
+ return tag
141
+
142
+ def append_ifd_sequence(self, ifds):
143
+ """Write list of IFDs as a chained sequence at the end of the file.
144
+
145
+ Returns a list of new Ifd objects with updated offsets.
146
+
147
+ """
148
+ self.file.seek(0, os.SEEK_END)
149
+ new_ifds = []
150
+ for ifd in ifds:
151
+ offset = self.file.tell()
152
+ self.write(self.ntags_format, len(ifd.tags))
153
+ for tag in ifd.tags:
154
+ self.write_tag(tag)
155
+ offset_next = (
156
+ self.file.tell() + self.offset_size if ifd is not ifds[-1] else 0
157
+ )
158
+ self.write(self.offset_format, offset_next)
159
+ offset_range = range(offset, self.file.tell())
160
+ new_ifd = dataclasses.replace(
161
+ ifd,
162
+ offset=offset,
163
+ offset_next=offset_next,
164
+ offset_range=offset_range,
165
+ )
166
+ new_ifds.append(new_ifd)
167
+ return new_ifds
168
+
169
+ def append_tag_data(self, code, datatype, value):
170
+ """Build new tag and write data to the end of the file if necessary.
171
+
172
+ Returns a Tag object corresponding to the passed parameters. This
173
+ function only writes any "overflow" data and not the IFD entry itself,
174
+ so the returned Tag must still be written to an IFD.
175
+
176
+ If the value is small enough to fit in the data field within an IFD, no
177
+ data will actually be written to the file and the returned Tag object
178
+ will have the value encoded in its data attribute. Otherwise the data
179
+ will be appended to the file and the returned Tag's data attribute will
180
+ encode the corresponding offset.
181
+
182
+ """
183
+ fmt = datatype_formats[datatype]
184
+ # FIXME Should we perform our own check that values match datatype?
185
+ # struct.pack will do it but the exception won't be as understandable.
186
+ original_value = value
187
+ if isinstance(value, str):
188
+ if not self.encoding:
189
+ raise ValueError(
190
+ "ASCII tag values must be bytes if encoding is not set"
191
+ )
192
+ value = [value.encode(self.encoding) + b"\x00"]
193
+ count = len(value[0])
194
+ elif isinstance(value, bytes):
195
+ value = [value + b"\x00"]
196
+ count = len(value[0])
197
+ else:
198
+ try:
199
+ len(value)
200
+ except TypeError:
201
+ value = [value]
202
+ count = len(value)
203
+ struct_count = count
204
+ if datatype in rational_datatypes:
205
+ value = [i for v in value for i in v.as_integer_ratio()]
206
+ count //= 2
207
+ byte_count = struct_count * struct.calcsize(fmt)
208
+ if byte_count <= self.offset_size:
209
+ data = self.pack(str(struct_count) + fmt, *value)
210
+ data += bytes(self.offset_size - byte_count)
211
+ else:
212
+ self.file.seek(0, os.SEEK_END)
213
+ data = self.pack(self.offset_format, self.file.tell())
214
+ self.write(str(count) + fmt, *value)
215
+ # TODO Compute and set offset_range.
216
+ tag = Tag(code, datatype, count, data, original_value)
217
+ return tag
218
+
219
+ def write_first_ifd_offset(self, offset):
220
+ self.file.seek(self.first_ifd_offset_offset)
221
+ self.write(self.offset_format, offset)
222
+
223
+ def write_tag(self, tag):
224
+ self.write(self.tag_format, tag.code, tag.datatype, tag.count, tag.data)
225
+
226
+ def tag_value(self, tag):
227
+ """Return decoded tag data and the file offset range."""
228
+ fmt = datatype_formats[tag.datatype]
229
+ count = tag.count
230
+ if tag.datatype in rational_datatypes:
231
+ count *= 2
232
+ byte_count = count * struct.calcsize(fmt)
233
+ if byte_count <= self.offset_size:
234
+ value = self.unpack(str(count) + fmt, tag.data)
235
+ offset_range = range(0, 0)
236
+ else:
237
+ offset = self.unpack(self.offset_format, tag.data)
238
+ self.file.seek(offset)
239
+ value = self.read(str(count) + fmt)
240
+ offset_range = range(offset, offset + byte_count)
241
+ if tag.datatype == 2:
242
+ value = value.rstrip(b"\x00")
243
+ if self.encoding:
244
+ try:
245
+ value = value.decode(self.encoding)
246
+ except UnicodeDecodeError as e:
247
+ raise FormatError(f"tag {tag.code}: {e}") from None
248
+ elif tag.datatype in rational_datatypes:
249
+ value = [
250
+ fractions.Fraction(*v) for v in zip(value[::2], value[1::2])
251
+ ]
252
+ if len(value) == 1:
253
+ value = value[0]
254
+ return value, offset_range
255
+
256
+ def close(self):
257
+ self.file.close()
258
+
259
+
260
+ @dataclasses.dataclass(frozen=True)
261
+ class Tag:
262
+ code: int
263
+ datatype: int
264
+ count: int
265
+ data: bytes
266
+ value: Any = None
267
+ offset_range: range = None
268
+
269
+ _vrepr = reprlib.Repr()
270
+ _vrepr.maxstring = 60
271
+ _vrepr.maxother = 60
272
+ vrepr = _vrepr.repr
273
+
274
+ def __repr__(self):
275
+ return (
276
+ self.__class__.__qualname__ + "("
277
+ + f"code={self.code!r}, datatype={self.datatype!r}, "
278
+ + f"count={self.count!r}, data={self.data!r}, "
279
+ + f"value={self.vrepr(self.value)}"
280
+ + ")"
281
+ )
282
+
283
+ @dataclasses.dataclass(frozen=True)
284
+ class TagSet:
285
+ """Container for Tag objects as stored in a TIFF IFD.
286
+
287
+ Tag objects are maintained in a list that's always sorted in ascending order
288
+ by the tag code. Only one tag for a given code may be present, which is where
289
+ the "set" name comes from.
290
+
291
+ """
292
+
293
+ tags: List[Tag] = dataclasses.field(default_factory=list)
294
+
295
+ def __post_init__(self):
296
+ code_counter = collections.Counter(self.codes)
297
+ dups = [item for item, count in code_counter.items() if count > 1]
298
+ if dups:
299
+ warnings.warn(f"Duplicate tags: {dups}")
300
+
301
+ def __repr__(self):
302
+ ret = type(self).__name__ + "(["
303
+ if self.tags:
304
+ ret += "\n"
305
+ ret += "".join([f" {t},\n" for t in self.tags])
306
+ ret += "])"
307
+ return ret
308
+
309
+ @property
310
+ def codes(self):
311
+ return [t.code for t in self.tags]
312
+
313
+ def __getitem__(self, code):
314
+ for t in self.tags:
315
+ if code == t.code:
316
+ return t
317
+ else:
318
+ raise KeyError(code)
319
+
320
+ def __delitem__(self, code):
321
+ try:
322
+ i = self.codes.index(code)
323
+ except ValueError:
324
+ raise KeyError(code) from None
325
+ self.tags[:] = self.tags[:i] + self.tags[i+1:]
326
+
327
+ def __contains__(self, code):
328
+ return code in self.codes
329
+
330
+ def __len__(self):
331
+ return len(self.tags)
332
+
333
+ def __iter__(self):
334
+ return iter(self.tags)
335
+
336
+ def get(self, code, default=None):
337
+ try:
338
+ return self[code]
339
+ except KeyError:
340
+ return default
341
+
342
+ def get_value(self, code, default=None):
343
+ tag = self.get(code)
344
+ if tag:
345
+ return tag.value
346
+ else:
347
+ return default
348
+
349
+ def insert(self, tag):
350
+ """Add a new tag or replace an existing one."""
351
+ for i, t in enumerate(self.tags):
352
+ if tag.code == t.code:
353
+ self.tags[i] = tag
354
+ return
355
+ elif tag.code < t.code:
356
+ break
357
+ else:
358
+ i = len(self.tags)
359
+ n = len(self.tags)
360
+ self.tags[i:n+1] = [tag] + self.tags[i:n]
361
+
362
+
363
+ @dataclasses.dataclass(frozen=True)
364
+ class Ifd:
365
+ tags: TagSet
366
+ offset: int
367
+ offset_next: int
368
+ offset_range: range = dataclasses.field(repr=False)
369
+
370
+
371
+ class FormatError(Exception):
372
+ pass
plexora/server_cli.py ADDED
@@ -0,0 +1,48 @@
1
+ import argparse
2
+ import os
3
+
4
+
5
+ def main(argv=None):
6
+ parser = argparse.ArgumentParser(description="Run Plexora as a notebook-friendly sidecar server.")
7
+ parser.add_argument("--host", default="127.0.0.1")
8
+ parser.add_argument("--port", default="8000")
9
+ parser.add_argument("--data-dir", default=None)
10
+ parser.add_argument("--base-url", default=None)
11
+ parser.add_argument("--notebook-mode", action="store_true")
12
+ parser.add_argument("--plugins", default=None,
13
+ help="Comma-separated plugins to activate. Omit for all installed; pass an empty string for a core-only build.")
14
+ args = parser.parse_args(argv)
15
+
16
+ if args.data_dir:
17
+ os.environ["PLEXORA_DATA_PATH"] = args.data_dir
18
+ if args.base_url is not None:
19
+ os.environ["PLEXORA_BASE_URL"] = args.base_url
20
+ if args.notebook_mode:
21
+ os.environ["PLEXORA_NOTEBOOK_MODE"] = "1"
22
+ if args.plugins is not None:
23
+ os.environ["PLEXORA_PLUGINS"] = args.plugins
24
+
25
+ from waitress import serve
26
+ from plexora import app, _clean_base_url
27
+
28
+ app.config["PLEXORA_NOTEBOOK_MODE"] = args.notebook_mode or app.config.get("PLEXORA_NOTEBOOK_MODE", False)
29
+ if args.base_url is not None:
30
+ app.config["PLEXORA_BASE_URL"] = _clean_base_url(args.base_url)
31
+ # Plugin registration (Blueprint mounting) already happened inside
32
+ # create_app() at the `from plexora import app` line above, keyed off the
33
+ # PLEXORA_PLUGINS env var set above -- unlike PLEXORA_BASE_URL and
34
+ # PLEXORA_NOTEBOOK_MODE, there is no post-import app.config override that
35
+ # could retroactively register a Blueprint.
36
+ print(f"Serving Plexora on {args.host}:{args.port}")
37
+ serve(
38
+ app,
39
+ host=args.host,
40
+ port=int(args.port),
41
+ max_request_body_size=1073741824000000,
42
+ max_request_header_size=85899345920000,
43
+ threads=8,
44
+ )
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()
plexora/test.py ADDED
File without changes