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

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