@genome-spy/core 0.65.0 → 0.67.0
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.
- package/dist/bundle/browser-BRemItdO.js +138 -0
- package/dist/bundle/{index-CD7FLu9x.js → index-BatuyGAI.js} +23 -21
- package/dist/bundle/{index-C0llXMqm.js → index-ByuE8dvu.js} +140 -88
- package/dist/bundle/index-Cq3QFUxX.js +1781 -0
- package/dist/bundle/index-D28m8tSW.js +1607 -0
- package/dist/bundle/index-DbJ0oeYM.js +631 -0
- package/dist/bundle/index.es.js +15821 -14601
- package/dist/bundle/index.js +214 -212
- package/dist/bundle/{inflate-DRgHi_KK.js → inflate-GtwLkvSP.js} +222 -224
- package/dist/bundle/unzip-NywezaRR.js +1492 -0
- package/dist/schema.json +13 -3
- package/dist/src/config/scaleDefaults.d.ts +8 -0
- package/dist/src/config/scaleDefaults.d.ts.map +1 -0
- package/dist/src/config/scaleDefaults.js +45 -0
- package/dist/src/data/flowHandle.d.ts +2 -0
- package/dist/src/data/flowHandle.d.ts.map +1 -1
- package/dist/src/data/flowHandle.js +1 -0
- package/dist/src/data/flowInit.d.ts +12 -4
- package/dist/src/data/flowInit.d.ts.map +1 -1
- package/dist/src/data/flowInit.js +115 -16
- package/dist/src/data/sources/lazy/axisTickSource.js +1 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +1 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.js +10 -3
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +5 -1
- package/dist/src/data/transforms/filterScoredLabels.d.ts +1 -1
- package/dist/src/data/transforms/filterScoredLabels.d.ts.map +1 -1
- package/dist/src/data/transforms/filterScoredLabels.js +1 -1
- package/dist/src/data/transforms/linearizeGenomicCoordinate.d.ts.map +1 -1
- package/dist/src/data/transforms/linearizeGenomicCoordinate.js +2 -1
- package/dist/src/encoder/encoder.d.ts +1 -1
- package/dist/src/encoder/encoder.d.ts.map +1 -1
- package/dist/src/encoder/encoder.js +1 -1
- package/dist/src/genome/scaleLocus.d.ts +39 -0
- package/dist/src/genome/scaleLocus.d.ts.map +1 -1
- package/dist/src/genome/scaleLocus.js +76 -0
- package/dist/src/genomeSpy/canvasExport.d.ts +19 -0
- package/dist/src/genomeSpy/canvasExport.d.ts.map +1 -0
- package/dist/src/genomeSpy/canvasExport.js +66 -0
- package/dist/src/genomeSpy/containerUi.d.ts +17 -0
- package/dist/src/genomeSpy/containerUi.d.ts.map +1 -0
- package/dist/src/genomeSpy/containerUi.js +78 -0
- package/dist/src/genomeSpy/eventListenerRegistry.d.ts +19 -0
- package/dist/src/genomeSpy/eventListenerRegistry.d.ts.map +1 -0
- package/dist/src/genomeSpy/eventListenerRegistry.js +38 -0
- package/dist/src/genomeSpy/inputBindingManager.d.ts +14 -0
- package/dist/src/genomeSpy/inputBindingManager.d.ts.map +1 -0
- package/dist/src/genomeSpy/inputBindingManager.js +63 -0
- package/dist/src/genomeSpy/interactionController.d.ts +40 -0
- package/dist/src/genomeSpy/interactionController.d.ts.map +1 -0
- package/dist/src/genomeSpy/interactionController.js +371 -0
- package/dist/src/genomeSpy/keyboardListenerManager.d.ts +10 -0
- package/dist/src/genomeSpy/keyboardListenerManager.d.ts.map +1 -0
- package/dist/src/genomeSpy/keyboardListenerManager.js +31 -0
- package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +15 -0
- package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -0
- package/dist/src/genomeSpy/loadingIndicatorManager.js +92 -0
- package/dist/src/genomeSpy/renderCoordinator.d.ts +22 -0
- package/dist/src/genomeSpy/renderCoordinator.d.ts.map +1 -0
- package/dist/src/genomeSpy/renderCoordinator.js +118 -0
- package/dist/src/genomeSpy/viewContextFactory.d.ts +18 -0
- package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -0
- package/dist/src/genomeSpy/viewContextFactory.js +79 -0
- package/dist/src/genomeSpy/viewDataInit.d.ts +22 -0
- package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -0
- package/dist/src/genomeSpy/viewDataInit.js +160 -0
- package/dist/src/genomeSpy/viewDataInit.test.d.ts +2 -0
- package/dist/src/genomeSpy/viewDataInit.test.d.ts.map +1 -0
- package/dist/src/genomeSpy/viewHierarchyConfig.d.ts +14 -0
- package/dist/src/genomeSpy/viewHierarchyConfig.d.ts.map +1 -0
- package/dist/src/genomeSpy/viewHierarchyConfig.js +24 -0
- package/dist/src/genomeSpy/viewHighlight.d.ts +5 -0
- package/dist/src/genomeSpy/viewHighlight.d.ts.map +1 -0
- package/dist/src/genomeSpy/viewHighlight.js +30 -0
- package/dist/src/genomeSpy.d.ts +17 -71
- package/dist/src/genomeSpy.d.ts.map +1 -1
- package/dist/src/genomeSpy.js +197 -741
- package/dist/src/gl/dataToVertices.d.ts.map +1 -1
- package/dist/src/gl/dataToVertices.js +16 -4
- package/dist/src/gl/glslScaleGenerator.d.ts +1 -1
- package/dist/src/gl/webGLHelper.d.ts +2 -2
- package/dist/src/gl/webGLHelper.d.ts.map +1 -1
- package/dist/src/gl/webGLHelper.js +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -12
- package/dist/src/marks/mark.d.ts.map +1 -1
- package/dist/src/marks/mark.js +4 -2
- package/dist/src/{view → scales}/axisResolution.d.ts +9 -16
- package/dist/src/scales/axisResolution.d.ts.map +1 -0
- package/dist/src/{view → scales}/axisResolution.js +29 -18
- package/dist/src/scales/axisResolution.test.d.ts.map +1 -0
- package/dist/src/scales/scaleDomainAggregator.d.ts +57 -0
- package/dist/src/scales/scaleDomainAggregator.d.ts.map +1 -0
- package/dist/src/scales/scaleDomainAggregator.js +167 -0
- package/dist/src/scales/scaleDomainAggregator.test.d.ts +2 -0
- package/dist/src/scales/scaleDomainAggregator.test.d.ts.map +1 -0
- package/dist/src/scales/scaleInstanceManager.d.ts +40 -0
- package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -0
- package/dist/src/scales/scaleInstanceManager.js +317 -0
- package/dist/src/scales/scaleInstanceManager.test.d.ts +2 -0
- package/dist/src/scales/scaleInstanceManager.test.d.ts.map +1 -0
- package/dist/src/scales/scaleInteractionController.d.ts +73 -0
- package/dist/src/scales/scaleInteractionController.d.ts.map +1 -0
- package/dist/src/scales/scaleInteractionController.js +336 -0
- package/dist/src/scales/scaleInteractionController.test.d.ts +2 -0
- package/dist/src/scales/scaleInteractionController.test.d.ts.map +1 -0
- package/dist/src/scales/scalePropsResolver.d.ts +23 -0
- package/dist/src/scales/scalePropsResolver.d.ts.map +1 -0
- package/dist/src/scales/scalePropsResolver.js +74 -0
- package/dist/src/{view → scales}/scaleResolution.d.ts +53 -35
- package/dist/src/scales/scaleResolution.d.ts.map +1 -0
- package/dist/src/scales/scaleResolution.js +732 -0
- package/dist/src/scales/scaleResolution.test.d.ts.map +1 -0
- package/dist/src/scales/scaleResolutionConstants.d.ts +6 -0
- package/dist/src/scales/scaleResolutionConstants.d.ts.map +1 -0
- package/dist/src/scales/scaleResolutionConstants.js +5 -0
- package/dist/src/scales/scaleRules.d.ts +16 -0
- package/dist/src/scales/scaleRules.d.ts.map +1 -0
- package/dist/src/scales/scaleRules.js +103 -0
- package/dist/src/scales/scaleRules.test.d.ts +2 -0
- package/dist/src/scales/scaleRules.test.d.ts.map +1 -0
- package/dist/src/spec/channel.d.ts +13 -18
- package/dist/src/spec/scale.d.ts +6 -0
- package/dist/src/types/embedApi.d.ts +5 -0
- package/dist/src/types/scaleResolutionApi.d.ts +1 -1
- package/dist/src/utils/domainArray.d.ts.map +1 -1
- package/dist/src/utils/domainArray.js +3 -0
- package/dist/src/utils/indexer.d.ts +3 -0
- package/dist/src/utils/indexer.d.ts.map +1 -1
- package/dist/src/utils/indexer.js +3 -0
- package/dist/src/view/concatView.d.ts +18 -0
- package/dist/src/view/concatView.d.ts.map +1 -1
- package/dist/src/view/concatView.js +73 -0
- package/dist/src/view/concatView.test.d.ts +2 -0
- package/dist/src/view/concatView.test.d.ts.map +1 -0
- package/dist/src/view/containerMutationHelper.d.ts +74 -0
- package/dist/src/view/containerMutationHelper.d.ts.map +1 -0
- package/dist/src/view/containerMutationHelper.js +118 -0
- package/dist/src/view/containerView.d.ts +0 -7
- package/dist/src/view/containerView.d.ts.map +1 -1
- package/dist/src/view/containerView.js +0 -10
- package/dist/src/view/facetView.d.ts.map +1 -1
- package/dist/src/view/facetView.js +0 -15
- package/dist/src/view/flowBuilder.d.ts +5 -3
- package/dist/src/view/flowBuilder.d.ts.map +1 -1
- package/dist/src/view/flowBuilder.js +69 -6
- package/dist/src/view/gridView/gridChild.d.ts +11 -0
- package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
- package/dist/src/view/gridView/gridChild.js +32 -6
- package/dist/src/view/gridView/gridView.d.ts +39 -1
- package/dist/src/view/gridView/gridView.d.ts.map +1 -1
- package/dist/src/view/gridView/gridView.js +106 -48
- package/dist/src/view/gridView/gridView.test.d.ts +2 -0
- package/dist/src/view/gridView/gridView.test.d.ts.map +1 -0
- package/dist/src/view/gridView/scrollbar.d.ts +39 -8
- package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
- package/dist/src/view/gridView/scrollbar.js +184 -69
- package/dist/src/view/layerView.d.ts +14 -0
- package/dist/src/view/layerView.d.ts.map +1 -1
- package/dist/src/view/layerView.js +66 -0
- package/dist/src/view/layerView.test.d.ts +2 -0
- package/dist/src/view/layerView.test.d.ts.map +1 -0
- package/dist/src/view/testUtils.d.ts.map +1 -1
- package/dist/src/view/testUtils.js +7 -1
- package/dist/src/view/unitView.d.ts.map +1 -1
- package/dist/src/view/unitView.js +41 -36
- package/dist/src/view/view.d.ts +18 -6
- package/dist/src/view/view.d.ts.map +1 -1
- package/dist/src/view/view.js +30 -4
- package/package.json +2 -2
- package/dist/bundle/browser-txUcLy2H.js +0 -123
- package/dist/bundle/index-BQpbYrv4.js +0 -1712
- package/dist/bundle/index-BhtHKLUo.js +0 -73
- package/dist/bundle/index-CCe8rnZz.js +0 -716
- package/dist/bundle/index-DhcU-Gk-.js +0 -1487
- package/dist/src/data/collector.test.js +0 -138
- package/dist/src/data/dataFlow.test.js +0 -38
- package/dist/src/data/flow.test.js +0 -81
- package/dist/src/data/flowInit.test.js +0 -413
- package/dist/src/data/flowNode.test.js +0 -50
- package/dist/src/data/flowOptimizer.test.js +0 -209
- package/dist/src/data/formats/fasta.test.js +0 -27
- package/dist/src/data/sources/inlineSource.test.js +0 -63
- package/dist/src/data/sources/sequenceSource.test.js +0 -81
- package/dist/src/data/transforms/aggregate.test.js +0 -134
- package/dist/src/data/transforms/clone.test.js +0 -11
- package/dist/src/data/transforms/coverage.test.js +0 -238
- package/dist/src/data/transforms/filter.test.js +0 -20
- package/dist/src/data/transforms/flatten.test.js +0 -96
- package/dist/src/data/transforms/flattenDelimited.test.js +0 -90
- package/dist/src/data/transforms/flattenSequence.test.js +0 -34
- package/dist/src/data/transforms/formula.test.js +0 -25
- package/dist/src/data/transforms/identifier.test.js +0 -92
- package/dist/src/data/transforms/pileup.test.js +0 -70
- package/dist/src/data/transforms/project.test.js +0 -32
- package/dist/src/data/transforms/regexExtract.test.js +0 -70
- package/dist/src/data/transforms/regexFold.test.js +0 -201
- package/dist/src/data/transforms/sample.test.js +0 -38
- package/dist/src/data/transforms/stack.test.js +0 -91
- package/dist/src/encoder/accessor.test.js +0 -162
- package/dist/src/encoder/encoder.test.js +0 -105
- package/dist/src/genome/genome.test.js +0 -268
- package/dist/src/genome/genomes.test.js +0 -8
- package/dist/src/genome/scaleIndex.test.js +0 -78
- package/dist/src/genome/scaleLocus.test.js +0 -4
- package/dist/src/scale/scale.test.js +0 -326
- package/dist/src/scale/ticks.test.js +0 -46
- package/dist/src/selection/selection.test.js +0 -14
- package/dist/src/utils/addBaseUrl.test.js +0 -30
- package/dist/src/utils/binnedIndex.test.js +0 -201
- package/dist/src/utils/cloner.test.js +0 -35
- package/dist/src/utils/coalesce.test.js +0 -16
- package/dist/src/utils/concatIterables.test.js +0 -8
- package/dist/src/utils/domainArray.test.js +0 -130
- package/dist/src/utils/indexer.test.js +0 -49
- package/dist/src/utils/interactionEvent.test.js +0 -35
- package/dist/src/utils/iterateNestedMaps.test.js +0 -33
- package/dist/src/utils/kWayMerge.test.js +0 -30
- package/dist/src/utils/mergeObjects.test.js +0 -42
- package/dist/src/utils/numberExtractor.test.js +0 -6
- package/dist/src/utils/propertyCacher.test.js +0 -89
- package/dist/src/utils/propertyCoalescer.test.js +0 -25
- package/dist/src/utils/radixSort.test.js +0 -51
- package/dist/src/utils/reservationMap.test.js +0 -20
- package/dist/src/utils/ringBuffer.test.js +0 -39
- package/dist/src/utils/topK.test.js +0 -54
- package/dist/src/utils/trees.test.js +0 -135
- package/dist/src/utils/url.test.js +0 -28
- package/dist/src/utils/variableTools.test.js +0 -13
- package/dist/src/view/axisResolution.d.ts.map +0 -1
- package/dist/src/view/axisResolution.test.d.ts.map +0 -1
- package/dist/src/view/axisResolution.test.js +0 -206
- package/dist/src/view/flowBuilder.test.js +0 -125
- package/dist/src/view/gridView/selectionRect.test.js +0 -87
- package/dist/src/view/layout/flexLayout.test.js +0 -323
- package/dist/src/view/layout/grid.test.js +0 -71
- package/dist/src/view/layout/rectangle.test.js +0 -192
- package/dist/src/view/paramMediator.test.js +0 -282
- package/dist/src/view/scaleResolution.d.ts.map +0 -1
- package/dist/src/view/scaleResolution.js +0 -1059
- package/dist/src/view/scaleResolution.test.d.ts.map +0 -1
- package/dist/src/view/scaleResolution.test.js +0 -645
- package/dist/src/view/view.test.js +0 -245
- package/dist/src/view/viewDispose.test.js +0 -110
- package/dist/src/view/viewFactory.test.js +0 -25
- package/dist/src/view/viewUtils.test.js +0 -87
- /package/dist/src/{view → scales}/axisResolution.test.d.ts +0 -0
- /package/dist/src/{view → scales}/scaleResolution.test.d.ts +0 -0
package/dist/src/genomeSpy.js
CHANGED
|
@@ -1,43 +1,40 @@
|
|
|
1
1
|
import { formats as vegaFormats } from "vega-loader";
|
|
2
|
-
import { html, nothing, render } from "lit";
|
|
3
|
-
import { styleMap } from "lit/directives/style-map.js";
|
|
4
|
-
import SPINNER from "./img/90-ring-with-bg.svg";
|
|
5
|
-
|
|
6
|
-
import css from "./styles/genome-spy.css.js";
|
|
7
|
-
import Tooltip from "./utils/ui/tooltip.js";
|
|
8
2
|
|
|
9
3
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "./
|
|
15
|
-
import
|
|
4
|
+
createContainerUi,
|
|
5
|
+
createMessageBox,
|
|
6
|
+
} from "./genomeSpy/containerUi.js";
|
|
7
|
+
import LoadingIndicatorManager from "./genomeSpy/loadingIndicatorManager.js";
|
|
8
|
+
import { createViewHighlighter } from "./genomeSpy/viewHighlight.js";
|
|
9
|
+
import KeyboardListenerManager from "./genomeSpy/keyboardListenerManager.js";
|
|
10
|
+
import EventListenerRegistry from "./genomeSpy/eventListenerRegistry.js";
|
|
11
|
+
import InputBindingManager from "./genomeSpy/inputBindingManager.js";
|
|
12
|
+
|
|
13
|
+
import { calculateCanvasSize } from "./view/viewUtils.js";
|
|
14
|
+
import {
|
|
15
|
+
initializeViewData,
|
|
16
|
+
initializeVisibleViewData,
|
|
17
|
+
} from "./genomeSpy/viewDataInit.js";
|
|
16
18
|
import UnitView from "./view/unitView.js";
|
|
17
19
|
|
|
18
|
-
import WebGLHelper
|
|
19
|
-
framebufferToDataUrl,
|
|
20
|
-
readPickingPixel,
|
|
21
|
-
} from "./gl/webGLHelper.js";
|
|
22
|
-
import Rectangle from "./view/layout/rectangle.js";
|
|
23
|
-
import BufferedViewRenderingContext from "./view/renderingContext/bufferedViewRenderingContext.js";
|
|
24
|
-
import CompositeViewRenderingContext from "./view/renderingContext/compositeViewRenderingContext.js";
|
|
25
|
-
import InteractionEvent from "./utils/interactionEvent.js";
|
|
26
|
-
import Point from "./view/layout/point.js";
|
|
20
|
+
import WebGLHelper from "./gl/webGLHelper.js";
|
|
27
21
|
import Animator from "./utils/animator.js";
|
|
28
22
|
import DataFlow from "./data/dataFlow.js";
|
|
29
23
|
import GenomeStore from "./genome/genomeStore.js";
|
|
30
24
|
import BmFontManager from "./fonts/bmFontManager.js";
|
|
31
25
|
import fasta from "./data/formats/fasta.js";
|
|
32
|
-
import { VISIT_STOP } from "./view/view.js";
|
|
33
|
-
import Inertia, { makeEventTemplate } from "./utils/inertia.js";
|
|
34
26
|
import refseqGeneTooltipHandler from "./tooltip/refseqGeneTooltipHandler.js";
|
|
35
27
|
import dataTooltipHandler from "./tooltip/dataTooltipHandler.js";
|
|
36
28
|
import { invalidatePrefix } from "./utils/propertyCacher.js";
|
|
37
29
|
import { VIEW_ROOT_NAME, ViewFactory } from "./view/viewFactory.js";
|
|
38
|
-
import
|
|
39
|
-
import
|
|
40
|
-
import {
|
|
30
|
+
import InteractionController from "./genomeSpy/interactionController.js";
|
|
31
|
+
import RenderCoordinator from "./genomeSpy/renderCoordinator.js";
|
|
32
|
+
import { createViewContext } from "./genomeSpy/viewContextFactory.js";
|
|
33
|
+
import {
|
|
34
|
+
configureViewHierarchy,
|
|
35
|
+
configureViewOpacity,
|
|
36
|
+
} from "./genomeSpy/viewHierarchyConfig.js";
|
|
37
|
+
import { exportCanvas } from "./genomeSpy/canvasExport.js";
|
|
41
38
|
|
|
42
39
|
/**
|
|
43
40
|
* Events that are broadcasted to all views.
|
|
@@ -47,6 +44,23 @@ import { createFramebufferInfo } from "twgl.js";
|
|
|
47
44
|
vegaFormats("fasta", fasta);
|
|
48
45
|
|
|
49
46
|
export default class GenomeSpy {
|
|
47
|
+
/** @type {(() => void)[]} */
|
|
48
|
+
#destructionCallbacks = [];
|
|
49
|
+
/** @type {RenderCoordinator} */
|
|
50
|
+
#renderCoordinator;
|
|
51
|
+
/** @type {LoadingIndicatorManager} */
|
|
52
|
+
#loadingIndicatorManager;
|
|
53
|
+
/** @type {InputBindingManager} */
|
|
54
|
+
#inputBindingManager;
|
|
55
|
+
/** @type {InteractionController} */
|
|
56
|
+
#interactionController;
|
|
57
|
+
/** @type {WebGLHelper} */
|
|
58
|
+
#glHelper;
|
|
59
|
+
|
|
60
|
+
#keyboardListenerManager = new KeyboardListenerManager();
|
|
61
|
+
#eventListeners = new EventListenerRegistry();
|
|
62
|
+
#extraBroadcastListeners = new EventListenerRegistry();
|
|
63
|
+
|
|
50
64
|
/**
|
|
51
65
|
* @typedef {import("./view/view.js").default} View
|
|
52
66
|
* @typedef {import("./spec/view.js").ViewSpec} ViewSpec
|
|
@@ -66,9 +80,6 @@ export default class GenomeSpy {
|
|
|
66
80
|
|
|
67
81
|
options.inputBindingContainer ??= "default";
|
|
68
82
|
|
|
69
|
-
/** @type {(() => void)[]} */
|
|
70
|
-
this._destructionCallbacks = [];
|
|
71
|
-
|
|
72
83
|
/** Root level configuration object */
|
|
73
84
|
this.spec = spec;
|
|
74
85
|
|
|
@@ -90,43 +101,6 @@ export default class GenomeSpy {
|
|
|
90
101
|
*/
|
|
91
102
|
this.viewVisibilityPredicate = (view) => view.isVisibleInSpec();
|
|
92
103
|
|
|
93
|
-
/** @type {BufferedViewRenderingContext} */
|
|
94
|
-
this._renderingContext = undefined;
|
|
95
|
-
/** @type {BufferedViewRenderingContext} */
|
|
96
|
-
this._pickingContext = undefined;
|
|
97
|
-
|
|
98
|
-
/** Does picking buffer need to be rendered again */
|
|
99
|
-
this._dirtyPickingBuffer = false;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Currently hovered mark and datum
|
|
103
|
-
* @type {{ mark: import("./marks/mark.js").default, datum: import("./data/flowNode.js").Datum, uniqueId: number }}
|
|
104
|
-
*/
|
|
105
|
-
this._currentHover = undefined;
|
|
106
|
-
|
|
107
|
-
this._wheelInertia = new Inertia(this.animator);
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Keeping track so that these can be cleaned up upon finalization.
|
|
111
|
-
* @type {Map<string, (function(KeyboardEvent):void)[]>}
|
|
112
|
-
*/
|
|
113
|
-
this._keyboardListeners = new Map();
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Listers for exposed high-level events such as click on a mark instance.
|
|
117
|
-
* These should probably be in the View class and support bubbling through
|
|
118
|
-
* the hierarchy.
|
|
119
|
-
*
|
|
120
|
-
* @type {Map<string, Set<(event: any) => void>>}
|
|
121
|
-
*/
|
|
122
|
-
this._eventListeners = new Map();
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
126
|
-
* @type {Map<string, Set<(event: any) => void>>}
|
|
127
|
-
*/
|
|
128
|
-
this._extraBroadcastListeners = new Map();
|
|
129
|
-
|
|
130
104
|
/** @type {Record<string, import("./tooltip/tooltipHandler.js").TooltipHandler>}> */
|
|
131
105
|
this.tooltipHandlers = {
|
|
132
106
|
default: dataTooltipHandler,
|
|
@@ -137,20 +111,7 @@ export default class GenomeSpy {
|
|
|
137
111
|
/** @type {View} */
|
|
138
112
|
this.viewRoot = undefined;
|
|
139
113
|
|
|
140
|
-
|
|
141
|
-
* Views that are currently loading data using lazy sources.
|
|
142
|
-
*
|
|
143
|
-
* @type {Map<View, { status: import("./types/viewContext.js").DataLoadingStatus, detail?: string }>}
|
|
144
|
-
*/
|
|
145
|
-
this._loadingViews = new Map();
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* @type {HTMLElement}
|
|
149
|
-
*/
|
|
150
|
-
this._inputBindingContainer = undefined;
|
|
151
|
-
|
|
152
|
-
/** @type {Point} */
|
|
153
|
-
this._mouseDownCoords = undefined;
|
|
114
|
+
this.#inputBindingManager = new InputBindingManager(container, options);
|
|
154
115
|
|
|
155
116
|
this.dpr = window.devicePixelRatio;
|
|
156
117
|
}
|
|
@@ -162,37 +123,7 @@ export default class GenomeSpy {
|
|
|
162
123
|
}
|
|
163
124
|
|
|
164
125
|
#initializeParameterBindings() {
|
|
165
|
-
|
|
166
|
-
const inputs = [];
|
|
167
|
-
|
|
168
|
-
this.viewRoot.visit((view) => {
|
|
169
|
-
const mediator = view.paramMediator;
|
|
170
|
-
inputs.push(...createBindingInputs(mediator));
|
|
171
|
-
});
|
|
172
|
-
const ibc = this.options.inputBindingContainer;
|
|
173
|
-
|
|
174
|
-
if (!ibc || ibc == "none" || !inputs.length) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
this._inputBindingContainer = element("div", {
|
|
179
|
-
className: "gs-input-bindings",
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (ibc == "default") {
|
|
183
|
-
this.container.appendChild(this._inputBindingContainer);
|
|
184
|
-
} else if (ibc instanceof HTMLElement) {
|
|
185
|
-
ibc.appendChild(this._inputBindingContainer);
|
|
186
|
-
} else {
|
|
187
|
-
throw new Error("Invalid inputBindingContainer");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (inputs.length) {
|
|
191
|
-
render(
|
|
192
|
-
html`<div class="gs-input-binding">${inputs}</div>`,
|
|
193
|
-
this._inputBindingContainer
|
|
194
|
-
);
|
|
195
|
-
}
|
|
126
|
+
this.#inputBindingManager.initialize(this.viewRoot);
|
|
196
127
|
}
|
|
197
128
|
|
|
198
129
|
/**
|
|
@@ -232,6 +163,22 @@ export default class GenomeSpy {
|
|
|
232
163
|
this.animator.requestRender();
|
|
233
164
|
}
|
|
234
165
|
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} type
|
|
168
|
+
* @param {(event: any) => void} listener
|
|
169
|
+
*/
|
|
170
|
+
addEventListener(type, listener) {
|
|
171
|
+
this.#eventListeners.add(type, listener);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {string} type
|
|
176
|
+
* @param {(event: any) => void} listener
|
|
177
|
+
*/
|
|
178
|
+
removeEventListener(type, listener) {
|
|
179
|
+
this.#eventListeners.remove(type, listener);
|
|
180
|
+
}
|
|
181
|
+
|
|
235
182
|
/**
|
|
236
183
|
* Broadcast a message to all views
|
|
237
184
|
|
|
@@ -241,71 +188,7 @@ export default class GenomeSpy {
|
|
|
241
188
|
broadcast(type, payload) {
|
|
242
189
|
const message = { type, payload };
|
|
243
190
|
this.viewRoot.visit((view) => view.handleBroadcast(message));
|
|
244
|
-
this.
|
|
245
|
-
.get(type)
|
|
246
|
-
?.forEach((listener) => listener(message));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Draw some layers on top of the canvas. It's easier to do fancy spinning
|
|
251
|
-
* animations with html elements than with WebGL.
|
|
252
|
-
*/
|
|
253
|
-
_updateLoadingIndicators() {
|
|
254
|
-
/** @type {import("lit").TemplateResult[]} */
|
|
255
|
-
const indicators = [];
|
|
256
|
-
|
|
257
|
-
const isSomethingVisible = () =>
|
|
258
|
-
[...this._loadingViews.values()].some(
|
|
259
|
-
(v) => v.status == "loading" || v.status == "error"
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
for (const [view, status] of this._loadingViews) {
|
|
263
|
-
const c = view.coords;
|
|
264
|
-
if (c) {
|
|
265
|
-
const style = {
|
|
266
|
-
left: `${c.x}px`,
|
|
267
|
-
top: `${c.y}px`,
|
|
268
|
-
width: `${c.width}px`,
|
|
269
|
-
height: `${c.height}px`,
|
|
270
|
-
};
|
|
271
|
-
indicators.push(
|
|
272
|
-
html`<div style=${styleMap(style)}>
|
|
273
|
-
<div class=${status.status}>
|
|
274
|
-
${status.status == "error"
|
|
275
|
-
? html`<span
|
|
276
|
-
>Loading
|
|
277
|
-
failed${status.detail
|
|
278
|
-
? html`: ${status.detail}`
|
|
279
|
-
: nothing}</span
|
|
280
|
-
>`
|
|
281
|
-
: html`
|
|
282
|
-
<img src="${SPINNER}" alt="" />
|
|
283
|
-
<span>Loading...</span>
|
|
284
|
-
`}
|
|
285
|
-
</div>
|
|
286
|
-
</div>`
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Do some hacks to stop css animations of the loading indicators.
|
|
292
|
-
// Otherwise they fire animation frames even when their opacity is zero.
|
|
293
|
-
// TODO: Instead of this, replace the animated spinners with static images.
|
|
294
|
-
// Or even better, once more widely supported, use `allow-discrete`
|
|
295
|
-
// https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior
|
|
296
|
-
// to enable transition of the display property.
|
|
297
|
-
if (isSomethingVisible()) {
|
|
298
|
-
this.loadingIndicatorsElement.style.display = "block";
|
|
299
|
-
} else {
|
|
300
|
-
// TODO: Clear previous timeout
|
|
301
|
-
setTimeout(() => {
|
|
302
|
-
if (!isSomethingVisible()) {
|
|
303
|
-
this.loadingIndicatorsElement.style.display = "none";
|
|
304
|
-
}
|
|
305
|
-
}, 3000);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
render(indicators, this.loadingIndicatorsElement);
|
|
191
|
+
this.#extraBroadcastListeners.emit(type, message);
|
|
309
192
|
}
|
|
310
193
|
|
|
311
194
|
#setupDpr() {
|
|
@@ -315,7 +198,7 @@ export default class GenomeSpy {
|
|
|
315
198
|
);
|
|
316
199
|
|
|
317
200
|
const resizeCallback = () => {
|
|
318
|
-
this.
|
|
201
|
+
this.#glHelper.invalidateSize();
|
|
319
202
|
this.dpr = window.devicePixelRatio;
|
|
320
203
|
dprSetter(this.dpr);
|
|
321
204
|
this.computeLayout();
|
|
@@ -327,7 +210,7 @@ export default class GenomeSpy {
|
|
|
327
210
|
// TODO: Size should be observed only if the content is not absolutely sized
|
|
328
211
|
const resizeObserver = new ResizeObserver(resizeCallback);
|
|
329
212
|
resizeObserver.observe(this.container);
|
|
330
|
-
this.
|
|
213
|
+
this.#destructionCallbacks.push(() => resizeObserver.disconnect());
|
|
331
214
|
}
|
|
332
215
|
|
|
333
216
|
/** @type {() => void} */
|
|
@@ -349,25 +232,19 @@ export default class GenomeSpy {
|
|
|
349
232
|
updatePixelRatio();
|
|
350
233
|
|
|
351
234
|
if (remove) {
|
|
352
|
-
this.
|
|
235
|
+
this.#destructionCallbacks.push(remove);
|
|
353
236
|
}
|
|
354
237
|
}
|
|
355
238
|
|
|
356
239
|
#prepareContainer() {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const canvasWrapper = element("div", {
|
|
364
|
-
class: "canvas-wrapper",
|
|
365
|
-
});
|
|
366
|
-
this.container.appendChild(canvasWrapper);
|
|
367
|
-
|
|
368
|
-
canvasWrapper.classList.add("loading");
|
|
240
|
+
const {
|
|
241
|
+
canvasWrapper,
|
|
242
|
+
loadingMessageElement,
|
|
243
|
+
loadingIndicatorsElement,
|
|
244
|
+
tooltip,
|
|
245
|
+
} = createContainerUi(this.container);
|
|
369
246
|
|
|
370
|
-
this
|
|
247
|
+
this.#glHelper = new WebGLHelper(
|
|
371
248
|
canvasWrapper,
|
|
372
249
|
() =>
|
|
373
250
|
this.viewRoot
|
|
@@ -377,30 +254,16 @@ export default class GenomeSpy {
|
|
|
377
254
|
);
|
|
378
255
|
|
|
379
256
|
// The initial loading message that is shown until the first frame is rendered
|
|
380
|
-
this.loadingMessageElement =
|
|
381
|
-
class: "loading-message",
|
|
382
|
-
innerHTML: `<div class="message">Loading<span class="ellipsis">...</span></div>`,
|
|
383
|
-
});
|
|
384
|
-
canvasWrapper.appendChild(this.loadingMessageElement);
|
|
385
|
-
|
|
257
|
+
this.loadingMessageElement = loadingMessageElement;
|
|
386
258
|
// A container for loading indicators (for lazy data sources.)
|
|
387
259
|
// These could alternatively be included in the view hierarchy,
|
|
388
260
|
// but it's easier this way – particularly if we want to show
|
|
389
261
|
// some fancy animated spinners.
|
|
390
|
-
this.loadingIndicatorsElement =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
this.tooltip = new Tooltip(this.container);
|
|
396
|
-
|
|
397
|
-
this.loadingMessageElement
|
|
398
|
-
.querySelector(".message")
|
|
399
|
-
.addEventListener("transitionend", () => {
|
|
400
|
-
/** @type {HTMLElement} */ (
|
|
401
|
-
this.loadingMessageElement
|
|
402
|
-
).style.display = "none";
|
|
403
|
-
});
|
|
262
|
+
this.loadingIndicatorsElement = loadingIndicatorsElement;
|
|
263
|
+
this.tooltip = tooltip;
|
|
264
|
+
this.#loadingIndicatorManager = new LoadingIndicatorManager(
|
|
265
|
+
loadingIndicatorsElement
|
|
266
|
+
);
|
|
404
267
|
}
|
|
405
268
|
|
|
406
269
|
/**
|
|
@@ -414,128 +277,91 @@ export default class GenomeSpy {
|
|
|
414
277
|
this.container.classList.remove("genome-spy");
|
|
415
278
|
canvasWrapper.classList.remove("loading");
|
|
416
279
|
|
|
417
|
-
|
|
418
|
-
for (const listener of listeners) {
|
|
419
|
-
document.removeEventListener(type, listener);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
280
|
+
this.#keyboardListenerManager.removeAll();
|
|
422
281
|
|
|
423
|
-
this.
|
|
282
|
+
this.#destructionCallbacks.forEach((callback) => callback());
|
|
424
283
|
|
|
425
|
-
this.
|
|
284
|
+
this.#glHelper.finalize();
|
|
426
285
|
|
|
427
|
-
this.
|
|
286
|
+
this.#inputBindingManager.remove();
|
|
428
287
|
|
|
429
288
|
while (this.container.firstChild) {
|
|
430
289
|
this.container.firstChild.remove();
|
|
431
290
|
}
|
|
432
291
|
}
|
|
433
292
|
|
|
434
|
-
async
|
|
293
|
+
async #prepareViewsAndData() {
|
|
294
|
+
await this.#initializeGenomeStore();
|
|
295
|
+
const context = this.#createViewContext();
|
|
296
|
+
await this.#initializeViewHierarchy(context);
|
|
297
|
+
await initializeViewData(
|
|
298
|
+
this.viewRoot,
|
|
299
|
+
context.dataFlow,
|
|
300
|
+
context.fontManager,
|
|
301
|
+
(flow) => this.broadcast("dataFlowBuilt", flow)
|
|
302
|
+
);
|
|
303
|
+
this.#finalizeViewInitialization(context);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async #initializeGenomeStore() {
|
|
435
307
|
if (this.spec.genome) {
|
|
436
308
|
this.genomeStore = new GenomeStore(this.spec.baseUrl);
|
|
437
309
|
await this.genomeStore.initialize(this.spec.genome);
|
|
438
310
|
}
|
|
311
|
+
}
|
|
439
312
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
/** @type {import("./types/viewContext.js").default} */
|
|
444
|
-
const context = {
|
|
313
|
+
#createViewContext() {
|
|
314
|
+
return createViewContext({
|
|
445
315
|
dataFlow: new DataFlow(),
|
|
446
|
-
glHelper: this
|
|
316
|
+
glHelper: this.#glHelper,
|
|
447
317
|
animator: this.animator,
|
|
448
318
|
genomeStore: this.genomeStore,
|
|
449
|
-
fontManager: new BmFontManager(this
|
|
450
|
-
|
|
451
|
-
requestLayoutReflow: () => {
|
|
452
|
-
// placeholder
|
|
453
|
-
},
|
|
319
|
+
fontManager: new BmFontManager(this.#glHelper),
|
|
454
320
|
updateTooltip: this.updateTooltip.bind(this),
|
|
455
321
|
getNamedDataFromProvider: this.getNamedDataFromProvider.bind(this),
|
|
456
|
-
getCurrentHover: () =>
|
|
457
|
-
|
|
458
|
-
setDataLoadingStatus: (view, status, detail) =>
|
|
459
|
-
this.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
322
|
+
getCurrentHover: () =>
|
|
323
|
+
this.#interactionController.getCurrentHover(),
|
|
324
|
+
setDataLoadingStatus: (view, status, detail) =>
|
|
325
|
+
this.#loadingIndicatorManager.setDataLoadingStatus(
|
|
326
|
+
view,
|
|
327
|
+
status,
|
|
328
|
+
detail
|
|
329
|
+
),
|
|
463
330
|
addKeyboardListener: (type, listener) => {
|
|
464
331
|
// TODO: Listeners should be called only when the mouse pointer is inside the
|
|
465
332
|
// container or the app covers the full document.
|
|
466
|
-
|
|
467
|
-
let listeners = this._keyboardListeners.get(type);
|
|
468
|
-
if (!listeners) {
|
|
469
|
-
listeners = [];
|
|
470
|
-
this._keyboardListeners.set(type, listeners);
|
|
471
|
-
}
|
|
472
|
-
listeners.push(listener);
|
|
333
|
+
this.#keyboardListenerManager.add(type, listener);
|
|
473
334
|
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
listenersByType.set(type, listeners);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
listeners.add(listener);
|
|
486
|
-
},
|
|
487
|
-
|
|
488
|
-
removeBroadcastListener(type, listener) {
|
|
489
|
-
const listenersByType = self._extraBroadcastListeners;
|
|
490
|
-
|
|
491
|
-
listenersByType.get(type)?.delete(listener);
|
|
492
|
-
},
|
|
493
|
-
|
|
494
|
-
isViewConfiguredVisible: self.viewVisibilityPredicate,
|
|
495
|
-
|
|
496
|
-
isViewSpec: (spec) => self.viewFactory.isViewSpec(spec),
|
|
497
|
-
|
|
498
|
-
createOrImportView: async function (
|
|
335
|
+
addBroadcastListener: (type, listener) =>
|
|
336
|
+
this.#extraBroadcastListeners.add(type, listener),
|
|
337
|
+
removeBroadcastListener: (type, listener) =>
|
|
338
|
+
this.#extraBroadcastListeners.remove(type, listener),
|
|
339
|
+
isViewConfiguredVisible: this.viewVisibilityPredicate,
|
|
340
|
+
isViewSpec: (spec) => this.viewFactory.isViewSpec(spec),
|
|
341
|
+
createOrImportViewWithContext: (
|
|
342
|
+
ctx,
|
|
499
343
|
spec,
|
|
500
344
|
layoutParent,
|
|
501
345
|
dataParent,
|
|
502
346
|
defaultName,
|
|
503
347
|
validator
|
|
504
|
-
)
|
|
505
|
-
|
|
348
|
+
) =>
|
|
349
|
+
this.viewFactory.createOrImportView(
|
|
506
350
|
spec,
|
|
507
|
-
|
|
351
|
+
ctx,
|
|
508
352
|
layoutParent,
|
|
509
353
|
dataParent,
|
|
510
354
|
defaultName,
|
|
511
355
|
validator
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.container.querySelector(".view-highlight")?.remove();
|
|
517
|
-
if (view) {
|
|
518
|
-
if (!view.isConfiguredVisible()) {
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
const coords = view.coords;
|
|
522
|
-
if (coords) {
|
|
523
|
-
const div = document.createElement("div");
|
|
524
|
-
div.className = "view-highlight";
|
|
525
|
-
div.style.position = "absolute";
|
|
526
|
-
div.style.left = coords.x + "px";
|
|
527
|
-
div.style.top = coords.y + "px";
|
|
528
|
-
div.style.width = coords.width + "px";
|
|
529
|
-
div.style.height = coords.height + "px";
|
|
530
|
-
div.style.border = "1px solid green";
|
|
531
|
-
div.style.backgroundColor = "rgba(0, 255, 0, 0.1)";
|
|
532
|
-
div.style.pointerEvents = "none";
|
|
533
|
-
this.container.appendChild(div);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
},
|
|
537
|
-
};
|
|
356
|
+
),
|
|
357
|
+
highlightView: createViewHighlighter(this.container),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
538
360
|
|
|
361
|
+
/**
|
|
362
|
+
* @param {import("./types/viewContext.js").default} context
|
|
363
|
+
*/
|
|
364
|
+
async #initializeViewHierarchy(context) {
|
|
539
365
|
/** @type {ViewSpec & RootConfig} */
|
|
540
366
|
const rootSpec = this.spec;
|
|
541
367
|
|
|
@@ -557,47 +383,51 @@ export default class GenomeSpy {
|
|
|
557
383
|
|
|
558
384
|
this.#initializeParameterBindings();
|
|
559
385
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
setImplicitScaleNames(this.viewRoot);
|
|
563
|
-
|
|
564
|
-
const views = this.viewRoot.getDescendants();
|
|
565
|
-
|
|
566
|
-
// View opacity should be configured after all scales have been resolved.
|
|
567
|
-
// Currently this doesn't work if new views are added dynamically.
|
|
568
|
-
// TODO: Figure out how to handle dynamic view addition/removal nicely.
|
|
569
|
-
views.forEach((view) => view.configureViewOpacity());
|
|
386
|
+
configureViewHierarchy(this.viewRoot);
|
|
387
|
+
configureViewOpacity(this.viewRoot);
|
|
570
388
|
|
|
571
389
|
// We should now have a complete view hierarchy. Let's update the canvas size
|
|
572
390
|
// and ensure that the loading message is visible.
|
|
573
|
-
this.
|
|
574
|
-
this.#
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
this.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
// Have to wait until asynchronous font loading is complete.
|
|
583
|
-
// Text mark's geometry builder needs font metrics before data can be
|
|
584
|
-
// converted into geometries.
|
|
585
|
-
// TODO: Make updateGraphicsData async and await font loading there.
|
|
586
|
-
await context.fontManager.waitUntilReady();
|
|
391
|
+
this.#glHelper.invalidateSize();
|
|
392
|
+
this.#renderCoordinator = new RenderCoordinator({
|
|
393
|
+
viewRoot: this.viewRoot,
|
|
394
|
+
glHelper: this.#glHelper,
|
|
395
|
+
getBackground: () => this.spec.background,
|
|
396
|
+
broadcast: this.broadcast.bind(this),
|
|
397
|
+
onLayoutComputed: () =>
|
|
398
|
+
this.#loadingIndicatorManager.updateLayout(),
|
|
399
|
+
});
|
|
587
400
|
|
|
588
|
-
//
|
|
589
|
-
|
|
401
|
+
// Allow early layout requests from view subscriptions created during initialization.
|
|
402
|
+
// Layout will be recomputed anyway once launch completes.
|
|
403
|
+
context.requestLayoutReflow = this.computeLayout.bind(this);
|
|
590
404
|
|
|
591
|
-
|
|
405
|
+
this.#setupDpr();
|
|
406
|
+
}
|
|
592
407
|
|
|
593
|
-
|
|
408
|
+
/**
|
|
409
|
+
* @param {import("./types/viewContext.js").default} context
|
|
410
|
+
*/
|
|
411
|
+
#finalizeViewInitialization(context) {
|
|
412
|
+
// Allow layout computation (in case a custom context overrode the early assignment).
|
|
594
413
|
// eslint-disable-next-line require-atomic-updates
|
|
595
414
|
context.requestLayoutReflow = this.computeLayout.bind(this);
|
|
596
415
|
|
|
597
416
|
// Invalidate cached sizes to ensure that step-based sizes are current.
|
|
598
417
|
// TODO: This should be done automatically when the domains of band/point scales are updated.
|
|
599
418
|
this.viewRoot.visit((view) => invalidatePrefix(view, "size"));
|
|
600
|
-
this.
|
|
419
|
+
this.#glHelper.invalidateSize();
|
|
420
|
+
|
|
421
|
+
this.#interactionController = new InteractionController({
|
|
422
|
+
viewRoot: this.viewRoot,
|
|
423
|
+
glHelper: this.#glHelper,
|
|
424
|
+
tooltip: this.tooltip,
|
|
425
|
+
animator: this.animator,
|
|
426
|
+
emitEvent: this.#eventListeners.emit.bind(this.#eventListeners),
|
|
427
|
+
tooltipHandlers: this.tooltipHandlers,
|
|
428
|
+
renderPickingFramebuffer: this.renderPickingFramebuffer.bind(this),
|
|
429
|
+
getDevicePixelRatio: () => this.dpr,
|
|
430
|
+
});
|
|
601
431
|
}
|
|
602
432
|
|
|
603
433
|
/**
|
|
@@ -608,7 +438,7 @@ export default class GenomeSpy {
|
|
|
608
438
|
try {
|
|
609
439
|
this.#prepareContainer();
|
|
610
440
|
|
|
611
|
-
await this
|
|
441
|
+
await this.#prepareViewsAndData();
|
|
612
442
|
|
|
613
443
|
this.registerMouseEvents();
|
|
614
444
|
|
|
@@ -621,7 +451,10 @@ export default class GenomeSpy {
|
|
|
621
451
|
reason.view ? `At "${reason.view.getPathString()}": ` : ""
|
|
622
452
|
}${reason.toString()}`;
|
|
623
453
|
console.error(reason.stack);
|
|
624
|
-
|
|
454
|
+
const handled = this.options.onError?.(reason, this.container);
|
|
455
|
+
if (!handled) {
|
|
456
|
+
createMessageBox(this.container, message);
|
|
457
|
+
}
|
|
625
458
|
|
|
626
459
|
return false;
|
|
627
460
|
} finally {
|
|
@@ -633,277 +466,27 @@ export default class GenomeSpy {
|
|
|
633
466
|
}
|
|
634
467
|
}
|
|
635
468
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// TODO: This function is huge. Refactor this into a separate class
|
|
640
|
-
// that would also contain state-related stuff that currently pollute the
|
|
641
|
-
// GenomeSpy class.
|
|
642
|
-
|
|
643
|
-
let lastWheelEvent = performance.now();
|
|
644
|
-
|
|
645
|
-
let longPressTriggered = false;
|
|
646
|
-
|
|
647
|
-
/** @param {Event} event */
|
|
648
|
-
const listener = (event) => {
|
|
649
|
-
const now = performance.now();
|
|
650
|
-
const wheeling = now - lastWheelEvent < 200;
|
|
651
|
-
|
|
652
|
-
if (event instanceof MouseEvent) {
|
|
653
|
-
const rect = canvas.getBoundingClientRect();
|
|
654
|
-
const point = new Point(
|
|
655
|
-
event.clientX - rect.left - canvas.clientLeft,
|
|
656
|
-
event.clientY - rect.top - canvas.clientTop
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
if (event.type == "mousemove" && !wheeling) {
|
|
660
|
-
this.tooltip.handleMouseMove(event);
|
|
661
|
-
this._tooltipUpdateRequested = false;
|
|
662
|
-
|
|
663
|
-
// Disable picking during dragging. Also postpone picking until
|
|
664
|
-
// the user has stopped zooming as reading pixels from the
|
|
665
|
-
// picking buffer is slow and ruins smooth animations.
|
|
666
|
-
if (event.buttons == 0 && !isStillZooming()) {
|
|
667
|
-
this.renderPickingFramebuffer();
|
|
668
|
-
this._handlePicking(point.x, point.y);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* @param {MouseEvent} event
|
|
674
|
-
*/
|
|
675
|
-
const dispatchEvent = (event) => {
|
|
676
|
-
this.viewRoot.propagateInteractionEvent(
|
|
677
|
-
new InteractionEvent(point, event)
|
|
678
|
-
);
|
|
679
|
-
|
|
680
|
-
if (!this._tooltipUpdateRequested) {
|
|
681
|
-
this.tooltip.clear();
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
if (event.type != "wheel") {
|
|
686
|
-
this._wheelInertia.cancel();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
(event.type == "mousedown" || event.type == "mouseup") &&
|
|
691
|
-
!isStillZooming()
|
|
692
|
-
) {
|
|
693
|
-
// Actually, only needed when clicking on a mark
|
|
694
|
-
this.renderPickingFramebuffer();
|
|
695
|
-
} else if (event.type == "wheel") {
|
|
696
|
-
lastWheelEvent = now;
|
|
697
|
-
this._tooltipUpdateRequested = false;
|
|
698
|
-
|
|
699
|
-
const wheelEvent = /** @type {WheelEvent} */ (event);
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
Math.abs(wheelEvent.deltaX) >
|
|
703
|
-
Math.abs(wheelEvent.deltaY)
|
|
704
|
-
) {
|
|
705
|
-
// If the viewport is panned (horizontally) using the wheel (touchpad),
|
|
706
|
-
// the picking buffer becomes stale and needs redrawing. However, we
|
|
707
|
-
// optimize by just clearing the currently hovered item so that snapping
|
|
708
|
-
// doesn't work incorrectly when zooming in/out.
|
|
709
|
-
|
|
710
|
-
// TODO: More robust solution (handle at higher level such as ScaleResolution's zoom method)
|
|
711
|
-
this._currentHover = null;
|
|
712
|
-
|
|
713
|
-
this._wheelInertia.cancel();
|
|
714
|
-
} else {
|
|
715
|
-
// Vertical wheeling zooms.
|
|
716
|
-
// We use inertia to generate fake wheel events for smoother zooming
|
|
717
|
-
|
|
718
|
-
const template = makeEventTemplate(wheelEvent);
|
|
719
|
-
|
|
720
|
-
this._wheelInertia.setMomentum(
|
|
721
|
-
wheelEvent.deltaY * (wheelEvent.deltaMode ? 80 : 1),
|
|
722
|
-
(delta) => {
|
|
723
|
-
const e = new WheelEvent("wheel", {
|
|
724
|
-
...template,
|
|
725
|
-
deltaMode: 0,
|
|
726
|
-
deltaX: 0,
|
|
727
|
-
deltaY: delta,
|
|
728
|
-
});
|
|
729
|
-
dispatchEvent(e);
|
|
730
|
-
}
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
wheelEvent.preventDefault();
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// TODO: Should be handled at the view level, not globally
|
|
739
|
-
if (event.type == "click") {
|
|
740
|
-
if (longPressTriggered) {
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const e = this._currentHover
|
|
745
|
-
? {
|
|
746
|
-
type: event.type,
|
|
747
|
-
viewPath: this._currentHover.mark.unitView
|
|
748
|
-
.getLayoutAncestors()
|
|
749
|
-
.map((view) => view.name)
|
|
750
|
-
.reverse(),
|
|
751
|
-
datum: this._currentHover.datum,
|
|
752
|
-
}
|
|
753
|
-
: {
|
|
754
|
-
type: event.type,
|
|
755
|
-
viewPath: null,
|
|
756
|
-
datum: null,
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
this._eventListeners
|
|
760
|
-
.get("click")
|
|
761
|
-
?.forEach((listener) => listener(e));
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (
|
|
765
|
-
event.type != "click" ||
|
|
766
|
-
// Suppress click events if the mouse has been dragged
|
|
767
|
-
this._mouseDownCoords?.subtract(Point.fromMouseEvent(event))
|
|
768
|
-
.length < 3
|
|
769
|
-
) {
|
|
770
|
-
dispatchEvent(event);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
[
|
|
776
|
-
"mousedown",
|
|
777
|
-
"mouseup",
|
|
778
|
-
"wheel",
|
|
779
|
-
"click",
|
|
780
|
-
"mousemove",
|
|
781
|
-
"gesturechange",
|
|
782
|
-
"contextmenu",
|
|
783
|
-
"dblclick",
|
|
784
|
-
].forEach((type) => canvas.addEventListener(type, listener));
|
|
785
|
-
|
|
786
|
-
canvas.addEventListener("mousedown", (/** @type {MouseEvent} */ e) => {
|
|
787
|
-
this._mouseDownCoords = Point.fromMouseEvent(e);
|
|
788
|
-
if (this.tooltip.sticky) {
|
|
789
|
-
this.tooltip.sticky = false;
|
|
790
|
-
this.tooltip.clear();
|
|
791
|
-
// A hack to prevent selection if the tooltip is sticky.
|
|
792
|
-
// Let the tooltip be destickified first.
|
|
793
|
-
longPressTriggered = true;
|
|
794
|
-
} else {
|
|
795
|
-
longPressTriggered = false;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const disableTooltip = () => {
|
|
799
|
-
document.addEventListener(
|
|
800
|
-
"mouseup",
|
|
801
|
-
() => this.tooltip.popEnabledState(),
|
|
802
|
-
{ once: true }
|
|
803
|
-
);
|
|
804
|
-
this.tooltip.pushEnabledState(false);
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
// Opening context menu or using modifier keys disables the tooltip
|
|
808
|
-
if (e.button == 2 || e.shiftKey || e.ctrlKey || e.metaKey) {
|
|
809
|
-
disableTooltip();
|
|
810
|
-
} else if (this.tooltip.visible) {
|
|
811
|
-
// Make tooltip sticky if the user long-presses
|
|
812
|
-
const timeout = setTimeout(() => {
|
|
813
|
-
longPressTriggered = true;
|
|
814
|
-
this.tooltip.sticky = true;
|
|
815
|
-
}, 400);
|
|
816
|
-
|
|
817
|
-
const clear = () => clearTimeout(timeout);
|
|
818
|
-
document.addEventListener("mouseup", clear, { once: true });
|
|
819
|
-
document.addEventListener("mousemove", clear, { once: true });
|
|
820
|
-
}
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
// Prevent text selections etc while dragging
|
|
824
|
-
canvas.addEventListener("dragstart", (event) =>
|
|
825
|
-
event.stopPropagation()
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
canvas.addEventListener("mouseout", () => {
|
|
829
|
-
this.tooltip.clear();
|
|
830
|
-
this._currentHover = null;
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* @param {number} x
|
|
836
|
-
* @param {number} y
|
|
837
|
-
*/
|
|
838
|
-
_handlePicking(x, y) {
|
|
839
|
-
const dpr = this.dpr;
|
|
840
|
-
const pp = readPickingPixel(
|
|
841
|
-
this._glHelper.gl,
|
|
842
|
-
this._glHelper._pickingBufferInfo,
|
|
843
|
-
x * dpr,
|
|
844
|
-
y * dpr
|
|
845
|
-
);
|
|
846
|
-
|
|
847
|
-
const uniqueId = pp[0] | (pp[1] << 8) | (pp[2] << 16) | (pp[3] << 24);
|
|
848
|
-
|
|
849
|
-
if (uniqueId == 0) {
|
|
850
|
-
this._currentHover = null;
|
|
469
|
+
async initializeVisibleViewData() {
|
|
470
|
+
if (!this.viewRoot) {
|
|
851
471
|
return;
|
|
852
472
|
}
|
|
853
473
|
|
|
854
|
-
|
|
855
|
-
this.
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
this.viewRoot.visit((view) => {
|
|
860
|
-
if (view instanceof UnitView) {
|
|
861
|
-
if (
|
|
862
|
-
view.mark.isPickingParticipant() &&
|
|
863
|
-
[...view.facetCoords.values()].some((coords) =>
|
|
864
|
-
coords.containsPoint(x, y)
|
|
865
|
-
)
|
|
866
|
-
) {
|
|
867
|
-
const datum = view
|
|
868
|
-
.getCollector()
|
|
869
|
-
.findDatumByUniqueId(uniqueId);
|
|
870
|
-
if (datum) {
|
|
871
|
-
this._currentHover = {
|
|
872
|
-
mark: view.mark,
|
|
873
|
-
datum,
|
|
874
|
-
uniqueId,
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
if (this._currentHover) {
|
|
879
|
-
return VISIT_STOP;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
if (this._currentHover) {
|
|
886
|
-
const mark = this._currentHover.mark;
|
|
887
|
-
this.updateTooltip(this._currentHover.datum, async (datum) => {
|
|
888
|
-
if (!mark.isPickingParticipant()) {
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const tooltipProps = mark.properties.tooltip;
|
|
474
|
+
await initializeVisibleViewData(
|
|
475
|
+
this.viewRoot,
|
|
476
|
+
this.viewRoot.context.dataFlow,
|
|
477
|
+
this.viewRoot.context.fontManager
|
|
478
|
+
);
|
|
893
479
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
480
|
+
// Visibility toggles can change sizes; ensure layout is recomputed even
|
|
481
|
+
// when callers don't explicitly request it.
|
|
482
|
+
this.viewRoot._invalidateCacheByPrefix("size", "progeny");
|
|
483
|
+
this.#glHelper.invalidateSize();
|
|
484
|
+
this.computeLayout();
|
|
485
|
+
this.animator.requestRender();
|
|
486
|
+
}
|
|
902
487
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
});
|
|
906
|
-
}
|
|
488
|
+
registerMouseEvents() {
|
|
489
|
+
this.#interactionController.registerMouseEvents();
|
|
907
490
|
}
|
|
908
491
|
|
|
909
492
|
/**
|
|
@@ -915,14 +498,7 @@ export default class GenomeSpy {
|
|
|
915
498
|
* @template T
|
|
916
499
|
*/
|
|
917
500
|
updateTooltip(datum, converter) {
|
|
918
|
-
|
|
919
|
-
this.tooltip.updateWithDatum(datum, converter);
|
|
920
|
-
this._tooltipUpdateRequested = true;
|
|
921
|
-
} else {
|
|
922
|
-
throw new Error(
|
|
923
|
-
"Tooltip has already been updated! Duplicate event handler?"
|
|
924
|
-
);
|
|
925
|
-
}
|
|
501
|
+
this.#interactionController.updateTooltip(datum, converter);
|
|
926
502
|
}
|
|
927
503
|
|
|
928
504
|
/**
|
|
@@ -940,49 +516,14 @@ export default class GenomeSpy {
|
|
|
940
516
|
devicePixelRatio,
|
|
941
517
|
clearColor = "white"
|
|
942
518
|
) {
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const width = Math.floor(logicalWidth * devicePixelRatio);
|
|
952
|
-
const height = Math.floor(logicalHeight * devicePixelRatio);
|
|
953
|
-
|
|
954
|
-
const framebufferInfo = createFramebufferInfo(
|
|
955
|
-
gl,
|
|
956
|
-
[
|
|
957
|
-
{
|
|
958
|
-
format: gl.RGBA,
|
|
959
|
-
type: gl.UNSIGNED_BYTE,
|
|
960
|
-
minMag: gl.LINEAR,
|
|
961
|
-
wrap: gl.CLAMP_TO_EDGE,
|
|
962
|
-
},
|
|
963
|
-
],
|
|
964
|
-
width,
|
|
965
|
-
height
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
const renderingContext = new BufferedViewRenderingContext(
|
|
969
|
-
{ picking: false },
|
|
970
|
-
{
|
|
971
|
-
webGLHelper: this._glHelper,
|
|
972
|
-
canvasSize: { width: logicalWidth, height: logicalHeight },
|
|
973
|
-
devicePixelRatio,
|
|
974
|
-
clearColor,
|
|
975
|
-
framebufferInfo,
|
|
976
|
-
}
|
|
977
|
-
);
|
|
978
|
-
|
|
979
|
-
this.viewRoot.render(
|
|
980
|
-
renderingContext,
|
|
981
|
-
Rectangle.create(0, 0, logicalWidth, logicalHeight)
|
|
982
|
-
);
|
|
983
|
-
renderingContext.render();
|
|
984
|
-
|
|
985
|
-
const pngUrl = framebufferToDataUrl(gl, framebufferInfo, "image/png");
|
|
519
|
+
const pngUrl = exportCanvas({
|
|
520
|
+
glHelper: this.#glHelper,
|
|
521
|
+
viewRoot: this.viewRoot,
|
|
522
|
+
logicalWidth,
|
|
523
|
+
logicalHeight,
|
|
524
|
+
devicePixelRatio,
|
|
525
|
+
clearColor,
|
|
526
|
+
});
|
|
986
527
|
|
|
987
528
|
// Clean up
|
|
988
529
|
this.computeLayout();
|
|
@@ -991,74 +532,20 @@ export default class GenomeSpy {
|
|
|
991
532
|
return pngUrl;
|
|
992
533
|
}
|
|
993
534
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
this.broadcast("layout");
|
|
1001
|
-
|
|
1002
|
-
const canvasSize = this._glHelper.getLogicalCanvasSize();
|
|
1003
|
-
|
|
1004
|
-
if (isNaN(canvasSize.width) || isNaN(canvasSize.height)) {
|
|
1005
|
-
// TODO: Figure out what causes this
|
|
1006
|
-
console.log(
|
|
1007
|
-
`NaN in canvas size: ${canvasSize.width}x${canvasSize.height}. Skipping computeLayout().`
|
|
1008
|
-
);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
const commonOptions = {
|
|
1013
|
-
webGLHelper: this._glHelper,
|
|
1014
|
-
canvasSize,
|
|
1015
|
-
devicePixelRatio: window.devicePixelRatio ?? 1,
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
this._renderingContext = new BufferedViewRenderingContext(
|
|
1019
|
-
{ picking: false },
|
|
1020
|
-
{
|
|
1021
|
-
...commonOptions,
|
|
1022
|
-
clearColor: this.spec.background,
|
|
1023
|
-
}
|
|
1024
|
-
);
|
|
1025
|
-
this._pickingContext = new BufferedViewRenderingContext(
|
|
1026
|
-
{ picking: true },
|
|
1027
|
-
{
|
|
1028
|
-
...commonOptions,
|
|
1029
|
-
framebufferInfo: this._glHelper._pickingBufferInfo,
|
|
1030
|
-
}
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
root.render(
|
|
1034
|
-
new CompositeViewRenderingContext(
|
|
1035
|
-
this._renderingContext,
|
|
1036
|
-
this._pickingContext
|
|
1037
|
-
),
|
|
1038
|
-
// Canvas should now be sized based on the root view or the container
|
|
1039
|
-
Rectangle.create(0, 0, canvasSize.width, canvasSize.height)
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
// The view coordinates may have not been known during the initial data loading.
|
|
1043
|
-
// Thus, update them so that possible error messages are shown in the correct place.
|
|
1044
|
-
this._updateLoadingIndicators();
|
|
535
|
+
getLogicalCanvasSize() {
|
|
536
|
+
return this.#glHelper.getLogicalCanvasSize();
|
|
537
|
+
}
|
|
1045
538
|
|
|
1046
|
-
|
|
539
|
+
computeLayout() {
|
|
540
|
+
this.#renderCoordinator.computeLayout();
|
|
1047
541
|
}
|
|
1048
542
|
|
|
1049
543
|
renderAll() {
|
|
1050
|
-
this.
|
|
1051
|
-
|
|
1052
|
-
this._dirtyPickingBuffer = true;
|
|
544
|
+
this.#renderCoordinator.renderAll();
|
|
1053
545
|
}
|
|
1054
546
|
|
|
1055
547
|
renderPickingFramebuffer() {
|
|
1056
|
-
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
this._pickingContext.render();
|
|
1061
|
-
this._dirtyPickingBuffer = false;
|
|
548
|
+
this.#renderCoordinator.renderPickingFramebuffer();
|
|
1062
549
|
}
|
|
1063
550
|
|
|
1064
551
|
getSearchableViews() {
|
|
@@ -1073,7 +560,7 @@ export default class GenomeSpy {
|
|
|
1073
560
|
}
|
|
1074
561
|
|
|
1075
562
|
getNamedScaleResolutions() {
|
|
1076
|
-
/** @type {Map<string, import("./
|
|
563
|
+
/** @type {Map<string, import("./scales/scaleResolution.js").default>} */
|
|
1077
564
|
const resolutions = new Map();
|
|
1078
565
|
this.viewRoot.visit((view) => {
|
|
1079
566
|
for (const resolution of Object.values(view.resolutions.scale)) {
|
|
@@ -1085,34 +572,3 @@ export default class GenomeSpy {
|
|
|
1085
572
|
return resolutions;
|
|
1086
573
|
}
|
|
1087
574
|
}
|
|
1088
|
-
|
|
1089
|
-
/**
|
|
1090
|
-
*
|
|
1091
|
-
* @param {HTMLElement} container
|
|
1092
|
-
* @param {string} message
|
|
1093
|
-
*/
|
|
1094
|
-
function createMessageBox(container, message) {
|
|
1095
|
-
// Uh, need a templating thingy
|
|
1096
|
-
const messageBox = document.createElement("div");
|
|
1097
|
-
messageBox.className = "message-box";
|
|
1098
|
-
const messageText = document.createElement("div");
|
|
1099
|
-
messageText.textContent = message;
|
|
1100
|
-
messageBox.appendChild(messageText);
|
|
1101
|
-
container.appendChild(messageBox);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
/**
|
|
1105
|
-
* @param {string} tag
|
|
1106
|
-
* @param {Record<string, any>} attrs
|
|
1107
|
-
*/
|
|
1108
|
-
function element(tag, attrs) {
|
|
1109
|
-
const el = document.createElement(tag);
|
|
1110
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
1111
|
-
if (["innerHTML", "innerText", "className"].includes(key)) {
|
|
1112
|
-
// @ts-ignore
|
|
1113
|
-
el[key] = value;
|
|
1114
|
-
}
|
|
1115
|
-
el.setAttribute(key, value);
|
|
1116
|
-
}
|
|
1117
|
-
return el;
|
|
1118
|
-
}
|