@jbrowse/plugin-linear-genome-view 2.0.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/BaseLinearDisplay/components/BaseLinearDisplay.js +28 -55
  2. package/dist/BaseLinearDisplay/components/BaseLinearDisplay.js.map +1 -1
  3. package/dist/BaseLinearDisplay/components/Block.js +17 -28
  4. package/dist/BaseLinearDisplay/components/Block.js.map +1 -1
  5. package/dist/BaseLinearDisplay/components/LinearBlocks.js +19 -21
  6. package/dist/BaseLinearDisplay/components/LinearBlocks.js.map +1 -1
  7. package/dist/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js +28 -48
  8. package/dist/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js.map +1 -1
  9. package/dist/BaseLinearDisplay/components/Tooltip.js +29 -58
  10. package/dist/BaseLinearDisplay/components/Tooltip.js.map +1 -1
  11. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.js +242 -363
  12. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.js.map +1 -1
  13. package/dist/BaseLinearDisplay/models/baseLinearDisplayConfigSchema.js +1 -1
  14. package/dist/BaseLinearDisplay/models/baseLinearDisplayConfigSchema.js.map +1 -1
  15. package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.js +77 -129
  16. package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.js.map +1 -1
  17. package/dist/LinearBareDisplay/configSchema.js +2 -2
  18. package/dist/LinearBareDisplay/configSchema.js.map +1 -1
  19. package/dist/LinearBareDisplay/model.js +13 -19
  20. package/dist/LinearBareDisplay/model.js.map +1 -1
  21. package/dist/LinearBasicDisplay/components/SetMaxHeight.js +14 -31
  22. package/dist/LinearBasicDisplay/components/SetMaxHeight.js.map +1 -1
  23. package/dist/LinearBasicDisplay/configSchema.js +3 -3
  24. package/dist/LinearBasicDisplay/configSchema.js.map +1 -1
  25. package/dist/LinearBasicDisplay/model.js +119 -147
  26. package/dist/LinearBasicDisplay/model.js.map +1 -1
  27. package/dist/LinearGenomeView/components/CenterLine.js +11 -12
  28. package/dist/LinearGenomeView/components/CenterLine.js.map +1 -1
  29. package/dist/LinearGenomeView/components/ExportSvgDialog.js +30 -96
  30. package/dist/LinearGenomeView/components/ExportSvgDialog.js.map +1 -1
  31. package/dist/LinearGenomeView/components/GetSequenceDialog.d.ts +9 -0
  32. package/dist/LinearGenomeView/components/GetSequenceDialog.js +172 -0
  33. package/dist/LinearGenomeView/components/GetSequenceDialog.js.map +1 -0
  34. package/dist/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
  35. package/dist/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +19 -21
  36. package/dist/LinearGenomeView/components/Gridlines.js.map +1 -0
  37. package/dist/LinearGenomeView/components/Header.js +26 -31
  38. package/dist/LinearGenomeView/components/Header.js.map +1 -1
  39. package/dist/LinearGenomeView/components/HelpDialog.js +10 -11
  40. package/dist/LinearGenomeView/components/HelpDialog.js.map +1 -1
  41. package/dist/LinearGenomeView/components/ImportForm.js +91 -154
  42. package/dist/LinearGenomeView/components/ImportForm.js.map +1 -1
  43. package/dist/LinearGenomeView/components/LinearGenomeView.js +20 -25
  44. package/dist/LinearGenomeView/components/LinearGenomeView.js.map +1 -1
  45. package/dist/LinearGenomeView/components/LinearGenomeViewSvg.js +86 -157
  46. package/dist/LinearGenomeView/components/LinearGenomeViewSvg.js.map +1 -1
  47. package/dist/LinearGenomeView/components/MiniControls.js +16 -32
  48. package/dist/LinearGenomeView/components/MiniControls.js.map +1 -1
  49. package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +0 -11
  50. package/dist/LinearGenomeView/components/OverviewRubberBand.js +55 -84
  51. package/dist/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
  52. package/dist/LinearGenomeView/components/OverviewScaleBar.js +94 -117
  53. package/dist/LinearGenomeView/components/OverviewScaleBar.js.map +1 -1
  54. package/dist/LinearGenomeView/components/RefNameAutocomplete.js +90 -172
  55. package/dist/LinearGenomeView/components/RefNameAutocomplete.js.map +1 -1
  56. package/dist/LinearGenomeView/components/RubberBand.js +51 -72
  57. package/dist/LinearGenomeView/components/RubberBand.js.map +1 -1
  58. package/dist/LinearGenomeView/components/Ruler.js +17 -18
  59. package/dist/LinearGenomeView/components/Ruler.js.map +1 -1
  60. package/dist/LinearGenomeView/components/ScaleBar.js +37 -58
  61. package/dist/LinearGenomeView/components/ScaleBar.js.map +1 -1
  62. package/dist/LinearGenomeView/components/SearchBox.js +69 -133
  63. package/dist/LinearGenomeView/components/SearchBox.js.map +1 -1
  64. package/dist/LinearGenomeView/components/SearchResultsDialog.js +32 -33
  65. package/dist/LinearGenomeView/components/SearchResultsDialog.js.map +1 -1
  66. package/dist/LinearGenomeView/components/{SequenceDialog.d.ts → SequenceSearchDialog.d.ts} +0 -0
  67. package/dist/LinearGenomeView/components/SequenceSearchDialog.js +104 -0
  68. package/dist/LinearGenomeView/components/SequenceSearchDialog.js.map +1 -0
  69. package/dist/LinearGenomeView/components/TrackContainer.d.ts +2 -1
  70. package/dist/LinearGenomeView/components/TrackContainer.js +36 -43
  71. package/dist/LinearGenomeView/components/TrackContainer.js.map +1 -1
  72. package/dist/LinearGenomeView/components/TrackLabel.js +50 -85
  73. package/dist/LinearGenomeView/components/TrackLabel.js.map +1 -1
  74. package/dist/LinearGenomeView/components/TracksContainer.js +33 -50
  75. package/dist/LinearGenomeView/components/TracksContainer.js.map +1 -1
  76. package/dist/LinearGenomeView/components/ZoomControls.js +15 -32
  77. package/dist/LinearGenomeView/components/ZoomControls.js.map +1 -1
  78. package/dist/LinearGenomeView/components/util.js +14 -87
  79. package/dist/LinearGenomeView/components/util.js.map +1 -1
  80. package/dist/LinearGenomeView/index.d.ts +66 -79
  81. package/dist/LinearGenomeView/index.js +534 -710
  82. package/dist/LinearGenomeView/index.js.map +1 -1
  83. package/dist/LinearGenomeView/util.js +17 -36
  84. package/dist/LinearGenomeView/util.js.map +1 -1
  85. package/dist/index.js +75 -146
  86. package/dist/index.js.map +1 -1
  87. package/esm/BaseLinearDisplay/components/BaseLinearDisplay.js +1 -1
  88. package/esm/BaseLinearDisplay/components/BaseLinearDisplay.js.map +1 -1
  89. package/esm/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js +1 -0
  90. package/esm/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js.map +1 -1
  91. package/esm/LinearGenomeView/components/GetSequenceDialog.d.ts +9 -0
  92. package/esm/LinearGenomeView/components/{SequenceDialog.js → GetSequenceDialog.js} +5 -8
  93. package/esm/LinearGenomeView/components/GetSequenceDialog.js.map +1 -0
  94. package/esm/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
  95. package/esm/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
  96. package/esm/LinearGenomeView/components/Gridlines.js.map +1 -0
  97. package/esm/LinearGenomeView/components/ImportForm.js +1 -0
  98. package/esm/LinearGenomeView/components/ImportForm.js.map +1 -1
  99. package/esm/LinearGenomeView/components/LinearGenomeView.js +3 -7
  100. package/esm/LinearGenomeView/components/LinearGenomeView.js.map +1 -1
  101. package/esm/LinearGenomeView/components/OverviewRubberBand.d.ts +0 -11
  102. package/esm/LinearGenomeView/components/OverviewRubberBand.js +16 -21
  103. package/esm/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
  104. package/esm/LinearGenomeView/components/OverviewScaleBar.js +0 -2
  105. package/esm/LinearGenomeView/components/OverviewScaleBar.js.map +1 -1
  106. package/esm/LinearGenomeView/components/RubberBand.js +0 -2
  107. package/esm/LinearGenomeView/components/RubberBand.js.map +1 -1
  108. package/esm/LinearGenomeView/components/SearchBox.js.map +1 -1
  109. package/esm/LinearGenomeView/components/{SequenceDialog.d.ts → SequenceSearchDialog.d.ts} +0 -0
  110. package/esm/LinearGenomeView/components/SequenceSearchDialog.js +76 -0
  111. package/esm/LinearGenomeView/components/SequenceSearchDialog.js.map +1 -0
  112. package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -1
  113. package/esm/LinearGenomeView/components/TrackContainer.js +15 -20
  114. package/esm/LinearGenomeView/components/TrackContainer.js.map +1 -1
  115. package/esm/LinearGenomeView/components/TrackLabel.js +22 -23
  116. package/esm/LinearGenomeView/components/TrackLabel.js.map +1 -1
  117. package/esm/LinearGenomeView/components/TracksContainer.js +2 -2
  118. package/esm/LinearGenomeView/components/TracksContainer.js.map +1 -1
  119. package/esm/LinearGenomeView/index.d.ts +66 -79
  120. package/esm/LinearGenomeView/index.js +268 -407
  121. package/esm/LinearGenomeView/index.js.map +1 -1
  122. package/package.json +3 -4
  123. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +0 -1
  124. package/src/BaseLinearDisplay/components/ServerSideRenderedBlockContent.tsx +2 -1
  125. package/src/LinearGenomeView/components/{SequenceDialog.tsx → GetSequenceDialog.tsx} +5 -18
  126. package/src/LinearGenomeView/components/{VerticalGuides.tsx → Gridlines.tsx} +0 -0
  127. package/src/LinearGenomeView/components/ImportForm.tsx +1 -0
  128. package/src/LinearGenomeView/components/LinearGenomeView.tsx +4 -8
  129. package/src/LinearGenomeView/components/OverviewRubberBand.tsx +19 -34
  130. package/src/LinearGenomeView/components/OverviewScaleBar.tsx +0 -2
  131. package/src/LinearGenomeView/components/RubberBand.tsx +0 -2
  132. package/src/LinearGenomeView/components/SearchBox.tsx +1 -1
  133. package/src/LinearGenomeView/components/SequenceSearchDialog.tsx +165 -0
  134. package/src/LinearGenomeView/components/TrackContainer.tsx +18 -28
  135. package/src/LinearGenomeView/components/TrackLabel.tsx +60 -53
  136. package/src/LinearGenomeView/components/TracksContainer.tsx +2 -2
  137. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +14 -17
  138. package/src/LinearGenomeView/index.test.ts +14 -36
  139. package/src/LinearGenomeView/index.tsx +389 -541
  140. package/dist/LinearGenomeView/components/SequenceDialog.js +0 -242
  141. package/dist/LinearGenomeView/components/SequenceDialog.js.map +0 -1
  142. package/dist/LinearGenomeView/components/VerticalGuides.js.map +0 -1
  143. package/esm/LinearGenomeView/components/SequenceDialog.js.map +0 -1
  144. package/esm/LinearGenomeView/components/VerticalGuides.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { lazy } from 'react'
