@jbrowse/plugin-linear-genome-view 4.3.0 → 5.0.0-beta.2

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