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

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 (126) hide show
  1. package/dist/index.esm.js +5474 -4937
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +4609 -4089
  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 +3634 -3500
  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 +5 -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 +5 -5
  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 +210 -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 +3 -3
  43. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +21 -21
  44. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -4
  45. package/src/FeatureDetailsWidget/model.ts +8 -3
  46. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +6 -6
  47. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +13 -14
  48. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +9 -9
  49. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +6 -5
  50. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -7
  51. package/src/LinearApolloDisplay/stateModel/base.ts +52 -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 +13 -12
  55. package/src/LinearApolloDisplay/stateModel/rendering.ts +59 -31
  56. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +221 -0
  57. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +40 -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 +821 -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 +261 -0
  65. package/src/LinearApolloSixFrameDisplay/stateModel/index.ts +27 -0
  66. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +236 -0
  67. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +349 -0
  68. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +199 -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 +8 -6
  77. package/src/OntologyManager/util.ts +3 -2
  78. package/src/TabularEditor/HybridGrid/ChangeHandling.ts +2 -2
  79. package/src/TabularEditor/HybridGrid/Feature.tsx +9 -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 +6 -6
  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/AddChildFeature.tsx +6 -5
  89. package/src/components/AddFeature.tsx +211 -38
  90. package/src/components/AddRefSeqAliases.tsx +14 -12
  91. package/src/components/CopyFeature.tsx +8 -7
  92. package/src/components/CreateApolloAnnotation.tsx +9 -8
  93. package/src/components/DeleteAssembly.tsx +9 -8
  94. package/src/components/DeleteFeature.tsx +5 -4
  95. package/src/components/Dialog.tsx +1 -1
  96. package/src/components/DownloadGFF3.tsx +11 -10
  97. package/src/components/FilterFeatures.tsx +6 -4
  98. package/src/components/ImportFeatures.tsx +7 -6
  99. package/src/components/LogOut.tsx +5 -4
  100. package/src/components/ManageChecks.tsx +9 -8
  101. package/src/components/ManageUsers.tsx +11 -10
  102. package/src/components/OntologyTermAutocomplete.tsx +5 -5
  103. package/src/components/OntologyTermMultiSelect.tsx +6 -6
  104. package/src/components/OpenLocalFile.tsx +4 -3
  105. package/src/components/ViewChangeLog.tsx +7 -6
  106. package/src/components/ViewCheckResults.tsx +8 -7
  107. package/src/extensions/annotationFromJBrowseFeature.test.ts +1 -0
  108. package/src/extensions/annotationFromJBrowseFeature.ts +11 -10
  109. package/src/extensions/annotationFromPileup.ts +6 -6
  110. package/src/index.ts +33 -50
  111. package/src/makeDisplayComponent.tsx +90 -37
  112. package/src/session/ClientDataStore.ts +21 -17
  113. package/src/session/session.ts +20 -26
  114. package/src/types.ts +4 -4
  115. package/src/util/annotationFeatureUtils.ts +1 -1
  116. package/src/util/index.ts +3 -3
  117. package/src/util/loadAssemblyIntoClient.ts +10 -3
  118. package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +0 -13
  119. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +0 -707
  120. package/src/ApolloSixFrameRenderer/configSchema.ts +0 -7
  121. package/src/ApolloSixFrameRenderer/index.ts +0 -3
  122. package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +0 -19
  123. package/src/SixFrameFeatureDisplay/components/index.ts +0 -1
  124. package/src/SixFrameFeatureDisplay/configSchema.ts +0 -21
  125. package/src/SixFrameFeatureDisplay/index.ts +0 -2
  126. package/src/SixFrameFeatureDisplay/stateModel.ts +0 -443
