@apollo-annotation/jbrowse-plugin-apollo 0.3.7 → 0.3.8

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 (71) hide show
  1. package/dist/index.esm.js +2371 -1642
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2384 -1641
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +4387 -2952
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +15 -15
  12. package/src/ApolloInternetAccount/model.ts +48 -13
  13. package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
  14. package/src/ChangeManager.ts +33 -13
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  17. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +33 -31
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +60 -72
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +441 -180
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  23. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  24. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  25. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  26. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  27. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  28. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  29. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  34. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +95 -38
  35. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +221 -201
  36. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  37. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  38. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  39. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  40. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  41. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  42. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  43. package/src/components/AddAssembly.tsx +1 -1
  44. package/src/components/AddAssemblyAliases.tsx +1 -1
  45. package/src/components/AddChildFeature.tsx +5 -2
  46. package/src/components/AddFeature.tsx +9 -3
  47. package/src/components/AddRefSeqAliases.tsx +9 -9
  48. package/src/components/CopyFeature.tsx +3 -1
  49. package/src/components/CreateApolloAnnotation.tsx +1 -0
  50. package/src/components/DeleteAssembly.tsx +1 -1
  51. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  52. package/src/components/FilterFeatures.tsx +7 -7
  53. package/src/components/FilterTranscripts.tsx +6 -6
  54. package/src/components/ImportFeatures.tsx +1 -1
  55. package/src/components/ManageChecks.tsx +1 -1
  56. package/src/components/MergeTranscripts.tsx +12 -15
  57. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  58. package/src/components/OpenLocalFile.tsx +11 -7
  59. package/src/components/ViewCheckResults.tsx +1 -1
  60. package/src/components/index.ts +1 -0
  61. package/src/config.ts +6 -0
  62. package/src/index.ts +42 -105
  63. package/src/makeDisplayComponent.tsx +0 -1
  64. package/src/menus/index.ts +1 -0
  65. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  66. package/src/menus/topLevelMenuAdmin.ts +154 -0
  67. package/src/session/session.ts +162 -116
  68. package/src/util/annotationFeatureUtils.ts +15 -21
  69. package/src/util/displayUtils.ts +149 -0
  70. package/src/util/glyphUtils.ts +152 -0
  71. package/src/util/mouseEventsUtils.ts +32 -0
@@ -1,25 +1,34 @@
1
1
  import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import { readConfObject } from '@jbrowse/core/configuration'
3
+ import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
2
4
  import { type MenuItem } from '@jbrowse/core/ui'
3
5
  import {
4
6
  type AbstractSessionModel,
7
+ getContainingView,
5
8
  getFrame,
6
9
  intersection2,
7
10
  isSessionModelWithWidgets,
8
11
  } from '@jbrowse/core/util'
12
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
13
+ import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded'
14
+ import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded'
9
15
  import { alpha } from '@mui/material'
10
16
 
11
17
  import { type OntologyRecord } from '../../OntologyManager'
12
18
  import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
13
19
  import { type ApolloSessionModel } from '../../session'
14
- import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
15
- import { getFeaturesUnderClick } from '../../util/annotationFeatureUtils'
16
- import { type LinearApolloDisplay } from '../stateModel'
17
20
  import {
18
- type LinearApolloDisplayMouseEvents,
19
21
  type MousePosition,
20
- type MousePositionWithFeatureAndGlyph,
21
- isMousePositionWithFeatureAndGlyph,
22
- } from '../stateModel/mouseEvents'
22
+ type MousePositionWithFeature,
23
+ containsSelectedFeature,
24
+ getMinAndMaxPx,
25
+ getOverlappingEdge,
26
+ isMousePositionWithFeature,
27
+ navToFeatureCenter,
28
+ } from '../../util'
29
+ import { getRelatedFeatures } from '../../util/annotationFeatureUtils'
30
+ import { type LinearApolloDisplay } from '../stateModel'
31
+ import { type LinearApolloDisplayMouseEvents } from '../stateModel/mouseEvents'
23
32
  import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
24
33
  import { type CanvasMouseEvent } from '../types'
25
34
 
@@ -77,52 +86,102 @@ if (canvas?.getContext) {
77
86
  }
78
87
  }
79
88
 
