@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
|
@@ -4,21 +4,30 @@
|
|
|
4
4
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
5
5
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
6
6
|
import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
|
|
7
|
-
import { AddFeatureChange } from '@apollo-annotation/shared'
|
|
8
7
|
import { type Assembly } from '@jbrowse/core/assemblyManager/assembly'
|
|
9
8
|
import { type DisplayType } from '@jbrowse/core/pluggableElementTypes'
|
|
10
9
|
import type PluggableElementBase from '@jbrowse/core/pluggableElementTypes/PluggableElementBase'
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type AbstractSessionModel,
|
|
12
|
+
getContainingView,
|
|
13
|
+
getSession,
|
|
14
|
+
} from '@jbrowse/core/util'
|
|
12
15
|
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
13
16
|
import AddIcon from '@mui/icons-material/Add'
|
|
14
17
|
import ObjectID from 'bson-objectid'
|
|
15
18
|
|
|
16
|
-
import {
|
|
19
|
+
import { CreateApolloAnnotation } from '../components/CreateApolloAnnotation'
|
|
17
20
|
|
|
18
|
-
function parseCigar(cigar: string): [string
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
function parseCigar(cigar: string): [string, number][] {
|
|
22
|
+
const regex = /(\d+)([MIDNSHPX=])/g
|
|
23
|
+
const result: [string, number][] = []
|
|
24
|
+
let match
|
|
25
|
+
|
|
26
|
+
while ((match = regex.exec(cigar)) !== null) {
|
|
27
|
+
result.push([match[2], Number.parseInt(match[1], 10)])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export function annotationFromPileup(pluggableElement: PluggableElementBase) {
|
|
@@ -62,55 +71,87 @@ export function annotationFromPileup(pluggableElement: PluggableElementBase) {
|
|
|
62
71
|
}
|
|
63
72
|
return refSeqId
|
|
64
73
|
},
|
|
65
|
-
|
|
74
|
+
getAnnotationFeature() {
|
|
66
75
|
const feature = self.contextMenuFeature
|
|
67
76
|
const assembly = self.getAssembly()
|
|
68
77
|
const refSeqId = self.getRefSeqId(assembly)
|
|
78
|
+
const start: number = feature.get('start')
|
|
79
|
+
const end: number = feature.get('end')
|
|
80
|
+
const strand = feature.get('strand')
|
|
81
|
+
const name = feature.get('name')
|
|
82
|
+
|
|
69
83
|
const cigarData: string = feature.get('CIGAR')
|
|
70
84
|
const ops = parseCigar(cigarData)
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
let openStart: number | undefined
|
|
85
|
+
let position = start
|
|
86
|
+
let currentExonStart: number | undefined
|
|
74
87
|
const exons: {
|
|
75
88
|
start: number
|
|
76
89
|
end: number
|
|
77
90
|
}[] = []
|
|
91
|
+
|
|
92
|
+
// Example: [[96,S], [4,M], [4216,N], [357,M], [1,I], [628,M], [94,S]]
|
|
93
|
+
// Results in 2 exons
|
|
94
|
+
// M, = and X are matches -> exon
|
|
95
|
+
// N is a gap in the reference sequence -> intron
|
|
96
|
+
// I, S, H and P -> not counted in reference position
|
|
78
97
|
for (const [op, len] of ops) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
switch (op) {
|
|
99
|
+
case 'M':
|
|
100
|
+
case '=':
|
|
101
|
+
case 'X': {
|
|
102
|
+
if (currentExonStart === undefined) {
|
|
103
|
+
currentExonStart = position
|
|
104
|
+
}
|
|
105
|
+
position += len
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'N': {
|
|
110
|
+
if (currentExonStart !== undefined) {
|
|
111
|
+
exons.push({
|
|
112
|
+
start: currentExonStart,
|
|
113
|
+
end: position,
|
|
114
|
+
})
|
|
115
|
+
currentExonStart = undefined
|
|
116
|
+
}
|
|
117
|
+
position += len
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
case 'D': {
|
|
121
|
+
position += len
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
case 'I':
|
|
125
|
+
case 'S':
|
|
126
|
+
case 'H':
|
|
127
|
+
case 'P': {
|
|
128
|
+
// These operations do not affect the position in the reference sequence
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
default: {
|
|
132
|
+
throw new Error(`Unknown CIGAR operation: ${op}`)
|
|
85
133
|
}
|
|
86
|
-
} else if (op === 'N' && openStart !== undefined) {
|
|
87
|
-
// if it was open, then close and add the subfeature
|
|
88
|
-
exons.push({
|
|
89
|
-
start: openStart,
|
|
90
|
-
end: currOffset + openStart,
|
|
91
|
-
})
|
|
92
|
-
openStart = undefined
|
|
93
|
-
}
|
|
94
|
-
if (op !== 'I') {
|
|
95
|
-
// we ignore insertions when calculating potential exon length
|
|
96
|
-
currOffset += len
|
|
97
134
|
}
|
|
98
135
|
}
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
|
|
137
|
+
// If still in exon at end
|
|
138
|
+
if (currentExonStart !== undefined) {
|
|
101
139
|
exons.push({
|
|
102
|
-
start:
|
|
103
|
-
end:
|
|
140
|
+
start: currentExonStart,
|
|
141
|
+
end: position,
|
|
104
142
|
})
|
|
105
143
|
}
|
|
106
144
|
|
|
107
145
|
const newFeature: AnnotationFeatureSnapshot = {
|
|
108
146
|
_id: ObjectID().toHexString(),
|
|
109
147
|
refSeq: refSeqId,
|
|
110
|
-
min:
|
|
111
|
-
max:
|
|
148
|
+
min: start,
|
|
149
|
+
max: end,
|
|
112
150
|
type: 'mRNA',
|
|
113
|
-
strand
|
|
151
|
+
strand,
|
|
152
|
+
attributes: {
|
|
153
|
+
name: [name],
|
|
154
|
+
},
|
|
114
155
|
}
|
|
115
156
|
if (exons.length === 0) {
|
|
116
157
|
return newFeature
|
|
@@ -118,75 +159,28 @@ export function annotationFromPileup(pluggableElement: PluggableElementBase) {
|
|
|
118
159
|
|
|
119
160
|
const children: Record<string, AnnotationFeatureSnapshot> = {}
|
|
120
161
|
newFeature.children = children
|
|
121
|
-
const [firstExon] = exons
|
|
122
|
-
const cdsFeature: AnnotationFeatureSnapshot = {
|
|
123
|
-
_id: ObjectID().toHexString(),
|
|
124
|
-
refSeq: refSeqId,
|
|
125
|
-
min: firstExon.start,
|
|
126
|
-
max: firstExon.end,
|
|
127
|
-
type: 'CDS',
|
|
128
|
-
strand: feature.get('strand'),
|
|
129
|
-
}
|
|
130
|
-
newFeature.children[cdsFeature._id] = cdsFeature
|
|
131
|
-
if (exons.length === 1) {
|
|
132
|
-
const exon: AnnotationFeatureSnapshot = {
|
|
133
|
-
_id: ObjectID().toHexString(),
|
|
134
|
-
refSeq: refSeqId,
|
|
135
|
-
min: firstExon.start,
|
|
136
|
-
max: firstExon.end,
|
|
137
|
-
type: 'exon',
|
|
138
|
-
strand: feature.get('strand'),
|
|
139
|
-
}
|
|
140
|
-
newFeature.children[exon._id] = exon
|
|
141
|
-
return newFeature
|
|
142
|
-
}
|
|
143
162
|
|
|
144
|
-
const discontinuousLocations: {
|
|
145
|
-
start: number
|
|
146
|
-
end: number
|
|
147
|
-
phase: 0 | 1 | 2
|
|
148
|
-
}[] = []
|
|
149
|
-
let phase: 0 | 1 | 2 = 0
|
|
150
163
|
for (const exon of exons) {
|
|
151
|
-
cdsFeature.min = Math.min(cdsFeature.min, exon.start)
|
|
152
|
-
cdsFeature.max = Math.max(cdsFeature.max, exon.end)
|
|
153
|
-
const { end, start } = exon
|
|
154
|
-
discontinuousLocations.push({ start, end, phase })
|
|
155
|
-
const localPhase = (end - start) % 3
|
|
156
|
-
phase = ((phase + localPhase) % 3) as 0 | 1 | 2
|
|
157
164
|
const newExon: AnnotationFeatureSnapshot = {
|
|
158
165
|
_id: ObjectID().toHexString(),
|
|
159
166
|
refSeq: refSeqId,
|
|
160
|
-
min: start,
|
|
161
|
-
max: end,
|
|
167
|
+
min: exon.start,
|
|
168
|
+
max: exon.end,
|
|
162
169
|
type: 'exon',
|
|
163
|
-
strand
|
|
170
|
+
strand,
|
|
164
171
|
}
|
|
165
172
|
newFeature.children[newExon._id] = newExon
|
|
166
173
|
}
|
|
167
174
|
return newFeature
|
|
168
175
|
},
|
|
169
|
-
async onPileupFeatureContext() {
|
|
170
|
-
const newFeature = self.createFeature()
|
|
171
|
-
const assembly = self.getAssembly()
|
|
172
|
-
const assemblyId = assembly.name
|
|
173
|
-
const change = new AddFeatureChange({
|
|
174
|
-
changedIds: [newFeature._id],
|
|
175
|
-
typeName: 'AddFeatureChange',
|
|
176
|
-
assembly: assemblyId,
|
|
177
|
-
addedFeature: newFeature,
|
|
178
|
-
})
|
|
179
|
-
const session = getSession(self)
|
|
180
|
-
await (
|
|
181
|
-
session as unknown as ApolloSessionModel
|
|
182
|
-
).apolloDataStore.changeManager.submit(change)
|
|
183
|
-
session.notify('Annotation added successfully', 'success')
|
|
184
|
-
},
|
|
185
176
|
}))
|
|
186
177
|
.views((self) => {
|
|
187
178
|
const superContextMenuItems = self.contextMenuItems
|
|
188
179
|
return {
|
|
189
180
|
contextMenuItems() {
|
|
181
|
+
const session = getSession(self)
|
|
182
|
+
const assembly = self.getAssembly()
|
|
183
|
+
const region = self.getFirstRegion()
|
|
190
184
|
const feature = self.contextMenuFeature
|
|
191
185
|
if (!feature) {
|
|
192
186
|
return superContextMenuItems()
|
|
@@ -196,7 +190,23 @@ export function annotationFromPileup(pluggableElement: PluggableElementBase) {
|
|
|
196
190
|
{
|
|
197
191
|
label: 'Create Apollo annotation',
|
|
198
192
|
icon: AddIcon,
|
|
199
|
-
onClick:
|
|
193
|
+
onClick: () => {
|
|
194
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
195
|
+
(doneCallback) => [
|
|
196
|
+
CreateApolloAnnotation,
|
|
197
|
+
{
|
|
198
|
+
session,
|
|
199
|
+
handleClose: () => {
|
|
200
|
+
doneCallback()
|
|
201
|
+
},
|
|
202
|
+
annotationFeature: self.getAnnotationFeature(assembly),
|
|
203
|
+
assembly,
|
|
204
|
+
refSeqId: self.getRefSeqId(assembly),
|
|
205
|
+
region,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
},
|
|
200
210
|
},
|
|
201
211
|
]
|
|
202
212
|
},
|
package/src/session/session.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { type ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
|
|
37
37
|
import { ApolloJobModel } from '../ApolloJobModel'
|
|
38
38
|
import { type ChangeManager } from '../ChangeManager'
|
|
39
|
+
import type ApolloPluginConfigurationSchema from '../config'
|
|
39
40
|
import { type ApolloRootModel } from '../types'
|
|
40
41
|
import { createFetchErrorMessage } from '../util'
|
|
41
42
|
|
|
@@ -185,15 +186,6 @@ export function extendSession(
|
|
|
185
186
|
}))
|
|
186
187
|
.actions((self) => ({
|
|
187
188
|
afterCreate: flow(function* afterCreate() {
|
|
188
|
-
// When the initial config.json loads, it doesn't include the Apollo
|
|
189
|
-
// tracks, which would result in a potentially invalid session snapshot
|
|
190
|
-
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
191
|
-
// empty session snapshot, and then restore the original session
|
|
192
|
-
// snapshot after the updated config.json loads.
|
|
193
|
-
const sessionSnapshot = getSnapshot(self)
|
|
194
|
-
const { id, name } = sessionSnapshot
|
|
195
|
-
applySnapshot(self, { name, id })
|
|
196
|
-
const { internetAccounts, jbrowse } = getRoot<ApolloRootModel>(self)
|
|
197
189
|
autorun(
|
|
198
190
|
() => {
|
|
199
191
|
// broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
|
|
@@ -254,7 +246,29 @@ export function extendSession(
|
|
|
254
246
|
},
|
|
255
247
|
{ name: 'ApolloSession' },
|
|
256
248
|
)
|
|
257
|
-
//
|
|
249
|
+
// When the initial config.json loads, it doesn't include the Apollo
|
|
250
|
+
// tracks, which would result in a potentially invalid session snapshot
|
|
251
|
+
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
252
|
+
// empty session snapshot, and then restore the original session
|
|
253
|
+
// snapshot after the updated config.json loads.
|
|
254
|
+
// @ts-expect-error type is missing on ApolloRootModel
|
|
255
|
+
const { internetAccounts, jbrowse, reloadPluginManagerCallback } =
|
|
256
|
+
getRoot<ApolloRootModel>(self)
|
|
257
|
+
const pluginConfiguration =
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
259
|
+
jbrowse.configuration.ApolloPlugin as Instance<
|
|
260
|
+
typeof ApolloPluginConfigurationSchema
|
|
261
|
+
>
|
|
262
|
+
const hasRole = readConfObject(
|
|
263
|
+
pluginConfiguration,
|
|
264
|
+
'hasRole',
|
|
265
|
+
) as boolean
|
|
266
|
+
if (hasRole) {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const sessionSnapshot = getSnapshot(self)
|
|
270
|
+
const { id, name } = sessionSnapshot
|
|
271
|
+
applySnapshot(self, { name, id })
|
|
258
272
|
|
|
259
273
|
// fetch and initialize assemblies for each of our Apollo internet accounts
|
|
260
274
|
for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
|
|
@@ -290,9 +304,8 @@ export function extendSession(
|
|
|
290
304
|
console.error(error)
|
|
291
305
|
continue
|
|
292
306
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
applySnapshot(self, sessionSnapshot)
|
|
307
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
308
|
+
reloadPluginManagerCallback(jbrowseConfig, sessionSnapshot)
|
|
296
309
|
}
|
|
297
310
|
}),
|
|
298
311
|
beforeDestroy() {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
2
|
|
|
3
|
+
import { type MousePosition } from '../LinearApolloDisplay/stateModel/mouseEvents'
|
|
4
|
+
|
|
3
5
|
export function getFeatureName(feature: AnnotationFeature) {
|
|
4
6
|
const { attributes } = feature
|
|
5
7
|
const name = attributes.get('gff_name')
|
|
@@ -51,3 +53,66 @@ export function getStrand(strand: number | undefined) {
|
|
|
51
53
|
}
|
|
52
54
|
return ''
|
|
53
55
|
}
|
|
56
|
+
|
|
57
|
+
function getChildren(feature: AnnotationFeature): AnnotationFeature[] {
|
|
58
|
+
const children: AnnotationFeature[] = []
|
|
59
|
+
//
|
|
60
|
+
if (feature.children) {
|
|
61
|
+
for (const [, ff] of feature.children) {
|
|
62
|
+
children.push(ff)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return children
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getParents(feature: AnnotationFeature): AnnotationFeature[] {
|
|
69
|
+
const parents: AnnotationFeature[] = []
|
|
70
|
+
let { parent } = feature
|
|
71
|
+
while (parent) {
|
|
72
|
+
parents.push(parent)
|
|
73
|
+
;({ parent } = parent)
|
|
74
|
+
}
|
|
75
|
+
return parents
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getFeaturesUnderClick(
|
|
79
|
+
mousePosition: MousePosition,
|
|
80
|
+
includeSiblings = false,
|
|
81
|
+
): AnnotationFeature[] {
|
|
82
|
+
const clickedFeatures: AnnotationFeature[] = []
|
|
83
|
+
if (!mousePosition.featureAndGlyphUnderMouse) {
|
|
84
|
+
return clickedFeatures
|
|
85
|
+
}
|
|
86
|
+
clickedFeatures.push(mousePosition.featureAndGlyphUnderMouse.feature)
|
|
87
|
+
for (const x of getParents(mousePosition.featureAndGlyphUnderMouse.feature)) {
|
|
88
|
+
clickedFeatures.push(x)
|
|
89
|
+
}
|
|
90
|
+
const { bp } = mousePosition
|
|
91
|
+
const children = getChildren(mousePosition.featureAndGlyphUnderMouse.feature)
|
|
92
|
+
for (const child of children) {
|
|
93
|
+
if (child.min < bp && child.max >= bp) {
|
|
94
|
+
clickedFeatures.push(child)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!includeSiblings) {
|
|
98
|
+
return clickedFeatures
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Also add siblings , i.e. features having the same parent as the clicked
|
|
102
|
+
// one and intersecting the click position
|
|
103
|
+
if (mousePosition.featureAndGlyphUnderMouse.feature.parent) {
|
|
104
|
+
const siblings =
|
|
105
|
+
mousePosition.featureAndGlyphUnderMouse.feature.parent.children
|
|
106
|
+
if (siblings) {
|
|
107
|
+
for (const [, sib] of siblings) {
|
|
108
|
+
if (sib._id == mousePosition.featureAndGlyphUnderMouse.feature._id) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if (sib.min < bp && sib.max >= bp) {
|
|
112
|
+
clickedFeatures.push(sib)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return clickedFeatures
|
|
118
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export async function copyToClipboard(element: HTMLElement) {
|
|
2
|
+
if (isSecureContext) {
|
|
3
|
+
const textBlob = new Blob([element.outerText], { type: 'text/plain' })
|
|
4
|
+
const htmlBlob = new Blob([element.outerHTML], { type: 'text/html' })
|
|
5
|
+
const clipboardItem = new ClipboardItem({
|
|
6
|
+
[textBlob.type]: textBlob,
|
|
7
|
+
[htmlBlob.type]: htmlBlob,
|
|
8
|
+
})
|
|
9
|
+
return navigator.clipboard.write([clipboardItem])
|
|
10
|
+
}
|
|
11
|
+
const copyCallback = (event: ClipboardEvent) => {
|
|
12
|
+
event.clipboardData?.setData('text/plain', element.outerText)
|
|
13
|
+
event.clipboardData?.setData('text/html', element.outerHTML)
|
|
14
|
+
event.preventDefault()
|
|
15
|
+
}
|
|
16
|
+
document.addEventListener('copy', copyCallback)
|
|
17
|
+
// fall back to deprecated only in non-secure contexts
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
19
|
+
document.execCommand('copy')
|
|
20
|
+
document.removeEventListener('copy', copyCallback)
|
|
21
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnnotationFeature,
|
|
3
|
+
type TranscriptPartCoding,
|
|
4
|
+
} from '@apollo-annotation/mst'
|
|
5
|
+
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
6
|
+
|
|
7
|
+
export function getMinAndMaxPx(
|
|
8
|
+
feature: AnnotationFeature | TranscriptPartCoding,
|
|
9
|
+
refName: string,
|
|
10
|
+
regionNumber: number,
|
|
11
|
+
lgv: LinearGenomeViewModel,
|
|
12
|
+
): [number, number] | undefined {
|
|
13
|
+
const minPxInfo = lgv.bpToPx({
|
|
14
|
+
refName,
|
|
15
|
+
coord: feature.min,
|
|
16
|
+
regionNumber,
|
|
17
|
+
})
|
|
18
|
+
const maxPxInfo = lgv.bpToPx({
|
|
19
|
+
refName,
|
|
20
|
+
coord: feature.max,
|
|
21
|
+
regionNumber,
|
|
22
|
+
})
|
|
23
|
+
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const { offsetPx } = lgv
|
|
27
|
+
const minPx = minPxInfo.offsetPx - offsetPx
|
|
28
|
+
const maxPx = maxPxInfo.offsetPx - offsetPx
|
|
29
|
+
return [minPx, maxPx]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getOverlappingEdge(
|
|
33
|
+
feature: AnnotationFeature,
|
|
34
|
+
x: number,
|
|
35
|
+
minMax: [number, number],
|
|
36
|
+
): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
|
|
37
|
+
const [minPx, maxPx] = minMax
|
|
38
|
+
// Feature is too small to tell if we're overlapping an edge
|
|
39
|
+
if (Math.abs(maxPx - minPx) < 8) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (Math.abs(minPx - x) < 4) {
|
|
43
|
+
return { feature, edge: 'min' }
|
|
44
|
+
}
|
|
45
|
+
if (Math.abs(maxPx - x) < 4) {
|
|
46
|
+
return { feature, edge: 'max' }
|
|
47
|
+
}
|
|
48
|
+
return
|
|
49
|
+
}
|
package/src/util/index.ts
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
+
|
|
3
|
+
type MinEdge = 'min'
|
|
4
|
+
type MaxEdge = 'max'
|
|
5
|
+
export type Edge = MinEdge | MaxEdge
|
|
6
|
+
|
|
7
|
+
interface LocationChange {
|
|
8
|
+
featureId: string
|
|
9
|
+
oldLocation: number
|
|
10
|
+
newLocation: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function expandFeatures(
|
|
14
|
+
feature: AnnotationFeature,
|
|
15
|
+
newLocation: number,
|
|
16
|
+
edge: Edge,
|
|
17
|
+
): LocationChange[] {
|
|
18
|
+
const featureId = feature._id
|
|
19
|
+
const oldLocation = feature[edge]
|
|
20
|
+
const changes: LocationChange[] = [{ featureId, oldLocation, newLocation }]
|
|
21
|
+
const { parent } = feature
|
|
22
|
+
if (
|
|
23
|
+
parent &&
|
|
24
|
+
((edge === 'min' && parent[edge] > newLocation) ||
|
|
25
|
+
(edge === 'max' && parent[edge] < newLocation))
|
|
26
|
+
) {
|
|
27
|
+
changes.push(...expandFeatures(parent, newLocation, edge))
|
|
28
|
+
}
|
|
29
|
+
return changes
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shrinkFeatures(
|
|
33
|
+
feature: AnnotationFeature,
|
|
34
|
+
newLocation: number,
|
|
35
|
+
edge: Edge,
|
|
36
|
+
shrinkParent: boolean,
|
|
37
|
+
childIdToSkip?: string,
|
|
38
|
+
): LocationChange[] {
|
|
39
|
+
const featureId = feature._id
|
|
40
|
+
const oldLocation = feature[edge]
|
|
41
|
+
const changes: LocationChange[] = [{ featureId, oldLocation, newLocation }]
|
|
42
|
+
const { parent, children } = feature
|
|
43
|
+
if (children) {
|
|
44
|
+
for (const [, child] of children) {
|
|
45
|
+
if (child._id === childIdToSkip) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
(edge === 'min' && child[edge] < newLocation) ||
|
|
50
|
+
(edge === 'max' && child[edge] > newLocation)
|
|
51
|
+
) {
|
|
52
|
+
changes.push(...shrinkFeatures(child, newLocation, edge, shrinkParent))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (parent && shrinkParent) {
|
|
57
|
+
const siblings: AnnotationFeature[] = []
|
|
58
|
+
if (parent.children) {
|
|
59
|
+
for (const [, c] of parent.children) {
|
|
60
|
+
if (c._id === featureId) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
siblings.push(c)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (siblings.length === 0) {
|
|
67
|
+
changes.push(
|
|
68
|
+
...shrinkFeatures(parent, newLocation, edge, shrinkParent, featureId),
|
|
69
|
+
)
|
|
70
|
+
} else {
|
|
71
|
+
const oldLocation = parent[edge]
|
|
72
|
+
const boundedLocation = Math[edge](
|
|
73
|
+
...siblings.map((s) => s[edge]),
|
|
74
|
+
newLocation,
|
|
75
|
+
)
|
|
76
|
+
if (boundedLocation !== oldLocation) {
|
|
77
|
+
changes.push(
|
|
78
|
+
...shrinkFeatures(
|
|
79
|
+
parent,
|
|
80
|
+
boundedLocation,
|
|
81
|
+
edge,
|
|
82
|
+
shrinkParent,
|
|
83
|
+
featureId,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return changes
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getPropagatedLocationChanges(
|
|
93
|
+
feature: AnnotationFeature,
|
|
94
|
+
newLocation: number,
|
|
95
|
+
edge: Edge,
|
|
96
|
+
shrinkParent = false,
|
|
97
|
+
): LocationChange[] {
|
|
98
|
+
const oldLocation = feature[edge]
|
|
99
|
+
if (newLocation === oldLocation) {
|
|
100
|
+
throw new Error(`New and existing locations are the same: "${newLocation}"`)
|
|
101
|
+
}
|
|
102
|
+
if (edge === 'min') {
|
|
103
|
+
if (newLocation > oldLocation) {
|
|
104
|
+
// shrinking feature, may need to shrink children and/or parents
|
|
105
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent)
|
|
106
|
+
}
|
|
107
|
+
return expandFeatures(feature, newLocation, edge)
|
|
108
|
+
}
|
|
109
|
+
if (newLocation < oldLocation) {
|
|
110
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent)
|
|
111
|
+
}
|
|
112
|
+
return expandFeatures(feature, newLocation, edge)
|
|
113
|
+
}
|