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,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projectEdit.js -- editing an existing project.
|
|
3
|
+
*
|
|
4
|
+
* The page is generated from the project record, not from a fixed form: a
|
|
5
|
+
* section renders only when `project.has` says it applies. A project with just
|
|
6
|
+
* an image shows an image summary and two empty slots, not a wall of
|
|
7
|
+
* coordinate pickers it has no columns for; an AnnData project shows its read
|
|
8
|
+
* spec and never the CSV column classifier.
|
|
9
|
+
*
|
|
10
|
+
* The image is deliberately the one thing that cannot change — everything a
|
|
11
|
+
* project derives (channel names, the tile pyramid, the mask's geometry) is
|
|
12
|
+
* keyed to it, so swapping it is a new project rather than an edit.
|
|
13
|
+
*
|
|
14
|
+
* Saving posts only what changed. The server merges, so a field this page
|
|
15
|
+
* never renders can never be cleared by saving.
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
function ready(fn) {
|
|
19
|
+
if (document.readyState === "loading") {
|
|
20
|
+
document.addEventListener("DOMContentLoaded", fn);
|
|
21
|
+
} else {
|
|
22
|
+
fn();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ready(() => {
|
|
27
|
+
const root = document.getElementById("project-edit");
|
|
28
|
+
if (!root) return;
|
|
29
|
+
|
|
30
|
+
const project = JSON.parse(root.dataset.project);
|
|
31
|
+
const needs = root.dataset.needs ? JSON.parse(root.dataset.needs) : null;
|
|
32
|
+
const state = { roles: {}, columns: null };
|
|
33
|
+
let classifier = null;
|
|
34
|
+
let coordinates = null;
|
|
35
|
+
|
|
36
|
+
// Mirrors requirementsModal.js's option value for "only one image" --
|
|
37
|
+
// both surfaces translate it into the payload's `single_image` flag
|
|
38
|
+
// and neither ever sends it as it stands.
|
|
39
|
+
const SINGLE_IMAGE = "\u0000single-image";
|
|
40
|
+
// And the cell id's "number the rows" choice -- see requirementsModal.js
|
|
41
|
+
// for why it is an option rather than the wording on the blank.
|
|
42
|
+
const ROW_NUMBER = "\u0000row-number";
|
|
43
|
+
|
|
44
|
+
const maskInput = document.getElementById("edit_segmentation");
|
|
45
|
+
if (maskInput) {
|
|
46
|
+
// Nothing else on this page wires a Browse button: the upload
|
|
47
|
+
// page's script is what attaches [data-browse-target], and it does
|
|
48
|
+
// not load here -- so both buttons sat there doing nothing.
|
|
49
|
+
const maskBrowse = maskInput.parentElement
|
|
50
|
+
.querySelector(".browse-button");
|
|
51
|
+
if (typeof attachBrowseButton === "function") {
|
|
52
|
+
attachBrowseButton(maskBrowse, maskInput,
|
|
53
|
+
{ mode: "file", filter: "image" });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// -- Data file ------------------------------------------------------
|
|
58
|
+
// The shared control (dataSourceField.js), not a bare input: a .zarr
|
|
59
|
+
// store with several tables, or a table spanning several images, needs
|
|
60
|
+
// narrowing before it can be read at all, and swapping one in here used
|
|
61
|
+
// to post a path the server could only refuse.
|
|
62
|
+
const dataMount = document.getElementById("edit_data_field");
|
|
63
|
+
const dataField = dataMount && window.PlexoraDataSourceField.mount(dataMount, {
|
|
64
|
+
id: "edit_data",
|
|
65
|
+
value: project.data?.src || "",
|
|
66
|
+
hint: dataMount.dataset.hint,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// -- Column classifier (CSV only) ---------------------------------
|
|
70
|
+
const classifierMount = document.getElementById("edit_classifier");
|
|
71
|
+
if (classifierMount && project.has.columns) {
|
|
72
|
+
classifier = PlexoraColumnClassifier.mount(classifierMount, {
|
|
73
|
+
markers: project.columns.markers,
|
|
74
|
+
metadata: project.columns.metadata,
|
|
75
|
+
roles: project.roles,
|
|
76
|
+
roleLabels: project.roleLabels,
|
|
77
|
+
onChange: () => {
|
|
78
|
+
const value = classifier.value();
|
|
79
|
+
state.columns = { markers: value.markers, metadata: value.metadata };
|
|
80
|
+
Object.assign(state.roles, value.roles);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// -- Role selects (AnnData / SpatialData) --------------------------
|
|
86
|
+
// The classifier already owns roles for CSV, so these render only for
|
|
87
|
+
// the formats whose columns are not user-classified. Options, current
|
|
88
|
+
// answer and blank-option wording all come from the server, which is
|
|
89
|
+
// what keeps this identical to the requirements modal -- for these
|
|
90
|
+
// formats the question is about the file's own obs columns and the
|
|
91
|
+
// answer goes into the read spec, and neither surface knows that.
|
|
92
|
+
const rolesMount = document.getElementById("edit_roles");
|
|
93
|
+
if (rolesMount && !project.has.columns && project.has.data) {
|
|
94
|
+
const candidates = project.roleColumns || [];
|
|
95
|
+
|
|
96
|
+
function configField(labelText) {
|
|
97
|
+
const field = document.createElement("div");
|
|
98
|
+
field.className = "config-field";
|
|
99
|
+
const label = document.createElement("label");
|
|
100
|
+
label.textContent = labelText;
|
|
101
|
+
field.appendChild(label);
|
|
102
|
+
rolesMount.appendChild(field);
|
|
103
|
+
return field;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// x/y are not column roles for these formats -- an obsm array holds
|
|
107
|
+
// both axes, so they are one question with its own control. Shared
|
|
108
|
+
// with the requirements modal (coordinateField.js) so the two
|
|
109
|
+
// surfaces ask and post the same thing.
|
|
110
|
+
const coordinateOptions = project.coordinateOptions;
|
|
111
|
+
const asksCoordinates = !!coordinateOptions
|
|
112
|
+
&& Object.keys(coordinateOptions).length > 0;
|
|
113
|
+
const roles = (project.editableRoles || []).filter(
|
|
114
|
+
(role) => !(asksCoordinates && (role === "x" || role === "y")));
|
|
115
|
+
|
|
116
|
+
roles.forEach((role) => {
|
|
117
|
+
const field = configField(project.roleLabels[role]);
|
|
118
|
+
const select = document.createElement("select");
|
|
119
|
+
select.className = "form-select";
|
|
120
|
+
select.append(new Option("—", ""));
|
|
121
|
+
if (role === "image_id") {
|
|
122
|
+
// The other answer, and the only true one for data with no
|
|
123
|
+
// such column. See the same option in requirementsModal.js.
|
|
124
|
+
select.append(new Option("This data has only one image",
|
|
125
|
+
SINGLE_IMAGE, false, project.singleImage));
|
|
126
|
+
}
|
|
127
|
+
// Likewise for the cell id: an option, not the wording on the
|
|
128
|
+
// blank, so choosing it is something the user did.
|
|
129
|
+
const fallback = (project.roleDefaults || {})[role];
|
|
130
|
+
if (fallback) {
|
|
131
|
+
select.append(new Option(fallback, ROW_NUMBER, false,
|
|
132
|
+
project.rowNumberIds));
|
|
133
|
+
}
|
|
134
|
+
const current = (project.roleAnswers || {})[role];
|
|
135
|
+
candidates.forEach((name) => {
|
|
136
|
+
select.append(new Option(name, name, false, current === name));
|
|
137
|
+
});
|
|
138
|
+
select.addEventListener("change", () => {
|
|
139
|
+
if (role === "image_id") {
|
|
140
|
+
state.single_image = select.value === SINGLE_IMAGE;
|
|
141
|
+
state.roles[role] =
|
|
142
|
+
select.value === SINGLE_IMAGE ? null : (select.value || null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (fallback) {
|
|
146
|
+
state.row_number_ids = select.value === ROW_NUMBER;
|
|
147
|
+
state.roles[role] =
|
|
148
|
+
select.value === ROW_NUMBER ? null : (select.value || null);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
state.roles[role] = select.value || null;
|
|
152
|
+
});
|
|
153
|
+
field.appendChild(select);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (asksCoordinates) {
|
|
157
|
+
const field = configField("Cell coordinates");
|
|
158
|
+
coordinates = window.PlexoraCoordinateField.mount(field, {
|
|
159
|
+
...coordinateOptions,
|
|
160
|
+
name: "edit-coordinates",
|
|
161
|
+
onChange: (value) => { state.coordinates = value; },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// -- Cell layer ----------------------------------------------------
|
|
167
|
+
const cellLayerInput = document.getElementById("edit_cell_layer");
|
|
168
|
+
|
|
169
|
+
// -- Expression matrix ---------------------------------------------
|
|
170
|
+
const featuresInput = document.getElementById("edit_features_layer");
|
|
171
|
+
const featuresLogInput = document.getElementById("edit_features_log");
|
|
172
|
+
|
|
173
|
+
// -- Highlight what a tool is waiting for --------------------------
|
|
174
|
+
// Reached from the Tools menu without JavaScript: the same requirement
|
|
175
|
+
// list the modal would have rendered, pointed at the fields that
|
|
176
|
+
// answer it. `confirm` counts as well as `missing` -- a tool opened for
|
|
177
|
+
// the first time on a well-named table has nothing missing and still
|
|
178
|
+
// wants its guesses looked at.
|
|
179
|
+
const outstanding = [...(needs?.missing || []), ...(needs?.confirm || [])];
|
|
180
|
+
if (needs && outstanding.length) {
|
|
181
|
+
const banner = document.getElementById("edit_needs");
|
|
182
|
+
banner.hidden = false;
|
|
183
|
+
banner.textContent = needs.missing.length
|
|
184
|
+
? `${needs.label} needs: ${needs.missing.map((r) => r.label).join(", ")}.`
|
|
185
|
+
: `Check what ${needs.label} will use, then save.`;
|
|
186
|
+
const first = outstanding[0];
|
|
187
|
+
const target = first.kind === "segmentation" ? maskInput
|
|
188
|
+
: first.kind === "data" ? dataMount
|
|
189
|
+
: first.kind === "features" ? (featuresInput || featuresLogInput)
|
|
190
|
+
: classifierMount || rolesMount;
|
|
191
|
+
target?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
192
|
+
target?.classList.add("field-wanted");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// -- Save ----------------------------------------------------------
|
|
196
|
+
const save = document.getElementById("edit_save");
|
|
197
|
+
const error = document.getElementById("edit_error");
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The requirement keys this page put in front of the user, in the
|
|
201
|
+
* server's vocabulary (plexora/api/plugin.py). Derived from what was
|
|
202
|
+
* rendered rather than hardcoded, so a section that does not apply to
|
|
203
|
+
* this project never claims to have asked about it.
|
|
204
|
+
*/
|
|
205
|
+
function renderedKeys() {
|
|
206
|
+
const keys = [];
|
|
207
|
+
if (classifierMount && project.has.columns) keys.push("markers");
|
|
208
|
+
if (project.has.features) keys.push("features");
|
|
209
|
+
const roleFields = classifier ? Object.keys(project.roleLabels)
|
|
210
|
+
: (project.editableRoles || []);
|
|
211
|
+
keys.push(...roleFields.map((role) => `role:${role}`));
|
|
212
|
+
// Its own key, because for these formats it is its own question --
|
|
213
|
+
// and role:x/role:y are never asked here, so confirming them would
|
|
214
|
+
// claim the user was shown something the page did not render.
|
|
215
|
+
if (coordinates) {
|
|
216
|
+
keys.push("coordinates");
|
|
217
|
+
}
|
|
218
|
+
return keys;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
save.addEventListener("click", async () => {
|
|
222
|
+
save.disabled = true;
|
|
223
|
+
error.hidden = true;
|
|
224
|
+
|
|
225
|
+
// A data file the user has not finished describing -- a store
|
|
226
|
+
// whose table is still unpicked -- is not something to post. The
|
|
227
|
+
// server would refuse it by asking for a choice, which reads as
|
|
228
|
+
// nonsense with the control offering that choice right there.
|
|
229
|
+
const waiting = dataField?.blocking();
|
|
230
|
+
if (waiting) {
|
|
231
|
+
error.textContent = waiting;
|
|
232
|
+
error.hidden = false;
|
|
233
|
+
save.disabled = false;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const task = window.PlexoraStatus?.begin("Saving project");
|
|
238
|
+
|
|
239
|
+
const payload = {};
|
|
240
|
+
// Only send a path field when its value differs from what is
|
|
241
|
+
// stored. Sending it unchanged would be harmless but would make
|
|
242
|
+
// the server re-read the file and rebuild the mask pyramid, which
|
|
243
|
+
// is minutes of work for a no-op save.
|
|
244
|
+
if (maskInput && maskInput.value.trim() !== (project.segmentation.src || "")) {
|
|
245
|
+
payload.segmentation = maskInput.value.trim();
|
|
246
|
+
}
|
|
247
|
+
// The data file carries three companions when it changes -- which
|
|
248
|
+
// table inside the store, and which image inside the table. They
|
|
249
|
+
// go only with a changed path, since that is the only thing they
|
|
250
|
+
// describe.
|
|
251
|
+
const data = dataField?.value();
|
|
252
|
+
if (data && data.data !== (project.data?.src || "")) {
|
|
253
|
+
payload.data = data.data;
|
|
254
|
+
if (data.table) payload.table = data.table;
|
|
255
|
+
if (data.subset_column) {
|
|
256
|
+
payload.subset_column = data.subset_column;
|
|
257
|
+
payload.subset_value = data.subset_value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (Object.keys(state.roles).length) payload.roles = state.roles;
|
|
261
|
+
if (state.columns) payload.columns = state.columns;
|
|
262
|
+
if (state.single_image !== undefined) {
|
|
263
|
+
payload.single_image = state.single_image;
|
|
264
|
+
}
|
|
265
|
+
if (state.row_number_ids !== undefined) {
|
|
266
|
+
payload.row_number_ids = state.row_number_ids;
|
|
267
|
+
}
|
|
268
|
+
// Same rule as the path fields above: only when it differs. The
|
|
269
|
+
// control seeds itself from the stored spec at mount, so sending it
|
|
270
|
+
// unconditionally would re-read the file on every no-op save.
|
|
271
|
+
const chosen = state.coordinates;
|
|
272
|
+
const stored = (project.coordinateOptions || {}).current || {};
|
|
273
|
+
if (chosen && JSON.stringify(chosen) !== JSON.stringify(stored)) {
|
|
274
|
+
payload.coordinates = chosen;
|
|
275
|
+
}
|
|
276
|
+
// The same rule as every other field here, and it was the only one
|
|
277
|
+
// not following it: sending the select's value unconditionally
|
|
278
|
+
// turned a default into an override on every save, including saves
|
|
279
|
+
// that were about something else entirely. "" is a real value to
|
|
280
|
+
// send -- it clears an override -- so this compares rather than
|
|
281
|
+
// testing for truth.
|
|
282
|
+
if (cellLayerInput
|
|
283
|
+
&& cellLayerInput.value !== (cellLayerInput.dataset.stored || "")) {
|
|
284
|
+
payload.cellLayer = cellLayerInput.value;
|
|
285
|
+
}
|
|
286
|
+
// Sent only when they differ: the server re-reads the file for
|
|
287
|
+
// either, and saving an unchanged form should stay free.
|
|
288
|
+
if (featuresInput && featuresInput.value !== project.featureSource) {
|
|
289
|
+
payload.features_layer = featuresInput.value;
|
|
290
|
+
}
|
|
291
|
+
if (featuresLogInput && featuresLogInput.checked !== Boolean(project.featureLog)) {
|
|
292
|
+
payload.features_log = featuresLogInput.checked;
|
|
293
|
+
}
|
|
294
|
+
// Saving is the user looking at these values and accepting them,
|
|
295
|
+
// which is the same act the requirements modal asks for -- so a
|
|
296
|
+
// tool opened afterwards must not ask about them again. Sent as the
|
|
297
|
+
// keys this page rendered, whether or not their value changed.
|
|
298
|
+
payload.confirm = renderedKeys();
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(
|
|
302
|
+
plexoraUrl(`project/${encodeURIComponent(project.name)}`),
|
|
303
|
+
{
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "Content-Type": "application/json" },
|
|
306
|
+
body: JSON.stringify(payload),
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
const result = await response.json();
|
|
310
|
+
if (!response.ok || !result.success) {
|
|
311
|
+
throw new Error(result.error || "Could not save the project");
|
|
312
|
+
}
|
|
313
|
+
task?.done();
|
|
314
|
+
|
|
315
|
+
const viewerUrl = plexoraUrl(encodeURIComponent(project.name));
|
|
316
|
+
if (result.segmentation_pending) {
|
|
317
|
+
awaitSegmentationThenOpen({
|
|
318
|
+
datasource: project.name,
|
|
319
|
+
redirectUrl: viewerUrl,
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
window.location.href = viewerUrl;
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
// The old save reported failure as HTTP 200 with no message and
|
|
326
|
+
// the client navigated away regardless. Say what went wrong and
|
|
327
|
+
// stay put.
|
|
328
|
+
task?.fail("Save failed");
|
|
329
|
+
error.textContent = e.message;
|
|
330
|
+
error.hidden = false;
|
|
331
|
+
save.disabled = false;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
})();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quickViewLanding.js -- wires the home-page (index.html's {% else %}
|
|
3
|
+
* branch, shown when no datasource is loaded yet). The full path to a
|
|
4
|
+
* local image is always POSTed to /quick_view -- never the file's bytes,
|
|
5
|
+
* so huge OME-TIFFs load instantly instead of being copied over HTTP.
|
|
6
|
+
*
|
|
7
|
+
* Clicking the dropzone asks the *server* (which runs on the same machine,
|
|
8
|
+
* launched from a terminal or Jupyter) to pop a native OS file dialog and
|
|
9
|
+
* hand back the real path -- see /browse_path and
|
|
10
|
+
* server/utils/native_dialog.py. That only works when there's a real
|
|
11
|
+
* desktop session for the dialog to appear on, so the manual path input is
|
|
12
|
+
* a soft fallback, hidden until the browse call actually fails.
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
const dropzone = document.getElementById("quick_view_dropzone");
|
|
16
|
+
const pathFallback = document.getElementById("quick_view_path_fallback");
|
|
17
|
+
const pathInput = document.getElementById("quick_view_path_input");
|
|
18
|
+
const loadButton = document.getElementById("quick_view_path_load");
|
|
19
|
+
const status = document.getElementById("quick_view_status");
|
|
20
|
+
|
|
21
|
+
if (!dropzone) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setStatus(message, isError) {
|
|
26
|
+
if (!message) {
|
|
27
|
+
status.hidden = true;
|
|
28
|
+
status.textContent = "";
|
|
29
|
+
status.classList.remove("error");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
status.hidden = false;
|
|
33
|
+
status.textContent = message;
|
|
34
|
+
status.classList.toggle("error", !!isError);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setBusy(busy) {
|
|
38
|
+
dropzone.style.pointerEvents = busy ? "none" : "";
|
|
39
|
+
loadButton.disabled = busy || !pathInput.value.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function submitQuickView(path) {
|
|
43
|
+
setBusy(true);
|
|
44
|
+
setStatus("Loading " + path.split(/[\\/]/).pop() + "...", false);
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(plexoraUrl("quick_view"), {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {"Content-Type": "application/json"},
|
|
49
|
+
body: JSON.stringify({path: path}),
|
|
50
|
+
});
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
if (!response.ok || !result.success) {
|
|
53
|
+
throw new Error(result.error || "Could not load that image.");
|
|
54
|
+
}
|
|
55
|
+
window.location.href = result.redirect;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
setBusy(false);
|
|
58
|
+
setStatus(error.message || "Could not load that image.", true);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Reveals the manual path input -- used only once the native dialog has
|
|
63
|
+
// failed to produce a full path on its own.
|
|
64
|
+
function showPathFallback(message) {
|
|
65
|
+
pathFallback.hidden = false;
|
|
66
|
+
setStatus(message || null, !!message);
|
|
67
|
+
pathInput.focus();
|
|
68
|
+
pathInput.dispatchEvent(new Event("input"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function browseForImage() {
|
|
72
|
+
setStatus("Opening file browser...", false);
|
|
73
|
+
await browseForPath({
|
|
74
|
+
mode: "file",
|
|
75
|
+
filter: "image",
|
|
76
|
+
onPicked: (path) => {
|
|
77
|
+
setStatus(null);
|
|
78
|
+
submitQuickView(path);
|
|
79
|
+
},
|
|
80
|
+
onUnavailable: () => {
|
|
81
|
+
// Soft fallback: no desktop session for a dialog to appear on
|
|
82
|
+
// (headless/remote server, no display, tkinter missing, ...).
|
|
83
|
+
showPathFallback("Automatic browsing isn't available here -- paste the full path instead.");
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dropzone.addEventListener("click", () => browseForImage());
|
|
89
|
+
dropzone.addEventListener("keydown", (event) => {
|
|
90
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
browseForImage();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let validationRequestId = 0;
|
|
97
|
+
pathInput.addEventListener("input", async () => {
|
|
98
|
+
const path = pathInput.value.trim();
|
|
99
|
+
pathInput.classList.remove("is-invalid");
|
|
100
|
+
loadButton.disabled = true;
|
|
101
|
+
if (!path) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const requestId = ++validationRequestId;
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(plexoraUrl("check_file_existence"), {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {"Content-Type": "application/json"},
|
|
109
|
+
body: JSON.stringify({path: path}),
|
|
110
|
+
});
|
|
111
|
+
const exists = await response.json();
|
|
112
|
+
if (requestId !== validationRequestId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
pathInput.classList.toggle("is-invalid", !exists);
|
|
116
|
+
loadButton.disabled = !exists;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (requestId === validationRequestId) {
|
|
119
|
+
pathInput.classList.add("is-invalid");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
pathInput.addEventListener("keydown", (event) => {
|
|
125
|
+
if (event.key === "Enter" && !loadButton.disabled) {
|
|
126
|
+
submitQuickView(pathInput.value.trim());
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
loadButton.addEventListener("click", () => {
|
|
131
|
+
const path = pathInput.value.trim();
|
|
132
|
+
if (path) {
|
|
133
|
+
submitQuickView(path);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
})();
|