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

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 (136) hide show
  1. package/dist/index.esm.js +6964 -4598
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +6610 -4261
  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 +11563 -7493
  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 +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +23 -2
  13. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -0
  14. package/src/ApolloInternetAccount/components/LoginButtons.tsx +1 -1
  15. package/src/ApolloInternetAccount/components/LoginIcons.tsx +1 -1
  16. package/src/ApolloInternetAccount/configSchema.ts +1 -1
  17. package/src/ApolloInternetAccount/model.ts +11 -10
  18. package/src/ApolloJobModel.ts +1 -1
  19. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +8 -6
  20. package/src/ApolloRefNameAliasAdapter/index.ts +2 -2
  21. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +4 -4
  22. package/src/ApolloSequenceAdapter/index.ts +1 -1
  23. package/src/ApolloTextSearchAdapter/ApolloTextSearchAdapter.ts +8 -7
  24. package/src/ApolloTextSearchAdapter/index.ts +1 -1
  25. package/src/BackendDrivers/BackendDriver.ts +7 -7
  26. package/src/BackendDrivers/CollaborationServerDriver.ts +14 -10
  27. package/src/BackendDrivers/DesktopFileDriver.ts +11 -10
  28. package/src/BackendDrivers/InMemoryFileDriver.ts +10 -6
  29. package/src/ChangeManager.ts +15 -11
  30. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +8 -7
  31. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +35 -14
  32. package/src/FeatureDetailsWidget/AttributeKey.tsx +50 -0
  33. package/src/FeatureDetailsWidget/AttributeKeySelector.tsx +104 -0
  34. package/src/FeatureDetailsWidget/Attributes.tsx +215 -367
  35. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -5
  36. package/src/FeatureDetailsWidget/DefaultAttributeEditor.tsx +104 -0
  37. package/src/FeatureDetailsWidget/DefaultAttributeViewer.tsx +22 -0
  38. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +4 -4
  39. package/src/FeatureDetailsWidget/NumberTextField.tsx +1 -1
  40. package/src/FeatureDetailsWidget/Sequence.tsx +2 -2
  41. package/src/FeatureDetailsWidget/StringTextField.tsx +1 -1
  42. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +15 -23
  43. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +950 -196
  44. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +8 -4
  45. package/src/FeatureDetailsWidget/model.ts +8 -3
  46. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +7 -7
  47. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +59 -72
  48. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +253 -60
  49. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +52 -6
  50. package/src/LinearApolloDisplay/glyphs/Glyph.ts +16 -8
  51. package/src/LinearApolloDisplay/stateModel/base.ts +81 -10
  52. package/src/LinearApolloDisplay/stateModel/index.ts +4 -3
  53. package/src/LinearApolloDisplay/stateModel/layouts.ts +8 -39
  54. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +63 -46
  55. package/src/LinearApolloDisplay/stateModel/rendering.ts +60 -31
  56. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +226 -0
  57. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +32 -0
  58. package/src/LinearApolloSixFrameDisplay/components/index.ts +2 -0
  59. package/src/LinearApolloSixFrameDisplay/configSchema.ts +7 -0
  60. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +940 -0
  61. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +63 -0
  62. package/src/LinearApolloSixFrameDisplay/glyphs/index.ts +1 -0
  63. package/src/LinearApolloSixFrameDisplay/index.ts +2 -0
  64. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +302 -0
  65. package/src/LinearApolloSixFrameDisplay/stateModel/index.ts +27 -0
  66. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +252 -0
  67. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +368 -0
  68. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +201 -0
  69. package/src/LinearApolloSixFrameDisplay/types.ts +1 -0
  70. package/src/OntologyManager/OntologyStore/fulltext.test.ts +1 -1
  71. package/src/OntologyManager/OntologyStore/fulltext.ts +8 -3
  72. package/src/OntologyManager/OntologyStore/index.test.ts +3 -1
  73. package/src/OntologyManager/OntologyStore/index.ts +19 -14
  74. package/src/OntologyManager/OntologyStore/indexeddb-schema.ts +6 -5
  75. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +11 -5
  76. package/src/OntologyManager/index.ts +12 -7
  77. package/src/OntologyManager/util.ts +3 -2
  78. package/src/TabularEditor/HybridGrid/ChangeHandling.ts +2 -2
  79. package/src/TabularEditor/HybridGrid/Feature.tsx +13 -7
  80. package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +1 -1
  81. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +3 -2
  82. package/src/TabularEditor/HybridGrid/ToolBar.tsx +1 -1
  83. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +114 -22
  84. package/src/TabularEditor/TabularEditorPane.tsx +1 -1
  85. package/src/TabularEditor/model.ts +2 -2
  86. package/src/TabularEditor/types.ts +5 -2
  87. package/src/components/AddAssembly.tsx +182 -179
  88. package/src/components/AddAssemblyAliases.tsx +114 -0
  89. package/src/components/AddChildFeature.tsx +8 -10
  90. package/src/components/AddFeature.tsx +216 -44
  91. package/src/components/AddRefSeqAliases.tsx +14 -12
  92. package/src/components/CopyFeature.tsx +10 -11
  93. package/src/components/CreateApolloAnnotation.tsx +342 -158
  94. package/src/components/DeleteAssembly.tsx +9 -8
  95. package/src/components/DeleteFeature.tsx +362 -14
  96. package/src/components/Dialog.tsx +1 -1
  97. package/src/components/DownloadGFF3.tsx +31 -11
  98. package/src/components/FilterFeatures.tsx +6 -4
  99. package/src/components/FilterTranscripts.tsx +86 -0
  100. package/src/components/ImportFeatures.tsx +7 -6
  101. package/src/components/LogOut.tsx +5 -4
  102. package/src/components/ManageChecks.tsx +9 -8
  103. package/src/components/ManageUsers.tsx +11 -10
  104. package/src/components/MergeExons.tsx +193 -0
  105. package/src/components/MergeTranscripts.tsx +185 -0
  106. package/src/components/OntologyTermAutocomplete.tsx +5 -5
  107. package/src/components/OntologyTermMultiSelect.tsx +6 -6
  108. package/src/components/OpenLocalFile.tsx +4 -3
  109. package/src/components/SplitExon.tsx +134 -0
  110. package/src/components/ViewChangeLog.tsx +7 -6
  111. package/src/components/ViewCheckResults.tsx +8 -7
  112. package/src/components/index.ts +3 -0
  113. package/src/config.ts +5 -0
  114. package/src/extensions/annotationFromJBrowseFeature.test.ts +1 -0
  115. package/src/extensions/annotationFromJBrowseFeature.ts +13 -10
  116. package/src/extensions/annotationFromPileup.ts +104 -94
  117. package/src/index.ts +33 -50
  118. package/src/makeDisplayComponent.tsx +90 -37
  119. package/src/session/ClientDataStore.ts +21 -17
  120. package/src/session/session.ts +46 -39
  121. package/src/types.ts +4 -4
  122. package/src/util/annotationFeatureUtils.ts +66 -1
  123. package/src/util/copyToClipboard.ts +21 -0
  124. package/src/util/glyphUtils.ts +49 -0
  125. package/src/util/index.ts +5 -3
  126. package/src/util/loadAssemblyIntoClient.ts +10 -3
  127. package/src/util/mouseEventsUtils.ts +113 -0
  128. package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +0 -13
  129. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +0 -707
  130. package/src/ApolloSixFrameRenderer/configSchema.ts +0 -7
  131. package/src/ApolloSixFrameRenderer/index.ts +0 -3
  132. package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +0 -19
  133. package/src/SixFrameFeatureDisplay/components/index.ts +0 -1
  134. package/src/SixFrameFeatureDisplay/configSchema.ts +0 -21
  135. package/src/SixFrameFeatureDisplay/index.ts +0 -2
  136. package/src/SixFrameFeatureDisplay/stateModel.ts +0 -443