80
- function draw(
89
+ function drawBackground(
81
90
  ctx: CanvasRenderingContext2D,
82
91
  feature: AnnotationFeature,
83
- row: number,
84
92
  stateModel: LinearApolloDisplayRendering,
85
93
  displayedRegionIndex: number,
86
- ): void {
87
- const { apolloRowHeight, lgv, session, theme } = stateModel
94
+ row: number,
95
+ color: string,
96
+ ) {
97
+ const { apolloRowHeight, lgv, session } = stateModel
88
98
  const { bpPerPx, displayedRegions, offsetPx } = lgv
89
99
  const displayedRegion = displayedRegions[displayedRegionIndex]
90
100
  const { refName, reversed } = displayedRegion
91
- const rowHeight = apolloRowHeight
92
- const cdsHeight = Math.round(0.9 * rowHeight)
93
- const { children, min, strand } = feature
94
- if (!children) {
95
- return
96
- }
97
- const { apolloSelectedFeature } = session
98
101
  const { apolloDataStore } = session
99
102
  const { featureTypeOntology } = apolloDataStore.ontologyManager
100
103
  if (!featureTypeOntology) {
101
104
  throw new Error('featureTypeOntology is undefined')
102
105
  }
103
106
 
104
- // Draw background for gene
105
107
  const topLevelFeatureMinX =
106
108
  (lgv.bpToPx({
107
109
  refName,
108
- coord: min,
110
+ coord: feature.min,
109
111
  regionNumber: displayedRegionIndex,
110
112
  })?.offsetPx ?? 0) - offsetPx
111
113
  const topLevelFeatureWidthPx = feature.length / bpPerPx
112
114
  const topLevelFeatureStartPx = reversed
113
115
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
114
116
  : topLevelFeatureMinX
115
- const topLevelFeatureTop = row * rowHeight
117
+ const topLevelFeatureTop = row * apolloRowHeight
116
118
  const topLevelFeatureHeight =
117
- getRowCount(feature, featureTypeOntology) * rowHeight
119
+ getRowCount(feature, featureTypeOntology) * apolloRowHeight
118
120
 
119
- ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6)
121
+ ctx.fillStyle = color
120
122
  ctx.fillRect(
121
123
  topLevelFeatureStartPx,
122
124
  topLevelFeatureTop,
123
125
  topLevelFeatureWidthPx,
124
126
  topLevelFeatureHeight,
125
127
  )
128
+ }
129
+
130
+ function backgroundColorForFeature(
131
+ session: ApolloSessionModel,
132
+ featureType: string,
133
+ ): string {
134
+ const color = readConfObject(
135
+ session.getPluginConfiguration(),
136
+ 'backgroundColorForFeature',
137
+ { featureType },
138
+ ) as string
139
+ return color
140
+ }
141
+
142
+ function draw(
143
+ ctx: CanvasRenderingContext2D,
144
+ feature: AnnotationFeature,
145
+ row: number,
146
+ stateModel: LinearApolloDisplayRendering,
147
+ displayedRegionIndex: number,
148
+ ): void {
149
+ const { apolloRowHeight, lgv, selectedFeature, session, theme } = stateModel
150
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
151
+ const displayedRegion = displayedRegions[displayedRegionIndex]
152
+ const { refName, reversed } = displayedRegion
153
+ const rowHeight = apolloRowHeight
154
+ const cdsHeight = Math.round(0.9 * rowHeight)
155
+ const { children, strand } = feature
156
+ if (!children) {
157
+ return
158
+ }
159
+ const { apolloDataStore } = session
160
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
161
+ if (!featureTypeOntology) {
162
+ throw new Error('featureTypeOntology is undefined')
163
+ }
164
+
165
+ // Draw background for gene
166
+ drawBackground(
167
+ ctx,
168
+ feature,
169
+ stateModel,
170
+ displayedRegionIndex,
171
+ row,
172
+ alpha(theme.palette.background.paper, 0.6),
173
+ )
174
+
175
+ if (featureTypeOntology.isTypeOf(feature.type, 'pseudogene')) {
176
+ drawBackground(
177
+ ctx,
178
+ feature,
179
+ stateModel,
180
+ displayedRegionIndex,
181
+ row,
182
+ backgroundColorForFeature(session, 'pseudogenic_transcript'),
183
+ )
184
+ }
126
185
 
127
186
  // Draw lines on different rows for each transcript
128
187
  let currentRow = 0
@@ -140,6 +199,29 @@ function draw(
140
199
  }
141
200
 
142
201
  const cdsCount = getCDSCount(transcript, featureTypeOntology)
202
+ if (cdsCount === 0) {
203
+ drawBackground(
204
+ ctx,
205
+ transcript,
206
+ stateModel,
207
+ displayedRegionIndex,
208
+ currentRow,
209
+ backgroundColorForFeature(session, 'nonCodingTranscript'),
210
+ )
211
+ }
212
+ if (
213
+ featureTypeOntology.isTypeOf(transcript.type, 'pseudogenic_transcript')
214
+ ) {
215
+ drawBackground(
216
+ ctx,
217
+ transcript,
218
+ stateModel,
219
+ displayedRegionIndex,
220
+ currentRow,
221
+ backgroundColorForFeature(session, 'pseudogenic_transcript'),
222
+ )
223
+ }
224
+
143
225
  for (const [, childFeature] of transcriptChildren) {
144
226
  if (!featureTypeOntology.isTypeOf(childFeature.type, 'CDS')) {
145
227
  continue
@@ -169,9 +251,9 @@ function draw(
169
251
  }
170
252
 
171
253
  const forwardFill =
172
- theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
254
+ theme.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
173
255
  const backwardFill =
174
- theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
256
+ theme.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
175
257
  // Draw exon and CDS for each transcript
176
258
  currentRow = 0
177
259
  for (const [, child] of children) {
@@ -188,7 +270,7 @@ function draw(
188
270
  const cdsCount = getCDSCount(child, featureTypeOntology)
189
271
  if (cdsCount != 0) {
190
272
  for (const cdsRow of child.cdsLocations) {
191
- const { _id, children: transcriptChildren } = child
273
+ const { children: transcriptChildren } = child
192
274
  if (!transcriptChildren) {
193
275
  continue
194
276
  }
@@ -217,7 +299,7 @@ function draw(
217
299
  regionNumber: displayedRegionIndex,
218
300
  })?.offsetPx ?? 0) - offsetPx
219
301
  const cdsStartPx = reversed ? minX - cdsWidthPx : minX
220
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
302
+ ctx.fillStyle = theme.palette.text.primary
221
303
  const cdsTop =
222
304
  (row + currentRow) * rowHeight + (rowHeight - cdsHeight) / 2
223
305
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
@@ -234,12 +316,8 @@ function draw(
234
316
  child.strand ?? 1,
235
317
  cds.phase,
236
318
  )
237
- const frameColor = theme?.palette.framesCDS.at(frame)?.main
238
- const cdsColorCode = frameColor ?? 'rgb(171,71,188)'
239
- ctx.fillStyle =
240
- apolloSelectedFeature && _id === apolloSelectedFeature._id
241
- ? 'rgb(0,0,0)'
242
- : cdsColorCode
319
+ const frameColor = theme.palette.framesCDS.at(frame)?.main
320
+ ctx.fillStyle = frameColor ?? 'black'
243
321
  ctx.fillRect(
244
322
  cdsStartPx + 1,
245
323
  cdsTop + 1,
@@ -295,6 +373,9 @@ function draw(
295
373
  currentRow += 1
296
374
  }
297
375
  }
376
+ if (selectedFeature && containsSelectedFeature(feature, selectedFeature)) {
377
+ drawHighlight(stateModel, ctx, selectedFeature, true)
378
+ }
298
379
  }
299
380
 
300
381
  function drawExon(
@@ -308,11 +389,10 @@ function drawExon(
308
389
  forwardFill: CanvasPattern | null,
309
390
  backwardFill: CanvasPattern | null,
310
391
  ) {
311
- const { apolloRowHeight, lgv, session, theme } = stateModel
392
+ const { apolloRowHeight, lgv, theme } = stateModel
312
393
  const { bpPerPx, displayedRegions, offsetPx } = lgv
313
394
  const displayedRegion = displayedRegions[displayedRegionIndex]
314
395
  const { refName, reversed } = displayedRegion
315
- const { apolloSelectedFeature } = session
316
396
 
317
397
  const minX =
318
398
  (lgv.bpToPx({
@@ -326,14 +406,11 @@ function drawExon(
326
406
  const top = (row + currentRow) * apolloRowHeight
327
407
  const exonHeight = Math.round(0.6 * apolloRowHeight)
328
408
  const exonTop = top + (apolloRowHeight - exonHeight) / 2
329
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
409
+ ctx.fillStyle = theme.palette.text.primary
330
410
  ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
331
411
  if (widthPx > 2) {
332
412
  ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
333
- ctx.fillStyle =
334
- apolloSelectedFeature && exon._id === apolloSelectedFeature._id
335
- ? 'rgb(0,0,0)'
336
- : 'rgb(211,211,211)'
413
+ ctx.fillStyle = 'rgb(211,211,211)'
337
414
  ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
338
415
  if (forwardFill && backwardFill && strand) {
339
416
  const reversal = reversed ? -1 : 1
@@ -354,6 +431,21 @@ function drawExon(
354
431
  }
355
432
  }
356
433
 
434
+ function* range(start: number, stop: number, step = 1): Generator<number> {
435
+ if (start === stop) {
436
+ return
437
+ }
438
+ if (start < stop) {
439
+ for (let i = start; i < stop; i += step) {
440
+ yield i
441
+ }
442
+ return
443
+ }
444
+ for (let i = start; i > stop; i -= step) {
445
+ yield i
446
+ }
447
+ }
448
+
357
449
  function drawLine(
358
450
  ctx: CanvasRenderingContext2D,
359
451
  stateModel: LinearApolloDisplayRendering,
@@ -372,14 +464,33 @@ function drawLine(
372
464
  coord: transcript.min,
373
465
  regionNumber: displayedRegionIndex,
374
466
  })?.offsetPx ?? 0) - offsetPx
375
- const widthPx = transcript.length / bpPerPx
467
+ const widthPx = Math.round(transcript.length / bpPerPx)
376
468
  const startPx = reversed ? minX - widthPx : minX
377
469
  const height =
378
470
  Math.round((currentRow + 1 / 2) * apolloRowHeight) + row * apolloRowHeight
379
- ctx.strokeStyle = theme?.palette.text.primary ?? 'black'
471
+ ctx.strokeStyle = theme.palette.text.primary
472
+ const { strand = 1 } = transcript
380
473
  ctx.beginPath()
381
- ctx.moveTo(startPx, height)
382
- ctx.lineTo(startPx + widthPx, height)
474
+ // Draw the transcript line, and extend it out a bit on the 3` end
475
+ const lineStart = startPx - (strand === -1 ? 5 : 0)
476
+ const lineEnd = startPx + widthPx + (strand === -1 ? 0 : 5)
477
+ ctx.moveTo(lineStart, height)
478
+ ctx.lineTo(lineEnd, height)
479
+ // Now to draw arrows every 20 pixels along the line
480
+ // Make the arrow range a bit shorter to avoid an arrow hanging off the 5` end
481
+ const arrowsStart = lineStart + (strand === -1 ? 0 : 3)
482
+ const arrowsEnd = lineEnd - (strand === -1 ? 3 : 0)
483
+ // Offset determines if the arrows face left or right
484
+ const offset = strand === -1 ? 3 : -3
485
+ const arrowRange =
486
+ strand === -1
487
+ ? range(arrowsStart, arrowsEnd, 20)
488
+ : range(arrowsEnd, arrowsStart, 20)
489
+ for (const arrowLocation of arrowRange) {
490
+ ctx.moveTo(arrowLocation + offset, height + offset)
491
+ ctx.lineTo(arrowLocation, height)
492
+ ctx.lineTo(arrowLocation + offset, height - offset)
493
+ }
383
494
  ctx.stroke()
384
495
  }
385
496
 
@@ -405,24 +516,22 @@ function drawDragPreview(
405
516
  const rectY = row * apolloRowHeight
406
517
  const rectWidth = Math.abs(current.x - featureEdgePx)
407
518
  const rectHeight = apolloRowHeight * rowCount
408
- overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
519
+ overlayCtx.strokeStyle = theme.palette.info.main
409
520
  overlayCtx.setLineDash([6])
410
521
  overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
411
- overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
522
+ overlayCtx.fillStyle = alpha(theme.palette.info.main, 0.2)
412
523
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
413
524
  }
414
525
 
415
- function drawHover(
416
- stateModel: LinearApolloDisplay,
526
+ function drawHighlight(
527
+ stateModel: LinearApolloDisplayRendering,
417
528
  ctx: CanvasRenderingContext2D,
529
+ feature: AnnotationFeature,
530
+ selected = false,
418
531
  ) {
419
- const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel
532
+ const { apolloRowHeight, lgv, session, theme } = stateModel
420
533
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager
421
534
 
422
- if (!apolloHover) {
423
- return
424
- }
425
- const { feature } = apolloHover
426
535
  const position = stateModel.getFeatureLayoutPosition(feature)
427
536
  if (!position) {
428
537
  return
@@ -441,7 +550,9 @@ function drawHover(
441
550
  const row = layoutRow + featureRow
442
551
  const top = row * apolloRowHeight
443
552
  const widthPx = length / bpPerPx
444
- ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)'
553
+ ctx.fillStyle = selected
554
+ ? theme.palette.action.disabled
555
+ : theme.palette.action.focus
445
556
 
446
557
  if (!featureTypeOntology) {
447
558
  throw new Error('featureTypeOntology is undefined')
@@ -454,6 +565,18 @@ function drawHover(
454
565
  )
455
566
  }
456
567
 
568
+ function drawHover(
569
+ stateModel: LinearApolloDisplay,
570
+ ctx: CanvasRenderingContext2D,
571
+ ) {
572
+ const { hoveredFeature } = stateModel
573
+
574
+ if (!hoveredFeature) {
575
+ return
576
+ }
577
+ drawHighlight(stateModel, ctx, hoveredFeature.feature)
578
+ }
579
+
457
580
  function getFeatureFromLayout(
458
581
  feature: AnnotationFeature,
459
582
  bp: number,
@@ -613,15 +736,53 @@ function getRowForFeature(
613
736
  return
614
737
  }
615
738
 
739
+ function selectFeatureAndOpenWidget(
740
+ stateModel: LinearApolloDisplayMouseEvents,
741
+ feature: AnnotationFeature,
742
+ ) {
743
+ if (stateModel.apolloDragging) {
744
+ return
745
+ }
746
+ stateModel.setSelectedFeature(feature)
747
+ const { session } = stateModel
748
+ const { apolloDataStore } = session
749
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
750
+ if (!featureTypeOntology) {
751
+ throw new Error('featureTypeOntology is undefined')
752
+ }
753
+
754
+ let containsCDSOrExon = false
755
+ for (const [, child] of feature.children ?? []) {
756
+ if (
757
+ featureTypeOntology.isTypeOf(child.type, 'CDS') ||
758
+ featureTypeOntology.isTypeOf(child.type, 'exon')
759
+ ) {
760
+ containsCDSOrExon = true
761
+ break
762
+ }
763
+ }
764
+ if (
765
+ (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
766
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
767
+ containsCDSOrExon
768
+ ) {
769
+ stateModel.showFeatureDetailsWidget(feature, [
770
+ 'ApolloTranscriptDetails',
771
+ 'apolloTranscriptDetails',
772
+ ])
773
+ } else {
774
+ stateModel.showFeatureDetailsWidget(feature)
775
+ }
776
+ }
777
+
616
778
  function onMouseDown(
617
779
  stateModel: LinearApolloDisplay,
618
- currentMousePosition: MousePositionWithFeatureAndGlyph,
780
+ currentMousePosition: MousePositionWithFeature,
619
781
  event: CanvasMouseEvent,
620
782
  ) {
621
- const { featureAndGlyphUnderMouse } = currentMousePosition
783
+ const { feature } = currentMousePosition
622
784
  // swallow the mouseDown if we are on the edge of the feature so that we
623
785
  // don't start dragging the view if we try to drag the feature edge
624
- const { feature } = featureAndGlyphUnderMouse
625
786
  const draggableFeature = getDraggableFeatureInfo(
626
787
  currentMousePosition,
627
788
  feature,
@@ -642,10 +803,9 @@ function onMouseMove(
642
803
  stateModel: LinearApolloDisplay,
643
804
  mousePosition: MousePosition,
644
805
  ) {
645
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
646
- const { featureAndGlyphUnderMouse } = mousePosition
647
- stateModel.setApolloHover(featureAndGlyphUnderMouse)
648
- const { feature } = featureAndGlyphUnderMouse
806
+ if (isMousePositionWithFeature(mousePosition)) {
807
+ const { feature, bp } = mousePosition
808
+ stateModel.setHoveredFeature({ feature, bp })
649
809
  const draggableFeature = getDraggableFeatureInfo(
650
810
  mousePosition,
651
811
  feature,
@@ -666,41 +826,11 @@ function onMouseUp(
666
826
  if (stateModel.apolloDragging) {
667
827
  return
668
828
  }
669
- const { featureAndGlyphUnderMouse } = mousePosition
670
- if (!featureAndGlyphUnderMouse) {
829
+ const { feature } = mousePosition
830
+ if (!feature) {
671
831
  return
672
832
  }
673
- const { feature } = featureAndGlyphUnderMouse
674
- stateModel.setSelectedFeature(feature)
675
- const { session } = stateModel
676
- const { apolloDataStore } = session
677
- const { featureTypeOntology } = apolloDataStore.ontologyManager
678
- if (!featureTypeOntology) {
679
- throw new Error('featureTypeOntology is undefined')
680
- }
681
-
682
- let containsCDSOrExon = false
683
- for (const [, child] of feature.children ?? []) {
684
- if (
685
- featureTypeOntology.isTypeOf(child.type, 'CDS') ||
686
- featureTypeOntology.isTypeOf(child.type, 'exon')
687
- ) {
688
- containsCDSOrExon = true
689
- break
690
- }
691
- }
692
- if (
693
- (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
694
- featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
695
- containsCDSOrExon
696
- ) {
697
- stateModel.showFeatureDetailsWidget(feature, [
698
- 'ApolloTranscriptDetails',
699
- 'apolloTranscriptDetails',
700
- ])
701
- } else {
702
- stateModel.showFeatureDetailsWidget(feature)
703
- }
833
+ selectFeatureAndOpenWidget(stateModel, feature)
704
834
  }
705
835
 
706
836
  function getDraggableFeatureInfo(
@@ -805,13 +935,102 @@ function isCDSFeature(
805
935
  return featureTypeOntology.isTypeOf(feature.type, 'CDS')
806
936
  }
807
937
 
938
+ interface AdjacentExons {
939
+ upstream: AnnotationFeature | undefined
940
+ downstream: AnnotationFeature | undefined
941
+ }
942
+
943
+ function getAdjacentExons(
944
+ currentExon: AnnotationFeature,
945
+ display: LinearApolloDisplayMouseEvents,
946
+ mousePosition: MousePositionWithFeature,
947
+ session: ApolloSessionModel,
948
+ ): AdjacentExons {
949
+ const lgv = getContainingView(
950
+ display as BaseDisplayModel,
951
+ ) as unknown as LinearGenomeViewModel
952
+
953
+ // Genomic coords of current view
954
+ const viewGenomicLeft = mousePosition.bp - lgv.bpPerPx * mousePosition.x
955
+ const viewGenomicRight = viewGenomicLeft + lgv.coarseTotalBp
956
+ if (!currentExon.parent) {
957
+ return { upstream: undefined, downstream: undefined }
958
+ }
959
+ const transcript = currentExon.parent
960
+ if (!transcript.children) {
961
+ throw new Error(`Error getting children of ${transcript._id}`)
962
+ }
963
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
964
+ if (!featureTypeOntology) {
965
+ throw new Error('featureTypeOntology is undefined')
966
+ }
967
+
968
+ let exons = []
969
+ for (const [, child] of transcript.children) {
970
+ if (featureTypeOntology.isTypeOf(child.type, 'exon')) {
971
+ exons.push(child)
972
+ }
973
+ }
974
+ const adjacentExons: AdjacentExons = {
975
+ upstream: undefined,
976
+ downstream: undefined,
977
+ }
978
+ exons = exons.sort((a, b) => (a.min < b.min ? -1 : 1))
979
+ for (const exon of exons) {
980
+ if (exon.min > viewGenomicRight) {
981
+ adjacentExons.downstream = exon
982
+ break
983
+ }
984
+ }
985
+ exons = exons.sort((a, b) => (a.min > b.min ? -1 : 1))
986
+ for (const exon of exons) {
987
+ if (exon.max < viewGenomicLeft) {
988
+ adjacentExons.upstream = exon
989
+ break
990
+ }
991
+ }
992
+ if (transcript.strand === -1) {
993
+ const newUpstream = adjacentExons.downstream
994
+ adjacentExons.downstream = adjacentExons.upstream
995
+ adjacentExons.upstream = newUpstream
996
+ }
997
+ return adjacentExons
998
+ }
999
+
1000
+ function getStreamIcon(
1001
+ strand: 1 | -1 | undefined,
1002
+ isUpstream: boolean,
1003
+ isFlipped: boolean | undefined,
1004
+ ) {
1005
+ // This is the icon you would use for strand=1, downstream, straight
1006
+ // (non-flipped) view
1007
+ let icon = SkipNextRoundedIcon
1008
+
1009
+ if (strand === -1) {
1010
+ icon = SkipPreviousRoundedIcon
1011
+ }
1012
+ if (isUpstream) {
1013
+ icon =
1014
+ icon === SkipPreviousRoundedIcon
1015
+ ? SkipNextRoundedIcon
1016
+ : SkipPreviousRoundedIcon
1017
+ }
1018
+ if (isFlipped) {
1019
+ icon =
1020
+ icon === SkipPreviousRoundedIcon
1021
+ ? SkipNextRoundedIcon
1022
+ : SkipPreviousRoundedIcon
1023
+ }
1024
+ return icon
1025
+ }
1026
+
808
1027
  function getContextMenuItems(
809
1028
  display: LinearApolloDisplayMouseEvents,
810
- mousePosition: MousePositionWithFeatureAndGlyph,
1029
+ mousePosition: MousePositionWithFeature,
811
1030
  ): MenuItem[] {
812
1031
  const {
813
1032
  apolloInternetAccount: internetAccount,
814
- apolloHover,
1033
+ hoveredFeature,
815
1034
  changeManager,
816
1035
  regions,
817
1036
  selectedFeature,
@@ -822,53 +1041,120 @@ function getContextMenuItems(
822
1041
  const menuItems: MenuItem[] = []
823
1042
  const role = internetAccount ? internetAccount.role : 'admin'
824
1043
  const admin = role === 'admin'
825
- if (!apolloHover) {
1044
+ if (!hoveredFeature) {
826
1045
  return menuItems
827
1046
  }
828
1047
 
829
- let featuresUnderClick = getFeaturesUnderClick(mousePosition)
830
- if (isCDSFeature(mousePosition.featureAndGlyphUnderMouse.feature, session)) {
831
- featuresUnderClick = getFeaturesUnderClick(mousePosition, true)
832
- }
1048
+ if (isMousePositionWithFeature(mousePosition)) {
1049
+ const { bp, feature } = mousePosition
1050
+ let featuresUnderClick = getRelatedFeatures(feature, bp)
1051
+ if (isCDSFeature(feature, session)) {
1052
+ featuresUnderClick = getRelatedFeatures(feature, bp, true)
1053
+ }
833
1054
 
834
- for (const feature of featuresUnderClick) {
835
- const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
836
- display,
837
- feature,
838
- )
839
- if (isExonFeature(feature, session)) {
840
- contextMenuItemsForFeature.push(
841
- {
842
- label: 'Merge exons',
843
- disabled: !admin,
844
- onClick: () => {
845
- ;(session as unknown as AbstractSessionModel).queueDialog(
846
- (doneCallback) => [
847
- MergeExons,
848
- {
849
- session,
850
- handleClose: () => {
851
- doneCallback()
1055
+ for (const feature of featuresUnderClick) {
1056
+ const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
1057
+ display,
1058
+ feature,
1059
+ )
1060
+ if (isExonFeature(feature, session)) {
1061
+ const adjacentExons = getAdjacentExons(
1062
+ feature,
1063
+ display,
1064
+ mousePosition,
1065
+ session,
1066
+ )
1067
+ const lgv = getContainingView(
1068
+ display as BaseDisplayModel,
1069
+ ) as unknown as LinearGenomeViewModel
1070
+ if (adjacentExons.upstream) {
1071
+ const exon = adjacentExons.upstream
1072
+ contextMenuItemsForFeature.push({
1073
+ label: 'Go to upstream exon',
1074
+ icon: getStreamIcon(
1075
+ feature.strand,
1076
+ true,
1077
+ lgv.displayedRegions.at(0)?.reversed,
1078
+ ),
1079
+ onClick: () => {
1080
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
1081
+ selectFeatureAndOpenWidget(display, exon)
1082
+ },
1083
+ })
1084
+ }
1085
+ if (adjacentExons.downstream) {
1086
+ const exon = adjacentExons.downstream
1087
+ contextMenuItemsForFeature.push({
1088
+ label: 'Go to downstream exon',
1089
+ icon: getStreamIcon(
1090
+ feature.strand,
1091
+ false,
1092
+ lgv.displayedRegions.at(0)?.reversed,
1093
+ ),
1094
+ onClick: () => {
1095
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
1096
+ selectFeatureAndOpenWidget(display, exon)
1097
+ },
1098
+ })
1099
+ }
1100
+ contextMenuItemsForFeature.push(
1101
+ {
1102
+ label: 'Merge exons',
1103
+ disabled: !admin,
1104
+ onClick: () => {
1105
+ ;(session as unknown as AbstractSessionModel).queueDialog(
1106
+ (doneCallback) => [
1107
+ MergeExons,
1108
+ {
1109
+ session,
1110
+ handleClose: () => {
1111
+ doneCallback()
1112
+ },
1113
+ changeManager,
1114
+ sourceFeature: feature,
1115
+ sourceAssemblyId: currentAssemblyId,
1116
+ selectedFeature,
1117
+ setSelectedFeature: (feature?: AnnotationFeature) => {
1118
+ display.setSelectedFeature(feature)
1119
+ },
852
1120
  },
853
- changeManager,
854
- sourceFeature: feature,
855
- sourceAssemblyId: currentAssemblyId,
856
- selectedFeature,
857
- setSelectedFeature: (feature?: AnnotationFeature) => {
858
- display.setSelectedFeature(feature)
1121
+ ],
1122
+ )
1123
+ },
1124
+ },
1125
+ {
1126
+ label: 'Split exon',
1127
+ disabled: !admin,
1128
+ onClick: () => {
1129
+ ;(session as unknown as AbstractSessionModel).queueDialog(
1130
+ (doneCallback) => [
1131
+ SplitExon,
1132
+ {
1133
+ session,
1134
+ handleClose: () => {
1135
+ doneCallback()
1136
+ },
1137
+ changeManager,
1138
+ sourceFeature: feature,
1139
+ sourceAssemblyId: currentAssemblyId,
1140
+ selectedFeature,
1141
+ setSelectedFeature: (feature?: AnnotationFeature) => {
1142
+ display.setSelectedFeature(feature)
1143
+ },
859
1144
  },
860
- },
861
- ],
862
- )
1145
+ ],
1146
+ )
1147
+ },
863
1148
  },
864
- },
865
- {
866
- label: 'Split exon',
867
- disabled: !admin,
1149
+ )
1150
+ }
1151
+ if (isTranscriptFeature(feature, session)) {
1152
+ contextMenuItemsForFeature.push({
1153
+ label: 'Merge transcript',
868
1154
  onClick: () => {
869
1155
  ;(session as unknown as AbstractSessionModel).queueDialog(
870
1156
  (doneCallback) => [
871
- SplitExon,
1157
+ MergeTranscripts,
872
1158
  {
873
1159
  session,
874
1160
  handleClose: () => {
@@ -885,56 +1171,31 @@ function getContextMenuItems(
885
1171
  ],
886
1172
  )
887
1173
  },
888
- },
889
- )
890
- }
891
- if (isTranscriptFeature(feature, session)) {
892
- contextMenuItemsForFeature.push({
893
- label: 'Merge transcript',
894
- onClick: () => {
895
- ;(session as unknown as AbstractSessionModel).queueDialog(
896
- (doneCallback) => [
897
- MergeTranscripts,
898
- {
899
- session,
900
- handleClose: () => {
901
- doneCallback()
902
- },
903
- changeManager,
904
- sourceFeature: feature,
905
- sourceAssemblyId: currentAssemblyId,
906
- selectedFeature,
907
- setSelectedFeature: (feature?: AnnotationFeature) => {
908
- display.setSelectedFeature(feature)
909
- },
910
- },
911
- ],
912
- )
913
- },
914
- })
915
- if (isSessionModelWithWidgets(session)) {
916
- contextMenuItemsForFeature.push({
917
- label: 'Open transcript details',
918
- onClick: () => {
919
- const apolloTranscriptWidget = session.addWidget(
920
- 'ApolloTranscriptDetails',
921
- 'apolloTranscriptDetails',
922
- {
923
- feature,
924
- assembly: currentAssemblyId,
925
- changeManager,
926
- refName: region.refName,
927
- },
928
- )
929
- session.showWidget(apolloTranscriptWidget)
930
- },
931
1174
  })
1175
+ if (isSessionModelWithWidgets(session)) {
1176
+ contextMenuItemsForFeature.push({
1177
+ label: 'Open transcript details',
1178
+ onClick: () => {
1179
+ const apolloTranscriptWidget = session.addWidget(
1180
+ 'ApolloTranscriptDetails',
1181
+ 'apolloTranscriptDetails',
1182
+ {
1183
+ feature,
1184
+ assembly: currentAssemblyId,
1185
+ changeManager,
1186
+ refName: region.refName,
1187
+ },
1188
+ )
1189
+ session.showWidget(apolloTranscriptWidget)
1190
+ },
1191
+ })
1192
+ }
932
1193
  }
1194
+ menuItems.push({
1195
+ label: feature.type,
1196
+ subMenu: contextMenuItemsForFeature,
1197
+ })
933
1198
  }
934
- menuItems.push({
935
- label: feature.type,
936
- subMenu: contextMenuItemsForFeature,
937
- })
938
1199
  }
939
1200
  return menuItems
940
1201
  }