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,305 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ import threading
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import polars as pl
9
+
10
+ from plexora import paths
11
+ from plexora.server.models.project import Project
12
+
13
+
14
+ CACHE_VERSION = 1
15
+ DEFAULT_TILE_SIZE = 512
16
+ DEFAULT_MAX_POINTS_PER_TILE = 5000
17
+ INTERNAL_DTYPE = np.dtype([
18
+ ("row", "<u4"),
19
+ ("id", "<u4"),
20
+ ("x", "<f4"),
21
+ ("y", "<f4"),
22
+ ])
23
+ RESPONSE_DTYPE = np.dtype([
24
+ ("id", "<u4"),
25
+ ("x", "<f4"),
26
+ ("y", "<f4"),
27
+ ])
28
+
29
+ _cache_locks = {}
30
+ _cache_locks_guard = threading.Lock()
31
+ _filter_tables = {}
32
+
33
+
34
+ def _lock_for(datasource_name):
35
+ with _cache_locks_guard:
36
+ if datasource_name not in _cache_locks:
37
+ _cache_locks[datasource_name] = threading.RLock()
38
+ return _cache_locks[datasource_name]
39
+
40
+
41
+ def _cache_dir(datasource_name):
42
+ """This datasource's centroid tile cache.
43
+
44
+ A derived artifact, so it goes beside the project when that root can be
45
+ written to and into the user's own root when it cannot -- see
46
+ paths.derived_root. Reads go through the same function rather than
47
+ find_derived because the manifest is checked for existence anyway, and a
48
+ half-built cache in the other root is not something to adopt.
49
+ """
50
+ return paths.derived_root(datasource_name) / "centroids_v1"
51
+
52
+
53
+ def _manifest_path(datasource_name):
54
+ return _cache_dir(datasource_name) / "manifest.json"
55
+
56
+
57
+ def _project(config, datasource_name):
58
+ return Project.from_entry(datasource_name, config[datasource_name])
59
+
60
+
61
+ def _load_table(config, datasource_name):
62
+ """Load the full normalized datasource table through the same adapter
63
+ data_model.py dispatches on, rather than assuming the source file is
64
+ always a CSV -- that assumption broke entirely for non-CSV datasources
65
+ (e.g. AnnData, where the columns named by the id/X/Y roles only exist
66
+ after adapter normalization, not in the raw source file at all). Local
67
+ import avoids a circular dependency with data_model.py, which itself
68
+ imports this module.
69
+ """
70
+ from plexora.server.models.adapters import get_adapter
71
+
72
+ spec = _project(config, datasource_name).dataset
73
+ return get_adapter(spec.type)(spec).load_table().table
74
+
75
+
76
+ def _source_signature(csv_path):
77
+ stat = csv_path.stat()
78
+ return {
79
+ "csv_path": str(csv_path.resolve()),
80
+ "csv_size": stat.st_size,
81
+ "csv_mtime_ns": stat.st_mtime_ns,
82
+ }
83
+
84
+
85
+ def _expected_manifest(config, datasource_name):
86
+ project = _project(config, datasource_name)
87
+ csv_path = Path(project.dataset.src).expanduser().resolve()
88
+ tile_size = int(config[datasource_name].get("tileWidth") or DEFAULT_TILE_SIZE)
89
+ tile_size = max(1, tile_size)
90
+ width = int(config[datasource_name]["width"])
91
+ height = int(config[datasource_name]["height"])
92
+ max_level = max(1, int(config[datasource_name].get("maxLevel", 1)))
93
+ return {
94
+ "version": CACHE_VERSION,
95
+ **_source_signature(csv_path),
96
+ "id_column": project.roles.cell_id or "id",
97
+ "x_column": project.roles.x,
98
+ "y_column": project.roles.y,
99
+ "width": width,
100
+ "height": height,
101
+ "tile_size": tile_size,
102
+ "level_count": max_level,
103
+ "max_points_per_tile": DEFAULT_MAX_POINTS_PER_TILE,
104
+ "record_dtype": "row:uint32,id:uint32,x:float32,y:float32",
105
+ "response_dtype": "id:uint32,x:float32,y:float32",
106
+ }
107
+
108
+
109
+ def _is_manifest_current(manifest, expected):
110
+ keys = [
111
+ "version",
112
+ "csv_path",
113
+ "csv_size",
114
+ "csv_mtime_ns",
115
+ "id_column",
116
+ "x_column",
117
+ "y_column",
118
+ "width",
119
+ "height",
120
+ "tile_size",
121
+ "level_count",
122
+ ]
123
+ return all(manifest.get(key) == expected.get(key) for key in keys)
124
+
125
+
126
+ def get_manifest(config, datasource_name, build=True):
127
+ project = _project(config, datasource_name)
128
+ if not project.has_table or not (project.roles.x and project.roles.y):
129
+ # Nothing to place: either no feature table at all (image-only), or
130
+ # one whose coordinate columns nobody has identified yet. Centroids
131
+ # are positions, so an unresolved x/y role is as good as no table.
132
+ return {"status": "missing"}
133
+ lock = _lock_for(datasource_name)
134
+ with lock:
135
+ expected = _expected_manifest(config, datasource_name)
136
+ path = _manifest_path(datasource_name)
137
+ if path.exists():
138
+ with path.open("r", encoding="utf-8") as handle:
139
+ manifest = json.load(handle)
140
+ if _is_manifest_current(manifest, expected):
141
+ return {**manifest, "status": "ready"}
142
+
143
+ if not build:
144
+ return {**expected, "status": "missing"}
145
+
146
+ manifest = build_cache(config, datasource_name, expected)
147
+ return {**manifest, "status": "ready"}
148
+
149
+
150
+ def build_cache(config, datasource_name, expected=None):
151
+ expected = expected or _expected_manifest(config, datasource_name)
152
+ root = _cache_dir(datasource_name)
153
+ tmp_root = root.with_name(f"{root.name}.tmp.{os.getpid()}.{threading.get_ident()}")
154
+ if tmp_root.exists():
155
+ shutil.rmtree(tmp_root)
156
+ tmp_root.mkdir(parents=True, exist_ok=True)
157
+
158
+ usecols = [expected["id_column"], expected["x_column"], expected["y_column"]]
159
+ table = _load_table(config, datasource_name).select(usecols)
160
+
161
+ # Explicit null->NaN fill (rather than relying on Polars' default float
162
+ # export behavior) so invalid/unparseable values behave like pandas'
163
+ # pd.to_numeric(errors="coerce") did: NaN, not null, so the np.isfinite
164
+ # checks below keep working unchanged.
165
+ ids = table[expected["id_column"]].cast(pl.Float64, strict=False).fill_null(float("nan")).to_numpy()
166
+ xs = table[expected["x_column"]].cast(pl.Float64, strict=False).fill_null(float("nan")).to_numpy()
167
+ ys = table[expected["y_column"]].cast(pl.Float64, strict=False).fill_null(float("nan")).to_numpy()
168
+ valid = np.isfinite(ids) & np.isfinite(xs) & np.isfinite(ys)
169
+ rows = np.nonzero(valid)[0].astype(np.uint32, copy=False)
170
+ ids = ids[valid].astype(np.uint32, copy=False)
171
+ xs = xs[valid].astype(np.float32, copy=False)
172
+ ys = ys[valid].astype(np.float32, copy=False)
173
+
174
+ for level in range(expected["level_count"]):
175
+ _write_level(tmp_root, level, rows, ids, xs, ys, expected)
176
+
177
+ manifest = {
178
+ **expected,
179
+ "point_count": int(len(ids)),
180
+ }
181
+ manifest_path = tmp_root / "manifest.json"
182
+ with manifest_path.open("w", encoding="utf-8") as handle:
183
+ json.dump(manifest, handle, indent=2)
184
+
185
+ if root.exists():
186
+ shutil.rmtree(root)
187
+ try:
188
+ shutil.move(str(tmp_root), str(root))
189
+ except PermissionError:
190
+ target_manifest = root / "manifest.json"
191
+ if target_manifest.exists():
192
+ with target_manifest.open("r", encoding="utf-8") as handle:
193
+ promoted = json.load(handle)
194
+ if _is_manifest_current(promoted, expected):
195
+ _filter_tables.pop(datasource_name, None)
196
+ return promoted
197
+ raise
198
+ _filter_tables.pop(datasource_name, None)
199
+ return manifest
200
+
201
+
202
+ def _write_level(root, level, rows, ids, xs, ys, manifest):
203
+ level_dir = root / f"level_{level}"
204
+ level_dir.mkdir(parents=True, exist_ok=True)
205
+ tile_span = manifest["tile_size"] * (2 ** level)
206
+ tx = np.floor(xs / tile_span).astype(np.int64, copy=False)
207
+ ty = np.floor(ys / tile_span).astype(np.int64, copy=False)
208
+ tx = np.clip(tx, 0, max(0, int(np.ceil(manifest["width"] / tile_span)) - 1))
209
+ ty = np.clip(ty, 0, max(0, int(np.ceil(manifest["height"] / tile_span)) - 1))
210
+
211
+ order = np.lexsort((rows, tx, ty))
212
+ tx_sorted = tx[order]
213
+ ty_sorted = ty[order]
214
+ starts = np.r_[0, np.flatnonzero((tx_sorted[1:] != tx_sorted[:-1]) | (ty_sorted[1:] != ty_sorted[:-1])) + 1]
215
+ stops = np.r_[starts[1:], len(order)]
216
+
217
+ max_points = int(manifest["max_points_per_tile"])
218
+ for start, stop in zip(starts, stops):
219
+ tile_order = order[start:stop]
220
+ if level > 0 and len(tile_order) > max_points:
221
+ step = int(np.ceil(len(tile_order) / max_points))
222
+ tile_order = tile_order[::step][:max_points]
223
+ records = np.empty(len(tile_order), dtype=INTERNAL_DTYPE)
224
+ records["row"] = rows[tile_order]
225
+ records["id"] = ids[tile_order]
226
+ records["x"] = xs[tile_order]
227
+ records["y"] = ys[tile_order]
228
+ tile_x = int(tx[tile_order[0]])
229
+ tile_y = int(ty[tile_order[0]])
230
+ records.tofile(level_dir / f"tile_{tile_x}_{tile_y}.bin")
231
+
232
+
233
+ def _read_tile(datasource_name, level, tile):
234
+ path = _cache_dir(datasource_name) / f"level_{int(level)}" / f"tile_{int(tile['x'])}_{int(tile['y'])}.bin"
235
+ if not path.exists():
236
+ return np.empty(0, dtype=INTERNAL_DTYPE)
237
+ return np.fromfile(path, dtype=INTERNAL_DTYPE)
238
+
239
+
240
+ def _load_filter_table(config, datasource_name, gates):
241
+ if not gates:
242
+ return None
243
+ expected = _expected_manifest(config, datasource_name)
244
+ gate_columns = tuple(sorted(gates.keys()))
245
+ key = (
246
+ datasource_name,
247
+ expected["csv_path"],
248
+ expected["csv_size"],
249
+ expected["csv_mtime_ns"],
250
+ gate_columns,
251
+ )
252
+ cached = _filter_tables.get(key)
253
+ if cached is not None:
254
+ return cached
255
+
256
+ table = _load_table(config, datasource_name).select(list(gate_columns))
257
+ numeric = {}
258
+ for column in gate_columns:
259
+ numeric[column] = (
260
+ table[column].cast(pl.Float32, strict=False).fill_null(float("nan")).to_numpy()
261
+ )
262
+ _filter_tables.clear()
263
+ _filter_tables[key] = numeric
264
+ return numeric
265
+
266
+
267
+ def _apply_gates(records, filter_table, gates):
268
+ if records.size == 0 or not gates:
269
+ return records
270
+ rows = records["row"].astype(np.int64, copy=False)
271
+ keep = np.ones(records.shape[0], dtype=bool)
272
+ for column, values in gates.items():
273
+ if column not in filter_table:
274
+ continue
275
+ low = float(values[0])
276
+ high = float(values[1])
277
+ column_values = filter_table[column][rows]
278
+ keep &= np.isfinite(column_values) & (column_values > low) & (column_values < high)
279
+ return records[keep]
280
+
281
+
282
+ def get_tiles(config, datasource_name, level, tiles, gates=None, max_points=None):
283
+ project = _project(config, datasource_name)
284
+ if not project.has_table or not (project.roles.x and project.roles.y):
285
+ return []
286
+ get_manifest(config, datasource_name, build=True)
287
+ gates = gates or {}
288
+ arrays = [_read_tile(datasource_name, level, tile) for tile in tiles]
289
+ if not arrays:
290
+ records = np.empty(0, dtype=INTERNAL_DTYPE)
291
+ else:
292
+ records = np.concatenate(arrays) if len(arrays) > 1 else arrays[0]
293
+
294
+ filter_table = _load_filter_table(config, datasource_name, gates)
295
+ records = _apply_gates(records, filter_table, gates)
296
+
297
+ if max_points and records.shape[0] > int(max_points):
298
+ step = int(np.ceil(records.shape[0] / int(max_points)))
299
+ records = records[::step][:int(max_points)]
300
+
301
+ response = np.empty(records.shape[0], dtype=RESPONSE_DTYPE)
302
+ response["id"] = records["id"]
303
+ response["x"] = records["x"]
304
+ response["y"] = records["y"]
305
+ return response