@jbrowse/plugin-linear-genome-view 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
- package/dist/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
- package/dist/LinearGenomeView/components/Gridlines.js.map +1 -0
- package/dist/LinearGenomeView/components/OverviewRubberBand.js +13 -10
- package/dist/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
- package/dist/LinearGenomeView/components/RubberBand.js +0 -1
- package/dist/LinearGenomeView/components/RubberBand.js.map +1 -1
- package/dist/LinearGenomeView/components/SequenceDialog.js +1 -1
- package/dist/LinearGenomeView/components/SequenceDialog.js.map +1 -1
- package/dist/LinearGenomeView/components/TracksContainer.js +2 -2
- package/dist/LinearGenomeView/components/TracksContainer.js.map +1 -1
- package/dist/LinearGenomeView/index.d.ts +63 -76
- package/dist/LinearGenomeView/index.js +239 -365
- package/dist/LinearGenomeView/index.js.map +1 -1
- package/esm/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
- package/esm/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
- package/esm/LinearGenomeView/components/Gridlines.js.map +1 -0
- package/esm/LinearGenomeView/components/OverviewRubberBand.js +13 -10
- package/esm/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
- package/esm/LinearGenomeView/components/RubberBand.js +0 -1
- package/esm/LinearGenomeView/components/RubberBand.js.map +1 -1
- package/esm/LinearGenomeView/components/SequenceDialog.js +1 -1
- package/esm/LinearGenomeView/components/SequenceDialog.js.map +1 -1
- package/esm/LinearGenomeView/components/TracksContainer.js +2 -2
- package/esm/LinearGenomeView/components/TracksContainer.js.map +1 -1
- package/esm/LinearGenomeView/index.d.ts +63 -76
- package/esm/LinearGenomeView/index.js +243 -389
- package/esm/LinearGenomeView/index.js.map +1 -1
- package/package.json +3 -3
- package/src/LinearGenomeView/components/{VerticalGuides.tsx → Gridlines.tsx} +0 -0
- package/src/LinearGenomeView/components/OverviewRubberBand.tsx +14 -19
- package/src/LinearGenomeView/components/RubberBand.tsx +0 -1
- package/src/LinearGenomeView/components/SequenceDialog.tsx +1 -1
- package/src/LinearGenomeView/components/TracksContainer.tsx +2 -2
- package/src/LinearGenomeView/index.test.ts +13 -36
- package/src/LinearGenomeView/index.tsx +360 -519
- package/dist/LinearGenomeView/components/VerticalGuides.js.map +0 -1
- package/esm/LinearGenomeView/components/VerticalGuides.js.map +0 -1
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
measureText,
|
|
15
15
|
parseLocString,
|
|
16
16
|
springAnimate,
|
|
17
|
-
viewBpToPx,
|
|
18
17
|
} from '@jbrowse/core/util'
|
|
19
18
|
import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
|
|
20
19
|
import { BlockSet, BaseBlock } from '@jbrowse/core/util/blockTypes'
|
|
@@ -33,9 +32,10 @@ import {
|
|
|
33
32
|
} from 'mobx-state-tree'
|
|
34
33
|
|
|
35
34
|
import Base1DView from '@jbrowse/core/util/Base1DViewModel'
|
|
36
|
-
import
|
|
37
|
-
import clone from 'clone'
|
|
35
|
+
import { moveTo, pxToBp, bpToPx } from '@jbrowse/core/util/Base1DUtils'
|
|
38
36
|
import { saveAs } from 'file-saver'
|
|
37
|
+
import clone from 'clone'
|
|
38
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
39
39
|
|
|
40
40
|
// icons
|
|
41
41
|
import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons'
|
|
@@ -144,6 +144,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
144
144
|
const setting = localStorageGetItem('lgv-showCytobands')
|
|
145
145
|
return setting !== undefined && setting !== null ? !!+setting : true
|
|
146
146
|
}),
|
|
147
|
+
showGridlines: true,
|
|
147
148
|
}),
|
|
148
149
|
)
|
|
149
150
|
.volatile(() => ({
|
|
@@ -288,105 +289,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
288
289
|
}
|
|
289
290
|
},
|
|
290
291
|
|
|
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
292
|
getTrack(id: string) {
|
|
391
293
|
return self.tracks.find(t => t.configuration.trackId === id)
|
|
392
294
|
},
|
|
@@ -437,12 +339,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
437
339
|
|
|
438
340
|
return allActions
|
|
439
341
|
},
|
|
440
|
-
|
|
441
|
-
get centerLineInfo() {
|
|
442
|
-
return self.displayedRegions.length
|
|
443
|
-
? this.pxToBp(self.width / 2)
|
|
444
|
-
: undefined
|
|
445
|
-
},
|
|
446
342
|
}))
|
|
447
343
|
.actions(self => ({
|
|
448
344
|
setShowCytobands(flag: boolean) {
|
|
@@ -466,6 +362,9 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
466
362
|
toggleNoTracksActive() {
|
|
467
363
|
self.hideNoTracksActive = !self.hideNoTracksActive
|
|
468
364
|
},
|
|
365
|
+
toggleShowGridlines() {
|
|
366
|
+
self.showGridlines = !self.showGridlines
|
|
367
|
+
},
|
|
469
368
|
|
|
470
369
|
scrollTo(offsetPx: number) {
|
|
471
370
|
const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset)
|
|
@@ -651,429 +550,83 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
651
550
|
throw new Error(`invalid track selector type ${self.trackSelectorType}`)
|
|
652
551
|
},
|
|
653
552
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
}
|
|
553
|
+
/**
|
|
554
|
+
* Helper method for the fetchSequence.
|
|
555
|
+
* Retrieves the corresponding regions that were selected by the rubberband
|
|
556
|
+
*
|
|
557
|
+
* @param leftOffset - `object as {start, end, index, offset}`, offset = start of user drag
|
|
558
|
+
* @param rightOffset - `object as {start, end, index, offset}`, offset = end of user drag
|
|
559
|
+
* @returns array of Region[]
|
|
560
|
+
*/
|
|
561
|
+
getSelectedRegions(leftOffset?: BpOffset, rightOffset?: BpOffset) {
|
|
562
|
+
const snap = getSnapshot(self)
|
|
563
|
+
const simView = Base1DView.create({
|
|
564
|
+
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
|
|
565
|
+
...(snap as Omit<typeof self, symbol>),
|
|
566
|
+
interRegionPaddingWidth: self.interRegionPaddingWidth,
|
|
567
|
+
})
|
|
689
568
|
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
}
|
|
569
|
+
simView.setVolatileWidth(self.width)
|
|
570
|
+
simView.moveTo(leftOffset, rightOffset)
|
|
711
571
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
572
|
+
return simView.dynamicBlocks.contentBlocks.map(region => ({
|
|
573
|
+
...region,
|
|
574
|
+
start: Math.floor(region.start),
|
|
575
|
+
end: Math.ceil(region.end),
|
|
576
|
+
}))
|
|
577
|
+
},
|
|
718
578
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
])
|
|
724
|
-
const { start, end, parentRegion } = loc
|
|
579
|
+
// schedule something to be run after the next time displayedRegions is set
|
|
580
|
+
afterDisplayedRegionsSet(cb: Function) {
|
|
581
|
+
self.afterDisplayedRegionsSetCallbacks.push(cb)
|
|
582
|
+
},
|
|
725
583
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
} else {
|
|
732
|
-
this.setDisplayedRegions(
|
|
733
|
-
// @ts-ignore
|
|
734
|
-
locations.map(r => (r.start === undefined ? r.parentRegion : r)),
|
|
735
|
-
)
|
|
736
|
-
this.showAllRegions()
|
|
737
|
-
}
|
|
584
|
+
horizontalScroll(distance: number) {
|
|
585
|
+
const oldOffsetPx = self.offsetPx
|
|
586
|
+
// newOffsetPx is the actual offset after the scroll is clamped
|
|
587
|
+
const newOffsetPx = self.scrollTo(self.offsetPx + distance)
|
|
588
|
+
return newOffsetPx - oldOffsetPx
|
|
738
589
|
},
|
|
739
590
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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])
|
|
591
|
+
center() {
|
|
592
|
+
const centerBp = self.totalBp / 2
|
|
593
|
+
const centerPx = centerBp / self.bpPerPx
|
|
594
|
+
self.scrollTo(Math.round(centerPx - self.width / 2))
|
|
753
595
|
},
|
|
754
596
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
start,
|
|
760
|
-
end,
|
|
761
|
-
assemblyName = self.assemblyNames[0],
|
|
762
|
-
} = firstLocation
|
|
597
|
+
showAllRegions() {
|
|
598
|
+
self.zoomTo(self.maxBpPerPx)
|
|
599
|
+
this.center()
|
|
600
|
+
},
|
|
763
601
|
|
|
764
|
-
|
|
765
|
-
throw new Error(`start "${start + 1}" is greater than end "${end}"`)
|
|
766
|
-
}
|
|
602
|
+
showAllRegionsInAssembly(assemblyName?: string) {
|
|
767
603
|
const session = getSession(self)
|
|
768
604
|
const { assemblyManager } = session
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
605
|
+
if (!assemblyName) {
|
|
606
|
+
const assemblyNames = [
|
|
607
|
+
...new Set(
|
|
608
|
+
self.displayedRegions.map(region => region.assemblyName),
|
|
609
|
+
),
|
|
610
|
+
]
|
|
611
|
+
if (assemblyNames.length > 1) {
|
|
612
|
+
session.notify(
|
|
613
|
+
`Can't perform this with multiple assemblies currently`,
|
|
614
|
+
)
|
|
615
|
+
return
|
|
774
616
|
}
|
|
617
|
+
|
|
618
|
+
;[assemblyName] = assemblyNames
|
|
775
619
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
|
620
|
+
const assembly = assemblyManager.get(assemblyName)
|
|
621
|
+
if (assembly) {
|
|
622
|
+
const { regions } = assembly
|
|
623
|
+
if (regions) {
|
|
624
|
+
this.setDisplayedRegions(regions)
|
|
625
|
+
self.zoomTo(self.maxBpPerPx)
|
|
626
|
+
this.center()
|
|
793
627
|
}
|
|
794
|
-
return false
|
|
795
628
|
}
|
|
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
|
-
},
|
|
629
|
+
},
|
|
1077
630
|
|
|
1078
631
|
setDraggingTrackId(idx?: string) {
|
|
1079
632
|
self.draggingTrackId = idx
|
|
@@ -1220,6 +773,13 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
1220
773
|
checked: !self.hideNoTracksActive,
|
|
1221
774
|
onClick: self.toggleNoTracksActive,
|
|
1222
775
|
},
|
|
776
|
+
{
|
|
777
|
+
label: 'Show gridlines',
|
|
778
|
+
icon: VisibilityIcon,
|
|
779
|
+
type: 'checkbox',
|
|
780
|
+
checked: self.showGridlines,
|
|
781
|
+
onClick: self.toggleShowGridlines,
|
|
782
|
+
},
|
|
1223
783
|
{
|
|
1224
784
|
label: 'Track labels',
|
|
1225
785
|
icon: LabelIcon,
|
|
@@ -1346,6 +906,249 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
1346
906
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
1347
907
|
saveAs(blob, opts.filename || 'image.svg')
|
|
1348
908
|
},
|
|
909
|
+
/**
|
|
910
|
+
* offset is the base-pair-offset in the displayed region, index is the index of the
|
|
911
|
+
* displayed region in the linear genome view
|
|
912
|
+
*
|
|
913
|
+
* @param start - object as `{start, end, offset, index}`
|
|
914
|
+
* @param end - object as `{start, end, offset, index}`
|
|
915
|
+
*/
|
|
916
|
+
moveTo(start?: BpOffset, end?: BpOffset) {
|
|
917
|
+
moveTo(self, start, end)
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
navToLocString(locString: string, optAssemblyName?: string) {
|
|
921
|
+
const { assemblyNames } = self
|
|
922
|
+
const { assemblyManager } = getSession(self)
|
|
923
|
+
const { isValidRefName } = assemblyManager
|
|
924
|
+
const assemblyName = optAssemblyName || assemblyNames[0]
|
|
925
|
+
let parsedLocStrings
|
|
926
|
+
const inputs = locString
|
|
927
|
+
.split(/(\s+)/)
|
|
928
|
+
.map(f => f.trim())
|
|
929
|
+
.filter(f => !!f)
|
|
930
|
+
|
|
931
|
+
// first try interpreting as a whitespace-separated sequence of
|
|
932
|
+
// multiple locstrings
|
|
933
|
+
try {
|
|
934
|
+
parsedLocStrings = inputs.map(l =>
|
|
935
|
+
parseLocString(l, ref => isValidRefName(ref, assemblyName)),
|
|
936
|
+
)
|
|
937
|
+
} catch (e) {
|
|
938
|
+
// if this fails, try interpreting as a whitespace-separated refname,
|
|
939
|
+
// start, end if start and end are integer inputs
|
|
940
|
+
const [refName, start, end] = inputs
|
|
941
|
+
if (
|
|
942
|
+
`${e}`.match(/Unknown reference sequence/) &&
|
|
943
|
+
Number.isInteger(+start) &&
|
|
944
|
+
Number.isInteger(+end)
|
|
945
|
+
) {
|
|
946
|
+
parsedLocStrings = [
|
|
947
|
+
parseLocString(refName + ':' + start + '..' + end, ref =>
|
|
948
|
+
isValidRefName(ref, assemblyName),
|
|
949
|
+
),
|
|
950
|
+
]
|
|
951
|
+
} else {
|
|
952
|
+
throw e
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const locations = parsedLocStrings?.map(region => {
|
|
957
|
+
const asmName = region.assemblyName || assemblyName
|
|
958
|
+
const asm = assemblyManager.get(asmName)
|
|
959
|
+
const { refName } = region
|
|
960
|
+
if (!asm) {
|
|
961
|
+
throw new Error(`assembly ${asmName} not found`)
|
|
962
|
+
}
|
|
963
|
+
const { regions } = asm
|
|
964
|
+
if (!regions) {
|
|
965
|
+
throw new Error(`regions not loaded yet for ${asmName}`)
|
|
966
|
+
}
|
|
967
|
+
const canonicalRefName = asm.getCanonicalRefName(region.refName)
|
|
968
|
+
if (!canonicalRefName) {
|
|
969
|
+
throw new Error(`Could not find refName ${refName} in ${asm.name}`)
|
|
970
|
+
}
|
|
971
|
+
const parentRegion = regions.find(
|
|
972
|
+
region => region.refName === canonicalRefName,
|
|
973
|
+
)
|
|
974
|
+
if (!parentRegion) {
|
|
975
|
+
throw new Error(`Could not find refName ${refName} in ${asmName}`)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
...region,
|
|
980
|
+
assemblyName: asmName,
|
|
981
|
+
parentRegion,
|
|
982
|
+
}
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
if (locations.length === 1) {
|
|
986
|
+
const loc = locations[0]
|
|
987
|
+
self.setDisplayedRegions([
|
|
988
|
+
{ reversed: loc.reversed, ...loc.parentRegion },
|
|
989
|
+
])
|
|
990
|
+
const { start, end, parentRegion } = loc
|
|
991
|
+
|
|
992
|
+
this.navTo({
|
|
993
|
+
...loc,
|
|
994
|
+
start: clamp(start ?? 0, 0, parentRegion.end),
|
|
995
|
+
end: clamp(end ?? parentRegion.end, 0, parentRegion.end),
|
|
996
|
+
})
|
|
997
|
+
} else {
|
|
998
|
+
self.setDisplayedRegions(
|
|
999
|
+
// @ts-ignore
|
|
1000
|
+
locations.map(r => (r.start === undefined ? r.parentRegion : r)),
|
|
1001
|
+
)
|
|
1002
|
+
self.showAllRegions()
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Navigate to a location based on its refName and optionally start, end,
|
|
1008
|
+
* and assemblyName. Can handle if there are multiple displayedRegions
|
|
1009
|
+
* from same refName. Only navigates to a location if it is entirely
|
|
1010
|
+
* within a displayedRegion. Navigates to the first matching location
|
|
1011
|
+
* encountered.
|
|
1012
|
+
*
|
|
1013
|
+
* Throws an error if navigation was unsuccessful
|
|
1014
|
+
*
|
|
1015
|
+
* @param location - a proposed location to navigate to
|
|
1016
|
+
*/
|
|
1017
|
+
navTo(query: NavLocation) {
|
|
1018
|
+
this.navToMultiple([query])
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
navToMultiple(locations: NavLocation[]) {
|
|
1022
|
+
const firstLocation = locations[0]
|
|
1023
|
+
let { refName } = firstLocation
|
|
1024
|
+
const {
|
|
1025
|
+
start,
|
|
1026
|
+
end,
|
|
1027
|
+
assemblyName = self.assemblyNames[0],
|
|
1028
|
+
} = firstLocation
|
|
1029
|
+
|
|
1030
|
+
if (start !== undefined && end !== undefined && start > end) {
|
|
1031
|
+
throw new Error(`start "${start + 1}" is greater than end "${end}"`)
|
|
1032
|
+
}
|
|
1033
|
+
const session = getSession(self)
|
|
1034
|
+
const { assemblyManager } = session
|
|
1035
|
+
const assembly = assemblyManager.get(assemblyName)
|
|
1036
|
+
if (assembly) {
|
|
1037
|
+
const canonicalRefName = assembly.getCanonicalRefName(refName)
|
|
1038
|
+
if (canonicalRefName) {
|
|
1039
|
+
refName = canonicalRefName
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
let s = start
|
|
1043
|
+
let e = end
|
|
1044
|
+
let refNameMatched = false
|
|
1045
|
+
const predicate = (r: Region) => {
|
|
1046
|
+
if (refName === r.refName) {
|
|
1047
|
+
refNameMatched = true
|
|
1048
|
+
if (s === undefined) {
|
|
1049
|
+
s = r.start
|
|
1050
|
+
}
|
|
1051
|
+
if (e === undefined) {
|
|
1052
|
+
e = r.end
|
|
1053
|
+
}
|
|
1054
|
+
if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
|
|
1055
|
+
return true
|
|
1056
|
+
}
|
|
1057
|
+
s = start
|
|
1058
|
+
e = end
|
|
1059
|
+
}
|
|
1060
|
+
return false
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const lastIndex = findLastIndex(self.displayedRegions, predicate)
|
|
1064
|
+
let index
|
|
1065
|
+
while (index !== lastIndex) {
|
|
1066
|
+
try {
|
|
1067
|
+
const previousIndex: number | undefined = index
|
|
1068
|
+
index = self.displayedRegions
|
|
1069
|
+
.slice(previousIndex === undefined ? 0 : previousIndex + 1)
|
|
1070
|
+
.findIndex(predicate)
|
|
1071
|
+
if (previousIndex !== undefined) {
|
|
1072
|
+
index += previousIndex + 1
|
|
1073
|
+
}
|
|
1074
|
+
if (!refNameMatched) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`could not find a region with refName "${refName}"`,
|
|
1077
|
+
)
|
|
1078
|
+
}
|
|
1079
|
+
if (s === undefined) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`could not find a region with refName "${refName}" that contained an end position ${e}`,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
if (e === undefined) {
|
|
1085
|
+
throw new Error(
|
|
1086
|
+
`could not find a region with refName "${refName}" that contained a start position ${
|
|
1087
|
+
s + 1
|
|
1088
|
+
}`,
|
|
1089
|
+
)
|
|
1090
|
+
}
|
|
1091
|
+
if (index === -1) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
`could not find a region that completely contained "${assembleLocString(
|
|
1094
|
+
firstLocation,
|
|
1095
|
+
)}"`,
|
|
1096
|
+
)
|
|
1097
|
+
}
|
|
1098
|
+
if (locations.length === 1) {
|
|
1099
|
+
const f = self.displayedRegions[index]
|
|
1100
|
+
this.moveTo(
|
|
1101
|
+
{ index, offset: f.reversed ? f.end - e : s - f.start },
|
|
1102
|
+
{ index, offset: f.reversed ? f.end - s : e - f.start },
|
|
1103
|
+
)
|
|
1104
|
+
return
|
|
1105
|
+
}
|
|
1106
|
+
let locationIndex = 0
|
|
1107
|
+
let locationStart = 0
|
|
1108
|
+
let locationEnd = 0
|
|
1109
|
+
for (
|
|
1110
|
+
locationIndex;
|
|
1111
|
+
locationIndex < locations.length;
|
|
1112
|
+
locationIndex++
|
|
1113
|
+
) {
|
|
1114
|
+
const location = locations[locationIndex]
|
|
1115
|
+
const region = self.displayedRegions[index + locationIndex]
|
|
1116
|
+
locationStart = location.start || region.start
|
|
1117
|
+
locationEnd = location.end || region.end
|
|
1118
|
+
if (location.refName !== region.refName) {
|
|
1119
|
+
throw new Error(
|
|
1120
|
+
`Entered location ${assembleLocString(
|
|
1121
|
+
location,
|
|
1122
|
+
)} does not match with displayed regions`,
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
locationIndex -= 1
|
|
1127
|
+
const startDisplayedRegion = self.displayedRegions[index]
|
|
1128
|
+
const endDisplayedRegion =
|
|
1129
|
+
self.displayedRegions[index + locationIndex]
|
|
1130
|
+
this.moveTo(
|
|
1131
|
+
{
|
|
1132
|
+
index,
|
|
1133
|
+
offset: startDisplayedRegion.reversed
|
|
1134
|
+
? startDisplayedRegion.end - e
|
|
1135
|
+
: s - startDisplayedRegion.start,
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
index: index + locationIndex,
|
|
1139
|
+
offset: endDisplayedRegion.reversed
|
|
1140
|
+
? endDisplayedRegion.end - locationStart
|
|
1141
|
+
: locationEnd - endDisplayedRegion.start,
|
|
1142
|
+
},
|
|
1143
|
+
)
|
|
1144
|
+
return
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (index === lastIndex) {
|
|
1147
|
+
throw error
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1349
1152
|
}))
|
|
1350
1153
|
.views(self => ({
|
|
1351
1154
|
rubberBandMenuItems(): MenuItem[] {
|
|
@@ -1355,9 +1158,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
1355
1158
|
icon: ZoomInIcon,
|
|
1356
1159
|
onClick: () => {
|
|
1357
1160
|
const { leftOffset, rightOffset } = self
|
|
1358
|
-
|
|
1359
|
-
self.moveTo(leftOffset, rightOffset)
|
|
1360
|
-
}
|
|
1161
|
+
self.moveTo(leftOffset, rightOffset)
|
|
1361
1162
|
},
|
|
1362
1163
|
},
|
|
1363
1164
|
{
|
|
@@ -1369,6 +1170,46 @@ export function stateModelFactory(pluginManager: PluginManager) {
|
|
|
1369
1170
|
},
|
|
1370
1171
|
]
|
|
1371
1172
|
},
|
|
1173
|
+
|
|
1174
|
+
bpToPx({
|
|
1175
|
+
refName,
|
|
1176
|
+
coord,
|
|
1177
|
+
regionNumber,
|
|
1178
|
+
}: {
|
|
1179
|
+
refName: string
|
|
1180
|
+
coord: number
|
|
1181
|
+
regionNumber?: number
|
|
1182
|
+
}) {
|
|
1183
|
+
return bpToPx({ refName, coord, regionNumber, self })
|
|
1184
|
+
},
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* scrolls the view to center on the given bp. if that is not in any
|
|
1188
|
+
* of the displayed regions, does nothing
|
|
1189
|
+
* @param coord - basepair at which you want to center the view
|
|
1190
|
+
* @param refName - refName of the displayedRegion you are centering at
|
|
1191
|
+
* @param regionNumber - index of the displayedRegion
|
|
1192
|
+
*/
|
|
1193
|
+
centerAt(coord: number, refName: string, regionNumber: number) {
|
|
1194
|
+
const centerPx = this.bpToPx({
|
|
1195
|
+
refName,
|
|
1196
|
+
coord,
|
|
1197
|
+
regionNumber,
|
|
1198
|
+
})
|
|
1199
|
+
if (centerPx) {
|
|
1200
|
+
self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2))
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
|
|
1204
|
+
pxToBp(px: number) {
|
|
1205
|
+
return pxToBp(self, px)
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
get centerLineInfo() {
|
|
1209
|
+
return self.displayedRegions.length
|
|
1210
|
+
? this.pxToBp(self.width / 2)
|
|
1211
|
+
: undefined
|
|
1212
|
+
},
|
|
1372
1213
|
}))
|
|
1373
1214
|
}
|
|
1374
1215
|
|