1
2
  import { getConf, AnyConfigurationModel } from '@jbrowse/core/configuration'
2
3
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'
3
4
  import { Region } from '@jbrowse/core/util/types'
@@ -14,7 +15,6 @@ import {
14
15
  measureText,
15
16
  parseLocString,
16
17
  springAnimate,
17
- viewBpToPx,
18
18
  } from '@jbrowse/core/util'
19
19
  import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
20
20
  import { BlockSet, BaseBlock } from '@jbrowse/core/util/blockTypes'
@@ -33,9 +33,10 @@ import {
33
33
  } from 'mobx-state-tree'
34
34
 
35
35
  import Base1DView from '@jbrowse/core/util/Base1DViewModel'
36
- import PluginManager from '@jbrowse/core/PluginManager'
37
- import clone from 'clone'
36
+ import { moveTo, pxToBp, bpToPx } from '@jbrowse/core/util/Base1DUtils'
38
37
  import { saveAs } from 'file-saver'
38
+ import clone from 'clone'
39
+ import PluginManager from '@jbrowse/core/PluginManager'
39
40
 
40
41
  // icons
41
42
  import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons'
@@ -53,6 +54,10 @@ import RefNameAutocomplete from './components/RefNameAutocomplete'
53
54
  import SearchBox from './components/SearchBox'
