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,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* main.js Initializes main client/interface setup, and distributes events to respective views
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
//EVENTHANDLER
|
|
6
|
+
const eventHandler = new SimpleEventHandler(d3.select("body").node());
|
|
7
|
+
const datasource = flaskVariables.datasource;
|
|
8
|
+
|
|
9
|
+
//VIEWS
|
|
10
|
+
const __plexora = window.__plexora = {
|
|
11
|
+
dataLayer: null,
|
|
12
|
+
channelList: null,
|
|
13
|
+
seaDragonViewer: null,
|
|
14
|
+
viewerSidebar: null,
|
|
15
|
+
// Activated plugins by name: { definition, instance, sidebarController,
|
|
16
|
+
// cleanups }. Replaces the single csv_gatingList slot -- core no longer has
|
|
17
|
+
// a place that can only hold one plugin, or that names a particular one.
|
|
18
|
+
plugins: new Map()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//DATA MANAGEMENT
|
|
22
|
+
const dataSrcIndex = 0;
|
|
23
|
+
|
|
24
|
+
//CHANNELS
|
|
25
|
+
const imageChannels = {}; // lookup table between channel id and channel name (for image viewer)
|
|
26
|
+
const imageChannelsIdx = {};
|
|
27
|
+
|
|
28
|
+
//OTHER SETTINGS
|
|
29
|
+
document.getElementById("openseadragon").addEventListener("contextmenu", (event) => event.preventDefault()); //Disable right clicking on element
|
|
30
|
+
|
|
31
|
+
//LOAD DATA
|
|
32
|
+
// Data prevent caching on the config file, as it may have been modified
|
|
33
|
+
window.__plexoraReady = d3.json(`${plexoraUrl("config")}?t=${Date.now()}`).then(function (config) {
|
|
34
|
+
return init(config[datasource]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
//INITS
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Init all views.
|
|
41
|
+
*
|
|
42
|
+
* @param conf - The configuration json file
|
|
43
|
+
*/
|
|
44
|
+
async function init(config) {
|
|
45
|
+
// Flat RGB quick-view datasource (no channels, no gating, no feature
|
|
46
|
+
// table worth loading) -- hand off to the minimal pan/zoom-only viewer
|
|
47
|
+
// and skip DataLayer/ChannelList/ViewerSidebar/module setup entirely.
|
|
48
|
+
if (config.image_kind === "rgb") {
|
|
49
|
+
const rgbViewer = new RgbImageViewer(config);
|
|
50
|
+
__plexora.seaDragonViewer = rgbViewer;
|
|
51
|
+
await rgbViewer.init();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//maximum selections
|
|
56
|
+
config.maxSelections = 4;
|
|
57
|
+
config.extraZoomLevels = 0;
|
|
58
|
+
if (Array.isArray(config.imageData)) {
|
|
59
|
+
config.imageData.forEach(function (channel) {
|
|
60
|
+
if (channel.src && channel.src.startsWith("/")) {
|
|
61
|
+
channel.src = plexoraUrl(channel.src);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//channel information
|
|
66
|
+
for (let idx = 0; idx < config["imageData"].length; idx++) {
|
|
67
|
+
imageChannels[config["imageData"][idx].fullname] = idx;
|
|
68
|
+
let name = config["imageData"][idx].name;
|
|
69
|
+
if (name !== "Area") {
|
|
70
|
+
imageChannelsIdx[idx] = name;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//initialize metadata
|
|
75
|
+
const dataLayer = new DataLayer(config, imageChannels);
|
|
76
|
+
const numericData = new NumericData(config, dataLayer);
|
|
77
|
+
const columns = await dataLayer.getChannelNames(true);
|
|
78
|
+
let imgMetadata = null;
|
|
79
|
+
try {
|
|
80
|
+
imgMetadata = await dataLayer.getMetadata();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Error getting metadata:", error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//Create channel panels
|
|
86
|
+
channelList = new ChannelList(config, columns, dataLayer, eventHandler);
|
|
87
|
+
__plexora.channelList = channelList;
|
|
88
|
+
__plexora.dataLayer = dataLayer;
|
|
89
|
+
|
|
90
|
+
// Plugins whose scripts this page loaded (see the server's Plugin.scripts).
|
|
91
|
+
// Any number may be registered; each is activated below.
|
|
92
|
+
const pluginDefs = window.Plexora?.plugins?.all() ?? [];
|
|
93
|
+
|
|
94
|
+
//Create image viewer
|
|
95
|
+
const imageArgs = [imgMetadata, numericData, eventHandler];
|
|
96
|
+
const seaDragonViewer = new ImageViewer(config, dataLayer, ...imageArgs);
|
|
97
|
+
__plexora.seaDragonViewer = seaDragonViewer;
|
|
98
|
+
const viewerManager = new ViewerManager(seaDragonViewer, channelList);
|
|
99
|
+
|
|
100
|
+
// Core viewer toggles (Centroids/HD/Outlines) -- unconditional, independent of
|
|
101
|
+
// whichever tool (if any) is active, so they work on a plain base viewer too.
|
|
102
|
+
const viewerControls = new ViewerControls(seaDragonViewer, config, eventHandler);
|
|
103
|
+
__plexora.viewerControls = viewerControls;
|
|
104
|
+
viewerControls.init();
|
|
105
|
+
|
|
106
|
+
// Fire the database description request without awaiting it here --
|
|
107
|
+
// ImageViewer.init() below never reads dd (only WebGL/OSD setup), so
|
|
108
|
+
// there's no reason to serialize tile-viewer construction behind this
|
|
109
|
+
// network round trip. Only channelList.init(dd) and each plugin's own
|
|
110
|
+
// init(dd) below actually need dd, and they run after it resolves anyway.
|
|
111
|
+
const ddPromise = dataLayer.getDatabaseDescription();
|
|
112
|
+
|
|
113
|
+
const imageInit = [viewerManager, channelList, null, [], []];
|
|
114
|
+
const [dd] = await Promise.all([ddPromise, dataLayer.init(), seaDragonViewer.init(...imageInit)]);
|
|
115
|
+
__plexora.databaseDescription = dd;
|
|
116
|
+
// The dataset handed to every plugin -- same shape as the server's
|
|
117
|
+
// plexora.api dataset, so a plugin reads roles rather than column names.
|
|
118
|
+
__plexora.dataset = PlexoraDataset.build(config, imageChannels, dd);
|
|
119
|
+
channelList.init(dd);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Re-fetch what the server says this project's numbers are.
|
|
123
|
+
*
|
|
124
|
+
* Both of the things a tool is drawn from -- the read spec in `config` and
|
|
125
|
+
* the per-column statistics in `dd` -- are fetched once at page load. That
|
|
126
|
+
* is fine right up until the user changes which matrix is read, or turns
|
|
127
|
+
* the log transform on, from the requirements modal: the server re-reads
|
|
128
|
+
* the table and clears its own caches, but the page is still holding the
|
|
129
|
+
* ranges and histograms of the matrix it is no longer reading. Thresholding
|
|
130
|
+
* then opens on a panel of X values with a read spec pointing at a layer.
|
|
131
|
+
*
|
|
132
|
+
* Both `config` and `dd` are updated in place rather than replaced. Each is
|
|
133
|
+
* held by reference by things that outlive this call -- `config` by the
|
|
134
|
+
* viewer, the channel list and ViewerControls; `dd` by ChannelList and
|
|
135
|
+
* ViewerSidebar, which took it at boot (channelList.init(dd) /
|
|
136
|
+
* viewerSidebar.init(dd)) and read their ranges and histograms straight out
|
|
137
|
+
* of it ever after. Handing them a new object leaves them on the old one:
|
|
138
|
+
* rebinding only __plexora's reference is what left Thresholding drawing a
|
|
139
|
+
* log-valued slider over an X-valued histogram, since the gating panel
|
|
140
|
+
* reads the sidebar's copy.
|
|
141
|
+
*/
|
|
142
|
+
__plexora.refreshDataset = async function refreshDataset() {
|
|
143
|
+
const fresh = await d3.json(`${plexoraUrl("config")}?t=${Date.now()}`);
|
|
144
|
+
const entry = fresh?.[datasource];
|
|
145
|
+
if (!entry) return;
|
|
146
|
+
// Only the read spec: attaching a mask also rewrites imageData, and
|
|
147
|
+
// adopting a new channel list mid-session would shift every index the
|
|
148
|
+
// tile path and the channel sliders are keyed on.
|
|
149
|
+
config.dataset = entry.dataset;
|
|
150
|
+
const description = await dataLayer.getDatabaseDescription();
|
|
151
|
+
for (const [column, stats] of Object.entries(description || {})) {
|
|
152
|
+
// Merged into the existing entry, not assigned over it: a channel's
|
|
153
|
+
// image_min/image_max/image_histogram and its quantization window
|
|
154
|
+
// are fetched lazily on activation (ChannelList.ensureChannelStats)
|
|
155
|
+
// and live in these same entries. Only which numbers the feature
|
|
156
|
+
// table holds has changed -- the image has not -- so they stay.
|
|
157
|
+
dd[column] = { ...dd[column], ...stats };
|
|
158
|
+
}
|
|
159
|
+
__plexora.dataset = PlexoraDataset.build(config, imageChannels, dd);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// The three calls toolLoader makes while painting a card, defined HERE
|
|
163
|
+
// rather than beside activatePlugin below: a page opened with ?tool=<name>
|
|
164
|
+
// reports its already-live tool through registerLoaded partway down this
|
|
165
|
+
// function, which goes through show() and reaches all three. Assigned later,
|
|
166
|
+
// they would be undefined at exactly that moment -- and every call site
|
|
167
|
+
// guards with `?.`, so the boot path would silently skip them instead of
|
|
168
|
+
// failing.
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Point the shared controls at a tool the user has just switched to.
|
|
172
|
+
*
|
|
173
|
+
* Every layer that was visible STAYS visible -- that is the whole point of
|
|
174
|
+
* layers. What moves is which one the Cells control, the opacity slider,
|
|
175
|
+
* picking and the gate flows act on.
|
|
176
|
+
*
|
|
177
|
+
* Only for plugins that declared ownsCellLayer: a tool that draws its own
|
|
178
|
+
* overlay (ROI) must not take the active layer off the one that legitimately
|
|
179
|
+
* holds it merely by being looked at. The controls are re-synced either way,
|
|
180
|
+
* because opening ROI does not change what they should show.
|
|
181
|
+
*/
|
|
182
|
+
__plexora.setActiveTool = function setActiveTool(name) {
|
|
183
|
+
const record = name ? __plexora.plugins.get(name) : null;
|
|
184
|
+
if (record?.instance && record.definition?.ownsCellLayer) {
|
|
185
|
+
seaDragonViewer.setActiveCellLayer(name);
|
|
186
|
+
}
|
|
187
|
+
viewerControls.syncToActiveLayer();
|
|
188
|
+
return seaDragonViewer.cellLayerOwner === name;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Draw a tool's layer, or stop drawing it, without unloading the tool.
|
|
193
|
+
*
|
|
194
|
+
* Returns whether this plugin has a layer at all -- a tool that draws its
|
|
195
|
+
* own overlay answers false, and its card's toggle is its controller's to
|
|
196
|
+
* honour (see the onVisibilityChange hook in pluginRegistry.js).
|
|
197
|
+
*/
|
|
198
|
+
__plexora.setToolLayerVisible = function setToolLayerVisible(name, visible) {
|
|
199
|
+
const record = __plexora.plugins.get(name);
|
|
200
|
+
if (!record?.definition?.ownsCellLayer) return false;
|
|
201
|
+
// Only on a real change, and only then: the mask item and the point
|
|
202
|
+
// overlay are shared, so whether they are needed at all is a question
|
|
203
|
+
// about the whole stack rather than about this one layer.
|
|
204
|
+
if (seaDragonViewer.setCellLayerVisible(name, visible)) {
|
|
205
|
+
viewerControls.refreshLayerSurfaces();
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** Restack the cell layers, bottom of the sidebar order first. */
|
|
211
|
+
__plexora.setToolLayerOrder = function setToolLayerOrder(names) {
|
|
212
|
+
return seaDragonViewer.setCellLayerOrder(names);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Instantiate plugins after the viewer exists but before tile loading gets
|
|
216
|
+
// going, matching the order the single-module path used to run in.
|
|
217
|
+
for (const definition of pluginDefs) {
|
|
218
|
+
activatePluginInstance(definition, dd);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
//EVENT HANDLING
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Listen to Color Transfer Change Events and forwards it to respective views.
|
|
225
|
+
*
|
|
226
|
+
* @param d - The color map object
|
|
227
|
+
*/
|
|
228
|
+
const actionColorTransferChange = (d) => {
|
|
229
|
+
//map to full name
|
|
230
|
+
d.name = dataLayer.getFullChannelName(d.name);
|
|
231
|
+
// d3.select('body').style('cursor', 'progress');
|
|
232
|
+
seaDragonViewer.updateChannelColors(d.name, d.color, d.type);
|
|
233
|
+
// d3.select('body').style('cursor', 'default');
|
|
234
|
+
};
|
|
235
|
+
eventHandler.bind(ChannelList.events.COLOR_TRANSFER_CHANGE, actionColorTransferChange);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Listen to Render Mode Events and forwards it to respective views.
|
|
239
|
+
*
|
|
240
|
+
* @param d - The render mode object
|
|
241
|
+
*/
|
|
242
|
+
const actionRenderingModeChange = (d) => {
|
|
243
|
+
seaDragonViewer.updateRenderingMode(d);
|
|
244
|
+
};
|
|
245
|
+
eventHandler.bind(ImageViewer.events.renderingMode, actionRenderingModeChange);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Listen to Channels set for Rendering and forwards it to respective views.
|
|
249
|
+
*
|
|
250
|
+
* @param d - The channel package object
|
|
251
|
+
*/
|
|
252
|
+
const actionChannelsToRenderChange = (d) => {
|
|
253
|
+
//map to full name
|
|
254
|
+
d.name = dataLayer.getFullChannelName(d.name);
|
|
255
|
+
|
|
256
|
+
//send to image viewer
|
|
257
|
+
const action = ["remove", "add"][+d.status];
|
|
258
|
+
seaDragonViewer.updateActiveChannels(d.name, action);
|
|
259
|
+
// The tiles this kicks off are reported by the status indicator via
|
|
260
|
+
// OSD's fully-loaded-change (see appStatus.js watchViewer). The
|
|
261
|
+
// cursor:progress that used to bracket this call was set and cleared
|
|
262
|
+
// in the same synchronous turn, so it was never actually on screen
|
|
263
|
+
// while the work it described was running.
|
|
264
|
+
};
|
|
265
|
+
eventHandler.bind(ChannelList.events.CHANNELS_CHANGE, actionChannelsToRenderChange);
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Listen to regional or single cell selection.
|
|
269
|
+
*
|
|
270
|
+
* @param d - The selections
|
|
271
|
+
*/
|
|
272
|
+
const actionImageClickedMultiSel = (d) => {
|
|
273
|
+
// Nothing to select against without a feature table, or without a
|
|
274
|
+
// cell id naming the column that identifies a row.
|
|
275
|
+
const idField = __plexora.dataset?.schema?.cellId;
|
|
276
|
+
if (!idField) return;
|
|
277
|
+
const task = window.PlexoraStatus?.begin("Selecting");
|
|
278
|
+
try {
|
|
279
|
+
// add newly clicked item to selection
|
|
280
|
+
if (!Array.isArray(d.item)) {
|
|
281
|
+
dataLayer.addToCurrentSelection(d.item, true, d.clearPriors);
|
|
282
|
+
const picked = [d.item[idField]];
|
|
283
|
+
updateSeaDragonSelection({ picked });
|
|
284
|
+
} else {
|
|
285
|
+
dataLayer.addAllToCurrentSelection(d.item);
|
|
286
|
+
const picked = d.item.map(i => i[idField]);
|
|
287
|
+
updateSeaDragonSelection({ picked });
|
|
288
|
+
}
|
|
289
|
+
} finally {
|
|
290
|
+
task?.done();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
eventHandler.bind(ImageViewer.events.imageClickedMultiSel, actionImageClickedMultiSel);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Listen to Channel Select Click Events.
|
|
297
|
+
*
|
|
298
|
+
* @param sels - The selected/deselected channels
|
|
299
|
+
*/
|
|
300
|
+
const channelSelect = async (sels) => {
|
|
301
|
+
updateSeaDragonSelection();
|
|
302
|
+
let channelCells = await dataLayer.getChannelCellIds(sels);
|
|
303
|
+
dataLayer.addAllToCurrentSelection(channelCells);
|
|
304
|
+
};
|
|
305
|
+
eventHandler.bind(ChannelList.events.CHANNEL_SELECT, channelSelect);
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Listens to and updates based on selection changes (specific for seadragon).
|
|
309
|
+
*
|
|
310
|
+
* @param props - may contain cell id
|
|
311
|
+
*/
|
|
312
|
+
function updateSeaDragonSelection(props = {}) {
|
|
313
|
+
if ("picked" in props) {
|
|
314
|
+
seaDragonViewer.pickedIds = props.picked;
|
|
315
|
+
}
|
|
316
|
+
seaDragonViewer.forceRepaint();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Ranges one layer's plugin wants drawn, or {} when there is none.
|
|
321
|
+
*
|
|
322
|
+
* @param name - whose layer to ask. Null means the ACTIVE one, which is what
|
|
323
|
+
* core's own paths want; a plugin's gate flow passes its own name so a
|
|
324
|
+
* tool the user has switched away from still gates its OWN cells rather
|
|
325
|
+
* than whichever layer happens to be active.
|
|
326
|
+
*
|
|
327
|
+
* Core asks the viewer rather than reading a named plugin's state, so these
|
|
328
|
+
* paths work for any plugin -- or none.
|
|
329
|
+
*/
|
|
330
|
+
function cellFilterFor(name = null) {
|
|
331
|
+
const provider = name
|
|
332
|
+
? seaDragonViewer.getCellLayer(name)?.provider
|
|
333
|
+
: seaDragonViewer.cellLayer;
|
|
334
|
+
return provider?.getColorCodedRanges?.() ?? {};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function currentCellFilter() {
|
|
338
|
+
return cellFilterFor(null);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let centroidGateRequest = 0;
|
|
342
|
+
const updateCentroidsForGate = async (name = null) => {
|
|
343
|
+
if (!seaDragonViewer.shouldDrawCentroids()) return;
|
|
344
|
+
const requestId = ++centroidGateRequest;
|
|
345
|
+
seaDragonViewer.setLoading(true);
|
|
346
|
+
try {
|
|
347
|
+
await seaDragonViewer.ensureCentroidsReady(false);
|
|
348
|
+
if (requestId === centroidGateRequest) {
|
|
349
|
+
seaDragonViewer.updateCentroidFilter(cellFilterFor(name), true);
|
|
350
|
+
}
|
|
351
|
+
} finally {
|
|
352
|
+
seaDragonViewer.setLoading(false);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
let segmentationGateRequest = 0;
|
|
356
|
+
const updateSegmentationForGate = async (showSpinner = true, name = undefined) => {
|
|
357
|
+
if (!seaDragonViewer.viewerManagerVMain?.sel_outlines) return;
|
|
358
|
+
const requestId = ++segmentationGateRequest;
|
|
359
|
+
await seaDragonViewer.updateSegmentationFilter(
|
|
360
|
+
cellFilterFor(name ?? null), showSpinner, name);
|
|
361
|
+
if (requestId !== segmentationGateRequest) {
|
|
362
|
+
seaDragonViewer.forceRepaint();
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
// Runs on every move tick, as fast as the network+render round trip allows, rather than
|
|
366
|
+
// waiting on a fixed interval: if a request is still in flight when the next tick arrives,
|
|
367
|
+
// it's marked pending and replayed immediately (with the latest gate values) as soon as the
|
|
368
|
+
// in-flight one finishes, so the mask keeps following the handle continuously while dragging.
|
|
369
|
+
let segmentationGateRunning = false;
|
|
370
|
+
let segmentationGatePending = false;
|
|
371
|
+
const runSegmentationGate = async (showSpinner, name = undefined) => {
|
|
372
|
+
if (!seaDragonViewer.viewerManagerVMain?.sel_outlines) return;
|
|
373
|
+
if (segmentationGateRunning) {
|
|
374
|
+
segmentationGatePending = true;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
segmentationGateRunning = true;
|
|
378
|
+
try {
|
|
379
|
+
await updateSegmentationForGate(showSpinner, name);
|
|
380
|
+
while (segmentationGatePending) {
|
|
381
|
+
segmentationGatePending = false;
|
|
382
|
+
await updateSegmentationForGate(false, name);
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
segmentationGateRunning = false;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
eventHandler.bind(ChannelList.events.BRUSH_MOVE, (d) => {
|
|
390
|
+
const fullName = dataLayer.getFullChannelName(d.name);
|
|
391
|
+
seaDragonViewer.updateChannelRange(fullName, d.dataRange[0], d.dataRange[1]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reset the (core) channel list to its initial values. Add-on modules hook their own
|
|
396
|
+
* reset behavior onto this same event via bindEvents() below.
|
|
397
|
+
*/
|
|
398
|
+
const reset_lists = () => {
|
|
399
|
+
channelList.resetChannelList();
|
|
400
|
+
seaDragonViewer.forceRepaint();
|
|
401
|
+
};
|
|
402
|
+
eventHandler.bind(ChannelList.events.RESET_LISTS, reset_lists);
|
|
403
|
+
|
|
404
|
+
for (const definition of pluginDefs) {
|
|
405
|
+
bindPluginEvents(definition);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (typeof ViewerSidebar !== "undefined" && document.getElementById("viewer_sidebar")) {
|
|
409
|
+
const viewerSidebar = new ViewerSidebar(config, columns, dataLayer, eventHandler, channelList);
|
|
410
|
+
__plexora.viewerSidebar = viewerSidebar;
|
|
411
|
+
for (const definition of pluginDefs) {
|
|
412
|
+
const controller = createPluginSidebar(definition, viewerSidebar);
|
|
413
|
+
if (!controller) continue;
|
|
414
|
+
viewerSidebar.registerModule(controller);
|
|
415
|
+
// Tell toolLoader.js this tool is already live (rendered server-side via
|
|
416
|
+
// a direct/bookmarked ?tool= link) so its close button and any later
|
|
417
|
+
// Tools-menu click work off the real state instead of re-fetching and
|
|
418
|
+
// re-activating a plugin that is already registered.
|
|
419
|
+
if (window.PlexoraToolLoader) {
|
|
420
|
+
const slotIds = Array.from(
|
|
421
|
+
document.querySelectorAll(`[data-tool-mount="${definition.name}"]`)
|
|
422
|
+
).map((el) => el.id);
|
|
423
|
+
window.PlexoraToolLoader.registerLoaded(definition.name, slotIds, controller);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
await viewerSidebar.init(dd);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Segmentation-mask processing (pyramid/outline generation) runs in a
|
|
430
|
+
// background job on upload -- see data_model.start_segmentation_job -- so
|
|
431
|
+
// the viewer can open before it's done. Poll until it's ready (or errors
|
|
432
|
+
// out), then take the finished mask on in place.
|
|
433
|
+
//
|
|
434
|
+
// This used to call location.reload(). On a large mask that job runs for
|
|
435
|
+
// minutes, so the reload landed well into a working session: the viewer
|
|
436
|
+
// went blank and came back at the default viewport with every channel,
|
|
437
|
+
// tool, gate and pan/zoom gone, and nothing on screen said why. It is also
|
|
438
|
+
// avoidable -- see adoptSegmentation below for why nothing needs rebuilding.
|
|
439
|
+
//
|
|
440
|
+
// Each reading is announced as well as acted on. A plugin that is waiting
|
|
441
|
+
// for this mask rather than drawing a substitute (viewerControls'
|
|
442
|
+
// enableCellLayer) has to be able to say how far along it is, and the poll
|
|
443
|
+
// that already knows is the only thing that should be asking the server.
|
|
444
|
+
if (config.segmentation_status === 'pending') {
|
|
445
|
+
const announce = (what, detail) => window.dispatchEvent(
|
|
446
|
+
new CustomEvent(`plexora:segmentation-${what}`, { detail }));
|
|
447
|
+
//: Consecutive polls that came back with nothing. getSegmentationStatus
|
|
448
|
+
//: swallows its own errors and returns undefined, so this is the only
|
|
449
|
+
//: way to tell a dead server from a job that is simply still running.
|
|
450
|
+
let silent = 0;
|
|
451
|
+
const pollSegmentationStatus = async () => {
|
|
452
|
+
const status = await dataLayer.getSegmentationStatus();
|
|
453
|
+
if (status?.status === 'ready') {
|
|
454
|
+
adoptSegmentation(status.segmentation);
|
|
455
|
+
announce('ready', { segmentation: status.segmentation });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (status?.status === 'error') {
|
|
459
|
+
announce('failed', { error: status.error || '' });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (status?.status === 'pending') {
|
|
463
|
+
silent = 0;
|
|
464
|
+
announce('progress', {
|
|
465
|
+
progress: typeof status.progress === 'number' ? status.progress : null,
|
|
466
|
+
message: status.message || '',
|
|
467
|
+
});
|
|
468
|
+
} else if ((silent += 1) > 10) {
|
|
469
|
+
// Long enough that no ordinary hiccup reaches it, short enough
|
|
470
|
+
// that a panel waiting on this loop is not left waiting on a
|
|
471
|
+
// server that is never going to answer.
|
|
472
|
+
announce('failed', { error: '' });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
// One bad answer must not abandon a job that is still running
|
|
476
|
+
// server-side, which is why the count above has to run out first.
|
|
477
|
+
window.setTimeout(pollSegmentationStatus, 1500);
|
|
478
|
+
};
|
|
479
|
+
// Asked straight away rather than after a first interval: something may
|
|
480
|
+
// be showing a progress bar with nothing in it until this answers.
|
|
481
|
+
pollSegmentationStatus();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Take on the mask the background job just finished, without a reload.
|
|
486
|
+
*
|
|
487
|
+
* Nothing about the page is stale except one fact. The "Area" placeholder
|
|
488
|
+
* channel is inserted into imageData when the mask is *attached* (see
|
|
489
|
+
* import_routes.attach_segmentation), not when its pyramid lands, so no
|
|
490
|
+
* channel index shifts here and nothing keyed off one has to be rebuilt.
|
|
491
|
+
* The only thing that changes is config.segmentation going from null to a
|
|
492
|
+
* path -- and the label layer is loaded lazily from that, on demand, by
|
|
493
|
+
* viewerManager.load_label_image().
|
|
494
|
+
*
|
|
495
|
+
* @param path the derived pyramid the job produced.
|
|
496
|
+
*/
|
|
497
|
+
function adoptSegmentation(path) {
|
|
498
|
+
if (!path || config.segmentation) return;
|
|
499
|
+
config.segmentation = path;
|
|
500
|
+
config.segmentation_status = 'ready';
|
|
501
|
+
// Both were set when the viewer opened and found no mask to fetch;
|
|
502
|
+
// clearing them is what lets the layer be requested now. `noLabel` also
|
|
503
|
+
// short-circuits ensureSegmentationReady(), so it has to go first.
|
|
504
|
+
seaDragonViewer.noLabel = false;
|
|
505
|
+
if (seaDragonViewer.viewerManagerVMain) {
|
|
506
|
+
seaDragonViewer.viewerManagerVMain.labelLayerRequested = false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Outlines and Filled were disabled while there was no mask to draw,
|
|
510
|
+
// and now there is one. Done before the swap below so the control is
|
|
511
|
+
// right either way -- a user who is drawing nothing on purpose still
|
|
512
|
+
// gains the option they were previously not offered.
|
|
513
|
+
viewerControls.refreshAvailability();
|
|
514
|
+
|
|
515
|
+
// A tool asked for this mask and was told to wait rather than given
|
|
516
|
+
// centroids instead (viewerControls.enableCellLayer). Nothing is drawing
|
|
517
|
+
// yet, so this is the moment the layer it asked for actually turns on.
|
|
518
|
+
if (seaDragonViewer.cellLayerAwaitingMask) {
|
|
519
|
+
seaDragonViewer.cellLayerAwaitingMask = false;
|
|
520
|
+
viewerControls.selectMode(
|
|
521
|
+
viewerControls.maskMode(viewerControls.ownerMaskPreference()));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Swap the drawing over only when what is showing is the fallback this
|
|
526
|
+
// very absence caused. A user who chose Centroids themselves gets to
|
|
527
|
+
// keep them, and a viewer drawing nothing was drawing nothing on
|
|
528
|
+
// purpose -- neither is a state a finished background job should
|
|
529
|
+
// overrule.
|
|
530
|
+
if (viewerControls.mode !== 'centroids') return;
|
|
531
|
+
if (!seaDragonViewer.centroidsFromFallback) return;
|
|
532
|
+
// Drawn the way whatever holds the cell layer asks for, not always as
|
|
533
|
+
// outlines. A project that gained its mask from the edit page reaches
|
|
534
|
+
// this path rather than enableCellLayer's, and hardcoding outlines here
|
|
535
|
+
// meant the tool's own preference was honoured on every later page load
|
|
536
|
+
// and never on the one where the mask actually arrived.
|
|
537
|
+
viewerControls.selectMode(
|
|
538
|
+
viewerControls.maskMode(viewerControls.ownerMaskPreference()));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Record of one activated plugin. Cleanups let a plugin release globals it
|
|
543
|
+
* registered (window listeners, timers) when it is torn down -- the old
|
|
544
|
+
* single-module world never needed this because the one module lived for
|
|
545
|
+
* the life of the page.
|
|
546
|
+
*/
|
|
547
|
+
function pluginRecord(definition) {
|
|
548
|
+
let record = __plexora.plugins.get(definition.name);
|
|
549
|
+
if (!record) {
|
|
550
|
+
record = { definition, instance: null, sidebarController: null, cleanups: [] };
|
|
551
|
+
__plexora.plugins.set(definition.name, record);
|
|
552
|
+
}
|
|
553
|
+
return record;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** The context every plugin hook receives. */
|
|
557
|
+
function pluginContext(definition, extra = {}) {
|
|
558
|
+
const record = pluginRecord(definition);
|
|
559
|
+
return {
|
|
560
|
+
// What this plugin is given: image data always, plus segmentation
|
|
561
|
+
// and a feature table when the project has them.
|
|
562
|
+
dataset: __plexora.dataset,
|
|
563
|
+
config,
|
|
564
|
+
columns,
|
|
565
|
+
dataLayer,
|
|
566
|
+
eventHandler,
|
|
567
|
+
channelList,
|
|
568
|
+
viewer: seaDragonViewer,
|
|
569
|
+
datasource,
|
|
570
|
+
// Core event names, so a plugin can hook core behaviour without
|
|
571
|
+
// reaching for a concrete core class off window.
|
|
572
|
+
coreEvents: ChannelList.events,
|
|
573
|
+
url: plexoraUrl,
|
|
574
|
+
// Ask the user for something this plugin declared in its Requires
|
|
575
|
+
// but the project has not got. Resolves true once it is recorded --
|
|
576
|
+
// centrally, so another plugin needing the same thing finds it
|
|
577
|
+
// already answered. A plugin should never build its own "type a
|
|
578
|
+
// column name" input; declare the requirement and call this.
|
|
579
|
+
requirements: {
|
|
580
|
+
require: (keys) => window.PlexoraRequirements.require(
|
|
581
|
+
datasource, definition.name, keys),
|
|
582
|
+
},
|
|
583
|
+
onCleanup: (fn) => record.cleanups.push(fn),
|
|
584
|
+
instance: record.instance,
|
|
585
|
+
...extra,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function activatePluginInstance(definition, databaseDescription) {
|
|
590
|
+
const record = pluginRecord(definition);
|
|
591
|
+
record.instance = definition.createInstance
|
|
592
|
+
? definition.createInstance(pluginContext(definition))
|
|
593
|
+
: null;
|
|
594
|
+
|
|
595
|
+
// Only a plugin that says it colours cells gets a layer. Registering is
|
|
596
|
+
// explicit because it is what puts a card in the sidebar and a pass in
|
|
597
|
+
// the compositor -- see ImageViewer.registerCellLayer.
|
|
598
|
+
if (record.instance && definition.ownsCellLayer) {
|
|
599
|
+
record.instance.pluginName = definition.name;
|
|
600
|
+
// Carried on the provider, so core can ask the viewer who holds the
|
|
601
|
+
// layer instead of learning plugin names. The paths that turn the
|
|
602
|
+
// mask on without a plugin activating -- a pyramid finishing
|
|
603
|
+
// conversion mid-session -- read it from there.
|
|
604
|
+
record.instance.preferredCellMode = definition.preferredCellMode || null;
|
|
605
|
+
seaDragonViewer.registerCellLayer(definition.name, record.instance, {
|
|
606
|
+
supportedModes: definition.supportedCellModes || null,
|
|
607
|
+
});
|
|
608
|
+
// The shared Cells control follows whichever layer is active, and
|
|
609
|
+
// this one just became it.
|
|
610
|
+
viewerControls.syncToActiveLayer();
|
|
611
|
+
// Nothing is drawn over the image until something needs it -- this
|
|
612
|
+
// is that moment. WHICH layer is the project's recorded choice, not
|
|
613
|
+
// this plugin's: the next plugin to register must get the same
|
|
614
|
+
// answer. HOW the mask is drawn -- filled or outlines -- is the
|
|
615
|
+
// plugin's, because it depends on what it is showing. See
|
|
616
|
+
// viewerControls.enableCellLayer.
|
|
617
|
+
viewerControls.enableCellLayer(definition.preferredCellMode, definition.name);
|
|
618
|
+
}
|
|
619
|
+
if (record.instance?.init) {
|
|
620
|
+
record.instance.init(databaseDescription, seaDragonViewer);
|
|
621
|
+
}
|
|
622
|
+
return record.instance;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function bindPluginEvents(definition) {
|
|
626
|
+
if (!definition.bindEvents) return;
|
|
627
|
+
// The two gate actions are bound to THIS plugin's own layer. A plugin
|
|
628
|
+
// that gates cells while another tool is the active one must move its
|
|
629
|
+
// own cells, not the active layer's -- passing the name here is what
|
|
630
|
+
// keeps a plugin from having to know it exists.
|
|
631
|
+
definition.bindEvents(pluginContext(definition, {
|
|
632
|
+
seaDragonViewer,
|
|
633
|
+
moduleInstance: pluginRecord(definition).instance,
|
|
634
|
+
updateSeaDragonSelection,
|
|
635
|
+
updateCentroidsForGate: () => updateCentroidsForGate(definition.name),
|
|
636
|
+
runSegmentationGate: (showSpinner) => runSegmentationGate(showSpinner, definition.name),
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function createPluginSidebar(definition, sidebar) {
|
|
641
|
+
if (!definition.createSidebarController) return null;
|
|
642
|
+
const record = pluginRecord(definition);
|
|
643
|
+
const controller = definition.createSidebarController(pluginContext(definition, {
|
|
644
|
+
sidebar,
|
|
645
|
+
moduleInstance: record.instance,
|
|
646
|
+
}));
|
|
647
|
+
record.sidebarController = controller || null;
|
|
648
|
+
return controller || null;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Activate a plugin discovered after boot (see toolLoader.js), once the
|
|
653
|
+
* viewer, dataLayer and ViewerSidebar are already up. The boot path above
|
|
654
|
+
* is left running its own sequence rather than routed through here: its
|
|
655
|
+
* timing is load-order sensitive (instance.init() must precede tile
|
|
656
|
+
* loading), and both paths end in the same registered state.
|
|
657
|
+
*/
|
|
658
|
+
__plexora.activatePlugin = async function activatePlugin(definition) {
|
|
659
|
+
const instance = activatePluginInstance(definition, __plexora.databaseDescription);
|
|
660
|
+
bindPluginEvents(definition);
|
|
661
|
+
|
|
662
|
+
let sidebarController = null;
|
|
663
|
+
if (__plexora.viewerSidebar) {
|
|
664
|
+
sidebarController = createPluginSidebar(definition, __plexora.viewerSidebar);
|
|
665
|
+
if (sidebarController) {
|
|
666
|
+
await __plexora.viewerSidebar.registerModuleLate(sidebarController);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return { moduleInstance: instance, instance, sidebarController };
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Tear a plugin down: run its cleanups, drop its cell layer, unhook its
|
|
674
|
+
* sidebar controller, and forget it. Without this a deactivated plugin
|
|
675
|
+
* leaves window-level listeners behind and a dead controller still receiving
|
|
676
|
+
* the sidebar's lifecycle calls -- neither of which could happen while a
|
|
677
|
+
* plugin lived for the life of the page.
|
|
678
|
+
*/
|
|
679
|
+
__plexora.deactivatePlugin = function deactivatePlugin(name) {
|
|
680
|
+
const record = __plexora.plugins.get(name);
|
|
681
|
+
if (!record) return false;
|
|
682
|
+
for (const fn of record.cleanups) {
|
|
683
|
+
try {
|
|
684
|
+
fn();
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error(`Plexora: cleanup failed for plugin "${name}"`, error);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
record.definition.destroy?.();
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error(`Plexora: destroy() failed for plugin "${name}"`, error);
|
|
693
|
+
}
|
|
694
|
+
if (record.sidebarController) {
|
|
695
|
+
__plexora.viewerSidebar?.unregisterModule?.(record.sidebarController);
|
|
696
|
+
}
|
|
697
|
+
if (seaDragonViewer.unregisterCellLayer(name)) {
|
|
698
|
+
viewerControls.refreshLayerSurfaces();
|
|
699
|
+
}
|
|
700
|
+
__plexora.plugins.delete(name);
|
|
701
|
+
viewerControls.syncToActiveLayer();
|
|
702
|
+
return true;
|
|
703
|
+
};
|
|
704
|
+
}
|