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

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 (86) hide show
  1. package/dist/index.esm.js +11212 -10483
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +11251 -10509
  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 +7726 -9014
  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 +18 -18
  12. package/src/ApolloInternetAccount/model.ts +123 -70
  13. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  14. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +72 -20
  16. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  17. package/src/ChangeManager.ts +36 -14
  18. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  22. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +72 -234
  23. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +23 -131
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +279 -217
  27. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  28. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  29. package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
  30. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  31. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  32. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  33. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  34. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  36. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  37. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
  38. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
  39. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  40. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  41. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  42. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +157 -0
  43. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +101 -38
  44. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +334 -262
  45. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  46. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  47. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  48. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  49. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  50. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  51. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  52. package/src/components/AddAssembly.tsx +34 -38
  53. package/src/components/AddAssemblyAliases.tsx +1 -1
  54. package/src/components/AddChildFeature.tsx +5 -2
  55. package/src/components/AddFeature.tsx +30 -21
  56. package/src/components/AddRefSeqAliases.tsx +64 -50
  57. package/src/components/CopyFeature.tsx +4 -2
  58. package/src/components/CreateApolloAnnotation.tsx +22 -9
  59. package/src/components/DeleteAssembly.tsx +3 -10
  60. package/src/components/DownloadGFF3.tsx +2 -2
  61. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  62. package/src/components/FilterFeatures.tsx +7 -7
  63. package/src/components/FilterTranscripts.tsx +6 -6
  64. package/src/components/ImportFeatures.tsx +1 -1
  65. package/src/components/ManageChecks.tsx +3 -10
  66. package/src/components/ManageUsers.tsx +23 -22
  67. package/src/components/MergeTranscripts.tsx +12 -15
  68. package/src/components/OntologyTermAutocomplete.tsx +1 -8
  69. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  70. package/src/components/OpenLocalFile.tsx +11 -7
  71. package/src/components/ViewChangeLog.tsx +25 -50
  72. package/src/components/ViewCheckResults.tsx +2 -8
  73. package/src/components/index.ts +1 -0
  74. package/src/config.ts +6 -0
  75. package/src/index.ts +53 -115
  76. package/src/makeDisplayComponent.tsx +9 -14
  77. package/src/menus/index.ts +1 -0
  78. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  79. package/src/menus/topLevelMenuAdmin.ts +154 -0
  80. package/src/session/ClientDataStore.ts +32 -14
  81. package/src/session/session.ts +159 -121
  82. package/src/util/annotationFeatureUtils.ts +15 -21
  83. package/src/util/displayUtils.ts +149 -0
  84. package/src/util/glyphUtils.ts +329 -0
  85. package/src/util/loadAssemblyIntoClient.ts +3 -2
  86. package/src/util/mouseEventsUtils.ts +32 -0
@@ -1,25 +1,37 @@
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'
9
13
  import { alpha } from '@mui/material'
10
14
 
11
15
  import { type OntologyRecord } from '../../OntologyManager'
12
16
  import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
13
- import { type ApolloSessionModel } from '../../session'
14
- import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
15
- import { getFeaturesUnderClick } from '../../util/annotationFeatureUtils'
16
- import { type LinearApolloDisplay } from '../stateModel'
17
17
  import {
18
- type LinearApolloDisplayMouseEvents,
19
18
  type MousePosition,
20
- type MousePositionWithFeatureAndGlyph,
21
- isMousePositionWithFeatureAndGlyph,
22
- } from '../stateModel/mouseEvents'
19
+ type MousePositionWithFeature,
20
+ containsSelectedFeature,
21
+ getAdjacentExons,
22
+ getMinAndMaxPx,
23
+ getOverlappingEdge,
24
+ getStreamIcon,
25
+ isCDSFeature,
26
+ isExonFeature,
27
+ isMousePositionWithFeature,
28
+ isTranscriptFeature,
29
+ navToFeatureCenter,
30
+ selectFeatureAndOpenWidget,
31
+ } from '../../util'
32
+ import { getRelatedFeatures } from '../../util/annotationFeatureUtils'
33
+ import { type LinearApolloDisplay } from '../stateModel'
34
+ import { type LinearApolloDisplayMouseEvents } from '../stateModel/mouseEvents'
23
35
  import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