54
55
  import ExportSvgDlg from './components/ExportSvgDialog'
55
56
 
57
+ const SequenceSearchDialog = lazy(
58
+ () => import('./components/SequenceSearchDialog'),
59
+ )
60
+
56
61
  export interface BpOffset {
57
62
  refName?: string
58
63
  index: number
@@ -144,6 +149,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
144
149
  const setting = localStorageGetItem('lgv-showCytobands')
145
150
  return setting !== undefined && setting !== null ? !!+setting : true
146
151
  }),
152
+ showGridlines: true,
147
153
  }),
148
154
  )
149
155
  .volatile(() => ({
@@ -288,105 +294,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
288
294
  }
289
295
  },
290
296
 
291
- bpToPx({
292
- refName,
293
- coord,
294
- regionNumber,
295
- }: {
296
- refName: string
297
- coord: number
298
- regionNumber?: number
299
- }) {
300
- return viewBpToPx({ refName, coord, regionNumber, self })
301
- },
302
- /**
303
- *
304
- * @param px - px in the view area, return value is the displayed regions
305
- * @returns BpOffset of the displayed region that it lands in
306
- */
307
- pxToBp(px: number) {
308
- let bpSoFar = 0
309
- const bp = (self.offsetPx + px) * self.bpPerPx
310
- const n = self.displayedRegions.length
311
- if (bp < 0) {
312
- const region = self.displayedRegions[0]
313
- const offset = bp
314
- const snap = getSnapshot(region)
315
- return {
316
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
317
- ...(snap as Omit<typeof snap, symbol>),
318
- oob: true,
319
- coord: region.reversed
320
- ? Math.floor(region.end - offset) + 1
321
- : Math.floor(region.start + offset) + 1,
322
- offset,
323
- index: 0,
324
- }
325
- }
326
-
327
- const interRegionPaddingBp = self.interRegionPaddingWidth * self.bpPerPx
328
- const minimumBlockBp = self.minimumBlockWidth * self.bpPerPx
329
-
330
- for (let index = 0; index < self.displayedRegions.length; index += 1) {
331
- const region = self.displayedRegions[index]
332
- const len = region.end - region.start
333
- const offset = bp - bpSoFar
334
- if (len + bpSoFar > bp && bpSoFar <= bp) {
335
- const snap = getSnapshot(region)
336
- return {
337
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
338
- ...(snap as Omit<typeof snap, symbol>),
339
- oob: false,
340
- offset,
341
- coord: region.reversed
342
- ? Math.floor(region.end - offset) + 1
343
- : Math.floor(region.start + offset) + 1,
344
- index,
345
- }
346
- }
347
-
348
- // add the interRegionPaddingWidth if the boundary is in the screen
349
- // e.g. offset>0 && offset<width
350
- if (
351
- region.end - region.start > minimumBlockBp &&
352
- offset / self.bpPerPx > 0 &&
353
- offset / self.bpPerPx < self.width
354
- ) {
355
- bpSoFar += len + interRegionPaddingBp
356
- } else {
357
- bpSoFar += len
358
- }
359
- }
360
-
361
- if (bp >= bpSoFar) {
362
- const region = self.displayedRegions[n - 1]
363
- const len = region.end - region.start
364
- const offset = bp - bpSoFar + len
365
- const snap = getSnapshot(region)
366
- return {
367
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
368
- ...(snap as Omit<typeof snap, symbol>),
369
- oob: true,
370
- offset,
371
- coord: region.reversed
372
- ? Math.floor(region.end - offset) + 1
373
- : Math.floor(region.start + offset) + 1,
374
- index: n - 1,
375
- }
376
- }
377
- return {
378
- coord: 0,
379
- index: 0,
380
- refName: '',
381
- oob: true,
382
- assemblyName: '',
383
- offset: 0,
384
- start: 0,
385
- end: 0,
386
- reversed: false,
387
- }
388
- },
389
-
390
297
  getTrack(id: string) {
391
298
  return self.tracks.find(t => t.configuration.trackId === id)
392
299
  },
@@ -437,12 +344,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
437
344
 
438
345
  return allActions
439
346
  },
440
-
441
- get centerLineInfo() {
442
- return self.displayedRegions.length
443
- ? this.pxToBp(self.width / 2)
444
- : undefined
445
- },
446
347
  }))