@@ -0,0 +1,821 @@
1
+ import {
2
+ type AnnotationFeature,
3
+ type TranscriptPartCoding,
4
+ } from '@apollo-annotation/mst'
5
+ import { type MenuItem } from '@jbrowse/core/ui'
6
+ import {
7
+ type AbstractSessionModel,
8
+ type SessionWithWidgets,
9
+ getFrame,
10
+ intersection2,
11
+ isSessionModelWithWidgets,
12
+ } from '@jbrowse/core/util'
13
+ import { alpha } from '@mui/material'
14
+ import equal from 'fast-deep-equal/es6'
15
+
16
+ import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
17
+ import { type LinearApolloSixFrameDisplay } from '../stateModel'
18
+ import {
19
+ type LinearApolloSixFrameDisplayMouseEvents,
20
+ type MousePosition,
21
+ type MousePositionWithFeatureAndGlyph,
22
+ isMousePositionWithFeatureAndGlyph,
23
+ } from '../stateModel/mouseEvents'
24
+ import { type LinearApolloSixFrameDisplayRendering } from '../stateModel/rendering'
25
+ import { type CanvasMouseEvent } from '../types'
26
+
27
+ import { type Glyph } from './Glyph'
28
+
29
+ let forwardFillLight: CanvasPattern | null = null
30
+ let backwardFillLight: CanvasPattern | null = null
31
+ let forwardFillDark: CanvasPattern | null = null
32
+ let backwardFillDark: CanvasPattern | null = null
33
+ if ('document' in globalThis) {
34
+ for (const direction of ['forward', 'backward']) {
35
+ for (const themeMode of ['light', 'dark']) {
36
+ const canvas = document.createElement('canvas')
37
+ const canvasSize = 10
38
+ canvas.width = canvas.height = canvasSize
39
+ const ctx = canvas.getContext('2d')
40
+ if (ctx) {
41
+ const stripeColor1 =
42
+ themeMode === 'light' ? 'rgba(0,0,0,0)' : 'rgba(0,0,0,0.75)'
43
+ const stripeColor2 =
44
+ themeMode === 'light' ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.50)'
45
+ const gradient =
46
+ direction === 'forward'
47
+ ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
48
+ : ctx.createLinearGradient(0, 0, canvasSize, canvasSize)
49
+ gradient.addColorStop(0, stripeColor1)
50
+ gradient.addColorStop(0.25, stripeColor1)
51
+ gradient.addColorStop(0.25, stripeColor2)
52
+ gradient.addColorStop(0.5, stripeColor2)
53
+ gradient.addColorStop(0.5, stripeColor1)
54
+ gradient.addColorStop(0.75, stripeColor1)
55
+ gradient.addColorStop(0.75, stripeColor2)
56
+ gradient.addColorStop(1, stripeColor2)
57
+ ctx.fillStyle = gradient
58
+ ctx.fillRect(0, 0, 10, 10)
59
+ if (direction === 'forward') {
60
+ if (themeMode === 'light') {
61
+ forwardFillLight = ctx.createPattern(canvas, 'repeat')
62
+ } else {
63
+ forwardFillDark = ctx.createPattern(canvas, 'repeat')
64
+ }
65
+ } else {
66
+ if (themeMode === 'light') {
67
+ backwardFillLight = ctx.createPattern(canvas, 'repeat')
68
+ } else {
69
+ backwardFillDark = ctx.createPattern(canvas, 'repeat')
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ function deepSetHas<T>(set: Set<T>, item: T): boolean {
78
+ for (const elem of set) {
79
+ if (equal(elem, item)) {
80
+ return true
81
+ }
82
+ }
83
+ return false
84
+ }
85
+
86
+ function draw(
87
+ ctx: CanvasRenderingContext2D,
88
+ topLevelFeature: AnnotationFeature,
89
+ _row: number,
90
+ stateModel: LinearApolloSixFrameDisplayRendering,
91
+ displayedRegionIndex: number,
92
+ ): void {
93
+ const { apolloRowHeight, lgv, session, theme, highestRow } = stateModel
94
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
95
+ const displayedRegion = displayedRegions[displayedRegionIndex]
96
+ const { refName, reversed } = displayedRegion
97
+ const rowHeight = apolloRowHeight
98
+ const exonHeight = Math.round(0.6 * rowHeight)
99
+ const cdsHeight = Math.round(0.7 * rowHeight)
100
+ const { children, min, strand, _id } = topLevelFeature
101
+ if (!children) {
102
+ return
103
+ }
104
+ const { apolloSelectedFeature } = session
105
+ const { apolloDataStore } = session
106
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
107
+ if (!featureTypeOntology) {
108
+ throw new Error('featureTypeOntology is undefined')
109
+ }
110
+
111
+ // Draw background for gene
112
+ const topLevelFeatureMinX =
113
+ (lgv.bpToPx({
114
+ refName,
115
+ coord: min,
116
+ regionNumber: displayedRegionIndex,
117
+ })?.offsetPx ?? 0) - offsetPx
118
+ const topLevelFeatureWidthPx = topLevelFeature.length / bpPerPx
119
+ const topLevelFeatureStartPx = reversed
120
+ ? topLevelFeatureMinX - topLevelFeatureWidthPx
121
+ : topLevelFeatureMinX
122
+ const topLevelRow = strand == 1 ? 3 : 4
123
+ const topLevelFeatureTop = topLevelRow * rowHeight
124
+ const topLevelFeatureHeight = Math.round(0.7 * rowHeight)
125
+
126
+ ctx.fillStyle = theme?.palette.text.primary ?? 'black'
127
+ ctx.fillRect(
128
+ topLevelFeatureStartPx,
129
+ topLevelFeatureTop,
130
+ topLevelFeatureWidthPx,
131
+ topLevelFeatureHeight,
132
+ )
133
+
134
+ ctx.fillStyle =
135
+ apolloSelectedFeature && _id === apolloSelectedFeature._id
136
+ ? alpha('rgb(0,0,0)', 0.7)
137
+ : alpha(theme?.palette.background.paper ?? '#ffffff', 0.7)
138
+ ctx.fillRect(
139
+ topLevelFeatureStartPx + 1,
140
+ topLevelFeatureTop + 1,
141
+ topLevelFeatureWidthPx - 2,
142
+ topLevelFeatureHeight - 2,
143
+ )
144
+
145
+ const forwardFill =
146
+ theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
147
+ const backwardFill =
148
+ theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
149
+ const reversal = reversed ? -1 : 1
150
+ let topFill: CanvasPattern | null = null,
151
+ bottomFill: CanvasPattern | null = null
152
+ if (strand) {
153
+ ;[topFill, bottomFill] =
154
+ strand * reversal === 1
155
+ ? [forwardFill, backwardFill]
156
+ : [backwardFill, forwardFill]
157
+ }
158
+
159
+ if (topFill && bottomFill) {
160
+ ctx.fillStyle = topFill
161
+ ctx.fillRect(
162
+ topLevelFeatureStartPx + 1,
163
+ topLevelFeatureTop + 1,
164
+ topLevelFeatureWidthPx - 2,
165
+ (topLevelFeatureHeight - 2) / 2,
166
+ )
167
+ ctx.fillStyle = bottomFill
168
+ ctx.fillRect(
169
+ topLevelFeatureStartPx + 1,
170
+ topLevelFeatureTop + (topLevelFeatureHeight - 2) / 2,
171
+ topLevelFeatureWidthPx - 2,
172
+ (topLevelFeatureHeight - 2) / 2,
173
+ )
174
+ }
175
+
176
+ const renderedCDS = new Set<TranscriptPartCoding>()
177
+ // Draw exon and CDS for each mRNA
178
+ for (const [, child] of children) {
179
+ if (
180
+ !(
181
+ featureTypeOntology.isTypeOf(child.type, 'transcript') ||
182
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript')
183
+ )
184
+ ) {
185
+ continue
186
+ }
187
+ const { children: childrenOfmRNA, cdsLocations, _id } = child
188
+ if (!childrenOfmRNA) {
189
+ continue
190
+ }
191
+ for (const [, exon] of childrenOfmRNA) {
192
+ if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
193
+ continue
194
+ }
195
+ const minX =
196
+ (lgv.bpToPx({
197
+ refName,
198
+ coord: exon.min,
199
+ regionNumber: displayedRegionIndex,
200
+ })?.offsetPx ?? 0) - offsetPx
201
+ const widthPx = exon.length / bpPerPx
202
+ const startPx = reversed ? minX - widthPx : minX
203
+
204
+ const exonTop =
205
+ topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2
206
+ ctx.fillStyle = theme?.palette.text.primary ?? 'black'
207
+ ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
208
+ if (widthPx > 2) {
209
+ ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
210
+ ctx.fillStyle =
211
+ apolloSelectedFeature && exon._id === apolloSelectedFeature._id
212
+ ? 'rgb(0,0,0)'
213
+ : alpha('#f5f500', 0.6)
214
+ ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
215
+ if (topFill && bottomFill) {
216
+ ctx.fillStyle = topFill
217
+ ctx.fillRect(
218
+ startPx + 1,
219
+ exonTop + 1,
220
+ widthPx - 2,
221
+ (exonHeight - 2) / 2,
222
+ )
223
+ ctx.fillStyle = bottomFill
224
+ ctx.fillRect(
225
+ startPx + 1,
226
+ exonTop + 1 + (exonHeight - 2) / 2,
227
+ widthPx - 2,
228
+ (exonHeight - 2) / 2,
229
+ )
230
+ }
231
+ }
232
+ }
233
+
234
+ for (const cdsRow of cdsLocations) {
235
+ let prevCDSTop = 0
236
+ let prevCDSEndPx = 0
237
+ let counter = 1
238
+ for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
239
+ if (
240
+ (apolloSelectedFeature &&
241
+ featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS') &&
242
+ _id === apolloSelectedFeature.parent?._id) ||
243
+ !deepSetHas(renderedCDS, cds)
244
+ ) {
245
+ const cdsWidthPx = (cds.max - cds.min) / bpPerPx
246
+ const minX =
247
+ (lgv.bpToPx({
248
+ refName,
249
+ coord: cds.min,
250
+ regionNumber: displayedRegionIndex,
251
+ })?.offsetPx ?? 0) - offsetPx
252
+ const cdsStartPx = reversed ? minX - cdsWidthPx : minX
253
+ ctx.fillStyle = theme?.palette.text.primary ?? 'black'
254
+ const frame = getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase)
255
+ const frameAdjust = frame < 0 ? -1 * frame + 5 : frame
256
+ const cdsTop =
257
+ (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2
258
+ ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
259
+ if (cdsWidthPx > 2) {
260
+ ctx.clearRect(
261
+ cdsStartPx + 1,
262
+ cdsTop + 1,
263
+ cdsWidthPx - 2,
264
+ cdsHeight - 2,
265
+ )
266
+
267
+ const frameColor = theme?.palette.framesCDS.at(frame)?.main
268
+ const cdsColorCode = frameColor ?? 'rgb(171,71,188)'
269
+ ctx.fillStyle = cdsColorCode
270
+ ctx.fillStyle =
271
+ apolloSelectedFeature &&
272
+ featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS') &&
273
+ _id === apolloSelectedFeature.parent?._id
274
+ ? 'rgb(0,0,0)'
275
+ : cdsColorCode
276
+ ctx.fillRect(
277
+ cdsStartPx + 1,
278
+ cdsTop + 1,
279
+ cdsWidthPx - 2,
280
+ cdsHeight - 2,
281
+ )
282
+
283
+ // Draw lines to connect CDS features with shared mRNA parent
284
+ if (counter > 1) {
285
+ // Mid-point for intron line "hat"
286
+ const midPoint: [number, number] = [
287
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
288
+ Math.max(
289
+ frame < 0 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
290
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
291
+ ),
292
+ ]
293
+ ctx.strokeStyle = 'rgb(0, 128, 128)'
294
+ ctx.beginPath()
295
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
296
+ ctx.lineTo(...midPoint)
297
+ ctx.stroke()
298
+ ctx.moveTo(...midPoint)
299
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
300
+ ctx.stroke()
301
+ }
302
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
303
+ prevCDSTop = cdsTop + rowHeight / 2
304
+ counter += 1
305
+
306
+ if (topFill && bottomFill) {
307
+ ctx.fillStyle = topFill
308
+ ctx.fillRect(
309
+ cdsStartPx + 1,
310
+ cdsTop + 1,
311
+ cdsWidthPx - 2,
312
+ (cdsHeight - 2) / 2,
313
+ )
314
+ ctx.fillStyle = bottomFill
315
+ ctx.fillRect(
316
+ cdsStartPx + 1,
317
+ cdsTop + (cdsHeight - 2) / 2,
318
+ cdsWidthPx - 2,
319
+ (cdsHeight - 2) / 2,
320
+ )
321
+ }
322
+ }
323
+ renderedCDS.add(cds)
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ function drawDragPreview(
331
+ stateModel: LinearApolloSixFrameDisplay,
332
+ overlayCtx: CanvasRenderingContext2D,
333
+ ) {
334
+ const { apolloDragging, apolloRowHeight, lgv, theme } = stateModel
335
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
336
+ if (!apolloDragging) {
337
+ return
338
+ }
339
+ const { current, edge, feature, start } = apolloDragging
340
+
341
+ const row = Math.floor(start.y / apolloRowHeight)
342
+ const region = displayedRegions[start.regionNumber]
343
+ const rowCount = 1
344
+ const featureEdgeBp = region.reversed
345
+ ? region.end - feature[edge]
346
+ : feature[edge] - region.start
347
+ const featureEdgePx = featureEdgeBp / bpPerPx - offsetPx
348
+ const rectX = Math.min(current.x, featureEdgePx)
349
+ const rectY = row * apolloRowHeight
350
+ const rectWidth = Math.abs(current.x - featureEdgePx)
351
+ const rectHeight = apolloRowHeight * rowCount
352
+ overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
353
+ overlayCtx.setLineDash([6])
354
+ overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
355
+ overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
356
+ overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
357
+ }
358
+
359
+ function drawHover(
360
+ stateModel: LinearApolloSixFrameDisplay,
361
+ ctx: CanvasRenderingContext2D,
362
+ ) {
363
+ const { apolloHover, apolloRowHeight, lgv, highestRow, session } = stateModel
364
+ if (!apolloHover) {
365
+ return
366
+ }
367
+ const { apolloDataStore } = session
368
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
369
+ if (!featureTypeOntology) {
370
+ throw new Error('featureTypeOntology is undefined')
371
+ }
372
+ const { feature } = apolloHover
373
+ if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
374
+ return
375
+ }
376
+ const position = stateModel.getFeatureLayoutPosition(feature)
377
+ if (!position) {
378
+ return
379
+ }
380
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
381
+ const { layoutIndex } = position
382
+ const displayedRegion = displayedRegions[layoutIndex]
383
+ const { refName, reversed } = displayedRegion
384
+ const rowHeight = apolloRowHeight
385
+ const cdsHeight = Math.round(0.7 * rowHeight)
386
+ const { cdsLocations, strand } = feature
387
+ for (const cdsRow of cdsLocations) {
388
+ let prevCDSTop = 0
389
+ let prevCDSEndPx = 0
390
+ let counter = 1
391
+ for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
392
+ const cdsWidthPx = (cds.max - cds.min) / bpPerPx
393
+ if (cdsWidthPx > 2) {
394
+ const minX =
395
+ (lgv.bpToPx({
396
+ refName,
397
+ coord: cds.min,
398
+ regionNumber: layoutIndex,
399
+ })?.offsetPx ?? 0) - offsetPx
400
+ const cdsStartPx = reversed ? minX - cdsWidthPx : minX
401
+ const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
402
+ const frameAdjust = frame < 0 ? -1 * frame + 5 : frame
403
+ const cdsTop =
404
+ (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2
405
+ ctx.fillStyle = 'rgba(255,0,0,0.6)'
406
+ ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
407
+
408
+ if (counter > 1) {
409
+ // Mid-point for intron line "hat"
410
+ const midPoint: [number, number] = [
411
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
412
+ Math.max(
413
+ frame < 0 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
414
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
415
+ ),
416
+ ]
417
+ ctx.strokeStyle = 'rgb(0, 0, 0)'
418
+ ctx.lineWidth = 2
419
+ ctx.beginPath()
420
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
421
+ ctx.lineTo(...midPoint)
422
+ ctx.stroke()
423
+ ctx.moveTo(...midPoint)
424
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
425
+ ctx.stroke()
426
+ }
427
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
428
+ prevCDSTop = cdsTop + rowHeight / 2
429
+ counter += 1
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ function onMouseDown(
436
+ stateModel: LinearApolloSixFrameDisplay,
437
+ currentMousePosition: MousePositionWithFeatureAndGlyph,
438
+ event: CanvasMouseEvent,
439
+ ) {
440
+ const { featureAndGlyphUnderMouse } = currentMousePosition
441
+ // swallow the mouseDown if we are on the edge of the feature so that we
442
+ // don't start dragging the view if we try to drag the feature edge
443
+ const { cds, feature } = featureAndGlyphUnderMouse
444
+ const draggableFeature = getDraggableFeatureInfo(
445
+ currentMousePosition,
446
+ cds,
447
+ feature,
448
+ stateModel,
449
+ )
450
+ if (draggableFeature) {
451
+ event.stopPropagation()
452
+ stateModel.startDrag(
453
+ currentMousePosition,
454
+ draggableFeature.feature,
455
+ draggableFeature.edge,
456
+ )
457
+ }
458
+ }
459
+
460
+ function onMouseMove(
461
+ stateModel: LinearApolloSixFrameDisplay,
462
+ mousePosition: MousePosition,
463
+ ) {
464
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
465
+ const { featureAndGlyphUnderMouse } = mousePosition
466
+ stateModel.setApolloHover(featureAndGlyphUnderMouse)
467
+ const { cds, feature } = featureAndGlyphUnderMouse
468
+ const draggableFeature = getDraggableFeatureInfo(
469
+ mousePosition,
470
+ cds,
471
+ feature,
472
+ stateModel,
473
+ )
474
+ if (draggableFeature) {
475
+ stateModel.setCursor('col-resize')
476
+ return
477
+ }
478
+ }
479
+ stateModel.setCursor()
480
+ }
481
+
482
+ function onMouseUp(
483
+ stateModel: LinearApolloSixFrameDisplay,
484
+ mousePosition: MousePosition,
485
+ ) {
486
+ if (stateModel.apolloDragging) {
487
+ return
488
+ }
489
+ const { featureAndGlyphUnderMouse } = mousePosition
490
+ const { session } = stateModel
491
+ const { apolloDataStore } = session
492
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
493
+ if (featureAndGlyphUnderMouse?.cds) {
494
+ const { cds, feature } = featureAndGlyphUnderMouse
495
+ if (!featureTypeOntology) {
496
+ throw new Error('featureTypeOntology is undefined')
497
+ }
498
+ if (!feature.children) {
499
+ return
500
+ }
501
+ for (const child of feature.children.values()) {
502
+ const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS')
503
+ if (childIsCDS && cds.max <= child.max && cds.min >= child.min) {
504
+ stateModel.setSelectedFeature(child)
505
+ break
506
+ }
507
+ }
508
+ } else if (featureAndGlyphUnderMouse?.feature) {
509
+ stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
510
+ }
511
+ }
512
+
513
+ function getDraggableFeatureInfo(
514
+ mousePosition: MousePosition,
515
+ cds: TranscriptPartCoding | null,
516
+ feature: AnnotationFeature,
517
+ stateModel: LinearApolloSixFrameDisplay,
518
+ ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
519
+ const { session } = stateModel
520
+ const { apolloDataStore } = session
521
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
522
+ if (!featureTypeOntology) {
523
+ throw new Error('featureTypeOntology is undefined')
524
+ }
525
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript')
526
+ if (cds === null) {
527
+ return
528
+ }
529
+ const { bp, refName, regionNumber, x } = mousePosition
530
+ const { lgv } = stateModel
531
+ const { offsetPx } = lgv
532
+
533
+ const minPxInfo = lgv.bpToPx({ refName, coord: cds.min, regionNumber })
534
+ const maxPxInfo = lgv.bpToPx({ refName, coord: cds.max, regionNumber })
535
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
536
+ return
537
+ }
538
+ const minPx = minPxInfo.offsetPx - offsetPx
539
+ const maxPx = maxPxInfo.offsetPx - offsetPx
540
+ if (Math.abs(maxPx - minPx) < 8) {
541
+ return
542
+ }
543
+ if (isTranscript) {
544
+ const transcript = feature
545
+ if (!transcript.children) {
546
+ return
547
+ }
548
+ const exonChildren: AnnotationFeature[] = []
549
+ for (const child of transcript.children.values()) {
550
+ const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon')
551
+ if (childIsExon) {
552
+ exonChildren.push(child)
553
+ }
554
+ }
555
+
556
+ const overlappingExon = exonChildren.find((child) => {
557
+ const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
558
+ return start !== undefined && end !== undefined
559
+ })
560
+ if (!overlappingExon) {
561
+ return
562
+ }
563
+ const minPxInfo = lgv.bpToPx({
564
+ refName,
565
+ coord: overlappingExon.min,
566
+ regionNumber,
567
+ })
568
+ const maxPxInfo = lgv.bpToPx({
569
+ refName,
570
+ coord: overlappingExon.max,
571
+ regionNumber,
572
+ })
573
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
574
+ return
575
+ }
576
+ const minPx = minPxInfo.offsetPx - offsetPx
577
+ const maxPx = maxPxInfo.offsetPx - offsetPx
578
+ if (Math.abs(maxPx - minPx) < 8) {
579
+ return
580
+ }
581
+ if (Math.abs(minPx - x) < 4) {
582
+ return { feature: overlappingExon, edge: 'min' }
583
+ }
584
+ if (Math.abs(maxPx - x) < 4) {
585
+ return { feature: overlappingExon, edge: 'max' }
586
+ }
587
+ }
588
+ return
589
+ }
590
+
591
+ function drawTooltip(
592
+ display: LinearApolloSixFrameDisplayMouseEvents,
593
+ context: CanvasRenderingContext2D,
594
+ ): void {
595
+ const { apolloHover, apolloRowHeight, lgv, theme } = display
596
+ if (!apolloHover) {
597
+ return
598
+ }
599
+ const { cds, feature } = apolloHover
600
+ if (!cds) {
601
+ return
602
+ }
603
+ const position = display.getFeatureLayoutPosition(feature)
604
+ if (!position) {
605
+ return
606
+ }
607
+ const { layoutIndex } = position
608
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
609
+ const displayedRegion = displayedRegions[layoutIndex]
610
+ const { refName, reversed } = displayedRegion
611
+ const rowHeight = apolloRowHeight
612
+ const cdsHeight = Math.round(0.7 * rowHeight)
613
+ let location = 'Loc: '
614
+
615
+ const { strand } = feature
616
+ const { max, min, phase } = cds
617
+ location += `${min + 1}–${max}`
618
+
619
+ let startPx =
620
+ (lgv.bpToPx({
621
+ refName,
622
+ coord: reversed ? max : min,
623
+ regionNumber: layoutIndex,
624
+ })?.offsetPx ?? 0) - offsetPx
625
+ const frame = getFrame(min, max, strand ?? 1, phase)
626
+ const frameAdjust = frame < 0 ? -1 * frame + 5 : frame
627
+ const cdsTop = (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2
628
+ const cdsWidthPx = (max - min) / bpPerPx
629
+
630
+ const featureType = `Type: ${cds.type}`
631
+ const { attributes } = feature
632
+ const featureName = attributes.get('gff_name')?.find((name) => name !== '')
633
+ const textWidth = [
634
+ context.measureText(featureType).width,
635
+ context.measureText(location).width,
636
+ ]
637
+ if (featureName) {
638
+ textWidth.push(
639
+ context.measureText(`Parent Type: ${feature.type}`).width,
640
+ context.measureText(`Parent Name: ${featureName}`).width,
641
+ )
642
+ }
643
+ const maxWidth = Math.max(...textWidth)
644
+
645
+ startPx = startPx + cdsWidthPx + 5
646
+ context.fillStyle = alpha(theme?.palette.text.primary ?? 'rgb(1, 1, 1)', 0.7)
647
+ context.fillRect(
648
+ startPx,
649
+ cdsTop,
650
+ maxWidth + 4,
651
+ textWidth.length === 4 ? 55 : 35,
652
+ )
653
+ context.beginPath()
654
+ context.moveTo(startPx, cdsTop)
655
+ context.lineTo(startPx - 5, cdsTop + 5)
656
+ context.lineTo(startPx, cdsTop + 10)
657
+ context.fill()
658
+ context.fillStyle = theme?.palette.background.default ?? 'rgba(255, 255, 255)'
659
+ let textTop = cdsTop + 12
660
+ context.fillText(featureType, startPx + 2, textTop)
661
+ if (featureName) {
662
+ textTop = textTop + 12
663
+ context.fillText(`Parent Type: ${feature.type}`, startPx + 2, textTop)
664
+ textTop = textTop + 12
665
+ context.fillText(`Parent Name: ${featureName}`, startPx + 2, textTop)
666
+ }
667
+ textTop = textTop + 12
668
+ context.fillText(location, startPx + 2, textTop)
669
+ }
670
+
671
+ function getContextMenuItems(
672
+ display: LinearApolloSixFrameDisplayMouseEvents,
673
+ ): MenuItem[] {
674
+ const {
675
+ apolloHover,
676
+ apolloInternetAccount: internetAccount,
677
+ changeManager,
678
+ regions,
679
+ selectedFeature,
680
+ session,
681
+ } = display
682
+ const menuItems: MenuItem[] = []
683
+ if (!apolloHover) {
684
+ return menuItems
685
+ }
686
+ const { feature: sourceFeature } = apolloHover
687
+ const role = internetAccount ? internetAccount.role : 'admin'
688
+ const admin = role === 'admin'
689
+ const readOnly = !(role && ['admin', 'user'].includes(role))
690
+ const [region] = regions
691
+ const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
692
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
693
+ menuItems.push(
694
+ {
695
+ label: 'Add child feature',
696
+ disabled: readOnly,
697
+ onClick: () => {
698
+ ;(session as unknown as AbstractSessionModel).queueDialog(
699
+ (doneCallback) => [
700
+ AddChildFeature,
701
+ {
702
+ session,
703
+ handleClose: () => {
704
+ doneCallback()
705
+ },
706
+ changeManager,
707
+ sourceFeature,
708
+ sourceAssemblyId,
709
+ internetAccount,
710
+ },
711
+ ],
712
+ )
713
+ },
714
+ },
715
+ {
716
+ label: 'Copy features and annotations',
717
+ disabled: readOnly,
718
+ onClick: () => {
719
+ ;(session as unknown as AbstractSessionModel).queueDialog(
720
+ (doneCallback) => [
721
+ CopyFeature,
722
+ {
723
+ session,
724
+ handleClose: () => {
725
+ doneCallback()
726
+ },
727
+ changeManager,
728
+ sourceFeature,
729
+ sourceAssemblyId: currentAssemblyId,
730
+ },
731
+ ],
732
+ )
733
+ },
734
+ },
735
+ {
736
+ label: 'Delete feature',
737
+ disabled: !admin,
738
+ onClick: () => {
739
+ ;(session as unknown as AbstractSessionModel).queueDialog(
740
+ (doneCallback) => [
741
+ DeleteFeature,
742
+ {
743
+ session,
744
+ handleClose: () => {
745
+ doneCallback()
746
+ },
747
+ changeManager,
748
+ sourceFeature,
749
+ sourceAssemblyId: currentAssemblyId,
750
+ selectedFeature,
751
+ setSelectedFeature: (feature?: AnnotationFeature) => {
752
+ display.setSelectedFeature(feature)
753
+ },
754
+ },
755
+ ],
756
+ )
757
+ },
758
+ },
759
+ {
760
+ label: 'Edit feature details',
761
+ onClick: () => {
762
+ const apolloFeatureWidget = (
763
+ session as unknown as SessionWithWidgets
764
+ ).addWidget(
765
+ 'ApolloFeatureDetailsWidget',
766
+ 'apolloFeatureDetailsWidget',
767
+ {
768
+ feature: sourceFeature,
769
+ assembly: currentAssemblyId,
770
+ refName: region.refName,
771
+ },
772
+ )
773
+ ;(session as unknown as SessionWithWidgets).showWidget(
774
+ apolloFeatureWidget,
775
+ )
776
+ },
777
+ },
778
+ )
779
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
780
+ if (!featureTypeOntology) {
781
+ throw new Error('featureTypeOntology is undefined')
782
+ }
783
+ if (
784
+ featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
785
+ isSessionModelWithWidgets(session)
786
+ ) {
787
+ menuItems.push({
788
+ label: 'Edit transcript details',
789
+ onClick: () => {
790
+ const apolloTranscriptWidget = session.addWidget(
791
+ 'ApolloTranscriptDetails',
792
+ 'apolloTranscriptDetails',
793
+ {
794
+ feature: sourceFeature,
795
+ assembly: currentAssemblyId,
796
+ changeManager,
797
+ refName: region.refName,
798
+ },
799
+ )
800
+ session.showWidget(apolloTranscriptWidget)
801
+ },
802
+ })
803
+ }
804
+ return menuItems
805
+ }
806
+
807
+ function onMouseLeave(): void {
808
+ return
809
+ }
810
+
811
+ export const geneGlyph: Glyph = {
812
+ draw,
813
+ drawDragPreview,
814
+ drawHover,
815
+ drawTooltip,
816
+ getContextMenuItems,
817
+ onMouseDown,
818
+ onMouseLeave,
819
+ onMouseMove,
820
+ onMouseUp,
821
+ }