@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 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 (84) hide show
  1. package/dist/index.esm.js +4603 -2045
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
  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 +9387 -4016
  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 +42 -18
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
  18. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
  19. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  20. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
  21. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
  22. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
  23. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
  24. package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
  25. package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
  26. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  27. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
  28. package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
  29. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  34. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  36. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
  37. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  38. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
  39. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  40. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
  41. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
  42. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
  43. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
  44. package/src/OntologyManager/index.ts +4 -1
  45. package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
  46. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  47. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  48. package/src/components/AddAssembly.tsx +1 -1
  49. package/src/components/AddAssemblyAliases.tsx +114 -0
  50. package/src/components/AddChildFeature.tsx +7 -7
  51. package/src/components/AddFeature.tsx +20 -15
  52. package/src/components/AddRefSeqAliases.tsx +9 -9
  53. package/src/components/CopyFeature.tsx +4 -4
  54. package/src/components/CreateApolloAnnotation.tsx +335 -151
  55. package/src/components/DeleteAssembly.tsx +1 -1
  56. package/src/components/DeleteFeature.tsx +358 -11
  57. package/src/components/DownloadGFF3.tsx +20 -1
  58. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  59. package/src/components/FilterFeatures.tsx +7 -7
  60. package/src/components/FilterTranscripts.tsx +86 -0
  61. package/src/components/ImportFeatures.tsx +1 -1
  62. package/src/components/ManageChecks.tsx +1 -1
  63. package/src/components/MergeExons.tsx +193 -0
  64. package/src/components/MergeTranscripts.tsx +182 -0
  65. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  66. package/src/components/OpenLocalFile.tsx +11 -7
  67. package/src/components/SplitExon.tsx +134 -0
  68. package/src/components/ViewCheckResults.tsx +1 -1
  69. package/src/components/index.ts +4 -0
  70. package/src/config.ts +11 -0
  71. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  72. package/src/extensions/annotationFromPileup.ts +99 -89
  73. package/src/index.ts +42 -105
  74. package/src/makeDisplayComponent.tsx +0 -1
  75. package/src/menus/index.ts +1 -0
  76. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
  77. package/src/menus/topLevelMenuAdmin.ts +154 -0
  78. package/src/session/session.ts +163 -104
  79. package/src/util/annotationFeatureUtils.ts +59 -0
  80. package/src/util/copyToClipboard.ts +21 -0
  81. package/src/util/displayUtils.ts +149 -0
  82. package/src/util/glyphUtils.ts +201 -0
  83. package/src/util/index.ts +2 -0
  84. package/src/util/mouseEventsUtils.ts +145 -0
@@ -1,14 +1,34 @@
1
1
  import { type AnnotationFeature } from '@apollo-annotation/mst'
