@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.
Files changed (38) hide show
  1. package/dist/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
  2. package/dist/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
  3. package/dist/LinearGenomeView/components/Gridlines.js.map +1 -0
  4. package/dist/LinearGenomeView/components/OverviewRubberBand.js +13 -10
  5. package/dist/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
  6. package/dist/LinearGenomeView/components/RubberBand.js +0 -1
  7. package/dist/LinearGenomeView/components/RubberBand.js.map +1 -1
  8. package/dist/LinearGenomeView/components/SequenceDialog.js +1 -1
  9. package/dist/LinearGenomeView/components/SequenceDialog.js.map +1 -1
  10. package/dist/LinearGenomeView/components/TracksContainer.js +2 -2
  11. package/dist/LinearGenomeView/components/TracksContainer.js.map +1 -1
  12. package/dist/LinearGenomeView/index.d.ts +63 -76
  13. package/dist/LinearGenomeView/index.js +239 -365
  14. package/dist/LinearGenomeView/index.js.map +1 -1
  15. package/esm/LinearGenomeView/components/{VerticalGuides.d.ts → Gridlines.d.ts} +0 -0
  16. package/esm/LinearGenomeView/components/{VerticalGuides.js → Gridlines.js} +1 -1
  17. package/esm/LinearGenomeView/components/Gridlines.js.map +1 -0
  18. package/esm/LinearGenomeView/components/OverviewRubberBand.js +13 -10
  19. package/esm/LinearGenomeView/components/OverviewRubberBand.js.map +1 -1
  20. package/esm/LinearGenomeView/components/RubberBand.js +0 -1
  21. package/esm/LinearGenomeView/components/RubberBand.js.map +1 -1
  22. package/esm/LinearGenomeView/components/SequenceDialog.js +1 -1
  23. package/esm/LinearGenomeView/components/SequenceDialog.js.map +1 -1
  24. package/esm/LinearGenomeView/components/TracksContainer.js +2 -2
  25. package/esm/LinearGenomeView/components/TracksContainer.js.map +1 -1
  26. package/esm/LinearGenomeView/index.d.ts +63 -76
  27. package/esm/LinearGenomeView/index.js +243 -389
  28. package/esm/LinearGenomeView/index.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/LinearGenomeView/components/{VerticalGuides.tsx → Gridlines.tsx} +0 -0
  31. package/src/LinearGenomeView/components/OverviewRubberBand.tsx +14 -19
  32. package/src/LinearGenomeView/components/RubberBand.tsx +0 -1
  33. package/src/LinearGenomeView/components/SequenceDialog.tsx +1 -1
  34. package/src/LinearGenomeView/components/TracksContainer.tsx +2 -2
  35. package/src/LinearGenomeView/index.test.ts +13 -36
  36. package/src/LinearGenomeView/index.tsx +360 -519
  37. package/dist/LinearGenomeView/components/VerticalGuides.js.map +0 -1
  38. package/esm/LinearGenomeView/components/VerticalGuides.js.map +0 -1
@@ -2,15 +2,16 @@ import { getConf } from '@jbrowse/core/configuration';
2
2
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
3
3
  import { ElementId, Region as MUIRegion } from '@jbrowse/core/util/types/mst';
4
4
  import { ReturnToImportFormDialog } from '@jbrowse/core/ui';
5
- import { assembleLocString, clamp, findLastIndex, getContainingView, getSession, isViewContainer, isSessionModelWithWidgets, measureText, parseLocString, springAnimate, viewBpToPx, } from '@jbrowse/core/util';
5
+ import { assembleLocString, clamp, findLastIndex, getContainingView, getSession, isViewContainer, isSessionModelWithWidgets, measureText, parseLocString, springAnimate, } from '@jbrowse/core/util';
6
6
  import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
7
7
  import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
8
8
  import { getParentRenderProps } from '@jbrowse/core/util/tracks';
9
9
  import { transaction, autorun } from 'mobx';
10
10
  import { addDisposer, cast, getSnapshot, getRoot, resolveIdentifier, types, } from 'mobx-state-tree';
11
11
  import Base1DView from '@jbrowse/core/util/Base1DViewModel';
