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,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class ViewerControls - wires the core, module-independent sidebar controls:
|
|
3
|
+
* how cells are drawn, and HD tiles. These render whenever the viewer has
|
|
4
|
+
* channels (see index.html's image_kind != 'rgb' gate) regardless of which tool,
|
|
5
|
+
* if any, is currently open -- so their listeners live here instead of inside a
|
|
6
|
+
* specific tool module (previously csvGatingList.js, which meant they silently
|
|
7
|
+
* did nothing unless Thresholding happened to be the active tool on this page
|
|
8
|
+
* view).
|
|
9
|
+
*
|
|
10
|
+
* **Cells is one choice, not several switches.** None / Centroids / Outlines /
|
|
11
|
+
* Filled, exactly one active. This replaced two independent checkboxes, which
|
|
12
|
+
* could express states the renderer had to arbitrate ("Outlines" and
|
|
13
|
+
* "Centroids" both on) and could not express one that matters (Filled). The
|
|
14
|
+
* boundary it draws is the important part: geometry is core's, so a plugin that
|
|
15
|
+
* colours cells supplies a colour per cell id and gets all three
|
|
16
|
+
* representations, rather than shipping a centroid renderer, an outline
|
|
17
|
+
* renderer and a mask renderer of its own.
|
|
18
|
+
*
|
|
19
|
+
* Nothing is drawn over the image on load. A cell layer costs a manifest fetch,
|
|
20
|
+
* a mask pyramid read and a full repaint, and a user who opened a project to
|
|
21
|
+
* look at the image wanted the image. It is turned on by `enableCellLayer()`
|
|
22
|
+
* when a plugin that colours cells activates, using the layer the project
|
|
23
|
+
* recorded (server/models/project.py's CELL_LAYERS).
|
|
24
|
+
*
|
|
25
|
+
* This used to default Outlines on whenever a mask existed, and fall back to
|
|
26
|
+
* centroids when it did not. That produced the worst version of both: a project
|
|
27
|
+
* whose mask pyramid was still being built in the background reported no
|
|
28
|
+
* segmentation yet, so it silently opened on centroids -- for a slide the user
|
|
29
|
+
* had just supplied a mask for -- and the mask only appeared minutes later, if
|
|
30
|
+
* they thought to toggle it.
|
|
31
|
+
*/
|
|
32
|
+
class ViewerControls {
|
|
33
|
+
|
|
34
|
+
//: Every representation the control offers, in the order it shows them.
|
|
35
|
+
static MODES = ["none", "centroids", "outlines", "filled"];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @constructor
|
|
39
|
+
* @param seaDragonViewer - the ImageViewer instance
|
|
40
|
+
* @param config - the configuration file (json)
|
|
41
|
+
* @param eventHandler - the event handler for distributing interface and data updates
|
|
42
|
+
*/
|
|
43
|
+
constructor(seaDragonViewer, config, eventHandler) {
|
|
44
|
+
this.seaDragonViewer = seaDragonViewer;
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.eventHandler = eventHandler;
|
|
47
|
+
this.mode = "none";
|
|
48
|
+
this.control = null;
|
|
49
|
+
this.buttons = new Map();
|
|
50
|
+
//: The point-size row, shown only while centroids are the drawing. It
|
|
51
|
+
//: is meaningless for the other three, and a control that is present
|
|
52
|
+
//: but inert reads as broken rather than as not applicable.
|
|
53
|
+
this.pointSizeRow = null;
|
|
54
|
+
//: The per-layer opacity row, shown only while a plugin layer is active
|
|
55
|
+
//: -- there is nothing to fade a plain white cell layer against.
|
|
56
|
+
this.opacityRow = null;
|
|
57
|
+
this.opacitySlider = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @function currentFilter - ranges the active layer's plugin wants drawn, or
|
|
62
|
+
* {} when there is no active layer (a plain viewer, or one whose tools are
|
|
63
|
+
* all closed). Core asks the viewer who is active rather than reading a
|
|
64
|
+
* named plugin off window, so this works for any plugin.
|
|
65
|
+
*/
|
|
66
|
+
currentFilter() {
|
|
67
|
+
return window.__plexora?.seaDragonViewer?.cellLayer?.getColorCodedRanges?.() || {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @function activeLayer - the cell layer these controls are editing, or null
|
|
72
|
+
* for a viewer with no plugin layers.
|
|
73
|
+
*
|
|
74
|
+
* "Active" and "visible" are different questions: other layers may well be
|
|
75
|
+
* on screen. This is the one whose mode this control shows and whose opacity
|
|
76
|
+
* the slider moves -- the tool the user selected in the sidebar.
|
|
77
|
+
*/
|
|
78
|
+
activeLayer() {
|
|
79
|
+
const viewer = this.seaDragonViewer;
|
|
80
|
+
return viewer?.getCellLayer?.(viewer.cellLayerOwner) || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every registered layer, bottom of the stack first. */
|
|
84
|
+
layers() {
|
|
85
|
+
return this.seaDragonViewer?.cellLayers?.() || [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @function init - binds the Cells control and the HD checkbox. No-ops on
|
|
90
|
+
* datasources that don't render them at all (e.g. RGB quick-view, where
|
|
91
|
+
* index.html never emits the controls).
|
|
92
|
+
*/
|
|
93
|
+
init() {
|
|
94
|
+
const control = document.querySelector('#cell_display_control');
|
|
95
|
+
const hdEl = document.querySelector('#viewer_controls_hd');
|
|
96
|
+
if (!control || !hdEl) return;
|
|
97
|
+
|
|
98
|
+
this.control = control;
|
|
99
|
+
control.querySelectorAll('[data-cell-mode]').forEach((button) => {
|
|
100
|
+
this.buttons.set(button.dataset.cellMode, button);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// config.segmentation is the authoritative signal -- it's set only when
|
|
104
|
+
// a segmentation file was actually registered. imageData[0].src is NOT
|
|
105
|
+
// a reliable proxy: it's the label/"Area" channel only when segmentation
|
|
106
|
+
// exists, otherwise it's just the first real image channel (always has
|
|
107
|
+
// a real src), which used to make this evaluate true even with no
|
|
108
|
+
// segmentation at all.
|
|
109
|
+
//
|
|
110
|
+
// Recorded here rather than acted on: the viewer needs to know there is
|
|
111
|
+
// no mask to fetch, whether or not anything asks to draw one.
|
|
112
|
+
if (!this.hasSegmentation()) {
|
|
113
|
+
this.seaDragonViewer.noLabel = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.refreshAvailability();
|
|
117
|
+
|
|
118
|
+
// Delegated, so the disabled check happens in one place and a button
|
|
119
|
+
// added later needs no wiring of its own.
|
|
120
|
+
control.addEventListener('click', (event) => {
|
|
121
|
+
const button = event.target.closest?.('[data-cell-mode]');
|
|
122
|
+
if (!button || button.disabled || !control.contains(button)) return;
|
|
123
|
+
// A click is a decision, so it outranks whatever the automatic
|
|
124
|
+
// fallback chose -- main.js's adoptSegmentation reads this to know
|
|
125
|
+
// whether a mask arriving later may take the drawing over.
|
|
126
|
+
this.seaDragonViewer.centroidsFromFallback = false;
|
|
127
|
+
this.selectMode(button.dataset.cellMode);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Left/right arrows across the group, which is what a radiogroup is
|
|
131
|
+
// expected to do and the only way to reach it without a pointer.
|
|
132
|
+
control.addEventListener('keydown', (event) => {
|
|
133
|
+
const step = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1
|
|
134
|
+
: event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1
|
|
135
|
+
: 0;
|
|
136
|
+
if (!step) return;
|
|
137
|
+
const enabled = ViewerControls.MODES
|
|
138
|
+
.filter((mode) => !this.buttons.get(mode)?.disabled);
|
|
139
|
+
const index = enabled.indexOf(this.mode);
|
|
140
|
+
const next = enabled[(index + step + enabled.length) % enabled.length];
|
|
141
|
+
if (!next) return;
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
this.seaDragonViewer.centroidsFromFallback = false;
|
|
144
|
+
this.buttons.get(next)?.focus();
|
|
145
|
+
this.selectMode(next);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Toggle HD (full-precision 16-bit) tile quality
|
|
149
|
+
hdEl.addEventListener('change', e => {
|
|
150
|
+
this.seaDragonViewer.viewerManagerVMain.setHdMode(e.target.checked);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.bindPointSize();
|
|
154
|
+
this.bindLayerOpacity();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @function bindPointSize - the centroid dot size slider.
|
|
159
|
+
*
|
|
160
|
+
* Geometry is core's, so this is core's: every plugin that colours cells
|
|
161
|
+
* gets it, and none of them ships a centroid renderer to put it on. Fires
|
|
162
|
+
* on `input` rather than `change` because it is a redraw of what is already
|
|
163
|
+
* in view -- there is nothing to defer to the end of the drag.
|
|
164
|
+
*/
|
|
165
|
+
bindPointSize() {
|
|
166
|
+
const slider = document.querySelector('#cell_point_size');
|
|
167
|
+
this.pointSizeRow = document.querySelector('#cell_point_size_row');
|
|
168
|
+
if (!slider) return;
|
|
169
|
+
slider.value = String(this.seaDragonViewer.centroidPointScale ?? 1);
|
|
170
|
+
slider.addEventListener('input', (event) => {
|
|
171
|
+
this.seaDragonViewer.setCentroidPointScale?.(Number(event.target.value));
|
|
172
|
+
});
|
|
173
|
+
this.paintPointSize();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Show the size slider exactly when there are points to size. */
|
|
177
|
+
paintPointSize() {
|
|
178
|
+
if (this.pointSizeRow) this.pointSizeRow.hidden = this.mode !== "centroids";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @function bindLayerOpacity - how strongly the ACTIVE layer sits over what
|
|
183
|
+
* is under it.
|
|
184
|
+
*
|
|
185
|
+
* Core's, and shared, for the same reason the mode buttons are: geometry and
|
|
186
|
+
* compositing belong to the viewer, so every plugin that colours cells gets
|
|
187
|
+
* this without shipping a slider of its own. It used to live inside Cell
|
|
188
|
+
* Explorer's panel, where a second such plugin would have had to grow a
|
|
189
|
+
* duplicate -- and where it silently moved whichever layer happened to be
|
|
190
|
+
* active rather than the one the panel was about.
|
|
191
|
+
*
|
|
192
|
+
* Two events, two costs. `input` fires per pixel of drag and only changes
|
|
193
|
+
* the alpha the tile canvases composite at, which is a redraw. `change`
|
|
194
|
+
* fires once on release, and is the only one a plugin needs to hear in order
|
|
195
|
+
* to persist the value.
|
|
196
|
+
*/
|
|
197
|
+
bindLayerOpacity() {
|
|
198
|
+
const slider = document.querySelector('#cell_layer_opacity');
|
|
199
|
+
this.opacityRow = document.querySelector('#cell_layer_opacity_row');
|
|
200
|
+
if (!slider) return;
|
|
201
|
+
this.opacitySlider = slider;
|
|
202
|
+
slider.addEventListener('input', (event) => {
|
|
203
|
+
const layer = this.activeLayer();
|
|
204
|
+
if (!layer) return;
|
|
205
|
+
const value = Number(event.target.value) / 100;
|
|
206
|
+
this.seaDragonViewer.setLayerOpacity(layer.name, value);
|
|
207
|
+
this.paintOpacityReadout(value);
|
|
208
|
+
});
|
|
209
|
+
slider.addEventListener('change', (event) => {
|
|
210
|
+
const layer = this.activeLayer();
|
|
211
|
+
if (!layer) return;
|
|
212
|
+
window.dispatchEvent(new CustomEvent("plexora:cell-layer-opacity-changed", {
|
|
213
|
+
detail: { layer: layer.name, value: Number(event.target.value) / 100 },
|
|
214
|
+
}));
|
|
215
|
+
});
|
|
216
|
+
this.paintLayerOpacity();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Put the slider where the active layer actually is, and show it only when
|
|
220
|
+
* there is a layer for it to act on. */
|
|
221
|
+
paintLayerOpacity() {
|
|
222
|
+
const layer = this.activeLayer();
|
|
223
|
+
if (this.opacityRow) this.opacityRow.hidden = !layer;
|
|
224
|
+
if (!this.opacitySlider || !layer) return;
|
|
225
|
+
this.opacitySlider.value = String(Math.round(layer.opacity * 100));
|
|
226
|
+
this.paintOpacityReadout(layer.opacity);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
paintOpacityReadout(value) {
|
|
230
|
+
const readout = document.querySelector('#cell_layer_opacity_value');
|
|
231
|
+
if (readout) readout.textContent = `${Math.round(value * 100)}%`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @function selectMode - draw cells the given way.
|
|
236
|
+
*
|
|
237
|
+
* The one path that changes what the cell layer shows. Both halves of the
|
|
238
|
+
* old pair of handlers live here because they are not independent: turning
|
|
239
|
+
* outlines on means turning centroids off, and the two checkboxes could
|
|
240
|
+
* only express that by each undoing the other after the fact.
|
|
241
|
+
*
|
|
242
|
+
* @param mode - "none" | "centroids" | "outlines" | "filled"
|
|
243
|
+
*/
|
|
244
|
+
async selectMode(mode) {
|
|
245
|
+
const next = ViewerControls.MODES.includes(mode) ? mode : "none";
|
|
246
|
+
if (next === this.mode) return;
|
|
247
|
+
const button = this.buttons.get(next);
|
|
248
|
+
if (button?.disabled) return;
|
|
249
|
+
|
|
250
|
+
const previous = this.mode;
|
|
251
|
+
this.paint(next);
|
|
252
|
+
// Told before the work below, so the label tiles that ensureSegmentationReady
|
|
253
|
+
// renders on their way in are already rendered the right way -- otherwise
|
|
254
|
+
// switching to Filled would draw outlines once and re-render them.
|
|
255
|
+
this.applyMode(next);
|
|
256
|
+
|
|
257
|
+
// Not "what did the user just pick" but "what is on screen now": the
|
|
258
|
+
// mask item and the point overlay are one each, shared by every layer,
|
|
259
|
+
// so a second layer drawing outlines has to keep them on when the active
|
|
260
|
+
// layer moves to centroids.
|
|
261
|
+
const wantsMask = this.maskWanted();
|
|
262
|
+
const wantsPoints = this.pointsWanted();
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
this.seaDragonViewer.viewerManagerVMain.sel_outlines = wantsMask;
|
|
266
|
+
await this.seaDragonViewer.updateCentroidVisibility(wantsPoints);
|
|
267
|
+
if (wantsMask) {
|
|
268
|
+
await this.seaDragonViewer.ensureSegmentationReady(true);
|
|
269
|
+
await this.seaDragonViewer.updateSegmentationFilter(this.currentFilter(), true);
|
|
270
|
+
}
|
|
271
|
+
if (wantsPoints) {
|
|
272
|
+
this.seaDragonViewer.setLoading(true);
|
|
273
|
+
try {
|
|
274
|
+
this.seaDragonViewer.updateCentroidFilter(this.currentFilter(), true);
|
|
275
|
+
} finally {
|
|
276
|
+
this.seaDragonViewer.setLoading(false);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!wantsMask) {
|
|
280
|
+
this.seaDragonViewer.viewer.forceRedraw();
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.warn(`Unable to draw cells as "${next}".`, error);
|
|
284
|
+
// Centroids are the fallback only when there are positions to draw:
|
|
285
|
+
// a project with no table, or one whose coordinate roles nobody has
|
|
286
|
+
// answered, would spend the fetch on a manifest with no points in it.
|
|
287
|
+
if (wantsMask && this.canDrawCentroids()) {
|
|
288
|
+
this.seaDragonViewer.viewerManagerVMain.sel_outlines = false;
|
|
289
|
+
await this.seaDragonViewer.updateCentroidFallback(true);
|
|
290
|
+
} else {
|
|
291
|
+
this.paint(previous === next ? "none" : previous);
|
|
292
|
+
this.applyMode(this.mode);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.eventHandler.trigger("SELECTION_CHANGED", this.currentFilter());
|
|
297
|
+
this.announce();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Push a mode onto whatever this control is editing: the active layer, or
|
|
302
|
+
* core itself when there is none.
|
|
303
|
+
*
|
|
304
|
+
* Choosing a mode for a layer the user had switched off is also a request to
|
|
305
|
+
* see it -- otherwise the click would visibly do nothing.
|
|
306
|
+
*/
|
|
307
|
+
applyMode(mode) {
|
|
308
|
+
const layer = this.activeLayer();
|
|
309
|
+
if (!layer) {
|
|
310
|
+
this.seaDragonViewer.setCellDisplayMode(mode);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (mode !== "none") {
|
|
314
|
+
this.seaDragonViewer.setCellLayerVisible(layer.name, true);
|
|
315
|
+
}
|
|
316
|
+
this.seaDragonViewer.setCellLayerMode(layer.name, mode);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Whether anything on screen is drawn from the label tiles right now. */
|
|
320
|
+
maskWanted() {
|
|
321
|
+
const layers = this.layers();
|
|
322
|
+
if (!layers.length) return this.mode === "outlines" || this.mode === "filled";
|
|
323
|
+
return layers.some((layer) => layer.visible
|
|
324
|
+
&& (layer.mode === "outlines" || layer.mode === "filled"));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Whether anything on screen is drawn as points right now. */
|
|
328
|
+
pointsWanted() {
|
|
329
|
+
const layers = this.layers();
|
|
330
|
+
if (!layers.length) return this.mode === "centroids";
|
|
331
|
+
return layers.some((layer) => layer.visible && layer.mode === "centroids");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @function refreshLayerSurfaces - bring the shared surfaces into line with
|
|
336
|
+
* whatever the layers now want.
|
|
337
|
+
*
|
|
338
|
+
* The label item and the point overlay are one each and every layer draws
|
|
339
|
+
* onto them, so neither can be switched by the layer that happened to
|
|
340
|
+
* change. Called after a card's eye is toggled or a tool is removed --
|
|
341
|
+
* selectMode does this work inline for the click that caused it.
|
|
342
|
+
*
|
|
343
|
+
* Without this, turning a layer back on while nothing else was drawing a
|
|
344
|
+
* mask left the mask item unloaded: the layer was visible, its canvases were
|
|
345
|
+
* built, and the eye did nothing anyone could see.
|
|
346
|
+
*/
|
|
347
|
+
async refreshLayerSurfaces() {
|
|
348
|
+
const manager = this.seaDragonViewer?.viewerManagerVMain;
|
|
349
|
+
if (!this.control || !manager) return;
|
|
350
|
+
const wantsMask = this.maskWanted();
|
|
351
|
+
const wantsPoints = this.pointsWanted();
|
|
352
|
+
// Loading the mask is the expensive half, so it is done only on the edge
|
|
353
|
+
// -- turning it off and on again for a layer that was already drawing
|
|
354
|
+
// one would re-read the pyramid for nothing.
|
|
355
|
+
const maskArriving = wantsMask && !manager.sel_outlines;
|
|
356
|
+
manager.sel_outlines = wantsMask;
|
|
357
|
+
try {
|
|
358
|
+
await this.seaDragonViewer.updateCentroidVisibility(wantsPoints);
|
|
359
|
+
if (maskArriving) {
|
|
360
|
+
await this.seaDragonViewer.ensureSegmentationReady(true);
|
|
361
|
+
await this.seaDragonViewer.updateSegmentationFilter(this.currentFilter(), true);
|
|
362
|
+
}
|
|
363
|
+
if (wantsPoints) {
|
|
364
|
+
this.seaDragonViewer.updateCentroidFilter(this.currentFilter(), true);
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.warn("Unable to update what the cell layers draw.", error);
|
|
368
|
+
}
|
|
369
|
+
this.seaDragonViewer.viewer?.forceRedraw?.();
|
|
370
|
+
this.announce();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @function syncToActiveLayer - point the shared controls at whichever layer
|
|
375
|
+
* is now active.
|
|
376
|
+
*
|
|
377
|
+
* Called when a plugin registers, when the user selects a different tool's
|
|
378
|
+
* card, and when one is removed. Purely a repaint: nothing about what is
|
|
379
|
+
* drawn changes here, only what the control says and offers.
|
|
380
|
+
*/
|
|
381
|
+
syncToActiveLayer() {
|
|
382
|
+
if (!this.control) return;
|
|
383
|
+
const layer = this.activeLayer();
|
|
384
|
+
this.refreshAvailability();
|
|
385
|
+
this.paint(layer && ViewerControls.MODES.includes(layer.mode)
|
|
386
|
+
? layer.mode : this.mode);
|
|
387
|
+
this.announce();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @function adoptMode - reflect a mode the viewer switched to on its own.
|
|
392
|
+
*
|
|
393
|
+
* For the automatic paths -- a mask that failed to load, a pyramid that
|
|
394
|
+
* arrived late -- where the drawing has already changed and the control has
|
|
395
|
+
* to agree with it. Deliberately does none of the loading work: the caller
|
|
396
|
+
* has done it, and re-entering selectMode from inside one of those paths
|
|
397
|
+
* would recurse.
|
|
398
|
+
*/
|
|
399
|
+
adoptMode(mode) {
|
|
400
|
+
if (!ViewerControls.MODES.includes(mode) || mode === this.mode) return;
|
|
401
|
+
this.paint(mode);
|
|
402
|
+
this.applyMode(mode);
|
|
403
|
+
this.announce();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Move the selection. Visual only -- see selectMode for the work. */
|
|
407
|
+
paint(mode) {
|
|
408
|
+
this.mode = mode;
|
|
409
|
+
this.buttons.forEach((button, name) => {
|
|
410
|
+
const active = name === mode;
|
|
411
|
+
button.classList.toggle("is-active", active);
|
|
412
|
+
button.setAttribute("aria-checked", active ? "true" : "false");
|
|
413
|
+
});
|
|
414
|
+
this.paintPointSize();
|
|
415
|
+
this.paintLayerOpacity();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Tell the rest of the page. The two legacy events are still fired because
|
|
420
|
+
* they are the cross-file, non-module notification other views already
|
|
421
|
+
* listen for (navbarControls.js, and any plugin that hooked them); the mode
|
|
422
|
+
* event is what a listener that understands all four should use.
|
|
423
|
+
*
|
|
424
|
+
* `layer` is the important addition: with several plugins loaded, a mode
|
|
425
|
+
* change belongs to ONE of them, and a plugin storing the user's choice must
|
|
426
|
+
* ignore the others' -- otherwise opening a second tool and clicking
|
|
427
|
+
* Outlines silently rewrites the first tool's saved preference.
|
|
428
|
+
*/
|
|
429
|
+
announce() {
|
|
430
|
+
const layer = this.activeLayer();
|
|
431
|
+
const detail = {
|
|
432
|
+
mode: this.mode,
|
|
433
|
+
available: this.availability(),
|
|
434
|
+
offered: this.offeredModes(),
|
|
435
|
+
layer: layer?.name || null,
|
|
436
|
+
opacity: layer ? layer.opacity : null,
|
|
437
|
+
};
|
|
438
|
+
window.dispatchEvent(new CustomEvent("plexora:cell-mode-changed", { detail }));
|
|
439
|
+
window.dispatchEvent(new CustomEvent("plexora:outlines-changed", {
|
|
440
|
+
detail: { enabled: this.maskWanted() },
|
|
441
|
+
}));
|
|
442
|
+
window.dispatchEvent(new CustomEvent("plexora:centroids-changed", {
|
|
443
|
+
detail: { enabled: this.pointsWanted() },
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @function hasSegmentation - whether a mask pyramid exists and can be
|
|
449
|
+
* drawn right now. False while the background conversion job is still
|
|
450
|
+
* running, which is a state the viewer sees often: import starts the job
|
|
451
|
+
* and opens the project without waiting for it.
|
|
452
|
+
*/
|
|
453
|
+
hasSegmentation() {
|
|
454
|
+
return Boolean(this.config?.segmentation);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* @function maskPending - whether a mask exists but is still being
|
|
459
|
+
* converted. The other half of hasSegmentation: between them, "no mask" and
|
|
460
|
+
* "not yet" are two different projects and want two different answers.
|
|
461
|
+
*/
|
|
462
|
+
maskPending() {
|
|
463
|
+
return this.config?.segmentation_status === "pending";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
canDrawOutlines() {
|
|
467
|
+
return this.hasSegmentation() && !this.seaDragonViewer.noLabel;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Filled needs the labels stored whole. A pyramid written already reduced
|
|
472
|
+
* to boundaries (`segmentationMode` "outlines") has no interior pixels, so
|
|
473
|
+
* there is literally nothing to fill -- offering it would be a button that
|
|
474
|
+
* changes nothing.
|
|
475
|
+
*/
|
|
476
|
+
canDrawFilled() {
|
|
477
|
+
return this.canDrawOutlines() && this.config?.segmentationMode === "filled";
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
canDrawCentroids() {
|
|
481
|
+
return PlexoraDataset.hasCentroids(this.config);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
availability() {
|
|
485
|
+
return {
|
|
486
|
+
none: true,
|
|
487
|
+
centroids: this.canDrawCentroids(),
|
|
488
|
+
outlines: this.canDrawOutlines(),
|
|
489
|
+
filled: this.canDrawFilled(),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @function offeredModes - what the control actually shows right now.
|
|
495
|
+
*
|
|
496
|
+
* Two filters, in order. The PROJECT decides what can be drawn at all
|
|
497
|
+
* (availability). An active layer then narrows that to what its plugin can
|
|
498
|
+
* draw -- a tool that only marks a handful of cells has no use for Filled,
|
|
499
|
+
* and offering it is offering a button whose result the tool did not design
|
|
500
|
+
* for. A plugin that declares nothing gets everything the project can do.
|
|
501
|
+
*
|
|
502
|
+
* "None" is offered only while there is NO active layer. With one, the
|
|
503
|
+
* card's own eye toggle is what turns a layer off, and a second control
|
|
504
|
+
* meaning almost-but-not-quite the same thing is the kind of ambiguity that
|
|
505
|
+
* makes both of them feel broken.
|
|
506
|
+
*/
|
|
507
|
+
offeredModes() {
|
|
508
|
+
const available = this.availability();
|
|
509
|
+
const layer = this.activeLayer();
|
|
510
|
+
const supported = layer?.supportedModes || null;
|
|
511
|
+
const offered = {};
|
|
512
|
+
ViewerControls.MODES.forEach((mode) => {
|
|
513
|
+
offered[mode] = Boolean(available[mode])
|
|
514
|
+
&& (!supported || supported.includes(mode));
|
|
515
|
+
});
|
|
516
|
+
if (layer) offered.none = false;
|
|
517
|
+
return offered;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @function refreshAvailability - show and enable what can be drawn now.
|
|
522
|
+
*
|
|
523
|
+
* Called at init, whenever a background mask conversion lands (main.js's
|
|
524
|
+
* adoptSegmentation), and whenever the active layer changes. Every option
|
|
525
|
+
* ships disabled from the template, so a Filled button is never briefly
|
|
526
|
+
* clickable on a project that cannot fill.
|
|
527
|
+
*
|
|
528
|
+
* Disabled and hidden mean different things here. A mode the PROJECT cannot
|
|
529
|
+
* draw stays visible and disabled, with the reason on its tooltip -- that is
|
|
530
|
+
* a fact about this dataset the user should be able to see. A mode the
|
|
531
|
+
* active PLUGIN does not use is hidden outright, because "not applicable to
|
|
532
|
+
* the tool you have open" has no explanation worth a tooltip and would
|
|
533
|
+
* otherwise leave a row of permanently greyed buttons. The buttons stay in
|
|
534
|
+
* the DOM either way: they belong to the control, not to whichever tool
|
|
535
|
+
* happens to be open.
|
|
536
|
+
*/
|
|
537
|
+
refreshAvailability() {
|
|
538
|
+
if (!this.control) return;
|
|
539
|
+
const available = this.availability();
|
|
540
|
+
const offered = this.offeredModes();
|
|
541
|
+
const reasons = {
|
|
542
|
+
centroids: "Needs cell coordinates",
|
|
543
|
+
outlines: "Needs a segmentation mask",
|
|
544
|
+
filled: this.canDrawOutlines()
|
|
545
|
+
? "This mask is stored as outlines, so there is nothing to fill"
|
|
546
|
+
: "Needs a segmentation mask",
|
|
547
|
+
};
|
|
548
|
+
this.buttons.forEach((button, name) => {
|
|
549
|
+
const usable = Boolean(offered[name]);
|
|
550
|
+
button.style.display = !usable && Boolean(available[name]) ? "none" : "";
|
|
551
|
+
button.disabled = !usable;
|
|
552
|
+
if (usable) {
|
|
553
|
+
button.removeAttribute("title");
|
|
554
|
+
} else if (reasons[name]) {
|
|
555
|
+
button.title = reasons[name];
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @function enableCellLayer - turn on the layer this project draws cells
|
|
562
|
+
* with, for a plugin that colours them.
|
|
563
|
+
*
|
|
564
|
+
* Two separate questions, deliberately kept apart:
|
|
565
|
+
*
|
|
566
|
+
* WHICH LAYER -- mask or points -- is the project's, recorded on it
|
|
567
|
+
* (server/models/project.py's CELL_LAYERS). A plugin does not get to
|
|
568
|
+
* overrule that, or the answer would change depending on which tool was
|
|
569
|
+
* opened first.
|
|
570
|
+
*
|
|
571
|
+
* HOW TO DRAW THE MASK -- filled or outlines -- is the plugin's, because it
|
|
572
|
+
* depends on what the plugin is showing. A tool that colours every cell by
|
|
573
|
+
* a phenotype wants filled; one that marks a few cells wants outlines over
|
|
574
|
+
* visible tissue.
|
|
575
|
+
*
|
|
576
|
+
* @param preference - "filled" | "outlines" | "centroids" | null, from the
|
|
577
|
+
* plugin's `preferredCellMode`. "segmentation" is accepted as an older
|
|
578
|
+
* spelling of "outlines". Anything the project or the mask cannot
|
|
579
|
+
* actually do falls back rather than failing.
|
|
580
|
+
*
|
|
581
|
+
* The recorded layer is a preference, not a promise: a mask whose pyramid
|
|
582
|
+
* is still being built cannot be drawn yet, so this falls back to centroids
|
|
583
|
+
* for now and leaves the stored choice alone. main.js polls the conversion
|
|
584
|
+
* job and swaps the drawing over when it lands.
|
|
585
|
+
*
|
|
586
|
+
* @param name - the layer this is being decided for, or null for core's own
|
|
587
|
+
* control on a viewer with no layers.
|
|
588
|
+
*
|
|
589
|
+
* Does nothing if that layer is already drawing something -- the user's own
|
|
590
|
+
* choice outranks a tool's opinion, and re-opening a tool must not undo it.
|
|
591
|
+
* Asked per layer rather than of the control as a whole: with one shared
|
|
592
|
+
* control, "something is already showing" was true as soon as ANY tool had
|
|
593
|
+
* turned the mask on, so the second plugin to open never got the mode it
|
|
594
|
+
* asked for.
|
|
595
|
+
*/
|
|
596
|
+
async enableCellLayer(preference, name = null) {
|
|
597
|
+
if (!this.control) return;
|
|
598
|
+
const layer = name ? this.seaDragonViewer.getCellLayer?.(name) : null;
|
|
599
|
+
if (layer ? layer.mode !== "none" : this.mode !== "none") return;
|
|
600
|
+
|
|
601
|
+
const wanted = preference === 'centroids' ? 'centroids'
|
|
602
|
+
: this.config?.cellLayer || null;
|
|
603
|
+
const canOutline = this.canDrawOutlines();
|
|
604
|
+
const canCentroid = this.canDrawCentroids();
|
|
605
|
+
const onMask = this.maskMode(preference);
|
|
606
|
+
|
|
607
|
+
// A mask that is still converting is not a project without a mask.
|
|
608
|
+
// Standing centroids in for it draws a different representation of the
|
|
609
|
+
// same cells, silently, for however long the job runs -- and a tool that
|
|
610
|
+
// asked for the mask asked because cell SHAPE is what it is showing, so
|
|
611
|
+
// the substitute is the wrong picture rather than a rougher one. Nothing
|
|
612
|
+
// is turned on here: whoever asked shows the wait (see Cell Explorer's
|
|
613
|
+
// panel), and adoptSegmentation turns the layer on when the pyramid
|
|
614
|
+
// lands. A user who would rather not wait can say so, which is what
|
|
615
|
+
// fallBackToCentroids is for.
|
|
616
|
+
if (wanted !== 'centroids' && !canOutline && this.maskPending()) {
|
|
617
|
+
this.seaDragonViewer.cellLayerAwaitingMask = true;
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Segmentation first whenever it is wanted and ready: it shows the real
|
|
622
|
+
// cell shape, and a user who supplied a mask supplied it to be used.
|
|
623
|
+
const target = (wanted !== 'centroids' && canOutline) ? onMask
|
|
624
|
+
: canCentroid ? "centroids"
|
|
625
|
+
: canOutline ? onMask
|
|
626
|
+
: null;
|
|
627
|
+
if (!target) return;
|
|
628
|
+
|
|
629
|
+
await this.selectMode(target);
|
|
630
|
+
|
|
631
|
+
// Set after the switch, which clears it: selectMode treats a click as a
|
|
632
|
+
// user decision, and this one was not. Centroids reached here despite a
|
|
633
|
+
// mask being wanted means there is no mask coming at all -- a pending
|
|
634
|
+
// one returned above -- but the project can still gain one from the edit
|
|
635
|
+
// page mid-session, and main.js swaps the drawing over when it does.
|
|
636
|
+
if (this.mode === "centroids" && wanted !== 'centroids' && !canOutline) {
|
|
637
|
+
this.seaDragonViewer.centroidsFromFallback = true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Draw centroids for now, and take the mask when it arrives.
|
|
643
|
+
*
|
|
644
|
+
* The way out of the wait above, for somebody who would rather see roughly
|
|
645
|
+
* where the cells are than nothing at all while a long conversion finishes.
|
|
646
|
+
* Deliberately marked as a fallback rather than as a choice: it is standing
|
|
647
|
+
* in for a mask that is still coming, so adoptSegmentation swaps the drawing
|
|
648
|
+
* over when the pyramid lands, exactly as it would have without the wait.
|
|
649
|
+
*
|
|
650
|
+
* @returns whether there were centroids to fall back to.
|
|
651
|
+
*/
|
|
652
|
+
async fallBackToCentroids() {
|
|
653
|
+
if (!this.canDrawCentroids()) return false;
|
|
654
|
+
this.seaDragonViewer.cellLayerAwaitingMask = false;
|
|
655
|
+
await this.selectMode("centroids");
|
|
656
|
+
this.seaDragonViewer.centroidsFromFallback = true;
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* How to draw the mask for a plugin that asked for `preference`.
|
|
662
|
+
*
|
|
663
|
+
* Filled only when the plugin wants it AND this mask can do it -- a pyramid
|
|
664
|
+
* written already reduced to boundaries has no interior to fill, so asking
|
|
665
|
+
* for filled there has to land on outlines rather than on a mode the
|
|
666
|
+
* control itself has disabled.
|
|
667
|
+
*/
|
|
668
|
+
maskMode(preference) {
|
|
669
|
+
return preference === 'filled' && this.canDrawFilled() ? "filled" : "outlines";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* @function ownerMaskPreference - how the plugin currently holding the cell
|
|
674
|
+
* layer would like the mask drawn, or null when nothing holds it.
|
|
675
|
+
*
|
|
676
|
+
* Asked of the viewer rather than of a named plugin, the same way
|
|
677
|
+
* currentFilter() is -- core never learns which plugins exist. main.js puts
|
|
678
|
+
* the preference on the provider when the layer is claimed.
|
|
679
|
+
*
|
|
680
|
+
* This exists for the paths that turn the mask on WITHOUT a plugin
|
|
681
|
+
* activating: a pyramid that finishes converting minutes into a session is
|
|
682
|
+
* the one that matters. That used to hardcode outlines, so a project whose
|
|
683
|
+
* mask was attached from the edit page came up as outlines for the rest of
|
|
684
|
+
* the session however the active tool wanted to draw it.
|
|
685
|
+
*/
|
|
686
|
+
ownerMaskPreference() {
|
|
687
|
+
return this.seaDragonViewer?.cellLayer?.preferredCellMode || null;
|
|
688
|
+
}
|
|
689
|
+
}
|