@jbrowse/plugin-linear-genome-view 4.3.0 → 5.0.0-beta.10
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/esm/FeatureTrack/configSchema.d.ts +202 -71
- package/esm/FeatureTrack/configSchema.js +47 -1
- package/esm/FeatureTrack/index.js +1 -1
- package/esm/LaunchLinearGenomeView/index.d.ts +14 -0
- package/esm/LaunchLinearGenomeView/index.js +6 -12
- package/esm/LinearGenomeView/afterAttach.d.ts +20 -2
- package/esm/LinearGenomeView/afterAttach.js +265 -120
- package/esm/LinearGenomeView/components/CenterLine.d.ts +1 -1
- package/esm/LinearGenomeView/components/CenterLine.js +20 -9
- package/esm/LinearGenomeView/components/ConnectedHoverHighlight.d.ts +7 -0
- package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +22 -0
- package/esm/LinearGenomeView/components/CrisprGuidePanel.d.ts +3 -0
- package/esm/LinearGenomeView/components/CrisprGuidePanel.js +142 -0
- package/esm/LinearGenomeView/components/Cytobands.d.ts +6 -6
- package/esm/LinearGenomeView/components/Cytobands.js +76 -53
- package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +8 -3
- package/esm/LinearGenomeView/components/ExportSvgDialog.js +15 -67
- package/esm/LinearGenomeView/components/GetSequenceDialog.d.ts +5 -9
- package/esm/LinearGenomeView/components/GetSequenceDialog.js +84 -88
- package/esm/LinearGenomeView/components/Gridlines.d.ts +1 -1
- package/esm/LinearGenomeView/components/Gridlines.js +29 -58
- package/esm/LinearGenomeView/components/Header.d.ts +1 -1
- package/esm/LinearGenomeView/components/Header.js +42 -16
- package/esm/LinearGenomeView/components/HeaderClearHighlightButton.d.ts +21 -0
- package/esm/LinearGenomeView/components/HeaderClearHighlightButton.js +35 -0
- package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -2
- package/esm/LinearGenomeView/components/HeaderPanControls.js +18 -8
- package/esm/LinearGenomeView/components/HeaderRegionWidth.d.ts +1 -1
- package/esm/LinearGenomeView/components/HeaderRegionWidth.js +9 -6
- package/esm/LinearGenomeView/components/HeaderSearchBoxRow.d.ts +6 -0
- package/esm/LinearGenomeView/components/HeaderSearchBoxRow.js +27 -0
- package/esm/LinearGenomeView/components/HeaderSearchBoxes.d.ts +5 -0
- package/esm/LinearGenomeView/components/HeaderSearchBoxes.js +28 -0
- package/esm/LinearGenomeView/components/HeaderTrackSelectorButton.d.ts +3 -2
- package/esm/LinearGenomeView/components/HeaderTrackSelectorButton.js +11 -4
- package/esm/LinearGenomeView/components/HeaderZoomControls.d.ts +3 -2
- package/esm/LinearGenomeView/components/HeaderZoomControls.js +38 -52
- package/esm/LinearGenomeView/components/Highlight.d.ts +1 -1
- package/esm/LinearGenomeView/components/Highlight.js +21 -25
- package/esm/LinearGenomeView/components/HighlightBand.d.ts +3 -2
- package/esm/LinearGenomeView/components/HighlightBand.js +25 -8
- package/esm/LinearGenomeView/components/HighlightChip.d.ts +8 -0
- package/esm/LinearGenomeView/components/HighlightChip.js +13 -0
- package/esm/LinearGenomeView/components/HoverPositionHighlight.d.ts +12 -0
- package/esm/LinearGenomeView/components/HoverPositionHighlight.js +25 -0
- package/esm/LinearGenomeView/components/ImportForm.d.ts +3 -4
- package/esm/LinearGenomeView/components/ImportForm.js +95 -61
- package/esm/LinearGenomeView/components/LinearGenomeView.d.ts +1 -1
- package/esm/LinearGenomeView/components/LinearGenomeView.js +7 -6
- package/esm/LinearGenomeView/components/LinearGenomeViewContainer.d.ts +1 -1
- package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +88 -30
- package/esm/LinearGenomeView/components/MiniControls.d.ts +1 -1
- package/esm/LinearGenomeView/components/MiniControls.js +14 -10
- package/esm/LinearGenomeView/components/MotifListPanel.d.ts +3 -0
- package/esm/LinearGenomeView/components/MotifListPanel.js +59 -0
- package/esm/LinearGenomeView/components/NavigationAnnouncer.d.ts +32 -0
- package/esm/LinearGenomeView/components/NavigationAnnouncer.js +51 -0
- package/esm/LinearGenomeView/components/NoTracksActiveButton.d.ts +1 -1
- package/esm/LinearGenomeView/components/NoTracksActiveButton.js +1 -1
- package/esm/LinearGenomeView/components/OverviewHighlight.d.ts +2 -4
- package/esm/LinearGenomeView/components/OverviewHighlight.js +23 -44
- package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +9 -0
- package/esm/LinearGenomeView/components/OverviewHighlightBand.js +25 -0
- package/esm/LinearGenomeView/components/OverviewRubberband.d.ts +3 -3
- package/esm/LinearGenomeView/components/OverviewRubberband.js +29 -67
- package/esm/LinearGenomeView/components/OverviewRubberbandHoverTooltip.d.ts +4 -5
- package/esm/LinearGenomeView/components/OverviewRubberbandHoverTooltip.js +13 -20
- package/esm/LinearGenomeView/components/OverviewScalebar.d.ts +3 -2
- package/esm/LinearGenomeView/components/OverviewScalebar.js +87 -83
- package/esm/LinearGenomeView/components/OverviewScalebarPolygon.d.ts +19 -5
- package/esm/LinearGenomeView/components/OverviewScalebarPolygon.js +57 -32
- package/esm/LinearGenomeView/components/OverviewScalebarTickLabels.d.ts +4 -5
- package/esm/LinearGenomeView/components/OverviewScalebarTickLabels.js +16 -18
- package/esm/LinearGenomeView/components/PaddingBlocks.d.ts +7 -0
- package/esm/LinearGenomeView/components/PaddingBlocks.js +56 -0
- package/esm/LinearGenomeView/components/RangeSelectOverlay.d.ts +9 -0
- package/esm/LinearGenomeView/components/RangeSelectOverlay.js +19 -0
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +2 -3
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +19 -26
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.d.ts +5 -0
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +71 -0
- package/esm/LinearGenomeView/components/Rubberband.d.ts +2 -3
- package/esm/LinearGenomeView/components/Rubberband.js +15 -17
- package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -0
- package/esm/LinearGenomeView/components/SVGHighlightBand.js +8 -0
- package/esm/LinearGenomeView/components/Scalebar.d.ts +2 -6
- package/esm/LinearGenomeView/components/Scalebar.js +20 -22
- package/esm/LinearGenomeView/components/ScalebarCoordinateLabels.d.ts +1 -1
- package/esm/LinearGenomeView/components/ScalebarCoordinateLabels.js +54 -35
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.d.ts +1 -1
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +157 -126
- package/esm/LinearGenomeView/components/SearchBox.d.ts +22 -2
- package/esm/LinearGenomeView/components/SearchBox.js +61 -68
- package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +9 -0
- package/esm/LinearGenomeView/components/SearchPanelForm.js +18 -0
- package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +5 -4
- package/esm/LinearGenomeView/components/SearchResultsDialog.js +6 -8
- package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +5 -3
- package/esm/LinearGenomeView/components/SearchResultsTable.js +31 -56
- package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.d.ts +5 -0
- package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.js +37 -0
- package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlightExtension.d.ts +2 -0
- package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlightExtension.js +5 -0
- package/esm/LinearGenomeView/components/SequencePatternPanel.d.ts +3 -0
- package/esm/LinearGenomeView/components/SequencePatternPanel.js +42 -0
- package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +3 -5
- package/esm/LinearGenomeView/components/SequenceSearchDialog.js +31 -53
- package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +8 -0
- package/esm/LinearGenomeView/components/StrandCheckboxes.js +13 -0
- package/esm/LinearGenomeView/components/TrackContainer.d.ts +1 -1
- package/esm/LinearGenomeView/components/TrackContainer.js +136 -38
- package/esm/LinearGenomeView/components/TrackLabel.d.ts +2 -3
- package/esm/LinearGenomeView/components/TrackLabel.js +20 -26
- package/esm/LinearGenomeView/components/TrackLabelButtons.d.ts +7 -0
- package/esm/LinearGenomeView/components/TrackLabelButtons.js +33 -0
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.d.ts +2 -5
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +6 -2
- package/esm/LinearGenomeView/components/TrackLabelMenu.d.ts +1 -1
- package/esm/LinearGenomeView/components/TrackLabelMenu.js +35 -87
- package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.d.ts +22 -0
- package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.js +46 -0
- package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +5 -2
- package/esm/LinearGenomeView/components/TrackRenderingContainer.js +66 -24
- package/esm/LinearGenomeView/components/TracksContainer.d.ts +1 -1
- package/esm/LinearGenomeView/components/TracksContainer.js +53 -32
- package/esm/LinearGenomeView/components/VerticalGuide.d.ts +2 -3
- package/esm/LinearGenomeView/components/VerticalGuide.js +6 -26
- package/esm/LinearGenomeView/components/ZoomButton.d.ts +7 -0
- package/esm/LinearGenomeView/components/ZoomButton.js +27 -0
- package/esm/LinearGenomeView/components/ZoomTransform.d.ts +9 -0
- package/esm/LinearGenomeView/components/ZoomTransform.js +37 -0
- package/esm/LinearGenomeView/components/defaultMotifs.d.ts +1 -0
- package/esm/LinearGenomeView/components/defaultMotifs.js +22 -0
- package/esm/LinearGenomeView/components/fetchSequence.d.ts +6 -8
- package/esm/LinearGenomeView/components/fetchSequence.js +17 -9
- package/esm/LinearGenomeView/components/recentLocationsMenu.d.ts +15 -0
- package/esm/LinearGenomeView/components/recentLocationsMenu.js +46 -0
- package/esm/LinearGenomeView/components/searchModes.d.ts +13 -0
- package/esm/LinearGenomeView/components/searchModes.js +22 -0
- package/esm/LinearGenomeView/components/trackLabelMenuItems.d.ts +7 -0
- package/esm/LinearGenomeView/components/trackLabelMenuItems.js +67 -0
- package/esm/LinearGenomeView/components/useRangeSelect.d.ts +9 -67
- package/esm/LinearGenomeView/components/useRangeSelect.js +54 -102
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +26 -0
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +58 -0
- package/esm/LinearGenomeView/components/useSideScroll.d.ts +17 -3
- package/esm/LinearGenomeView/components/useSideScroll.js +163 -45
- package/esm/LinearGenomeView/components/util.d.ts +24 -6
- package/esm/LinearGenomeView/components/util.js +28 -5
- package/esm/LinearGenomeView/consts.d.ts +9 -2
- package/esm/LinearGenomeView/consts.js +20 -2
- package/esm/LinearGenomeView/containingLgv.d.ts +21 -0
- package/esm/LinearGenomeView/containingLgv.js +23 -0
- package/esm/LinearGenomeView/flyTo.d.ts +38 -0
- package/esm/LinearGenomeView/flyTo.js +135 -0
- package/esm/LinearGenomeView/headerFit.d.ts +29 -0
- package/esm/LinearGenomeView/headerFit.js +72 -0
- package/esm/LinearGenomeView/index.d.ts +28 -3
- package/esm/LinearGenomeView/index.js +13 -7
- package/esm/LinearGenomeView/keyboardHandler.d.ts +9 -0
- package/esm/LinearGenomeView/keyboardHandler.js +25 -1
- package/esm/LinearGenomeView/launchKeys.d.ts +2 -0
- package/esm/LinearGenomeView/launchKeys.js +27 -0
- package/esm/LinearGenomeView/lazyDialogs.d.ts +8 -14
- package/esm/LinearGenomeView/lazyDialogs.js +4 -3
- package/esm/LinearGenomeView/linkedViewSync.d.ts +12 -0
- package/esm/LinearGenomeView/linkedViewSync.js +36 -0
- package/esm/LinearGenomeView/menuItems.d.ts +25 -2
- package/esm/LinearGenomeView/menuItems.js +306 -179
- package/esm/LinearGenomeView/model.d.ts +3363 -164
- package/esm/LinearGenomeView/model.js +2548 -565
- package/esm/LinearGenomeView/sharedScaleContainer.d.ts +18 -0
- package/esm/LinearGenomeView/sharedScaleContainer.js +17 -0
- package/esm/LinearGenomeView/showRegionsWithUndo.d.ts +35 -0
- package/esm/LinearGenomeView/showRegionsWithUndo.js +50 -0
- package/esm/LinearGenomeView/stickyChrome.d.ts +15 -0
- package/esm/LinearGenomeView/stickyChrome.js +26 -0
- package/esm/LinearGenomeView/svgcomponents/SVGGridlines.d.ts +1 -1
- package/esm/LinearGenomeView/svgcomponents/SVGGridlines.js +17 -18
- package/esm/LinearGenomeView/svgcomponents/SVGHeader.d.ts +5 -3
- package/esm/LinearGenomeView/svgcomponents/SVGHeader.js +34 -35
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +5 -0
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +26 -0
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +5 -0
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +13 -0
- package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.d.ts +1 -3
- package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +73 -41
- package/esm/LinearGenomeView/svgcomponents/SVGRegionSeparators.d.ts +1 -1
- package/esm/LinearGenomeView/svgcomponents/SVGRegionSeparators.js +20 -3
- package/esm/LinearGenomeView/svgcomponents/SVGRowHeader.d.ts +8 -0
- package/esm/LinearGenomeView/svgcomponents/SVGRowHeader.js +36 -0
- package/esm/LinearGenomeView/svgcomponents/SVGRuler.d.ts +3 -3
- package/esm/LinearGenomeView/svgcomponents/SVGRuler.js +66 -39
- package/esm/LinearGenomeView/svgcomponents/SVGScalebar.d.ts +1 -1
- package/esm/LinearGenomeView/svgcomponents/SVGScalebar.js +9 -7
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +27 -0
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +12 -0
- package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.d.ts +5 -3
- package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +54 -9
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +9 -15
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +17 -21
- package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +18 -0
- package/esm/LinearGenomeView/svgcomponents/SVGView.js +13 -0
- package/esm/LinearGenomeView/svgcomponents/figureContract.d.ts +45 -0
- package/esm/LinearGenomeView/svgcomponents/figureContract.js +130 -0
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +76 -0
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +66 -0
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.d.ts +70 -0
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +245 -0
- package/esm/LinearGenomeView/svgcomponents/util.d.ts +96 -5
- package/esm/LinearGenomeView/svgcomponents/util.js +242 -4
- package/esm/LinearGenomeView/types.d.ts +20 -37
- package/esm/LinearGenomeView/types.js +1 -0
- package/esm/LinearGenomeView/util.d.ts +347 -12
- package/esm/LinearGenomeView/util.js +577 -62
- package/esm/MultiLevelRubberband/MultiLevelRubberband.d.ts +6 -0
- package/esm/MultiLevelRubberband/MultiLevelRubberband.js +40 -0
- package/esm/MultiLevelRubberband/index.d.ts +4 -0
- package/esm/MultiLevelRubberband/index.js +3 -0
- package/esm/MultiLevelRubberband/menuItems.d.ts +34 -0
- package/esm/MultiLevelRubberband/menuItems.js +52 -0
- package/esm/MultiLevelRubberband/rowLabel.d.ts +22 -0
- package/esm/MultiLevelRubberband/rowLabel.js +27 -0
- package/esm/MultiLevelRubberband/types.d.ts +7 -0
- package/esm/MultiLevelRubberband/useRangeSelect.d.ts +25 -0
- package/esm/MultiLevelRubberband/useRangeSelect.js +120 -0
- package/esm/displayKitTests/perRegionTestEnv.d.ts +1391 -0
- package/esm/displayKitTests/perRegionTestEnv.js +142 -0
- package/esm/index.d.ts +50 -1245
- package/esm/index.js +88 -33
- package/esm/searchUtils.d.ts +62 -12
- package/esm/searchUtils.js +335 -48
- package/esm/shared/RubberbandSpan.d.ts +15 -0
- package/esm/shared/RubberbandSpan.js +34 -0
- package/esm/shared/coordLabels.d.ts +47 -0
- package/esm/shared/coordLabels.js +133 -0
- package/esm/shared/useWindowDrag.d.ts +18 -0
- package/esm/shared/useWindowDrag.js +45 -0
- package/package.json +18 -19
- package/esm/BaseLinearDisplay/SVGLegend.d.ts +0 -6
- package/esm/BaseLinearDisplay/SVGLegend.js +0 -17
- package/esm/BaseLinearDisplay/calculateSvgLegendWidth.d.ts +0 -2
- package/esm/BaseLinearDisplay/calculateSvgLegendWidth.js +0 -11
- package/esm/BaseLinearDisplay/components/BaseLinearDisplay.d.ts +0 -8
- package/esm/BaseLinearDisplay/components/BaseLinearDisplay.js +0 -53
- package/esm/BaseLinearDisplay/components/Block.d.ts +0 -14
- package/esm/BaseLinearDisplay/components/Block.js +0 -45
- package/esm/BaseLinearDisplay/components/BlockErrorMessage.d.ts +0 -7
- package/esm/BaseLinearDisplay/components/BlockErrorMessage.js +0 -22
- package/esm/BaseLinearDisplay/components/BlockMsg.d.ts +0 -6
- package/esm/BaseLinearDisplay/components/BlockMsg.js +0 -26
- package/esm/BaseLinearDisplay/components/CollapseIntronsDialog/CollapseIntronsDialog.d.ts +0 -9
- package/esm/BaseLinearDisplay/components/CollapseIntronsDialog/CollapseIntronsDialog.js +0 -42
- package/esm/BaseLinearDisplay/components/CollapseIntronsDialog/TranscriptTable.d.ts +0 -11
- package/esm/BaseLinearDisplay/components/CollapseIntronsDialog/TranscriptTable.js +0 -40
- package/esm/BaseLinearDisplay/components/CollapseIntronsDialog/util.d.ts +0 -19
- package/esm/BaseLinearDisplay/components/CollapseIntronsDialog/util.js +0 -45
- package/esm/BaseLinearDisplay/components/FloatingLabels.d.ts +0 -5
- package/esm/BaseLinearDisplay/components/FloatingLabels.js +0 -75
- package/esm/BaseLinearDisplay/components/FloatingLegend.d.ts +0 -8
- package/esm/BaseLinearDisplay/components/FloatingLegend.js +0 -43
- package/esm/BaseLinearDisplay/components/LinearBlocks.d.ts +0 -5
- package/esm/BaseLinearDisplay/components/LinearBlocks.js +0 -23
- package/esm/BaseLinearDisplay/components/LoadingOverlay.d.ts +0 -5
- package/esm/BaseLinearDisplay/components/LoadingOverlay.js +0 -49
- package/esm/BaseLinearDisplay/components/MaxHeightReachedIndicator.d.ts +0 -4
- package/esm/BaseLinearDisplay/components/MaxHeightReachedIndicator.js +0 -26
- package/esm/BaseLinearDisplay/components/MenuPage.d.ts +0 -8
- package/esm/BaseLinearDisplay/components/MenuPage.js +0 -24
- package/esm/BaseLinearDisplay/components/NonBlockCanvasDisplayComponent.d.ts +0 -21
- package/esm/BaseLinearDisplay/components/NonBlockCanvasDisplayComponent.js +0 -52
- package/esm/BaseLinearDisplay/components/RenderedBlocks.d.ts +0 -12
- package/esm/BaseLinearDisplay/components/RenderedBlocks.js +0 -22
- package/esm/BaseLinearDisplay/components/ServerSideRenderedBlockContent.d.ts +0 -12
- package/esm/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js +0 -18
- package/esm/BaseLinearDisplay/components/TooLargeMessage.d.ts +0 -10
- package/esm/BaseLinearDisplay/components/TooLargeMessage.js +0 -20
- package/esm/BaseLinearDisplay/components/Tooltip.d.ts +0 -13
- package/esm/BaseLinearDisplay/components/Tooltip.js +0 -22
- package/esm/BaseLinearDisplay/components/types.d.ts +0 -1
- package/esm/BaseLinearDisplay/components/util.d.ts +0 -21
- package/esm/BaseLinearDisplay/components/util.js +0 -107
- package/esm/BaseLinearDisplay/index.d.ts +0 -17
- package/esm/BaseLinearDisplay/index.js +0 -13
- package/esm/BaseLinearDisplay/model.d.ts +0 -1197
- package/esm/BaseLinearDisplay/model.js +0 -432
- package/esm/BaseLinearDisplay/models/FeatureDensityMixin.d.ts +0 -32
- package/esm/BaseLinearDisplay/models/FeatureDensityMixin.js +0 -113
- package/esm/BaseLinearDisplay/models/NonBlockCanvasDisplayMixin.d.ts +0 -26
- package/esm/BaseLinearDisplay/models/NonBlockCanvasDisplayMixin.js +0 -57
- package/esm/BaseLinearDisplay/models/SvgFloatingLabels.d.ts +0 -8
- package/esm/BaseLinearDisplay/models/SvgFloatingLabels.js +0 -19
- package/esm/BaseLinearDisplay/models/TrackHeightMixin.d.ts +0 -11
- package/esm/BaseLinearDisplay/models/TrackHeightMixin.js +0 -31
- package/esm/BaseLinearDisplay/models/autorunFeatureDensityStats.d.ts +0 -2
- package/esm/BaseLinearDisplay/models/autorunFeatureDensityStats.js +0 -29
- package/esm/BaseLinearDisplay/models/configSchema.d.ts +0 -29
- package/esm/BaseLinearDisplay/models/configSchema.js +0 -33
- package/esm/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +0 -98
- package/esm/BaseLinearDisplay/models/serverSideRenderedBlock.js +0 -242
- package/esm/BaseLinearDisplay/models/util.d.ts +0 -9
- package/esm/BaseLinearDisplay/models/util.js +0 -43
- package/esm/BaseLinearDisplay/renderSvg.d.ts +0 -3
- package/esm/BaseLinearDisplay/renderSvg.js +0 -63
- package/esm/BaseLinearDisplay/types.d.ts +0 -43
- package/esm/BaseLinearDisplay/types.js +0 -3
- package/esm/BaseLinearDisplay/util.d.ts +0 -16
- package/esm/BaseLinearDisplay/util.js +0 -71
- package/esm/BasicTrack/configSchema.d.ts +0 -85
- package/esm/BasicTrack/configSchema.js +0 -8
- package/esm/BasicTrack/index.d.ts +0 -2
- package/esm/BasicTrack/index.js +0 -12
- package/esm/LinearBareDisplay/configSchema.d.ts +0 -32
- package/esm/LinearBareDisplay/configSchema.js +0 -11
- package/esm/LinearBareDisplay/index.d.ts +0 -4
- package/esm/LinearBareDisplay/index.js +0 -20
- package/esm/LinearBareDisplay/model.d.ts +0 -1196
- package/esm/LinearBareDisplay/model.js +0 -32
- package/esm/LinearBasicDisplay/components/LinearBasicDisplayComponent.d.ts +0 -5
- package/esm/LinearBasicDisplay/components/LinearBasicDisplayComponent.js +0 -9
- package/esm/LinearBasicDisplay/configSchema.d.ts +0 -32
- package/esm/LinearBasicDisplay/configSchema.js +0 -11
- package/esm/LinearBasicDisplay/index.d.ts +0 -4
- package/esm/LinearBasicDisplay/index.js +0 -20
- package/esm/LinearBasicDisplay/model.d.ts +0 -1391
- package/esm/LinearBasicDisplay/model.js +0 -158
- package/esm/LinearFeatureDisplay/components/AddFiltersDialog.d.ts +0 -9
- package/esm/LinearFeatureDisplay/components/AddFiltersDialog.js +0 -58
- package/esm/LinearFeatureDisplay/components/SetMaxHeightDialog.d.ts +0 -8
- package/esm/LinearFeatureDisplay/components/SetMaxHeightDialog.js +0 -26
- package/esm/LinearFeatureDisplay/configSchema.d.ts +0 -32
- package/esm/LinearFeatureDisplay/configSchema.js +0 -11
- package/esm/LinearFeatureDisplay/index.d.ts +0 -3
- package/esm/LinearFeatureDisplay/index.js +0 -2
- package/esm/LinearFeatureDisplay/model.d.ts +0 -1443
- package/esm/LinearFeatureDisplay/model.js +0 -305
- package/esm/LinearGenomeView/SVGErrorBox.d.ts +0 -5
- package/esm/LinearGenomeView/SVGErrorBox.js +0 -4
- package/esm/LinearGenomeView/components/ImportFormRefNameAutocomplete.d.ts +0 -11
- package/esm/LinearGenomeView/components/ImportFormRefNameAutocomplete.js +0 -27
- package/esm/LinearGenomeView/components/RefNameAutocomplete/AutocompleteTextField.d.ts +0 -5
- package/esm/LinearGenomeView/components/RefNameAutocomplete/AutocompleteTextField.js +0 -13
- package/esm/LinearGenomeView/components/RefNameAutocomplete/EndAdornment.d.ts +0 -3
- package/esm/LinearGenomeView/components/RefNameAutocomplete/EndAdornment.js +0 -17
- package/esm/LinearGenomeView/components/RefNameAutocomplete/HelpDialog.d.ts +0 -3
- package/esm/LinearGenomeView/components/RefNameAutocomplete/HelpDialog.js +0 -8
- package/esm/LinearGenomeView/components/RefNameAutocomplete/index.d.ts +0 -16
- package/esm/LinearGenomeView/components/RefNameAutocomplete/index.js +0 -101
- package/esm/LinearGenomeView/components/RefNameAutocomplete/util.d.ts +0 -10
- package/esm/LinearGenomeView/components/RefNameAutocomplete/util.js +0 -45
- package/esm/LinearGenomeView/components/RubberbandSpan.d.ts +0 -15
- package/esm/LinearGenomeView/components/RubberbandSpan.js +0 -32
- package/esm/LinearGenomeView/components/RubberbandTooltip.d.ts +0 -5
- package/esm/LinearGenomeView/components/RubberbandTooltip.js +0 -17
- package/esm/LinearGenomeView/components/TrackLabelContainer.d.ts +0 -8
- package/esm/LinearGenomeView/components/TrackLabelContainer.js +0 -25
- package/esm/LinearGenomeView/components/useWheelScroll.d.ts +0 -7
- package/esm/LinearGenomeView/components/useWheelScroll.js +0 -69
- package/esm/LinearGenomeView/svgExportUtil.d.ts +0 -9
- package/esm/LinearGenomeView/svgExportUtil.js +0 -6
- package/esm/LinearGenomeView/svgcomponents/SVGBackground.d.ts +0 -5
- package/esm/LinearGenomeView/svgcomponents/SVGBackground.js +0 -7
- /package/esm/{BaseLinearDisplay/components → MultiLevelRubberband}/types.js +0 -0
|
@@ -1,186 +1,779 @@
|
|
|
1
1
|
import { lazy } from 'react';
|
|
2
2
|
import { getConf } from '@jbrowse/core/configuration';
|
|
3
3
|
import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
|
|
4
|
-
import {
|
|
5
|
-
import { assembleLocString, clamp,
|
|
6
|
-
import { bpToPx, moveTo, pxToBp } from '@jbrowse/core/util/Base1DUtils';
|
|
7
|
-
import
|
|
4
|
+
import { exportViewSvg } from '@jbrowse/core/svg/exportViewSvg';
|
|
5
|
+
import { assembleLocString, clamp, getBpDisplayStr, getDialogHost, getSession, getTickDisplayStr, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, springAnimate, sum, } from '@jbrowse/core/util';
|
|
6
|
+
import { bpToLinearBp, bpToPx, computeMoveToLayout, createOverviewLayout, getLayoutHighlightCoords, getOverviewRegionPxSpan, moveTo, pxToBp, } from '@jbrowse/core/util/Base1DUtils';
|
|
7
|
+
import { scaleBandHeights } from '@jbrowse/core/util/bandHeight';
|
|
8
|
+
import { wholeBaseRegions } from '@jbrowse/core/util/blockTypes';
|
|
8
9
|
import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
|
|
9
10
|
import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
|
|
10
|
-
import {
|
|
11
|
+
import { tickLabelsWorthDrawing } from '@jbrowse/core/util/tickLabels';
|
|
12
|
+
import { hideTrackGeneric, launchToggleTrackGeneric, launchTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
|
|
11
13
|
import { ElementId } from '@jbrowse/core/util/types/mst';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
14
|
+
import { highlightsOnAssemblies } from '@jbrowse/core/util/viewHighlights';
|
|
15
|
+
import { assemblyErrorMessage, computeViewStatus, viewLoading, } from '@jbrowse/core/util/viewStatus';
|
|
16
|
+
import { pendingLaunch, withLaunchInput, } from '@jbrowse/core/util/withLaunchInput';
|
|
17
|
+
import { MIN_DISPLAY_HEIGHT } from '@jbrowse/display-kit/const';
|
|
18
|
+
import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
|
|
19
|
+
import { cast, getParent, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
|
|
20
|
+
import { observable, when } from 'mobx';
|
|
21
|
+
import { handleSelectedRegion, navToOption } from '../searchUtils.js';
|
|
22
|
+
import { doAfterAttach } from './afterAttach.js';
|
|
23
|
+
import { getCytobands, shouldSwapTracks } from './components/util.js';
|
|
24
|
+
import { HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, MIN_BP_PER_PX, MINIMIZED_TRACK_HEIGHT, MINIMUM_BLOCK_WIDTH, RESIZE_ALL_HANDLE_HEIGHT, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, SHOW_ALL_REGIONS_FILL, TRACK_OUTLINE_BORDER, TRACK_TOP_GAP, } from './consts.js';
|
|
25
|
+
import { planFlight } from './flyTo.js';
|
|
26
|
+
import { setupKeyboardHandler } from './keyboardHandler.js';
|
|
27
|
+
import { lgvLaunchKeys } from './launchKeys.js';
|
|
28
|
+
import { buildHighlightsSubMenuItems, buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, } from './menuItems.js';
|
|
29
|
+
import { sharedScaleContainerOf } from './sharedScaleContainer.js';
|
|
30
|
+
import { calculateVisibleLocStrings, cytobandLabelGutterWidth, expandRegion, fitAllRegionsWindow, generateLocations, getScalebarRefNameLabels, regionsOrientation, groupContiguousBlocks, labelFitsInBlock, makeBlockTicks, runRefNameLabelPx, setDisplayedRegionsKeepingCenter, tickLabelWidth, } from './util.js';
|
|
31
|
+
// lazies
|
|
32
|
+
const SearchResultsDialog = lazy(() => import('./components/SearchResultsDialog.js'));
|
|
33
|
+
// The one array `paddingSpans` returns when a view has nothing to mask, so that
|
|
34
|
+
// computed's value repeats by identity. Frozen because every consumer maps over
|
|
35
|
+
// it and a caller that sorted in place would do so for the whole session.
|
|
36
|
+
const NO_PADDING_SPANS = Object.freeze([]);
|
|
37
|
+
/**
|
|
38
|
+
* Calculate the offsetPx needed to center content within a viewport.
|
|
39
|
+
* Returns a negative offset when content is smaller than viewport (padding on left).
|
|
40
|
+
*/
|
|
24
41
|
function getCenteredOffsetPx(contentPx, viewportPx) {
|
|
25
42
|
return Math.round(contentPx / 2 - viewportPx / 2);
|
|
26
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A refName in the named assembly's canonical spelling, or unchanged when that
|
|
46
|
+
* assembly is missing or unknown, so a highlight authored without one still
|
|
47
|
+
* lands in a single-assembly view.
|
|
48
|
+
*/
|
|
49
|
+
function canonicalRefName(assemblyManager, assemblyName, refName) {
|
|
50
|
+
const asm = assemblyName ? assemblyManager.get(assemblyName) : undefined;
|
|
51
|
+
return asm?.getCanonicalRefName2(refName) ?? refName;
|
|
52
|
+
}
|
|
53
|
+
function canonicalHighlight(self, region) {
|
|
54
|
+
const { assemblyManager } = getSession(self);
|
|
55
|
+
return {
|
|
56
|
+
...region,
|
|
57
|
+
refName: canonicalRefName(assemblyManager, region.assemblyName, region.refName),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// bpPerPx deltas smaller than this are treated as no zoom change, avoiding
|
|
61
|
+
// pointless offset re-anchoring on micro-steps
|
|
62
|
+
const BP_PER_PX_EPSILON = 0.000001;
|
|
63
|
+
// px of the rightmost content kept on-screen at max scroll-right, so the genome
|
|
64
|
+
// can't be scrolled entirely off the left edge. Shared by maxOffset and
|
|
65
|
+
// getSelectedRegions' clamp so the two bounds can't drift apart.
|
|
66
|
+
const MAX_OFFSET_PADDING_PX = 10;
|
|
67
|
+
// px of the leftmost content kept on-screen at max scroll-left, mirroring
|
|
68
|
+
// MAX_OFFSET_PADDING_PX at the other end
|
|
69
|
+
const MIN_OFFSET_PADDING_PX = 30;
|
|
70
|
+
// whether two BlockSets cover the same blocks, by key and in order
|
|
71
|
+
function sameBlockKeys(a, b) {
|
|
72
|
+
return (a.blocks.length === b.blocks.length &&
|
|
73
|
+
a.blocks.every((block, i) => block.key === b.blocks[i].key));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve one end of a navigation range to the displayedRegion index that
|
|
77
|
+
* contains it plus the bp offset into that region. `side` selects which edge of
|
|
78
|
+
* the (grow-expanded) interval anchors the offset — 'left' for the range start,
|
|
79
|
+
* 'right' for the range end — accounting for reversed regions. Omitted
|
|
80
|
+
* start/end default to the first occurrence of the refName; containment is then
|
|
81
|
+
* resolved against those raw coords, and only the region it picks bounds `grow`.
|
|
82
|
+
*/
|
|
83
|
+
function resolveNavEndpoint({ location, assemblyManager, defaultAssemblyName, side, displayedRegions, grow, }) {
|
|
84
|
+
const refName = canonicalRefName(assemblyManager, location.assemblyName || defaultAssemblyName, location.refName);
|
|
85
|
+
const first = displayedRegions.find(r => r.refName === refName);
|
|
86
|
+
if (!first) {
|
|
87
|
+
throw new Error(`could not find a region with refName "${refName}"`);
|
|
88
|
+
}
|
|
89
|
+
const rawStart = location.start ?? first.start;
|
|
90
|
+
const rawEnd = location.end ?? first.end;
|
|
91
|
+
const index = displayedRegions.findIndex(r => r.refName === refName &&
|
|
92
|
+
rawStart >= r.start &&
|
|
93
|
+
rawStart <= r.end &&
|
|
94
|
+
rawEnd <= r.end &&
|
|
95
|
+
rawEnd >= r.start);
|
|
96
|
+
if (index === -1) {
|
|
97
|
+
throw new Error(`could not find a region that contained "${assembleLocString(location)}"`);
|
|
98
|
+
}
|
|
99
|
+
const r = displayedRegions[index];
|
|
100
|
+
// grow AFTER resolving the containing region, and clamp to that region — not
|
|
101
|
+
// to the first occurrence of the refName. Growing first clamped the padded
|
|
102
|
+
// interval to a region that need not be the one containing it, so on a
|
|
103
|
+
// duplicated refName (collapsed introns, a refName displayed twice) the
|
|
104
|
+
// clamp dragged an endpoint back to the wrong region and the containment
|
|
105
|
+
// search below then found nothing: `navTo(loc)` succeeded where
|
|
106
|
+
// `navTo(loc, 0.2)` threw for the very same loc.
|
|
107
|
+
const { start, end } = grow
|
|
108
|
+
? expandRegion(rawStart, rawEnd, grow, r.start, r.end)
|
|
109
|
+
: { start: rawStart, end: rawEnd };
|
|
110
|
+
const leftEdge = r.reversed ? r.end - end : start - r.start;
|
|
111
|
+
const rightEdge = r.reversed ? r.end - start : end - r.start;
|
|
112
|
+
return { index, offset: side === 'left' ? leftEdge : rightEdge };
|
|
113
|
+
}
|
|
114
|
+
// Widget id of the hierarchical track selector. One id, shared by the two
|
|
115
|
+
// helpers below, since `isTrackSelectorVisible` identifies the widget by id
|
|
116
|
+
// rather than by holding a reference to it.
|
|
117
|
+
const TRACK_SELECTOR_WIDGET_ID = 'hierarchicalTrackSelector';
|
|
118
|
+
/**
|
|
119
|
+
* Add (or reuse) the hierarchical track-selector widget for this view. Returns
|
|
120
|
+
* the widget alongside the (narrowed) session so callers can show/hide it.
|
|
121
|
+
*/
|
|
122
|
+
function openTrackSelectorWidget(self) {
|
|
123
|
+
const session = getSession(self);
|
|
124
|
+
if (isSessionModelWithWidgets(session)) {
|
|
125
|
+
const selector = session.addWidget('HierarchicalTrackSelectorWidget', TRACK_SELECTOR_WIDGET_ID, { view: self });
|
|
126
|
+
return { session, selector };
|
|
127
|
+
}
|
|
128
|
+
throw new Error('session does not support widgets');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether the track selector is the drawer widget on screen right now. One
|
|
132
|
+
* spelling for the `isTrackSelectorOpen` getter (which lights up the header
|
|
133
|
+
* button) and for `toggleTrackSelector` (which decides hide-vs-show) — the
|
|
134
|
+
* `!minimized` term is exactly the kind of thing that goes into one of two
|
|
135
|
+
* hand-written copies and not the other.
|
|
136
|
+
*/
|
|
137
|
+
function isTrackSelectorVisible(self) {
|
|
138
|
+
const session = getSession(self);
|
|
139
|
+
return (isSessionModelWithWidgets(session) &&
|
|
140
|
+
session.visibleWidget?.id === TRACK_SELECTOR_WIDGET_ID &&
|
|
141
|
+
!session.minimized);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* #stateModel LinearGenomeView
|
|
145
|
+
* #category view
|
|
146
|
+
*
|
|
147
|
+
* #example
|
|
148
|
+
* A `LinearGenomeView` is what you hand-author under `defaultSession.views`, and
|
|
149
|
+
* every setting goes directly on the view object. `assembly`/`loc` fill in
|
|
150
|
+
* `displayedRegions`/`bpPerPx`/`offsetPx` for you:
|
|
151
|
+
* ```js
|
|
152
|
+
* defaultSession: {
|
|
153
|
+
* name: 'My session',
|
|
154
|
+
* views: [
|
|
155
|
+
* {
|
|
156
|
+
* type: 'LinearGenomeView',
|
|
157
|
+
* colorByCDS: true,
|
|
158
|
+
* assembly: 'hg38',
|
|
159
|
+
* loc: 'chr1:1,000,000-1,100,000',
|
|
160
|
+
* tracks: ['genes', 'alignments'],
|
|
161
|
+
* },
|
|
162
|
+
* ],
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
* `assembly`, `loc`, `tracks`, `tracklist`, `nav` and `highlight` need
|
|
166
|
+
* on-attach resolution and are captured into the `launch` property below;
|
|
167
|
+
* `colorByCDS`, `showAminoAcids`, `showCenterLine` and `trackLabels` are plain
|
|
168
|
+
* view props MST restores natively. Both are written the same way.
|
|
169
|
+
* At runtime the same model is driven imperatively — every property and action
|
|
170
|
+
* below is reachable on `viewState.session.views[0]`:
|
|
171
|
+
* ```js
|
|
172
|
+
* const view = viewState.session.views[0]
|
|
173
|
+
* await view.navToLocString('chr1:2,000,000-2,100,000')
|
|
174
|
+
* view.showTrack('alignments')
|
|
175
|
+
* view.zoomTo(view.bpPerPx * 2) // zoom out 2x
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
27
178
|
export function stateModelFactory(pluginManager) {
|
|
28
|
-
|
|
179
|
+
const model = types
|
|
29
180
|
.compose('LinearGenomeView', BaseViewModel, types.model({
|
|
181
|
+
/**
|
|
182
|
+
* #property
|
|
183
|
+
*/
|
|
30
184
|
id: ElementId,
|
|
185
|
+
/**
|
|
186
|
+
* #property
|
|
187
|
+
* this is a string instead of the const literal 'LinearGenomeView' to
|
|
188
|
+
* reduce some typescripting strictness, but you should pass the string
|
|
189
|
+
* 'LinearGenomeView' to the model explicitly
|
|
190
|
+
*/
|
|
31
191
|
type: types.literal('LinearGenomeView'),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
192
|
+
/**
|
|
193
|
+
* #property
|
|
194
|
+
* Left edge of the viewport, in linearized bp — the concatenated
|
|
195
|
+
* `displayedRegions` space that `offsetPx` indexes, which carries no
|
|
196
|
+
* inter-region padding, so the two differ only by `bpPerPx`. May be
|
|
197
|
+
* negative, which is the view scrolled past the left end.
|
|
198
|
+
*
|
|
199
|
+
* The viewport is stored as the genomic WINDOW it frames rather than as
|
|
200
|
+
* the pixels that framed it, because pixels mean nothing without the
|
|
201
|
+
* width they were measured at and a snapshot does not carry one. Storing
|
|
202
|
+
* them anyway is why a session authored in a 1000px window used to open
|
|
203
|
+
* at 500px showing half the region its author was looking at, while the
|
|
204
|
+
* same location as a `&loc=` opened correctly — the two ways to share a
|
|
205
|
+
* view disagreed, and only the one that stores intent was right.
|
|
206
|
+
*/
|
|
207
|
+
windowStartBp: types.stripDefault(types.number, 0),
|
|
208
|
+
/**
|
|
209
|
+
* #property
|
|
210
|
+
* Width of the viewport in bp. Zero means "not established yet": no
|
|
211
|
+
* width has been measured, so there is nothing to divide by. The first
|
|
212
|
+
* measure fills it in, and `bpPerPx` is `windowWidthBp / width` from
|
|
213
|
+
* then on.
|
|
214
|
+
*/
|
|
215
|
+
windowWidthBp: types.stripDefault(types.number, 0),
|
|
216
|
+
/**
|
|
217
|
+
* #property
|
|
218
|
+
* MIGRATION ONLY, and permanent: share links written before the window
|
|
219
|
+
* was stored carry `bpPerPx`/`offsetPx`, and there are many of them.
|
|
220
|
+
*
|
|
221
|
+
* A snapshot written before the window was stored carries `offsetPx` and
|
|
222
|
+
* `bpPerPx` but not the width they were measured at, so the window they
|
|
223
|
+
* framed cannot be recovered. `windowStartBp` can (it is
|
|
224
|
+
* `offsetPx * bpPerPx`, no width needed); the width in bp cannot. This
|
|
225
|
+
* carries the old `bpPerPx` to the first measure, which adopts it at
|
|
226
|
+
* whatever width arrives — exactly what the old code did — and clears
|
|
227
|
+
* this. So an old link keeps its old behavior rather than being
|
|
228
|
+
* reinterpreted, and everything authored since restores its window.
|
|
229
|
+
*/
|
|
230
|
+
legacyBpPerPx: types.stripDefault(types.number, 0),
|
|
231
|
+
/**
|
|
232
|
+
* #property
|
|
233
|
+
* currently displayed regions, can be a single chromosome, arbitrary
|
|
234
|
+
* subsections, or the entire set of chromosomes in the genome, but it not
|
|
235
|
+
* advised to use the entire set of chromosomes if your assembly is very
|
|
236
|
+
* fragmented
|
|
237
|
+
*/
|
|
238
|
+
displayedRegions: types.stripDefault(types.frozen(), []),
|
|
239
|
+
/**
|
|
240
|
+
* #property
|
|
241
|
+
* array of currently displayed tracks state models instances
|
|
242
|
+
*/
|
|
35
243
|
tracks: types.array(pluginManager.pluggableMstType('track', 'stateModel')),
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
244
|
+
/**
|
|
245
|
+
* #property
|
|
246
|
+
* drop the header bar entirely — location box, navigation buttons and
|
|
247
|
+
* overview
|
|
248
|
+
*/
|
|
249
|
+
hideHeader: types.stripDefault(types.boolean, false),
|
|
250
|
+
/**
|
|
251
|
+
* #property
|
|
252
|
+
* keep the header, drop the whole-chromosome overview strip below it
|
|
253
|
+
*/
|
|
254
|
+
hideHeaderOverview: types.stripDefault(types.boolean, false),
|
|
255
|
+
/**
|
|
256
|
+
* #property
|
|
257
|
+
* suppress the "No tracks active" placeholder, for an embed that opens
|
|
258
|
+
* with no tracks on purpose
|
|
259
|
+
*/
|
|
260
|
+
hideNoTracksActive: types.stripDefault(types.boolean, false),
|
|
261
|
+
/**
|
|
262
|
+
* #property
|
|
263
|
+
* vestigial: the hierarchical selector is the only one that exists, so
|
|
264
|
+
* this value is ignored. Retained because saved sessions and share
|
|
265
|
+
* links persist it.
|
|
266
|
+
*/
|
|
267
|
+
trackSelectorType: types.stripDefault(types.enumeration(['hierarchical']), 'hierarchical'),
|
|
268
|
+
/**
|
|
269
|
+
* #property
|
|
270
|
+
* show the "center line"
|
|
271
|
+
*/
|
|
40
272
|
showCenterLine: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCenterLine', false)),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
273
|
+
/**
|
|
274
|
+
* #property
|
|
275
|
+
* whether to show the "cytobands" in the overview scale bar (the
|
|
276
|
+
* resolved, capability-gated value is the `effectiveShowCytobands`
|
|
277
|
+
* getter)
|
|
278
|
+
*/
|
|
279
|
+
showCytobands: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCytobands', true)),
|
|
280
|
+
/**
|
|
281
|
+
* #property
|
|
282
|
+
* how to display the track labels, can be "overlapping", "offset", or
|
|
283
|
+
* "hidden", or empty string "" (which results in the
|
|
284
|
+
* LinearGenomeViewPlugin config default being used). the resolved value
|
|
285
|
+
* is the `effectiveTrackLabels` getter. see LinearGenomeViewPlugin
|
|
286
|
+
* https://jbrowse.org/jb2/docs/config/lineargenomeviewconfigschema/ docs for
|
|
287
|
+
* how conf is used
|
|
288
|
+
*/
|
|
289
|
+
trackLabels: types.optional(types.string, () => localStorageGetItem('lgv-trackLabels') ?? ''),
|
|
290
|
+
/**
|
|
291
|
+
* #property
|
|
292
|
+
* show the "gridlines" in the track area
|
|
293
|
+
*/
|
|
294
|
+
showGridlines: types.stripDefault(types.boolean, true),
|
|
295
|
+
/**
|
|
296
|
+
* #property
|
|
297
|
+
* color CDS segments by reading frame
|
|
298
|
+
*/
|
|
47
299
|
colorByCDS: types.optional(types.boolean, () => localStorageGetBoolean('lgv-colorByCDS', false)),
|
|
300
|
+
/**
|
|
301
|
+
* #property
|
|
302
|
+
* draw translated codons on coding features once zoomed in far enough:
|
|
303
|
+
* an alternating per-codon shading, and the amino acid letters on top of
|
|
304
|
+
* it at base-level zoom. Independent of `colorByCDS`, which only recolors
|
|
305
|
+
* the segments by frame.
|
|
306
|
+
*/
|
|
307
|
+
showAminoAcids: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showAminoAcids', true)),
|
|
308
|
+
/**
|
|
309
|
+
* #property
|
|
310
|
+
* show the track outlines
|
|
311
|
+
*/
|
|
48
312
|
showTrackOutlines: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showTrackOutlines', true)),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
isScalebarRefNameMenuOpen: false,
|
|
65
|
-
scalebarRefNameClickPending: false,
|
|
66
|
-
volatileGuides: [],
|
|
313
|
+
/**
|
|
314
|
+
* #property
|
|
315
|
+
* when true, only the header and coordinate scalebar are rendered
|
|
316
|
+
*/
|
|
317
|
+
scalebarOnly: types.stripDefault(types.boolean, false),
|
|
318
|
+
/**
|
|
319
|
+
* #property
|
|
320
|
+
* transient launch state: the settings written on the view object that
|
|
321
|
+
* need resolving before they can be view state — an assembly, a
|
|
322
|
+
* location, track recipes, highlights. `preProcessSnapshot` moves them
|
|
323
|
+
* here off the snapshot, the afterAttach autorun applies them and
|
|
324
|
+
* clears this, so a saved session never retains it. Not written by
|
|
325
|
+
* hand: author every setting directly on the view.
|
|
326
|
+
*/
|
|
327
|
+
launch: types.frozen(),
|
|
67
328
|
}))
|
|
329
|
+
.volatile(() => {
|
|
330
|
+
// typed locals so `unknown`/`Record` aren't narrowed to `undefined`/`{}`; inline
|
|
331
|
+
// type assertions here get stripped by no-unnecessary-type-assertion
|
|
332
|
+
const volatileError = undefined;
|
|
333
|
+
return {
|
|
334
|
+
/**
|
|
335
|
+
* #volatile
|
|
336
|
+
* Height of each track's in-flow label band, measured by
|
|
337
|
+
* TrackContainer as the rendering container's offset inside its Paper.
|
|
338
|
+
* Absent (0) while the label is hidden, overlapping, or not yet
|
|
339
|
+
* measured. Observable, so the offset and height getters re-derive
|
|
340
|
+
* when a band changes.
|
|
341
|
+
*/
|
|
342
|
+
trackLabelBands: observable.map(),
|
|
343
|
+
/**
|
|
344
|
+
* #volatile
|
|
345
|
+
* How much taller the view's box is than the scroll port it sits in,
|
|
346
|
+
* in px, negative when it is shorter. Measured by the view component
|
|
347
|
+
* for a top-level view; undefined until then, and for a nested one.
|
|
348
|
+
*/
|
|
349
|
+
scrollPortExcess: undefined,
|
|
350
|
+
/**
|
|
351
|
+
* #volatile
|
|
352
|
+
*/
|
|
353
|
+
volatileWidth: undefined,
|
|
354
|
+
/**
|
|
355
|
+
* #volatile
|
|
356
|
+
*/
|
|
357
|
+
draggingTrackId: undefined,
|
|
358
|
+
/**
|
|
359
|
+
* #volatile
|
|
360
|
+
*/
|
|
361
|
+
lastTrackDragY: undefined,
|
|
362
|
+
/**
|
|
363
|
+
* #volatile
|
|
364
|
+
*/
|
|
365
|
+
volatileError,
|
|
366
|
+
/**
|
|
367
|
+
* #volatile
|
|
368
|
+
*/
|
|
369
|
+
coarseDynamicBlocks: [],
|
|
370
|
+
/**
|
|
371
|
+
* #volatile
|
|
372
|
+
*/
|
|
373
|
+
coarseTotalBp: 0,
|
|
374
|
+
/**
|
|
375
|
+
* #volatile
|
|
376
|
+
*/
|
|
377
|
+
// A real scale rather than the not-yet-measured 0, for the canvas
|
|
378
|
+
// displays that lay out against it; `setWidth` seeds the true one at
|
|
379
|
+
// the first measure.
|
|
380
|
+
coarseBpPerPx: 1,
|
|
381
|
+
/**
|
|
382
|
+
* #volatile
|
|
383
|
+
*/
|
|
384
|
+
leftOffset: undefined,
|
|
385
|
+
/**
|
|
386
|
+
* #volatile
|
|
387
|
+
*/
|
|
388
|
+
rightOffset: undefined,
|
|
389
|
+
/**
|
|
390
|
+
* #volatile
|
|
391
|
+
*/
|
|
392
|
+
isScalebarRefNameMenuOpen: false,
|
|
393
|
+
/**
|
|
394
|
+
* #volatile
|
|
395
|
+
* temporary vertical guides that can be set by displays (e.g., LD display hover)
|
|
396
|
+
*/
|
|
397
|
+
volatileGuides: [],
|
|
398
|
+
};
|
|
399
|
+
})
|
|
68
400
|
.views(self => ({
|
|
401
|
+
// The viewport in pixels, derived from the window it frames — declared
|
|
402
|
+
// ahead of every other getter so that the ~300 reads of `self.bpPerPx` /
|
|
403
|
+
// `self.offsetPx` across the codebase go on resolving by name. Only the
|
|
404
|
+
// writes had to move, and they are six actions below.
|
|
405
|
+
/**
|
|
406
|
+
* #getter
|
|
407
|
+
* the launch state that still has something to apply — the gate every
|
|
408
|
+
* loading and error path below reads.
|
|
409
|
+
*/
|
|
410
|
+
get pendingLaunch() {
|
|
411
|
+
return pendingLaunch(self.launch);
|
|
412
|
+
},
|
|
413
|
+
/**
|
|
414
|
+
* #getter
|
|
415
|
+
* corresponds roughly to the zoom level, base-pairs per pixel.
|
|
416
|
+
*
|
|
417
|
+
* Zero before a width is measured, which is the same not-yet-measured
|
|
418
|
+
* sentinel `Base1DView` uses and which `displayedRegionsTotalPx` and the
|
|
419
|
+
* offset getters already guard for. Reads `volatileWidth` rather than the
|
|
420
|
+
* `width` getter on purpose: that one throws when unmeasured, and these
|
|
421
|
+
* are read from the first render.
|
|
422
|
+
*/
|
|
423
|
+
get bpPerPx() {
|
|
424
|
+
const width = self.volatileWidth;
|
|
425
|
+
return width === undefined || width <= 0 || self.windowWidthBp <= 0
|
|
426
|
+
? 0
|
|
427
|
+
: self.windowWidthBp / width;
|
|
428
|
+
},
|
|
429
|
+
/**
|
|
430
|
+
* #getter
|
|
431
|
+
* corresponds roughly to the horizontal scroll of the LGV
|
|
432
|
+
*/
|
|
433
|
+
get offsetPx() {
|
|
434
|
+
const { bpPerPx } = this;
|
|
435
|
+
return bpPerPx <= 0 ? 0 : self.windowStartBp / bpPerPx;
|
|
436
|
+
},
|
|
437
|
+
/**
|
|
438
|
+
* #getter
|
|
439
|
+
* a displayed region narrower than this many pixels draws as an elided
|
|
440
|
+
* block
|
|
441
|
+
*/
|
|
442
|
+
get minimumBlockWidth() {
|
|
443
|
+
return MINIMUM_BLOCK_WIDTH;
|
|
444
|
+
},
|
|
445
|
+
/**
|
|
446
|
+
* #getter
|
|
447
|
+
* scroll-to-zoom is a global, personal preference resolved from the
|
|
448
|
+
* session; toggling it in any view applies everywhere
|
|
449
|
+
*/
|
|
450
|
+
get scrollZoom() {
|
|
451
|
+
return getSession(self).scrollZoom;
|
|
452
|
+
},
|
|
453
|
+
/**
|
|
454
|
+
* #getter
|
|
455
|
+
*/
|
|
69
456
|
get pinnedTracks() {
|
|
70
457
|
return self.tracks.filter(t => t.pinned);
|
|
71
458
|
},
|
|
459
|
+
/**
|
|
460
|
+
* #getter
|
|
461
|
+
*/
|
|
72
462
|
get unpinnedTracks() {
|
|
73
463
|
return self.tracks.filter(t => !t.pinned);
|
|
74
464
|
},
|
|
75
|
-
|
|
465
|
+
/**
|
|
466
|
+
* #getter
|
|
467
|
+
* the effective track labels setting, resolving the stored `trackLabels`
|
|
468
|
+
* against the LinearGenomeViewPlugin config default
|
|
469
|
+
*/
|
|
470
|
+
get effectiveTrackLabels() {
|
|
76
471
|
const sessionSetting = getConf(getSession(self), [
|
|
77
472
|
'LinearGenomeViewPlugin',
|
|
78
473
|
'trackLabels',
|
|
79
474
|
]);
|
|
80
475
|
return self.trackLabels || sessionSetting;
|
|
81
476
|
},
|
|
477
|
+
/**
|
|
478
|
+
* #getter
|
|
479
|
+
*/
|
|
82
480
|
get width() {
|
|
83
481
|
if (self.volatileWidth === undefined) {
|
|
84
482
|
throw new Error('width undefined, make sure to check for model.initialized');
|
|
85
483
|
}
|
|
86
484
|
return self.volatileWidth;
|
|
87
485
|
},
|
|
88
|
-
|
|
89
|
-
|
|
486
|
+
/**
|
|
487
|
+
* #getter
|
|
488
|
+
* width minus track outline borders (1px each side when shown)
|
|
489
|
+
*/
|
|
490
|
+
get trackWidthPx() {
|
|
491
|
+
return this.width - (self.showTrackOutlines ? 2 : 0);
|
|
90
492
|
},
|
|
493
|
+
/**
|
|
494
|
+
* #getter
|
|
495
|
+
*/
|
|
91
496
|
get assemblyNames() {
|
|
92
497
|
return [...new Set(self.displayedRegions.map(r => r.assemblyName))];
|
|
93
498
|
},
|
|
499
|
+
/**
|
|
500
|
+
* #getter
|
|
501
|
+
*/
|
|
94
502
|
get assemblyDisplayNames() {
|
|
95
503
|
const { assemblyManager } = getSession(self);
|
|
96
|
-
return this.assemblyNames.map(a => assemblyManager.
|
|
97
|
-
},
|
|
504
|
+
return this.assemblyNames.map(a => assemblyManager.getDisplayName(a));
|
|
505
|
+
},
|
|
506
|
+
/**
|
|
507
|
+
* #getter
|
|
508
|
+
* the session's highlights on this view's assemblies
|
|
509
|
+
*/
|
|
510
|
+
get highlights() {
|
|
511
|
+
const { highlights, assemblyManager } = getSession(self);
|
|
512
|
+
return highlightsOnAssemblies(highlights, this.assemblyNames, assemblyManager);
|
|
513
|
+
},
|
|
514
|
+
/**
|
|
515
|
+
* #getter
|
|
516
|
+
* checking if lgv is a 'top-level' view is used for toggling pin track
|
|
517
|
+
* capability, sticky positioning
|
|
518
|
+
*/
|
|
98
519
|
get isTopLevelView() {
|
|
99
520
|
return getSession(self).views.some(r => r.id === self.id);
|
|
100
521
|
},
|
|
522
|
+
/**
|
|
523
|
+
* #getter
|
|
524
|
+
* only uses sticky view headers when it is a 'top-level' view and
|
|
525
|
+
* session allows it
|
|
526
|
+
*/
|
|
101
527
|
get stickyViewHeaders() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (this.stickyViewHeaders) {
|
|
110
|
-
pinnedTracksTop = VIEW_HEADER_HEIGHT;
|
|
111
|
-
if (!self.hideHeader) {
|
|
112
|
-
pinnedTracksTop += HEADER_BAR_HEIGHT;
|
|
113
|
-
if (!self.hideHeaderOverview) {
|
|
114
|
-
pinnedTracksTop += HEADER_OVERVIEW_HEIGHT;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return pinnedTracksTop;
|
|
119
|
-
},
|
|
120
|
-
get pinnedTracksTop() {
|
|
121
|
-
return this.rubberbandTop + SCALE_BAR_HEIGHT;
|
|
528
|
+
// `=== true` rather than a session guard: the member is optional on
|
|
529
|
+
// AbstractSessionModel, and a session with no such notion (the embedded
|
|
530
|
+
// products) should read as "don't pin", which is what absent gives.
|
|
531
|
+
// Tested before isTopLevelView, the order the guard used to impose — a
|
|
532
|
+
// session without the preference is also the one least likely to have
|
|
533
|
+
// anything useful behind `views`, and there is no reason to walk it
|
|
534
|
+
return (getSession(self).stickyViewHeaders === true && this.isTopLevelView);
|
|
122
535
|
},
|
|
123
536
|
}))
|
|
124
537
|
.views(self => ({
|
|
125
|
-
|
|
126
|
-
|
|
538
|
+
/**
|
|
539
|
+
* #getter
|
|
540
|
+
* The census entry for this view: the tracks it holds itself. Declared
|
|
541
|
+
* rather than derived — see `BaseViewModel.ownTracks`.
|
|
542
|
+
*/
|
|
543
|
+
get ownTracks() {
|
|
544
|
+
return [...self.tracks];
|
|
545
|
+
},
|
|
546
|
+
/**
|
|
547
|
+
* #getter
|
|
548
|
+
* See `BaseViewModel.rendersDisplays`.
|
|
549
|
+
*/
|
|
550
|
+
get rendersDisplays() {
|
|
551
|
+
return !self.scalebarOnly;
|
|
552
|
+
},
|
|
553
|
+
/**
|
|
554
|
+
* #getter
|
|
555
|
+
* Assembly name captioning the scalebar ahead of its refName labels, or
|
|
556
|
+
* undefined for none. A container view (e.g. LinearSyntenyView) opts its sub-views in by
|
|
557
|
+
* exposing showAssemblyNameInSubviewScalebar; duck-typed rather than
|
|
558
|
+
* matching a concrete view type so no upward plugin dependency is needed
|
|
559
|
+
* and any container can opt in. A wrong nesting depth simply yields no
|
|
560
|
+
* prefix — hence the hasParent guard, since getParent throws (rather than
|
|
561
|
+
* returning undefined) when the view sits shallower than depth 2.
|
|
562
|
+
*/
|
|
563
|
+
get scalebarDisplayPrefix() {
|
|
564
|
+
const parent = hasParent(self, 2)
|
|
565
|
+
? getParent(self, 2)
|
|
566
|
+
: undefined;
|
|
567
|
+
return parent?.showAssemblyNameInSubviewScalebar
|
|
127
568
|
? self.assemblyDisplayNames[0]
|
|
128
|
-
:
|
|
129
|
-
},
|
|
130
|
-
MiniControlsComponent() {
|
|
131
|
-
return MiniControls;
|
|
132
|
-
},
|
|
133
|
-
HeaderComponent() {
|
|
134
|
-
return Header;
|
|
569
|
+
: undefined;
|
|
135
570
|
},
|
|
571
|
+
/**
|
|
572
|
+
* #getter
|
|
573
|
+
*/
|
|
136
574
|
get assembliesNotFound() {
|
|
137
575
|
const { assemblyManager } = getSession(self);
|
|
138
576
|
const r0 = self.assemblyNames
|
|
139
|
-
.
|
|
140
|
-
.filter(f => !!f)
|
|
577
|
+
.filter(a => !assemblyManager.assemblyNameMap[a])
|
|
141
578
|
.join(',');
|
|
142
579
|
return r0 ? `Assemblies ${r0} not found` : undefined;
|
|
143
580
|
},
|
|
581
|
+
/**
|
|
582
|
+
* #getter
|
|
583
|
+
*/
|
|
144
584
|
get assemblyErrors() {
|
|
145
585
|
const { assemblyManager } = getSession(self);
|
|
146
|
-
return self.assemblyNames
|
|
147
|
-
.map(a => assemblyManager.get(a)?.error)
|
|
148
|
-
.filter(f => !!f)
|
|
149
|
-
.join(', ');
|
|
586
|
+
return assemblyErrorMessage(assemblyManager, self.assemblyNames);
|
|
150
587
|
},
|
|
588
|
+
/**
|
|
589
|
+
* #getter
|
|
590
|
+
*/
|
|
151
591
|
get assembliesInitialized() {
|
|
152
592
|
const { assemblyManager } = getSession(self);
|
|
153
|
-
return self.assemblyNames.every(
|
|
593
|
+
return self.assemblyNames.every(name => assemblyManager.get(name)?.initialized);
|
|
594
|
+
},
|
|
595
|
+
/**
|
|
596
|
+
* #getter
|
|
597
|
+
* the assembly named by a pending `init`, or undefined when no init is
|
|
598
|
+
* set. `init`'s assembly isn't in `assemblyNames` yet (that derives from
|
|
599
|
+
* displayedRegions, still empty pre-navigation), so init-phase readiness
|
|
600
|
+
* and error checks resolve it directly through here.
|
|
601
|
+
*/
|
|
602
|
+
get initAssembly() {
|
|
603
|
+
const name = this.launchAssemblyName;
|
|
604
|
+
return self.pendingLaunch && name !== undefined
|
|
605
|
+
? getSession(self).assemblyManager.get(name)
|
|
606
|
+
: undefined;
|
|
154
607
|
},
|
|
608
|
+
/**
|
|
609
|
+
* #getter
|
|
610
|
+
* The assembly a pending launch is for: the one it names, else the one
|
|
611
|
+
* the view already shows. A re-launch of a materialized view (a
|
|
612
|
+
* `jb.setSession` document writing `loc` beside the built state) need
|
|
613
|
+
* not restate it; without the default the readiness gate waited on
|
|
614
|
+
* `assemblyManager.get(undefined)` for good. Undefined on a fresh view
|
|
615
|
+
* whose blob names none, which `error` reports.
|
|
616
|
+
*/
|
|
617
|
+
get launchAssemblyName() {
|
|
618
|
+
return self.pendingLaunch?.assembly ?? self.assemblyNames[0];
|
|
619
|
+
},
|
|
620
|
+
/**
|
|
621
|
+
* #getter
|
|
622
|
+
*/
|
|
155
623
|
get initialized() {
|
|
156
624
|
if (self.volatileWidth === undefined) {
|
|
157
625
|
return false;
|
|
158
626
|
}
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
const asm =
|
|
627
|
+
// if init is set, wait for that assembly to have regions loaded
|
|
628
|
+
if (self.pendingLaunch) {
|
|
629
|
+
const asm = this.initAssembly;
|
|
162
630
|
return !!(asm?.initialized && asm.regions);
|
|
163
631
|
}
|
|
164
632
|
return this.assembliesInitialized;
|
|
165
633
|
},
|
|
634
|
+
/**
|
|
635
|
+
* #getter
|
|
636
|
+
*/
|
|
166
637
|
get hasDisplayedRegions() {
|
|
167
638
|
return self.displayedRegions.length > 0;
|
|
168
639
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
640
|
+
/**
|
|
641
|
+
* #getter
|
|
642
|
+
* The assembly whose load the spinner is waiting on: the one `init` names
|
|
643
|
+
* before navigation (it isn't in assemblyNames yet), else the first of the
|
|
644
|
+
* displayed assemblies that hasn't finished loading.
|
|
645
|
+
*/
|
|
646
|
+
get loadingAssembly() {
|
|
647
|
+
return self.pendingLaunch
|
|
648
|
+
? this.initAssembly
|
|
649
|
+
: getSession(self).assemblyManager.loadingAssembly(self.assemblyNames);
|
|
650
|
+
},
|
|
651
|
+
/**
|
|
652
|
+
* #getter
|
|
653
|
+
* What the loading screen says while `showLoading`, read off the
|
|
654
|
+
* assembly whose load is the wait; undefined otherwise.
|
|
655
|
+
*/
|
|
656
|
+
get loading() {
|
|
657
|
+
return viewLoading(this.showLoading, () => this.loadingAssembly);
|
|
658
|
+
},
|
|
659
|
+
/**
|
|
660
|
+
* #getter
|
|
661
|
+
*/
|
|
172
662
|
get hasSomethingToShow() {
|
|
173
|
-
return this.hasDisplayedRegions || !!self.
|
|
174
|
-
},
|
|
663
|
+
return this.hasDisplayedRegions || !!self.pendingLaunch;
|
|
664
|
+
},
|
|
665
|
+
/**
|
|
666
|
+
* #getter
|
|
667
|
+
* init is set but its async navigation (the afterAttach autorun) hasn't
|
|
668
|
+
* populated displayedRegions yet. `initialized` can already be true here
|
|
669
|
+
* (it only tracks assembly readiness), so without this the container
|
|
670
|
+
* would mount over empty regions and pxToBp/hover would throw.
|
|
671
|
+
*
|
|
672
|
+
* **Not dotplot's or synteny's `initPending`**, which is the bare
|
|
673
|
+
* `!!self.pendingLaunch` — "an init blob has not been applied" — and which those
|
|
674
|
+
* views read from their `settled` gate rather than from `showLoading`.
|
|
675
|
+
* This one is narrower on purpose: it closes only the window where there
|
|
676
|
+
* is nothing on screen. Once navigation has produced regions the view is
|
|
677
|
+
* usable and gets shown, even though `init` is still set while the tracks
|
|
678
|
+
* and highlights land. Renamed away from the shared spelling because the
|
|
679
|
+
* two predicates disagree exactly there, and the bare name reads as
|
|
680
|
+
* theirs.
|
|
681
|
+
*/
|
|
682
|
+
get awaitingInitNavigation() {
|
|
683
|
+
return !!self.pendingLaunch && !this.hasDisplayedRegions;
|
|
684
|
+
},
|
|
685
|
+
/**
|
|
686
|
+
* #getter
|
|
687
|
+
* Whether to show a loading indicator instead of the import form or view
|
|
688
|
+
*/
|
|
175
689
|
get showLoading() {
|
|
176
|
-
return
|
|
177
|
-
|
|
690
|
+
return (this.hasSomethingToShow &&
|
|
691
|
+
!this.error &&
|
|
692
|
+
(!this.initialized || this.awaitingInitNavigation));
|
|
693
|
+
},
|
|
694
|
+
/**
|
|
695
|
+
* #getter
|
|
696
|
+
* Whether to show the import form
|
|
697
|
+
*/
|
|
178
698
|
get showImportForm() {
|
|
179
699
|
return !this.hasSomethingToShow || !!this.error;
|
|
180
700
|
},
|
|
181
|
-
|
|
182
|
-
|
|
701
|
+
/**
|
|
702
|
+
* #getter
|
|
703
|
+
* Is there anything to draw yet? The gate anything reading block geometry
|
|
704
|
+
* — a ruler, a scalebar, a display — has to pass first, because
|
|
705
|
+
* `width`/`staticBlocks` throw by design before the view has been
|
|
706
|
+
* measured and navigated.
|
|
707
|
+
*
|
|
708
|
+
* **Not `initialized`**, which is the trap this getter exists to close.
|
|
709
|
+
* That one answers "have the assembly's regions loaded", which is only
|
|
710
|
+
* the first of two async steps: navigating then populates
|
|
711
|
+
* `displayedRegions`, and in the window between the two `initialized` is
|
|
712
|
+
* already true while there is still nothing on screen. `showLoading`
|
|
713
|
+
* folds in `awaitingInitNavigation`, the getter that exists for exactly
|
|
714
|
+
* that gap.
|
|
715
|
+
*
|
|
716
|
+
* `error` is the third outcome and is why this is not a bare
|
|
717
|
+
* `!showLoading`: a failed assembly load also ends the loading state, so
|
|
718
|
+
* that alone would mount over the wreckage. Read `error` yourself if you
|
|
719
|
+
* want to draw it.
|
|
720
|
+
*/
|
|
721
|
+
get ready() {
|
|
722
|
+
return !this.showLoading && !this.error;
|
|
723
|
+
},
|
|
724
|
+
/**
|
|
725
|
+
* #getter
|
|
726
|
+
* The same question as `ready` with the other three outcomes named, for a
|
|
727
|
+
* host that draws its own chrome and has to render all four. See
|
|
728
|
+
* `computeViewStatus`, whose precedence this defers to — and note its
|
|
729
|
+
* `ready` is narrower than the getter above, which is true when nothing
|
|
730
|
+
* has told the view where to look.
|
|
731
|
+
*/
|
|
732
|
+
get status() {
|
|
733
|
+
return computeViewStatus({
|
|
734
|
+
error: this.error,
|
|
735
|
+
hasSomethingToShow: this.hasSomethingToShow,
|
|
736
|
+
loading: () => this.loading,
|
|
737
|
+
});
|
|
183
738
|
},
|
|
739
|
+
/**
|
|
740
|
+
* #getter
|
|
741
|
+
*/
|
|
742
|
+
get scalebarHeight() {
|
|
743
|
+
return SCALE_BAR_HEIGHT;
|
|
744
|
+
},
|
|
745
|
+
/**
|
|
746
|
+
* #getter
|
|
747
|
+
* What TrackContainer puts *above* a track's rendering container: the gap
|
|
748
|
+
* over its Paper, and the Paper's own top border when outlines are on.
|
|
749
|
+
*/
|
|
750
|
+
get trackLeadingChrome() {
|
|
751
|
+
return (TRACK_TOP_GAP + (self.showTrackOutlines ? TRACK_OUTLINE_BORDER : 0));
|
|
752
|
+
},
|
|
753
|
+
/**
|
|
754
|
+
* #getter
|
|
755
|
+
* ...and what it puts below: the resize divider, plus the matching bottom
|
|
756
|
+
* border.
|
|
757
|
+
*/
|
|
758
|
+
get trackTrailingChrome() {
|
|
759
|
+
return (RESIZE_HANDLE_HEIGHT +
|
|
760
|
+
(self.showTrackOutlines ? TRACK_OUTLINE_BORDER : 0));
|
|
761
|
+
},
|
|
762
|
+
/**
|
|
763
|
+
* #getter
|
|
764
|
+
* A track's full cost beyond its display height.
|
|
765
|
+
*
|
|
766
|
+
* The track *label* is not counted here: an offset label is an in-flow
|
|
767
|
+
* box whose height is whatever the theme renders a Paper of icon buttons
|
|
768
|
+
* at, so it is measured into `trackLabelBands` per track and added by
|
|
769
|
+
* `trackLabelBand` rather than derived.
|
|
770
|
+
*/
|
|
771
|
+
get trackChromeHeight() {
|
|
772
|
+
return this.trackLeadingChrome + this.trackTrailingChrome;
|
|
773
|
+
},
|
|
774
|
+
/**
|
|
775
|
+
* #getter
|
|
776
|
+
*/
|
|
184
777
|
get headerHeight() {
|
|
185
778
|
if (self.hideHeader) {
|
|
186
779
|
return 0;
|
|
@@ -192,47 +785,155 @@ export function stateModelFactory(pluginManager) {
|
|
|
192
785
|
return HEADER_BAR_HEIGHT + HEADER_OVERVIEW_HEIGHT;
|
|
193
786
|
}
|
|
194
787
|
},
|
|
788
|
+
/**
|
|
789
|
+
* #method
|
|
790
|
+
* rendered height of a single track, collapsing to a fixed height when
|
|
791
|
+
* minimized. Shared by trackHeights and getTrackYOffset so the two can't
|
|
792
|
+
* disagree. Reads `activeDisplay` — the display TrackContainer actually
|
|
793
|
+
* mounts — rather than re-picking `displays[0]`, so the view's height math
|
|
794
|
+
* can't diverge from what is on screen.
|
|
795
|
+
*/
|
|
796
|
+
trackHeight(track) {
|
|
797
|
+
return track.minimized
|
|
798
|
+
? MINIMIZED_TRACK_HEIGHT
|
|
799
|
+
: track.activeDisplay.height;
|
|
800
|
+
},
|
|
801
|
+
/**
|
|
802
|
+
* #method
|
|
803
|
+
* the measured in-flow label band above a track's rendering container,
|
|
804
|
+
* 0 until TrackContainer has measured one or when no label is in flow
|
|
805
|
+
*/
|
|
806
|
+
trackLabelBand(track) {
|
|
807
|
+
return self.trackLabelBands.get(track.configuration.trackId) ?? 0;
|
|
808
|
+
},
|
|
809
|
+
/**
|
|
810
|
+
* #getter
|
|
811
|
+
*/
|
|
195
812
|
get trackHeights() {
|
|
196
|
-
return sum(self.tracks.map(t => t
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
813
|
+
return sum(self.tracks.map(t => this.trackHeight(t)));
|
|
814
|
+
},
|
|
815
|
+
/**
|
|
816
|
+
* #getter
|
|
817
|
+
*/
|
|
818
|
+
get trackHeightsWithChrome() {
|
|
819
|
+
return (this.trackHeights +
|
|
820
|
+
self.tracks.length * this.trackChromeHeight +
|
|
821
|
+
sum(self.tracks.map(t => this.trackLabelBand(t))));
|
|
822
|
+
},
|
|
823
|
+
/**
|
|
824
|
+
* #getter
|
|
825
|
+
*/
|
|
201
826
|
get height() {
|
|
202
|
-
|
|
827
|
+
if (self.scalebarOnly) {
|
|
828
|
+
return this.headerHeight + this.scalebarHeight;
|
|
829
|
+
}
|
|
830
|
+
return (this.trackHeightsWithChrome +
|
|
203
831
|
this.headerHeight +
|
|
204
|
-
this.scalebarHeight
|
|
832
|
+
this.scalebarHeight +
|
|
833
|
+
(self.tracks.length && self.isTopLevelView
|
|
834
|
+
? RESIZE_ALL_HANDLE_HEIGHT
|
|
835
|
+
: 0));
|
|
836
|
+
},
|
|
837
|
+
/**
|
|
838
|
+
* #getter
|
|
839
|
+
* The display height of each track a whole-stack resize moves, by track
|
|
840
|
+
* instance id: every shown track that is not minimized.
|
|
841
|
+
*/
|
|
842
|
+
get resizableTrackHeights() {
|
|
843
|
+
return new Map(self.tracks
|
|
844
|
+
.filter(t => !t.minimized)
|
|
845
|
+
.map(t => [t.id, t.activeDisplay.height]));
|
|
846
|
+
},
|
|
847
|
+
/**
|
|
848
|
+
* #method
|
|
849
|
+
* Y offset (in pixels, from the top of the view) where a track's
|
|
850
|
+
* rendering container starts. Walks tracks in DOM render order (pinned
|
|
851
|
+
* first, then unpinned), from the same constants TrackContainer lays its
|
|
852
|
+
* Paper out with. Returns `undefined` if the track is not present.
|
|
853
|
+
*
|
|
854
|
+
* Includes each track's measured label band, this track's own included,
|
|
855
|
+
* since an in-flow label sits above the rendering container inside the
|
|
856
|
+
* same Paper. Exact once TrackContainer has measured; before that it is
|
|
857
|
+
* short by the unmeasured bands.
|
|
858
|
+
*/
|
|
859
|
+
getTrackYOffset(trackId) {
|
|
860
|
+
let y = this.headerHeight + this.scalebarHeight + this.trackLeadingChrome;
|
|
861
|
+
for (const t of [...self.pinnedTracks, ...self.unpinnedTracks]) {
|
|
862
|
+
y += this.trackLabelBand(t);
|
|
863
|
+
if (t.configuration.trackId === trackId) {
|
|
864
|
+
return y;
|
|
865
|
+
}
|
|
866
|
+
y += this.trackHeight(t) + this.trackChromeHeight;
|
|
867
|
+
}
|
|
868
|
+
return undefined;
|
|
205
869
|
},
|
|
870
|
+
/**
|
|
871
|
+
* #method
|
|
872
|
+
* the pinned or unpinned sibling list a track renders within; move
|
|
873
|
+
* up/down/top/bottom reorder inside this section rather than the full
|
|
874
|
+
* `tracks` array, since the two sections lay out independently
|
|
875
|
+
*/
|
|
876
|
+
trackSection(id) {
|
|
877
|
+
return self.tracks.find(t => t.id === id)?.pinned
|
|
878
|
+
? self.pinnedTracks
|
|
879
|
+
: self.unpinnedTracks;
|
|
880
|
+
},
|
|
881
|
+
/**
|
|
882
|
+
* #getter
|
|
883
|
+
*/
|
|
206
884
|
get totalBp() {
|
|
207
885
|
return sum(self.displayedRegions.map(r => r.end - r.start));
|
|
208
886
|
},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return self.displayedRegions.filter(r => (r.end - r.start) / bpPerPx >= self.minimumBlockWidth).length;
|
|
214
|
-
},
|
|
215
|
-
getInterRegionPaddingPx(bpPerPx) {
|
|
216
|
-
const nonElidedCount = this.getNonElidedRegionCount(bpPerPx);
|
|
217
|
-
const numPaddings = Math.max(0, nonElidedCount - 1);
|
|
218
|
-
return numPaddings * self.interRegionPaddingWidth;
|
|
219
|
-
},
|
|
220
|
-
get maxBpPerPx() {
|
|
887
|
+
/**
|
|
888
|
+
* #getter
|
|
889
|
+
*/
|
|
890
|
+
get fitBpPerPx() {
|
|
221
891
|
if (this.totalBp === 0 || self.width === 0) {
|
|
222
892
|
return 1;
|
|
223
893
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
894
|
+
// floored so a tiny region set cannot put maxBpPerPx under
|
|
895
|
+
// minBpPerPx, which would invert the zoom slider and zoomTo's clamp
|
|
896
|
+
return Math.max(this.minBpPerPx, this.totalBp / (self.width * SHOW_ALL_REGIONS_FILL));
|
|
897
|
+
},
|
|
898
|
+
/**
|
|
899
|
+
* #getter
|
|
900
|
+
* The zoom-out limit. Equal to the fit for this view's regions, except
|
|
901
|
+
* while a container holds it on a coarser shared scale, such as a small
|
|
902
|
+
* genome next to a large one, drawn short so the two compare by length.
|
|
903
|
+
*
|
|
904
|
+
* The shared scale raises the limit. A bpPerPx written past the limit
|
|
905
|
+
* would be undone by the first operation that clamps (a wheel tick, a
|
|
906
|
+
* rubberband, a `setDisplayedRegions`), as with the dotplot's locked
|
|
907
|
+
* aspect ratio turning "zoom out" into a zoom in (`axisMaxBpPerPx`).
|
|
908
|
+
* Every zoom clamps against the same ceiling, so full zoom-out stops at
|
|
909
|
+
* the shared scale.
|
|
910
|
+
*/
|
|
911
|
+
get maxBpPerPx() {
|
|
912
|
+
return Math.max(this.fitBpPerPx, this.sharedFitBpPerPx);
|
|
913
|
+
},
|
|
914
|
+
/**
|
|
915
|
+
* #getter
|
|
916
|
+
* The zoom-out ceiling a containing comparative view holds this row to,
|
|
917
|
+
* or 0 for a standalone view, a mode that is off, and a stack that cannot
|
|
918
|
+
* answer yet.
|
|
919
|
+
*
|
|
920
|
+
* Pulled rather than pushed down into a volatile: the number is a
|
|
921
|
+
* function of every row's fit, all of which move on a resize, and a
|
|
922
|
+
* stored copy would be stale by the next layout.
|
|
923
|
+
*/
|
|
924
|
+
get sharedFitBpPerPx() {
|
|
925
|
+
const fit = sharedScaleContainerOf(self)?.sharedFit;
|
|
926
|
+
return fit?.answered ? fit.bpPerPx : 0;
|
|
927
|
+
},
|
|
928
|
+
/**
|
|
929
|
+
* #getter
|
|
930
|
+
*/
|
|
233
931
|
get minBpPerPx() {
|
|
234
|
-
return
|
|
932
|
+
return MIN_BP_PER_PX;
|
|
235
933
|
},
|
|
934
|
+
/**
|
|
935
|
+
* #getter
|
|
936
|
+
*/
|
|
236
937
|
get error() {
|
|
237
938
|
if (self.volatileError) {
|
|
238
939
|
return self.volatileError;
|
|
@@ -243,47 +944,60 @@ export function stateModelFactory(pluginManager) {
|
|
|
243
944
|
if (this.assembliesNotFound) {
|
|
244
945
|
return this.assembliesNotFound;
|
|
245
946
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
947
|
+
// Check init assembly for errors (displayedRegions may be empty during init)
|
|
948
|
+
if (self.pendingLaunch) {
|
|
949
|
+
// `assembly` is required by InitState, but init is a frozen blob and
|
|
950
|
+
// hand-authored JSON is what fills it, so the type is not a guarantee.
|
|
951
|
+
// Naming the authoring mistake beats the downstream symptom, which is
|
|
952
|
+
// the literal string "Assembly undefined not found".
|
|
953
|
+
if (!this.launchAssemblyName) {
|
|
954
|
+
return 'LinearGenomeView init needs an "assembly"';
|
|
251
955
|
}
|
|
956
|
+
const asm = this.initAssembly;
|
|
252
957
|
if (!asm) {
|
|
253
|
-
return `Assembly ${
|
|
958
|
+
return `Assembly ${this.launchAssemblyName} not found`;
|
|
959
|
+
}
|
|
960
|
+
if (asm.error) {
|
|
961
|
+
return asm.error;
|
|
254
962
|
}
|
|
255
963
|
}
|
|
256
964
|
return undefined;
|
|
257
965
|
},
|
|
966
|
+
/**
|
|
967
|
+
* #getter
|
|
968
|
+
*/
|
|
258
969
|
get maxOffset() {
|
|
259
|
-
|
|
260
|
-
return this.displayedRegionsTotalPx -
|
|
970
|
+
// objectively determined to keep the linear genome on the main screen
|
|
971
|
+
return this.displayedRegionsTotalPx - MAX_OFFSET_PADDING_PX;
|
|
261
972
|
},
|
|
973
|
+
/**
|
|
974
|
+
* #getter
|
|
975
|
+
*/
|
|
262
976
|
get minOffset() {
|
|
263
|
-
|
|
264
|
-
return -self.width +
|
|
265
|
-
},
|
|
977
|
+
// objectively determined to keep the linear genome on the main screen
|
|
978
|
+
return -self.width + MIN_OFFSET_PADDING_PX;
|
|
979
|
+
},
|
|
980
|
+
/**
|
|
981
|
+
* #getter
|
|
982
|
+
* Whether the displayed regions read left-to-right, right-to-left, or
|
|
983
|
+
* some of each. `horizontallyFlip` reverses the order and flips every
|
|
984
|
+
* region at once, so a flipped row is uniformly `reversed` and `mixed`
|
|
985
|
+
* takes going out of your way — the scalebar label menu's per-region
|
|
986
|
+
* "Reverse region". Read off `displayedRegions` rather than off blocks
|
|
987
|
+
* because blocks cover the window and this is a fact about the row.
|
|
988
|
+
*/
|
|
989
|
+
get displayedRegionsOrientation() {
|
|
990
|
+
return regionsOrientation(self.displayedRegions);
|
|
991
|
+
},
|
|
992
|
+
/**
|
|
993
|
+
* #getter
|
|
994
|
+
*/
|
|
266
995
|
get displayedRegionsTotalPx() {
|
|
267
|
-
|
|
268
|
-
return 0;
|
|
269
|
-
}
|
|
270
|
-
const totalPaddingPx = this.getInterRegionPaddingPx(self.bpPerPx);
|
|
271
|
-
return this.totalBp / self.bpPerPx + totalPaddingPx;
|
|
272
|
-
},
|
|
273
|
-
renderProps() {
|
|
274
|
-
return {
|
|
275
|
-
...getParentRenderProps(self),
|
|
276
|
-
bpPerPx: self.bpPerPx,
|
|
277
|
-
colorByCDS: self.colorByCDS,
|
|
278
|
-
};
|
|
279
|
-
},
|
|
280
|
-
searchScope(assemblyName) {
|
|
281
|
-
return {
|
|
282
|
-
assemblyName,
|
|
283
|
-
includeAggregateIndexes: true,
|
|
284
|
-
tracks: self.tracks,
|
|
285
|
-
};
|
|
996
|
+
return self.bpPerPx === 0 ? 0 : this.totalBp / self.bpPerPx;
|
|
286
997
|
},
|
|
998
|
+
/**
|
|
999
|
+
* #getter
|
|
1000
|
+
*/
|
|
287
1001
|
get trackMap() {
|
|
288
1002
|
const map = new Map();
|
|
289
1003
|
for (const track of self.tracks) {
|
|
@@ -291,110 +1005,238 @@ export function stateModelFactory(pluginManager) {
|
|
|
291
1005
|
}
|
|
292
1006
|
return map;
|
|
293
1007
|
},
|
|
1008
|
+
/**
|
|
1009
|
+
* #method
|
|
1010
|
+
*/
|
|
294
1011
|
getTrack(id) {
|
|
295
1012
|
return this.trackMap.get(id);
|
|
296
1013
|
},
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const viewMenuActions = structuredClone(track.viewMenuActions);
|
|
306
|
-
rewriteOnClicks(self, track.type, viewMenuActions);
|
|
307
|
-
allActions.set(track.type, viewMenuActions);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return allActions;
|
|
1014
|
+
/**
|
|
1015
|
+
* #method
|
|
1016
|
+
* displayId of the active (shown) display for a track in this view, used
|
|
1017
|
+
* by the config editor to expand the relevant display and collapse the
|
|
1018
|
+
* track's other displays
|
|
1019
|
+
*/
|
|
1020
|
+
getActiveDisplayId(trackId) {
|
|
1021
|
+
return this.getTrack(trackId)?.activeDisplay.configuration.displayId;
|
|
311
1022
|
},
|
|
312
1023
|
}))
|
|
313
1024
|
.actions(self => ({
|
|
1025
|
+
/**
|
|
1026
|
+
* #action
|
|
1027
|
+
*/
|
|
314
1028
|
setShowTrackOutlines(arg) {
|
|
315
1029
|
self.showTrackOutlines = arg;
|
|
316
1030
|
},
|
|
1031
|
+
/**
|
|
1032
|
+
* #action
|
|
1033
|
+
*/
|
|
1034
|
+
setScrollZoom(flag) {
|
|
1035
|
+
getSession(self).setScrollZoom(flag);
|
|
1036
|
+
},
|
|
1037
|
+
/**
|
|
1038
|
+
* #action
|
|
1039
|
+
*/
|
|
317
1040
|
setColorByCDS(flag) {
|
|
318
1041
|
self.colorByCDS = flag;
|
|
319
1042
|
},
|
|
1043
|
+
/**
|
|
1044
|
+
* #action
|
|
1045
|
+
*/
|
|
1046
|
+
setShowAminoAcids(flag) {
|
|
1047
|
+
self.showAminoAcids = flag;
|
|
1048
|
+
},
|
|
1049
|
+
/**
|
|
1050
|
+
* #action
|
|
1051
|
+
*/
|
|
320
1052
|
setShowCytobands(flag) {
|
|
321
|
-
self.
|
|
1053
|
+
self.showCytobands = flag;
|
|
1054
|
+
},
|
|
1055
|
+
/**
|
|
1056
|
+
* #action
|
|
1057
|
+
*/
|
|
1058
|
+
setTrackLabelBand(trackId, band) {
|
|
1059
|
+
if (band === 0) {
|
|
1060
|
+
self.trackLabelBands.delete(trackId);
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
self.trackLabelBands.set(trackId, band);
|
|
1064
|
+
}
|
|
322
1065
|
},
|
|
1066
|
+
/**
|
|
1067
|
+
* #action
|
|
1068
|
+
*/
|
|
1069
|
+
setScrollPortExcess(px) {
|
|
1070
|
+
self.scrollPortExcess = px;
|
|
1071
|
+
},
|
|
1072
|
+
/**
|
|
1073
|
+
* #action
|
|
1074
|
+
* Grow or shrink every unminimized track by `distance` px in all, each
|
|
1075
|
+
* by its share of the height it has above the display minimum. `from` is
|
|
1076
|
+
* the heights to scale, by track instance id; a drag passes the heights
|
|
1077
|
+
* it started with, so whole-pixel rounding does not compound frame by
|
|
1078
|
+
* frame. Returns how far the stack moved.
|
|
1079
|
+
*/
|
|
1080
|
+
resizeTracks(distance, from = self.resizableTrackHeights) {
|
|
1081
|
+
const tracks = self.tracks.filter(t => !t.minimized && from.has(t.id));
|
|
1082
|
+
const start = tracks.map(t => from.get(t.id));
|
|
1083
|
+
const next = scaleBandHeights(start, distance, MIN_DISPLAY_HEIGHT);
|
|
1084
|
+
for (const [i, track] of tracks.entries()) {
|
|
1085
|
+
const display = track.activeDisplay;
|
|
1086
|
+
const delta = next[i] - display.height;
|
|
1087
|
+
if (delta !== 0) {
|
|
1088
|
+
display.resizeHeight(delta);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return sum(next) - sum(start);
|
|
1092
|
+
},
|
|
1093
|
+
/**
|
|
1094
|
+
* #action
|
|
1095
|
+
* A resize keeps the window in bp, so `bpPerPx` follows the new width
|
|
1096
|
+
* and the view keeps framing the same sequence (ADR-070).
|
|
1097
|
+
*/
|
|
323
1098
|
setWidth(newWidth) {
|
|
1099
|
+
const unmeasured = self.bpPerPx <= 0;
|
|
324
1100
|
self.volatileWidth = newWidth;
|
|
1101
|
+
if (newWidth > 0 && self.windowWidthBp <= 0) {
|
|
1102
|
+
// First measure, and nothing has established a window yet: a migrated
|
|
1103
|
+
// snapshot names the bpPerPx it was written with (see legacyBpPerPx),
|
|
1104
|
+
// and a view with no viewport at all takes the historical default of
|
|
1105
|
+
// 1. From here the window is the state and every later width divides
|
|
1106
|
+
// into it.
|
|
1107
|
+
self.windowWidthBp = (self.legacyBpPerPx || 1) * newWidth;
|
|
1108
|
+
self.legacyBpPerPx = 0;
|
|
1109
|
+
}
|
|
1110
|
+
if (unmeasured && self.bpPerPx > 0) {
|
|
1111
|
+
// The debounced coarse autorun is 500ms out; a restored snapshot's
|
|
1112
|
+
// consumers would lay out and fetch at 1 bp/px until then.
|
|
1113
|
+
self.coarseBpPerPx = self.bpPerPx;
|
|
1114
|
+
}
|
|
325
1115
|
},
|
|
1116
|
+
/**
|
|
1117
|
+
* #action
|
|
1118
|
+
*/
|
|
326
1119
|
setError(error) {
|
|
327
1120
|
self.volatileError = error;
|
|
328
1121
|
},
|
|
1122
|
+
/**
|
|
1123
|
+
* #action
|
|
1124
|
+
*/
|
|
329
1125
|
setIsScalebarRefNameMenuOpen(isOpen) {
|
|
330
1126
|
self.isScalebarRefNameMenuOpen = isOpen;
|
|
331
1127
|
},
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* #action
|
|
1130
|
+
*/
|
|
335
1131
|
setHideHeader(b) {
|
|
336
1132
|
self.hideHeader = b;
|
|
337
1133
|
},
|
|
1134
|
+
/**
|
|
1135
|
+
* #action
|
|
1136
|
+
*/
|
|
338
1137
|
setHideHeaderOverview(b) {
|
|
339
1138
|
self.hideHeaderOverview = b;
|
|
340
1139
|
},
|
|
1140
|
+
/**
|
|
1141
|
+
* #action
|
|
1142
|
+
*/
|
|
1143
|
+
setScalebarOnly(b) {
|
|
1144
|
+
self.scalebarOnly = b;
|
|
1145
|
+
},
|
|
1146
|
+
/**
|
|
1147
|
+
* #action
|
|
1148
|
+
*/
|
|
341
1149
|
setHideNoTracksActive(b) {
|
|
342
1150
|
self.hideNoTracksActive = b;
|
|
343
1151
|
},
|
|
1152
|
+
/**
|
|
1153
|
+
* #action
|
|
1154
|
+
*/
|
|
344
1155
|
setShowGridlines(b) {
|
|
345
1156
|
self.showGridlines = b;
|
|
346
1157
|
},
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
self.highlight = cast(highlight);
|
|
352
|
-
},
|
|
353
|
-
removeHighlight(highlight) {
|
|
354
|
-
self.highlight.remove(highlight);
|
|
355
|
-
},
|
|
356
|
-
updateHighlight(old, updates) {
|
|
357
|
-
const idx = self.highlight.indexOf(old);
|
|
358
|
-
if (idx !== -1) {
|
|
359
|
-
self.highlight.splice(idx, 1, { ...old, ...updates });
|
|
360
|
-
}
|
|
361
|
-
},
|
|
362
|
-
setHighlightsVisible(arg) {
|
|
363
|
-
self.highlightsVisible = arg;
|
|
364
|
-
},
|
|
365
|
-
setLabelsVisible(arg) {
|
|
366
|
-
self.labelsVisible = arg;
|
|
367
|
-
},
|
|
1158
|
+
/**
|
|
1159
|
+
* #action
|
|
1160
|
+
* set temporary vertical guides (e.g., for LD display hover)
|
|
1161
|
+
*/
|
|
368
1162
|
setVolatileGuides(guides) {
|
|
369
1163
|
self.volatileGuides = guides;
|
|
370
1164
|
},
|
|
1165
|
+
/**
|
|
1166
|
+
* #action
|
|
1167
|
+
*/
|
|
371
1168
|
scrollTo(offsetPx) {
|
|
372
1169
|
const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset);
|
|
373
|
-
self.
|
|
1170
|
+
self.windowStartBp = newOffsetPx * self.bpPerPx;
|
|
374
1171
|
return newOffsetPx;
|
|
375
1172
|
},
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1173
|
+
/**
|
|
1174
|
+
* #action
|
|
1175
|
+
* `scrollTo`'s bp-space twin: place the window's left edge at a
|
|
1176
|
+
* linearized bp coordinate, clamped to the same scroll limits.
|
|
1177
|
+
*
|
|
1178
|
+
* It exists so a caller that already knows where it wants to be in bp —
|
|
1179
|
+
* zoomTo, anchoring the base under the cursor — does not have to convert
|
|
1180
|
+
* to pixels and let this convert back. That round trip is lossy once per
|
|
1181
|
+
* frame, and a scroll-zoom burst is a few dozen frames, which is enough
|
|
1182
|
+
* to walk the base out from under the cursor.
|
|
1183
|
+
*/
|
|
1184
|
+
scrollToBp(startBp) {
|
|
1185
|
+
const { bpPerPx } = self;
|
|
1186
|
+
if (bpPerPx > 0) {
|
|
1187
|
+
self.windowStartBp = clamp(startBp, self.minOffset * bpPerPx, self.maxOffset * bpPerPx);
|
|
380
1188
|
}
|
|
1189
|
+
return self.windowStartBp;
|
|
1190
|
+
},
|
|
1191
|
+
/**
|
|
1192
|
+
* #action
|
|
1193
|
+
*/
|
|
1194
|
+
zoomTo(bpPerPx, offset = self.width / 2) {
|
|
1195
|
+
const newBpPerPx = clamp(bpPerPx, self.minBpPerPx, self.maxBpPerPx);
|
|
381
1196
|
const oldBpPerPx = self.bpPerPx;
|
|
382
|
-
if (Math.abs(oldBpPerPx - newBpPerPx)
|
|
383
|
-
|
|
384
|
-
|
|
1197
|
+
if (Math.abs(oldBpPerPx - newBpPerPx) >= BP_PER_PX_EPSILON) {
|
|
1198
|
+
// Anchor the base under the cursor. The window is stored in bp, so
|
|
1199
|
+
// this is exact arithmetic in the units the state is already in: the
|
|
1200
|
+
// cursor sits at `windowStartBp + offset * oldBpPerPx` before the
|
|
1201
|
+
// zoom, and must sit at the same base `offset` pixels in after it.
|
|
1202
|
+
//
|
|
1203
|
+
// Neither conversion to pixels nor rounding appears, and that is the
|
|
1204
|
+
// point — either one loses a fraction of a pixel per frame, which at
|
|
1205
|
+
// high bpPerPx is a real number of bases, and a scroll-zoom burst is
|
|
1206
|
+
// dozens of frames of it compounding.
|
|
1207
|
+
const anchorBp = self.windowStartBp + offset * oldBpPerPx;
|
|
1208
|
+
// widen/narrow about the left edge, then put the anchor back under
|
|
1209
|
+
// the cursor. Guarded on displayedRegions as it always was: with
|
|
1210
|
+
// nothing displayed there is no scroll space to place it in.
|
|
1211
|
+
self.windowWidthBp = newBpPerPx * self.width;
|
|
1212
|
+
if (self.displayedRegions.length) {
|
|
1213
|
+
this.scrollToBp(anchorBp - offset * newBpPerPx);
|
|
1214
|
+
}
|
|
385
1215
|
}
|
|
386
|
-
self.bpPerPx
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1216
|
+
return self.bpPerPx;
|
|
1217
|
+
},
|
|
1218
|
+
/**
|
|
1219
|
+
* #action
|
|
1220
|
+
* sets offsets of rubberband, used in the get sequence dialog can call
|
|
1221
|
+
* view.getSelectedRegions(view.leftOffset,view.rightOffset) to compute
|
|
1222
|
+
* the selected regions from the offsets
|
|
1223
|
+
*/
|
|
392
1224
|
setOffsets(left, right) {
|
|
393
1225
|
self.leftOffset = left;
|
|
394
1226
|
self.rightOffset = right;
|
|
395
1227
|
},
|
|
396
|
-
|
|
397
|
-
|
|
1228
|
+
/**
|
|
1229
|
+
* #action
|
|
1230
|
+
* Raise the picker over search results that name more than one place.
|
|
1231
|
+
* @param onPick - where a picked result lands; by default it navigates
|
|
1232
|
+
* there and shows the track its index came from
|
|
1233
|
+
*/
|
|
1234
|
+
setSearchResults(searchResults, searchQuery, assemblyName, onPick = result => navToOption({
|
|
1235
|
+
option: result,
|
|
1236
|
+
model: self,
|
|
1237
|
+
assemblyName,
|
|
1238
|
+
})) {
|
|
1239
|
+
getDialogHost(self).queueDialog(handleClose => [
|
|
398
1240
|
SearchResultsDialog,
|
|
399
1241
|
{
|
|
400
1242
|
model: self,
|
|
@@ -402,486 +1244,1610 @@ export function stateModelFactory(pluginManager) {
|
|
|
402
1244
|
searchQuery,
|
|
403
1245
|
handleClose,
|
|
404
1246
|
assemblyName,
|
|
1247
|
+
onPick,
|
|
405
1248
|
},
|
|
406
1249
|
]);
|
|
407
1250
|
},
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
},
|
|
412
|
-
|
|
413
|
-
self.displayedRegions = cast([...self.displayedRegions]
|
|
414
|
-
.reverse()
|
|
415
|
-
.map(region => ({ ...region, reversed: !region.reversed })));
|
|
416
|
-
this.scrollTo(self.totalBp / self.bpPerPx - self.offsetPx - self.width);
|
|
417
|
-
},
|
|
418
|
-
showTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}) {
|
|
419
|
-
return showTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot);
|
|
1251
|
+
/**
|
|
1252
|
+
* #action
|
|
1253
|
+
*/
|
|
1254
|
+
showTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
|
|
1255
|
+
return showTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
|
|
420
1256
|
},
|
|
1257
|
+
/**
|
|
1258
|
+
* #action
|
|
1259
|
+
*/
|
|
421
1260
|
hideTrack(trackId) {
|
|
422
1261
|
return hideTrackGeneric(self, trackId);
|
|
423
1262
|
},
|
|
424
1263
|
}))
|
|
425
1264
|
.actions(self => ({
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1265
|
+
/**
|
|
1266
|
+
* #action
|
|
1267
|
+
* Resize every unminimized track so the view is exactly as tall as the
|
|
1268
|
+
* scroll port it sits in. Does nothing until the view has measured one.
|
|
1269
|
+
*/
|
|
1270
|
+
fitTracksToWindow() {
|
|
1271
|
+
if (self.scrollPortExcess) {
|
|
1272
|
+
self.resizeTracks(-self.scrollPortExcess);
|
|
430
1273
|
}
|
|
431
1274
|
},
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
},
|
|
438
|
-
|
|
439
|
-
const track = self.tracks.find(track => track.id === id);
|
|
440
|
-
if (track) {
|
|
441
|
-
self.tracks = cast([track, ...self.tracks.filter(t => t.id !== id)]);
|
|
442
|
-
}
|
|
1275
|
+
/**
|
|
1276
|
+
* #action
|
|
1277
|
+
* showTrack for a track whose display state model may be lazily
|
|
1278
|
+
* loaded: loads it, then shows
|
|
1279
|
+
*/
|
|
1280
|
+
async launchTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
|
|
1281
|
+
return launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
|
|
443
1282
|
},
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1283
|
+
}))
|
|
1284
|
+
.views(self => ({
|
|
1285
|
+
/**
|
|
1286
|
+
* #method
|
|
1287
|
+
* The shown track a move action means: the menu passes a track model's
|
|
1288
|
+
* own `id`, an agent passes the `trackId` it knows the track by, and
|
|
1289
|
+
* both name one shown track. Answers the model id, or the argument
|
|
1290
|
+
* unchanged when nothing shown matches, so the move reports it.
|
|
1291
|
+
*/
|
|
1292
|
+
trackModelId(idOrTrackId) {
|
|
1293
|
+
return ((self.tracks.find(t => t.id === idOrTrackId) ??
|
|
1294
|
+
self.tracks.find(t => t.configuration.trackId === idOrTrackId))?.id ?? idOrTrackId);
|
|
449
1295
|
},
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
throw new Error(`Track ID ${targetId} not found`);
|
|
1296
|
+
}))
|
|
1297
|
+
.actions(self => {
|
|
1298
|
+
function shownIndex(idOrTrackId) {
|
|
1299
|
+
const id = self.trackModelId(idOrTrackId);
|
|
1300
|
+
const index = self.tracks.findIndex(track => track.id === id);
|
|
1301
|
+
if (index === -1) {
|
|
1302
|
+
throw new Error(`No shown track has id or trackId "${idOrTrackId}" — the view's tracks are ${self.tracks.map(t => t.configuration.trackId).join(', ') || 'none'}`);
|
|
458
1303
|
}
|
|
1304
|
+
return index;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* #action
|
|
1308
|
+
* Move one shown track to another's position; each is a track model id
|
|
1309
|
+
* or a trackId.
|
|
1310
|
+
*/
|
|
1311
|
+
function moveTrack(moving, target) {
|
|
1312
|
+
const oldIndex = shownIndex(moving);
|
|
1313
|
+
const newIndex = shownIndex(target);
|
|
1314
|
+
// splicing at newIndex after removing oldIndex lands a track dragged
|
|
1315
|
+
// down after its target and one dragged up before it
|
|
459
1316
|
const tracks = self.tracks.filter((_, idx) => idx !== oldIndex);
|
|
460
1317
|
tracks.splice(newIndex, 0, self.tracks[oldIndex]);
|
|
461
1318
|
self.tracks = cast(tracks);
|
|
462
|
-
}
|
|
1319
|
+
}
|
|
1320
|
+
function moveWithinSection(idOrTrackId, pick) {
|
|
1321
|
+
const moving = self.tracks[shownIndex(idOrTrackId)];
|
|
1322
|
+
const section = self.trackSection(moving.id);
|
|
1323
|
+
const target = pick(section, section.indexOf(moving));
|
|
1324
|
+
if (target && target !== moving) {
|
|
1325
|
+
moveTrack(moving.id, target.id);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
moveTrack,
|
|
1330
|
+
/**
|
|
1331
|
+
* #action
|
|
1332
|
+
*/
|
|
1333
|
+
moveTrackDown(idOrTrackId) {
|
|
1334
|
+
moveWithinSection(idOrTrackId, (section, i) => section[i + 1]);
|
|
1335
|
+
},
|
|
1336
|
+
/**
|
|
1337
|
+
* #action
|
|
1338
|
+
*/
|
|
1339
|
+
moveTrackUp(idOrTrackId) {
|
|
1340
|
+
moveWithinSection(idOrTrackId, (section, i) => section[i - 1]);
|
|
1341
|
+
},
|
|
1342
|
+
/**
|
|
1343
|
+
* #action
|
|
1344
|
+
*/
|
|
1345
|
+
moveTrackToTop(idOrTrackId) {
|
|
1346
|
+
moveWithinSection(idOrTrackId, section => section[0]);
|
|
1347
|
+
},
|
|
1348
|
+
/**
|
|
1349
|
+
* #action
|
|
1350
|
+
*/
|
|
1351
|
+
moveTrackToBottom(idOrTrackId) {
|
|
1352
|
+
moveWithinSection(idOrTrackId, section => section.at(-1));
|
|
1353
|
+
},
|
|
1354
|
+
};
|
|
1355
|
+
})
|
|
1356
|
+
.actions(self => ({
|
|
1357
|
+
/**
|
|
1358
|
+
* #action
|
|
1359
|
+
*/
|
|
463
1360
|
toggleTrack(trackId) {
|
|
464
|
-
toggleTrackGeneric(self, trackId);
|
|
465
|
-
},
|
|
1361
|
+
return toggleTrackGeneric(self, trackId);
|
|
1362
|
+
},
|
|
1363
|
+
/**
|
|
1364
|
+
* #action
|
|
1365
|
+
* toggleTrack with launchTrack's loading behavior
|
|
1366
|
+
*/
|
|
1367
|
+
async launchToggleTrack(trackId) {
|
|
1368
|
+
return launchToggleTrackGeneric(self, trackId);
|
|
1369
|
+
},
|
|
1370
|
+
/**
|
|
1371
|
+
* #action
|
|
1372
|
+
*/
|
|
466
1373
|
setTrackLabels(setting) {
|
|
467
1374
|
self.trackLabels = setting;
|
|
468
1375
|
},
|
|
1376
|
+
/**
|
|
1377
|
+
* #action
|
|
1378
|
+
*/
|
|
469
1379
|
setShowCenterLine(b) {
|
|
470
1380
|
self.showCenterLine = b;
|
|
471
1381
|
},
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
},
|
|
1382
|
+
/**
|
|
1383
|
+
* #action
|
|
1384
|
+
*/
|
|
476
1385
|
activateTrackSelector() {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1386
|
+
const { session, selector } = openTrackSelectorWidget(self);
|
|
1387
|
+
session.showWidget(selector);
|
|
1388
|
+
return selector;
|
|
1389
|
+
},
|
|
1390
|
+
/**
|
|
1391
|
+
* #action
|
|
1392
|
+
*/
|
|
1393
|
+
toggleTrackSelector() {
|
|
1394
|
+
const { session, selector } = openTrackSelectorWidget(self);
|
|
1395
|
+
if (isTrackSelectorVisible(self)) {
|
|
1396
|
+
session.hideWidget(selector);
|
|
484
1397
|
}
|
|
485
|
-
|
|
486
|
-
|
|
1398
|
+
else {
|
|
1399
|
+
session.showWidget(selector);
|
|
1400
|
+
}
|
|
1401
|
+
return selector;
|
|
1402
|
+
},
|
|
1403
|
+
/**
|
|
1404
|
+
* #method
|
|
1405
|
+
* Helper method for the fetchSequence.
|
|
1406
|
+
* Retrieves the corresponding regions that were selected by the
|
|
1407
|
+
* rubberband
|
|
1408
|
+
*
|
|
1409
|
+
* @param leftOffset - `object as {start, end, index, offset}`, offset = start
|
|
1410
|
+
* of user drag
|
|
1411
|
+
* @param rightOffset - `object as {start, end, index, offset}`,
|
|
1412
|
+
* offset = end of user drag
|
|
1413
|
+
* @returns array of Region[]
|
|
1414
|
+
*/
|
|
487
1415
|
getSelectedRegions(leftOffset, rightOffset) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1416
|
+
if (!leftOffset || !rightOffset) {
|
|
1417
|
+
return [];
|
|
1418
|
+
}
|
|
1419
|
+
const layout = {
|
|
1420
|
+
displayedRegions: self.displayedRegions,
|
|
1421
|
+
bpPerPx: self.bpPerPx,
|
|
1422
|
+
offsetPx: self.offsetPx,
|
|
1423
|
+
width: self.width,
|
|
1424
|
+
minimumBlockWidth: self.minimumBlockWidth,
|
|
1425
|
+
};
|
|
1426
|
+
const { bpPerPx, offsetPx: rawOffsetPx } = computeMoveToLayout(layout, leftOffset, rightOffset);
|
|
1427
|
+
// mirror Base1DView.scrollTo clamping: raw offsetPx can be far outside
|
|
1428
|
+
// the valid range when both offsets are oob on the same side
|
|
1429
|
+
const offsetPx = clamp(rawOffsetPx, self.minOffset, self.totalBp / bpPerPx - MAX_OFFSET_PADDING_PX);
|
|
1430
|
+
return wholeBaseRegions(calculateDynamicBlocks({
|
|
1431
|
+
...layout,
|
|
1432
|
+
bpPerPx,
|
|
1433
|
+
offsetPx,
|
|
1434
|
+
}).contentBlocks);
|
|
1435
|
+
},
|
|
1436
|
+
/**
|
|
1437
|
+
* #action
|
|
1438
|
+
*/
|
|
502
1439
|
horizontalScroll(distance) {
|
|
503
1440
|
const oldOffsetPx = self.offsetPx;
|
|
1441
|
+
// newOffsetPx is the actual offset after the scroll is clamped
|
|
504
1442
|
const newOffsetPx = self.scrollTo(self.offsetPx + distance);
|
|
505
1443
|
return newOffsetPx - oldOffsetPx;
|
|
506
1444
|
},
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
},
|
|
511
|
-
showAllRegionsInAssembly(assemblyName) {
|
|
512
|
-
const session = getSession(self);
|
|
513
|
-
const { assemblyManager } = session;
|
|
514
|
-
if (!assemblyName) {
|
|
515
|
-
const names = new Set(self.displayedRegions.map(r => r.assemblyName));
|
|
516
|
-
if (names.size > 1) {
|
|
517
|
-
session.notify(`Can't perform operation with multiple assemblies currently`);
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
;
|
|
521
|
-
[assemblyName] = [...names];
|
|
522
|
-
}
|
|
523
|
-
const regions = assemblyManager.get(assemblyName)?.regions;
|
|
524
|
-
if (!regions) {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
this.setDisplayedRegions(regions);
|
|
528
|
-
self.zoomTo(self.maxBpPerPx);
|
|
529
|
-
self.scrollTo(getCenteredOffsetPx(self.displayedRegionsTotalPx, self.width));
|
|
530
|
-
},
|
|
1445
|
+
/**
|
|
1446
|
+
* #action
|
|
1447
|
+
*/
|
|
531
1448
|
setDraggingTrackId(idx) {
|
|
532
1449
|
self.draggingTrackId = idx;
|
|
533
1450
|
if (idx === undefined) {
|
|
534
1451
|
self.lastTrackDragY = undefined;
|
|
535
1452
|
}
|
|
536
1453
|
},
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1454
|
+
/**
|
|
1455
|
+
* #action
|
|
1456
|
+
* called while dragging a track over the track at `targetId`; reorders
|
|
1457
|
+
* once the cursor has moved far enough (see shouldSwapTracks) to avoid
|
|
1458
|
+
* jitter when a short track is dragged over a tall one
|
|
1459
|
+
*/
|
|
1460
|
+
onTrackDragOver(targetId, currentY) {
|
|
1461
|
+
const { draggingTrackId } = self;
|
|
1462
|
+
if (draggingTrackId !== undefined && draggingTrackId !== targetId) {
|
|
1463
|
+
const draggingIdx = self.tracks.findIndex(t => t.id === draggingTrackId);
|
|
1464
|
+
const targetIdx = self.tracks.findIndex(t => t.id === targetId);
|
|
1465
|
+
if (draggingIdx !== -1 && targetIdx !== -1) {
|
|
1466
|
+
const movingDown = targetIdx > draggingIdx;
|
|
1467
|
+
if (shouldSwapTracks(self.lastTrackDragY, currentY, movingDown)) {
|
|
1468
|
+
self.lastTrackDragY = currentY;
|
|
1469
|
+
self.moveTrack(draggingTrackId, targetId);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
554
1473
|
},
|
|
1474
|
+
/**
|
|
1475
|
+
* #action
|
|
1476
|
+
*/
|
|
1477
|
+
setLaunch(launch) {
|
|
1478
|
+
self.launch = launch;
|
|
1479
|
+
},
|
|
1480
|
+
/**
|
|
1481
|
+
* #method
|
|
1482
|
+
* renders the view to SVG markup, which it returns; saves it through
|
|
1483
|
+
* FileSaver unless `save: false`
|
|
1484
|
+
*/
|
|
1485
|
+
// Promise<string> is stated: renderToSvg is typed against this very
|
|
1486
|
+
// model, so an inferred return makes the type reference itself (TS2456)
|
|
555
1487
|
async exportSvg(opts = {}) {
|
|
556
|
-
|
|
557
|
-
const html = await renderToSvg(self, opts);
|
|
558
|
-
const { saveAs } = await import('file-saver-es');
|
|
559
|
-
if (opts.format === 'png') {
|
|
560
|
-
const img = new Image();
|
|
561
|
-
const svgBlob = new Blob([html], { type: 'image/svg+xml' });
|
|
562
|
-
const url = URL.createObjectURL(svgBlob);
|
|
563
|
-
await new Promise((resolve, reject) => {
|
|
564
|
-
img.onload = () => {
|
|
565
|
-
const canvas = document.createElement('canvas');
|
|
566
|
-
canvas.width = img.width;
|
|
567
|
-
canvas.height = img.height;
|
|
568
|
-
const ctx = canvas.getContext('2d');
|
|
569
|
-
ctx.drawImage(img, 0, 0);
|
|
570
|
-
URL.revokeObjectURL(url);
|
|
571
|
-
canvas.toBlob(blob => {
|
|
572
|
-
if (blob) {
|
|
573
|
-
saveAs(blob, opts.filename || 'image.png');
|
|
574
|
-
resolve();
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
reject(new Error(`Failed to create PNG. The image may be too large (${img.width}x${img.height}). Try reducing the view size or use SVG format.`));
|
|
578
|
-
}
|
|
579
|
-
}, 'image/png');
|
|
580
|
-
};
|
|
581
|
-
img.onerror = () => {
|
|
582
|
-
URL.revokeObjectURL(url);
|
|
583
|
-
reject(new Error('Failed to load SVG for PNG conversion'));
|
|
584
|
-
};
|
|
585
|
-
img.src = url;
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
saveAs(new Blob([html], { type: 'image/svg+xml' }), opts.filename || 'image.svg');
|
|
590
|
-
}
|
|
1488
|
+
return exportViewSvg(self, opts, () => import('./svgcomponents/SVGLinearGenomeView.js'));
|
|
591
1489
|
},
|
|
592
1490
|
}))
|
|
593
1491
|
.actions(self => {
|
|
594
|
-
let
|
|
1492
|
+
let cancelLastSlide = () => { };
|
|
1493
|
+
let cancelLastZoom = () => { };
|
|
1494
|
+
// Both animations pass `read`, so the spring yields the moment anything
|
|
1495
|
+
// else moves the view: an animation drives zoomTo/scrollTo from its own
|
|
1496
|
+
// position for up to a second, and a direct interaction landing in that
|
|
1497
|
+
// window — a wheel/pinch zoom, a locstring nav, a rubberband "zoom to
|
|
1498
|
+
// region", a click-drag pan — used to be overwritten on the next frame.
|
|
1499
|
+
// Only the zoom slider defended itself, via cancelZoomAnimation, and
|
|
1500
|
+
// expecting every future call site to remember that is not a workable
|
|
1501
|
+
// contract.
|
|
1502
|
+
/**
|
|
1503
|
+
* #action
|
|
1504
|
+
* perform animated slide
|
|
1505
|
+
*/
|
|
595
1506
|
function slide(viewWidths) {
|
|
596
|
-
const [animate, cancelAnimation] = springAnimate(
|
|
597
|
-
|
|
598
|
-
|
|
1507
|
+
const [animate, cancelAnimation] = springAnimate({
|
|
1508
|
+
from: self.offsetPx,
|
|
1509
|
+
to: self.offsetPx + self.width * viewWidths,
|
|
1510
|
+
read: () => self.offsetPx,
|
|
1511
|
+
write: px => {
|
|
1512
|
+
self.scrollTo(px);
|
|
1513
|
+
},
|
|
1514
|
+
tension: 200,
|
|
1515
|
+
});
|
|
1516
|
+
cancelLastSlide();
|
|
1517
|
+
cancelLastSlide = cancelAnimation;
|
|
599
1518
|
animate();
|
|
600
1519
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1520
|
+
/**
|
|
1521
|
+
* #action
|
|
1522
|
+
* perform animated zoom
|
|
1523
|
+
*/
|
|
605
1524
|
function zoom(targetBpPerPx) {
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1525
|
+
cancelLastZoom();
|
|
1526
|
+
const effectiveTarget = clamp(targetBpPerPx, self.minBpPerPx, self.maxBpPerPx);
|
|
1527
|
+
// nothing to animate when already at the limit, or at the target
|
|
1528
|
+
if (effectiveTarget !== self.bpPerPx) {
|
|
1529
|
+
const [animate, cancelAnimation] = springAnimate({
|
|
1530
|
+
from: self.bpPerPx,
|
|
1531
|
+
to: effectiveTarget,
|
|
1532
|
+
read: () => self.bpPerPx,
|
|
1533
|
+
write: bpPerPx => {
|
|
1534
|
+
self.zoomTo(bpPerPx);
|
|
1535
|
+
},
|
|
1536
|
+
tension: 1000,
|
|
1537
|
+
friction: 50,
|
|
1538
|
+
});
|
|
1539
|
+
cancelLastZoom = cancelAnimation;
|
|
1540
|
+
animate();
|
|
614
1541
|
}
|
|
615
|
-
self.setTargetBpPerPx(effectiveTarget);
|
|
616
|
-
const targetScaleFactor = self.bpPerPx / effectiveTarget;
|
|
617
|
-
const [animate, cancelAnimation] = springAnimate(self.scaleFactor, targetScaleFactor, self.setScaleFactor, () => {
|
|
618
|
-
self.zoomTo(effectiveTarget);
|
|
619
|
-
self.setScaleFactor(1);
|
|
620
|
-
self.setTargetBpPerPx(undefined);
|
|
621
|
-
}, 0, 1000, 50);
|
|
622
|
-
cancelLastAnimation = cancelAnimation;
|
|
623
|
-
animate();
|
|
624
1542
|
}
|
|
625
|
-
|
|
1543
|
+
/**
|
|
1544
|
+
* #action
|
|
1545
|
+
* cancel an in-flight animated zoom. The animation already yields to any
|
|
1546
|
+
* other zoom on its own, so this is for stopping it *without* changing the
|
|
1547
|
+
* zoom — the slider grabbing the thumb, before it has a value to commit.
|
|
1548
|
+
*/
|
|
1549
|
+
function cancelZoomAnimation() {
|
|
1550
|
+
cancelLastZoom();
|
|
1551
|
+
}
|
|
1552
|
+
return { slide, zoom, cancelZoomAnimation };
|
|
626
1553
|
})
|
|
627
1554
|
.views(self => ({
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
get
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1555
|
+
/**
|
|
1556
|
+
* #getter
|
|
1557
|
+
*/
|
|
1558
|
+
get showsWholeChromosome() {
|
|
1559
|
+
const { displayedRegions } = self;
|
|
1560
|
+
const region = displayedRegions.length === 1 ? displayedRegions[0] : undefined;
|
|
1561
|
+
const full = region
|
|
1562
|
+
? getSession(self)
|
|
1563
|
+
.assemblyManager.get(region.assemblyName)
|
|
1564
|
+
?.getRegionForRefName(region.refName)
|
|
1565
|
+
: undefined;
|
|
1566
|
+
return region && full
|
|
1567
|
+
? region.start === full.start && region.end === full.end
|
|
1568
|
+
: false;
|
|
637
1569
|
},
|
|
1570
|
+
/**
|
|
1571
|
+
* #getter
|
|
1572
|
+
* an ideogram only reads correctly against an entire chromosome: on a
|
|
1573
|
+
* sub-region it is a meaningless slice of bands, and the centromere shows
|
|
1574
|
+
* up as a lone half-triangle. The chromosome needs bands of its own too:
|
|
1575
|
+
* a cytoband file often lists only the main chromosomes, not chrM or the
|
|
1576
|
+
* unplaced scaffolds
|
|
1577
|
+
*/
|
|
1578
|
+
get canShowCytobands() {
|
|
1579
|
+
const region = self.displayedRegions[0];
|
|
1580
|
+
return (this.showsWholeChromosome &&
|
|
1581
|
+
!!region &&
|
|
1582
|
+
getCytobands(getSession(self).assemblyManager.get(region.assemblyName), region.refName).length > 0);
|
|
1583
|
+
},
|
|
1584
|
+
/**
|
|
1585
|
+
* #getter
|
|
1586
|
+
* the `showCytobands` setting gated by whether cytobands can be shown at
|
|
1587
|
+
* all (whole chromosome with bands of its own) and by the header
|
|
1588
|
+
* overview they draw in being on, which the SVG export follows too
|
|
1589
|
+
*/
|
|
1590
|
+
get effectiveShowCytobands() {
|
|
1591
|
+
return (this.canShowCytobands &&
|
|
1592
|
+
self.showCytobands &&
|
|
1593
|
+
!self.hideHeaderOverview);
|
|
1594
|
+
},
|
|
1595
|
+
/**
|
|
1596
|
+
* #getter
|
|
1597
|
+
* the cytoband is displayed to the right of the chromosome name, and
|
|
1598
|
+
* that offset is calculated manually with this method
|
|
1599
|
+
*/
|
|
638
1600
|
get cytobandOffset() {
|
|
639
|
-
return this.
|
|
640
|
-
?
|
|
1601
|
+
return this.effectiveShowCytobands
|
|
1602
|
+
? cytobandLabelGutterWidth(self.displayedRegions[0]?.refName || '')
|
|
641
1603
|
: 0;
|
|
642
1604
|
},
|
|
1605
|
+
/**
|
|
1606
|
+
* #getter
|
|
1607
|
+
*/
|
|
1608
|
+
get isTrackSelectorOpen() {
|
|
1609
|
+
return isTrackSelectorVisible(self);
|
|
1610
|
+
},
|
|
643
1611
|
}))
|
|
644
1612
|
.views(self => ({
|
|
1613
|
+
/**
|
|
1614
|
+
* #method
|
|
1615
|
+
* return the view menu items
|
|
1616
|
+
*/
|
|
645
1617
|
menuItems() {
|
|
646
1618
|
return buildMenuItems(self);
|
|
647
1619
|
},
|
|
1620
|
+
/**
|
|
1621
|
+
* #method
|
|
1622
|
+
* the view menu's Highlights submenu; extend this to add a row there
|
|
1623
|
+
*/
|
|
1624
|
+
highlightsSubMenuItems() {
|
|
1625
|
+
return buildHighlightsSubMenuItems(self);
|
|
1626
|
+
},
|
|
1627
|
+
/**
|
|
1628
|
+
* #method
|
|
1629
|
+
* what a plugin can start from the selected region — a synteny view, a
|
|
1630
|
+
* consensus call. Extend this rather than `rubberBandMenuItems` so the
|
|
1631
|
+
* entries collect under the menu's "Launch" submenu; that grouping is
|
|
1632
|
+
* decided once here rather than by whichever contributor runs first, and
|
|
1633
|
+
* keeps the rubberband menu itself short as plugins pile on.
|
|
1634
|
+
*/
|
|
1635
|
+
rubberBandLaunchMenuItems() {
|
|
1636
|
+
return [];
|
|
1637
|
+
},
|
|
1638
|
+
/**
|
|
1639
|
+
* #getter
|
|
1640
|
+
* geometry of the overview scalebar — derived from displayedRegions,
|
|
1641
|
+
* width, and cytobandOffset so it stays cached by MobX
|
|
1642
|
+
*/
|
|
1643
|
+
get overviewLayout() {
|
|
1644
|
+
return createOverviewLayout({
|
|
1645
|
+
displayedRegions: self.displayedRegions,
|
|
1646
|
+
width: self.width - self.cytobandOffset,
|
|
1647
|
+
minimumBlockWidth: self.minimumBlockWidth,
|
|
1648
|
+
});
|
|
1649
|
+
},
|
|
648
1650
|
}))
|
|
649
1651
|
.views(self => {
|
|
650
1652
|
let currentlyCalculatedStaticBlocks;
|
|
651
|
-
let
|
|
1653
|
+
let coverageLeftPx = 0;
|
|
1654
|
+
let coverageRightPx = 0;
|
|
1655
|
+
let prevBpPerPx;
|
|
1656
|
+
let prevWidth;
|
|
1657
|
+
let prevDisplayedRegions;
|
|
652
1658
|
return {
|
|
1659
|
+
/**
|
|
1660
|
+
* #getter
|
|
1661
|
+
* static blocks are an important concept jbrowse uses to avoid
|
|
1662
|
+
* re-rendering when you scroll to the side. when you horizontally
|
|
1663
|
+
* scroll to the right, old blocks to the left may be removed, and new
|
|
1664
|
+
* blocks may be instantiated on the right. tracks may use the static
|
|
1665
|
+
* blocks to render their data for the region represented by the block
|
|
1666
|
+
*/
|
|
653
1667
|
get staticBlocks() {
|
|
1668
|
+
const { offsetPx, bpPerPx, width, displayedRegions } = self;
|
|
1669
|
+
// Fast path: if only offsetPx changed and viewport is still within
|
|
1670
|
+
// the coverage range of existing blocks, skip the expensive
|
|
1671
|
+
// calculateStaticBlocks call entirely.
|
|
1672
|
+
if (currentlyCalculatedStaticBlocks !== undefined &&
|
|
1673
|
+
bpPerPx === prevBpPerPx &&
|
|
1674
|
+
width === prevWidth &&
|
|
1675
|
+
displayedRegions === prevDisplayedRegions &&
|
|
1676
|
+
offsetPx >= coverageLeftPx &&
|
|
1677
|
+
offsetPx + width <= coverageRightPx) {
|
|
1678
|
+
return currentlyCalculatedStaticBlocks;
|
|
1679
|
+
}
|
|
654
1680
|
const newBlocks = calculateStaticBlocks(self);
|
|
655
|
-
|
|
1681
|
+
// Hand back the previous BlockSet when the recompute produced the same
|
|
1682
|
+
// blocks, so a sideways scroll past the coverage edge doesn't re-render
|
|
1683
|
+
// every track. Geometry is compared first: on a zoom it always
|
|
1684
|
+
// differs, which skips the key walk entirely — the whole-genome view
|
|
1685
|
+
// has one block per refName, so joining those keys into a string (as
|
|
1686
|
+
// this used to) allocated an array and a string per zoom frame.
|
|
656
1687
|
if (currentlyCalculatedStaticBlocks === undefined ||
|
|
657
|
-
|
|
1688
|
+
bpPerPx !== prevBpPerPx ||
|
|
1689
|
+
width !== prevWidth ||
|
|
1690
|
+
!sameBlockKeys(currentlyCalculatedStaticBlocks, newBlocks)) {
|
|
658
1691
|
currentlyCalculatedStaticBlocks = newBlocks;
|
|
659
|
-
currentBlockKeys = newKeys;
|
|
660
|
-
return currentlyCalculatedStaticBlocks;
|
|
661
1692
|
}
|
|
662
|
-
|
|
663
|
-
|
|
1693
|
+
// Update coverage range from content block extent only.
|
|
1694
|
+
// Using all blocks (including padding) would inflate the range
|
|
1695
|
+
// and let the fast path return stale blocks when the viewport
|
|
1696
|
+
// scrolls into the padding area where no content blocks exist.
|
|
1697
|
+
const cBlocks = currentlyCalculatedStaticBlocks.contentBlocks;
|
|
1698
|
+
if (cBlocks.length > 0) {
|
|
1699
|
+
const last = cBlocks[cBlocks.length - 1];
|
|
1700
|
+
coverageLeftPx = cBlocks[0].offsetPx;
|
|
1701
|
+
coverageRightPx = last.offsetPx + last.widthPx;
|
|
664
1702
|
}
|
|
1703
|
+
prevBpPerPx = bpPerPx;
|
|
1704
|
+
prevWidth = width;
|
|
1705
|
+
prevDisplayedRegions = displayedRegions;
|
|
1706
|
+
return currentlyCalculatedStaticBlocks;
|
|
665
1707
|
},
|
|
1708
|
+
/**
|
|
1709
|
+
* #getter
|
|
1710
|
+
* dynamic blocks represent the exact coordinates of the currently
|
|
1711
|
+
* visible genome regions on the screen. they are similar to static
|
|
1712
|
+
* blocks, but static blocks can go offscreen while dynamic blocks
|
|
1713
|
+
* represent exactly what is on screen
|
|
1714
|
+
*/
|
|
666
1715
|
get dynamicBlocks() {
|
|
667
1716
|
return calculateDynamicBlocks(self);
|
|
668
1717
|
},
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1718
|
+
/**
|
|
1719
|
+
* #getter
|
|
1720
|
+
* all overview scalebar blocks (content + elided), laid out on the
|
|
1721
|
+
* overviewLayout. memoized so the scalebar doesn't recompute it per
|
|
1722
|
+
* render
|
|
1723
|
+
*/
|
|
1724
|
+
get overviewBlocks() {
|
|
1725
|
+
return calculateDynamicBlocks(self.overviewLayout).blocks;
|
|
1726
|
+
},
|
|
1727
|
+
/**
|
|
1728
|
+
* #getter
|
|
1729
|
+
* leading/trailing pixel span of the visible regions projected onto the
|
|
1730
|
+
* overviewLayout — the geometry of the overview's "you are here"
|
|
1731
|
+
* rectangle, and of the top edge of the polygon drawn under it. Elided
|
|
1732
|
+
* regions count: at whole-genome zoom the tail of an assembly is all
|
|
1733
|
+
* coalesced tiny contigs, and stopping at the last *content* block left
|
|
1734
|
+
* the rectangle short of (or, scrolled onto that tail, absent from) the
|
|
1735
|
+
* polygon it sits on.
|
|
1736
|
+
*/
|
|
1737
|
+
get overviewRegionPxSpan() {
|
|
1738
|
+
return getOverviewRegionPxSpan({
|
|
1739
|
+
overview: self.overviewLayout,
|
|
1740
|
+
bpPerPx: self.bpPerPx,
|
|
1741
|
+
blocks: this.dynamicBlocks.blocks,
|
|
1742
|
+
});
|
|
1743
|
+
},
|
|
1744
|
+
/**
|
|
1745
|
+
* #getter
|
|
1746
|
+
* The x shift that maps the **staticBlocks frame** onto the viewport:
|
|
1747
|
+
* `translateX(view.staticBlocksTranslateX)` on one container places
|
|
1748
|
+
* every `gridlineTicks`, `scalebarLabels` and `paddingSpans` entry at
|
|
1749
|
+
* once, and a pan then moves that single transform rather than each
|
|
1750
|
+
* child.
|
|
1751
|
+
*
|
|
1752
|
+
* The frame exists because those three are laid out across every
|
|
1753
|
+
* displayed region rather than across the viewport — it overhangs on
|
|
1754
|
+
* both sides — so their `x` values are stable under a scroll and only
|
|
1755
|
+
* this number moves. `scalebarRefNameLabels` is the exception and says
|
|
1756
|
+
* so: its `transform` is already a screen x, because a sticky label's
|
|
1757
|
+
* position is a function of the scroll rather than of block geometry.
|
|
1758
|
+
*
|
|
1759
|
+
* **The subtraction must happen here, in float64, and that is the
|
|
1760
|
+
* load-bearing part.** `offsetPx` is a whole-genome pixel coordinate —
|
|
1761
|
+
* hg38 chr1 at base resolution is already past 1e10 — and a length that
|
|
1762
|
+
* size does not survive the trip through CSS: the transform matrix is
|
|
1763
|
+
* float32 by the time it reaches the compositor, where consecutive
|
|
1764
|
+
* representable values at 1e10 are ~1024px apart, and layout saturates
|
|
1765
|
+
* sooner still (Blink's `LayoutUnit` is int32 at 1/64px, so ±33.5M px).
|
|
1766
|
+
* So the shape that looks obvious — lay an overlay out in absolute
|
|
1767
|
+
* genome pixels, write `translateX(-view.offsetPx)` — does not lose a
|
|
1768
|
+
* subpixel, it puts the row somewhere else entirely, and only on large
|
|
1769
|
+
* assemblies at high zoom, which is not where anyone tests. Both
|
|
1770
|
+
* operands here are large and their difference is bounded by the
|
|
1771
|
+
* overhang (a block or two), so what reaches CSS is small and exact.
|
|
1772
|
+
* Same rule as the GPU side, one layer up —
|
|
1773
|
+
* `agent-docs/reference/BP_PRECISION.md`.
|
|
1774
|
+
*
|
|
1775
|
+
* Published because it is the one piece of coordinate arithmetic a host
|
|
1776
|
+
* drawing its own chrome would otherwise have to know, and because
|
|
1777
|
+
* having it written out at each call site is how the two in-tree copies
|
|
1778
|
+
* came to disagree about rounding. Round it where the content is text
|
|
1779
|
+
* (a fractional offset blurs a label); leave it alone for paths and
|
|
1780
|
+
* boxes.
|
|
1781
|
+
*/
|
|
1782
|
+
get staticBlocksTranslateX() {
|
|
1783
|
+
return this.staticBlocks.offsetPx - self.offsetPx;
|
|
1784
|
+
},
|
|
1785
|
+
/**
|
|
1786
|
+
* #getter
|
|
1787
|
+
* Gridline tick positions (x relative to the staticBlocks frame),
|
|
1788
|
+
* derived from staticBlocks + bpPerPx. Computed once and shared by every
|
|
1789
|
+
* Gridlines instance (scalebar, main view, each pinned track) rather
|
|
1790
|
+
* than recomputing the makeTicks loop per component.
|
|
1791
|
+
*/
|
|
1792
|
+
get gridlineTicks() {
|
|
1793
|
+
const { bpPerPx } = self;
|
|
1794
|
+
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1795
|
+
const ticks = [];
|
|
1796
|
+
// iterate merged region runs (like scalebarLabels) so a tick on an
|
|
1797
|
+
// internal ~800px chunk boundary isn't emitted twice (once as a
|
|
1798
|
+
// chunk's right edge, once as the next chunk's left edge)
|
|
1799
|
+
for (const run of groupContiguousBlocks(blocks)) {
|
|
1800
|
+
const runLeft = run.offsetPx - firstBlockOffset;
|
|
1801
|
+
for (const { type, x } of makeBlockTicks(run, bpPerPx)) {
|
|
1802
|
+
if (x >= 0 && x <= run.widthPx) {
|
|
1803
|
+
ticks.push({ x: runLeft + x, major: type === 'major' });
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return ticks;
|
|
1808
|
+
},
|
|
1809
|
+
/**
|
|
1810
|
+
* #getter
|
|
1811
|
+
* Scalebar coordinate labels (x in the staticBlocks frame + display
|
|
1812
|
+
* text). Sibling of gridlineTicks sharing the same makeBlockTicks
|
|
1813
|
+
* formula, so labels line up exactly with their gridlines. staticBlocks
|
|
1814
|
+
* chop a region into ~800px chunks; groupContiguousBlocks merges them
|
|
1815
|
+
* back per region so a label on an internal chunk boundary isn't clipped
|
|
1816
|
+
* away by both neighbors — only genuine region edges clip a label.
|
|
1817
|
+
*
|
|
1818
|
+
* A run that can hold too few labels to make a ruler goes unnumbered
|
|
1819
|
+
* (`tickLabelsWorthDrawing`), the same rule the overview scalebar and
|
|
1820
|
+
* the dotplot axes apply: with a whole genome displayed each chromosome
|
|
1821
|
+
* catches one lone coordinate, which conveys no scale by itself and
|
|
1822
|
+
* reads as the same number repeated across the row.
|
|
1823
|
+
*/
|
|
1824
|
+
get scalebarLabels() {
|
|
1825
|
+
const { bpPerPx } = self;
|
|
1826
|
+
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1827
|
+
const labels = [];
|
|
1828
|
+
const runs = groupContiguousBlocks(blocks);
|
|
1829
|
+
const refNameLabelPx = runRefNameLabelPx(runs, self.displayedRegionsOrientation);
|
|
1830
|
+
for (const [i, run] of runs.entries()) {
|
|
1831
|
+
const runLeft = run.offsetPx - firstBlockOffset;
|
|
1832
|
+
const runLabels = [];
|
|
1833
|
+
for (const { base, x } of makeBlockTicks(run, bpPerPx, false)) {
|
|
1834
|
+
const label = getTickDisplayStr(base + 1, bpPerPx);
|
|
1835
|
+
const w = tickLabelWidth(label);
|
|
1836
|
+
// the bold refName label pinned at the run's left edge takes
|
|
1837
|
+
// precedence, as it does on the overview scalebar
|
|
1838
|
+
if (x - w / 2 >= refNameLabelPx[i] &&
|
|
1839
|
+
labelFitsInBlock(x - w / 2, w, run.widthPx)) {
|
|
1840
|
+
runLabels.push({
|
|
1841
|
+
x: runLeft + x,
|
|
1842
|
+
label,
|
|
1843
|
+
key: `${run.offsetPx}-${base}`,
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (tickLabelsWorthDrawing(runLabels.length)) {
|
|
1848
|
+
labels.push(...runLabels);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return labels;
|
|
675
1852
|
},
|
|
1853
|
+
/**
|
|
1854
|
+
* #getter
|
|
1855
|
+
* The bold refName labels drawn along the scalebar, as plain data:
|
|
1856
|
+
* `{key, refName, displayedRegionIndex, lastDisplayedRegionIndex,
|
|
1857
|
+
* sticky, transform, maxWidth, paddingLeft, text}` each. One per run of
|
|
1858
|
+
* same-refName regions, plus a "sticky" one pinned to the viewport's
|
|
1859
|
+
* left edge naming the refName under it, so panning into a chromosome
|
|
1860
|
+
* does not scroll its own name off the screen.
|
|
1861
|
+
*
|
|
1862
|
+
* Three rules live in here that a host drawing region names will
|
|
1863
|
+
* otherwise rediscover the hard way, and two of them are invisible until
|
|
1864
|
+
* the data is awkward:
|
|
1865
|
+
*
|
|
1866
|
+
* - the sticky label rides the rightmost run that has scrolled off the
|
|
1867
|
+
* left, not the region's first block — that one is gone from
|
|
1868
|
+
* staticBlocks entirely once you zoom into a region's interior, taking
|
|
1869
|
+
* the chromosome name off screen at exactly the zoom where nothing
|
|
1870
|
+
* else names it.
|
|
1871
|
+
* - adjacent regions of the same refName (collapsed introns) get one
|
|
1872
|
+
* label between them, not one each. That label names the whole run and
|
|
1873
|
+
* is fitted to the whole run, and `displayedRegionIndex` /
|
|
1874
|
+
* `lastDisplayedRegionIndex` bracket what it stands for.
|
|
1875
|
+
* - a name is drawn whole or not at all, measured against the space its
|
|
1876
|
+
* run leaves. Clipped to its own width, `chr16` reads as `chr1` —
|
|
1877
|
+
* a different chromosome rather than a shortened name, which is why
|
|
1878
|
+
* this is a fit test rather than an ellipsis.
|
|
1879
|
+
*
|
|
1880
|
+
* **Viewport frame, unlike its siblings.** `transform` is a screen x,
|
|
1881
|
+
* already net of `offsetPx`, where gridlineTicks / scalebarLabels /
|
|
1882
|
+
* paddingSpans are all in the staticBlocks frame. The sticky label's
|
|
1883
|
+
* position is a function of `offsetPx` rather than of block geometry, so
|
|
1884
|
+
* it has no fixed position in that frame — and for the same reason this
|
|
1885
|
+
* getter recomputes on every scroll frame where those three do not.
|
|
1886
|
+
*
|
|
1887
|
+
* `caption` accompanies the labels: the chip at the row's left edge,
|
|
1888
|
+
* saying which assembly the row is (a container view — synteny — opts
|
|
1889
|
+
* into that through `scalebarDisplayPrefix`) and whether it is flipped.
|
|
1890
|
+
* A ` [rev]` there is a fact about the ROW; the same marker on a
|
|
1891
|
+
* chromosome name means that one region, which only happens under mixed
|
|
1892
|
+
* orientation. `captionSpanPx` is the width it takes: the sticky label
|
|
1893
|
+
* pins to its right edge rather than the viewport's, and the coordinate
|
|
1894
|
+
* numbers stay out from under it. The SVG export deliberately
|
|
1895
|
+
* calls `getScalebarRefNameLabels` itself with no prefix rather than
|
|
1896
|
+
* reading this, since it draws its own assembly name above the ruler.
|
|
1897
|
+
*/
|
|
1898
|
+
get scalebarRefNameLabels() {
|
|
1899
|
+
return getScalebarRefNameLabels({
|
|
1900
|
+
blocks: this.staticBlocks.blocks,
|
|
1901
|
+
offsetPx: self.offsetPx,
|
|
1902
|
+
prefix: self.scalebarDisplayPrefix,
|
|
1903
|
+
orientation: self.displayedRegionsOrientation,
|
|
1904
|
+
});
|
|
1905
|
+
},
|
|
1906
|
+
/**
|
|
1907
|
+
* #getter
|
|
1908
|
+
* Every span along the row that is not track data, as plain geometry in
|
|
1909
|
+
* the staticBlocks frame — the same frame as gridlineTicks and
|
|
1910
|
+
* scalebarLabels, so one `translateX(staticBlocksTranslateX)` places all
|
|
1911
|
+
* three. A host drawing its own chrome needs all three kinds:
|
|
1912
|
+
*
|
|
1913
|
+
* - `seam`: the 3px bar at a region's right edge. Displayed regions are
|
|
1914
|
+
* laid out **contiguously** (calculateStaticBlocks emits boundary
|
|
1915
|
+
* padding only before the first region and after the last), so this
|
|
1916
|
+
* bar is the only thing separating two of them. Without it a
|
|
1917
|
+
* two-region view looks like a single region.
|
|
1918
|
+
* `isRightEndOfDisplayedRegion` marks it; the right edge of the
|
|
1919
|
+
* currently *loaded* blocks, which block geometry gives you, is a
|
|
1920
|
+
* different set.
|
|
1921
|
+
* - `elided`: a region too narrow to draw at this zoom. Whole-genome on
|
|
1922
|
+
* a real assembly is mostly these (hg38 has 455 sequences, and all
|
|
1923
|
+
* but the 24 chromosomes are under a pixel wide), so a host that
|
|
1924
|
+
* skips them leaves that part of the row empty.
|
|
1925
|
+
* - `boundary`: past the start of the first region or the end of the
|
|
1926
|
+
* last. Greying it shows the last region's seam as the edge of a
|
|
1927
|
+
* filled area, where it would otherwise look like a line in the
|
|
1928
|
+
* middle of the track.
|
|
1929
|
+
*
|
|
1930
|
+
* Elided blocks get no seam even though they carry the flag: at the
|
|
1931
|
+
* zoom where regions elide, one bar per region would fill the row solid
|
|
1932
|
+
* grey.
|
|
1933
|
+
*
|
|
1934
|
+
* PaddingBlocks is the in-tree consumer. The SVG export does not use
|
|
1935
|
+
* it: `SVGRegionSeparators` walks dynamicBlocks itself and draws only
|
|
1936
|
+
* the seam, because `elided` and `boundary` mark state for an
|
|
1937
|
+
* interactive row. In a static figure the striped grey would cover most
|
|
1938
|
+
* of the row at whole-genome zoom. The export keeps the seam because
|
|
1939
|
+
* regions lay out contiguously and the seam is all that separates two
|
|
1940
|
+
* of them.
|
|
1941
|
+
*/
|
|
1942
|
+
// Annotated with the shape spelled out rather than as `PaddingSpan`,
|
|
1943
|
+
// because the API doc's type column is where a host drawing its own
|
|
1944
|
+
// chrome reads the field names, and a named type prints as its name.
|
|
1945
|
+
// `spans` and NO_PADDING_SPANS are both `PaddingSpan[]`, so the two
|
|
1946
|
+
// cannot drift without failing here.
|
|
1947
|
+
get paddingSpans() {
|
|
1948
|
+
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1949
|
+
const spans = [];
|
|
1950
|
+
for (const block of blocks) {
|
|
1951
|
+
const x = block.offsetPx - firstBlockOffset;
|
|
1952
|
+
if (block.type === 'ContentBlock') {
|
|
1953
|
+
if (block.isRightEndOfDisplayedRegion) {
|
|
1954
|
+
spans.push({
|
|
1955
|
+
key: `${block.key}-sep`,
|
|
1956
|
+
x: x + block.widthPx - 1,
|
|
1957
|
+
width: 3,
|
|
1958
|
+
kind: 'seam',
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
else {
|
|
1963
|
+
spans.push({
|
|
1964
|
+
key: block.key,
|
|
1965
|
+
x,
|
|
1966
|
+
width: block.widthPx,
|
|
1967
|
+
kind: block.type === 'ElidedBlock'
|
|
1968
|
+
? 'elided'
|
|
1969
|
+
: 'boundary',
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
// One shared array when there is nothing to draw, so the computed's
|
|
1974
|
+
// value repeats and MobX's `===` stops the chain here. This is the
|
|
1975
|
+
// COMMON case, not an edge one: a view sitting inside one contig has
|
|
1976
|
+
// no region seam, no elision and no boundary, and that is where a
|
|
1977
|
+
// reader spends nearly all of a session. A fresh `[]` each frame
|
|
1978
|
+
// instead re-rendered a PaddingBlocks per track plus one for the
|
|
1979
|
+
// container, every frame of every gesture, to draw nothing.
|
|
1980
|
+
return spans.length > 0 ? spans : NO_PADDING_SPANS;
|
|
1981
|
+
},
|
|
1982
|
+
/**
|
|
1983
|
+
* #getter
|
|
1984
|
+
* Integer-rounded sum of all visible block widths. Slightly less than
|
|
1985
|
+
* view.width when the genome ends before the right edge; use view.width
|
|
1986
|
+
* for SVG clip rects (display boundary) and this for paint canvas sizing
|
|
1987
|
+
* (actual content width).
|
|
1988
|
+
*/
|
|
1989
|
+
get totalWidthPx() {
|
|
1990
|
+
return Math.round(this.dynamicBlocks.totalWidthPx);
|
|
1991
|
+
},
|
|
1992
|
+
/**
|
|
1993
|
+
* #getter
|
|
1994
|
+
* Like totalWidthPx but excluding inter-region boundary blocks. Used
|
|
1995
|
+
* when column layout divides the canvas width by feature count.
|
|
1996
|
+
*/
|
|
1997
|
+
get totalWidthPxWithoutBorders() {
|
|
1998
|
+
return Math.round(this.dynamicBlocks.totalWidthPxWithoutBorders);
|
|
1999
|
+
},
|
|
2000
|
+
/**
|
|
2001
|
+
* #getter
|
|
2002
|
+
*/
|
|
2003
|
+
get visibleBp() {
|
|
2004
|
+
return this.dynamicBlocks.totalBp;
|
|
2005
|
+
},
|
|
2006
|
+
/**
|
|
2007
|
+
* #getter
|
|
2008
|
+
* Whether any part of a displayed region actually falls inside the
|
|
2009
|
+
* viewport. False both when the view holds no regions at all and when
|
|
2010
|
+
* it holds some but is scrolled entirely off them; either way there is
|
|
2011
|
+
* no visible span for the scalebar, ruler and refName labels to
|
|
2012
|
+
* describe. The SVG export checks it before drawing its header.
|
|
2013
|
+
* Distinct from `hasDisplayedRegions`, which only asks whether the
|
|
2014
|
+
* view has been given regions, not whether any are on screen.
|
|
2015
|
+
*/
|
|
2016
|
+
get hasVisibleContent() {
|
|
2017
|
+
return this.dynamicBlocks.contentBlocks.length > 0;
|
|
2018
|
+
},
|
|
2019
|
+
/**
|
|
2020
|
+
* #getter
|
|
2021
|
+
* What is on screen, as regions that address whole bases — what a
|
|
2022
|
+
* reader means by "the visible region" when they ask to fetch it,
|
|
2023
|
+
* bookmark it or read it back in a locString field. `visibleRegions`
|
|
2024
|
+
* below is the other half: pixels, and fractional by design.
|
|
2025
|
+
*/
|
|
2026
|
+
get visibleWholeBaseRegions() {
|
|
2027
|
+
return wholeBaseRegions(this.dynamicBlocks.contentBlocks);
|
|
2028
|
+
},
|
|
2029
|
+
/**
|
|
2030
|
+
* #getter
|
|
2031
|
+
* Returns the currently visible content blocks with screen pixel
|
|
2032
|
+
* positions and displayedRegionIndex guaranteed.
|
|
2033
|
+
* Used by WebGL displays for per-region data fetching and rendering.
|
|
2034
|
+
*/
|
|
2035
|
+
get visibleRegions() {
|
|
2036
|
+
return this.dynamicBlocks.contentBlocks.map(block => {
|
|
2037
|
+
const screenStartPx = block.offsetPx - self.offsetPx;
|
|
2038
|
+
return {
|
|
2039
|
+
refName: block.refName,
|
|
2040
|
+
start: block.start,
|
|
2041
|
+
end: block.end,
|
|
2042
|
+
assemblyName: block.assemblyName,
|
|
2043
|
+
reversed: block.reversed,
|
|
2044
|
+
displayedRegionIndex: block.displayedRegionIndex,
|
|
2045
|
+
screenStartPx,
|
|
2046
|
+
screenEndPx: screenStartPx + block.widthPx,
|
|
2047
|
+
};
|
|
2048
|
+
});
|
|
2049
|
+
},
|
|
2050
|
+
/**
|
|
2051
|
+
* #getter
|
|
2052
|
+
* Right edge (px, viewport-relative) of the on-screen content, clamped
|
|
2053
|
+
* to the track — where a right-pinned overlay such as a wiggle's colour
|
|
2054
|
+
* or score legend belongs, rather than out in the empty gutter the
|
|
2055
|
+
* regions can leave at whole-genome zoom. `@jbrowse/display-kit`'s
|
|
2056
|
+
* `contentRightEdgePx` states the rule; an SVG export applies the same
|
|
2057
|
+
* one against the export's canvas width instead of this track's.
|
|
2058
|
+
*
|
|
2059
|
+
* Published as a SCALAR on purpose. `visibleRegions` rebuilds a fresh
|
|
2060
|
+
* array of fresh objects on every pan and zoom frame, so a component
|
|
2061
|
+
* that derived this number itself re-rendered on every frame of every
|
|
2062
|
+
* gesture — to produce, whenever content fills the track, the unchanged
|
|
2063
|
+
* `trackWidthPx`. Reading the scalar lets MobX's `===` stop the chain at
|
|
2064
|
+
* this computed instead. Measured over 20 zoom frames: the wiggle
|
|
2065
|
+
* family's three body instances fell from 66 renders to 7.
|
|
2066
|
+
*/
|
|
2067
|
+
get contentRightEdgePx() {
|
|
2068
|
+
// The `trackWidthPx` rule sends a DISPLAY to `model.canvasWidthPx`
|
|
2069
|
+
// so it cannot pick the wrong one of four view getters. This is the
|
|
2070
|
+
// view, publishing its own geometry, and `canvasWidthPx` is defined
|
|
2071
|
+
// as exactly this host's `trackWidthPx` (MultiRegionDisplayMixin) —
|
|
2072
|
+
// the same source of truth, read from the side that owns it.
|
|
2073
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
2074
|
+
return contentRightEdgePx(this.visibleRegions, self.trackWidthPx);
|
|
2075
|
+
},
|
|
2076
|
+
/**
|
|
2077
|
+
* #getter
|
|
2078
|
+
* visibleRegions expanded by a half-screen buffer on each side,
|
|
2079
|
+
* clamped to displayedRegion bounds, with integer-rounded coordinates.
|
|
2080
|
+
* Use this when fetching data that should extend slightly beyond the
|
|
2081
|
+
* viewport for smooth scrolling.
|
|
2082
|
+
*/
|
|
2083
|
+
get bufferedVisibleRegions() {
|
|
2084
|
+
const bufferBp = Math.ceil(self.width * self.bpPerPx * 0.5);
|
|
2085
|
+
return this.visibleRegions.map(vr => {
|
|
2086
|
+
const dr = self.displayedRegions[vr.displayedRegionIndex];
|
|
2087
|
+
return {
|
|
2088
|
+
region: {
|
|
2089
|
+
refName: vr.refName,
|
|
2090
|
+
start: Math.max(dr.start, Math.floor(vr.start) - bufferBp),
|
|
2091
|
+
end: Math.min(dr.end, Math.ceil(vr.end) + bufferBp),
|
|
2092
|
+
assemblyName: vr.assemblyName,
|
|
2093
|
+
// orientation rides along with the fetch region, not just with
|
|
2094
|
+
// the render blocks: canvas records it on the rpcDataMap entry
|
|
2095
|
+
// (`reversedRegions`) because label overhang packs toward lower
|
|
2096
|
+
// bp in a flipped region. A flip mutates `displayedRegions`, so
|
|
2097
|
+
// DisplayedRegionsChange refetches and the recorded flag can't
|
|
2098
|
+
// go stale.
|
|
2099
|
+
reversed: vr.reversed,
|
|
2100
|
+
},
|
|
2101
|
+
displayedRegionIndex: vr.displayedRegionIndex,
|
|
2102
|
+
};
|
|
2103
|
+
});
|
|
2104
|
+
},
|
|
2105
|
+
/**
|
|
2106
|
+
* #getter
|
|
2107
|
+
* a single "combo-locstring" representing all the regions visible on
|
|
2108
|
+
* the screen
|
|
2109
|
+
*/
|
|
676
2110
|
get visibleLocStrings() {
|
|
677
2111
|
return calculateVisibleLocStrings(this.dynamicBlocks.contentBlocks);
|
|
678
2112
|
},
|
|
2113
|
+
/**
|
|
2114
|
+
* #getter
|
|
2115
|
+
* **The blocks a debounced consumer clips to**: the coarse blocks once
|
|
2116
|
+
* the view has settled at least once, the live ones before that.
|
|
2117
|
+
*
|
|
2118
|
+
* The coarse blocks stop a per-bp scan from recomputing on every
|
|
2119
|
+
* animation frame during a pan or zoom. Wiggle's autoscale domain, the
|
|
2120
|
+
* alignments coverage scale and MAF's coverage band use them. While the
|
|
2121
|
+
* coarse blocks are stale the result is a frame or two old, as the
|
|
2122
|
+
* debounce intends. **Empty coarse blocks are a different case.** A scan
|
|
2123
|
+
* over no blocks yields no entries, and the consumer falls back to the
|
|
2124
|
+
* domain `[0,1]`, which draws a line plot blank and a density plot
|
|
2125
|
+
* saturated. The coarse blocks are empty for the 500ms between a view
|
|
2126
|
+
* initializing and the coarse autorun's first run, and data can load
|
|
2127
|
+
* within that window.
|
|
2128
|
+
*
|
|
2129
|
+
* Falling back to the live blocks costs one extra recompute at that
|
|
2130
|
+
* transition, compared with N per frame without the debounce.
|
|
2131
|
+
*/
|
|
2132
|
+
get settledDynamicBlocks() {
|
|
2133
|
+
return self.coarseDynamicBlocks.length
|
|
2134
|
+
? self.coarseDynamicBlocks
|
|
2135
|
+
: this.dynamicBlocks.contentBlocks;
|
|
2136
|
+
},
|
|
2137
|
+
/**
|
|
2138
|
+
* #getter
|
|
2139
|
+
* same as visibleLocStrings, but only updated every 500ms
|
|
2140
|
+
*/
|
|
679
2141
|
get coarseVisibleLocStrings() {
|
|
680
2142
|
return calculateVisibleLocStrings(self.coarseDynamicBlocks);
|
|
681
2143
|
},
|
|
2144
|
+
/**
|
|
2145
|
+
* #getter
|
|
2146
|
+
*/
|
|
682
2147
|
get coarseTotalBpDisplayStr() {
|
|
683
2148
|
return getBpDisplayStr(self.coarseTotalBp);
|
|
684
2149
|
},
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
2150
|
+
/**
|
|
2151
|
+
* #getter
|
|
2152
|
+
*/
|
|
688
2153
|
get effectiveTotalBp() {
|
|
689
|
-
return
|
|
2154
|
+
return self.windowWidthBp;
|
|
690
2155
|
},
|
|
2156
|
+
/**
|
|
2157
|
+
* #getter
|
|
2158
|
+
*/
|
|
691
2159
|
get effectiveTotalBpDisplayStr() {
|
|
692
2160
|
return getBpDisplayStr(this.effectiveTotalBp);
|
|
693
2161
|
},
|
|
694
2162
|
};
|
|
695
2163
|
})
|
|
696
2164
|
.actions(self => ({
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
2165
|
+
/**
|
|
2166
|
+
* #action
|
|
2167
|
+
*/
|
|
2168
|
+
setCoarseDynamicBlocks(blocks, bpPerPx) {
|
|
2169
|
+
// Consumers diff the coarse blocks by reference (the alignments and
|
|
2170
|
+
// wiggle fetch autoruns), so assigning an equivalent array would make
|
|
2171
|
+
// them refetch. `settleCoarseBlocks` flushes these on a discrete jump
|
|
2172
|
+
// and the debounced autorun then fires once more with the same blocks —
|
|
2173
|
+
// that follow-up has to cost nothing. A block's key encodes its
|
|
2174
|
+
// assembly/refName/start/end, so keys are enough to compare.
|
|
2175
|
+
const next = blocks.contentBlocks;
|
|
2176
|
+
const same = bpPerPx === self.coarseBpPerPx &&
|
|
2177
|
+
next.length === self.coarseDynamicBlocks.length &&
|
|
2178
|
+
next.every((b, i) => b.key === self.coarseDynamicBlocks[i].key);
|
|
2179
|
+
if (!same) {
|
|
2180
|
+
self.coarseDynamicBlocks = next;
|
|
2181
|
+
self.coarseTotalBp = blocks.totalBp;
|
|
2182
|
+
self.coarseBpPerPx = bpPerPx;
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
}))
|
|
2186
|
+
.actions(self => ({
|
|
2187
|
+
/**
|
|
2188
|
+
* #action
|
|
2189
|
+
* Bring the coarse blocks to the viewport as it stands now, for a
|
|
2190
|
+
* placement that JUMPED rather than travelled.
|
|
2191
|
+
*
|
|
2192
|
+
* The coarse blocks are a 500ms throttle, and every consumer trades
|
|
2193
|
+
* freshness for not recomputing per animation frame — the location box,
|
|
2194
|
+
* canvas's on-screen feature set, and the two per-bp scans behind
|
|
2195
|
+
* `settledDynamicBlocks`. That trade is only sound while the answer is a
|
|
2196
|
+
* few frames old. **A jump makes it unrelated instead**, because there is
|
|
2197
|
+
* nothing to coalesce: the viewport left behind is not an approximation of
|
|
2198
|
+
* the new one, it is a different place. Unsettled, the location box reads
|
|
2199
|
+
* as a navigation that didn't happen, and wiggle's autoscale domain is
|
|
2200
|
+
* computed over the window the user just left — measured at `[0,200]`
|
|
2201
|
+
* against a correct `[0,300]` when a 40bp coarse window survived a jump to
|
|
2202
|
+
* 4040bp on screen. Only the per-region fetch moving to the leading edge
|
|
2203
|
+
* made that reachable: at 600ms the data never landed inside the hole.
|
|
2204
|
+
*
|
|
2205
|
+
* **The continuous paths deliberately do not come through here.** The
|
|
2206
|
+
* spring zoom writes through `zoomTo` per frame and a drag through
|
|
2207
|
+
* `scrollTo`, so this adds no per-frame work; the settle is idempotent
|
|
2208
|
+
* (`setCoarseDynamicBlocks` compares block keys), so the debounced
|
|
2209
|
+
* autorun's own follow-up run costs nothing.
|
|
2210
|
+
*
|
|
2211
|
+
* Every placer calls it, and `placersSettleCoarseBlocks.test.ts` is what
|
|
2212
|
+
* makes that true of the next one — it scans this file for the writes and
|
|
2213
|
+
* fails on a placer that reaches neither the settle nor its named list of
|
|
2214
|
+
* continuous paths. Two passes by hand missed four.
|
|
2215
|
+
*/
|
|
2216
|
+
settleCoarseBlocks() {
|
|
2217
|
+
if (self.initialized) {
|
|
2218
|
+
self.setCoarseDynamicBlocks(self.dynamicBlocks, self.bpPerPx);
|
|
2219
|
+
}
|
|
700
2220
|
},
|
|
701
2221
|
}))
|
|
702
2222
|
.actions(self => ({
|
|
2223
|
+
/**
|
|
2224
|
+
* #action
|
|
2225
|
+
* offset is the base-pair-offset in the displayed region, index is the
|
|
2226
|
+
* index of the displayed region in the linear genome view
|
|
2227
|
+
*
|
|
2228
|
+
* @param start - object as `{start, end, offset, index}`
|
|
2229
|
+
* @param end - object as `{start, end, offset, index}`
|
|
2230
|
+
*/
|
|
703
2231
|
moveTo(start, end) {
|
|
704
2232
|
moveTo(self, start, end);
|
|
2233
|
+
self.settleCoarseBlocks();
|
|
2234
|
+
},
|
|
2235
|
+
/**
|
|
2236
|
+
* #action
|
|
2237
|
+
* Place the viewport by the pair of PIXEL quantities it used to be stored
|
|
2238
|
+
* as. Prefer `setWindow`: pixels mean nothing without the width they were
|
|
2239
|
+
* measured at, so a round trip through here is only exact while the width
|
|
2240
|
+
* holds still. Kept for callers that genuinely have pixels — a wheel
|
|
2241
|
+
* gesture, a rubberband.
|
|
2242
|
+
*/
|
|
2243
|
+
setNewView(bpPerPx, offsetPx) {
|
|
2244
|
+
self.zoomTo(bpPerPx);
|
|
2245
|
+
self.scrollTo(offsetPx);
|
|
2246
|
+
self.settleCoarseBlocks();
|
|
2247
|
+
},
|
|
2248
|
+
/**
|
|
2249
|
+
* #action
|
|
2250
|
+
* Place the viewport by the pair it is actually STORED as: the window's
|
|
2251
|
+
* width and left edge, both in bp. `setNewView`'s bp-space twin, and the
|
|
2252
|
+
* right one for anything that captures a viewport and puts it back later
|
|
2253
|
+
* (an Undo, a saved location) — those two moments can be a window resize
|
|
2254
|
+
* apart, and this pair survives one where a pixel pair does not.
|
|
2255
|
+
*
|
|
2256
|
+
* Still clamped, by the same zoom and scroll limits as every other mover:
|
|
2257
|
+
* the regions may have changed under it, and a window the new set can't
|
|
2258
|
+
* hold is not restorable however it was spelled.
|
|
2259
|
+
*/
|
|
2260
|
+
setWindow(windowWidthBp, windowStartBp) {
|
|
2261
|
+
this.setWindowFrame(windowWidthBp, windowStartBp);
|
|
2262
|
+
self.settleCoarseBlocks();
|
|
2263
|
+
},
|
|
2264
|
+
/**
|
|
2265
|
+
* #action
|
|
2266
|
+
* `setWindow` without the settle, for a writer that runs per animation
|
|
2267
|
+
* frame. Flushing the coarse blocks sixty times a second is the one thing
|
|
2268
|
+
* such a writer must not do: they are a 500ms throttle that a synteny
|
|
2269
|
+
* follow pass and two autoscale domains hang off, and settling each frame
|
|
2270
|
+
* turns each of those into per-frame work — `positionViewOnSpans` states
|
|
2271
|
+
* the same rule for the same reason.
|
|
2272
|
+
*/
|
|
2273
|
+
setWindowFrame(windowWidthBp, windowStartBp) {
|
|
2274
|
+
// via zoomTo (not a bare assignment) for its clamp; `width` cancels, so
|
|
2275
|
+
// the bp width in is the bp width out at whatever width is current
|
|
2276
|
+
self.zoomTo(windowWidthBp / self.width);
|
|
2277
|
+
self.scrollToBp(windowStartBp);
|
|
2278
|
+
},
|
|
2279
|
+
/**
|
|
2280
|
+
* #action
|
|
2281
|
+
* The worst jump of them to leave unsettled: the coarse blocks are keyed
|
|
2282
|
+
* by `displayedRegionIndex`, so a set that outlives the region list has a
|
|
2283
|
+
* consumer reading one contig's data against another's blocks.
|
|
2284
|
+
*/
|
|
2285
|
+
setDisplayedRegions(regions) {
|
|
2286
|
+
self.displayedRegions = cast(regions);
|
|
2287
|
+
// new regions move both bounds: zoomTo re-clamps bpPerPx into the new
|
|
2288
|
+
// [minBpPerPx, maxBpPerPx], scrollTo re-clamps offsetPx into the new
|
|
2289
|
+
// [minOffset, maxOffset]. scrollTo is called explicitly rather than
|
|
2290
|
+
// left to zoomTo's internal scrollTo, which is skipped when bpPerPx
|
|
2291
|
+
// is already in range — shrinking the region set would otherwise
|
|
2292
|
+
// strand the view scrolled past the end, on blank space.
|
|
2293
|
+
self.zoomTo(self.bpPerPx);
|
|
2294
|
+
self.scrollTo(self.offsetPx);
|
|
2295
|
+
self.settleCoarseBlocks();
|
|
2296
|
+
},
|
|
2297
|
+
/**
|
|
2298
|
+
* #action
|
|
2299
|
+
* Re-assert `maxBpPerPx` over the current zoom, for a container whose
|
|
2300
|
+
* shared ceiling just fell — otherwise the view is stranded above it with
|
|
2301
|
+
* zoom-out disabled and the slider's min past its value. Goes through
|
|
2302
|
+
* `zoomTo` so the clamp keeps one definition, and settles because a
|
|
2303
|
+
* ceiling drop moves the window in one step.
|
|
2304
|
+
*
|
|
2305
|
+
* Requires an `initialized` view; the caller checks, since an MST action
|
|
2306
|
+
* reads untracked and a guard here would never re-run once the row
|
|
2307
|
+
* arrived.
|
|
2308
|
+
*/
|
|
2309
|
+
clampZoomToCeiling() {
|
|
2310
|
+
self.zoomTo(self.bpPerPx);
|
|
2311
|
+
self.settleCoarseBlocks();
|
|
2312
|
+
},
|
|
2313
|
+
/**
|
|
2314
|
+
* #action
|
|
2315
|
+
*/
|
|
2316
|
+
showAllRegions() {
|
|
2317
|
+
self.windowWidthBp = self.maxBpPerPx * self.width;
|
|
2318
|
+
self.scrollTo(getCenteredOffsetPx(self.displayedRegionsTotalPx, self.width));
|
|
2319
|
+
self.settleCoarseBlocks();
|
|
2320
|
+
},
|
|
2321
|
+
/**
|
|
2322
|
+
* #action
|
|
2323
|
+
* Fit the displayed regions to the width exactly, edge to edge.
|
|
2324
|
+
*
|
|
2325
|
+
* Not the same as `showAllRegions`, which goes to `maxBpPerPx` — the
|
|
2326
|
+
* zoom-out LIMIT, where `SHOW_ALL_REGIONS_FILL` deliberately keeps a 10%
|
|
2327
|
+
* margin so the whole genome doesn't sit flush against both edges. That
|
|
2328
|
+
* margin is right for "show me everything" and wrong for a caller that
|
|
2329
|
+
* named the regions it wants: it draws them at 1/0.9 of fit-to-width,
|
|
2330
|
+
* centered, which is a silent 11% scale difference from the
|
|
2331
|
+
* single-region path (`moveTo`, span/width) reached through the same
|
|
2332
|
+
* location box.
|
|
2333
|
+
*
|
|
2334
|
+
* The scale comes from `fitAllRegionsWindow`, which is where the rule is
|
|
2335
|
+
* written — a snapshot builder that cannot call an action needs the same
|
|
2336
|
+
* answer, and the two agreeing matters more than either being local. Where
|
|
2337
|
+
* it clamps up to `minBpPerPx` the content is narrower than the view, and
|
|
2338
|
+
* the centering is what frames it.
|
|
2339
|
+
*/
|
|
2340
|
+
fitAllRegions() {
|
|
2341
|
+
// The window IS the displayed regions, which is what fitting them means.
|
|
2342
|
+
// Nothing is fitted into a width of zero, where the pixel form was
|
|
2343
|
+
// equally meaningless.
|
|
2344
|
+
if (self.width) {
|
|
2345
|
+
self.windowWidthBp = fitAllRegionsWindow(self.totalBp, self.width, self.minBpPerPx).windowWidthBp;
|
|
2346
|
+
}
|
|
2347
|
+
// Deliberately not the helper's `windowStartBp`: this is the same
|
|
2348
|
+
// centering, but going through scrollTo gets the scroll clamp with it,
|
|
2349
|
+
// and getCenteredOffsetPx is already the one definition of it in pixels.
|
|
2350
|
+
self.scrollTo(getCenteredOffsetPx(self.displayedRegionsTotalPx, self.width));
|
|
2351
|
+
self.settleCoarseBlocks();
|
|
2352
|
+
},
|
|
2353
|
+
/**
|
|
2354
|
+
* #action
|
|
2355
|
+
*/
|
|
2356
|
+
horizontallyFlip() {
|
|
2357
|
+
self.displayedRegions = cast([...self.displayedRegions]
|
|
2358
|
+
.reverse()
|
|
2359
|
+
.map(region => ({ ...region, reversed: !region.reversed })));
|
|
2360
|
+
self.scrollTo(self.displayedRegionsTotalPx - self.offsetPx - self.width);
|
|
2361
|
+
self.settleCoarseBlocks();
|
|
705
2362
|
},
|
|
706
|
-
|
|
2363
|
+
}))
|
|
2364
|
+
.actions(self => ({
|
|
2365
|
+
/**
|
|
2366
|
+
* #action
|
|
2367
|
+
*/
|
|
2368
|
+
showAllRegionsInAssembly(assemblyName) {
|
|
2369
|
+
const session = getSession(self);
|
|
2370
|
+
const { assemblyManager } = session;
|
|
707
2371
|
const { assemblyNames } = self;
|
|
2372
|
+
if (!assemblyName && assemblyNames.length > 1) {
|
|
2373
|
+
session.notify(`Can't perform operation with multiple assemblies currently`);
|
|
2374
|
+
}
|
|
2375
|
+
else {
|
|
2376
|
+
const resolvedName = assemblyName ?? assemblyNames[0];
|
|
2377
|
+
const regions = resolvedName
|
|
2378
|
+
? assemblyManager.get(resolvedName)?.regions
|
|
2379
|
+
: undefined;
|
|
2380
|
+
if (regions) {
|
|
2381
|
+
self.setDisplayedRegions(regions);
|
|
2382
|
+
self.showAllRegions();
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
/**
|
|
2387
|
+
* #action
|
|
2388
|
+
* this "clears the view" and makes the view return to the import form
|
|
2389
|
+
*/
|
|
2390
|
+
clearView() {
|
|
2391
|
+
self.setDisplayedRegions([]);
|
|
2392
|
+
self.tracks.clear();
|
|
2393
|
+
},
|
|
2394
|
+
/**
|
|
2395
|
+
* #action
|
|
2396
|
+
* Navigate to a location based on its refName and optionally start, end,
|
|
2397
|
+
* and assemblyName. Will not try to change displayed regions, use
|
|
2398
|
+
* `navToLocations` instead. Only navigates to a location if it is
|
|
2399
|
+
* entirely within a displayedRegion. Navigates to the first matching
|
|
2400
|
+
* location encountered.
|
|
2401
|
+
*
|
|
2402
|
+
* Throws an error if navigation was unsuccessful
|
|
2403
|
+
*
|
|
2404
|
+
* @param query - a proposed location to navigate to
|
|
2405
|
+
* @param grow - optional multiplier to expand the region by (e.g., 0.2
|
|
2406
|
+
* adds 20% padding on each side)
|
|
2407
|
+
*/
|
|
2408
|
+
navTo(query, grow) {
|
|
2409
|
+
this.navToMultiple([query], grow);
|
|
2410
|
+
},
|
|
2411
|
+
/**
|
|
2412
|
+
* #action
|
|
2413
|
+
* Navigate to a location based on its refName and optionally start, end,
|
|
2414
|
+
* and assemblyName. Will not try to change displayed regions, use
|
|
2415
|
+
* navToLocations instead. Only navigates to a location if it is entirely
|
|
2416
|
+
* within a displayedRegion. Navigates to the first matching location
|
|
2417
|
+
* encountered.
|
|
2418
|
+
*
|
|
2419
|
+
* Throws an error if navigation was unsuccessful
|
|
2420
|
+
*
|
|
2421
|
+
* @param locations - proposed location to navigate to
|
|
2422
|
+
* @param grow - optional multiplier to expand the region by (e.g., 0.2
|
|
2423
|
+
* adds 20% padding on each side)
|
|
2424
|
+
*/
|
|
2425
|
+
navToMultiple(locations, grow) {
|
|
2426
|
+
if (locations.some(l => l.start !== undefined && l.end !== undefined && l.start > l.end)) {
|
|
2427
|
+
throw new Error('found start greater than end');
|
|
2428
|
+
}
|
|
2429
|
+
const firstLocation = locations.at(0);
|
|
2430
|
+
const lastLocation = locations.at(-1);
|
|
2431
|
+
if (!firstLocation || !lastLocation) {
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
const defaultAssemblyName = self.assemblyNames[0];
|
|
708
2435
|
const { assemblyManager } = getSession(self);
|
|
2436
|
+
const { displayedRegions } = self;
|
|
2437
|
+
// The range spans from the first location's left edge to the last
|
|
2438
|
+
// location's right edge (any locations in between are ignored).
|
|
2439
|
+
const common = {
|
|
2440
|
+
assemblyManager,
|
|
2441
|
+
defaultAssemblyName,
|
|
2442
|
+
displayedRegions,
|
|
2443
|
+
grow,
|
|
2444
|
+
};
|
|
2445
|
+
self.moveTo(resolveNavEndpoint({
|
|
2446
|
+
...common,
|
|
2447
|
+
location: firstLocation,
|
|
2448
|
+
side: 'left',
|
|
2449
|
+
}), resolveNavEndpoint({
|
|
2450
|
+
...common,
|
|
2451
|
+
location: lastLocation,
|
|
2452
|
+
side: 'right',
|
|
2453
|
+
}));
|
|
2454
|
+
},
|
|
2455
|
+
}))
|
|
2456
|
+
.actions(self => ({
|
|
2457
|
+
/**
|
|
2458
|
+
* #action
|
|
2459
|
+
* Replace the region list and place the viewport in one step. The pair is
|
|
2460
|
+
* the unit a coarse-block consumer can act on: called separately they
|
|
2461
|
+
* publish the viewport in between, and post-await in `navToLocations`
|
|
2462
|
+
* they are two transactions, so a per-bp scan runs over a window that was
|
|
2463
|
+
* never on screen.
|
|
2464
|
+
*
|
|
2465
|
+
* With no location named it fits the regions rather than going through
|
|
2466
|
+
* `showAllRegions`: the caller named the regions it wants, so it gets the
|
|
2467
|
+
* width, where showAllRegions goes to the zoom-out LIMIT and its 10%
|
|
2468
|
+
* margin is dead frame for a named subset.
|
|
2469
|
+
*/
|
|
2470
|
+
showRegions(regions, location) {
|
|
2471
|
+
self.setDisplayedRegions(regions);
|
|
2472
|
+
if (location) {
|
|
2473
|
+
self.navTo(location);
|
|
2474
|
+
}
|
|
2475
|
+
else {
|
|
2476
|
+
self.fitAllRegions();
|
|
2477
|
+
}
|
|
2478
|
+
},
|
|
2479
|
+
}))
|
|
2480
|
+
.actions(self => ({
|
|
2481
|
+
/**
|
|
2482
|
+
* #action
|
|
2483
|
+
* Navigate to the given locstring, will change displayed regions if
|
|
2484
|
+
* needed, and wait for assemblies to be initialized
|
|
2485
|
+
*
|
|
2486
|
+
* @param input - e.g. "chr1:1-100", "chr1:1-100 chr2:1-100", "chr 1 100"
|
|
2487
|
+
* @param optAssemblyName - (optional) the assembly name to use when
|
|
2488
|
+
* navigating to the locstring
|
|
2489
|
+
* @param grow - optional multiplier to expand the region by (e.g., 0.2
|
|
2490
|
+
* adds 20% padding on each side)
|
|
2491
|
+
* @param opts - `showHitTrack: false` keeps a text-search hit from also
|
|
2492
|
+
* showing the track its index was built from
|
|
2493
|
+
* @returns whether the view moved. A multi-hit search raises the picker
|
|
2494
|
+
* instead of navigating, and both resolve — see `handleSelectedRegion`.
|
|
2495
|
+
*/
|
|
2496
|
+
// annotated, not inferred: the same type cycle `showTrack` names. Left to
|
|
2497
|
+
// infer, the `boolean` reaches this model's type through `self` and 180
|
|
2498
|
+
// unrelated files lose their parameter types to it.
|
|
2499
|
+
async navToLocString(input, optAssemblyName, grow, opts) {
|
|
2500
|
+
const { assemblyNames } = self;
|
|
2501
|
+
const session = getSession(self);
|
|
2502
|
+
const { assemblyManager } = session;
|
|
709
2503
|
const assemblyName = optAssemblyName || assemblyNames[0];
|
|
710
2504
|
if (assemblyName) {
|
|
711
2505
|
await assemblyManager.waitForAssembly(assemblyName);
|
|
712
2506
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
2507
|
+
if (!isAlive(self)) {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
return handleSelectedRegion({
|
|
717
2511
|
input,
|
|
718
|
-
|
|
2512
|
+
assemblyName,
|
|
2513
|
+
grow,
|
|
2514
|
+
showHitTrack: opts?.showHitTrack,
|
|
719
2515
|
model: self,
|
|
720
2516
|
});
|
|
721
2517
|
},
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
2518
|
+
/**
|
|
2519
|
+
* #action
|
|
2520
|
+
* Similar to `navToLocString`, but accepts a list of parsed location
|
|
2521
|
+
* objects instead of a locstring. Will try to perform
|
|
2522
|
+
* `setDisplayedRegions` if changing regions
|
|
2523
|
+
*
|
|
2524
|
+
* @param regions - array of parsed location objects
|
|
2525
|
+
* @param assemblyName - optional assembly name
|
|
2526
|
+
* @param grow - optional multiplier to expand the region by (e.g., 0.2
|
|
2527
|
+
* adds 20% padding on each side)
|
|
2528
|
+
*/
|
|
725
2529
|
async navToLocations(regions, assemblyName, grow) {
|
|
2530
|
+
// an empty parse (e.g. blank/whitespace locstring) must not fall
|
|
2531
|
+
// through to the multi-region branch below, which would call
|
|
2532
|
+
// setDisplayedRegions([]) and blank the view
|
|
2533
|
+
if (regions.length === 0) {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
726
2536
|
const { assemblyManager } = getSession(self);
|
|
727
|
-
|
|
2537
|
+
// A one-sided `volatileWidth !== undefined` parks forever on a view
|
|
2538
|
+
// that never mounts, and `navToMultiLevelBreak` awaits a Promise.all of
|
|
2539
|
+
// these one line before the `whenViewSettled` that was written to cure
|
|
2540
|
+
// exactly that hang. `isAlive` is the second terminal state and the
|
|
2541
|
+
// guard below is what it means. NOT `self.error`, which is a transient
|
|
2542
|
+
// truth (`assembliesNotFound` while a session assembly is registering)
|
|
2543
|
+
// independent of mounting, so it would return without navigating where
|
|
2544
|
+
// waiting would have navigated correctly.
|
|
2545
|
+
await when(() => self.volatileWidth !== undefined || !isAlive(self));
|
|
2546
|
+
if (!isAlive(self) || self.volatileWidth === undefined) {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
// Generate locations from the parsed regions
|
|
728
2550
|
const locations = await generateLocations({
|
|
729
2551
|
regions,
|
|
730
2552
|
assemblyManager,
|
|
731
2553
|
assemblyName,
|
|
732
2554
|
grow,
|
|
733
2555
|
});
|
|
2556
|
+
// the view may have been closed/detached while the assembly loaded
|
|
2557
|
+
if (!isAlive(self)) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (locations.length === 0) {
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
// Handle single location case
|
|
734
2564
|
if (locations.length === 1) {
|
|
735
2565
|
const location = locations[0];
|
|
736
2566
|
const { reversed, parentRegion, start, end } = location;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
]);
|
|
743
|
-
this.navTo({
|
|
2567
|
+
// Clamped into the parentRegion bounds, whose lower bound is
|
|
2568
|
+
// parentRegion.start and not 0: the region being displayed IS
|
|
2569
|
+
// parentRegion, so a start below it fails navTo's containment check
|
|
2570
|
+
// wherever the parentRegion doesn't begin at 0.
|
|
2571
|
+
self.showRegions([{ ...parentRegion, reversed }], {
|
|
744
2572
|
...location,
|
|
745
|
-
start: clamp(start ??
|
|
746
|
-
end: clamp(end ?? parentRegion.end,
|
|
2573
|
+
start: clamp(start ?? parentRegion.start, parentRegion.start, parentRegion.end),
|
|
2574
|
+
end: clamp(end ?? parentRegion.end, parentRegion.start, parentRegion.end),
|
|
747
2575
|
});
|
|
748
2576
|
}
|
|
2577
|
+
// Handle multiple locations case. Each region is built from
|
|
2578
|
+
// parentRegion (not the parsed location) so it carries the canonical
|
|
2579
|
+
// refName rather than whatever alias/casing the user typed, doesn't
|
|
2580
|
+
// drag the whole nested parentRegion object into the persisted
|
|
2581
|
+
// displayedRegions, and stays clamped to the chromosome bounds that
|
|
2582
|
+
// `grow` may have pushed it past.
|
|
749
2583
|
else {
|
|
750
|
-
self.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
};
|
|
759
|
-
}));
|
|
760
|
-
self.showAllRegions();
|
|
2584
|
+
self.showRegions(locations.map(({ start, end, reversed, parentRegion }) => start === undefined || end === undefined
|
|
2585
|
+
? { ...parentRegion, reversed }
|
|
2586
|
+
: {
|
|
2587
|
+
...parentRegion,
|
|
2588
|
+
reversed,
|
|
2589
|
+
start: clamp(start, parentRegion.start, parentRegion.end),
|
|
2590
|
+
end: clamp(end, parentRegion.start, parentRegion.end),
|
|
2591
|
+
}));
|
|
761
2592
|
}
|
|
762
2593
|
},
|
|
763
|
-
|
|
764
|
-
|
|
2594
|
+
}))
|
|
2595
|
+
.actions(self => ({
|
|
2596
|
+
/**
|
|
2597
|
+
* #action
|
|
2598
|
+
* Similar to `navToLocString`, but accepts a parsed location object
|
|
2599
|
+
* instead of a locstring. Will try to perform `setDisplayedRegions` if
|
|
2600
|
+
* changing regions
|
|
2601
|
+
*
|
|
2602
|
+
* @param parsedLocString - a parsed location object with refName, start,
|
|
2603
|
+
* end, etc.
|
|
2604
|
+
* @param assemblyName - optional assembly name
|
|
2605
|
+
* @param grow - optional multiplier to expand the region by (e.g., 0.2
|
|
2606
|
+
* adds 20% padding on each side)
|
|
2607
|
+
*/
|
|
2608
|
+
async navToLocation(parsedLocString, assemblyName, grow) {
|
|
2609
|
+
return self.navToLocations([parsedLocString], assemblyName, grow);
|
|
2610
|
+
},
|
|
2611
|
+
/**
|
|
2612
|
+
* #action
|
|
2613
|
+
* adds the region in view to the session's highlights
|
|
2614
|
+
*/
|
|
2615
|
+
highlightCurrentRegion() {
|
|
2616
|
+
const [region] = self.visibleWholeBaseRegions;
|
|
2617
|
+
if (region) {
|
|
2618
|
+
getSession(self).addHighlight(region);
|
|
2619
|
+
}
|
|
765
2620
|
},
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
2621
|
+
/**
|
|
2622
|
+
* #action
|
|
2623
|
+
* navigates to the most recently added highlight on this view's
|
|
2624
|
+
* assemblies, zoomed out a little for context
|
|
2625
|
+
*/
|
|
2626
|
+
async navigateNewestHighlight() {
|
|
2627
|
+
const newest = self.highlights.at(-1);
|
|
2628
|
+
if (newest) {
|
|
2629
|
+
const { assemblyName, ...region } = newest;
|
|
2630
|
+
await self.navToLocString(assembleLocString(region), assemblyName, 0.2);
|
|
769
2631
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (!firstLocation || !lastLocation) {
|
|
773
|
-
return;
|
|
2632
|
+
else {
|
|
2633
|
+
getSession(self).notify('There are no highlights to navigate to.', 'info');
|
|
774
2634
|
}
|
|
775
|
-
const defaultAssemblyName = self.assemblyNames[0];
|
|
776
|
-
const { assemblyManager } = getSession(self);
|
|
777
|
-
const firstAssembly = assemblyManager.get(firstLocation.assemblyName || defaultAssemblyName);
|
|
778
|
-
const firstRefName = firstAssembly?.getCanonicalRefName(firstLocation.refName) ||
|
|
779
|
-
firstLocation.refName;
|
|
780
|
-
const firstRegion = self.displayedRegions.find(r => r.refName === firstRefName);
|
|
781
|
-
const lastAssembly = assemblyManager.get(lastLocation.assemblyName || defaultAssemblyName);
|
|
782
|
-
const lastRefName = lastAssembly?.getCanonicalRefName(lastLocation.refName) ||
|
|
783
|
-
lastLocation.refName;
|
|
784
|
-
const lastRegion = findLast(self.displayedRegions, r => r.refName === lastRefName);
|
|
785
|
-
if (!firstRegion) {
|
|
786
|
-
throw new Error(`could not find a region with refName "${firstRefName}"`);
|
|
787
|
-
}
|
|
788
|
-
if (!lastRegion) {
|
|
789
|
-
throw new Error(`could not find a region with refName "${lastRefName}"`);
|
|
790
|
-
}
|
|
791
|
-
let firstStart = firstLocation.start === undefined
|
|
792
|
-
? firstRegion.start
|
|
793
|
-
: firstLocation.start;
|
|
794
|
-
let firstEnd = firstLocation.end === undefined ? firstRegion.end : firstLocation.end;
|
|
795
|
-
let lastStart = lastLocation.start === undefined
|
|
796
|
-
? lastRegion.start
|
|
797
|
-
: lastLocation.start;
|
|
798
|
-
let lastEnd = lastLocation.end === undefined ? lastRegion.end : lastLocation.end;
|
|
799
|
-
if (grow) {
|
|
800
|
-
const expanded = expandRegion(firstStart, lastEnd, grow, firstRegion.start, lastRegion.end);
|
|
801
|
-
firstStart = expanded.start;
|
|
802
|
-
firstEnd = expanded.end;
|
|
803
|
-
lastStart = expanded.start;
|
|
804
|
-
lastEnd = expanded.end;
|
|
805
|
-
}
|
|
806
|
-
const firstIndex = self.displayedRegions.findIndex(r => firstRefName === r.refName &&
|
|
807
|
-
firstStart >= r.start &&
|
|
808
|
-
firstStart <= r.end &&
|
|
809
|
-
firstEnd <= r.end &&
|
|
810
|
-
firstEnd >= r.start);
|
|
811
|
-
const lastIndex = self.displayedRegions.findIndex(r => lastRefName === r.refName &&
|
|
812
|
-
lastStart >= r.start &&
|
|
813
|
-
lastStart <= r.end &&
|
|
814
|
-
lastEnd <= r.end &&
|
|
815
|
-
lastEnd >= r.start);
|
|
816
|
-
if (firstIndex === -1 || lastIndex === -1) {
|
|
817
|
-
throw new Error(`could not find a region that contained "${locations.map(l => assembleLocString(l))}"`);
|
|
818
|
-
}
|
|
819
|
-
const startDisplayedRegion = self.displayedRegions[firstIndex];
|
|
820
|
-
const endDisplayedRegion = self.displayedRegions[lastIndex];
|
|
821
|
-
const startOffset = startDisplayedRegion.reversed
|
|
822
|
-
? startDisplayedRegion.end - firstEnd
|
|
823
|
-
: firstStart - startDisplayedRegion.start;
|
|
824
|
-
const endOffset = endDisplayedRegion.reversed
|
|
825
|
-
? endDisplayedRegion.end - lastStart
|
|
826
|
-
: lastEnd - endDisplayedRegion.start;
|
|
827
|
-
this.moveTo({
|
|
828
|
-
index: firstIndex,
|
|
829
|
-
offset: startOffset,
|
|
830
|
-
}, {
|
|
831
|
-
index: lastIndex,
|
|
832
|
-
offset: endOffset,
|
|
833
|
-
});
|
|
834
2635
|
},
|
|
835
2636
|
}))
|
|
836
2637
|
.views(self => ({
|
|
2638
|
+
/**
|
|
2639
|
+
* #method
|
|
2640
|
+
*/
|
|
837
2641
|
rubberBandMenuItems() {
|
|
838
|
-
return buildRubberBandMenuItems(self);
|
|
839
|
-
},
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
2642
|
+
return buildRubberBandMenuItems(self, self.rubberBandLaunchMenuItems());
|
|
2643
|
+
},
|
|
2644
|
+
/**
|
|
2645
|
+
* #method
|
|
2646
|
+
*/
|
|
2647
|
+
bpToPx({ refName, coord, displayedRegionIndex, }) {
|
|
2648
|
+
return bpToPx({ refName, coord, displayedRegionIndex, self });
|
|
2649
|
+
},
|
|
2650
|
+
/**
|
|
2651
|
+
* #method
|
|
2652
|
+
* Map a highlight region to its pixel position+width inside
|
|
2653
|
+
* the tracks container. Falls back to the raw refName if the region's
|
|
2654
|
+
* assemblyName is missing or unknown so highlights authored without an
|
|
2655
|
+
* assembly still render in single-assembly views.
|
|
2656
|
+
*/
|
|
843
2657
|
getHighlightCoords(region) {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
2658
|
+
return getLayoutHighlightCoords(self, canonicalHighlight(self, region));
|
|
2659
|
+
},
|
|
2660
|
+
/**
|
|
2661
|
+
* #method
|
|
2662
|
+
* like getHighlightCoords but laid out against the overview scalebar and
|
|
2663
|
+
* shifted by the cytoband offset
|
|
2664
|
+
*/
|
|
2665
|
+
getOverviewHighlightCoords(region) {
|
|
2666
|
+
const coords = getLayoutHighlightCoords(self.overviewLayout, canonicalHighlight(self, region));
|
|
2667
|
+
return coords
|
|
2668
|
+
? { ...coords, left: coords.left + self.cytobandOffset }
|
|
856
2669
|
: undefined;
|
|
857
2670
|
},
|
|
858
|
-
|
|
2671
|
+
/**
|
|
2672
|
+
* #method
|
|
2673
|
+
* scrolls the view to center on the given bp. if that is not in any of
|
|
2674
|
+
* the displayed regions, does nothing
|
|
2675
|
+
*
|
|
2676
|
+
* @param coord - basepair at which you want to center the view
|
|
2677
|
+
* @param refName - refName of the displayedRegion you are centering at
|
|
2678
|
+
* @param displayedRegionIndex - index of the displayedRegion
|
|
2679
|
+
*/
|
|
2680
|
+
centerAt(coord, refName, displayedRegionIndex) {
|
|
859
2681
|
const centerPx = this.bpToPx({
|
|
860
2682
|
refName,
|
|
861
2683
|
coord,
|
|
862
|
-
|
|
2684
|
+
displayedRegionIndex,
|
|
863
2685
|
});
|
|
864
2686
|
if (centerPx !== undefined) {
|
|
865
2687
|
self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2));
|
|
2688
|
+
self.settleCoarseBlocks();
|
|
866
2689
|
}
|
|
867
2690
|
},
|
|
2691
|
+
/**
|
|
2692
|
+
* #method
|
|
2693
|
+
*/
|
|
868
2694
|
pxToBp(px) {
|
|
869
2695
|
return pxToBp(self, px);
|
|
870
2696
|
},
|
|
2697
|
+
/**
|
|
2698
|
+
* #getter
|
|
2699
|
+
*/
|
|
871
2700
|
get centerLineInfo() {
|
|
872
2701
|
return self.displayedRegions.length > 0
|
|
873
2702
|
? this.pxToBp(self.width / 2)
|
|
874
2703
|
: undefined;
|
|
875
2704
|
},
|
|
876
|
-
|
|
877
|
-
|
|
2705
|
+
}))
|
|
2706
|
+
.actions(self => ({
|
|
2707
|
+
/**
|
|
2708
|
+
* #action
|
|
2709
|
+
* a region edit from the ruler's menu as one action, which a synteny
|
|
2710
|
+
* stack's follow reads as the reader's gesture on this row; see
|
|
2711
|
+
* `setDisplayedRegionsKeepingCenter`
|
|
2712
|
+
*/
|
|
2713
|
+
editDisplayedRegions(edit) {
|
|
2714
|
+
setDisplayedRegionsKeepingCenter(self, edit);
|
|
878
2715
|
},
|
|
879
2716
|
}))
|
|
880
2717
|
.views(self => ({
|
|
2718
|
+
/**
|
|
2719
|
+
* #method
|
|
2720
|
+
*/
|
|
881
2721
|
rubberbandClickMenuItems(clickOffset) {
|
|
882
2722
|
return buildRubberbandClickMenuItems(self, clickOffset);
|
|
883
2723
|
},
|
|
2724
|
+
/**
|
|
2725
|
+
* #method
|
|
2726
|
+
* returns menu items for a highlight context menu. plugins can extend
|
|
2727
|
+
* this via Core-extendPluggableElement to add their own items
|
|
2728
|
+
*/
|
|
2729
|
+
highlightMenuItems(_highlight) {
|
|
2730
|
+
return [];
|
|
2731
|
+
},
|
|
884
2732
|
}))
|
|
2733
|
+
.actions(self => {
|
|
2734
|
+
let cancelLastFlight = () => { };
|
|
2735
|
+
// Where a flight in progress is going, and nothing while none is. A
|
|
2736
|
+
// flight's frames are presentation: the viewport the view has COMMITTED
|
|
2737
|
+
// to is the destination, and a reader that wants "the zoom this view is
|
|
2738
|
+
// on" wants that rather than whatever the arc is passing through. Without
|
|
2739
|
+
// it a second click landing mid-flight framed its mate at the apex's
|
|
2740
|
+
// zoom, which is a whole chromosome wide for no reason the reader can see.
|
|
2741
|
+
let heading;
|
|
2742
|
+
/**
|
|
2743
|
+
* #action
|
|
2744
|
+
* Animate to a window along the Van Wijk arc from the current window to
|
|
2745
|
+
* the target, over a duration derived from the arc.
|
|
2746
|
+
*
|
|
2747
|
+
* The final state is the one `setWindow` would produce; only the path in
|
|
2748
|
+
* between differs. A caller can therefore offer an Undo, a snackbar or a
|
|
2749
|
+
* follow anchor around this exactly as around the instant move.
|
|
2750
|
+
*
|
|
2751
|
+
* The flight stops when anything else moves the view: it reads back what
|
|
2752
|
+
* it wrote and stops as soon as the view holds something else, such as a
|
|
2753
|
+
* wheel zoom, a drag, a locstring nav, or the Undo on the snackbar the
|
|
2754
|
+
* flight was launched with. It compares against the written value, not
|
|
2755
|
+
* the requested one, because the write clamps: an arc that pulls back
|
|
2756
|
+
* past `maxBpPerPx` reads back its clamped result, and treating that as
|
|
2757
|
+
* interference would end the flight after one frame. `springAnimate`
|
|
2758
|
+
* handles the same value the same way.
|
|
2759
|
+
*/
|
|
2760
|
+
function flyTo(centerBp, windowWidthBp) {
|
|
2761
|
+
cancelLastFlight();
|
|
2762
|
+
cancelLastFlight = () => { };
|
|
2763
|
+
const flight = planFlight({
|
|
2764
|
+
centerBp: self.windowStartBp + self.windowWidthBp / 2,
|
|
2765
|
+
windowWidthBp: self.windowWidthBp,
|
|
2766
|
+
}, { centerBp, windowWidthBp });
|
|
2767
|
+
if (flight.durationMs > 0 && self.width > 0) {
|
|
2768
|
+
heading = { centerBp, windowWidthBp };
|
|
2769
|
+
const startedAt = performance.now();
|
|
2770
|
+
let frameId;
|
|
2771
|
+
let written;
|
|
2772
|
+
cancelLastFlight = () => {
|
|
2773
|
+
if (frameId !== undefined) {
|
|
2774
|
+
cancelAnimationFrame(frameId);
|
|
2775
|
+
}
|
|
2776
|
+
};
|
|
2777
|
+
function place(t) {
|
|
2778
|
+
const at = flight.at(t);
|
|
2779
|
+
self.setWindowFrame(at.windowWidthBp, at.centerBp - at.windowWidthBp / 2);
|
|
2780
|
+
written = {
|
|
2781
|
+
widthBp: self.windowWidthBp,
|
|
2782
|
+
startBp: self.windowStartBp,
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function frame() {
|
|
2786
|
+
// Liveness before any read: a row can be removed mid-flight — the
|
|
2787
|
+
// level holding it closed, the track detached — and every read
|
|
2788
|
+
// below throws on a dead node, in a callback with nobody to catch
|
|
2789
|
+
// it.
|
|
2790
|
+
if (isAlive(self)) {
|
|
2791
|
+
const moved = written !== undefined &&
|
|
2792
|
+
(self.windowWidthBp !== written.widthBp ||
|
|
2793
|
+
self.windowStartBp !== written.startBp);
|
|
2794
|
+
if (!moved) {
|
|
2795
|
+
const t = (performance.now() - startedAt) / flight.durationMs;
|
|
2796
|
+
if (t < 1) {
|
|
2797
|
+
place(t);
|
|
2798
|
+
frameId = requestAnimationFrame(frame);
|
|
2799
|
+
}
|
|
2800
|
+
else {
|
|
2801
|
+
place(1);
|
|
2802
|
+
self.settleCoarseBlocks();
|
|
2803
|
+
heading = undefined;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
else {
|
|
2807
|
+
heading = undefined;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
frame();
|
|
2812
|
+
}
|
|
2813
|
+
else {
|
|
2814
|
+
heading = undefined;
|
|
2815
|
+
self.setWindow(windowWidthBp, centerBp - windowWidthBp / 2);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return {
|
|
2819
|
+
flyTo,
|
|
2820
|
+
/**
|
|
2821
|
+
* #action
|
|
2822
|
+
* `centerAt`'s animated twin: the same destination, reached along the
|
|
2823
|
+
* arc instead of jumped to, at the zoom the view is already on.
|
|
2824
|
+
*/
|
|
2825
|
+
flyToCenter(coord, refName, displayedRegionIndex) {
|
|
2826
|
+
// `bpToLinearBp` rather than `bpToPx`, which rounds to a whole pixel
|
|
2827
|
+
// at the current zoom: mid-flight that zoom is a point on somebody
|
|
2828
|
+
// else's arc, and a destination quantized against it moves with it.
|
|
2829
|
+
const centerBp = bpToLinearBp({
|
|
2830
|
+
refName,
|
|
2831
|
+
coord,
|
|
2832
|
+
displayedRegions: self.displayedRegions,
|
|
2833
|
+
displayedRegionIndex,
|
|
2834
|
+
});
|
|
2835
|
+
if (centerBp !== undefined) {
|
|
2836
|
+
flyTo(centerBp, heading ? heading.windowWidthBp : self.windowWidthBp);
|
|
2837
|
+
}
|
|
2838
|
+
},
|
|
2839
|
+
/**
|
|
2840
|
+
* #action
|
|
2841
|
+
* A flight to a linear bp centre at the zoom the view is on, or is
|
|
2842
|
+
* already flying to, widened where that is narrower than `fitWidthBp`.
|
|
2843
|
+
* The current width mid-flight is a point on the arc, so a second
|
|
2844
|
+
* flight read from it would land pulled back.
|
|
2845
|
+
*/
|
|
2846
|
+
flyToFit(centerBp, fitWidthBp) {
|
|
2847
|
+
flyTo(centerBp, Math.max(heading ? heading.windowWidthBp : self.windowWidthBp, fitWidthBp));
|
|
2848
|
+
},
|
|
2849
|
+
};
|
|
2850
|
+
})
|
|
885
2851
|
.actions(self => ({
|
|
886
2852
|
afterCreate() {
|
|
887
2853
|
setupKeyboardHandler(self);
|
|
@@ -889,46 +2855,63 @@ export function stateModelFactory(pluginManager) {
|
|
|
889
2855
|
afterAttach() {
|
|
890
2856
|
doAfterAttach(self);
|
|
891
2857
|
},
|
|
892
|
-
}))
|
|
893
|
-
|
|
2858
|
+
}));
|
|
2859
|
+
return withLaunchInput(model, lgvLaunchKeys, {
|
|
2860
|
+
registry: pluginManager,
|
|
2861
|
+
materialized: snap => !!snap.displayedRegions?.length,
|
|
2862
|
+
})
|
|
2863
|
+
.preProcessSnapshot((snap) => {
|
|
894
2864
|
if (!snap) {
|
|
895
2865
|
return snap;
|
|
896
2866
|
}
|
|
897
|
-
|
|
2867
|
+
// v4's `showCytobandsSetting` persists as the bare `showCytobands` prop
|
|
2868
|
+
// (the capability-gated getter is `effectiveShowCytobands`).
|
|
2869
|
+
const { showCytobandsSetting, offsetPx, bpPerPx, ...rest } = snap;
|
|
898
2870
|
return {
|
|
899
|
-
|
|
900
|
-
?
|
|
901
|
-
:
|
|
2871
|
+
...(showCytobandsSetting !== undefined
|
|
2872
|
+
? { showCytobands: showCytobandsSetting }
|
|
2873
|
+
: {}),
|
|
2874
|
+
// The viewport used to persist as pixels. Half of that converts
|
|
2875
|
+
// exactly and needs no width — the left edge in bp is offsetPx *
|
|
2876
|
+
// bpPerPx — and half of it cannot convert at all, because the width
|
|
2877
|
+
// those pixels were measured at was never written down. So bpPerPx
|
|
2878
|
+
// rides to the first measure as `legacyBpPerPx` and is adopted at
|
|
2879
|
+
// whatever width arrives, which is precisely what the old code did.
|
|
2880
|
+
// An old link therefore keeps its old behavior instead of being
|
|
2881
|
+
// reinterpreted, and anything authored since restores its window.
|
|
2882
|
+
//
|
|
2883
|
+
// Also the path for the several places that BUILD a view from a
|
|
2884
|
+
// snapshot naming bpPerPx/offsetPx (a synteny row, a split view) —
|
|
2885
|
+
// they get the same treatment as a saved session, so none of them had
|
|
2886
|
+
// to change.
|
|
2887
|
+
...(typeof bpPerPx === 'number' &&
|
|
2888
|
+
bpPerPx > 0 &&
|
|
2889
|
+
rest.windowWidthBp === undefined
|
|
2890
|
+
? {
|
|
2891
|
+
legacyBpPerPx: bpPerPx,
|
|
2892
|
+
windowStartBp: (typeof offsetPx === 'number' ? offsetPx : 0) * bpPerPx,
|
|
2893
|
+
}
|
|
2894
|
+
: {}),
|
|
902
2895
|
...rest,
|
|
903
2896
|
};
|
|
904
2897
|
})
|
|
905
2898
|
.postProcessSnapshot(snap => {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
2899
|
+
// showCenterLine is purely a localStorage-backed preference (see
|
|
2900
|
+
// setupLocalStorageAutorun in afterAttach.ts) and is never persisted
|
|
2901
|
+
// into session snapshots. The remaining fields are also
|
|
2902
|
+
// localStorage-backed, but still persist when non-default: their strip
|
|
2903
|
+
// baseline is the universal default (hardcoded here), not the
|
|
2904
|
+
// localStorage-derived creation default, so a localStorage-set value
|
|
2905
|
+
// stays portable across browsers.
|
|
2906
|
+
const { showCenterLine, showCytobands, trackLabels, colorByCDS, showAminoAcids, showTrackOutlines, ...rest } = snap;
|
|
910
2907
|
return {
|
|
911
2908
|
...rest,
|
|
912
|
-
...(
|
|
913
|
-
...(bpPerPx !== 1 ? { bpPerPx } : {}),
|
|
914
|
-
...(hideHeader ? { hideHeader } : {}),
|
|
915
|
-
...(hideHeaderOverview ? { hideHeaderOverview } : {}),
|
|
916
|
-
...(hideNoTracksActive ? { hideNoTracksActive } : {}),
|
|
917
|
-
...(!showGridlines ? { showGridlines } : {}),
|
|
918
|
-
...(trackSelectorType !== 'hierarchical' ? { trackSelectorType } : {}),
|
|
919
|
-
...(displayedRegions.length ? { displayedRegions } : {}),
|
|
920
|
-
...(highlight.length ? { highlight } : {}),
|
|
921
|
-
...(showCenterLine ? { showCenterLine } : {}),
|
|
922
|
-
...(!showCytobandsSetting ? { showCytobandsSetting } : {}),
|
|
2909
|
+
...(!showCytobands ? { showCytobands } : {}),
|
|
923
2910
|
...(trackLabels ? { trackLabels } : {}),
|
|
924
2911
|
...(colorByCDS ? { colorByCDS } : {}),
|
|
2912
|
+
...(!showAminoAcids ? { showAminoAcids } : {}),
|
|
925
2913
|
...(!showTrackOutlines ? { showTrackOutlines } : {}),
|
|
926
|
-
...(!highlightsVisible ? { highlightsVisible } : {}),
|
|
927
|
-
...(!labelsVisible ? { labelsVisible } : {}),
|
|
928
2914
|
};
|
|
929
2915
|
});
|
|
930
2916
|
}
|
|
931
|
-
|
|
932
|
-
export { default as RefNameAutocomplete } from "./components/RefNameAutocomplete/index.js";
|
|
933
|
-
export { default as SearchBox } from "./components/SearchBox.js";
|
|
934
|
-
export { renderToSvg } from "./svgcomponents/SVGLinearGenomeView.js";
|
|
2917
|
+
// #endregion
|