12
- import clone from 'clone';
12
+ import { moveTo, pxToBp, bpToPx } from '@jbrowse/core/util/Base1DUtils';
13
13
  import { saveAs } from 'file-saver';
14
+ import clone from 'clone';
14
15
  // icons
15
16
  import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons';
16
17
  import SyncAltIcon from '@mui/icons-material/SyncAlt';
@@ -74,6 +75,7 @@ export function stateModelFactory(pluginManager) {
74
75
  const setting = localStorageGetItem('lgv-showCytobands');
75
76
  return setting !== undefined && setting !== null ? !!+setting : true;
76
77
  }),
78
+ showGridlines: true,
77
79
  }))
78
80
  .volatile(() => ({
79
81
  volatileWidth: undefined,
@@ -197,91 +199,6 @@ export function stateModelFactory(pluginManager) {
197
199
  tracks: self.tracks,
198
200
  };
199
201
  },
200
- bpToPx({ refName, coord, regionNumber, }) {
201
- return viewBpToPx({ refName, coord, regionNumber, self });
202
- },
203
- /**
204
- *
205
- * @param px - px in the view area, return value is the displayed regions
206
- * @returns BpOffset of the displayed region that it lands in
207
- */
208
- pxToBp(px) {
209
- let bpSoFar = 0;
210
- const bp = (self.offsetPx + px) * self.bpPerPx;
211
- const n = self.displayedRegions.length;
212
- if (bp < 0) {
213
- const region = self.displayedRegions[0];
214
- const offset = bp;
215
- const snap = getSnapshot(region);
216
- return {
217
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
218
- ...snap,
219
- oob: true,
220
- coord: region.reversed
221
- ? Math.floor(region.end - offset) + 1
222
- : Math.floor(region.start + offset) + 1,
223
- offset,
224
- index: 0,
225
- };
226
- }
227
- const interRegionPaddingBp = self.interRegionPaddingWidth * self.bpPerPx;
228
- const minimumBlockBp = self.minimumBlockWidth * self.bpPerPx;
229
- for (let index = 0; index < self.displayedRegions.length; index += 1) {
230
- const region = self.displayedRegions[index];
231
- const len = region.end - region.start;
232
- const offset = bp - bpSoFar;
233
- if (len + bpSoFar > bp && bpSoFar <= bp) {
234
- const snap = getSnapshot(region);
235
- return {
236
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
237
- ...snap,
238
- oob: false,
239
- offset,
240
- coord: region.reversed
241
- ? Math.floor(region.end - offset) + 1
242
- : Math.floor(region.start + offset) + 1,
243
- index,
244
- };
245
- }
246
- // add the interRegionPaddingWidth if the boundary is in the screen
247
- // e.g. offset>0 && offset<width
248
- if (region.end - region.start > minimumBlockBp &&
249
- offset / self.bpPerPx > 0 &&
250
- offset / self.bpPerPx < self.width) {
251
- bpSoFar += len + interRegionPaddingBp;
252
- }
253
- else {
254
- bpSoFar += len;
255
- }
256
- }
257
- if (bp >= bpSoFar) {
258
- const region = self.displayedRegions[n - 1];
259
- const len = region.end - region.start;
260
- const offset = bp - bpSoFar + len;
261
- const snap = getSnapshot(region);
262
- return {
263
- // xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
264
- ...snap,
265
- oob: true,
266
- offset,
267
- coord: region.reversed
268
- ? Math.floor(region.end - offset) + 1
269
- : Math.floor(region.start + offset) + 1,
270
- index: n - 1,
271
- };
272
- }
273
- return {
274
- coord: 0,
275
- index: 0,
276
- refName: '',
277
- oob: true,
278
- assemblyName: '',
279
- offset: 0,
280
- start: 0,
281
- end: 0,
282
- reversed: false,
283
- };
284
- },
285
202
  getTrack(id) {
286
203
  return self.tracks.find(t => t.configuration.trackId === id);
287
204
  },
@@ -326,11 +243,6 @@ export function stateModelFactory(pluginManager) {
326
243
  });
327
244
  return allActions;
328
245
  },
