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.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- 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
|