@@ -1,10 +1,10 @@
1
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import styled from '@emotion/styled'
3
+ import { Table, TableBody, TableCell, TableRow } from '@mui/material'
4
+ import { observer } from 'mobx-react'
1
5
  import React from 'react'
2
6
 
3
- import { AnnotationFeature } from '@apollo-annotation/mst'
4
- import { observer } from 'mobx-react'
5
7
  import { getFeatureId, getFeatureName, getStrand } from '../util'
6
- import { Table, TableBody, TableCell, TableRow } from '@mui/material'
7
- import styled from '@emotion/styled'
8
8
 
9
9
  const HeaderTableCell = styled(TableCell)(() => ({
10
10
  fontWeight: 'bold',
@@ -37,6 +37,10 @@ export const TranscriptWidgetSummary = observer(
37
37
  <TableCell>{getFeatureId(feature)}</TableCell>
38
38
  </TableRow>
39
39
  )}
40
+ <TableRow>
41
+ <HeaderTableCell>Type</HeaderTableCell>
42
+ <TableCell>{feature.type}</TableCell>
43
+ </TableRow>
40
44
  <TableRow>
41
45
  <HeaderTableCell>Location</HeaderTableCell>
42
46
  <TableCell>
@@ -1,15 +1,20 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-call */
2
2
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
3
3
  import {
4
- AnnotationFeature,
4
+ type AnnotationFeature,
5
5
  AnnotationFeatureModel,
6
6
  } from '@apollo-annotation/mst'
7
7
  import { getSession } from '@jbrowse/core/util'
8
8
  import { ElementId } from '@jbrowse/core/util/types/mst'
9
9
  import { autorun } from 'mobx'
10
- import { Instance, SnapshotIn, addDisposer, types } from 'mobx-state-tree'
10
+ import {
11
+ type Instance,
12
+ type SnapshotIn,
13
+ addDisposer,
14
+ types,
15
+ } from 'mobx-state-tree'
11
16
 
12
- import { ApolloSessionModel } from '../session'
17
+ import { type ApolloSessionModel } from '../session'
13
18
 
14
19
  export const ApolloFeatureDetailsWidgetModel = types
15
20
  .model('ApolloFeatureDetailsWidget', {
@@ -1,13 +1,13 @@
1
1
  /* eslint-disable @typescript-eslint/unbound-method */
2
2
  /* eslint-disable @typescript-eslint/no-misused-promises */
3
3
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
4
- import { Menu, MenuItem } from '@jbrowse/core/ui'
4
+ import { Menu, type MenuItem } from '@jbrowse/core/ui'
5
5
  import {
6
- AbstractSessionModel,
6
+ type AbstractSessionModel,
7
7
  doesIntersect2,
8
8
  getContainingView,
9
9
  } from '@jbrowse/core/util'
10
- import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
10
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
11
11
  import ErrorIcon from '@mui/icons-material/Error'
12
12
  import {
13
13
  Alert,
@@ -20,7 +20,7 @@ import { observer } from 'mobx-react'
20
20
  import React, { useEffect, useState } from 'react'
21
21
  import { makeStyles } from 'tss-react/mui'
22
22
 
23
- import { LinearApolloDisplay as LinearApolloDisplayI } from '../stateModel'
23
+ import { type LinearApolloDisplay as LinearApolloDisplayI } from '../stateModel'
24
24
 
25
25
  interface LinearApolloDisplayProps {
26
26
  model: LinearApolloDisplayI
@@ -95,7 +95,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
95
95
  const { assemblyManager } = session as unknown as AbstractSessionModel
96
96
  return (
97
97
  <>
98
- {lgv.bpPerPx <= 3 ? (
98
+ {3 / lgv.bpPerPx >= 1 ? (
99
99
  <div
100
100
  className={classes.canvasContainer}
101
101
  style={{
@@ -139,7 +139,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
139
139
  } else {
140
140
  const coord: [number, number] = [event.clientX, event.clientY]
141
141
  setContextCoord(coord)
142
- setContextMenuItems(getContextMenuItems(coord))
142
+ setContextMenuItems(getContextMenuItems(event))
143
143
  }
144
144
  }}
145
145
  >
@@ -238,7 +238,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
238
238
  pointerEvents: apolloDragging ? 'none' : 'auto',
239
239
  }}
240
240
  >
241
- <ErrorIcon />
241
+ <ErrorIcon data-testid="ErrorIcon" />
242
242
  </Avatar>
243
243
  </Tooltip>
244
244
  )
@@ -1,25 +1,20 @@
1
- import { AnnotationFeature } from '@apollo-annotation/mst'
2
- import { Theme, alpha } from '@mui/material'
3
- import { MenuItem } from '@jbrowse/core/ui'
4
-
5
- import {
6
- AbstractSessionModel,
7
- isSessionModelWithWidgets,
8
- SessionWithWidgets,
9
- } from '@jbrowse/core/util'
1
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import { type MenuItem } from '@jbrowse/core/ui'
3
+ import { type AbstractSessionModel } from '@jbrowse/core/util'
4
+ import { type Theme, alpha } from '@mui/material'
10
5
 
11
6
  import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
12
-
13
- import { LinearApolloDisplay } from '../stateModel'
7
+ import { type LinearApolloDisplay } from '../stateModel'
14
8
  import {
9
+ type LinearApolloDisplayMouseEvents,
10
+ type MousePosition,
11
+ type MousePositionWithFeatureAndGlyph,
15
12
  isMousePositionWithFeatureAndGlyph,
16
- LinearApolloDisplayMouseEvents,
17
- MousePosition,
18
- MousePositionWithFeatureAndGlyph,
19
13
  } from '../stateModel/mouseEvents'
20
- import { CanvasMouseEvent } from '../types'
21
- import { Glyph } from './Glyph'
22
- import { LinearApolloDisplayRendering } from '../stateModel/rendering'
14
+ import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
15
+ import { type CanvasMouseEvent } from '../types'
16
+
17
+ import { type Glyph } from './Glyph'
23
18
 
24
19
  function drawBoxOutline(
25
20
  ctx: CanvasRenderingContext2D,
@@ -258,9 +253,39 @@ export function drawBox(
258
253
 
259
254
  function getContextMenuItems(
260
255
  display: LinearApolloDisplayMouseEvents,
256
+ ): MenuItem[] {
257
+ const { apolloHover } = display
258
+ if (!apolloHover) {
259
+ return []
260
+ }
261
+ const { feature: sourceFeature } = apolloHover
262
+ return getContextMenuItemsForFeature(display, sourceFeature)
263
+ }
264
+
265
+ function makeFeatureLabel(feature: AnnotationFeature) {
266
+ let name: string | undefined
267
+ if (feature.attributes.get('gff_name')) {
268
+ name = feature.attributes.get('gff_name')?.join(',')
269
+ } else if (feature.attributes.get('gff_id')) {
270
+ name = feature.attributes.get('gff_id')?.join(',')
271
+ } else {
272
+ name = feature._id
273
+ }
274
+ const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`
275
+ const maxLen = 60
276
+ if (name && name.length + coords.length > maxLen + 5) {
277
+ const trim = maxLen - coords.length
278
+ name = trim > 0 ? name.slice(0, trim) : ''
279
+ name = `${name}[...]`
280
+ }
281
+ return `${name} ${coords}`
282
+ }
283
+
284
+ function getContextMenuItemsForFeature(
285
+ display: LinearApolloDisplayMouseEvents,
286
+ sourceFeature: AnnotationFeature,
261
287
  ): MenuItem[] {
262
288
  const {
263
- apolloHover,
264
289
  apolloInternetAccount: internetAccount,
265
290
  changeManager,
266
291
  regions,
@@ -268,17 +293,23 @@ function getContextMenuItems(
268
293
  session,
269
294
  } = display
270
295
  const menuItems: MenuItem[] = []
271
- if (!apolloHover) {
272
- return menuItems
273
- }
274
- const { feature: sourceFeature } = apolloHover
275
296
  const role = internetAccount ? internetAccount.role : 'admin'
276
297
  const admin = role === 'admin'
277
298
  const readOnly = !(role && ['admin', 'user'].includes(role))
278
299
  const [region] = regions
279
300
  const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
280
301
  const currentAssemblyId = display.getAssemblyId(region.assemblyName)
302
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
303
+ if (!featureTypeOntology) {
304
+ throw new Error('featureTypeOntology is undefined')
305
+ }
306
+
307
+ // Add only relevant options
281
308
  menuItems.push(
309
+ {
310
+ label: makeFeatureLabel(sourceFeature),
311
+ type: 'subHeader',
312
+ },
282
313
  {
283
314
  label: 'Add child feature',
284
315
  disabled: readOnly,
@@ -344,55 +375,7 @@ function getContextMenuItems(
344
375
  )
345
376
  },
346
377
  },
347
- {
348
- label: 'Edit feature details',
349
- onClick: () => {
350
- const apolloFeatureWidget = (
351
- session as unknown as SessionWithWidgets
352
- ).addWidget(
353
- 'ApolloFeatureDetailsWidget',
354
- 'apolloFeatureDetailsWidget',
355
- {
356
- feature: sourceFeature,
357
- assembly: currentAssemblyId,
358
- refName: region.refName,
359
- },
360
- )
361
- ;(session as unknown as SessionWithWidgets).showWidget(
362
- apolloFeatureWidget,
363
- )
364
- },
365
- },
366
378
  )
367
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
368
- if (!featureTypeOntology) {
369
- throw new Error('featureTypeOntology is undefined')
370
- }
371
- if (
372
- (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
373
- featureTypeOntology.isTypeOf(
374
- sourceFeature.type,
375
- 'pseudogenic_transcript',
376
- )) &&
377
- isSessionModelWithWidgets(session)
378
- ) {
379
- menuItems.push({
380
- label: 'Edit transcript details',
381
- onClick: () => {
382
- const apolloTranscriptWidget = session.addWidget(
383
- 'ApolloTranscriptDetails',
384
- 'apolloTranscriptDetails',
385
- {
386
- feature: sourceFeature,
387
- assembly: currentAssemblyId,
388
- changeManager,
389
- refName: region.refName,
390
- },
391
- )
392
- session.showWidget(apolloTranscriptWidget)
393
- },
394
- })
395
- }
396
379
  return menuItems
397
380
  }
398
381
 
@@ -460,9 +443,12 @@ function onMouseUp(
460
443
  return
461
444
  }
462
445
  const { featureAndGlyphUnderMouse } = mousePosition
463
- if (featureAndGlyphUnderMouse?.feature) {
464
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
446
+ if (!featureAndGlyphUnderMouse) {
447
+ return
465
448
  }
449
+ const { feature } = featureAndGlyphUnderMouse
450
+ stateModel.setSelectedFeature(feature)
451
+ stateModel.showFeatureDetailsWidget(feature)
466
452
  }
467
453
 
468
454
  /** @returns undefined if mouse not on the edge of this feature, otherwise 'start' or 'end' depending on which edge */
@@ -497,6 +483,7 @@ export const boxGlyph: Glyph = {
497
483
  drawDragPreview,
498
484
  drawHover,
499
485
  drawTooltip,
486
+ getContextMenuItemsForFeature,
500
487
  getContextMenuItems,
501
488
  getFeatureFromLayout,
502
489
  getRowCount,
@@ -1,24 +1,39 @@
1
- import { AnnotationFeature } from '@apollo-annotation/mst'
2
- import { getFrame, intersection2 } from '@jbrowse/core/util'
1
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import { type MenuItem } from '@jbrowse/core/ui'
3
+ import {
4
+ type AbstractSessionModel,
5
+ getFrame,
6
+ intersection2,
7
+ isSessionModelWithWidgets,
8
+ } from '@jbrowse/core/util'
3
9
  import { alpha } from '@mui/material'
4
10
 
5
- import { LinearApolloDisplay } from '../stateModel'
11
+ import { type OntologyRecord } from '../../OntologyManager'
12
+ 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'
6
17
  import {
18
+ type LinearApolloDisplayMouseEvents,
19
+ type MousePosition,
20
+ type MousePositionWithFeatureAndGlyph,
7
21
  isMousePositionWithFeatureAndGlyph,
8
- MousePosition,
9
- MousePositionWithFeatureAndGlyph,
10
22
  } from '../stateModel/mouseEvents'
11
- import { CanvasMouseEvent } from '../types'
12
- import { Glyph } from './Glyph'
23
+ import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
24
+ import { type CanvasMouseEvent } from '../types'
25
+
13
26
  import { boxGlyph } from './BoxGlyph'
14
- import { LinearApolloDisplayRendering } from '../stateModel/rendering'
15
- import { OntologyRecord } from '../../OntologyManager'
27
+ import { type Glyph } from './Glyph'
16
28
 
17
29
  let forwardFillLight: CanvasPattern | null = null
18
30
  let backwardFillLight: CanvasPattern | null = null
19
31
  let forwardFillDark: CanvasPattern | null = null
20
32
  let backwardFillDark: CanvasPattern | null = null
21
- if ('document' in globalThis) {
33
+ const canvas = globalThis.document.createElement('canvas')
34
+ // @ts-expect-error getContext is undefined in the web worker
35
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
36
+ if (canvas?.getContext) {
22
37
  for (const direction of ['forward', 'backward']) {
23
38
  for (const themeMode of ['light', 'dark']) {
24
39
  const canvas = document.createElement('canvas')
@@ -225,7 +240,6 @@ function draw(
225
240
  apolloSelectedFeature && _id === apolloSelectedFeature._id
226
241
  ? 'rgb(0,0,0)'
227
242
  : cdsColorCode
228
- ctx.fillStyle = cdsColorCode
229
243
  ctx.fillRect(
230
244
  cdsStartPx + 1,
231
245
  cdsTop + 1,
@@ -619,6 +633,7 @@ function onMouseDown(
619
633
  currentMousePosition,
620
634
  draggableFeature.feature,
621
635
  draggableFeature.edge,
636
+ true,
622
637
  )
623
638
  }
624
639
  }
@@ -652,8 +667,39 @@ function onMouseUp(
652
667
  return
653
668
  }
654
669
  const { featureAndGlyphUnderMouse } = mousePosition
655
- if (featureAndGlyphUnderMouse?.feature) {
656
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
670
+ if (!featureAndGlyphUnderMouse) {
671
+ return
672
+ }
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)
657
703
  }
658
704
  }
659
705
 
@@ -674,31 +720,18 @@ function getDraggableFeatureInfo(
674
720
  const isTranscript =
675
721
  featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
676
722
  featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
677
- const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS')
723
+ const isCDS = featureTypeOntology.isTypeOf(feature.type, 'CDS')
678
724
  if (isGene || isTranscript) {
725
+ // For gene glyphs, the sizes of genes and transcripts are determined by
726
+ // their child exons, so we don't make them draggable
679
727
  return
680
728
  }
729
+ // So now the type of feature is either CDS or exon. If an exon and CDS edge
730
+ // are in the same place, we want to prioritize dragging the exon. If the
731
+ // feature we're on is a CDS, let's find any exon it may overlap.
681
732
  const { bp, refName, regionNumber, x } = mousePosition
682
733
  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) {
734
+ if (isCDS) {
702
735
  const transcript = feature.parent
703
736
  if (!transcript?.children) {
704
737
  return
@@ -710,46 +743,205 @@ function getDraggableFeatureInfo(
710
743
  exonChildren.push(child)
711
744
  }
712
745
  }
713
-
714
746
  const overlappingExon = exonChildren.find((child) => {
715
747
  const [start, end] = intersection2(bp - 1, bp, child.min, child.max)
716
748
  return start !== undefined && end !== undefined
717
749
  })
718
-
719
- if (!overlappingExon) {
720
- return
721
- }
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
750
+ if (overlappingExon) {
751
+ // We are on an exon, are we on the edge of it?
752
+ const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv)
753
+ if (minMax) {
754
+ const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax)
755
+ if (overlappingEdge) {
756
+ return overlappingEdge
757
+ }
758
+ }
734
759
  }
735
- const minPx = minPxInfo.offsetPx - offsetPx
736
- const maxPx = maxPxInfo.offsetPx - offsetPx
737
- if (Math.abs(maxPx - minPx) < 8) {
738
- return
760
+ }
761
+ // End of special cases, let's see if we're on the edge of this CDS or exon
762
+ const minMax = getMinAndMaxPx(feature, refName, regionNumber, lgv)
763
+ if (minMax) {
764
+ const overlappingEdge = getOverlappingEdge(feature, x, minMax)
765
+ if (overlappingEdge) {
766
+ return overlappingEdge
739
767
  }
740
- if (Math.abs(minPx - x) < 4) {
741
- return { feature: overlappingExon, edge: 'min' }
768
+ }
769
+ return
770
+ }
771
+
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
+ function getContextMenuItems(
809
+ display: LinearApolloDisplayMouseEvents,
810
+ mousePosition: MousePositionWithFeatureAndGlyph,
811
+ ): MenuItem[] {
812
+ const {
813
+ apolloInternetAccount: internetAccount,
814
+ apolloHover,
815
+ changeManager,
816
+ regions,
817
+ selectedFeature,
818
+ session,
819
+ } = display
820
+ const [region] = regions
821
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
822
+ const menuItems: MenuItem[] = []
823
+ const role = internetAccount ? internetAccount.role : 'admin'
824
+ const admin = role === 'admin'
825
+ if (!apolloHover) {
826
+ return menuItems
827
+ }
828
+
829
+ let featuresUnderClick = getFeaturesUnderClick(mousePosition)
830
+ if (isCDSFeature(mousePosition.featureAndGlyphUnderMouse.feature, session)) {
831
+ featuresUnderClick = getFeaturesUnderClick(mousePosition, true)
832
+ }
833
+
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()
852
+ },
853
+ changeManager,
854
+ sourceFeature: feature,
855
+ sourceAssemblyId: currentAssemblyId,
856
+ selectedFeature,
857
+ setSelectedFeature: (feature?: AnnotationFeature) => {
858
+ display.setSelectedFeature(feature)
859
+ },
860
+ },
861
+ ],
862
+ )
863
+ },
864
+ },
865
+ {
866
+ label: 'Split exon',
867
+ disabled: !admin,
868
+ onClick: () => {
869
+ ;(session as unknown as AbstractSessionModel).queueDialog(
870
+ (doneCallback) => [
871
+ SplitExon,
872
+ {
873
+ session,
874
+ handleClose: () => {
875
+ doneCallback()
876
+ },
877
+ changeManager,
878
+ sourceFeature: feature,
879
+ sourceAssemblyId: currentAssemblyId,
880
+ selectedFeature,
881
+ setSelectedFeature: (feature?: AnnotationFeature) => {
882
+ display.setSelectedFeature(feature)
883
+ },
884
+ },
885
+ ],
886
+ )
887
+ },
888
+ },
889
+ )
742
890
  }
743
- if (Math.abs(maxPx - x) < 4) {
744
- return { feature: overlappingExon, edge: 'max' }
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
+ })
932
+ }
745
933
  }
934
+ menuItems.push({
935
+ label: feature.type,
936
+ subMenu: contextMenuItemsForFeature,
937
+ })
746
938
  }
747
- return
939
+ return menuItems
748
940
  }
749
941
 
750
942
  // False positive here, none of these functions use "this"
751
943
  /* eslint-disable @typescript-eslint/unbound-method */
752
- const { drawTooltip, getContextMenuItems, onMouseLeave } = boxGlyph
944
+ const { drawTooltip, getContextMenuItemsForFeature, onMouseLeave } = boxGlyph
753
945
  /* eslint-enable @typescript-eslint/unbound-method */
754
946
 
755
947
  export const geneGlyph: Glyph = {
@@ -758,6 +950,7 @@ export const geneGlyph: Glyph = {
758
950
  drawHover,
759
951
  drawTooltip,
760
952
  getContextMenuItems,
953
+ getContextMenuItemsForFeature,
761
954
  getFeatureFromLayout,
762
955
  getRowCount,
763
956
  getRowForFeature,