@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.
Files changed (86) hide show
  1. package/dist/index.esm.js +11212 -10483
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +11251 -10509
  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 +7726 -9014
  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 +18 -18
  12. package/src/ApolloInternetAccount/model.ts +123 -70
  13. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  14. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +72 -20
  16. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  17. package/src/ChangeManager.ts +36 -14
  18. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  22. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +72 -234
  23. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +23 -131
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +279 -217
  27. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  28. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  29. package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
  30. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  31. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  32. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  33. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  34. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  36. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  37. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
  38. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
  39. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  40. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  41. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  42. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +157 -0
  43. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +101 -38
  44. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +334 -262
  45. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  46. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  47. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  48. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  49. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  50. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  51. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  52. package/src/components/AddAssembly.tsx +34 -38
  53. package/src/components/AddAssemblyAliases.tsx +1 -1
  54. package/src/components/AddChildFeature.tsx +5 -2
  55. package/src/components/AddFeature.tsx +30 -21
  56. package/src/components/AddRefSeqAliases.tsx +64 -50
  57. package/src/components/CopyFeature.tsx +4 -2
  58. package/src/components/CreateApolloAnnotation.tsx +22 -9
  59. package/src/components/DeleteAssembly.tsx +3 -10
  60. package/src/components/DownloadGFF3.tsx +2 -2
  61. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  62. package/src/components/FilterFeatures.tsx +7 -7
  63. package/src/components/FilterTranscripts.tsx +6 -6
  64. package/src/components/ImportFeatures.tsx +1 -1
  65. package/src/components/ManageChecks.tsx +3 -10
  66. package/src/components/ManageUsers.tsx +23 -22
  67. package/src/components/MergeTranscripts.tsx +12 -15
  68. package/src/components/OntologyTermAutocomplete.tsx +1 -8
  69. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  70. package/src/components/OpenLocalFile.tsx +11 -7
  71. package/src/components/ViewChangeLog.tsx +25 -50
  72. package/src/components/ViewCheckResults.tsx +2 -8
  73. package/src/components/index.ts +1 -0
  74. package/src/config.ts +6 -0
  75. package/src/index.ts +53 -115
  76. package/src/makeDisplayComponent.tsx +9 -14
  77. package/src/menus/index.ts +1 -0
  78. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  79. package/src/menus/topLevelMenuAdmin.ts +154 -0
  80. package/src/session/ClientDataStore.ts +32 -14
  81. package/src/session/session.ts +159 -121
  82. package/src/util/annotationFeatureUtils.ts +15 -21
  83. package/src/util/displayUtils.ts +149 -0
  84. package/src/util/glyphUtils.ts +329 -0
  85. package/src/util/loadAssemblyIntoClient.ts +3 -2
  86. 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
+ }
@@ -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 gff, {
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
- gff.parseStringSync(gff3FileText, {
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
+ }