@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.
- package/dist/BaseLinearDisplay/components/BaseLinearDisplay.js +28 -55
- package/dist/BaseLinearDisplay/components/BaseLinearDisplay.js.map +1 -1
- package/dist/BaseLinearDisplay/components/Block.js +17 -28
- package/dist/BaseLinearDisplay/components/Block.js.map +1 -1
- package/dist/BaseLinearDisplay/components/LinearBlocks.js +19 -21
- package/dist/BaseLinearDisplay/components/LinearBlocks.js.map +1 -1
- package/dist/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js +28 -48
- package/dist/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js.map +1 -1
- package/dist/BaseLinearDisplay/components/Tooltip.js +29 -58
- package/dist/BaseLinearDisplay/components/Tooltip.js.map +1 -1
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.js +242 -363
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.js.map +1 -1
- package/dist/BaseLinearDisplay/models/baseLinearDisplayConfigSchema.js +1 -1
- package/dist/BaseLinearDisplay/models/baseLinearDisplayConfigSchema.js.map +1 -1
- package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.js +77 -129
- package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.js.map +1 -1
- package/dist/LinearBareDisplay/configSchema.js +2 -2
- package/dist/LinearBareDisplay/configSchema.js.map +1 -1
- package/dist/LinearBareDisplay/model.js +13 -19
- package/dist/LinearBareDisplay/model.js.map +1 -1
- package/dist/LinearBasicDisplay/components/SetMaxHeight.js +14 -31
- package/dist/LinearBasicDisplay/components/SetMaxHeight.js.map +1 -1
- package/dist/LinearBasicDisplay/configSchema.js +3 -3
- package/dist/LinearBasicDisplay/configSchema.js.map +1 -1
- package/dist/LinearBasicDisplay/model.js +119 -147
- package/dist/LinearBasicDisplay/model.js.map +1 -1
- package/dist/LinearGenomeView/components/CenterLine.js +11 -12
- package/dist/LinearGenomeView/components/CenterLine.js.map +1 -1
- package/dist/LinearGenomeView/components/ExportSvgDialog.js +30 -96
- package/dist/LinearGenomeView/components/ExportSvgDialog.js.map +1 -1
- package/dist/LinearGenomeView/components/GetSequenceDialog.d.ts +9 -0
- package/dist/LinearGenomeView/components/GetSequenceDialog.js +172 -0
- package/dist/LinearGenomeView/components/GetSequenceDialog.js.map +1 -0
- package/dist/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
- package/dist/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +19 -21
- package/dist/LinearGenomeView/components/Gridlines.js.map +1 -0
- package/dist/LinearGenomeView/components/Header.js +26 -31
- package/dist/LinearGenomeView/components/Header.js.map +1 -1
- package/dist/LinearGenomeView/components/HelpDialog.js +10 -11
- package/dist/LinearGenomeView/components/HelpDialog.js.map +1 -1
- package/dist/LinearGenomeView/components/ImportForm.js +91 -154
- package/dist/LinearGenomeView/components/ImportForm.js.map +1 -1
- package/dist/LinearGenomeView/components/LinearGenomeView.js +20 -25
- package/dist/LinearGenomeView/components/LinearGenomeView.js.map +1 -1
- package/dist/LinearGenomeView/components/LinearGenomeViewSvg.js +86 -157
- package/dist/LinearGenomeView/components/LinearGenomeViewSvg.js.map +1 -1
- package/dist/LinearGenomeView/components/MiniControls.js +16 -32
- package/dist/LinearGenomeView/components/MiniControls.js.map +1 -1
- package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +0 -11
- package/dist/LinearGenomeView/components/OverviewRubberBand.js +55 -84
- package/dist/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
- package/dist/LinearGenomeView/components/OverviewScaleBar.js +94 -117
- package/dist/LinearGenomeView/components/OverviewScaleBar.js.map +1 -1
- package/dist/LinearGenomeView/components/RefNameAutocomplete.js +90 -172
- package/dist/LinearGenomeView/components/RefNameAutocomplete.js.map +1 -1
- package/dist/LinearGenomeView/components/RubberBand.js +51 -72
- package/dist/LinearGenomeView/components/RubberBand.js.map +1 -1
- package/dist/LinearGenomeView/components/Ruler.js +17 -18
- package/dist/LinearGenomeView/components/Ruler.js.map +1 -1
- package/dist/LinearGenomeView/components/ScaleBar.js +37 -58
- package/dist/LinearGenomeView/components/ScaleBar.js.map +1 -1
- package/dist/LinearGenomeView/components/SearchBox.js +69 -133
- package/dist/LinearGenomeView/components/SearchBox.js.map +1 -1
- package/dist/LinearGenomeView/components/SearchResultsDialog.js +32 -33
- package/dist/LinearGenomeView/components/SearchResultsDialog.js.map +1 -1
- package/dist/LinearGenomeView/components/{SequenceDialog.d.ts → SequenceSearchDialog.d.ts} +0 -0
- package/dist/LinearGenomeView/components/SequenceSearchDialog.js +104 -0
- package/dist/LinearGenomeView/components/SequenceSearchDialog.js.map +1 -0
- package/dist/LinearGenomeView/components/TrackContainer.d.ts +2 -1
- package/dist/LinearGenomeView/components/TrackContainer.js +36 -43
- package/dist/LinearGenomeView/components/TrackContainer.js.map +1 -1
- package/dist/LinearGenomeView/components/TrackLabel.js +50 -85
- package/dist/LinearGenomeView/components/TrackLabel.js.map +1 -1
- package/dist/LinearGenomeView/components/TracksContainer.js +33 -50
- package/dist/LinearGenomeView/components/TracksContainer.js.map +1 -1
- package/dist/LinearGenomeView/components/ZoomControls.js +15 -32
- package/dist/LinearGenomeView/components/ZoomControls.js.map +1 -1
- package/dist/LinearGenomeView/components/util.js +14 -87
- package/dist/LinearGenomeView/components/util.js.map +1 -1
- package/dist/LinearGenomeView/index.d.ts +66 -79
- package/dist/LinearGenomeView/index.js +534 -710
- package/dist/LinearGenomeView/index.js.map +1 -1
- package/dist/LinearGenomeView/util.js +17 -36
- package/dist/LinearGenomeView/util.js.map +1 -1
- package/dist/index.js +75 -146
- package/dist/index.js.map +1 -1
- package/esm/BaseLinearDisplay/components/BaseLinearDisplay.js +1 -1
- package/esm/BaseLinearDisplay/components/BaseLinearDisplay.js.map +1 -1
- package/esm/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js +1 -0
- package/esm/BaseLinearDisplay/components/ServerSideRenderedBlockContent.js.map +1 -1
- package/esm/LinearGenomeView/components/GetSequenceDialog.d.ts +9 -0
- package/esm/LinearGenomeView/components/{SequenceDialog.js → GetSequenceDialog.js} +5 -8
- package/esm/LinearGenomeView/components/GetSequenceDialog.js.map +1 -0
- 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/ImportForm.js +1 -0
- package/esm/LinearGenomeView/components/ImportForm.js.map +1 -1
- package/esm/LinearGenomeView/components/LinearGenomeView.js +3 -7
- package/esm/LinearGenomeView/components/LinearGenomeView.js.map +1 -1
- package/esm/LinearGenomeView/components/OverviewRubberBand.d.ts +0 -11
- package/esm/LinearGenomeView/components/OverviewRubberBand.js +16 -21
- package/esm/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
- package/esm/LinearGenomeView/components/OverviewScaleBar.js +0 -2
- package/esm/LinearGenomeView/components/OverviewScaleBar.js.map +1 -1
- package/esm/LinearGenomeView/components/RubberBand.js +0 -2
- package/esm/LinearGenomeView/components/RubberBand.js.map +1 -1
- package/esm/LinearGenomeView/components/SearchBox.js.map +1 -1
- package/esm/LinearGenomeView/components/{SequenceDialog.d.ts → SequenceSearchDialog.d.ts} +0 -0
- package/esm/LinearGenomeView/components/SequenceSearchDialog.js +76 -0
- package/esm/LinearGenomeView/components/SequenceSearchDialog.js.map +1 -0
- package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -1
- package/esm/LinearGenomeView/components/TrackContainer.js +15 -20
- package/esm/LinearGenomeView/components/TrackContainer.js.map +1 -1
- package/esm/LinearGenomeView/components/TrackLabel.js +22 -23
- package/esm/LinearGenomeView/components/TrackLabel.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 +66 -79
- package/esm/LinearGenomeView/index.js +268 -407
- package/esm/LinearGenomeView/index.js.map +1 -1
- package/package.json +3 -4
- package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +0 -1
- package/src/BaseLinearDisplay/components/ServerSideRenderedBlockContent.tsx +2 -1
- package/src/LinearGenomeView/components/{SequenceDialog.tsx → GetSequenceDialog.tsx} +5 -18
- package/src/LinearGenomeView/components/{VerticalGuides.tsx → Gridlines.tsx} +0 -0
- package/src/LinearGenomeView/components/ImportForm.tsx +1 -0
- package/src/LinearGenomeView/components/LinearGenomeView.tsx +4 -8
- package/src/LinearGenomeView/components/OverviewRubberBand.tsx +19 -34
- package/src/LinearGenomeView/components/OverviewScaleBar.tsx +0 -2
- package/src/LinearGenomeView/components/RubberBand.tsx +0 -2
- package/src/LinearGenomeView/components/SearchBox.tsx +1 -1
- package/src/LinearGenomeView/components/SequenceSearchDialog.tsx +165 -0
- package/src/LinearGenomeView/components/TrackContainer.tsx +18 -28
- package/src/LinearGenomeView/components/TrackLabel.tsx +60 -53
- package/src/LinearGenomeView/components/TracksContainer.tsx +2 -2
- package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +14 -17
- package/src/LinearGenomeView/index.test.ts +14 -36
- package/src/LinearGenomeView/index.tsx +389 -541
- package/dist/LinearGenomeView/components/SequenceDialog.js +0 -242
- package/dist/LinearGenomeView/components/SequenceDialog.js.map +0 -1
- package/dist/LinearGenomeView/components/VerticalGuides.js.map +0 -1
- package/esm/LinearGenomeView/components/SequenceDialog.js.map +0 -1
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
570
|
+
simView.setVolatileWidth(self.width)
|
|
571
|
+
simView.moveTo(leftOffset, rightOffset)
|
|
711
572
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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])
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|