2
- import { getFrame, intersection2 } from '@jbrowse/core/util'
2
+ import { readConfObject } from '@jbrowse/core/configuration'
3
+ import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
4
+ import { type MenuItem } from '@jbrowse/core/ui'
5
+ import {
6
+ type AbstractSessionModel,
7
+ getContainingView,
8
+ getFrame,
9
+ intersection2,
10
+ isSessionModelWithWidgets,
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'
3
15
  import { alpha } from '@mui/material'
4
16
 
5
17
  import { type OntologyRecord } from '../../OntologyManager'
6
- import { type LinearApolloDisplay } from '../stateModel'
18
+ import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
19
+ import { type ApolloSessionModel } from '../../session'
7
20
  import {
8
21
  type MousePosition,
9
- type MousePositionWithFeatureAndGlyph,
10
- isMousePositionWithFeatureAndGlyph,
11
- } 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'
12
32
  import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
13
33
  import { type CanvasMouseEvent } from '../types'
14
34
 
@@ -19,7 +39,10 @@ let forwardFillLight: CanvasPattern | null = null
19
39
  let backwardFillLight: CanvasPattern | null = null
20
40
  let forwardFillDark: CanvasPattern | null = null
21
41
  let backwardFillDark: CanvasPattern | null = null
22
- if ('document' in globalThis) {
42
+ const canvas = globalThis.document.createElement('canvas')
43
+ // @ts-expect-error getContext is undefined in the web worker
44
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
45
+ if (canvas?.getContext) {
23
46
  for (const direction of ['forward', 'backward']) {
24
47
  for (const themeMode of ['light', 'dark']) {
25
48
  const canvas = document.createElement('canvas')
@@ -63,52 +86,102 @@ if ('document' in globalThis) {
63
86
  }
64
87
  }
65
88
 
66
- function draw(
89
+ function drawBackground(
67
90
  ctx: CanvasRenderingContext2D,
68
91
  feature: AnnotationFeature,
69
- row: number,
70
92
  stateModel: LinearApolloDisplayRendering,
71
93
  displayedRegionIndex: number,
72
- ): void {
73
- const { apolloRowHeight, lgv, session, theme } = stateModel
94
+ row: number,
95
+ color: string,
96
+ ) {
97
+ const { apolloRowHeight, lgv, session } = stateModel
74
98
  const { bpPerPx, displayedRegions, offsetPx } = lgv
75
99
  const displayedRegion = displayedRegions[displayedRegionIndex]
76
100
  const { refName, reversed } = displayedRegion
77
- const rowHeight = apolloRowHeight
78
- const cdsHeight = Math.round(0.9 * rowHeight)
79
- const { children, min, strand } = feature
80
- if (!children) {
81
- return
82
- }
83
- const { apolloSelectedFeature } = session
84
101
  const { apolloDataStore } = session
85
102
  const { featureTypeOntology } = apolloDataStore.ontologyManager
86
103
  if (!featureTypeOntology) {
87
104
  throw new Error('featureTypeOntology is undefined')
88
105
  }
89
106
 
90
- // Draw background for gene
91
107
  const topLevelFeatureMinX =
92
108
  (lgv.bpToPx({
93
109
  refName,
94
- coord: min,
110
+ coord: feature.min,
95
111
  regionNumber: displayedRegionIndex,
96
112
  })?.offsetPx ?? 0) - offsetPx
97
113
  const topLevelFeatureWidthPx = feature.length / bpPerPx
98
114
  const topLevelFeatureStartPx = reversed
99
115
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
100
116
  : topLevelFeatureMinX
101
- const topLevelFeatureTop = row * rowHeight
117
+ const topLevelFeatureTop = row * apolloRowHeight
102
118
  const topLevelFeatureHeight =
103
- getRowCount(feature, featureTypeOntology) * rowHeight
119
+ getRowCount(feature, featureTypeOntology) * apolloRowHeight
104
120
 
105
- ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6)
121
+ ctx.fillStyle = color
106
122
  ctx.fillRect(
107
123
  topLevelFeatureStartPx,
108
124
  topLevelFeatureTop,
109
125
  topLevelFeatureWidthPx,
110
126
  topLevelFeatureHeight,
111
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
+ }
112
185
 
113
186
  // Draw lines on different rows for each transcript
114
187
  let currentRow = 0
@@ -126,6 +199,29 @@ function draw(
126
199
  }
127
200
 
128
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
+
129
225
  for (const [, childFeature] of transcriptChildren) {
130
226
  if (!featureTypeOntology.isTypeOf(childFeature.type, 'CDS')) {
131
227
  continue
@@ -155,9 +251,9 @@ function draw(
155
251
  }
156
252
 
157
253
  const forwardFill =
158
- theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
254
+ theme.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
159
255
  const backwardFill =
160
- theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
256
+ theme.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
161
257
  // Draw exon and CDS for each transcript
162
258
  currentRow = 0
163
259
  for (const [, child] of children) {
@@ -174,7 +270,7 @@ function draw(
174
270
  const cdsCount = getCDSCount(child, featureTypeOntology)
175
271
  if (cdsCount != 0) {
176
272
  for (const cdsRow of child.cdsLocations) {
177
- const { _id, children: transcriptChildren } = child
273
+ const { children: transcriptChildren } = child
178
274
  if (!transcriptChildren) {
179
275
  continue
180
276
  }
@@ -203,7 +299,7 @@ function draw(
203
299
  regionNumber: displayedRegionIndex,
204
300
  })?.offsetPx ?? 0) - offsetPx
205
301
  const cdsStartPx = reversed ? minX - cdsWidthPx : minX
206
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
302
+ ctx.fillStyle = theme.palette.text.primary
207
303
  const cdsTop =
208
304
  (row + currentRow) * rowHeight + (rowHeight - cdsHeight) / 2
209
305
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
@@ -220,12 +316,8 @@ function draw(
220
316
  child.strand ?? 1,
221
317
  cds.phase,
222
318
  )
223
- const frameColor = theme?.palette.framesCDS.at(frame)?.main
224
- const cdsColorCode = frameColor ?? 'rgb(171,71,188)'
225
- ctx.fillStyle =
226
- apolloSelectedFeature && _id === apolloSelectedFeature._id
227
- ? 'rgb(0,0,0)'
228
- : cdsColorCode
319
+ const frameColor = theme.palette.framesCDS.at(frame)?.main
320
+ ctx.fillStyle = frameColor ?? 'black'
229
321
  ctx.fillRect(
230
322
  cdsStartPx + 1,
231
323
  cdsTop + 1,
@@ -281,6 +373,9 @@ function draw(
281
373
  currentRow += 1
282
374
  }
283
375
  }
376
+ if (selectedFeature && containsSelectedFeature(feature, selectedFeature)) {
377
+ drawHighlight(stateModel, ctx, selectedFeature, true)
378
+ }
284
379
  }
285
380
 
286
381
  function drawExon(
@@ -294,11 +389,10 @@ function drawExon(
294
389
  forwardFill: CanvasPattern | null,
295
390
  backwardFill: CanvasPattern | null,
296
391
  ) {
297
- const { apolloRowHeight, lgv, session, theme } = stateModel
392
+ const { apolloRowHeight, lgv, theme } = stateModel
298
393
  const { bpPerPx, displayedRegions, offsetPx } = lgv
299
394
  const displayedRegion = displayedRegions[displayedRegionIndex]
300
395
  const { refName, reversed } = displayedRegion
301
- const { apolloSelectedFeature } = session
302
396
 
303
397
  const minX =
304
398
  (lgv.bpToPx({
@@ -312,14 +406,11 @@ function drawExon(
312
406
  const top = (row + currentRow) * apolloRowHeight
313
407
  const exonHeight = Math.round(0.6 * apolloRowHeight)
314
408
  const exonTop = top + (apolloRowHeight - exonHeight) / 2
315
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
409
+ ctx.fillStyle = theme.palette.text.primary
316
410
  ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
317
411
  if (widthPx > 2) {
318
412
  ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
319
- ctx.fillStyle =
320
- apolloSelectedFeature && exon._id === apolloSelectedFeature._id
321
- ? 'rgb(0,0,0)'
322
- : 'rgb(211,211,211)'
413
+ ctx.fillStyle = 'rgb(211,211,211)'
323
414
  ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
324
415
  if (forwardFill && backwardFill && strand) {
325
416
  const reversal = reversed ? -1 : 1
@@ -340,6 +431,21 @@ function drawExon(
340
431
  }
341
432
  }
342
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
+
343
449
  function drawLine(
344
450
  ctx: CanvasRenderingContext2D,
345
451
  stateModel: LinearApolloDisplayRendering,
@@ -358,14 +464,33 @@ function drawLine(
358
464
  coord: transcript.min,
359
465
  regionNumber: displayedRegionIndex,
360
466
  })?.offsetPx ?? 0) - offsetPx
361
- const widthPx = transcript.length / bpPerPx
467
+ const widthPx = Math.round(transcript.length / bpPerPx)
362
468
  const startPx = reversed ? minX - widthPx : minX
363
469
  const height =
364
470
  Math.round((currentRow + 1 / 2) * apolloRowHeight) + row * apolloRowHeight
365
- ctx.strokeStyle = theme?.palette.text.primary ?? 'black'
471
+ ctx.strokeStyle = theme.palette.text.primary
472
+ const { strand = 1 } = transcript
366
473
  ctx.beginPath()
367
- ctx.moveTo(startPx, height)
368
- 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
+ }
369
494
  ctx.stroke()
370
495
  }
371
496
 
@@ -391,24 +516,22 @@ function drawDragPreview(
391
516
  const rectY = row * apolloRowHeight
392
517
  const rectWidth = Math.abs(current.x - featureEdgePx)
393
518
  const rectHeight = apolloRowHeight * rowCount
394
- overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
519
+ overlayCtx.strokeStyle = theme.palette.info.main
395
520
  overlayCtx.setLineDash([6])
396
521
  overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
397
- overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
522
+ overlayCtx.fillStyle = alpha(theme.palette.info.main, 0.2)
398
523
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
399
524
  }
400
525
 
401
- function drawHover(
402
- stateModel: LinearApolloDisplay,
526
+ function drawHighlight(
527
+ stateModel: LinearApolloDisplayRendering,
403
528
  ctx: CanvasRenderingContext2D,
529
+ feature: AnnotationFeature,
530
+ selected = false,
404
531
  ) {
405
- const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel
532
+ const { apolloRowHeight, lgv, session, theme } = stateModel
406
533
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager
407
534
 
408
- if (!apolloHover) {
409
- return
410
- }
411
- const { feature } = apolloHover
412
535
  const position = stateModel.getFeatureLayoutPosition(feature)
413
536
  if (!position) {
414
537
  return
@@ -427,7 +550,9 @@ function drawHover(
427
550
  const row = layoutRow + featureRow
428
551
  const top = row * apolloRowHeight
429
552
  const widthPx = length / bpPerPx
430
- 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
431
556
 
432
557
  if (!featureTypeOntology) {
433
558
  throw new Error('featureTypeOntology is undefined')
@@ -440,6 +565,18 @@ function drawHover(
440
565
  )
441
566
  }
442
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
+
443
580
  function getFeatureFromLayout(
444
581
  feature: AnnotationFeature,
445
582
  bp: number,
@@ -599,15 +736,53 @@ function getRowForFeature(
599
736
  return
600
737
  }
601
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
+
602
778
  function onMouseDown(
603
779
  stateModel: LinearApolloDisplay,
604
- currentMousePosition: MousePositionWithFeatureAndGlyph,
780
+ currentMousePosition: MousePositionWithFeature,
605
781
  event: CanvasMouseEvent,
606
782
  ) {
607
- const { featureAndGlyphUnderMouse } = currentMousePosition
783
+ const { feature } = currentMousePosition
608
784
  // swallow the mouseDown if we are on the edge of the feature so that we
609
785
  // don't start dragging the view if we try to drag the feature edge
610
- const { feature } = featureAndGlyphUnderMouse
611
786
  const draggableFeature = getDraggableFeatureInfo(
612
787
  currentMousePosition,
613
788
  feature,
@@ -619,6 +794,7 @@ function onMouseDown(
619
794
  currentMousePosition,
620
795
  draggableFeature.feature,
621
796
  draggableFeature.edge,
797
+ true,
622
798
  )
623
799
  }
624
800
  }
@@ -627,10 +803,9 @@ function onMouseMove(
627
803
  stateModel: LinearApolloDisplay,
628
804
  mousePosition: MousePosition,
629
805
  ) {
630
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
631
- const { featureAndGlyphUnderMouse } = mousePosition
632
- stateModel.setApolloHover(featureAndGlyphUnderMouse)
633
- const { feature } = featureAndGlyphUnderMouse
806
+ if (isMousePositionWithFeature(mousePosition)) {
807
+ const { feature, bp } = mousePosition
808
+ stateModel.setHoveredFeature({ feature, bp })
634
809
  const draggableFeature = getDraggableFeatureInfo(
635
810
  mousePosition,
636
811
  feature,
@@ -651,10 +826,11 @@ function onMouseUp(
651
826
  if (stateModel.apolloDragging) {
652
827
  return
653
828
  }
654
- const { featureAndGlyphUnderMouse } = mousePosition
655
- if (featureAndGlyphUnderMouse?.feature) {
656
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
829
+ const { feature } = mousePosition
830
+ if (!feature) {
831
+ return
657
832
  }
833
+ selectFeatureAndOpenWidget(stateModel, feature)
658
834
  }
659
835
 
660
836
  function getDraggableFeatureInfo(
@@ -674,31 +850,18 @@ function getDraggableFeatureInfo(
674
850
  const isTranscript =
675
851
  featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
676
852
  featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
677
- const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS')
853
+ const isCDS = featureTypeOntology.isTypeOf(feature.type, 'CDS')
678
854
  if (isGene || isTranscript) {
855
+ // For gene glyphs, the sizes of genes and transcripts are determined by
856
+ // their child exons, so we don't make them draggable
679
857
  return
680
858
  }
859
+ // So now the type of feature is either CDS or exon. If an exon and CDS edge
860
+ // are in the same place, we want to prioritize dragging the exon. If the
861
+ // feature we're on is a CDS, let's find any exon it may overlap.
681
862
  const { bp, refName, regionNumber, x } = mousePosition
682
863
  const { lgv } = stateModel
683
- const { offsetPx } = lgv
684
-
685
- const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber })
686
- const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber })
687
- if (minPxInfo === undefined || maxPxInfo === undefined) {
688
- return
689
- }
690
- const minPx = minPxInfo.offsetPx - offsetPx
691
- const maxPx = maxPxInfo.offsetPx - offsetPx
692
- if (Math.abs(maxPx - minPx) < 8) {
693
- return
694
- }
695
- if (Math.abs(minPx - x) < 4) {
696
- return { feature, edge: 'min' }
697
- }
698
- if (Math.abs(maxPx - x) < 4) {
699
- return { feature, edge: 'max' }
700
- }
701
- if (isCds) {
864
+ if (isCDS) {
702
865
  const transcript = feature.parent
703
866
  if (!transcript?.children) {
704
867
  return
@@ -710,46 +873,336 @@ function getDraggableFeatureInfo(
710
873
  exonChildren.push(child)
711
874
  }
712
875
  }
713
-
714
876
  const overlappingExon = exonChildren.find((child) => {
715
877
  const [start, end] = intersection2(bp - 1, bp, child.min, child.max)
716
878
  return start !== undefined && end !== undefined
717
879
  })
880
+ if (overlappingExon) {
881
+ // We are on an exon, are we on the edge of it?
882
+ const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv)
883
+ if (minMax) {
884
+ const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax)
885
+ if (overlappingEdge) {
886
+ return overlappingEdge
887
+ }
888
+ }
889
+ }
890
+ }
891
+ // End of special cases, let's see if we're on the edge of this CDS or exon
892
+ const minMax = getMinAndMaxPx(feature, refName, regionNumber, lgv)
893
+ if (minMax) {
894
+ const overlappingEdge = getOverlappingEdge(feature, x, minMax)
895
+ if (overlappingEdge) {
896
+ return overlappingEdge
897
+ }
898
+ }
899
+ return
900
+ }
718
901
 
719
- if (!overlappingExon) {
720
- return
902
+ function isTranscriptFeature(
903
+ feature: AnnotationFeature,
904
+ session: ApolloSessionModel,
905
+ ): boolean {
906
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
907
+ if (!featureTypeOntology) {
908
+ throw new Error('featureTypeOntology is undefined')
909
+ }
910
+ return (
911
+ featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
912
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
913
+ )
914
+ }
915
+
916
+ function isExonFeature(
917
+ feature: AnnotationFeature,
918
+ session: ApolloSessionModel,
919
+ ): boolean {
920
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
921
+ if (!featureTypeOntology) {
922
+ throw new Error('featureTypeOntology is undefined')
923
+ }
924
+ return featureTypeOntology.isTypeOf(feature.type, 'exon')
925
+ }
926
+
927
+ function isCDSFeature(
928
+ feature: AnnotationFeature,
929
+ session: ApolloSessionModel,
930
+ ): boolean {
931
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
932
+ if (!featureTypeOntology) {
933
+ throw new Error('featureTypeOntology is undefined')
934
+ }
935
+ return featureTypeOntology.isTypeOf(feature.type, 'CDS')
936
+ }
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)
721
972
  }
722
- const minPxInfo = lgv.bpToPx({
723
- refName,
724
- coord: overlappingExon.min,
725
- regionNumber,
726
- })
727
- const maxPxInfo = lgv.bpToPx({
728
- refName,
729
- coord: overlappingExon.max,
730
- regionNumber,
731
- })
732
- if (minPxInfo === undefined || maxPxInfo === undefined) {
733
- return
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
734
983
  }
735
- const minPx = minPxInfo.offsetPx - offsetPx
736
- const maxPx = maxPxInfo.offsetPx - offsetPx
737
- if (Math.abs(maxPx - minPx) < 8) {
738
- return
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
739
990
  }
740
- if (Math.abs(minPx - x) < 4) {
741
- return { feature: overlappingExon, edge: 'min' }
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
+
1027
+ function getContextMenuItems(
1028
+ display: LinearApolloDisplayMouseEvents,
1029
+ mousePosition: MousePositionWithFeature,
1030
+ ): MenuItem[] {
1031
+ const {
1032
+ apolloInternetAccount: internetAccount,
1033
+ hoveredFeature,
1034
+ changeManager,
1035
+ regions,
1036
+ selectedFeature,
1037
+ session,
1038
+ } = display
1039
+ const [region] = regions
1040
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
1041
+ const menuItems: MenuItem[] = []
1042
+ const role = internetAccount ? internetAccount.role : 'admin'
1043
+ const admin = role === 'admin'
1044
+ if (!hoveredFeature) {
1045
+ return menuItems
1046
+ }
1047
+
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)
742
1053
  }
743
- if (Math.abs(maxPx - x) < 4) {
744
- return { feature: overlappingExon, edge: 'max' }
1054
+
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
+ },
1120
+ },
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
+ },
1144
+ },
1145
+ ],
1146
+ )
1147
+ },
1148
+ },
1149
+ )
1150
+ }
1151
+ if (isTranscriptFeature(feature, session)) {
1152
+ contextMenuItemsForFeature.push({
1153
+ label: 'Merge transcript',
1154
+ onClick: () => {
1155
+ ;(session as unknown as AbstractSessionModel).queueDialog(
1156
+ (doneCallback) => [
1157
+ MergeTranscripts,
1158
+ {
1159
+ session,
1160
+ handleClose: () => {
1161
+ doneCallback()
1162
+ },
1163
+ changeManager,
1164
+ sourceFeature: feature,
1165
+ sourceAssemblyId: currentAssemblyId,
1166
+ selectedFeature,
1167
+ setSelectedFeature: (feature?: AnnotationFeature) => {
1168
+ display.setSelectedFeature(feature)
1169
+ },
1170
+ },
1171
+ ],
1172
+ )
1173
+ },
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
+ }
1193
+ }
1194
+ menuItems.push({
1195
+ label: feature.type,
1196
+ subMenu: contextMenuItemsForFeature,
1197
+ })
745
1198
  }
746
1199
  }
747
- return
1200
+ return menuItems
748
1201
  }
749
1202
 
750
1203
  // False positive here, none of these functions use "this"
751
1204
  /* eslint-disable @typescript-eslint/unbound-method */
752
- const { drawTooltip, getContextMenuItems, onMouseLeave } = boxGlyph
1205
+ const { drawTooltip, getContextMenuItemsForFeature, onMouseLeave } = boxGlyph
753
1206
  /* eslint-enable @typescript-eslint/unbound-method */
754
1207
 
755
1208
  export const geneGlyph: Glyph = {
@@ -758,6 +1211,7 @@ export const geneGlyph: Glyph = {
758
1211
  drawHover,
759
1212
  drawTooltip,
760
1213
  getContextMenuItems,
1214
+ getContextMenuItemsForFeature,
761
1215
  getFeatureFromLayout,
762
1216
  getRowCount,
763
1217
  getRowForFeature,