329
- get centerLineInfo() {
330
- return self.displayedRegions.length
331
- ? this.pxToBp(self.width / 2)
332
- : undefined;
333
- },
334
246
  }))
335
247
  .actions(self => ({
336
248
  setShowCytobands(flag) {
@@ -352,6 +264,9 @@ export function stateModelFactory(pluginManager) {
352
264
  toggleNoTracksActive() {
353
265
  self.hideNoTracksActive = !self.hideNoTracksActive;
354
266
  },
267
+ toggleShowGridlines() {
268
+ self.showGridlines = !self.showGridlines;
269
+ },
355
270
  scrollTo(offsetPx) {
356
271
  const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset);
357
272
  self.offsetPx = newOffsetPx;
@@ -498,233 +413,6 @@ export function stateModelFactory(pluginManager) {
498
413
  }
499
414
  throw new Error(`invalid track selector type ${self.trackSelectorType}`);
500
415
  },
501
- navToLocString(locString, optAssemblyName) {
502
- const { assemblyNames } = self;
503
- const { assemblyManager } = getSession(self);
504
- const { isValidRefName } = assemblyManager;
505
- const assemblyName = optAssemblyName || assemblyNames[0];
506
- let parsedLocStrings;
507
- const inputs = locString
508
- .split(/(\s+)/)
509
- .map(f => f.trim())
510
- .filter(f => !!f);
511
- // first try interpreting as a whitespace-separated sequence of
512
- // multiple locstrings
513
- try {
514
- parsedLocStrings = inputs.map(l => parseLocString(l, ref => isValidRefName(ref, assemblyName)));
515
- }
516
- catch (e) {
517
- // if this fails, try interpreting as a whitespace-separated refname,
518
- // start, end if start and end are integer inputs
519
- const [refName, start, end] = inputs;
520
- if (`${e}`.match(/Unknown reference sequence/) &&
521
- Number.isInteger(+start) &&
522
- Number.isInteger(+end)) {
523
- parsedLocStrings = [
524
- parseLocString(refName + ':' + start + '..' + end, ref => isValidRefName(ref, assemblyName)),
525
- ];
526
- }
527
- else {
528
- throw e;
529
- }
530
- }
531
- const locations = parsedLocStrings === null || parsedLocStrings === void 0 ? void 0 : parsedLocStrings.map(region => {
532
- const asmName = region.assemblyName || assemblyName;
533
- const asm = assemblyManager.get(asmName);
534
- const { refName } = region;
535
- if (!asm) {
536
- throw new Error(`assembly ${asmName} not found`);
537
- }
538
- const { regions } = asm;
539
- if (!regions) {
540
- throw new Error(`regions not loaded yet for ${asmName}`);
541
- }
542
- const canonicalRefName = asm.getCanonicalRefName(region.refName);
543
- if (!canonicalRefName) {
544
- throw new Error(`Could not find refName ${refName} in ${asm.name}`);
545
- }
546
- const parentRegion = regions.find(region => region.refName === canonicalRefName);
547
- if (!parentRegion) {
548
- throw new Error(`Could not find refName ${refName} in ${asmName}`);
549
- }
550
- return {
551
- ...region,
552
- assemblyName: asmName,
553
- parentRegion,
554
- };
555
- });
556
- if (locations.length === 1) {
557
- const loc = locations[0];
558
- this.setDisplayedRegions([
559
- { reversed: loc.reversed, ...loc.parentRegion },
560
- ]);
561
- const { start, end, parentRegion } = loc;
562
- this.navTo({
563
- ...loc,
564
- start: clamp(start !== null && start !== void 0 ? start : 0, 0, parentRegion.end),
565
- end: clamp(end !== null && end !== void 0 ? end : parentRegion.end, 0, parentRegion.end),
566
- });
567
- }
568
- else {
569
- this.setDisplayedRegions(
570
- // @ts-ignore
571
- locations.map(r => (r.start === undefined ? r.parentRegion : r)));
572
- this.showAllRegions();
573
- }
574
- },
575
- /**
576
- * Navigate to a location based on its refName and optionally start, end,
577
- * and assemblyName. Can handle if there are multiple displayedRegions
578
- * from same refName. Only navigates to a location if it is entirely
579
- * within a displayedRegion. Navigates to the first matching location
580
- * encountered.
581
- *
582
- * Throws an error if navigation was unsuccessful
583
- *
584
- * @param location - a proposed location to navigate to
585
- */
586
- navTo(query) {
587
- this.navToMultiple([query]);
588
- },
589
- navToMultiple(locations) {
590
- const firstLocation = locations[0];
591
- let { refName } = firstLocation;
592
- const { start, end, assemblyName = self.assemblyNames[0], } = firstLocation;
593
- if (start !== undefined && end !== undefined && start > end) {
594
- throw new Error(`start "${start + 1}" is greater than end "${end}"`);
595
- }
596
- const session = getSession(self);
597
- const { assemblyManager } = session;
598
- const assembly = assemblyManager.get(assemblyName);
599
- if (assembly) {
600
- const canonicalRefName = assembly.getCanonicalRefName(refName);
601
- if (canonicalRefName) {
602
- refName = canonicalRefName;
603
- }
604
- }
605
- let s = start;
606
- let e = end;
607
- let refNameMatched = false;
608
- const predicate = (r) => {
609
- if (refName === r.refName) {
610
- refNameMatched = true;
611
- if (s === undefined) {
612
- s = r.start;
613
- }
614
- if (e === undefined) {
615
- e = r.end;
616
- }
617
- if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
618
- return true;
619
- }
620
- s = start;
621
- e = end;
622
- }
623
- return false;
624
- };
625
- const lastIndex = findLastIndex(self.displayedRegions, predicate);
626
- let index;
627
- while (index !== lastIndex) {
628
- try {
629
- const previousIndex = index;
630
- index = self.displayedRegions
631
- .slice(previousIndex === undefined ? 0 : previousIndex + 1)
632
- .findIndex(predicate);
633
- if (previousIndex !== undefined) {
634
- index += previousIndex + 1;
635
- }
636
- if (!refNameMatched) {
637
- throw new Error(`could not find a region with refName "${refName}"`);
638
- }
639
- if (s === undefined) {
640
- throw new Error(`could not find a region with refName "${refName}" that contained an end position ${e}`);
641
- }
642
- if (e === undefined) {
643
- throw new Error(`could not find a region with refName "${refName}" that contained a start position ${s + 1}`);
644
- }
645
- if (index === -1) {
646
- throw new Error(`could not find a region that completely contained "${assembleLocString(firstLocation)}"`);
647
- }
648
- if (locations.length === 1) {
649
- const f = self.displayedRegions[index];
650
- this.moveTo({ index, offset: f.reversed ? f.end - e : s - f.start }, { index, offset: f.reversed ? f.end - s : e - f.start });
651
- return;
652
- }
653
- let locationIndex = 0;
654
- let locationStart = 0;
655
- let locationEnd = 0;
656
- for (locationIndex; locationIndex < locations.length; locationIndex++) {
657
- const location = locations[locationIndex];
658
- const region = self.displayedRegions[index + locationIndex];
659
- locationStart = location.start || region.start;
660
- locationEnd = location.end || region.end;
661
- if (location.refName !== region.refName) {
662
- throw new Error(`Entered location ${assembleLocString(location)} does not match with displayed regions`);
663
- }
664
- }
665
- locationIndex -= 1;
666
- const startDisplayedRegion = self.displayedRegions[index];
667
- const endDisplayedRegion = self.displayedRegions[index + locationIndex];
668
- this.moveTo({
669
- index,
670
- offset: startDisplayedRegion.reversed
671
- ? startDisplayedRegion.end - e
672
- : s - startDisplayedRegion.start,
673
- }, {
674
- index: index + locationIndex,
675
- offset: endDisplayedRegion.reversed
676
- ? endDisplayedRegion.end - locationStart
677
- : locationEnd - endDisplayedRegion.start,
678
- });
679
- return;
680
- }
681
- catch (error) {
682
- if (index === lastIndex) {
683
- throw error;
684
- }
685
- }
686
- }
687
- },
688
- /**
689
- * Navigate to a location based on user clicking and dragging on the
690
- * overview scale bar to select a region to zoom into.
691
- * Can handle if there are multiple displayedRegions from same refName.
692
- * Only navigates to a location if it is entirely within a displayedRegion.
693
- *
694
- * @param leftPx- `object as {start, end, index, offset}`, offset = start of user drag
695
- * @param rightPx- `object as {start, end, index, offset}`, offset = end of user drag
696
- */
697
- zoomToDisplayedRegions(leftPx, rightPx) {
698
- if (leftPx === undefined || rightPx === undefined) {
699
- return;
700
- }
701
- const singleRefSeq = leftPx.refName === rightPx.refName && leftPx.index === rightPx.index;
702
- // zooming into one displayed Region
703
- if ((singleRefSeq && rightPx.offset < leftPx.offset) ||
704
- leftPx.index > rightPx.index) {
705
- ;
706
- [leftPx, rightPx] = [rightPx, leftPx];
707
- }
708
- const startOffset = {
709
- start: leftPx.start,
710
- end: leftPx.end,
711
- index: leftPx.index,
712
- offset: leftPx.offset,
713
- };
714
- const endOffset = {
715
- start: rightPx.start,
716
- end: rightPx.end,
717
- index: rightPx.index,
718
- offset: rightPx.offset,
719
- };
720
- if (startOffset && endOffset) {
721
- this.moveTo(startOffset, endOffset);
722
- }
723
- else {
724
- const session = getSession(self);
725
- session.notify('No regions found to navigate to', 'warning');
726
- }
727
- },
728
416
  /**
729
417
  * Helper method for the fetchSequence.
730
418
  * Retrieves the corresponding regions that were selected by the rubberband
@@ -741,7 +429,7 @@ export function stateModelFactory(pluginManager) {
741
429
  interRegionPaddingWidth: self.interRegionPaddingWidth,
742
430
  });
743
431
  simView.setVolatileWidth(self.width);
744
- simView.zoomToDisplayedRegions(leftOffset, rightOffset);
432
+ simView.moveTo(leftOffset, rightOffset);
745
433
  return simView.dynamicBlocks.contentBlocks.map(region => ({
746
434
  ...region,
747
435
  start: Math.floor(region.start),
@@ -752,80 +440,16 @@ export function stateModelFactory(pluginManager) {
752
440
  afterDisplayedRegionsSet(cb) {
753
441
  self.afterDisplayedRegionsSetCallbacks.push(cb);
754
442
  },
755
- /**
756
- * offset is the base-pair-offset in the displayed region, index is the index of the
757
- * displayed region in the linear genome view
758
- *
759
- * @param start - object as `{start, end, offset, index}`
760
- * @param end - object as `{start, end, offset, index}`
761
- */
762
- moveTo(start, end) {
763
- // find locations in the modellist
764
- let bpSoFar = 0;
765
- if (start.index === end.index) {
766
- bpSoFar += end.offset - start.offset;
767
- }
768
- else {
769
- const s = self.displayedRegions[start.index];
770
- bpSoFar += s.end - s.start - start.offset;
771
- if (end.index - start.index >= 2) {
772
- for (let i = start.index + 1; i < end.index; i += 1) {
773
- bpSoFar +=
774
- self.displayedRegions[i].end - self.displayedRegions[i].start;
775
- }
776
- }
777
- bpSoFar += end.offset;
778
- }
779
- const targetBpPerPx = bpSoFar /
780
- (self.width -
781
- self.interRegionPaddingWidth * (end.index - start.index));
782
- const newBpPerPx = self.zoomTo(targetBpPerPx);
783
- // If our target bpPerPx was smaller than the allowed minBpPerPx, adjust
784
- // the scroll so the requested range is in the middle of the screen
785
- let extraBp = 0;
786
- if (targetBpPerPx < newBpPerPx) {
787
- extraBp = ((newBpPerPx - targetBpPerPx) * self.width) / 2;
788
- }
789
- let bpToStart = -extraBp;
790
- for (let i = 0; i < self.displayedRegions.length; i += 1) {
791
- const region = self.displayedRegions[i];
792
- if (start.index === i) {
793
- bpToStart += start.offset;
794
- break;
795
- }
796
- else {
797
- bpToStart += region.end - region.start;
798
- }
799
- }
800
- self.scrollTo(Math.round(bpToStart / self.bpPerPx) +
801
- self.interRegionPaddingWidth * start.index);
802
- },
803
443
  horizontalScroll(distance) {
804
444
  const oldOffsetPx = self.offsetPx;
805
445
  // newOffsetPx is the actual offset after the scroll is clamped
806
446
  const newOffsetPx = self.scrollTo(self.offsetPx + distance);
807
447
  return newOffsetPx - oldOffsetPx;
808
448
  },
809
- /**
810
- * scrolls the view to center on the given bp. if that is not in any
811
- * of the displayed regions, does nothing
812
- * @param bp - basepair at which you want to center the view
813
- * @param refName - refName of the displayedRegion you are centering at
814
- * @param regionIndex - index of the displayedRegion
815
- */
816
- centerAt(bp, refName, regionIndex) {
817
- const centerPx = self.bpToPx({
818
- refName,
819
- coord: bp,
820
- regionNumber: regionIndex,
821
- });
822
- if (centerPx) {
823
- self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2));
824
- }
825
- },
826
449
  center() {
827
450
  const centerBp = self.totalBp / 2;
828
- self.scrollTo(Math.round(centerBp / self.bpPerPx - self.width / 2));
451
+ const centerPx = centerBp / self.bpPerPx;
452
+ self.scrollTo(Math.round(centerPx - self.width / 2));
829
453
  },
830
454
  showAllRegions() {
831
455
  self.zoomTo(self.maxBpPerPx);
@@ -981,6 +605,13 @@ export function stateModelFactory(pluginManager) {
981
605
  checked: !self.hideNoTracksActive,
982
606
  onClick: self.toggleNoTracksActive,
983
607
  },
608
+ {
609
+ label: 'Show gridlines',
610
+ icon: VisibilityIcon,
611
+ type: 'checkbox',
612
+ checked: self.showGridlines,
613
+ onClick: self.toggleShowGridlines,
614
+ },
984
615
  {
985
616
  label: 'Track labels',
986
617
  icon: LabelIcon,
@@ -1094,6 +725,203 @@ export function stateModelFactory(pluginManager) {
1094
725
  const blob = new Blob([html], { type: 'image/svg+xml' });
1095
726
  saveAs(blob, opts.filename || 'image.svg');
1096
727
  },
728
+ /**
729
+ * offset is the base-pair-offset in the displayed region, index is the index of the
730
+ * displayed region in the linear genome view
731
+ *
732
+ * @param start - object as `{start, end, offset, index}`
733
+ * @param end - object as `{start, end, offset, index}`
734
+ */
735
+ moveTo(start, end) {
736
+ moveTo(self, start, end);
737
+ },
738
+ navToLocString(locString, optAssemblyName) {
739
+ const { assemblyNames } = self;
740
+ const { assemblyManager } = getSession(self);
741
+ const { isValidRefName } = assemblyManager;
742
+ const assemblyName = optAssemblyName || assemblyNames[0];
743
+ let parsedLocStrings;
744
+ const inputs = locString
745
+ .split(/(\s+)/)
746
+ .map(f => f.trim())
747
+ .filter(f => !!f);
748
+ // first try interpreting as a whitespace-separated sequence of
749
+ // multiple locstrings
750
+ try {
751
+ parsedLocStrings = inputs.map(l => parseLocString(l, ref => isValidRefName(ref, assemblyName)));
752
+ }
753
+ catch (e) {
754
+ // if this fails, try interpreting as a whitespace-separated refname,
755
+ // start, end if start and end are integer inputs
756
+ const [refName, start, end] = inputs;
757
+ if (`${e}`.match(/Unknown reference sequence/) &&
758
+ Number.isInteger(+start) &&
759
+ Number.isInteger(+end)) {
760
+ parsedLocStrings = [
761
+ parseLocString(refName + ':' + start + '..' + end, ref => isValidRefName(ref, assemblyName)),
762
+ ];
763
+ }
764
+ else {
765
+ throw e;
766
+ }
767
+ }
768
+ const locations = parsedLocStrings === null || parsedLocStrings === void 0 ? void 0 : parsedLocStrings.map(region => {
769
+ const asmName = region.assemblyName || assemblyName;
770
+ const asm = assemblyManager.get(asmName);
771
+ const { refName } = region;
772
+ if (!asm) {
773
+ throw new Error(`assembly ${asmName} not found`);
774
+ }
775
+ const { regions } = asm;
776
+ if (!regions) {
777
+ throw new Error(`regions not loaded yet for ${asmName}`);
778
+ }
779
+ const canonicalRefName = asm.getCanonicalRefName(region.refName);
780
+ if (!canonicalRefName) {
781
+ throw new Error(`Could not find refName ${refName} in ${asm.name}`);
782
+ }
783
+ const parentRegion = regions.find(region => region.refName === canonicalRefName);
784
+ if (!parentRegion) {
785
+ throw new Error(`Could not find refName ${refName} in ${asmName}`);
786
+ }
787
+ return {
788
+ ...region,
789
+ assemblyName: asmName,
790
+ parentRegion,
791
+ };
792
+ });
793
+ if (locations.length === 1) {
794
+ const loc = locations[0];
795
+ self.setDisplayedRegions([
796
+ { reversed: loc.reversed, ...loc.parentRegion },
797
+ ]);
798
+ const { start, end, parentRegion } = loc;
799
+ this.navTo({
800
+ ...loc,
801
+ start: clamp(start !== null && start !== void 0 ? start : 0, 0, parentRegion.end),
802
+ end: clamp(end !== null && end !== void 0 ? end : parentRegion.end, 0, parentRegion.end),
803
+ });
804
+ }
805
+ else {
806
+ self.setDisplayedRegions(
807
+ // @ts-ignore
808
+ locations.map(r => (r.start === undefined ? r.parentRegion : r)));
809
+ self.showAllRegions();
810
+ }
811
+ },
812
+ /**
813
+ * Navigate to a location based on its refName and optionally start, end,
814
+ * and assemblyName. Can handle if there are multiple displayedRegions
815
+ * from same refName. Only navigates to a location if it is entirely
816
+ * within a displayedRegion. Navigates to the first matching location
817
+ * encountered.
818
+ *
819
+ * Throws an error if navigation was unsuccessful
820
+ *
821
+ * @param location - a proposed location to navigate to
822
+ */
823
+ navTo(query) {
824
+ this.navToMultiple([query]);
825
+ },
826
+ navToMultiple(locations) {
827
+ const firstLocation = locations[0];
828
+ let { refName } = firstLocation;
829
+ const { start, end, assemblyName = self.assemblyNames[0], } = firstLocation;
830
+ if (start !== undefined && end !== undefined && start > end) {
831
+ throw new Error(`start "${start + 1}" is greater than end "${end}"`);
832
+ }
833
+ const session = getSession(self);
834
+ const { assemblyManager } = session;
835
+ const assembly = assemblyManager.get(assemblyName);
836
+ if (assembly) {
837
+ const canonicalRefName = assembly.getCanonicalRefName(refName);
838
+ if (canonicalRefName) {
839
+ refName = canonicalRefName;
840
+ }
841
+ }
842
+ let s = start;
843
+ let e = end;
844
+ let refNameMatched = false;
845
+ const predicate = (r) => {
846
+ if (refName === r.refName) {
847
+ refNameMatched = true;
848
+ if (s === undefined) {
849
+ s = r.start;
850
+ }
851
+ if (e === undefined) {
852
+ e = r.end;
853
+ }
854
+ if (s >= r.start && s <= r.end && e <= r.end && e >= r.start) {
855
+ return true;
856
+ }
857
+ s = start;
858
+ e = end;
859
+ }
860
+ return false;
861
+ };
862
+ const lastIndex = findLastIndex(self.displayedRegions, predicate);
863
+ let index;
864
+ while (index !== lastIndex) {
865
+ try {
866
+ const previousIndex = index;
867
+ index = self.displayedRegions
868
+ .slice(previousIndex === undefined ? 0 : previousIndex + 1)
869
+ .findIndex(predicate);
870
+ if (previousIndex !== undefined) {
871
+ index += previousIndex + 1;
872
+ }
873
+ if (!refNameMatched) {
874
+ throw new Error(`could not find a region with refName "${refName}"`);
875
+ }
876
+ if (s === undefined) {
877
+ throw new Error(`could not find a region with refName "${refName}" that contained an end position ${e}`);
878
+ }
879
+ if (e === undefined) {
880
+ throw new Error(`could not find a region with refName "${refName}" that contained a start position ${s + 1}`);
881
+ }
882
+ if (index === -1) {
883
+ throw new Error(`could not find a region that completely contained "${assembleLocString(firstLocation)}"`);
884
+ }
885
+ if (locations.length === 1) {
886
+ const f = self.displayedRegions[index];
887
+ this.moveTo({ index, offset: f.reversed ? f.end - e : s - f.start }, { index, offset: f.reversed ? f.end - s : e - f.start });
888
+ return;
889
+ }
890
+ let locationIndex = 0;
891
+ let locationStart = 0;
892
+ let locationEnd = 0;
893
+ for (locationIndex; locationIndex < locations.length; locationIndex++) {
894
+ const location = locations[locationIndex];
895
+ const region = self.displayedRegions[index + locationIndex];
896
+ locationStart = location.start || region.start;
897
+ locationEnd = location.end || region.end;
898
+ if (location.refName !== region.refName) {
899
+ throw new Error(`Entered location ${assembleLocString(location)} does not match with displayed regions`);
900
+ }
901
+ }
902
+ locationIndex -= 1;
903
+ const startDisplayedRegion = self.displayedRegions[index];
904
+ const endDisplayedRegion = self.displayedRegions[index + locationIndex];
905
+ this.moveTo({
906
+ index,
907
+ offset: startDisplayedRegion.reversed
908
+ ? startDisplayedRegion.end - e
909
+ : s - startDisplayedRegion.start,
910
+ }, {
911
+ index: index + locationIndex,
912
+ offset: endDisplayedRegion.reversed
913
+ ? endDisplayedRegion.end - locationStart
914
+ : locationEnd - endDisplayedRegion.start,
915
+ });
916
+ return;
917
+ }
918
+ catch (error) {
919
+ if (index === lastIndex) {
920
+ throw error;
921
+ }
922
+ }
923
+ }
924
+ },
1097
925
  }))
