@apollo-annotation/jbrowse-plugin-apollo 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +11212 -10483
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +11251 -10509
- 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 +7726 -9014
- 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 +18 -18
- package/src/ApolloInternetAccount/model.ts +123 -70
- package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
- package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
- package/src/BackendDrivers/CollaborationServerDriver.ts +72 -20
- package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
- package/src/ChangeManager.ts +36 -14
- package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
- package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
- package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +72 -234
- package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +23 -131
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +279 -217
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
- package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
- package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
- package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
- package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
- package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
- package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
- package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
- package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
- package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
- package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +157 -0
- package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +101 -38
- package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +334 -262
- package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
- package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
- package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
- package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
- package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
- package/src/components/AddAssembly.tsx +34 -38
- package/src/components/AddAssemblyAliases.tsx +1 -1
- package/src/components/AddChildFeature.tsx +5 -2
- package/src/components/AddFeature.tsx +30 -21
- package/src/components/AddRefSeqAliases.tsx +64 -50
- package/src/components/CopyFeature.tsx +4 -2
- package/src/components/CreateApolloAnnotation.tsx +22 -9
- package/src/components/DeleteAssembly.tsx +3 -10
- package/src/components/DownloadGFF3.tsx +2 -2
- package/src/components/EditZoomThresholdDialog.tsx +69 -0
- package/src/components/FilterFeatures.tsx +7 -7
- package/src/components/FilterTranscripts.tsx +6 -6
- package/src/components/ImportFeatures.tsx +1 -1
- package/src/components/ManageChecks.tsx +3 -10
- package/src/components/ManageUsers.tsx +23 -22
- package/src/components/MergeTranscripts.tsx +12 -15
- package/src/components/OntologyTermAutocomplete.tsx +1 -8
- package/src/components/OntologyTermMultiSelect.tsx +11 -11
- package/src/components/OpenLocalFile.tsx +11 -7
- package/src/components/ViewChangeLog.tsx +25 -50
- package/src/components/ViewCheckResults.tsx +2 -8
- package/src/components/index.ts +1 -0
- package/src/config.ts +6 -0
- package/src/index.ts +53 -115
- package/src/makeDisplayComponent.tsx +9 -14
- package/src/menus/index.ts +1 -0
- package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
- package/src/menus/topLevelMenuAdmin.ts +154 -0
- package/src/session/ClientDataStore.ts +32 -14
- package/src/session/session.ts +159 -121
- package/src/util/annotationFeatureUtils.ts +15 -21
- package/src/util/displayUtils.ts +149 -0
- package/src/util/glyphUtils.ts +329 -0
- package/src/util/loadAssemblyIntoClient.ts +3 -2
- package/src/util/mouseEventsUtils.ts +32 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { type CheckResultIdsType } from '@apollo-annotation/mst'
|
|
2
|
+
import { makeStyles } from 'tss-react/mui'
|
|
3
|
+
|
|
4
|
+
export { default as EditZoomThresholdDialog } from '../components/EditZoomThresholdDialog'
|
|
5
|
+
|
|
6
|
+
export type Coord = [number, number]
|
|
7
|
+
|
|
8
|
+
export const useStyles = makeStyles()((theme) => ({
|
|
9
|
+
canvasContainer: {
|
|
10
|
+
position: 'relative',
|
|
11
|
+
left: 0,
|
|
12
|
+
},
|
|
13
|
+
canvas: {
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
left: 0,
|
|
16
|
+
},
|
|
17
|
+
center: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
},
|
|
21
|
+
ellipses: {
|
|
22
|
+
textOverflow: 'ellipsis',
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
},
|
|
25
|
+
avatar: {
|
|
26
|
+
position: 'static',
|
|
27
|
+
height: '100%',
|
|
28
|
+
width: '100%',
|
|
29
|
+
overflow: 'visible',
|
|
30
|
+
color: theme.palette.warning.light,
|
|
31
|
+
backgroundColor: theme.palette.warning.contrastText,
|
|
32
|
+
},
|
|
33
|
+
box: {
|
|
34
|
+
position: 'absolute',
|
|
35
|
+
overflow: 'visible',
|
|
36
|
+
},
|
|
37
|
+
badge: {
|
|
38
|
+
display: 'inline-block',
|
|
39
|
+
},
|
|
40
|
+
loading: {
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
right: theme.spacing(3),
|
|
43
|
+
zIndex: 10,
|
|
44
|
+
pointerEvents: 'none',
|
|
45
|
+
textAlign: 'right',
|
|
46
|
+
},
|
|
47
|
+
locked: {
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
right: theme.spacing(3),
|
|
50
|
+
top: theme.spacing(6),
|
|
51
|
+
zIndex: 1,
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
textAlign: 'right',
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
export interface CheckResultCluster<T> {
|
|
58
|
+
_id: string
|
|
59
|
+
message: string
|
|
60
|
+
start: number
|
|
61
|
+
count: number
|
|
62
|
+
members: T[]
|
|
63
|
+
range: { min: number; max: number }
|
|
64
|
+
featureIds: CheckResultIdsType
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function clusterResultByMessage<
|
|
68
|
+
T extends {
|
|
69
|
+
_id: string
|
|
70
|
+
start: number
|
|
71
|
+
end: number
|
|
72
|
+
message: string
|
|
73
|
+
ids: CheckResultIdsType
|
|
74
|
+
},
|
|
75
|
+
>(
|
|
76
|
+
items: readonly T[],
|
|
77
|
+
width: number,
|
|
78
|
+
touchesAsOverlap: boolean,
|
|
79
|
+
): CheckResultCluster<T>[] {
|
|
80
|
+
const byMsg = new Map<string, T[]>()
|
|
81
|
+
for (const it of items) {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
83
|
+
;(byMsg.get(it.message) ?? byMsg.set(it.message, []).get(it.message)!).push(
|
|
84
|
+
it,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const clusters: CheckResultCluster<T>[] = []
|
|
89
|
+
const overlaps = (aEnd: number, bStart: number) =>
|
|
90
|
+
touchesAsOverlap ? bStart <= aEnd : bStart < aEnd
|
|
91
|
+
|
|
92
|
+
for (const [message, arr] of byMsg.entries()) {
|
|
93
|
+
if (arr.length === 0) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
arr.sort((a, b) => a.start - b.start)
|
|
98
|
+
|
|
99
|
+
let group: T[] = [arr[0]]
|
|
100
|
+
let curMin = arr[0].start
|
|
101
|
+
let curMax = arr[0].start + width
|
|
102
|
+
|
|
103
|
+
const pushResult = () => {
|
|
104
|
+
const starts = group.map((d) => d.start).sort((a, b) => a - b)
|
|
105
|
+
const mid = Math.floor(starts.length / 2)
|
|
106
|
+
const median: number =
|
|
107
|
+
starts.length % 2 ? starts[mid] : (starts[mid - 1] + starts[mid]) / 2
|
|
108
|
+
const clusterId = group[0]._id
|
|
109
|
+
const featureIds = group[0].ids
|
|
110
|
+
|
|
111
|
+
clusters.push({
|
|
112
|
+
_id: clusterId,
|
|
113
|
+
message,
|
|
114
|
+
start: median,
|
|
115
|
+
count: group.length,
|
|
116
|
+
members: [...group],
|
|
117
|
+
range: { min: curMin, max: curMax },
|
|
118
|
+
featureIds,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (let i = 1; i < arr.length; i++) {
|
|
123
|
+
const it = arr[i]
|
|
124
|
+
const itStart = it.start
|
|
125
|
+
const itEnd = itStart + width
|
|
126
|
+
|
|
127
|
+
if (overlaps(curMax, itStart)) {
|
|
128
|
+
group.push(it)
|
|
129
|
+
if (itStart < curMin) {
|
|
130
|
+
curMin = itStart
|
|
131
|
+
}
|
|
132
|
+
if (itEnd > curMax) {
|
|
133
|
+
curMax = itEnd
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
pushResult()
|
|
137
|
+
group = [it]
|
|
138
|
+
curMin = itStart
|
|
139
|
+
curMax = itEnd
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
pushResult()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
clusters.sort(
|
|
146
|
+
(a, b) => a.message.localeCompare(b.message) || a.start - b.start,
|
|
147
|
+
)
|
|
148
|
+
return clusters
|
|
149
|
+
}
|
package/src/util/glyphUtils.ts
CHANGED
|
@@ -2,7 +2,192 @@ import {
|
|
|
2
2
|
type AnnotationFeature,
|
|
3
3
|
type TranscriptPartCoding,
|
|
4
4
|
} from '@apollo-annotation/mst'
|
|
5
|
+
import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
|
|
6
|
+
import { type MenuItem } from '@jbrowse/core/ui'
|
|
7
|
+
import {
|
|
8
|
+
type AbstractSessionModel,
|
|
9
|
+
getContainingView,
|
|
10
|
+
} from '@jbrowse/core/util'
|
|
5
11
|
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
12
|
+
import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded'
|
|
13
|
+
import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded'
|
|
14
|
+
|
|
15
|
+
import { type LinearApolloDisplayMouseEvents } from '../LinearApolloDisplay/stateModel/mouseEvents'
|
|
16
|
+
import { type LinearApolloSixFrameDisplayMouseEvents } from '../LinearApolloSixFrameDisplay/stateModel/mouseEvents'
|
|
17
|
+
import { AddChildFeature, CopyFeature, DeleteFeature } from '../components'
|
|
18
|
+
import { type ApolloSessionModel } from '../session'
|
|
19
|
+
|
|
20
|
+
import { type MousePositionWithFeature } from '.'
|
|
21
|
+
|
|
22
|
+
type NavLocation = Parameters<LinearGenomeViewModel['navTo']>[0]
|
|
23
|
+
|
|
24
|
+
export function selectFeatureAndOpenWidget(
|
|
25
|
+
stateModel:
|
|
26
|
+
| LinearApolloDisplayMouseEvents
|
|
27
|
+
| LinearApolloSixFrameDisplayMouseEvents,
|
|
28
|
+
feature: AnnotationFeature,
|
|
29
|
+
) {
|
|
30
|
+
if (stateModel.apolloDragging) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
stateModel.setSelectedFeature(feature)
|
|
34
|
+
const { session } = stateModel
|
|
35
|
+
const { apolloDataStore } = session
|
|
36
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager
|
|
37
|
+
if (!featureTypeOntology) {
|
|
38
|
+
throw new Error('featureTypeOntology is undefined')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let containsCDSOrExon = false
|
|
42
|
+
for (const [, child] of feature.children ?? []) {
|
|
43
|
+
if (
|
|
44
|
+
featureTypeOntology.isTypeOf(child.type, 'CDS') ||
|
|
45
|
+
featureTypeOntology.isTypeOf(child.type, 'exon')
|
|
46
|
+
) {
|
|
47
|
+
containsCDSOrExon = true
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
(featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
53
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
|
|
54
|
+
containsCDSOrExon
|
|
55
|
+
) {
|
|
56
|
+
stateModel.showFeatureDetailsWidget(feature, [
|
|
57
|
+
'ApolloTranscriptDetails',
|
|
58
|
+
'apolloTranscriptDetails',
|
|
59
|
+
])
|
|
60
|
+
} else {
|
|
61
|
+
stateModel.showFeatureDetailsWidget(feature)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isTranscriptFeature(
|
|
66
|
+
feature: AnnotationFeature,
|
|
67
|
+
session: ApolloSessionModel,
|
|
68
|
+
): boolean {
|
|
69
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
70
|
+
if (!featureTypeOntology) {
|
|
71
|
+
throw new Error('featureTypeOntology is undefined')
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
75
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isExonFeature(
|
|
80
|
+
feature: AnnotationFeature,
|
|
81
|
+
session: ApolloSessionModel,
|
|
82
|
+
): boolean {
|
|
83
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
84
|
+
if (!featureTypeOntology) {
|
|
85
|
+
throw new Error('featureTypeOntology is undefined')
|
|
86
|
+
}
|
|
87
|
+
return featureTypeOntology.isTypeOf(feature.type, 'exon')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isCDSFeature(
|
|
91
|
+
feature: AnnotationFeature,
|
|
92
|
+
session: ApolloSessionModel,
|
|
93
|
+
): boolean {
|
|
94
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
95
|
+
if (!featureTypeOntology) {
|
|
96
|
+
throw new Error('featureTypeOntology is undefined')
|
|
97
|
+
}
|
|
98
|
+
return featureTypeOntology.isTypeOf(feature.type, 'CDS')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AdjacentExons {
|
|
102
|
+
upstream: AnnotationFeature | undefined
|
|
103
|
+
downstream: AnnotationFeature | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getAdjacentExons(
|
|
107
|
+
currentExon: AnnotationFeature,
|
|
108
|
+
display:
|
|
109
|
+
| LinearApolloDisplayMouseEvents
|
|
110
|
+
| LinearApolloSixFrameDisplayMouseEvents,
|
|
111
|
+
mousePosition: MousePositionWithFeature,
|
|
112
|
+
session: ApolloSessionModel,
|
|
113
|
+
): AdjacentExons {
|
|
114
|
+
const lgv = getContainingView(
|
|
115
|
+
display as BaseDisplayModel,
|
|
116
|
+
) as unknown as LinearGenomeViewModel
|
|
117
|
+
|
|
118
|
+
// Genomic coords of current view
|
|
119
|
+
const viewGenomicLeft = mousePosition.bp - lgv.bpPerPx * mousePosition.x
|
|
120
|
+
const viewGenomicRight = viewGenomicLeft + lgv.coarseTotalBp
|
|
121
|
+
if (!currentExon.parent) {
|
|
122
|
+
return { upstream: undefined, downstream: undefined }
|
|
123
|
+
}
|
|
124
|
+
const transcript = currentExon.parent
|
|
125
|
+
if (!transcript.children) {
|
|
126
|
+
throw new Error(`Error getting children of ${transcript._id}`)
|
|
127
|
+
}
|
|
128
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
129
|
+
if (!featureTypeOntology) {
|
|
130
|
+
throw new Error('featureTypeOntology is undefined')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let exons = []
|
|
134
|
+
for (const [, child] of transcript.children) {
|
|
135
|
+
if (featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
136
|
+
exons.push(child)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const adjacentExons: AdjacentExons = {
|
|
140
|
+
upstream: undefined,
|
|
141
|
+
downstream: undefined,
|
|
142
|
+
}
|
|
143
|
+
exons = exons.sort((a, b) => (a.min < b.min ? -1 : 1))
|
|
144
|
+
for (const exon of exons) {
|
|
145
|
+
if (exon.min > viewGenomicRight) {
|
|
146
|
+
adjacentExons.downstream = exon
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exons = exons.sort((a, b) => (a.min > b.min ? -1 : 1))
|
|
151
|
+
for (const exon of exons) {
|
|
152
|
+
if (exon.max < viewGenomicLeft) {
|
|
153
|
+
adjacentExons.upstream = exon
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (transcript.strand === -1) {
|
|
158
|
+
const newUpstream = adjacentExons.downstream
|
|
159
|
+
adjacentExons.downstream = adjacentExons.upstream
|
|
160
|
+
adjacentExons.upstream = newUpstream
|
|
161
|
+
}
|
|
162
|
+
return adjacentExons
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getStreamIcon(
|
|
166
|
+
strand: 1 | -1 | undefined,
|
|
167
|
+
isUpstream: boolean,
|
|
168
|
+
isFlipped: boolean | undefined,
|
|
169
|
+
) {
|
|
170
|
+
// This is the icon you would use for strand=1, downstream, straight
|
|
171
|
+
// (non-flipped) view
|
|
172
|
+
let icon = SkipNextRoundedIcon
|
|
173
|
+
|
|
174
|
+
if (strand === -1) {
|
|
175
|
+
icon = SkipPreviousRoundedIcon
|
|
176
|
+
}
|
|
177
|
+
if (isUpstream) {
|
|
178
|
+
icon =
|
|
179
|
+
icon === SkipPreviousRoundedIcon
|
|
180
|
+
? SkipNextRoundedIcon
|
|
181
|
+
: SkipPreviousRoundedIcon
|
|
182
|
+
}
|
|
183
|
+
if (isFlipped) {
|
|
184
|
+
icon =
|
|
185
|
+
icon === SkipPreviousRoundedIcon
|
|
186
|
+
? SkipNextRoundedIcon
|
|
187
|
+
: SkipPreviousRoundedIcon
|
|
188
|
+
}
|
|
189
|
+
return icon
|
|
190
|
+
}
|
|
6
191
|
|
|
7
192
|
export function getMinAndMaxPx(
|
|
8
193
|
feature: AnnotationFeature | TranscriptPartCoding,
|
|
@@ -47,3 +232,147 @@ export function getOverlappingEdge(
|
|
|
47
232
|
}
|
|
48
233
|
return
|
|
49
234
|
}
|
|
235
|
+
|
|
236
|
+
export function isSelectedFeature(
|
|
237
|
+
feature: AnnotationFeature,
|
|
238
|
+
selectedFeature: AnnotationFeature | undefined,
|
|
239
|
+
) {
|
|
240
|
+
return Boolean(selectedFeature && feature._id === selectedFeature._id)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function containsSelectedFeature(
|
|
244
|
+
feature: AnnotationFeature,
|
|
245
|
+
selectedFeature: AnnotationFeature | undefined,
|
|
246
|
+
): boolean {
|
|
247
|
+
if (!selectedFeature) {
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
if (feature._id === selectedFeature._id) {
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
return feature.hasDescendant(selectedFeature._id)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function makeFeatureLabel(feature: AnnotationFeature) {
|
|
257
|
+
let name: string | undefined
|
|
258
|
+
if (feature.attributes.get('gff_name')) {
|
|
259
|
+
name = feature.attributes.get('gff_name')?.join(',')
|
|
260
|
+
} else if (feature.attributes.get('gff_id')) {
|
|
261
|
+
name = feature.attributes.get('gff_id')?.join(',')
|
|
262
|
+
} else {
|
|
263
|
+
name = feature._id
|
|
264
|
+
}
|
|
265
|
+
const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`
|
|
266
|
+
const maxLen = 60
|
|
267
|
+
if (name && name.length + coords.length > maxLen + 5) {
|
|
268
|
+
const trim = maxLen - coords.length
|
|
269
|
+
name = trim > 0 ? name.slice(0, trim) : ''
|
|
270
|
+
name = `${name}[...]`
|
|
271
|
+
}
|
|
272
|
+
return `${name} ${coords}`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function getContextMenuItemsForFeature(
|
|
276
|
+
display:
|
|
277
|
+
| LinearApolloSixFrameDisplayMouseEvents
|
|
278
|
+
| LinearApolloDisplayMouseEvents,
|
|
279
|
+
sourceFeature: AnnotationFeature,
|
|
280
|
+
): MenuItem[] {
|
|
281
|
+
const {
|
|
282
|
+
apolloInternetAccount: internetAccount,
|
|
283
|
+
changeManager,
|
|
284
|
+
regions,
|
|
285
|
+
selectedFeature,
|
|
286
|
+
session,
|
|
287
|
+
} = display
|
|
288
|
+
const menuItems: MenuItem[] = []
|
|
289
|
+
const role = internetAccount ? internetAccount.role : 'admin'
|
|
290
|
+
const admin = role === 'admin'
|
|
291
|
+
const readOnly = !(role && ['admin', 'user'].includes(role))
|
|
292
|
+
const [region] = regions
|
|
293
|
+
const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
|
|
294
|
+
const currentAssemblyId = display.getAssemblyId(region.assemblyName)
|
|
295
|
+
menuItems.push(
|
|
296
|
+
{
|
|
297
|
+
label: makeFeatureLabel(sourceFeature),
|
|
298
|
+
type: 'subHeader',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
label: 'Add child feature',
|
|
302
|
+
disabled: readOnly,
|
|
303
|
+
onClick: () => {
|
|
304
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
305
|
+
(doneCallback) => [
|
|
306
|
+
AddChildFeature,
|
|
307
|
+
{
|
|
308
|
+
session,
|
|
309
|
+
handleClose: () => {
|
|
310
|
+
doneCallback()
|
|
311
|
+
},
|
|
312
|
+
changeManager,
|
|
313
|
+
sourceFeature,
|
|
314
|
+
sourceAssemblyId,
|
|
315
|
+
internetAccount,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
)
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
label: 'Copy features and annotations',
|
|
323
|
+
disabled: readOnly,
|
|
324
|
+
onClick: () => {
|
|
325
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
326
|
+
(doneCallback) => [
|
|
327
|
+
CopyFeature,
|
|
328
|
+
{
|
|
329
|
+
session,
|
|
330
|
+
handleClose: () => {
|
|
331
|
+
doneCallback()
|
|
332
|
+
},
|
|
333
|
+
changeManager,
|
|
334
|
+
sourceFeature,
|
|
335
|
+
sourceAssemblyId: currentAssemblyId,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
)
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
label: 'Delete feature',
|
|
343
|
+
disabled: !admin,
|
|
344
|
+
onClick: () => {
|
|
345
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
346
|
+
(doneCallback) => [
|
|
347
|
+
DeleteFeature,
|
|
348
|
+
{
|
|
349
|
+
session,
|
|
350
|
+
handleClose: () => {
|
|
351
|
+
doneCallback()
|
|
352
|
+
},
|
|
353
|
+
changeManager,
|
|
354
|
+
sourceFeature,
|
|
355
|
+
sourceAssemblyId: currentAssemblyId,
|
|
356
|
+
selectedFeature,
|
|
357
|
+
setSelectedFeature: (feature?: AnnotationFeature) => {
|
|
358
|
+
display.setSelectedFeature(feature)
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
)
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
return menuItems
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function navToFeatureCenter(
|
|
370
|
+
feature: AnnotationFeature,
|
|
371
|
+
paddingPct: number,
|
|
372
|
+
refSeqLength: number,
|
|
373
|
+
): NavLocation {
|
|
374
|
+
const paddingBp = (feature.max - feature.min) * paddingPct
|
|
375
|
+
const start = Math.max(feature.min - paddingBp, 1)
|
|
376
|
+
const end = Math.min(feature.max + paddingBp, refSeqLength)
|
|
377
|
+
return { refName: feature.refSeq, start, end }
|
|
378
|
+
}
|
|
@@ -4,10 +4,11 @@ import {
|
|
|
4
4
|
type CheckResultSnapshot,
|
|
5
5
|
} from '@apollo-annotation/mst'
|
|
6
6
|
import { gff3ToAnnotationFeature } from '@apollo-annotation/shared'
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
8
|
type GFF3Comment,
|
|
9
9
|
type GFF3Feature,
|
|
10
10
|
type GFF3Sequence,
|
|
11
|
+
parseStringSync,
|
|
11
12
|
} from '@gmod/gff'
|
|
12
13
|
import { getSnapshot } from 'mobx-state-tree'
|
|
13
14
|
|
|
@@ -17,7 +18,7 @@ export async function loadAssemblyIntoClient(
|
|
|
17
18
|
apolloDataStore: ClientDataStore,
|
|
18
19
|
) {
|
|
19
20
|
const featuresAndSequences: (GFF3Feature | GFF3Sequence | GFF3Comment)[] =
|
|
20
|
-
|
|
21
|
+
parseStringSync(gff3FileText, {
|
|
21
22
|
parseSequences: true,
|
|
22
23
|
parseComments: true,
|
|
23
24
|
parseDirectives: false,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
+
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
2
3
|
|
|
3
4
|
type MinEdge = 'min'
|
|
4
5
|
type MaxEdge = 'max'
|
|
@@ -111,3 +112,34 @@ export function getPropagatedLocationChanges(
|
|
|
111
112
|
}
|
|
112
113
|
return expandFeatures(feature, newLocation, edge)
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
/** extended information about the position of the mouse on the canvas, including the refName, bp, and displayedRegion number */
|
|
117
|
+
export interface MousePosition {
|
|
118
|
+
x: number
|
|
119
|
+
y: number
|
|
120
|
+
refName: string
|
|
121
|
+
bp: number
|
|
122
|
+
regionNumber: number
|
|
123
|
+
feature?: AnnotationFeature
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type MousePositionWithFeature = Required<MousePosition>
|
|
127
|
+
|
|
128
|
+
export function isMousePositionWithFeature(
|
|
129
|
+
mousePosition: MousePosition,
|
|
130
|
+
): mousePosition is MousePositionWithFeature {
|
|
131
|
+
return 'feature' in mousePosition
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getMousePosition(
|
|
135
|
+
event: React.MouseEvent,
|
|
136
|
+
lgv: LinearGenomeViewModel,
|
|
137
|
+
): MousePosition {
|
|
138
|
+
const canvas = event.currentTarget
|
|
139
|
+
const { clientX, clientY } = event
|
|
140
|
+
const { left, top } = canvas.getBoundingClientRect()
|
|
141
|
+
const x = clientX - left
|
|
142
|
+
const y = clientY - top
|
|
143
|
+
const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x)
|
|
144
|
+
return { x, y, refName, bp, regionNumber }
|
|
145
|
+
}
|