24
36
  import { type CanvasMouseEvent } from '../types'
25
37
 
@@ -77,52 +89,85 @@ if (canvas?.getContext) {
77
89
  }
78
90
  }
79
91
 
80
- function draw(
92
+ function drawBackground(
81
93
  ctx: CanvasRenderingContext2D,
82
94
  feature: AnnotationFeature,
83
- row: number,
84
95
  stateModel: LinearApolloDisplayRendering,
85
96
  displayedRegionIndex: number,
86
- ): void {
97
+ row: number,
98
+ color?: string,
99
+ ) {
87
100
  const { apolloRowHeight, lgv, session, theme } = stateModel
88
101
  const { bpPerPx, displayedRegions, offsetPx } = lgv
89
102
  const displayedRegion = displayedRegions[displayedRegionIndex]
90
103
  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
104
  const { apolloDataStore } = session
99
105
  const { featureTypeOntology } = apolloDataStore.ontologyManager
100
106
  if (!featureTypeOntology) {
101
107
  throw new Error('featureTypeOntology is undefined')
102
108
  }
103
109
 
104
- // Draw background for gene
105
110
  const topLevelFeatureMinX =
106
111
  (lgv.bpToPx({
107
112
  refName,
108
- coord: min,
113
+ coord: feature.min,
109
114
  regionNumber: displayedRegionIndex,
110
115
  })?.offsetPx ?? 0) - offsetPx
111
116
  const topLevelFeatureWidthPx = feature.length / bpPerPx
112
117
  const topLevelFeatureStartPx = reversed
113
118
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
114
119
  : topLevelFeatureMinX
115
- const topLevelFeatureTop = row * rowHeight
120
+ const topLevelFeatureTop = row * apolloRowHeight
116
121
  const topLevelFeatureHeight =
117
- getRowCount(feature, featureTypeOntology) * rowHeight
122
+ getRowCount(feature, featureTypeOntology) * apolloRowHeight
118
123
 
119
- ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6)
124
+ let selectedColor
125
+ if (color) {
126
+ selectedColor = color
127
+ } else {
128
+ selectedColor = readConfObject(
129
+ session.getPluginConfiguration(),
130
+ 'geneBackgroundColor',
131
+ { featureType: feature.type },
132
+ ) as string
133
+ if (!selectedColor) {
134
+ selectedColor = alpha(theme.palette.background.paper, 0.6)
135
+ }
136
+ }
137
+ ctx.fillStyle = selectedColor
120
138
  ctx.fillRect(
121
139
  topLevelFeatureStartPx,
122
140
  topLevelFeatureTop,
123
141
  topLevelFeatureWidthPx,
124
142
  topLevelFeatureHeight,
125
143
  )
144
+ }
145
+
146
+ function draw(
147
+ ctx: CanvasRenderingContext2D,
148
+ feature: AnnotationFeature,
149
+ row: number,
150
+ stateModel: LinearApolloDisplayRendering,
151
+ displayedRegionIndex: number,
152
+ ): void {
153
+ const { apolloRowHeight, lgv, selectedFeature, session, theme } = stateModel
154
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
155
+ const displayedRegion = displayedRegions[displayedRegionIndex]
156
+ const { refName, reversed } = displayedRegion
157
+ const rowHeight = apolloRowHeight
158
+ const cdsHeight = Math.round(0.9 * rowHeight)
159
+ const { children, strand } = feature
160
+ if (!children) {
161
+ return
162
+ }
163
+ const { apolloDataStore } = session
164
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
165
+ if (!featureTypeOntology) {
166
+ throw new Error('featureTypeOntology is undefined')
167
+ }
168
+
169
+ // Draw background for gene
170
+ drawBackground(ctx, feature, stateModel, displayedRegionIndex, row)
126
171
 
127
172
  // Draw lines on different rows for each transcript
128
173
  let currentRow = 0
@@ -138,8 +183,8 @@ function draw(
138
183
  if (!transcriptChildren) {
139
184
  continue
140
185
  }
141
-
142
186
  const cdsCount = getCDSCount(transcript, featureTypeOntology)
187
+
143
188
  for (const [, childFeature] of transcriptChildren) {
144
189
  if (!featureTypeOntology.isTypeOf(childFeature.type, 'CDS')) {
145
190
  continue
@@ -169,9 +214,9 @@ function draw(
169
214
  }
170
215
 
171
216
  const forwardFill =
172
- theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
217
+ theme.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
173
218
  const backwardFill =
174
- theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
219
+ theme.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
175
220
  // Draw exon and CDS for each transcript
176
221
  currentRow = 0
177
222
  for (const [, child] of children) {
@@ -188,7 +233,7 @@ function draw(
188
233
  const cdsCount = getCDSCount(child, featureTypeOntology)
189
234
  if (cdsCount != 0) {
190
235
  for (const cdsRow of child.cdsLocations) {
191
- const { _id, children: transcriptChildren } = child
236
+ const { children: transcriptChildren } = child
192
237
  if (!transcriptChildren) {
193
238
  continue
194
239
  }
@@ -217,7 +262,7 @@ function draw(
217
262
  regionNumber: displayedRegionIndex,
218
263
  })?.offsetPx ?? 0) - offsetPx
219
264
  const cdsStartPx = reversed ? minX - cdsWidthPx : minX
220
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
265
+ ctx.fillStyle = theme.palette.text.primary
221
266
  const cdsTop =
222
267
  (row + currentRow) * rowHeight + (rowHeight - cdsHeight) / 2
223
268
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
@@ -234,12 +279,8 @@ function draw(
234
279
  child.strand ?? 1,
235
280
  cds.phase,
236
281
  )
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
282
+ const frameColor = theme.palette.framesCDS.at(frame)?.main
283
+ ctx.fillStyle = frameColor ?? 'black'
243
284
  ctx.fillRect(
244
285
  cdsStartPx + 1,
245
286
  cdsTop + 1,
@@ -295,6 +336,9 @@ function draw(
295
336
  currentRow += 1
296
337
  }
297
338
  }
339
+ if (selectedFeature && containsSelectedFeature(feature, selectedFeature)) {
340
+ drawHighlight(stateModel, ctx, selectedFeature, true)
341
+ }
298
342
  }
299
343
 
300
344
  function drawExon(
@@ -308,11 +352,10 @@ function drawExon(
308
352
  forwardFill: CanvasPattern | null,
309
353
  backwardFill: CanvasPattern | null,
310
354
  ) {
311
- const { apolloRowHeight, lgv, session, theme } = stateModel
355
+ const { apolloRowHeight, lgv, theme } = stateModel
312
356
  const { bpPerPx, displayedRegions, offsetPx } = lgv
313
357
  const displayedRegion = displayedRegions[displayedRegionIndex]
314
358
  const { refName, reversed } = displayedRegion
315
- const { apolloSelectedFeature } = session
316
359
 
317
360
  const minX =
318
361
  (lgv.bpToPx({
@@ -326,14 +369,11 @@ function drawExon(
326
369
  const top = (row + currentRow) * apolloRowHeight
327
370
  const exonHeight = Math.round(0.6 * apolloRowHeight)
328
371
  const exonTop = top + (apolloRowHeight - exonHeight) / 2
329
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
372
+ ctx.fillStyle = theme.palette.text.primary
330
373
  ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
331
374
  if (widthPx > 2) {
332
375
  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)'
376
+ ctx.fillStyle = 'rgb(211,211,211)'
337
377
  ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
338
378
  if (forwardFill && backwardFill && strand) {
339
379
  const reversal = reversed ? -1 : 1
@@ -354,6 +394,21 @@ function drawExon(
354
394
  }
355
395
  }
356
396
 
397
+ function* range(start: number, stop: number, step = 1): Generator<number> {
398
+ if (start === stop) {
399
+ return
400
+ }
401
+ if (start < stop) {
402
+ for (let i = start; i < stop; i += step) {
403
+ yield i
404
+ }
405
+ return
406
+ }
407
+ for (let i = start; i > stop; i -= step) {
408
+ yield i
409
+ }
410
+ }
411
+
357
412
  function drawLine(
358
413
  ctx: CanvasRenderingContext2D,
359
414
  stateModel: LinearApolloDisplayRendering,
@@ -372,14 +427,35 @@ function drawLine(
372
427
  coord: transcript.min,
373
428
  regionNumber: displayedRegionIndex,
374
429
  })?.offsetPx ?? 0) - offsetPx
375
- const widthPx = transcript.length / bpPerPx
430
+ const widthPx = Math.round(transcript.length / bpPerPx)
376
431
  const startPx = reversed ? minX - widthPx : minX
377
432
  const height =
378
433
  Math.round((currentRow + 1 / 2) * apolloRowHeight) + row * apolloRowHeight
379
- ctx.strokeStyle = theme?.palette.text.primary ?? 'black'
434
+ ctx.strokeStyle = theme.palette.text.primary
435
+ const { strand = 1 } = transcript
380
436
  ctx.beginPath()
381
- ctx.moveTo(startPx, height)
382
- ctx.lineTo(startPx + widthPx, height)
437
+ // If view is reversed, draw forward as reverse and vice versa
438
+ const effectiveStrand = strand * (reversed ? -1 : 1)
439
+ // Draw the transcript line, and extend it out a bit on the 3` end
440
+ const lineStart = startPx - (effectiveStrand === -1 ? 5 : 0)
441
+ const lineEnd = startPx + widthPx + (effectiveStrand === -1 ? 0 : 5)
442
+ ctx.moveTo(lineStart, height)
443
+ ctx.lineTo(lineEnd, height)
444
+ // Now to draw arrows every 20 pixels along the line
445
+ // Make the arrow range a bit shorter to avoid an arrow hanging off the 5` end
446
+ const arrowsStart = lineStart + (effectiveStrand === -1 ? 0 : 3)
447
+ const arrowsEnd = lineEnd - (effectiveStrand === -1 ? 3 : 0)
448
+ // Offset determines if the arrows face left or right
449
+ const offset = effectiveStrand === -1 ? 3 : -3
450
+ const arrowRange =
451
+ effectiveStrand === -1
452
+ ? range(arrowsStart, arrowsEnd, 20)
453
+ : range(arrowsEnd, arrowsStart, 20)
454
+ for (const arrowLocation of arrowRange) {
455
+ ctx.moveTo(arrowLocation + offset, height + offset)
456
+ ctx.lineTo(arrowLocation, height)
457
+ ctx.lineTo(arrowLocation + offset, height - offset)
458
+ }
383
459
  ctx.stroke()
384
460
  }
385
461
 
@@ -405,24 +481,22 @@ function drawDragPreview(
405
481
  const rectY = row * apolloRowHeight
406
482
  const rectWidth = Math.abs(current.x - featureEdgePx)
407
483
  const rectHeight = apolloRowHeight * rowCount
408
- overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
484
+ overlayCtx.strokeStyle = theme.palette.info.main
409
485
  overlayCtx.setLineDash([6])
410
486
  overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
411
- overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
487
+ overlayCtx.fillStyle = alpha(theme.palette.info.main, 0.2)
412
488
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
413
489
  }
414
490
 
415
- function drawHover(
416
- stateModel: LinearApolloDisplay,
491
+ function drawHighlight(
492
+ stateModel: LinearApolloDisplayRendering,
417
493
  ctx: CanvasRenderingContext2D,
494
+ feature: AnnotationFeature,
495
+ selected = false,
418
496
  ) {
419
- const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel
497
+ const { apolloRowHeight, lgv, session, theme } = stateModel
420
498
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager
421
499
 
422
- if (!apolloHover) {
423
- return
424
- }
425
- const { feature } = apolloHover
426
500
  const position = stateModel.getFeatureLayoutPosition(feature)
427
501
  if (!position) {
428
502
  return
@@ -441,7 +515,9 @@ function drawHover(
441
515
  const row = layoutRow + featureRow
442
516
  const top = row * apolloRowHeight
443
517
  const widthPx = length / bpPerPx
444
- ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)'
518
+ ctx.fillStyle = selected
519
+ ? theme.palette.action.disabled
520
+ : theme.palette.action.focus
445
521
 
446
522
  if (!featureTypeOntology) {
447
523
  throw new Error('featureTypeOntology is undefined')
@@ -454,6 +530,18 @@ function drawHover(
454
530
  )
455
531
  }
456
532
 
533
+ function drawHover(
534
+ stateModel: LinearApolloDisplay,
535
+ ctx: CanvasRenderingContext2D,
536
+ ) {
537
+ const { hoveredFeature } = stateModel
538
+
539
+ if (!hoveredFeature) {
540
+ return
541
+ }
542
+ drawHighlight(stateModel, ctx, hoveredFeature.feature)
543
+ }
544
+
457
545
  function getFeatureFromLayout(
458
546
  feature: AnnotationFeature,
459
547
  bp: number,
@@ -615,13 +703,12 @@ function getRowForFeature(
615
703
 
616
704
  function onMouseDown(
617
705
  stateModel: LinearApolloDisplay,
618
- currentMousePosition: MousePositionWithFeatureAndGlyph,
706
+ currentMousePosition: MousePositionWithFeature,
619
707
  event: CanvasMouseEvent,
620
708
  ) {
621
- const { featureAndGlyphUnderMouse } = currentMousePosition
709
+ const { feature } = currentMousePosition
622
710
  // swallow the mouseDown if we are on the edge of the feature so that we
623
711
  // don't start dragging the view if we try to drag the feature edge
624
- const { feature } = featureAndGlyphUnderMouse
625
712
  const draggableFeature = getDraggableFeatureInfo(
626
713
  currentMousePosition,
627
714
  feature,
@@ -642,10 +729,9 @@ function onMouseMove(
642
729
  stateModel: LinearApolloDisplay,
643
730
  mousePosition: MousePosition,
644
731
  ) {
645
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
646
- const { featureAndGlyphUnderMouse } = mousePosition
647
- stateModel.setApolloHover(featureAndGlyphUnderMouse)
648
- const { feature } = featureAndGlyphUnderMouse
732
+ if (isMousePositionWithFeature(mousePosition)) {
733
+ const { feature, bp } = mousePosition
734
+ stateModel.setHoveredFeature({ feature, bp })
649
735
  const draggableFeature = getDraggableFeatureInfo(
650
736
  mousePosition,
651
737
  feature,
@@ -666,41 +752,11 @@ function onMouseUp(
666
752
  if (stateModel.apolloDragging) {
667
753
  return
668
754
  }
669
- const { featureAndGlyphUnderMouse } = mousePosition
670
- if (!featureAndGlyphUnderMouse) {
755
+ const { feature } = mousePosition
756
+ if (!feature) {
671
757
  return
672
758
  }
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
- }
759
+ selectFeatureAndOpenWidget(stateModel, feature)
704
760
  }
705
761
 
706
762
  function getDraggableFeatureInfo(
@@ -769,49 +825,13 @@ function getDraggableFeatureInfo(
769
825
  return
770
826
  }
771
827
 
772
- function isTranscriptFeature(
773
- feature: AnnotationFeature,
774
- session: ApolloSessionModel,
775
- ): boolean {
776
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
777
- if (!featureTypeOntology) {
778
- throw new Error('featureTypeOntology is undefined')
779
- }
780
- return (
781
- featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
782
- featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
783
- )
784
- }
785
-
786
- function isExonFeature(
787
- feature: AnnotationFeature,
788
- session: ApolloSessionModel,
789
- ): boolean {
790
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
791
- if (!featureTypeOntology) {
792
- throw new Error('featureTypeOntology is undefined')
793
- }
794
- return featureTypeOntology.isTypeOf(feature.type, 'exon')
795
- }
796
-
797
- function isCDSFeature(
798
- feature: AnnotationFeature,
799
- session: ApolloSessionModel,
800
- ): boolean {
801
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
802
- if (!featureTypeOntology) {
803
- throw new Error('featureTypeOntology is undefined')
804
- }
805
- return featureTypeOntology.isTypeOf(feature.type, 'CDS')
806
- }
807
-
808
828
  function getContextMenuItems(
809
829
  display: LinearApolloDisplayMouseEvents,
810
- mousePosition: MousePositionWithFeatureAndGlyph,
830
+ mousePosition: MousePositionWithFeature,
811
831
  ): MenuItem[] {
812
832
  const {
813
833
  apolloInternetAccount: internetAccount,
814
- apolloHover,
834
+ hoveredFeature,
815
835
  changeManager,
816
836
  regions,
817
837
  selectedFeature,
@@ -822,53 +842,120 @@ function getContextMenuItems(
822
842
  const menuItems: MenuItem[] = []
823
843
  const role = internetAccount ? internetAccount.role : 'admin'
824
844
  const admin = role === 'admin'
825
- if (!apolloHover) {
845
+ if (!hoveredFeature) {
826
846
  return menuItems
827
847
  }
828
848
 
829
- let featuresUnderClick = getFeaturesUnderClick(mousePosition)
830
- if (isCDSFeature(mousePosition.featureAndGlyphUnderMouse.feature, session)) {
831
- featuresUnderClick = getFeaturesUnderClick(mousePosition, true)
832
- }
849
+ if (isMousePositionWithFeature(mousePosition)) {
850
+ const { bp, feature } = mousePosition
851
+ let featuresUnderClick = getRelatedFeatures(feature, bp)
852
+ if (isCDSFeature(feature, session)) {
853
+ featuresUnderClick = getRelatedFeatures(feature, bp, true)
854
+ }
833
855
 
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()
856
+ for (const feature of featuresUnderClick) {
857
+ const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
858
+ display,
859
+ feature,
860
+ )
861
+ if (isExonFeature(feature, session)) {
862
+ const adjacentExons = getAdjacentExons(
863
+ feature,
864
+ display,
865
+ mousePosition,
866
+ session,
867
+ )
868
+ const lgv = getContainingView(
869
+ display as BaseDisplayModel,
870
+ ) as unknown as LinearGenomeViewModel
871
+ if (adjacentExons.upstream) {
872
+ const exon = adjacentExons.upstream
873
+ contextMenuItemsForFeature.push({
874
+ label: 'Go to upstream exon',
875
+ icon: getStreamIcon(
876
+ feature.strand,
877
+ true,
878
+ lgv.displayedRegions.at(0)?.reversed,
879
+ ),
880
+ onClick: () => {
881
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
882
+ selectFeatureAndOpenWidget(display, exon)
883
+ },
884
+ })
885
+ }
886
+ if (adjacentExons.downstream) {
887
+ const exon = adjacentExons.downstream
888
+ contextMenuItemsForFeature.push({
889
+ label: 'Go to downstream exon',
890
+ icon: getStreamIcon(
891
+ feature.strand,
892
+ false,
893
+ lgv.displayedRegions.at(0)?.reversed,
894
+ ),
895
+ onClick: () => {
896
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
897
+ selectFeatureAndOpenWidget(display, exon)
898
+ },
899
+ })
900
+ }
901
+ contextMenuItemsForFeature.push(
902
+ {
903
+ label: 'Merge exons',
904
+ disabled: !admin,
905
+ onClick: () => {
906
+ ;(session as unknown as AbstractSessionModel).queueDialog(
907
+ (doneCallback) => [
908
+ MergeExons,
909
+ {
910
+ session,
911
+ handleClose: () => {
912
+ doneCallback()
913
+ },
914
+ changeManager,
915
+ sourceFeature: feature,
916
+ sourceAssemblyId: currentAssemblyId,
917
+ selectedFeature,
918
+ setSelectedFeature: (feature?: AnnotationFeature) => {
919
+ display.setSelectedFeature(feature)
920
+ },
852
921
  },
853
- changeManager,
854
- sourceFeature: feature,
855
- sourceAssemblyId: currentAssemblyId,
856
- selectedFeature,
857
- setSelectedFeature: (feature?: AnnotationFeature) => {
858
- display.setSelectedFeature(feature)
922
+ ],
923
+ )
924
+ },
925
+ },
926
+ {
927
+ label: 'Split exon',
928
+ disabled: !admin,
929
+ onClick: () => {
930
+ ;(session as unknown as AbstractSessionModel).queueDialog(
931
+ (doneCallback) => [
932
+ SplitExon,
933
+ {
934
+ session,
935
+ handleClose: () => {
936
+ doneCallback()
937
+ },
938
+ changeManager,
939
+ sourceFeature: feature,
940
+ sourceAssemblyId: currentAssemblyId,
941
+ selectedFeature,
942
+ setSelectedFeature: (feature?: AnnotationFeature) => {
943
+ display.setSelectedFeature(feature)
944
+ },
859
945
  },
860
- },
861
- ],
862
- )
946
+ ],
947
+ )
948
+ },
863
949
  },
864
- },
865
- {
866
- label: 'Split exon',
867
- disabled: !admin,
950
+ )
951
+ }
952
+ if (isTranscriptFeature(feature, session)) {
953
+ contextMenuItemsForFeature.push({
954
+ label: 'Merge transcript',
868
955
  onClick: () => {
869
956
  ;(session as unknown as AbstractSessionModel).queueDialog(
870
957
  (doneCallback) => [
871
- SplitExon,
958
+ MergeTranscripts,
872
959
  {
873
960
  session,
874
961
  handleClose: () => {
@@ -885,56 +972,31 @@ function getContextMenuItems(
885
972
  ],
886
973
  )
887
974
  },
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
975
  })
976
+ if (isSessionModelWithWidgets(session)) {
977
+ contextMenuItemsForFeature.push({
978
+ label: 'Open transcript details',
979
+ onClick: () => {
980
+ const apolloTranscriptWidget = session.addWidget(
981
+ 'ApolloTranscriptDetails',
982
+ 'apolloTranscriptDetails',
983
+ {
984
+ feature,
985
+ assembly: currentAssemblyId,
986
+ changeManager,
987
+ refName: region.refName,
988
+ },
989
+ )
990
+ session.showWidget(apolloTranscriptWidget)
991
+ },
992
+ })
993
+ }
932
994
  }
995
+ menuItems.push({
996
+ label: feature.type,
997
+ subMenu: contextMenuItemsForFeature,
998
+ })
933
999
  }
934
- menuItems.push({
935
- label: feature.type,
936
- subMenu: contextMenuItemsForFeature,
937
- })
938
1000
  }
939
1001
  return menuItems
940
1002
  }