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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rgbImageViewer.js
|
|
3
|
+
*
|
|
4
|
+
* @class RgbImageViewer -- minimal pan/zoom viewer for a flat RGB quick-view
|
|
5
|
+
* datasource (config.image_kind === 'rgb', see main.js's init()). Kept as
|
|
6
|
+
* its own small class rather than a mode flag inside ImageViewer: that
|
|
7
|
+
* class unconditionally builds one grayscale transfer function per channel
|
|
8
|
+
* and a custom WebGL colorize pass (see its constructor) -- none of that
|
|
9
|
+
* applies to a flat image with imageData: [], so it's simpler and safer to
|
|
10
|
+
* hand OpenSeadragon its native single-image tile source directly and skip
|
|
11
|
+
* DataLayer/ChannelList/ViewerSidebar/gating entirely.
|
|
12
|
+
*/
|
|
13
|
+
class RgbImageViewer {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async init() {
|
|
19
|
+
this.viewer = OpenSeadragon({
|
|
20
|
+
id: "openseadragon",
|
|
21
|
+
prefixUrl: plexoraUrl("client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/"),
|
|
22
|
+
tileSources: {
|
|
23
|
+
type: "image",
|
|
24
|
+
url: plexoraUrl(`generated/rgb/${datasource}`),
|
|
25
|
+
},
|
|
26
|
+
minZoomImageRatio: 0.9,
|
|
27
|
+
visibilityRatio: 1,
|
|
28
|
+
homeFillsViewer: false,
|
|
29
|
+
showNavigator: false,
|
|
30
|
+
});
|
|
31
|
+
window.PlexoraStatus?.watchViewer(this.viewer);
|
|
32
|
+
const loader = document.getElementById("openseadragon_loader");
|
|
33
|
+
this.viewer.addHandler("open", () => {
|
|
34
|
+
if (loader) {
|
|
35
|
+
loader.style.display = "none";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
this.initProjectLabel();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
initProjectLabel() {
|
|
42
|
+
const wrapper = document.getElementById("openseadragon_wrapper");
|
|
43
|
+
if (!wrapper || document.getElementById("viewer_project_label")) return;
|
|
44
|
+
const label = document.createElement("div");
|
|
45
|
+
label.id = "viewer_project_label";
|
|
46
|
+
label.className = "viewer-project-label";
|
|
47
|
+
label.textContent = datasource || "";
|
|
48
|
+
wrapper.appendChild(label);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
downloadCurrentView(format = "png") {
|
|
52
|
+
const canvas = this.viewer?.drawer?.canvas;
|
|
53
|
+
if (!canvas) return;
|
|
54
|
+
|
|
55
|
+
if (format === "pdf") {
|
|
56
|
+
const pdf = new jsPDF({
|
|
57
|
+
orientation: canvas.width >= canvas.height ? "landscape" : "portrait",
|
|
58
|
+
unit: "px",
|
|
59
|
+
format: [canvas.width, canvas.height],
|
|
60
|
+
});
|
|
61
|
+
pdf.addImage(canvas.toDataURL("image/png"), "PNG", 0, 0, canvas.width, canvas.height);
|
|
62
|
+
// No channels/scale bar on a flat RGB image -- the project label
|
|
63
|
+
// is the only overlay, drawn as real vector text/shape rather
|
|
64
|
+
// than baked into the image pixels (matches ImageViewer's PDF
|
|
65
|
+
// export, see imageViewer.js's drawProjectLabelVector).
|
|
66
|
+
this.drawProjectLabelVector(pdf);
|
|
67
|
+
pdf.save(`${datasource || "plexora"}_current_view.pdf`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const link = document.createElement("a");
|
|
71
|
+
link.download = `${datasource || "plexora"}_current_view.png`;
|
|
72
|
+
link.href = canvas.toDataURL("image/png");
|
|
73
|
+
document.body.appendChild(link);
|
|
74
|
+
link.click();
|
|
75
|
+
document.body.removeChild(link);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getOverlayRectInCanvasSpace(el) {
|
|
79
|
+
const canvasEl = this.viewer?.drawer?.canvas;
|
|
80
|
+
if (!canvasEl || !el) return null;
|
|
81
|
+
const canvasRect = canvasEl.getBoundingClientRect();
|
|
82
|
+
const elRect = el.getBoundingClientRect();
|
|
83
|
+
if (!canvasRect.width || !canvasRect.height) return null;
|
|
84
|
+
const scaleX = canvasEl.width / canvasRect.width;
|
|
85
|
+
const scaleY = canvasEl.height / canvasRect.height;
|
|
86
|
+
return {
|
|
87
|
+
x: (elRect.left - canvasRect.left) * scaleX,
|
|
88
|
+
y: (elRect.top - canvasRect.top) * scaleY,
|
|
89
|
+
width: elRect.width * scaleX,
|
|
90
|
+
height: elRect.height * scaleY,
|
|
91
|
+
scale: scaleX,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
drawProjectLabelVector(pdf) {
|
|
96
|
+
const labelEl = document.getElementById("viewer_project_label");
|
|
97
|
+
if (!labelEl) return;
|
|
98
|
+
const rect = this.getOverlayRectInCanvasSpace(labelEl);
|
|
99
|
+
if (!rect || !rect.width) return;
|
|
100
|
+
|
|
101
|
+
pdf.saveGraphicsState();
|
|
102
|
+
pdf.setGState(new pdf.GState({ opacity: 0.86 }));
|
|
103
|
+
pdf.setFillColor(17, 24, 39);
|
|
104
|
+
pdf.roundedRect(rect.x, rect.y, rect.width, rect.height, 3 * rect.scale, 3 * rect.scale, "F");
|
|
105
|
+
pdf.restoreGraphicsState();
|
|
106
|
+
|
|
107
|
+
pdf.setFont("helvetica", "bold");
|
|
108
|
+
pdf.setFontSize(Math.max(6, 13 * rect.scale));
|
|
109
|
+
pdf.setTextColor(241, 245, 249);
|
|
110
|
+
pdf.text(labelEl.textContent || "", rect.x + rect.width / 2, rect.y + rect.height / 2, {
|
|
111
|
+
align: "center",
|
|
112
|
+
baseline: "middle",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class SearchableSelect - lightweight typeahead combobox replacing a plain <select>.
|
|
3
|
+
* Mounted into a container element; filters its option list live as the user types.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes, because two different jobs:
|
|
6
|
+
*
|
|
7
|
+
* trigger: "input" (the default) -- the field IS the combobox. It shows the
|
|
8
|
+
* current value, and typing over it filters. Right where the control is
|
|
9
|
+
* the only thing on its line and the value is what you want to read.
|
|
10
|
+
*
|
|
11
|
+
* trigger: "button" -- a button showing the current value, with the search
|
|
12
|
+
* field inside the dropdown it opens. Narrower, because it is sized to
|
|
13
|
+
* its label rather than to a text field, so it can share a line with
|
|
14
|
+
* other controls; and the search is where somebody looking for a search
|
|
15
|
+
* box would look, which a field that doubles as the value display is
|
|
16
|
+
* not -- there is nothing about it that says it can be typed into.
|
|
17
|
+
*
|
|
18
|
+
* Both filter the same list, report through the same onChange, and are reached
|
|
19
|
+
* the same way from the keyboard.
|
|
20
|
+
*/
|
|
21
|
+
class SearchableSelect {
|
|
22
|
+
constructor(mount, options = {}) {
|
|
23
|
+
this.mount = mount;
|
|
24
|
+
this.options = [...(options.options || [])];
|
|
25
|
+
this.value = options.value || "";
|
|
26
|
+
this.placeholder = options.placeholder || "Search…";
|
|
27
|
+
//: What the search field inside a button-triggered menu says. The
|
|
28
|
+
//: trigger's own label is the current value, so the two cannot share
|
|
29
|
+
//: one string.
|
|
30
|
+
this.searchPlaceholder = options.searchPlaceholder || this.placeholder;
|
|
31
|
+
//: Shown on a button trigger that has no value yet.
|
|
32
|
+
this.emptyLabel = options.emptyLabel || this.placeholder;
|
|
33
|
+
this.describeOption = options.describeOption || (() => "");
|
|
34
|
+
// Optional per-option status dot (e.g. "this marker already has a
|
|
35
|
+
// gate set") -- separate from describeOption's always-visible text
|
|
36
|
+
// hint, since this is a lightweight visual cue whose detail only
|
|
37
|
+
// needs to show up on hover (title attribute), not inline text.
|
|
38
|
+
this.getIndicator = options.getIndicator || null;
|
|
39
|
+
// What an exhausted filter says. Defaulted to the marker wording this
|
|
40
|
+
// widget was written for, so the channel rows and the gate picker read
|
|
41
|
+
// as they always did.
|
|
42
|
+
this.emptyText = options.emptyText || "No markers match";
|
|
43
|
+
// For callers whose visible <label> cannot use `for` -- this mounts a
|
|
44
|
+
// generated input, so there is no stable id to point one at.
|
|
45
|
+
this.ariaLabel = options.ariaLabel || "";
|
|
46
|
+
this.onChange = options.onChange || (() => {});
|
|
47
|
+
this.trigger = options.trigger === "button" ? "button" : "input";
|
|
48
|
+
this.filtered = [...this.options];
|
|
49
|
+
this.activeIndex = -1;
|
|
50
|
+
this.isOpen = false;
|
|
51
|
+
this.render();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render() {
|
|
55
|
+
this.mount.innerHTML = "";
|
|
56
|
+
this.mount.classList.add("marker-combobox");
|
|
57
|
+
|
|
58
|
+
this.menu = document.createElement("div");
|
|
59
|
+
this.menu.className = "marker-combobox-menu";
|
|
60
|
+
this.menu.hidden = true;
|
|
61
|
+
// Appended to <body> (a positioning "portal"), not this.mount: a disabled/dimmed
|
|
62
|
+
// ancestor row has opacity < 1, which creates its own CSS stacking context and
|
|
63
|
+
// would trap this menu's z-index behind the *next* row otherwise.
|
|
64
|
+
document.body.appendChild(this.menu);
|
|
65
|
+
|
|
66
|
+
if (this.trigger === "button") {
|
|
67
|
+
this.renderButtonTrigger();
|
|
68
|
+
} else {
|
|
69
|
+
this.renderInputTrigger();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// `field` is whatever the typing goes into and `anchor` is what the
|
|
73
|
+
// menu hangs off. They are the same element for an input trigger and
|
|
74
|
+
// deliberately different for a button one, which is the whole of the
|
|
75
|
+
// difference between the two shapes.
|
|
76
|
+
this.field.addEventListener("input", () => this.filter(this.field.value));
|
|
77
|
+
this.field.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
78
|
+
this.field.addEventListener("blur", () => {
|
|
79
|
+
window.setTimeout(() => {
|
|
80
|
+
const active = document.activeElement;
|
|
81
|
+
if (active === this.field || this.menu?.contains(active)
|
|
82
|
+
|| active === this.anchor) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.close();
|
|
86
|
+
if (this.trigger === "input") this.field.value = this.value;
|
|
87
|
+
}, 120);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The classic shape: one field that is both the value and the search. */
|
|
92
|
+
renderInputTrigger() {
|
|
93
|
+
this.input = document.createElement("input");
|
|
94
|
+
this.input.type = "text";
|
|
95
|
+
this.input.className = "sidebar-select marker-combobox-input";
|
|
96
|
+
this.input.autocomplete = "off";
|
|
97
|
+
this.input.spellcheck = false;
|
|
98
|
+
this.input.placeholder = this.placeholder;
|
|
99
|
+
this.input.setAttribute("role", "combobox");
|
|
100
|
+
this.input.setAttribute("aria-expanded", "false");
|
|
101
|
+
this.input.setAttribute("aria-autocomplete", "list");
|
|
102
|
+
if (this.ariaLabel) this.input.setAttribute("aria-label", this.ariaLabel);
|
|
103
|
+
this.input.value = this.value;
|
|
104
|
+
this.mount.appendChild(this.input);
|
|
105
|
+
|
|
106
|
+
this.input.addEventListener("focus", () => this.open(true));
|
|
107
|
+
this.input.addEventListener("click", () => this.open(true));
|
|
108
|
+
|
|
109
|
+
this.anchor = this.input;
|
|
110
|
+
this.field = this.input;
|
|
111
|
+
this.menu.setAttribute("role", "listbox");
|
|
112
|
+
this.list = this.menu;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The compact shape: a button, with the search inside the dropdown. */
|
|
116
|
+
renderButtonTrigger() {
|
|
117
|
+
this.button = document.createElement("button");
|
|
118
|
+
this.button.type = "button";
|
|
119
|
+
this.button.className = "sidebar-select marker-combobox-trigger";
|
|
120
|
+
this.button.setAttribute("aria-haspopup", "listbox");
|
|
121
|
+
this.button.setAttribute("aria-expanded", "false");
|
|
122
|
+
if (this.ariaLabel) {
|
|
123
|
+
this.button.setAttribute("aria-label", this.ariaLabel);
|
|
124
|
+
this.button.title = this.ariaLabel;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.buttonLabel = document.createElement("span");
|
|
128
|
+
this.buttonLabel.className = "marker-combobox-trigger-label";
|
|
129
|
+
const caret = document.createElement("span");
|
|
130
|
+
caret.className = "fas fa-chevron-down marker-combobox-caret";
|
|
131
|
+
caret.setAttribute("aria-hidden", "true");
|
|
132
|
+
this.button.append(this.buttonLabel, caret);
|
|
133
|
+
this.button.addEventListener("click", () => this.toggle());
|
|
134
|
+
this.mount.appendChild(this.button);
|
|
135
|
+
|
|
136
|
+
this.menu.classList.add("has-search");
|
|
137
|
+
const searchRow = document.createElement("div");
|
|
138
|
+
searchRow.className = "marker-combobox-searchbox";
|
|
139
|
+
const icon = document.createElement("span");
|
|
140
|
+
icon.className = "fas fa-magnifying-glass";
|
|
141
|
+
icon.setAttribute("aria-hidden", "true");
|
|
142
|
+
this.field = document.createElement("input");
|
|
143
|
+
this.field.type = "text";
|
|
144
|
+
this.field.className = "marker-combobox-search";
|
|
145
|
+
this.field.autocomplete = "off";
|
|
146
|
+
this.field.spellcheck = false;
|
|
147
|
+
this.field.placeholder = this.searchPlaceholder;
|
|
148
|
+
this.field.setAttribute("role", "combobox");
|
|
149
|
+
this.field.setAttribute("aria-expanded", "true");
|
|
150
|
+
this.field.setAttribute("aria-autocomplete", "list");
|
|
151
|
+
this.field.setAttribute("aria-label", this.ariaLabel || this.searchPlaceholder);
|
|
152
|
+
searchRow.append(icon, this.field);
|
|
153
|
+
|
|
154
|
+
this.list = document.createElement("div");
|
|
155
|
+
this.list.className = "marker-combobox-list";
|
|
156
|
+
this.list.setAttribute("role", "listbox");
|
|
157
|
+
this.menu.append(searchRow, this.list);
|
|
158
|
+
|
|
159
|
+
this.anchor = this.button;
|
|
160
|
+
this.paintTrigger();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The button's label: the current value, or what to pick if there is none. */
|
|
164
|
+
paintTrigger() {
|
|
165
|
+
if (!this.buttonLabel) return;
|
|
166
|
+
this.buttonLabel.textContent = this.value || this.emptyLabel;
|
|
167
|
+
this.buttonLabel.classList.toggle("is-placeholder", !this.value);
|
|
168
|
+
// The value can be far wider than the button, which truncates it -- so
|
|
169
|
+
// the full text is on the title, where the label alone would leave a
|
|
170
|
+
// column name unreadable in a narrow sidebar.
|
|
171
|
+
this.button.title = this.value
|
|
172
|
+
? (this.ariaLabel ? `${this.ariaLabel}: ${this.value}` : this.value)
|
|
173
|
+
: (this.ariaLabel || this.emptyLabel);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setOptions(names) {
|
|
177
|
+
this.options = [...names];
|
|
178
|
+
if (this.isOpen) {
|
|
179
|
+
this.filter(this.field.value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setValue(name) {
|
|
184
|
+
this.value = name || "";
|
|
185
|
+
if (this.trigger === "button") {
|
|
186
|
+
this.paintTrigger();
|
|
187
|
+
} else {
|
|
188
|
+
this.field.value = this.value;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
filter(query) {
|
|
193
|
+
const q = query.trim().toLowerCase();
|
|
194
|
+
this.filtered = q ? this.options.filter((name) => name.toLowerCase().includes(q)) : [...this.options];
|
|
195
|
+
this.activeIndex = this.filtered.length ? 0 : -1;
|
|
196
|
+
this.renderMenu();
|
|
197
|
+
this.open(false);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
toggle() {
|
|
201
|
+
if (this.isOpen) {
|
|
202
|
+
this.close();
|
|
203
|
+
} else {
|
|
204
|
+
this.open(true);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
open(reset) {
|
|
209
|
+
if (reset) {
|
|
210
|
+
this.filtered = [...this.options];
|
|
211
|
+
this.activeIndex = Math.max(this.options.indexOf(this.value), 0);
|
|
212
|
+
this.renderMenu();
|
|
213
|
+
if (this.trigger === "button") {
|
|
214
|
+
// Opened empty rather than seeded with the current value: this
|
|
215
|
+
// field is a search, not the value, and a query already in it
|
|
216
|
+
// would show one match and look like the list.
|
|
217
|
+
this.field.value = "";
|
|
218
|
+
this.field.focus();
|
|
219
|
+
} else {
|
|
220
|
+
this.field.select();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.isOpen = true;
|
|
224
|
+
this.positionMenu();
|
|
225
|
+
this.menu.hidden = false;
|
|
226
|
+
requestAnimationFrame(() => this.menu.classList.add("is-open"));
|
|
227
|
+
this.anchor.setAttribute("aria-expanded", "true");
|
|
228
|
+
this.bindDismissListeners();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
positionMenu() {
|
|
232
|
+
const rect = this.anchor.getBoundingClientRect();
|
|
233
|
+
this.menu.style.left = `${rect.left}px`;
|
|
234
|
+
this.menu.style.top = `${rect.bottom + 4}px`;
|
|
235
|
+
// A button trigger is sized to its label, which can be narrower than
|
|
236
|
+
// the option names it is choosing between -- so the menu takes the
|
|
237
|
+
// wider of the two rather than inheriting a width nothing fits in.
|
|
238
|
+
this.menu.style.width = this.trigger === "button"
|
|
239
|
+
? `${Math.max(rect.width, 240)}px`
|
|
240
|
+
: `${rect.width}px`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
close() {
|
|
244
|
+
this.isOpen = false;
|
|
245
|
+
if (!this.menu) return;
|
|
246
|
+
this.menu.classList.remove("is-open");
|
|
247
|
+
this.anchor.setAttribute("aria-expanded", "false");
|
|
248
|
+
window.setTimeout(() => {
|
|
249
|
+
// The menu may have been destroyed inside this delay -- closing is
|
|
250
|
+
// the first thing destroy() does, and the blur handler also lands
|
|
251
|
+
// here on its own timer.
|
|
252
|
+
if (!this.isOpen && this.menu) this.menu.hidden = true;
|
|
253
|
+
}, 150);
|
|
254
|
+
this.unbindDismissListeners();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
bindDismissListeners() {
|
|
258
|
+
if (this._dismissScroll) return;
|
|
259
|
+
// Follows the trigger rather than shutting on any scroll at all.
|
|
260
|
+
//
|
|
261
|
+
// Closing was the cheap way to stop a body-portaled menu lingering at a
|
|
262
|
+
// stale position, and the cure was worse: a wheel the option list could
|
|
263
|
+
// not absorb -- at either end of it, or over the search box, which does
|
|
264
|
+
// not scroll -- reached the panel behind, so the menu being read
|
|
265
|
+
// vanished while the page moved under it. Repositioning keeps the
|
|
266
|
+
// promise (the menu is never somewhere its trigger is not) without
|
|
267
|
+
// taking the menu away from someone using it.
|
|
268
|
+
//
|
|
269
|
+
// Scrolling the menu's own list is not a page scroll and is ignored.
|
|
270
|
+
this._dismissScroll = (event) => {
|
|
271
|
+
if (event.target === this.menu || this.menu.contains(event.target)) return;
|
|
272
|
+
this.positionMenu();
|
|
273
|
+
// Scrolled out of view is the one case with nothing left to follow.
|
|
274
|
+
const rect = this.anchor.getBoundingClientRect();
|
|
275
|
+
const limit = window.innerHeight || document.documentElement?.clientHeight;
|
|
276
|
+
if (limit && (rect.bottom < 0 || rect.top > limit)) this.close();
|
|
277
|
+
};
|
|
278
|
+
this._dismissResize = () => this.close();
|
|
279
|
+
// A button trigger takes focus itself, so its menu cannot rely on the
|
|
280
|
+
// field's blur alone -- clicking anywhere else on the page has to shut
|
|
281
|
+
// it, the same way every other popover in the viewer behaves.
|
|
282
|
+
this._dismissClick = (event) => {
|
|
283
|
+
if (!this.isOpen) return;
|
|
284
|
+
if (this.menu.contains(event.target) || this.mount.contains(event.target)) return;
|
|
285
|
+
this.close();
|
|
286
|
+
};
|
|
287
|
+
window.addEventListener("scroll", this._dismissScroll, true);
|
|
288
|
+
window.addEventListener("resize", this._dismissResize);
|
|
289
|
+
document.addEventListener("click", this._dismissClick);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
unbindDismissListeners() {
|
|
293
|
+
if (!this._dismissScroll) return;
|
|
294
|
+
window.removeEventListener("scroll", this._dismissScroll, true);
|
|
295
|
+
window.removeEventListener("resize", this._dismissResize);
|
|
296
|
+
document.removeEventListener("click", this._dismissClick);
|
|
297
|
+
this._dismissScroll = null;
|
|
298
|
+
this._dismissResize = null;
|
|
299
|
+
this._dismissClick = null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
renderMenu() {
|
|
303
|
+
this.list.innerHTML = "";
|
|
304
|
+
if (!this.filtered.length) {
|
|
305
|
+
const empty = document.createElement("div");
|
|
306
|
+
empty.className = "marker-combobox-empty";
|
|
307
|
+
empty.textContent = this.emptyText;
|
|
308
|
+
this.list.appendChild(empty);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.filtered.forEach((name, index) => {
|
|
312
|
+
const option = document.createElement("div");
|
|
313
|
+
option.className = "marker-combobox-option";
|
|
314
|
+
option.setAttribute("role", "option");
|
|
315
|
+
option.dataset.value = name;
|
|
316
|
+
if (name === this.value) option.classList.add("is-selected");
|
|
317
|
+
if (index === this.activeIndex) option.classList.add("is-active");
|
|
318
|
+
|
|
319
|
+
if (this.getIndicator) {
|
|
320
|
+
const indicatorTitle = this.getIndicator(name);
|
|
321
|
+
if (indicatorTitle) {
|
|
322
|
+
const dot = document.createElement("span");
|
|
323
|
+
dot.className = "marker-combobox-option-indicator";
|
|
324
|
+
dot.title = indicatorTitle;
|
|
325
|
+
option.appendChild(dot);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const label = document.createElement("span");
|
|
330
|
+
label.className = "marker-combobox-option-label";
|
|
331
|
+
label.textContent = name;
|
|
332
|
+
option.appendChild(label);
|
|
333
|
+
|
|
334
|
+
const hint = this.describeOption(name);
|
|
335
|
+
if (hint) {
|
|
336
|
+
const tag = document.createElement("span");
|
|
337
|
+
tag.className = "marker-combobox-hint";
|
|
338
|
+
tag.textContent = hint;
|
|
339
|
+
option.appendChild(tag);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
option.addEventListener("mousedown", (event) => {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
this.selectOption(name);
|
|
345
|
+
});
|
|
346
|
+
this.list.appendChild(option);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
selectOption(name) {
|
|
351
|
+
this.setValue(name);
|
|
352
|
+
this.close();
|
|
353
|
+
// Focus goes back to what opened the menu, or a keyboard user is left
|
|
354
|
+
// on an element that is no longer visible.
|
|
355
|
+
if (this.trigger === "button") this.button.focus();
|
|
356
|
+
this.onChange(name);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
handleKeydown(event) {
|
|
360
|
+
if (!this.isOpen && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
361
|
+
event.preventDefault();
|
|
362
|
+
this.open(true);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!this.isOpen) return;
|
|
366
|
+
if (event.key === "ArrowDown") {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
this.activeIndex = Math.min(this.activeIndex + 1, this.filtered.length - 1);
|
|
369
|
+
this.renderMenu();
|
|
370
|
+
this.scrollActiveIntoView();
|
|
371
|
+
} else if (event.key === "ArrowUp") {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
this.activeIndex = Math.max(this.activeIndex - 1, 0);
|
|
374
|
+
this.renderMenu();
|
|
375
|
+
this.scrollActiveIntoView();
|
|
376
|
+
} else if (event.key === "Enter") {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
const active = this.filtered[this.activeIndex];
|
|
379
|
+
if (active) this.selectOption(active);
|
|
380
|
+
} else if (event.key === "Escape") {
|
|
381
|
+
this.close();
|
|
382
|
+
if (this.trigger === "button") {
|
|
383
|
+
this.button.focus();
|
|
384
|
+
} else {
|
|
385
|
+
this.field.value = this.value;
|
|
386
|
+
this.field.blur();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
scrollActiveIntoView() {
|
|
392
|
+
const active = this.menu.querySelector(".marker-combobox-option.is-active");
|
|
393
|
+
if (active) active.scrollIntoView({ block: "nearest" });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Take this combobox off the page for good.
|
|
398
|
+
*
|
|
399
|
+
* The menu is portaled onto <body> (see render), so removing the element it
|
|
400
|
+
* was mounted into leaves the menu behind -- a panel that is torn down and
|
|
401
|
+
* rebuilt otherwise strands one orphan per rebuild, and a stale one can
|
|
402
|
+
* still be opened by keyboard.
|
|
403
|
+
*/
|
|
404
|
+
destroy() {
|
|
405
|
+
this.close();
|
|
406
|
+
this.unbindDismissListeners();
|
|
407
|
+
this.menu?.remove();
|
|
408
|
+
this.menu = null;
|
|
409
|
+
this.list = null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared step-2 wait for segmentation-mask generation.
|
|
3
|
+
*
|
|
4
|
+
* Both import flows start the mask job when their first form page is submitted
|
|
5
|
+
* (see data_model.start_segmentation_job), so by the time the user finishes the
|
|
6
|
+
* second page the conversion is usually well underway or already done. This
|
|
7
|
+
* shows what is left of it rather than dropping the user into a viewer whose
|
|
8
|
+
* cell layer would pop in some seconds later.
|
|
9
|
+
*
|
|
10
|
+
* Loaded as a plain <script>, like the pages that use it, so it exports one
|
|
11
|
+
* global rather than an ES module binding.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Take over the page with a progress panel until the mask is ready, then go to
|
|
16
|
+
* `redirectUrl`.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options
|
|
19
|
+
* @param {string} options.datasource Dataset name to poll for.
|
|
20
|
+
* @param {string} options.redirectUrl Where to go once the mask is ready.
|
|
21
|
+
* @param {number} [options.pollIntervalMs=1000]
|
|
22
|
+
*/
|
|
23
|
+
function awaitSegmentationThenOpen(options) {
|
|
24
|
+
const datasource = options.datasource;
|
|
25
|
+
const redirectUrl = options.redirectUrl;
|
|
26
|
+
const pollIntervalMs = options.pollIntervalMs || 1000;
|
|
27
|
+
|
|
28
|
+
const panel = renderPanel();
|
|
29
|
+
let stopped = false;
|
|
30
|
+
|
|
31
|
+
function go() {
|
|
32
|
+
stopped = true;
|
|
33
|
+
window.location = redirectUrl;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function poll() {
|
|
37
|
+
if (stopped) return;
|
|
38
|
+
fetch(plexoraUrl('get_segmentation_status') + '?' + new URLSearchParams({datasource: datasource}))
|
|
39
|
+
.then(function (response) { return response.json(); })
|
|
40
|
+
.then(function (status) {
|
|
41
|
+
if (stopped) return;
|
|
42
|
+
if (!status || status.status === 'ready') {
|
|
43
|
+
panel.setProgress(100, 'Segmentation mask ready');
|
|
44
|
+
// Let the finished bar actually render before navigating,
|
|
45
|
+
// otherwise a fast conversion just looks like a flicker.
|
|
46
|
+
window.setTimeout(go, 350);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (status.status === 'error') {
|
|
50
|
+
panel.setError(status.error, go);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
panel.setProgress(
|
|
54
|
+
typeof status.progress === 'number' ? status.progress : null,
|
|
55
|
+
// The server's message says which kind of mask is being
|
|
56
|
+
// built and, when the supplied one could not be used
|
|
57
|
+
// as-is, which requirement it missed.
|
|
58
|
+
status.message || 'Converting segmentation mask'
|
|
59
|
+
);
|
|
60
|
+
window.setTimeout(poll, pollIntervalMs);
|
|
61
|
+
})
|
|
62
|
+
.catch(function () {
|
|
63
|
+
// A transient fetch failure shouldn't abandon a job that is
|
|
64
|
+
// still running server-side -- keep polling.
|
|
65
|
+
if (!stopped) window.setTimeout(poll, pollIntervalMs);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
poll();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderPanel() {
|
|
73
|
+
const overlay = document.createElement('div');
|
|
74
|
+
overlay.className = 'segmentation-progress-overlay';
|
|
75
|
+
overlay.setAttribute('role', 'status');
|
|
76
|
+
overlay.setAttribute('aria-live', 'polite');
|
|
77
|
+
overlay.innerHTML = [
|
|
78
|
+
'<div class="segmentation-progress-card">',
|
|
79
|
+
' <div class="segmentation-progress-title">Preparing segmentation mask</div>',
|
|
80
|
+
' <div class="segmentation-progress-detail">Converting segmentation mask</div>',
|
|
81
|
+
' <div class="segmentation-progress-track">',
|
|
82
|
+
' <div class="segmentation-progress-fill is-indeterminate"></div>',
|
|
83
|
+
' </div>',
|
|
84
|
+
' <div class="segmentation-progress-actions"></div>',
|
|
85
|
+
'</div>',
|
|
86
|
+
].join('');
|
|
87
|
+
document.body.appendChild(overlay);
|
|
88
|
+
|
|
89
|
+
const detail = overlay.querySelector('.segmentation-progress-detail');
|
|
90
|
+
const fill = overlay.querySelector('.segmentation-progress-fill');
|
|
91
|
+
const title = overlay.querySelector('.segmentation-progress-title');
|
|
92
|
+
const actions = overlay.querySelector('.segmentation-progress-actions');
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
setProgress: function (percent, message) {
|
|
96
|
+
detail.textContent = message;
|
|
97
|
+
if (percent === null) {
|
|
98
|
+
fill.classList.add('is-indeterminate');
|
|
99
|
+
fill.style.width = '';
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
fill.classList.remove('is-indeterminate');
|
|
103
|
+
fill.style.width = Math.max(0, Math.min(100, percent)) + '%';
|
|
104
|
+
},
|
|
105
|
+
setError: function (message, onSkip) {
|
|
106
|
+
title.textContent = 'Segmentation mask failed';
|
|
107
|
+
title.classList.add('has-error');
|
|
108
|
+
detail.textContent = message || 'The mask could not be converted.';
|
|
109
|
+
fill.classList.remove('is-indeterminate');
|
|
110
|
+
fill.classList.add('has-error');
|
|
111
|
+
fill.style.width = '100%';
|
|
112
|
+
// The rest of the import succeeded, so offer the viewer without a
|
|
113
|
+
// cell layer rather than leaving the user stuck on this panel.
|
|
114
|
+
const button = document.createElement('button');
|
|
115
|
+
button.type = 'button';
|
|
116
|
+
button.className = 'btn btn-secondary';
|
|
117
|
+
button.textContent = 'Open viewer without segmentation';
|
|
118
|
+
button.addEventListener('click', onSkip);
|
|
119
|
+
actions.appendChild(button);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|