447
348
  .actions(self => ({
448
349
  setShowCytobands(flag: boolean) {
@@ -466,6 +367,9 @@ export function stateModelFactory(pluginManager: PluginManager) {
466
367
  toggleNoTracksActive() {
467
368
  self.hideNoTracksActive = !self.hideNoTracksActive
468
369
  },
370
+ toggleShowGridlines() {
371
+ self.showGridlines = !self.showGridlines
372
+ },
469
373
 
470
374
  scrollTo(offsetPx: number) {
471
375
  const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset)
@@ -497,21 +401,18 @@ export function stateModelFactory(pluginManager: PluginManager) {
497
401
  return newBpPerPx
498
402
  },
499
403
 
500
- setOffsets(left: undefined | BpOffset, right: undefined | BpOffset) {
404
+ setOffsets(left?: BpOffset, right?: BpOffset) {
501
405
  // sets offsets used in the get sequence dialog
502
406
  self.leftOffset = left
503
407
  self.rightOffset = right
504
408
  },
505
409
 
506
- setSearchResults(
507
- results: BaseResult[] | undefined,
508
- query: string | undefined,
509
- ) {
410
+ setSearchResults(results?: BaseResult[], query?: string) {
510
411
  self.searchResults = results
511
412
  self.searchQuery = query
512
413
  },
513
414
 
514
- setSequenceDialogOpen(open: boolean) {
415
+ setGetSequenceDialogOpen(open: boolean) {
515
416
  self.seqDialogDisplayed = open
516
417
  },
517
418
 
@@ -651,429 +552,82 @@ export function stateModelFactory(pluginManager: PluginManager) {
651
552
  throw new Error(`invalid track selector type ${self.trackSelectorType}`)
652
553
  },
653
554
 
654
- navToLocString(locString: string, optAssemblyName?: string) {
655
- const { assemblyNames } = self
656
- const { assemblyManager } = getSession(self)
657
- const { isValidRefName } = assemblyManager
658
- const assemblyName = optAssemblyName || assemblyNames[0]
659
- let parsedLocStrings
660
- const inputs = locString
661
- .split(/(\s+)/)
662
- .map(f => f.trim())
663
- .filter(f => !!f)
664
-
665
- // first try interpreting as a whitespace-separated sequence of
666
- // multiple locstrings
667
- try {
668
- parsedLocStrings = inputs.map(l =>
669
- parseLocString(l, ref => isValidRefName(ref, assemblyName)),
670
- )
671
- } catch (e) {
672
- // if this fails, try interpreting as a whitespace-separated refname,
673
- // start, end if start and end are integer inputs
674
- const [refName, start, end] = inputs
675
- if (
676
- `${e}`.match(/Unknown reference sequence/) &&
677
- Number.isInteger(+start) &&
678
- Number.isInteger(+end)
679
- ) {
680
- parsedLocStrings = [
681
- parseLocString(refName + ':' + start + '..' + end, ref =>
682
- isValidRefName(ref, assemblyName),
683
- ),
684
- ]
685
- } else {
686
- throw e
687
- }
688
- }
555
+ /**
556
+ * Helper method for the fetchSequence.
557
+ * Retrieves the corresponding regions that were selected by the rubberband
558
+ *
559
+ * @param leftOffset - `object as {start, end, index, offset}`, offset = start of user drag
560
+ * @param rightOffset - `object as {start, end, index, offset}`, offset = end of user drag
561
+ * @returns array of Region[]
562
+ */
563
+ getSelectedRegions(leftOffset?: BpOffset, rightOffset?: BpOffset) {
564
+ const snap = getSnapshot(self)
565
+ const simView = Base1DView.create({
566
+ ...snap,
567
+ interRegionPaddingWidth: self.interRegionPaddingWidth,
568
+ })
689
569
 
690
- const locations = parsedLocStrings?.map(region => {
691
- const asmName = region.assemblyName || assemblyName
692
- const asm = assemblyManager.get(asmName)
693
- const { refName } = region
694
- if (!asm) {
695
- throw new Error(`assembly ${asmName} not found`)
696
- }
697
- const { regions } = asm
698
- if (!regions) {
699
- throw new Error(`regions not loaded yet for ${asmName}`)
700
- }
701
- const canonicalRefName = asm.getCanonicalRefName(region.refName)
702
- if (!canonicalRefName) {
703
- throw new Error(`Could not find refName ${refName} in ${asm.name}`)
704
- }
705
- const parentRegion = regions.find(
706
- region => region.refName === canonicalRefName,
707
- )
708
- if (!parentRegion) {
709
- throw new Error(`Could not find refName ${refName} in ${asmName}`)
710
- }
570
+ simView.setVolatileWidth(self.width)
571
+ simView.moveTo(leftOffset, rightOffset)
711
572
 
712
- return {
713
- ...region,
714
- assemblyName: asmName,
715
- parentRegion,
716
- }
717
- })
573
+ return simView.dynamicBlocks.contentBlocks.map(region => ({
574
+ ...region,
575
+ start: Math.floor(region.start),
576
+ end: Math.ceil(region.end),
577
+ }))
578
+ },
718
579
 
719
- if (locations.length === 1) {
720
- const loc = locations[0]
721
- this.setDisplayedRegions([
722
- { reversed: loc.reversed, ...loc.parentRegion },
723
- ])
724
- const { start, end, parentRegion } = loc
580
+ // schedule something to be run after the next time displayedRegions is set
581
+ afterDisplayedRegionsSet(cb: Function) {
582
+ self.afterDisplayedRegionsSetCallbacks.push(cb)
583
+ },
725
584
 
726
- this.navTo({
727
- ...loc,
728
- start: clamp(start ?? 0, 0, parentRegion.end),
729
- end: clamp(end ?? parentRegion.end, 0, parentRegion.end),
730
- })
731
- } else {
732
- this.setDisplayedRegions(
733
- // @ts-ignore
734
- locations.map(r => (r.start === undefined ? r.parentRegion : r)),
735
- )
736
- this.showAllRegions()
737
- }
585
+ horizontalScroll(distance: number) {
586
+ const oldOffsetPx = self.offsetPx
587
+ // newOffsetPx is the actual offset after the scroll is clamped
588
+ const newOffsetPx = self.scrollTo(self.offsetPx + distance)
589
+ return newOffsetPx - oldOffsetPx
738
590
  },
739
591
 
740
- /**
741
- * Navigate to a location based on its refName and optionally start, end,
742
- * and assemblyName. Can handle if there are multiple displayedRegions
743
- * from same refName. Only navigates to a location if it is entirely
744
- * within a displayedRegion. Navigates to the first matching location
745
- * encountered.
746
- *
747
- * Throws an error if navigation was unsuccessful
748
- *
749
- * @param location - a proposed location to navigate to
750
- */
751
- navTo(query: NavLocation) {
752
- this.navToMultiple([query])
592
+ center() {
593
+ const centerBp = self.totalBp / 2
594
+ const centerPx = centerBp / self.bpPerPx
595
+ self.scrollTo(Math.round(centerPx - self.width / 2))
753
596
  },
754
597
 
755
- navToMultiple(locations: NavLocation[]) {
756
- const firstLocation = locations[0]
757
- let { refName } = firstLocation
758
- const {
759
- start,
760
- end,
761
- assemblyName = self.assemblyNames[0],
762
- } = firstLocation
598
+ showAllRegions() {
599
+ self.zoomTo(self.maxBpPerPx)
600
+ this.center()
601
+ },
763
602
 
764
- if (start !== undefined && end !== undefined && start > end) {
765
- throw new Error(`start "${start + 1}" is greater than end "${end}"`)
766
- }
603
+ showAllRegionsInAssembly(assemblyName?: string) {
767
604
  const session = getSession(self)
768
605
  const { assemblyManager } = session
769
- const assembly = assemblyManager.get(assemblyName)
770
- if (assembly) {
771
- const canonicalRefName = assembly.getCanonicalRefName(refName)
772
- if (canonicalRefName) {
773
- refName = canonicalRefName
606
+ if (!assemblyName) {
607
+ const assemblyNames = [
608
+ ...new Set(
609
+ self.displayedRegions.map(region => region.assemblyName),
610
+ ),
611
+ ]
612
+ if (assemblyNames.length > 1) {
613
+ session.notify(
614
+ `Can't perform this with multiple assemblies currently`,
615
+ )
616
+ return
774
617
  }
618
+
619
+ ;[assemblyName] = assemblyNames
775
620
  }
776
- let s = start
777
- let e = end
778
- let refNameMatched = false
779
- const predicate = (r: Region) => {
780
- if (refName === r.refName) {
781
- refNameMatched = true
782
- if (s === undefined) {
783
- s = r.start
784
- }
785
- if (e === undefined) {
786
- e = r.end
787
- }
788
- if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
789
- return true
790
- }
791
- s = start
792
- e = end
621
+ const assembly = assemblyManager.get(assemblyName)
622
+ if (assembly) {
623
+ const { regions } = assembly
624
+ if (regions) {
625
+ this.setDisplayedRegions(regions)
626
+ self.zoomTo(self.maxBpPerPx)
627
+ this.center()
793
628
  }
794
- return false
795
629
  }
796
-
797
- const lastIndex = findLastIndex(self.displayedRegions, predicate)
798
- let index
799
- while (index !== lastIndex) {
800
- try {
801
- const previousIndex: number | undefined = index
802
- index = self.displayedRegions
803
- .slice(previousIndex === undefined ? 0 : previousIndex + 1)
804
- .findIndex(predicate)
805
- if (previousIndex !== undefined) {
806
- index += previousIndex + 1
807
- }
808
- if (!refNameMatched) {
809
- throw new Error(
810
- `could not find a region with refName "${refName}"`,
811
- )
812
- }
813
- if (s === undefined) {
814
- throw new Error(
815
- `could not find a region with refName "${refName}" that contained an end position ${e}`,
816
- )
817
- }
818
- if (e === undefined) {
819
- throw new Error(
820
- `could not find a region with refName "${refName}" that contained a start position ${
821
- s + 1
822
- }`,
823
- )
824
- }
825
- if (index === -1) {
826
- throw new Error(
827
- `could not find a region that completely contained "${assembleLocString(
828
- firstLocation,
829
- )}"`,
830
- )
831
- }
832
- if (locations.length === 1) {
833
- const f = self.displayedRegions[index]
834
- this.moveTo(
835
- { index, offset: f.reversed ? f.end - e : s - f.start },
836
- { index, offset: f.reversed ? f.end - s : e - f.start },
837
- )
838
- return
839
- }
840
- let locationIndex = 0
841
- let locationStart = 0
842
- let locationEnd = 0
843
- for (
844
- locationIndex;
845
- locationIndex < locations.length;
846
- locationIndex++
847
- ) {
848
- const location = locations[locationIndex]
849
- const region = self.displayedRegions[index + locationIndex]
850
- locationStart = location.start || region.start
851
- locationEnd = location.end || region.end
852
- if (location.refName !== region.refName) {
853
- throw new Error(
854
- `Entered location ${assembleLocString(
855
- location,
856
- )} does not match with displayed regions`,
857
- )
858
- }
859
- }
860
- locationIndex -= 1
861
- const startDisplayedRegion = self.displayedRegions[index]
862
- const endDisplayedRegion =
863
- self.displayedRegions[index + locationIndex]
864
- this.moveTo(
865
- {
866
- index,
867
- offset: startDisplayedRegion.reversed
868
- ? startDisplayedRegion.end - e
869
- : s - startDisplayedRegion.start,
870
- },
871
- {
872
- index: index + locationIndex,
873
- offset: endDisplayedRegion.reversed
874
- ? endDisplayedRegion.end - locationStart
875
- : locationEnd - endDisplayedRegion.start,
876
- },
877
- )
878
- return
879
- } catch (error) {
880
- if (index === lastIndex) {
881
- throw error
882
- }
883
- }
884
- }
885
- },
886
-
887
- /**
888
- * Navigate to a location based on user clicking and dragging on the
889
- * overview scale bar to select a region to zoom into.
890
- * Can handle if there are multiple displayedRegions from same refName.
891
- * Only navigates to a location if it is entirely within a displayedRegion.
892
- *
893
- * @param leftPx- `object as {start, end, index, offset}`, offset = start of user drag
894
- * @param rightPx- `object as {start, end, index, offset}`, offset = end of user drag
895
- */
896
- zoomToDisplayedRegions(leftPx: BpOffset, rightPx: BpOffset) {
897
- if (leftPx === undefined || rightPx === undefined) {
898
- return
899
- }
900
-
901
- const singleRefSeq =
902
- leftPx.refName === rightPx.refName && leftPx.index === rightPx.index
903
- // zooming into one displayed Region
904
- if (
905
- (singleRefSeq && rightPx.offset < leftPx.offset) ||
906
- leftPx.index > rightPx.index
907
- ) {
908
- ;[leftPx, rightPx] = [rightPx, leftPx]
909
- }
910
- const startOffset = {
911
- start: leftPx.start,
912
- end: leftPx.end,
913
- index: leftPx.index,
914
- offset: leftPx.offset,
915
- }
916
- const endOffset = {
917
- start: rightPx.start,
918
- end: rightPx.end,
919
- index: rightPx.index,
920
- offset: rightPx.offset,
921
- }
922
- if (startOffset && endOffset) {
923
- this.moveTo(startOffset, endOffset)
924
- } else {
925
- const session = getSession(self)
926
- session.notify('No regions found to navigate to', 'warning')
927
- }
928
- },
929
- /**
930
- * Helper method for the fetchSequence.
931
- * Retrieves the corresponding regions that were selected by the rubberband
932
- *
933
- * @param leftOffset - `object as {start, end, index, offset}`, offset = start of user drag
934
- * @param rightOffset - `object as {start, end, index, offset}`, offset = end of user drag
935
- * @returns array of Region[]
936
- */
937
- getSelectedRegions(
938
- leftOffset: BpOffset | undefined,
939
- rightOffset: BpOffset | undefined,
940
- ) {
941
- const snap = getSnapshot(self)
942
- const simView = Base1DView.create({
943
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
944
- ...(snap as Omit<typeof self, symbol>),
945
- interRegionPaddingWidth: self.interRegionPaddingWidth,
946
- })
947
-
948
- simView.setVolatileWidth(self.width)
949
- simView.zoomToDisplayedRegions(leftOffset, rightOffset)
950
-
951
- return simView.dynamicBlocks.contentBlocks.map(region => ({
952
- ...region,
953
- start: Math.floor(region.start),
954
- end: Math.ceil(region.end),
955
- }))
956
- },
957
-
958
- // schedule something to be run after the next time displayedRegions is set
959
- afterDisplayedRegionsSet(cb: Function) {
960
- self.afterDisplayedRegionsSetCallbacks.push(cb)
961
- },
962
- /**
963
- * offset is the base-pair-offset in the displayed region, index is the index of the
964
- * displayed region in the linear genome view
965
- *
966
- * @param start - object as `{start, end, offset, index}`
967
- * @param end - object as `{start, end, offset, index}`
968
- */
969
- moveTo(start: BpOffset, end: BpOffset) {
970
- // find locations in the modellist
971
- let bpSoFar = 0
972
-
973
- if (start.index === end.index) {
974
- bpSoFar += end.offset - start.offset
975
- } else {
976
- const s = self.displayedRegions[start.index]
977
- bpSoFar += s.end - s.start - start.offset
978
- if (end.index - start.index >= 2) {
979
- for (let i = start.index + 1; i < end.index; i += 1) {
980
- bpSoFar +=
981
- self.displayedRegions[i].end - self.displayedRegions[i].start
982
- }
983
- }
984
- bpSoFar += end.offset
985
- }
986
- const targetBpPerPx =
987
- bpSoFar /
988
- (self.width -
989
- self.interRegionPaddingWidth * (end.index - start.index))
990
- const newBpPerPx = self.zoomTo(targetBpPerPx)
991
- // If our target bpPerPx was smaller than the allowed minBpPerPx, adjust
992
- // the scroll so the requested range is in the middle of the screen
993
- let extraBp = 0
994
- if (targetBpPerPx < newBpPerPx) {
995
- extraBp = ((newBpPerPx - targetBpPerPx) * self.width) / 2
996
- }
997
-
998
- let bpToStart = -extraBp
999
- for (let i = 0; i < self.displayedRegions.length; i += 1) {
1000
- const region = self.displayedRegions[i]
1001
- if (start.index === i) {
1002
- bpToStart += start.offset
1003
- break
1004
- } else {
1005
- bpToStart += region.end - region.start
1006
- }
1007
- }
1008
- self.scrollTo(
1009
- Math.round(bpToStart / self.bpPerPx) +
1010
- self.interRegionPaddingWidth * start.index,
1011
- )
1012
- },
1013
-
1014
- horizontalScroll(distance: number) {
1015
- const oldOffsetPx = self.offsetPx
1016
- // newOffsetPx is the actual offset after the scroll is clamped
1017
- const newOffsetPx = self.scrollTo(self.offsetPx + distance)
1018
- return newOffsetPx - oldOffsetPx
1019
- },
1020
-
1021
- /**
1022
- * scrolls the view to center on the given bp. if that is not in any
1023
- * of the displayed regions, does nothing
1024
- * @param bp - basepair at which you want to center the view
1025
- * @param refName - refName of the displayedRegion you are centering at
1026
- * @param regionIndex - index of the displayedRegion
1027
- */
1028
- centerAt(bp: number, refName: string, regionIndex: number) {
1029
- const centerPx = self.bpToPx({
1030
- refName,
1031
- coord: bp,
1032
- regionNumber: regionIndex,
1033
- })
1034
- if (centerPx) {
1035
- self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2))
1036
- }
1037
- },
1038
-
1039
- center() {
1040
- const centerBp = self.totalBp / 2
1041
- self.scrollTo(Math.round(centerBp / self.bpPerPx - self.width / 2))
1042
- },
1043
-
1044
- showAllRegions() {
1045
- self.zoomTo(self.maxBpPerPx)
1046
- this.center()
1047
- },
1048
-
1049
- showAllRegionsInAssembly(assemblyName?: string) {
1050
- const session = getSession(self)
1051
- const { assemblyManager } = session
1052
- if (!assemblyName) {
1053
- const assemblyNames = [
1054
- ...new Set(
1055
- self.displayedRegions.map(region => region.assemblyName),
1056
- ),
1057
- ]
1058
- if (assemblyNames.length > 1) {
1059
- session.notify(
1060
- `Can't perform this with multiple assemblies currently`,
1061
- )
1062
- return
1063
- }
1064
-
1065
- ;[assemblyName] = assemblyNames
1066
- }
1067
- const assembly = assemblyManager.get(assemblyName)
1068
- if (assembly) {
1069
- const { regions } = assembly
1070
- if (regions) {
1071
- this.setDisplayedRegions(regions)
1072
- self.zoomTo(self.maxBpPerPx)
1073
- this.center()
1074
- }
1075
- }
1076
- },
630
+ },
1077
631
 
1078
632
  setDraggingTrackId(idx?: string) {
1079
633
  self.draggingTrackId = idx
@@ -1165,6 +719,15 @@ export function stateModelFactory(pluginManager: PluginManager) {
1165
719
  },
1166
720
  icon: FolderOpenIcon,
1167
721
  },
722
+ {
723
+ label: 'Sequence search',
724
+ onClick: () => {
725
+ getSession(self).queueDialog(handleClose => [
726
+ SequenceSearchDialog,
727
+ { model: self, handleClose },
728
+ ])
729
+ },
730
+ },
1168
731
  {
1169
732
  label: 'Export SVG',
1170
733
  icon: PhotoCameraIcon,
@@ -1220,6 +783,24 @@ export function stateModelFactory(pluginManager: PluginManager) {
1220
783
  checked: !self.hideNoTracksActive,
1221
784
  onClick: self.toggleNoTracksActive,
1222
785
  },
786
+ {
787
+ label: 'Show guidelines',
788
+ icon: VisibilityIcon,
789
+ type: 'checkbox',
790
+ checked: self.showGridlines,
791
+ onClick: self.toggleShowGridlines,
792
+ },
793
+ ...(canShowCytobands
794
+ ? [
795
+ {
796
+ label: 'Show ideogram',
797
+ icon: VisibilityIcon,
798
+ type: 'checkbox' as const,
799
+ checked: self.showCytobands,
800
+ onClick: () => self.setShowCytobands(!showCytobands),
801
+ },
802
+ ]
803
+ : []),
1223
804
  {
1224
805
  label: 'Track labels',
1225
806
  icon: LabelIcon,
@@ -1247,16 +828,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
1247
828
  },
1248
829
  ],
1249
830
  },
1250
- ...(canShowCytobands
1251
- ? [
1252
- {
1253
- label: showCytobands ? 'Hide ideogram' : 'Show ideograms',
1254
- onClick: () => {
1255
- self.setShowCytobands(!showCytobands)
1256
- },
1257
- },
1258
- ]
1259
- : []),
1260
831
  ]
1261
832
 
1262
833
  // add track's view level menu options
@@ -1266,9 +837,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
1266
837
  { type: 'divider' },
1267
838
  { type: 'subHeader', label: key },
1268
839
  )
1269
- value.forEach(action => {
1270
- menuItems.push(action)
1271
- })
840
+ value.forEach(action => menuItems.push(action))
1272
841
  }
1273
842
  }
1274
843
 
@@ -1346,6 +915,249 @@ export function stateModelFactory(pluginManager: PluginManager) {
1346
915
  const blob = new Blob([html], { type: 'image/svg+xml' })
1347
916
  saveAs(blob, opts.filename || 'image.svg')
1348
917
  },
918
+ /**
919
+ * offset is the base-pair-offset in the displayed region, index is the index of the
920
+ * displayed region in the linear genome view
921
+ *
922
+ * @param start - object as `{start, end, offset, index}`
923
+ * @param end - object as `{start, end, offset, index}`
924
+ */
925
+ moveTo(start?: BpOffset, end?: BpOffset) {
926
+ moveTo(self, start, end)
927
+ },
928
+
929
+ navToLocString(locString: string, optAssemblyName?: string) {
930
+ const { assemblyNames } = self
931
+ const { assemblyManager } = getSession(self)
932
+ const { isValidRefName } = assemblyManager
933
+ const assemblyName = optAssemblyName || assemblyNames[0]
934
+ let parsedLocStrings
935
+ const inputs = locString
936
+ .split(/(\s+)/)
937
+ .map(f => f.trim())
938
+ .filter(f => !!f)
939
+
940
+ // first try interpreting as a whitespace-separated sequence of
941
+ // multiple locstrings
942
+ try {
943
+ parsedLocStrings = inputs.map(l =>
944
+ parseLocString(l, ref => isValidRefName(ref, assemblyName)),
945
+ )
946
+ } catch (e) {
947
+ // if this fails, try interpreting as a whitespace-separated refname,
948
+ // start, end if start and end are integer inputs
949
+ const [refName, start, end] = inputs
950
+ if (
951
+ `${e}`.match(/Unknown reference sequence/) &&
952
+ Number.isInteger(+start) &&
953
+ Number.isInteger(+end)
954
+ ) {
955
+ parsedLocStrings = [
956
+ parseLocString(refName + ':' + start + '..' + end, ref =>
957
+ isValidRefName(ref, assemblyName),
958
+ ),
959
+ ]
960
+ } else {
961
+ throw e
962
+ }
963
+ }
964
+
965
+ const locations = parsedLocStrings?.map(region => {
966
+ const asmName = region.assemblyName || assemblyName
967
+ const asm = assemblyManager.get(asmName)
968
+ const { refName } = region
969
+ if (!asm) {
970
+ throw new Error(`assembly ${asmName} not found`)
971
+ }
972
+ const { regions } = asm
973
+ if (!regions) {
974
+ throw new Error(`regions not loaded yet for ${asmName}`)
975
+ }
976
+ const canonicalRefName = asm.getCanonicalRefName(region.refName)
977
+ if (!canonicalRefName) {
978
+ throw new Error(`Could not find refName ${refName} in ${asm.name}`)
979
+ }
980
+ const parentRegion = regions.find(
981
+ region => region.refName === canonicalRefName,
982
+ )
983
+ if (!parentRegion) {
984
+ throw new Error(`Could not find refName ${refName} in ${asmName}`)
985
+ }
986
+
987
+ return {
988
+ ...region,
989
+ assemblyName: asmName,
990
+ parentRegion,
991
+ }
992
+ })
993
+
994
+ if (locations.length === 1) {
995
+ const loc = locations[0]
996
+ self.setDisplayedRegions([
997
+ { reversed: loc.reversed, ...loc.parentRegion },
998
+ ])
999
+ const { start, end, parentRegion } = loc
1000
+
1001
+ this.navTo({
1002
+ ...loc,
1003
+ start: clamp(start ?? 0, 0, parentRegion.end),
1004
+ end: clamp(end ?? parentRegion.end, 0, parentRegion.end),
1005
+ })
1006
+ } else {
1007
+ self.setDisplayedRegions(
1008
+ // @ts-ignore
1009
+ locations.map(r => (r.start === undefined ? r.parentRegion : r)),
1010
+ )
1011
+ self.showAllRegions()
1012
+ }
1013
+ },
1014
+
1015
+ /**
1016
+ * Navigate to a location based on its refName and optionally start, end,
1017
+ * and assemblyName. Can handle if there are multiple displayedRegions
1018
+ * from same refName. Only navigates to a location if it is entirely
1019
+ * within a displayedRegion. Navigates to the first matching location
1020
+ * encountered.
1021
+ *
1022
+ * Throws an error if navigation was unsuccessful
1023
+ *
1024
+ * @param location - a proposed location to navigate to
1025
+ */
1026
+ navTo(query: NavLocation) {
1027
+ this.navToMultiple([query])
1028
+ },
1029
+
1030
+ navToMultiple(locations: NavLocation[]) {
1031
+ const firstLocation = locations[0]
1032
+ let { refName } = firstLocation
1033
+ const {
1034
+ start,
1035
+ end,
1036
+ assemblyName = self.assemblyNames[0],
1037
+ } = firstLocation
1038
+
1039
+ if (start !== undefined && end !== undefined && start > end) {
1040
+ throw new Error(`start "${start + 1}" is greater than end "${end}"`)
1041
+ }
1042
+ const session = getSession(self)
1043
+ const { assemblyManager } = session
1044
+ const assembly = assemblyManager.get(assemblyName)
1045
+ if (assembly) {
1046
+ const canonicalRefName = assembly.getCanonicalRefName(refName)
1047
+ if (canonicalRefName) {
1048
+ refName = canonicalRefName
1049
+ }
1050
+ }
1051
+ let s = start
1052
+ let e = end
1053
+ let refNameMatched = false
1054
+ const predicate = (r: Region) => {
1055
+ if (refName === r.refName) {
1056
+ refNameMatched = true
1057
+ if (s === undefined) {
1058
+ s = r.start
1059
+ }
1060
+ if (e === undefined) {
1061
+ e = r.end
1062
+ }
1063
+ if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
1064
+ return true
1065
+ }
1066
+ s = start
1067
+ e = end
1068
+ }
1069
+ return false
1070
+ }
1071
+
1072
+ const lastIndex = findLastIndex(self.displayedRegions, predicate)
1073
+ let index
1074
+ while (index !== lastIndex) {
1075
+ try {
1076
+ const previousIndex: number | undefined = index
1077
+ index = self.displayedRegions
1078
+ .slice(previousIndex === undefined ? 0 : previousIndex + 1)
1079
+ .findIndex(predicate)
1080
+ if (previousIndex !== undefined) {
1081
+ index += previousIndex + 1
1082
+ }
1083
+ if (!refNameMatched) {
1084
+ throw new Error(
1085
+ `could not find a region with refName "${refName}"`,
1086
+ )
1087
+ }
1088
+ if (s === undefined) {
1089
+ throw new Error(
1090
+ `could not find a region with refName "${refName}" that contained an end position ${e}`,
1091
+ )
1092
+ }
1093
+ if (e === undefined) {
1094
+ throw new Error(
1095
+ `could not find a region with refName "${refName}" that contained a start position ${
1096
+ s + 1
1097
+ }`,
1098
+ )
1099
+ }
1100
+ if (index === -1) {
1101
+ throw new Error(
1102
+ `could not find a region that completely contained "${assembleLocString(
1103
+ firstLocation,
1104
+ )}"`,
1105
+ )
1106
+ }
1107
+ if (locations.length === 1) {
1108
+ const f = self.displayedRegions[index]
1109
+ this.moveTo(
1110
+ { index, offset: f.reversed ? f.end - e : s - f.start },
1111
+ { index, offset: f.reversed ? f.end - s : e - f.start },
1112
+ )
1113
+ return
1114
+ }
1115
+ let locationIndex = 0
1116
+ let locationStart = 0
1117
+ let locationEnd = 0
1118
+ for (
1119
+ locationIndex;
1120
+ locationIndex < locations.length;
1121
+ locationIndex++
1122
+ ) {
1123
+ const location = locations[locationIndex]
1124
+ const region = self.displayedRegions[index + locationIndex]
1125
+ locationStart = location.start || region.start
1126
+ locationEnd = location.end || region.end
1127
+ if (location.refName !== region.refName) {
1128
+ throw new Error(
1129
+ `Entered location ${assembleLocString(
1130
+ location,
1131
+ )} does not match with displayed regions`,
1132
+ )
1133
+ }
1134
+ }
1135
+ locationIndex -= 1
1136
+ const startDisplayedRegion = self.displayedRegions[index]
1137
+ const endDisplayedRegion =
1138
+ self.displayedRegions[index + locationIndex]
1139
+ this.moveTo(
1140
+ {
1141
+ index,
1142
+ offset: startDisplayedRegion.reversed
1143
+ ? startDisplayedRegion.end - e
1144
+ : s - startDisplayedRegion.start,
1145
+ },
1146
+ {
1147
+ index: index + locationIndex,
1148
+ offset: endDisplayedRegion.reversed
1149
+ ? endDisplayedRegion.end - locationStart
1150
+ : locationEnd - endDisplayedRegion.start,
1151
+ },
1152
+ )
1153
+ return
1154
+ } catch (error) {
1155
+ if (index === lastIndex) {
1156
+ throw error
1157
+ }
1158
+ }
1159
+ }
1160
+ },
1349
1161
  }))
1350
1162
  .views(self => ({
1351
1163
  rubberBandMenuItems(): MenuItem[] {
@@ -1355,20 +1167,56 @@ export function stateModelFactory(pluginManager: PluginManager) {
1355
1167
  icon: ZoomInIcon,
1356
1168
  onClick: () => {
1357
1169
  const { leftOffset, rightOffset } = self
1358
- if (leftOffset && rightOffset) {
1359
- self.moveTo(leftOffset, rightOffset)
1360
- }
1170
+ self.moveTo(leftOffset, rightOffset)
1361
1171
  },
1362
1172
  },
1363
1173
  {
1364
1174
  label: 'Get sequence',
1365
1175
  icon: MenuOpenIcon,
1366
- onClick: () => {
1367
- self.setSequenceDialogOpen(true)
1368
- },
1176
+ onClick: () => self.setGetSequenceDialogOpen(true),
1369
1177
  },
1370
1178
  ]
1371
1179
  },
1180
+
1181
+ bpToPx({
1182
+ refName,
1183
+ coord,
1184
+ regionNumber,
1185
+ }: {
1186
+ refName: string
1187
+ coord: number
1188
+ regionNumber?: number
1189
+ }) {
1190
+ return bpToPx({ refName, coord, regionNumber, self })
1191
+ },
1192
+
1193
+ /**
1194
+ * scrolls the view to center on the given bp. if that is not in any
1195
+ * of the displayed regions, does nothing
1196
+ * @param coord - basepair at which you want to center the view
1197
+ * @param refName - refName of the displayedRegion you are centering at
1198
+ * @param regionNumber - index of the displayedRegion
1199
+ */
1200
+ centerAt(coord: number, refName: string, regionNumber: number) {
1201
+ const centerPx = this.bpToPx({
1202
+ refName,
1203
+ coord,
1204
+ regionNumber,
1205
+ })
1206
+ if (centerPx) {
1207
+ self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2))
1208
+ }
1209
+ },
1210
+
1211
+ pxToBp(px: number) {
1212
+ return pxToBp(self, px)
1213
+ },
1214
+
1215
+ get centerLineInfo() {
1216
+ return self.displayedRegions.length
1217
+ ? this.pxToBp(self.width / 2)
1218
+ : undefined
1219
+ },
1372
1220
  }))
1373
1221
  }
1374
1222