@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 0.3.8
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 +4603 -2045
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
- 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 +9387 -4016
- 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 +15 -15
- package/src/ApolloInternetAccount/model.ts +48 -13
- package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
- package/src/ChangeManager.ts +42 -18
- package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
- package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
- package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
- package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
- package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
- package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
- 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/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 +481 -0
- package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
- package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
- package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
- package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
- package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
- package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
- package/src/OntologyManager/index.ts +4 -1
- package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
- package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
- package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
- package/src/components/AddAssembly.tsx +1 -1
- package/src/components/AddAssemblyAliases.tsx +114 -0
- package/src/components/AddChildFeature.tsx +7 -7
- package/src/components/AddFeature.tsx +20 -15
- package/src/components/AddRefSeqAliases.tsx +9 -9
- package/src/components/CopyFeature.tsx +4 -4
- package/src/components/CreateApolloAnnotation.tsx +335 -151
- package/src/components/DeleteAssembly.tsx +1 -1
- package/src/components/DeleteFeature.tsx +358 -11
- package/src/components/DownloadGFF3.tsx +20 -1
- package/src/components/EditZoomThresholdDialog.tsx +69 -0
- package/src/components/FilterFeatures.tsx +7 -7
- package/src/components/FilterTranscripts.tsx +86 -0
- package/src/components/ImportFeatures.tsx +1 -1
- package/src/components/ManageChecks.tsx +1 -1
- package/src/components/MergeExons.tsx +193 -0
- package/src/components/MergeTranscripts.tsx +182 -0
- package/src/components/OntologyTermMultiSelect.tsx +11 -11
- package/src/components/OpenLocalFile.tsx +11 -7
- package/src/components/SplitExon.tsx +134 -0
- package/src/components/ViewCheckResults.tsx +1 -1
- package/src/components/index.ts +4 -0
- package/src/config.ts +11 -0
- package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
- package/src/extensions/annotationFromPileup.ts +99 -89
- package/src/index.ts +42 -105
- package/src/makeDisplayComponent.tsx +0 -1
- package/src/menus/index.ts +1 -0
- package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
- package/src/menus/topLevelMenuAdmin.ts +154 -0
- package/src/session/session.ts +163 -104
- package/src/util/annotationFeatureUtils.ts +59 -0
- package/src/util/copyToClipboard.ts +21 -0
- package/src/util/displayUtils.ts +149 -0
- package/src/util/glyphUtils.ts +201 -0
- package/src/util/index.ts +2 -0
- package/src/util/mouseEventsUtils.ts +145 -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
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnnotationFeature,
|
|
3
|
+
type TranscriptPartCoding,
|
|
4
|
+
} from '@apollo-annotation/mst'
|
|
5
|
+
import { type MenuItem } from '@jbrowse/core/ui'
|
|
6
|
+
import { type AbstractSessionModel } from '@jbrowse/core/util'
|
|
7
|
+
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
8
|
+
|
|
9
|
+
import { type LinearApolloDisplayMouseEvents } from '../LinearApolloDisplay/stateModel/mouseEvents'
|
|
10
|
+
import { type LinearApolloSixFrameDisplayMouseEvents } from '../LinearApolloSixFrameDisplay/stateModel/mouseEvents'
|
|
11
|
+
import { AddChildFeature, CopyFeature, DeleteFeature } from '../components'
|
|
12
|
+
|
|
13
|
+
type NavLocation = Parameters<LinearGenomeViewModel['navTo']>[0]
|
|
14
|
+
|
|
15
|
+
export function getMinAndMaxPx(
|
|
16
|
+
feature: AnnotationFeature | TranscriptPartCoding,
|
|
17
|
+
refName: string,
|
|
18
|
+
regionNumber: number,
|
|
19
|
+
lgv: LinearGenomeViewModel,
|
|
20
|
+
): [number, number] | undefined {
|
|
21
|
+
const minPxInfo = lgv.bpToPx({
|
|
22
|
+
refName,
|
|
23
|
+
coord: feature.min,
|
|
24
|
+
regionNumber,
|
|
25
|
+
})
|
|
26
|
+
const maxPxInfo = lgv.bpToPx({
|
|
27
|
+
refName,
|
|
28
|
+
coord: feature.max,
|
|
29
|
+
regionNumber,
|
|
30
|
+
})
|
|
31
|
+
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const { offsetPx } = lgv
|
|
35
|
+
const minPx = minPxInfo.offsetPx - offsetPx
|
|
36
|
+
const maxPx = maxPxInfo.offsetPx - offsetPx
|
|
37
|
+
return [minPx, maxPx]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getOverlappingEdge(
|
|
41
|
+
feature: AnnotationFeature,
|
|
42
|
+
x: number,
|
|
43
|
+
minMax: [number, number],
|
|
44
|
+
): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
|
|
45
|
+
const [minPx, maxPx] = minMax
|
|
46
|
+
// Feature is too small to tell if we're overlapping an edge
|
|
47
|
+
if (Math.abs(maxPx - minPx) < 8) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (Math.abs(minPx - x) < 4) {
|
|
51
|
+
return { feature, edge: 'min' }
|
|
52
|
+
}
|
|
53
|
+
if (Math.abs(maxPx - x) < 4) {
|
|
54
|
+
return { feature, edge: 'max' }
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isSelectedFeature(
|
|
60
|
+
feature: AnnotationFeature,
|
|
61
|
+
selectedFeature: AnnotationFeature | undefined,
|
|
62
|
+
) {
|
|
63
|
+
return Boolean(selectedFeature && feature._id === selectedFeature._id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function containsSelectedFeature(
|
|
67
|
+
feature: AnnotationFeature,
|
|
68
|
+
selectedFeature: AnnotationFeature | undefined,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (!selectedFeature) {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
if (feature._id === selectedFeature._id) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
return feature.hasDescendant(selectedFeature._id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeFeatureLabel(feature: AnnotationFeature) {
|
|
80
|
+
let name: string | undefined
|
|
81
|
+
if (feature.attributes.get('gff_name')) {
|
|
82
|
+
name = feature.attributes.get('gff_name')?.join(',')
|
|
83
|
+
} else if (feature.attributes.get('gff_id')) {
|
|
84
|
+
name = feature.attributes.get('gff_id')?.join(',')
|
|
85
|
+
} else {
|
|
86
|
+
name = feature._id
|
|
87
|
+
}
|
|
88
|
+
const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`
|
|
89
|
+
const maxLen = 60
|
|
90
|
+
if (name && name.length + coords.length > maxLen + 5) {
|
|
91
|
+
const trim = maxLen - coords.length
|
|
92
|
+
name = trim > 0 ? name.slice(0, trim) : ''
|
|
93
|
+
name = `${name}[...]`
|
|
94
|
+
}
|
|
95
|
+
return `${name} ${coords}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getContextMenuItemsForFeature(
|
|
99
|
+
display:
|
|
100
|
+
| LinearApolloSixFrameDisplayMouseEvents
|
|
101
|
+
| LinearApolloDisplayMouseEvents,
|
|
102
|
+
sourceFeature: AnnotationFeature,
|
|
103
|
+
): MenuItem[] {
|
|
104
|
+
const {
|
|
105
|
+
apolloInternetAccount: internetAccount,
|
|
106
|
+
changeManager,
|
|
107
|
+
regions,
|
|
108
|
+
selectedFeature,
|
|
109
|
+
session,
|
|
110
|
+
} = display
|
|
111
|
+
const menuItems: MenuItem[] = []
|
|
112
|
+
const role = internetAccount ? internetAccount.role : 'admin'
|
|
113
|
+
const admin = role === 'admin'
|
|
114
|
+
const readOnly = !(role && ['admin', 'user'].includes(role))
|
|
115
|
+
const [region] = regions
|
|
116
|
+
const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
|
|
117
|
+
const currentAssemblyId = display.getAssemblyId(region.assemblyName)
|
|
118
|
+
menuItems.push(
|
|
119
|
+
{
|
|
120
|
+
label: makeFeatureLabel(sourceFeature),
|
|
121
|
+
type: 'subHeader',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: 'Add child feature',
|
|
125
|
+
disabled: readOnly,
|
|
126
|
+
onClick: () => {
|
|
127
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
128
|
+
(doneCallback) => [
|
|
129
|
+
AddChildFeature,
|
|
130
|
+
{
|
|
131
|
+
session,
|
|
132
|
+
handleClose: () => {
|
|
133
|
+
doneCallback()
|
|
134
|
+
},
|
|
135
|
+
changeManager,
|
|
136
|
+
sourceFeature,
|
|
137
|
+
sourceAssemblyId,
|
|
138
|
+
internetAccount,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: 'Copy features and annotations',
|
|
146
|
+
disabled: readOnly,
|
|
147
|
+
onClick: () => {
|
|
148
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
149
|
+
(doneCallback) => [
|
|
150
|
+
CopyFeature,
|
|
151
|
+
{
|
|
152
|
+
session,
|
|
153
|
+
handleClose: () => {
|
|
154
|
+
doneCallback()
|
|
155
|
+
},
|
|
156
|
+
changeManager,
|
|
157
|
+
sourceFeature,
|
|
158
|
+
sourceAssemblyId: currentAssemblyId,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
)
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: 'Delete feature',
|
|
166
|
+
disabled: !admin,
|
|
167
|
+
onClick: () => {
|
|
168
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
169
|
+
(doneCallback) => [
|
|
170
|
+
DeleteFeature,
|
|
171
|
+
{
|
|
172
|
+
session,
|
|
173
|
+
handleClose: () => {
|
|
174
|
+
doneCallback()
|
|
175
|
+
},
|
|
176
|
+
changeManager,
|
|
177
|
+
sourceFeature,
|
|
178
|
+
sourceAssemblyId: currentAssemblyId,
|
|
179
|
+
selectedFeature,
|
|
180
|
+
setSelectedFeature: (feature?: AnnotationFeature) => {
|
|
181
|
+
display.setSelectedFeature(feature)
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
return menuItems
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function navToFeatureCenter(
|
|
193
|
+
feature: AnnotationFeature,
|
|
194
|
+
paddingPct: number,
|
|
195
|
+
refSeqLength: number,
|
|
196
|
+
): NavLocation {
|
|
197
|
+
const paddingBp = (feature.max - feature.min) * paddingPct
|
|
198
|
+
const start = Math.max(feature.min - paddingBp, 1)
|
|
199
|
+
const end = Math.min(feature.max + paddingBp, refSeqLength)
|
|
200
|
+
return { refName: feature.refSeq, start, end }
|
|
201
|
+
}
|
package/src/util/index.ts
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
+
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
3
|
+
|
|
4
|
+
type MinEdge = 'min'
|
|
5
|
+
type MaxEdge = 'max'
|
|
6
|
+
export type Edge = MinEdge | MaxEdge
|
|
7
|
+
|
|
8
|
+
interface LocationChange {
|
|
9
|
+
featureId: string
|
|
10
|
+
oldLocation: number
|
|
11
|
+
newLocation: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function expandFeatures(
|
|
15
|
+
feature: AnnotationFeature,
|
|
16
|
+
newLocation: number,
|
|
17
|
+
edge: Edge,
|
|
18
|
+
): LocationChange[] {
|
|
19
|
+
const featureId = feature._id
|
|
20
|
+
const oldLocation = feature[edge]
|
|
21
|
+
const changes: LocationChange[] = [{ featureId, oldLocation, newLocation }]
|
|
22
|
+
const { parent } = feature
|
|
23
|
+
if (
|
|
24
|
+
parent &&
|
|
25
|
+
((edge === 'min' && parent[edge] > newLocation) ||
|
|
26
|
+
(edge === 'max' && parent[edge] < newLocation))
|
|
27
|
+
) {
|
|
28
|
+
changes.push(...expandFeatures(parent, newLocation, edge))
|
|
29
|
+
}
|
|
30
|
+
return changes
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shrinkFeatures(
|
|
34
|
+
feature: AnnotationFeature,
|
|
35
|
+
newLocation: number,
|
|
36
|
+
edge: Edge,
|
|
37
|
+
shrinkParent: boolean,
|
|
38
|
+
childIdToSkip?: string,
|
|
39
|
+
): LocationChange[] {
|
|
40
|
+
const featureId = feature._id
|
|
41
|
+
const oldLocation = feature[edge]
|
|
42
|
+
const changes: LocationChange[] = [{ featureId, oldLocation, newLocation }]
|
|
43
|
+
const { parent, children } = feature
|
|
44
|
+
if (children) {
|
|
45
|
+
for (const [, child] of children) {
|
|
46
|
+
if (child._id === childIdToSkip) {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
(edge === 'min' && child[edge] < newLocation) ||
|
|
51
|
+
(edge === 'max' && child[edge] > newLocation)
|
|
52
|
+
) {
|
|
53
|
+
changes.push(...shrinkFeatures(child, newLocation, edge, shrinkParent))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (parent && shrinkParent) {
|
|
58
|
+
const siblings: AnnotationFeature[] = []
|
|
59
|
+
if (parent.children) {
|
|
60
|
+
for (const [, c] of parent.children) {
|
|
61
|
+
if (c._id === featureId) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
siblings.push(c)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (siblings.length === 0) {
|
|
68
|
+
changes.push(
|
|
69
|
+
...shrinkFeatures(parent, newLocation, edge, shrinkParent, featureId),
|
|
70
|
+
)
|
|
71
|
+
} else {
|
|
72
|
+
const oldLocation = parent[edge]
|
|
73
|
+
const boundedLocation = Math[edge](
|
|
74
|
+
...siblings.map((s) => s[edge]),
|
|
75
|
+
newLocation,
|
|
76
|
+
)
|
|
77
|
+
if (boundedLocation !== oldLocation) {
|
|
78
|
+
changes.push(
|
|
79
|
+
...shrinkFeatures(
|
|
80
|
+
parent,
|
|
81
|
+
boundedLocation,
|
|
82
|
+
edge,
|
|
83
|
+
shrinkParent,
|
|
84
|
+
featureId,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return changes
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getPropagatedLocationChanges(
|
|
94
|
+
feature: AnnotationFeature,
|
|
95
|
+
newLocation: number,
|
|
96
|
+
edge: Edge,
|
|
97
|
+
shrinkParent = false,
|
|
98
|
+
): LocationChange[] {
|
|
99
|
+
const oldLocation = feature[edge]
|
|
100
|
+
if (newLocation === oldLocation) {
|
|
101
|
+
throw new Error(`New and existing locations are the same: "${newLocation}"`)
|
|
102
|
+
}
|
|
103
|
+
if (edge === 'min') {
|
|
104
|
+
if (newLocation > oldLocation) {
|
|
105
|
+
// shrinking feature, may need to shrink children and/or parents
|
|
106
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent)
|
|
107
|
+
}
|
|
108
|
+
return expandFeatures(feature, newLocation, edge)
|
|
109
|
+
}
|
|
110
|
+
if (newLocation < oldLocation) {
|
|
111
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent)
|
|
112
|
+
}
|
|
113
|
+
return expandFeatures(feature, newLocation, edge)
|
|
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
|
+
}
|