1098
926
  .views(self => ({
1099
927
  rubberBandMenuItems() {
@@ -1103,9 +931,7 @@ export function stateModelFactory(pluginManager) {
1103
931
  icon: ZoomInIcon,
1104
932
  onClick: () => {
1105
933
  const { leftOffset, rightOffset } = self;
1106
- if (leftOffset && rightOffset) {
1107
- self.moveTo(leftOffset, rightOffset);
1108
- }
934
+ self.moveTo(leftOffset, rightOffset);
1109
935
  },
1110
936
  },
1111
937
  {
@@ -1117,6 +943,34 @@ export function stateModelFactory(pluginManager) {
1117
943
  },
1118
944
  ];
1119
945
  },
946
+ bpToPx({ refName, coord, regionNumber, }) {
947
+ return bpToPx({ refName, coord, regionNumber, self });
948
+ },
949
+ /**
950
+ * scrolls the view to center on the given bp. if that is not in any
951
+ * of the displayed regions, does nothing
952
+ * @param coord - basepair at which you want to center the view
953
+ * @param refName - refName of the displayedRegion you are centering at
954
+ * @param regionNumber - index of the displayedRegion
955
+ */
956
+ centerAt(coord, refName, regionNumber) {
957
+ const centerPx = this.bpToPx({
958
+ refName,
959
+ coord,
960
+ regionNumber,
961
+ });
962
+ if (centerPx) {
963
+ self.scrollTo(Math.round(centerPx.offsetPx - self.width / 2));
964
+ }
965
+ },
966
+ pxToBp(px) {
967
+ return pxToBp(self, px);
968
+ },
969
+ get centerLineInfo() {
970
+ return self.displayedRegions.length
971
+ ? this.pxToBp(self.width / 2)
972
+ : undefined;
973
+ },
1120
974
  }));
1121
975
  }
1122
976
  export { renderToSvg, RefNameAutocomplete, SearchBox };