@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 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.
- package/dist/index.esm.js +2679 -850
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +2676 -847
- package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.development.js +5194 -1258
- package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
- package/package.json +4 -4
- package/src/ApolloInternetAccount/addMenuItems.ts +18 -0
- package/src/ChangeManager.ts +10 -6
- package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +12 -20
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +929 -175
- package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -1
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +48 -60
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +244 -51
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +46 -1
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +9 -1
- package/src/LinearApolloDisplay/stateModel/base.ts +29 -0
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +51 -35
- package/src/LinearApolloDisplay/stateModel/rendering.ts +2 -1
- package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +7 -2
- package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
- package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +243 -124
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -1
- package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +19 -3
- package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +53 -34
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +4 -2
- package/src/OntologyManager/index.ts +4 -1
- package/src/TabularEditor/HybridGrid/Feature.tsx +4 -0
- package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
- package/src/components/AddAssemblyAliases.tsx +114 -0
- package/src/components/AddChildFeature.tsx +3 -6
- package/src/components/AddFeature.tsx +14 -15
- package/src/components/CopyFeature.tsx +2 -4
- package/src/components/CreateApolloAnnotation.tsx +334 -151
- package/src/components/DeleteFeature.tsx +358 -11
- package/src/components/DownloadGFF3.tsx +20 -1
- package/src/components/FilterTranscripts.tsx +86 -0
- package/src/components/MergeExons.tsx +193 -0
- package/src/components/MergeTranscripts.tsx +185 -0
- package/src/components/SplitExon.tsx +134 -0
- package/src/components/index.ts +3 -0
- package/src/config.ts +5 -0
- package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
- package/src/extensions/annotationFromPileup.ts +99 -89
- package/src/session/session.ts +26 -13
- package/src/util/annotationFeatureUtils.ts +65 -0
- package/src/util/copyToClipboard.ts +21 -0
- package/src/util/glyphUtils.ts +49 -0
- package/src/util/index.ts +2 -0
- package/src/util/mouseEventsUtils.ts +113 -0
|
@@ -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>
|
|
@@ -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(
|
|
142
|
+
setContextMenuItems(getContextMenuItems(event))
|
|
143
143
|
}
|
|
144
144
|
}}
|
|
145
145
|
>
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
2
|
import { type MenuItem } from '@jbrowse/core/ui'
|
|
3
|
-
import {
|
|
4
|
-
type AbstractSessionModel,
|
|
5
|
-
type SessionWithWidgets,
|
|
6
|
-
isSessionModelWithWidgets,
|
|
7
|
-
} from '@jbrowse/core/util'
|
|
3
|
+
import { type AbstractSessionModel } from '@jbrowse/core/util'
|
|
8
4
|
import { type Theme, alpha } from '@mui/material'
|
|
9
5
|
|
|
10
6
|
import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
|
|
@@ -257,9 +253,39 @@ export function drawBox(
|
|
|
257
253
|
|
|
258
254
|
function getContextMenuItems(
|
|
259
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,
|
|
260
287
|
): MenuItem[] {
|
|
261
288
|
const {
|
|
262
|
-
apolloHover,
|
|
263
289
|
apolloInternetAccount: internetAccount,
|
|
264
290
|
changeManager,
|
|
265
291
|
regions,
|
|
@@ -267,17 +293,23 @@ function getContextMenuItems(
|
|
|
267
293
|
session,
|
|
268
294
|
} = display
|
|
269
295
|
const menuItems: MenuItem[] = []
|
|
270
|
-
if (!apolloHover) {
|
|
271
|
-
return menuItems
|
|
272
|
-
}
|
|
273
|
-
const { feature: sourceFeature } = apolloHover
|
|
274
296
|
const role = internetAccount ? internetAccount.role : 'admin'
|
|
275
297
|
const admin = role === 'admin'
|
|
276
298
|
const readOnly = !(role && ['admin', 'user'].includes(role))
|
|
277
299
|
const [region] = regions
|
|
278
300
|
const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
|
|
279
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
|
|
280
308
|
menuItems.push(
|
|
309
|
+
{
|
|
310
|
+
label: makeFeatureLabel(sourceFeature),
|
|
311
|
+
type: 'subHeader',
|
|
312
|
+
},
|
|
281
313
|
{
|
|
282
314
|
label: 'Add child feature',
|
|
283
315
|
disabled: readOnly,
|
|
@@ -343,55 +375,7 @@ function getContextMenuItems(
|
|
|
343
375
|
)
|
|
344
376
|
},
|
|
345
377
|
},
|
|
346
|
-
{
|
|
347
|
-
label: 'Edit feature details',
|
|
348
|
-
onClick: () => {
|
|
349
|
-
const apolloFeatureWidget = (
|
|
350
|
-
session as unknown as SessionWithWidgets
|
|
351
|
-
).addWidget(
|
|
352
|
-
'ApolloFeatureDetailsWidget',
|
|
353
|
-
'apolloFeatureDetailsWidget',
|
|
354
|
-
{
|
|
355
|
-
feature: sourceFeature,
|
|
356
|
-
assembly: currentAssemblyId,
|
|
357
|
-
refName: region.refName,
|
|
358
|
-
},
|
|
359
|
-
)
|
|
360
|
-
;(session as unknown as SessionWithWidgets).showWidget(
|
|
361
|
-
apolloFeatureWidget,
|
|
362
|
-
)
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
378
|
)
|
|
366
|
-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
367
|
-
if (!featureTypeOntology) {
|
|
368
|
-
throw new Error('featureTypeOntology is undefined')
|
|
369
|
-
}
|
|
370
|
-
if (
|
|
371
|
-
(featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
|
|
372
|
-
featureTypeOntology.isTypeOf(
|
|
373
|
-
sourceFeature.type,
|
|
374
|
-
'pseudogenic_transcript',
|
|
375
|
-
)) &&
|
|
376
|
-
isSessionModelWithWidgets(session)
|
|
377
|
-
) {
|
|
378
|
-
menuItems.push({
|
|
379
|
-
label: 'Edit transcript details',
|
|
380
|
-
onClick: () => {
|
|
381
|
-
const apolloTranscriptWidget = session.addWidget(
|
|
382
|
-
'ApolloTranscriptDetails',
|
|
383
|
-
'apolloTranscriptDetails',
|
|
384
|
-
{
|
|
385
|
-
feature: sourceFeature,
|
|
386
|
-
assembly: currentAssemblyId,
|
|
387
|
-
changeManager,
|
|
388
|
-
refName: region.refName,
|
|
389
|
-
},
|
|
390
|
-
)
|
|
391
|
-
session.showWidget(apolloTranscriptWidget)
|
|
392
|
-
},
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
379
|
return menuItems
|
|
396
380
|
}
|
|
397
381
|
|
|
@@ -459,9 +443,12 @@ function onMouseUp(
|
|
|
459
443
|
return
|
|
460
444
|
}
|
|
461
445
|
const { featureAndGlyphUnderMouse } = mousePosition
|
|
462
|
-
if (featureAndGlyphUnderMouse
|
|
463
|
-
|
|
446
|
+
if (!featureAndGlyphUnderMouse) {
|
|
447
|
+
return
|
|
464
448
|
}
|
|
449
|
+
const { feature } = featureAndGlyphUnderMouse
|
|
450
|
+
stateModel.setSelectedFeature(feature)
|
|
451
|
+
stateModel.showFeatureDetailsWidget(feature)
|
|
465
452
|
}
|
|
466
453
|
|
|
467
454
|
/** @returns undefined if mouse not on the edge of this feature, otherwise 'start' or 'end' depending on which edge */
|
|
@@ -496,6 +483,7 @@ export const boxGlyph: Glyph = {
|
|
|
496
483
|
drawDragPreview,
|
|
497
484
|
drawHover,
|
|
498
485
|
drawTooltip,
|
|
486
|
+
getContextMenuItemsForFeature,
|
|
499
487
|
getContextMenuItems,
|
|
500
488
|
getFeatureFromLayout,
|
|
501
489
|
getRowCount,
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
-
import {
|
|
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
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'
|
|
6
16
|
import { type LinearApolloDisplay } from '../stateModel'
|
|
7
17
|
import {
|
|
18
|
+
type LinearApolloDisplayMouseEvents,
|
|
8
19
|
type MousePosition,
|
|
9
20
|
type MousePositionWithFeatureAndGlyph,
|
|
10
21
|
isMousePositionWithFeatureAndGlyph,
|
|
@@ -19,7 +30,10 @@ let forwardFillLight: CanvasPattern | null = null
|
|
|
19
30
|
let backwardFillLight: CanvasPattern | null = null
|
|
20
31
|
let forwardFillDark: CanvasPattern | null = null
|
|
21
32
|
let backwardFillDark: CanvasPattern | null = null
|
|
22
|
-
|
|
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) {
|
|
23
37
|
for (const direction of ['forward', 'backward']) {
|
|
24
38
|
for (const themeMode of ['light', 'dark']) {
|
|
25
39
|
const canvas = document.createElement('canvas')
|
|
@@ -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
|
|
656
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
741
|
-
|
|
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 (
|
|
744
|
-
|
|
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,
|
|
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,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
+
import { type MenuItem } from '@jbrowse/core/ui'
|
|
2
3
|
|
|
4
|
+
import { getFeaturesUnderClick } from '../../util/annotationFeatureUtils'
|
|
3
5
|
import { type LinearApolloDisplay } from '../stateModel'
|
|
6
|
+
import {
|
|
7
|
+
type LinearApolloDisplayMouseEvents,
|
|
8
|
+
type MousePositionWithFeatureAndGlyph,
|
|
9
|
+
} from '../stateModel/mouseEvents'
|
|
4
10
|
import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
|
|
5
11
|
|
|
6
12
|
import { boxGlyph, drawBox, isSelectedFeature } from './BoxGlyph'
|
|
@@ -130,12 +136,50 @@ function getRowForFeature(
|
|
|
130
136
|
return
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
function getContextMenuItems(
|
|
140
|
+
display: LinearApolloDisplayMouseEvents,
|
|
141
|
+
mousePosition: MousePositionWithFeatureAndGlyph,
|
|
142
|
+
): MenuItem[] {
|
|
143
|
+
const { apolloHover, session } = display
|
|
144
|
+
const menuItems: MenuItem[] = []
|
|
145
|
+
if (!apolloHover) {
|
|
146
|
+
return menuItems
|
|
147
|
+
}
|
|
148
|
+
const { feature: sourceFeature } = apolloHover
|
|
149
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
150
|
+
if (!featureTypeOntology) {
|
|
151
|
+
throw new Error('featureTypeOntology is undefined')
|
|
152
|
+
}
|
|
153
|
+
const sourceFeatureMenuItems = boxGlyph.getContextMenuItems(
|
|
154
|
+
display,
|
|
155
|
+
mousePosition,
|
|
156
|
+
)
|
|
157
|
+
menuItems.push({
|
|
158
|
+
label: sourceFeature.type,
|
|
159
|
+
subMenu: sourceFeatureMenuItems,
|
|
160
|
+
})
|
|
161
|
+
for (const relative of getFeaturesUnderClick(mousePosition)) {
|
|
162
|
+
if (relative._id === sourceFeature._id) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
|
|
166
|
+
display,
|
|
167
|
+
relative,
|
|
168
|
+
)
|
|
169
|
+
menuItems.push({
|
|
170
|
+
label: relative.type,
|
|
171
|
+
subMenu: contextMenuItemsForFeature,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
return menuItems
|
|
175
|
+
}
|
|
176
|
+
|
|
133
177
|
// False positive here, none of these functions use "this"
|
|
134
178
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
135
179
|
const {
|
|
136
180
|
drawDragPreview,
|
|
137
181
|
drawTooltip,
|
|
138
|
-
|
|
182
|
+
getContextMenuItemsForFeature,
|
|
139
183
|
onMouseDown,
|
|
140
184
|
onMouseLeave,
|
|
141
185
|
onMouseMove,
|
|
@@ -148,6 +192,7 @@ export const genericChildGlyph: Glyph = {
|
|
|
148
192
|
drawDragPreview,
|
|
149
193
|
drawHover,
|
|
150
194
|
drawTooltip,
|
|
195
|
+
getContextMenuItemsForFeature,
|
|
151
196
|
getContextMenuItems,
|
|
152
197
|
getFeatureFromLayout,
|
|
153
198
|
getRowCount,
|
|
@@ -76,5 +76,13 @@ export interface Glyph {
|
|
|
76
76
|
context: CanvasRenderingContext2D,
|
|
77
77
|
): void
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
getContextMenuItemsForFeature(
|
|
80
|
+
display: LinearApolloDisplayMouseEvents,
|
|
81
|
+
sourceFeature: AnnotationFeature,
|
|
82
|
+
): MenuItem[]
|
|
83
|
+
|
|
84
|
+
getContextMenuItems(
|
|
85
|
+
display: LinearApolloDisplayMouseEvents,
|
|
86
|
+
currentMousePosition: MousePositionWithFeatureAndGlyph,
|
|
87
|
+
): MenuItem[]
|
|
80
88
|
}
|
|
@@ -10,6 +10,7 @@ import { type AnyConfigurationSchemaType } from '@jbrowse/core/configuration/con
|
|
|
10
10
|
import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes'
|
|
11
11
|
import {
|
|
12
12
|
type AbstractSessionModel,
|
|
13
|
+
type SessionWithWidgets,
|
|
13
14
|
getContainingView,
|
|
14
15
|
getSession,
|
|
15
16
|
} from '@jbrowse/core/util'
|
|
@@ -285,6 +286,34 @@ export function baseModelFactory(
|
|
|
285
286
|
self.session as unknown as ApolloSessionModel
|
|
286
287
|
).apolloSetSelectedFeature(feature)
|
|
287
288
|
},
|
|
289
|
+
showFeatureDetailsWidget(
|
|
290
|
+
feature: AnnotationFeature,
|
|
291
|
+
customWidgetNameAndId?: [string, string],
|
|
292
|
+
) {
|
|
293
|
+
const [region] = self.regions
|
|
294
|
+
const { assemblyName, refName } = region
|
|
295
|
+
const assembly = self.getAssemblyId(assemblyName)
|
|
296
|
+
if (!assembly) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
const { session } = self
|
|
300
|
+
const { changeManager } = session.apolloDataStore
|
|
301
|
+
const [widgetName, widgetId] = customWidgetNameAndId ?? [
|
|
302
|
+
'ApolloFeatureDetailsWidget',
|
|
303
|
+
'apolloFeatureDetailsWidget',
|
|
304
|
+
]
|
|
305
|
+
const apolloFeatureWidget = (
|
|
306
|
+
session as unknown as SessionWithWidgets
|
|
307
|
+
).addWidget(widgetName, widgetId, {
|
|
308
|
+
feature,
|
|
309
|
+
assembly,
|
|
310
|
+
refName,
|
|
311
|
+
changeManager,
|
|
312
|
+
})
|
|
313
|
+
;(session as unknown as SessionWithWidgets).showWidget(
|
|
314
|
+
apolloFeatureWidget,
|
|
315
|
+
)
|
|
316
|
+
},
|
|
288
317
|
afterAttach() {
|
|
289
318
|
addDisposer(
|
|
290
319
|
self,
|