@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
package/dist/index.esm.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { checkRegistry, changeRegistry, Change, isAssemblySpecificChange } from '@apollo-annotation/common';
|
|
2
|
-
import { gff3ToAnnotationFeature, AddAssemblyFromExternalChange, AddAssemblyAndFeaturesFromFileChange, AddAssemblyFromFileChange, AddFeatureChange, DeleteAssemblyChange, DeleteFeatureChange, annotationFeatureToGFF3, AddFeaturesFromFileChange, UserChange, DeleteUserChange, AddRefSeqAliasesChange, getDecodedToken, makeUserSessionId, isGFFInternalAttribute, isGFFColumnInternal, internalToGFF, gffInternalToColumn, gffToInternal, gffColumnToInternal, FeatureAttributeChange, TypeChange, StrandChange,
|
|
2
|
+
import { gff3ToAnnotationFeature, AddAssemblyFromExternalChange, AddAssemblyAndFeaturesFromFileChange, AddAssemblyFromFileChange, AddFeatureChange, DeleteAssemblyChange, DeleteFeatureChange, LocationStartChange, LocationEndChange, annotationFeatureToGFF3, AddFeaturesFromFileChange, UserChange, DeleteUserChange, MergeExonsChange, MergeTranscriptsChange, AddRefSeqAliasesChange, SplitExonChange, AddAssemblyAliasesChange, getDecodedToken, makeUserSessionId, isGFFInternalAttribute, isGFFColumnInternal, internalToGFF, gffInternalToColumn, gffToInternal, gffColumnToInternal, FeatureAttributeChange, TypeChange, StrandChange, splitStringIntoChunks, validationRegistry, ValidationResultSet, filterJBrowseConfig, ImportJBrowseConfigChange, changes, CDSCheck, CoreValidation, ParentChildValidation } from '@apollo-annotation/shared';
|
|
3
3
|
import Plugin from '@jbrowse/core/Plugin';
|
|
4
4
|
import { ConfigurationSchema, readConfObject, getConf, ConfigurationReference } from '@jbrowse/core/configuration';
|
|
5
5
|
import { BaseInternetAccountConfig, InternetAccount, TextSearchAdapterType, BaseDisplay, WidgetType, createBaseTrackConfig, TrackType, createBaseTrackModel, InternetAccountType, DisplayType } from '@jbrowse/core/pluggableElementTypes';
|
|
6
|
-
import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getEnv, getSession, revcom, defaultCodonTable, isSessionModelWithWidgets, getFrame, intersection2, getContainingView, doesIntersect2 } from '@jbrowse/core/util';
|
|
6
|
+
import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getEnv, getSession, revcom, defaultCodonTable, isSessionModelWithWidgets, getFrame, intersection2, getContainingView, doesIntersect2, measureText } from '@jbrowse/core/util';
|
|
7
7
|
import AddIcon from '@mui/icons-material/Add';
|
|
8
8
|
import { autorun, entries, observable } from 'mobx';
|
|
9
|
-
import { getSnapshot, getParent, getRoot, types, addDisposer, flow,
|
|
9
|
+
import { getSnapshot, getParent, getRoot, types, addDisposer, flow, isAlive, cast, resolveIdentifier, getParentOfType, applySnapshot } from 'mobx-state-tree';
|
|
10
10
|
import { io } from 'socket.io-client';
|
|
11
11
|
import gff from '@gmod/gff';
|
|
12
12
|
import InfoIcon from '@mui/icons-material/Info';
|
|
@@ -55,7 +55,7 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
|
|
55
55
|
import ErrorIcon from '@mui/icons-material/Error';
|
|
56
56
|
import SaveIcon from '@mui/icons-material/Save';
|
|
57
57
|
|
|
58
|
-
var version = "0.3.
|
|
58
|
+
var version = "0.3.7";
|
|
59
59
|
|
|
60
60
|
const ApolloConfigSchema = ConfigurationSchema('ApolloInternetAccount', {
|
|
61
61
|
baseURL: {
|
|
@@ -135,7 +135,7 @@ async function checkFeatures(assembly) {
|
|
|
135
135
|
return checkResults;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function getFeatureName(feature) {
|
|
138
|
+
function getFeatureName$1(feature) {
|
|
139
139
|
const { attributes } = feature;
|
|
140
140
|
const name = attributes.get('gff_name');
|
|
141
141
|
if (name) {
|
|
@@ -164,7 +164,7 @@ function getFeatureId$1(feature) {
|
|
|
164
164
|
return '';
|
|
165
165
|
}
|
|
166
166
|
function getFeatureNameOrId$1(feature) {
|
|
167
|
-
const name = getFeatureName(feature);
|
|
167
|
+
const name = getFeatureName$1(feature);
|
|
168
168
|
const id = getFeatureId$1(feature);
|
|
169
169
|
if (name) {
|
|
170
170
|
return `: ${name}`;
|
|
@@ -183,6 +183,164 @@ function getStrand(strand) {
|
|
|
183
183
|
}
|
|
184
184
|
return '';
|
|
185
185
|
}
|
|
186
|
+
function getChildren(feature) {
|
|
187
|
+
const children = [];
|
|
188
|
+
//
|
|
189
|
+
if (feature.children) {
|
|
190
|
+
for (const [, ff] of feature.children) {
|
|
191
|
+
children.push(ff);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return children;
|
|
195
|
+
}
|
|
196
|
+
function getParents(feature) {
|
|
197
|
+
const parents = [];
|
|
198
|
+
let { parent } = feature;
|
|
199
|
+
while (parent) {
|
|
200
|
+
parents.push(parent);
|
|
201
|
+
({ parent } = parent);
|
|
202
|
+
}
|
|
203
|
+
return parents;
|
|
204
|
+
}
|
|
205
|
+
function getFeaturesUnderClick(mousePosition, includeSiblings = false) {
|
|
206
|
+
const clickedFeatures = [];
|
|
207
|
+
if (!mousePosition.featureAndGlyphUnderMouse) {
|
|
208
|
+
return clickedFeatures;
|
|
209
|
+
}
|
|
210
|
+
clickedFeatures.push(mousePosition.featureAndGlyphUnderMouse.feature);
|
|
211
|
+
for (const x of getParents(mousePosition.featureAndGlyphUnderMouse.feature)) {
|
|
212
|
+
clickedFeatures.push(x);
|
|
213
|
+
}
|
|
214
|
+
const { bp } = mousePosition;
|
|
215
|
+
const children = getChildren(mousePosition.featureAndGlyphUnderMouse.feature);
|
|
216
|
+
for (const child of children) {
|
|
217
|
+
if (child.min < bp && child.max >= bp) {
|
|
218
|
+
clickedFeatures.push(child);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!includeSiblings) {
|
|
222
|
+
return clickedFeatures;
|
|
223
|
+
}
|
|
224
|
+
// Also add siblings , i.e. features having the same parent as the clicked
|
|
225
|
+
// one and intersecting the click position
|
|
226
|
+
if (mousePosition.featureAndGlyphUnderMouse.feature.parent) {
|
|
227
|
+
const siblings = mousePosition.featureAndGlyphUnderMouse.feature.parent.children;
|
|
228
|
+
if (siblings) {
|
|
229
|
+
for (const [, sib] of siblings) {
|
|
230
|
+
if (sib._id == mousePosition.featureAndGlyphUnderMouse.feature._id) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (sib.min < bp && sib.max >= bp) {
|
|
234
|
+
clickedFeatures.push(sib);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return clickedFeatures;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getMinAndMaxPx(feature, refName, regionNumber, lgv) {
|
|
243
|
+
const minPxInfo = lgv.bpToPx({
|
|
244
|
+
refName,
|
|
245
|
+
coord: feature.min,
|
|
246
|
+
regionNumber,
|
|
247
|
+
});
|
|
248
|
+
const maxPxInfo = lgv.bpToPx({
|
|
249
|
+
refName,
|
|
250
|
+
coord: feature.max,
|
|
251
|
+
regionNumber,
|
|
252
|
+
});
|
|
253
|
+
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const { offsetPx } = lgv;
|
|
257
|
+
const minPx = minPxInfo.offsetPx - offsetPx;
|
|
258
|
+
const maxPx = maxPxInfo.offsetPx - offsetPx;
|
|
259
|
+
return [minPx, maxPx];
|
|
260
|
+
}
|
|
261
|
+
function getOverlappingEdge(feature, x, minMax) {
|
|
262
|
+
const [minPx, maxPx] = minMax;
|
|
263
|
+
// Feature is too small to tell if we're overlapping an edge
|
|
264
|
+
if (Math.abs(maxPx - minPx) < 8) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (Math.abs(minPx - x) < 4) {
|
|
268
|
+
return { feature, edge: 'min' };
|
|
269
|
+
}
|
|
270
|
+
if (Math.abs(maxPx - x) < 4) {
|
|
271
|
+
return { feature, edge: 'max' };
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function expandFeatures(feature, newLocation, edge) {
|
|
277
|
+
const featureId = feature._id;
|
|
278
|
+
const oldLocation = feature[edge];
|
|
279
|
+
const changes = [{ featureId, oldLocation, newLocation }];
|
|
280
|
+
const { parent } = feature;
|
|
281
|
+
if (parent &&
|
|
282
|
+
((edge === 'min' && parent[edge] > newLocation) ||
|
|
283
|
+
(edge === 'max' && parent[edge] < newLocation))) {
|
|
284
|
+
changes.push(...expandFeatures(parent, newLocation, edge));
|
|
285
|
+
}
|
|
286
|
+
return changes;
|
|
287
|
+
}
|
|
288
|
+
function shrinkFeatures(feature, newLocation, edge, shrinkParent, childIdToSkip) {
|
|
289
|
+
const featureId = feature._id;
|
|
290
|
+
const oldLocation = feature[edge];
|
|
291
|
+
const changes = [{ featureId, oldLocation, newLocation }];
|
|
292
|
+
const { parent, children } = feature;
|
|
293
|
+
if (children) {
|
|
294
|
+
for (const [, child] of children) {
|
|
295
|
+
if (child._id === childIdToSkip) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if ((edge === 'min' && child[edge] < newLocation) ||
|
|
299
|
+
(edge === 'max' && child[edge] > newLocation)) {
|
|
300
|
+
changes.push(...shrinkFeatures(child, newLocation, edge, shrinkParent));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (parent && shrinkParent) {
|
|
305
|
+
const siblings = [];
|
|
306
|
+
if (parent.children) {
|
|
307
|
+
for (const [, c] of parent.children) {
|
|
308
|
+
if (c._id === featureId) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
siblings.push(c);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (siblings.length === 0) {
|
|
315
|
+
changes.push(...shrinkFeatures(parent, newLocation, edge, shrinkParent, featureId));
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const oldLocation = parent[edge];
|
|
319
|
+
const boundedLocation = Math[edge](...siblings.map((s) => s[edge]), newLocation);
|
|
320
|
+
if (boundedLocation !== oldLocation) {
|
|
321
|
+
changes.push(...shrinkFeatures(parent, boundedLocation, edge, shrinkParent, featureId));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return changes;
|
|
326
|
+
}
|
|
327
|
+
function getPropagatedLocationChanges(feature, newLocation, edge, shrinkParent = false) {
|
|
328
|
+
const oldLocation = feature[edge];
|
|
329
|
+
if (newLocation === oldLocation) {
|
|
330
|
+
throw new Error(`New and existing locations are the same: "${newLocation}"`);
|
|
331
|
+
}
|
|
332
|
+
if (edge === 'min') {
|
|
333
|
+
if (newLocation > oldLocation) {
|
|
334
|
+
// shrinking feature, may need to shrink children and/or parents
|
|
335
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent);
|
|
336
|
+
}
|
|
337
|
+
return expandFeatures(feature, newLocation, edge);
|
|
338
|
+
}
|
|
339
|
+
if (newLocation < oldLocation) {
|
|
340
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent);
|
|
341
|
+
}
|
|
342
|
+
return expandFeatures(feature, newLocation, edge);
|
|
343
|
+
}
|
|
186
344
|
|
|
187
345
|
async function createFetchErrorMessage(response, additionalText) {
|
|
188
346
|
let errorMessage;
|
|
@@ -1501,7 +1659,9 @@ const OntologyRecordType = types
|
|
|
1501
1659
|
const equivalents = terms
|
|
1502
1660
|
.map((term) => term.lbl)
|
|
1503
1661
|
.filter((term) => term != undefined);
|
|
1504
|
-
self
|
|
1662
|
+
if (isAlive(self)) {
|
|
1663
|
+
self.setEquivalentTypes(type, equivalents);
|
|
1664
|
+
}
|
|
1505
1665
|
}),
|
|
1506
1666
|
}))
|
|
1507
1667
|
.actions((self) => ({
|
|
@@ -1782,8 +1942,8 @@ async function getValidTerms(ontologyStore, fetchValidTerms, filterTerms, signal
|
|
|
1782
1942
|
return filterTerms ? result.filter((element) => filterTerms(element)) : result;
|
|
1783
1943
|
}
|
|
1784
1944
|
|
|
1945
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1785
1946
|
function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId, sourceFeature, }) {
|
|
1786
|
-
const { notify } = session;
|
|
1787
1947
|
const [end, setEnd] = useState(String(sourceFeature.max));
|
|
1788
1948
|
const [start, setStart] = useState(String(sourceFeature.min + 1));
|
|
1789
1949
|
const [type, setType] = useState('');
|
|
@@ -1797,7 +1957,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
|
|
|
1797
1957
|
}
|
|
1798
1958
|
return terms;
|
|
1799
1959
|
}
|
|
1800
|
-
|
|
1960
|
+
function onSubmit(event) {
|
|
1801
1961
|
event.preventDefault();
|
|
1802
1962
|
setErrorMessage('');
|
|
1803
1963
|
const change = new AddFeatureChange({
|
|
@@ -1813,8 +1973,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
|
|
|
1813
1973
|
},
|
|
1814
1974
|
parentFeatureId: sourceFeature._id,
|
|
1815
1975
|
});
|
|
1816
|
-
|
|
1817
|
-
notify('Feature added successfully', 'success');
|
|
1976
|
+
void changeManager.submit(change);
|
|
1818
1977
|
handleClose();
|
|
1819
1978
|
event.preventDefault();
|
|
1820
1979
|
}
|
|
@@ -1844,6 +2003,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
|
|
|
1844
2003
|
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
1845
2004
|
}
|
|
1846
2005
|
|
|
2006
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1847
2007
|
var NewFeature;
|
|
1848
2008
|
(function (NewFeature) {
|
|
1849
2009
|
NewFeature["GENE_AND_SUBFEATURES"] = "GENE_AND_SUBFEATURES";
|
|
@@ -1882,14 +2042,13 @@ function makeCodingMrna(refSeqId, strand, min, max) {
|
|
|
1882
2042
|
return mRNA;
|
|
1883
2043
|
}
|
|
1884
2044
|
function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
1885
|
-
const { notify } = session;
|
|
1886
2045
|
const [end, setEnd] = useState(String(region.end));
|
|
1887
2046
|
const [start, setStart] = useState(String(region.start + 1));
|
|
1888
2047
|
const [type, setType] = useState(NewFeature.GENE_AND_SUBFEATURES);
|
|
1889
2048
|
const [customType, setCustomType] = useState();
|
|
1890
2049
|
const [strand, setStrand] = useState();
|
|
1891
2050
|
const [errorMessage, setErrorMessage] = useState('');
|
|
1892
|
-
|
|
2051
|
+
function onSubmit(event) {
|
|
1893
2052
|
event.preventDefault();
|
|
1894
2053
|
setErrorMessage('');
|
|
1895
2054
|
let refSeqId;
|
|
@@ -1903,7 +2062,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1903
2062
|
}
|
|
1904
2063
|
}
|
|
1905
2064
|
if (!refSeqId) {
|
|
1906
|
-
setErrorMessage('Invalid refseq id');
|
|
2065
|
+
setErrorMessage('Invalid refseq id. Make sure you have the Apollo annotation track open');
|
|
1907
2066
|
return;
|
|
1908
2067
|
}
|
|
1909
2068
|
if (type === NewFeature.GENE_AND_SUBFEATURES) {
|
|
@@ -1925,8 +2084,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1925
2084
|
children,
|
|
1926
2085
|
},
|
|
1927
2086
|
});
|
|
1928
|
-
|
|
1929
|
-
notify('Feature added successfully', 'success');
|
|
2087
|
+
void changeManager.submit(change);
|
|
1930
2088
|
handleClose();
|
|
1931
2089
|
return;
|
|
1932
2090
|
}
|
|
@@ -1938,8 +2096,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1938
2096
|
assembly: region.assemblyName,
|
|
1939
2097
|
addedFeature: mRNA,
|
|
1940
2098
|
});
|
|
1941
|
-
|
|
1942
|
-
notify('Feature added successfully', 'success');
|
|
2099
|
+
void changeManager.submit(change);
|
|
1943
2100
|
handleClose();
|
|
1944
2101
|
return;
|
|
1945
2102
|
}
|
|
@@ -1961,8 +2118,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1961
2118
|
strand,
|
|
1962
2119
|
},
|
|
1963
2120
|
});
|
|
1964
|
-
|
|
1965
|
-
notify('Feature added successfully', 'success');
|
|
2121
|
+
void changeManager.submit(change);
|
|
1966
2122
|
handleClose();
|
|
1967
2123
|
return;
|
|
1968
2124
|
}
|
|
@@ -1995,7 +2151,9 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1995
2151
|
}
|
|
1996
2152
|
};
|
|
1997
2153
|
let submitDisabled = Boolean(error) || !(start && end && type);
|
|
1998
|
-
if (type === NewFeature.CUSTOM && !customType)
|
|
2154
|
+
if ((type === NewFeature.CUSTOM && !customType) ||
|
|
2155
|
+
(!strand && type === NewFeature.GENE_AND_SUBFEATURES) ||
|
|
2156
|
+
(!strand && type === NewFeature.TRANSCRIPT_AND_SUBFEATURES)) {
|
|
1999
2157
|
submitDisabled = true;
|
|
2000
2158
|
}
|
|
2001
2159
|
return (React.createElement(Dialog, { open: true, title: "Add new feature", handleClose: handleClose, maxWidth: false, "data-testid": "add-feature-dialog" },
|
|
@@ -2067,7 +2225,7 @@ feature, featureIds) {
|
|
|
2067
2225
|
};
|
|
2068
2226
|
}
|
|
2069
2227
|
function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, sourceFeature, }) {
|
|
2070
|
-
const { assemblyManager
|
|
2228
|
+
const { assemblyManager } = session;
|
|
2071
2229
|
const assemblies = assemblyManager.assemblyList;
|
|
2072
2230
|
const [selectedAssemblyId, setSelectedAssemblyId] = useState(assemblies.find((a) => a.name !== sourceAssemblyId)?.name);
|
|
2073
2231
|
const [refNames, setRefNames] = useState([]);
|
|
@@ -2166,8 +2324,7 @@ function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, so
|
|
|
2166
2324
|
copyFeature: true,
|
|
2167
2325
|
allIds: featureIds,
|
|
2168
2326
|
});
|
|
2169
|
-
|
|
2170
|
-
notify('Feature copied successfully', 'success');
|
|
2327
|
+
void changeManager.submit(change);
|
|
2171
2328
|
handleClose();
|
|
2172
2329
|
event.preventDefault();
|
|
2173
2330
|
}
|
|
@@ -2291,30 +2448,318 @@ function DeleteAssembly({ changeManager, handleClose, session, }) {
|
|
|
2291
2448
|
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
2292
2449
|
}
|
|
2293
2450
|
|
|
2451
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
2452
|
+
function lumpLocationChanges(changes, assembly) {
|
|
2453
|
+
if (changes.length === 0) {
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const locationStartChange = new LocationStartChange({
|
|
2457
|
+
typeName: 'LocationStartChange',
|
|
2458
|
+
changedIds: [],
|
|
2459
|
+
changes: [],
|
|
2460
|
+
assembly,
|
|
2461
|
+
});
|
|
2462
|
+
const locationEndChange = new LocationEndChange({
|
|
2463
|
+
typeName: 'LocationEndChange',
|
|
2464
|
+
changedIds: [],
|
|
2465
|
+
changes: [],
|
|
2466
|
+
assembly,
|
|
2467
|
+
});
|
|
2468
|
+
for (const change of changes) {
|
|
2469
|
+
if (change.typeName === 'LocationStartChange') {
|
|
2470
|
+
locationStartChange.changedIds.push(change.changedId);
|
|
2471
|
+
const cc = {
|
|
2472
|
+
featureId: change.featureId,
|
|
2473
|
+
oldStart: change.oldLocation,
|
|
2474
|
+
newStart: change.newLocation,
|
|
2475
|
+
};
|
|
2476
|
+
locationStartChange.changes.push(cc);
|
|
2477
|
+
}
|
|
2478
|
+
if (change.typeName === 'LocationEndChange') {
|
|
2479
|
+
locationEndChange.changedIds.push(change.changedId);
|
|
2480
|
+
const cc = {
|
|
2481
|
+
featureId: change.featureId,
|
|
2482
|
+
oldEnd: change.oldLocation,
|
|
2483
|
+
newEnd: change.newLocation,
|
|
2484
|
+
};
|
|
2485
|
+
locationEndChange.changes.push(cc);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (locationStartChange.changedIds.length > 0 &&
|
|
2489
|
+
locationEndChange.changedIds.length === 0) {
|
|
2490
|
+
return locationStartChange;
|
|
2491
|
+
}
|
|
2492
|
+
if (locationEndChange.changedIds.length > 0 &&
|
|
2493
|
+
locationStartChange.changedIds.length === 0) {
|
|
2494
|
+
return locationEndChange;
|
|
2495
|
+
}
|
|
2496
|
+
throw new Error('Unexpected list of changes');
|
|
2497
|
+
}
|
|
2294
2498
|
function DeleteFeature({ changeManager, handleClose, selectedFeature, session, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
2295
|
-
const { notify } = session;
|
|
2296
2499
|
const [errorMessage, setErrorMessage] = useState('');
|
|
2500
|
+
const { ontologyManager } = session.apolloDataStore;
|
|
2501
|
+
const { featureTypeOntology } = ontologyManager;
|
|
2502
|
+
function trimCDS(sourceFeature) {
|
|
2503
|
+
if (!featureTypeOntology) {
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
if (!featureTypeOntology.isTypeOf(sourceFeature.type, 'exon')) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
if (!sourceFeature.parent?.cdsLocations ||
|
|
2510
|
+
sourceFeature.parent.cdsLocations.length === 0 ||
|
|
2511
|
+
sourceFeature.parent.cdsLocations[0].length === 0) {
|
|
2512
|
+
// No CDS - parent of this exon is a non-coding transcript
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (!sourceFeature.parent.children) {
|
|
2516
|
+
throw new Error('Unable to find parent of CDS');
|
|
2517
|
+
}
|
|
2518
|
+
if (sourceFeature.parent.cdsLocations.length != 1) {
|
|
2519
|
+
throw new Error('Unable to handle a transcript with multiple CDSs');
|
|
2520
|
+
}
|
|
2521
|
+
const _cdsLocations = sourceFeature.parent.cdsLocations.at(0) ?? [];
|
|
2522
|
+
const cdsLocations = _cdsLocations.sort(({ min: a }, { min: b }) => a - b);
|
|
2523
|
+
let cdsFeature;
|
|
2524
|
+
for (const child of sourceFeature.parent.children.values()) {
|
|
2525
|
+
if (child.type === cdsLocations[0].type) {
|
|
2526
|
+
cdsFeature = child;
|
|
2527
|
+
break;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (!cdsFeature) {
|
|
2531
|
+
throw new Error('Unable to find CDS');
|
|
2532
|
+
}
|
|
2533
|
+
const cdsStart = cdsLocations[0].min;
|
|
2534
|
+
// eslint-disable-next-line unicorn/prefer-at
|
|
2535
|
+
const cdsEnd = cdsLocations[cdsLocations.length - 1].max;
|
|
2536
|
+
if ((sourceFeature.min > cdsStart && sourceFeature.max < cdsEnd) ||
|
|
2537
|
+
sourceFeature.max < cdsStart ||
|
|
2538
|
+
sourceFeature.min > cdsEnd) {
|
|
2539
|
+
// No adjustment if the exon being deleted is fully contained in the CDS
|
|
2540
|
+
// or completely outside of the CDS
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (sourceFeature.min <= cdsStart && sourceFeature.max >= cdsEnd) {
|
|
2544
|
+
// CDS is fully contained in the exon, delete CDS
|
|
2545
|
+
return new DeleteFeatureChange({
|
|
2546
|
+
changedIds: [cdsFeature._id],
|
|
2547
|
+
typeName: 'DeleteFeatureChange',
|
|
2548
|
+
assembly: sourceAssemblyId,
|
|
2549
|
+
changes: [
|
|
2550
|
+
{
|
|
2551
|
+
deletedFeature: getSnapshot(cdsFeature),
|
|
2552
|
+
parentFeatureId: cdsFeature.parent?._id,
|
|
2553
|
+
},
|
|
2554
|
+
],
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
if (sourceFeature.min <= cdsStart && sourceFeature.max > cdsStart) {
|
|
2558
|
+
// Exon overlaps the start of the CDS so we need to move the CDS start
|
|
2559
|
+
let newCdsStart;
|
|
2560
|
+
for (const cdsLocation of cdsLocations) {
|
|
2561
|
+
if (cdsLocation.min > sourceFeature.max) {
|
|
2562
|
+
newCdsStart = cdsLocation.min;
|
|
2563
|
+
break;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (!newCdsStart) {
|
|
2567
|
+
throw new Error('Error setting new CDS start');
|
|
2568
|
+
}
|
|
2569
|
+
return {
|
|
2570
|
+
typeName: 'LocationStartChange',
|
|
2571
|
+
changedId: cdsFeature._id,
|
|
2572
|
+
featureId: cdsFeature._id,
|
|
2573
|
+
oldLocation: cdsFeature.min,
|
|
2574
|
+
newLocation: newCdsStart,
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
if (sourceFeature.min < cdsEnd && sourceFeature.max >= cdsEnd) {
|
|
2578
|
+
// Exon overlaps the end of the CDS so we need to move the CDS end
|
|
2579
|
+
let newCdsEnd;
|
|
2580
|
+
for (const cdsLocation of cdsLocations.reverse()) {
|
|
2581
|
+
if (cdsLocation.max < sourceFeature.min) {
|
|
2582
|
+
newCdsEnd = cdsLocation.max;
|
|
2583
|
+
break;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (!newCdsEnd) {
|
|
2587
|
+
throw new Error('Error setting new CDS end');
|
|
2588
|
+
}
|
|
2589
|
+
return {
|
|
2590
|
+
typeName: 'LocationEndChange',
|
|
2591
|
+
changedId: cdsFeature._id,
|
|
2592
|
+
featureId: cdsFeature._id,
|
|
2593
|
+
oldLocation: cdsFeature.max,
|
|
2594
|
+
newLocation: newCdsEnd,
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
throw new Error('Unexpected relationship between exon and CDS');
|
|
2598
|
+
}
|
|
2599
|
+
function trimParent(featureToDelete) {
|
|
2600
|
+
if (!featureToDelete.parent?.children ||
|
|
2601
|
+
featureToDelete.parent.children.size === 1) {
|
|
2602
|
+
// Do not resize if this parent has only one child (i.e. the feature being deleted)
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
const childrenByStart = [];
|
|
2606
|
+
for (const x of featureToDelete.parent.children.values()) {
|
|
2607
|
+
if (!featureTypeOntology?.isTypeOf(x.type, 'CDS')) {
|
|
2608
|
+
// CDS has been already handled so don't use it to resize parent
|
|
2609
|
+
childrenByStart.push(x);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
childrenByStart.sort((a, b) => a.min - b.min);
|
|
2613
|
+
const childrenByEnd = [];
|
|
2614
|
+
for (const x of featureToDelete.parent.children.values()) {
|
|
2615
|
+
if (!featureTypeOntology?.isTypeOf(x.type, 'CDS')) {
|
|
2616
|
+
// CDS has been already handled so don't use it to resize parent
|
|
2617
|
+
childrenByEnd.push(x);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
childrenByEnd.sort((a, b) => b.max - a.max);
|
|
2621
|
+
if (featureToDelete.min === childrenByStart[0].min) {
|
|
2622
|
+
// The feature to delete has the lowest start coordinate of all children
|
|
2623
|
+
// Find the next lowest coordinate and reset parent to this new start
|
|
2624
|
+
let newParentFeatureStart;
|
|
2625
|
+
for (const child of childrenByStart) {
|
|
2626
|
+
if (child._id !== featureToDelete._id &&
|
|
2627
|
+
child.min >= featureToDelete.min) {
|
|
2628
|
+
newParentFeatureStart = child.min;
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
if (newParentFeatureStart &&
|
|
2633
|
+
newParentFeatureStart != featureToDelete.parent.min) {
|
|
2634
|
+
return {
|
|
2635
|
+
typeName: 'LocationStartChange',
|
|
2636
|
+
changedId: featureToDelete.parent._id,
|
|
2637
|
+
featureId: featureToDelete.parent._id,
|
|
2638
|
+
oldLocation: featureToDelete.parent.min,
|
|
2639
|
+
newLocation: newParentFeatureStart,
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
if (featureToDelete.max === childrenByEnd[0].max) {
|
|
2644
|
+
// The feature to delete has the highest end coordinate of all children
|
|
2645
|
+
// Find the next highest coordinate and reset parent to this new end
|
|
2646
|
+
let newParentFeatureEnd;
|
|
2647
|
+
for (const child of childrenByEnd) {
|
|
2648
|
+
if (child._id != featureToDelete._id &&
|
|
2649
|
+
child.max <= featureToDelete.max) {
|
|
2650
|
+
newParentFeatureEnd = child.max;
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (newParentFeatureEnd &&
|
|
2655
|
+
newParentFeatureEnd != featureToDelete.parent.max) {
|
|
2656
|
+
return {
|
|
2657
|
+
typeName: 'LocationEndChange',
|
|
2658
|
+
changedId: featureToDelete.parent._id,
|
|
2659
|
+
featureId: featureToDelete.parent._id,
|
|
2660
|
+
oldLocation: featureToDelete.parent.max,
|
|
2661
|
+
newLocation: newParentFeatureEnd,
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2297
2667
|
async function onSubmit(event) {
|
|
2298
2668
|
event.preventDefault();
|
|
2299
2669
|
setErrorMessage('');
|
|
2300
2670
|
if (selectedFeature?._id === sourceFeature._id) {
|
|
2301
2671
|
setSelectedFeature();
|
|
2302
2672
|
}
|
|
2303
|
-
|
|
2304
|
-
const
|
|
2673
|
+
const locationChanges = [];
|
|
2674
|
+
// const deleteChanges: DeleteFeatureChange = []
|
|
2675
|
+
const deleteChanges = new DeleteFeatureChange({
|
|
2305
2676
|
changedIds: [sourceFeature._id],
|
|
2306
2677
|
typeName: 'DeleteFeatureChange',
|
|
2307
2678
|
assembly: sourceAssemblyId,
|
|
2308
|
-
|
|
2309
|
-
|
|
2679
|
+
changes: [
|
|
2680
|
+
{
|
|
2681
|
+
deletedFeature: getSnapshot(sourceFeature),
|
|
2682
|
+
parentFeatureId: sourceFeature.parent?._id,
|
|
2683
|
+
},
|
|
2684
|
+
],
|
|
2310
2685
|
});
|
|
2311
|
-
|
|
2312
|
-
|
|
2686
|
+
if (featureTypeOntology &&
|
|
2687
|
+
(featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
|
|
2688
|
+
featureTypeOntology.isTypeOf(sourceFeature.type, 'pseudogenic_transcript'))) {
|
|
2689
|
+
const geneChange = trimParent(sourceFeature);
|
|
2690
|
+
if (geneChange) {
|
|
2691
|
+
locationChanges.push(geneChange);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
if (featureTypeOntology &&
|
|
2695
|
+
featureTypeOntology.isTypeOf(sourceFeature.type, 'exon')) {
|
|
2696
|
+
const cdsChange = trimCDS(sourceFeature);
|
|
2697
|
+
if (cdsChange) {
|
|
2698
|
+
if (cdsChange.typeName === 'DeleteFeatureChange') {
|
|
2699
|
+
deleteChanges.changedIds.push(...cdsChange.changedIds);
|
|
2700
|
+
deleteChanges.changes.push(...cdsChange.changes);
|
|
2701
|
+
}
|
|
2702
|
+
else {
|
|
2703
|
+
locationChanges.push(cdsChange);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const txChange = trimParent(sourceFeature);
|
|
2707
|
+
if (txChange) {
|
|
2708
|
+
locationChanges.push(txChange);
|
|
2709
|
+
// Parent transcript has changed. See if we need to resize the parent gene
|
|
2710
|
+
const gene = sourceFeature.parent?.parent;
|
|
2711
|
+
if (gene?.children) {
|
|
2712
|
+
if (txChange.typeName === 'LocationStartChange') {
|
|
2713
|
+
let newGeneStart = txChange.newLocation;
|
|
2714
|
+
for (const [, tx] of gene.children) {
|
|
2715
|
+
if (tx._id != txChange.featureId && tx.min < newGeneStart) {
|
|
2716
|
+
// Reset to longest child (tx)
|
|
2717
|
+
newGeneStart = tx.min;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (newGeneStart != gene.min) {
|
|
2721
|
+
locationChanges.push({
|
|
2722
|
+
typeName: txChange.typeName,
|
|
2723
|
+
changedId: gene._id,
|
|
2724
|
+
featureId: gene._id,
|
|
2725
|
+
oldLocation: gene.min,
|
|
2726
|
+
newLocation: newGeneStart,
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
else {
|
|
2731
|
+
let newGeneEnd = txChange.newLocation;
|
|
2732
|
+
for (const [, tx] of gene.children) {
|
|
2733
|
+
if (tx._id != txChange.featureId && tx.max > newGeneEnd) {
|
|
2734
|
+
// Reset to longest child (tx)
|
|
2735
|
+
newGeneEnd = tx.max;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
if (newGeneEnd != gene.max) {
|
|
2739
|
+
locationChanges.push({
|
|
2740
|
+
typeName: txChange.typeName,
|
|
2741
|
+
changedId: gene._id,
|
|
2742
|
+
featureId: gene._id,
|
|
2743
|
+
oldLocation: gene.max,
|
|
2744
|
+
newLocation: newGeneEnd,
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
const lumpedLocChanges = lumpLocationChanges(locationChanges, sourceAssemblyId);
|
|
2752
|
+
await changeManager.submit(deleteChanges);
|
|
2753
|
+
if (lumpedLocChanges) {
|
|
2754
|
+
await changeManager.submit(lumpedLocChanges);
|
|
2755
|
+
}
|
|
2313
2756
|
handleClose();
|
|
2314
2757
|
event.preventDefault();
|
|
2315
2758
|
}
|
|
2316
2759
|
return (React.createElement(Dialog, { open: true, title: "Delete feature", handleClose: handleClose, maxWidth: false, "data-testid": "delete-feature" },
|
|
2317
|
-
React.createElement("form", { onSubmit:
|
|
2760
|
+
React.createElement("form", { onSubmit: (event) => {
|
|
2761
|
+
void onSubmit(event);
|
|
2762
|
+
} },
|
|
2318
2763
|
React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
2319
2764
|
React.createElement(DialogContentText, null, "Are you sure you want to delete the selected feature?")),
|
|
2320
2765
|
React.createElement(DialogActions, null,
|
|
@@ -2325,6 +2770,7 @@ function DeleteFeature({ changeManager, handleClose, selectedFeature, session, s
|
|
|
2325
2770
|
}
|
|
2326
2771
|
|
|
2327
2772
|
function DownloadGFF3({ handleClose, session }) {
|
|
2773
|
+
const [includeFASTA, setincludeFASTA] = useState(false);
|
|
2328
2774
|
const [selectedAssembly, setSelectedAssembly] = useState();
|
|
2329
2775
|
const [errorMessage, setErrorMessage] = useState('');
|
|
2330
2776
|
const { collaborationServerDriver, getInternetAccount, inMemoryFileDriver } = session.apolloDataStore;
|
|
@@ -2381,7 +2827,7 @@ function DownloadGFF3({ handleClose, session }) {
|
|
|
2381
2827
|
const exportURL = new URL('export', internetAccount.baseURL);
|
|
2382
2828
|
const params = {
|
|
2383
2829
|
exportID,
|
|
2384
|
-
includeFASTA: 'true',
|
|
2830
|
+
includeFASTA: includeFASTA ? 'true' : 'false',
|
|
2385
2831
|
};
|
|
2386
2832
|
const exportSearchParams = new URLSearchParams(params);
|
|
2387
2833
|
exportURL.search = exportSearchParams.toString();
|
|
@@ -2435,7 +2881,11 @@ function DownloadGFF3({ handleClose, session }) {
|
|
|
2435
2881
|
React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
2436
2882
|
React.createElement(DialogContentText, null, "Select assembly"),
|
|
2437
2883
|
React.createElement(Select, { labelId: "label", value: selectedAssembly?.name ?? '', onChange: handleChangeAssembly, disabled: assemblies.length === 0 }, assemblies.map((option) => (React.createElement(MenuItem, { key: option.name, value: option.name }, option.displayName ?? option.name)))),
|
|
2438
|
-
React.createElement(DialogContentText, null, "Select assembly to export to GFF3")
|
|
2884
|
+
React.createElement(DialogContentText, null, "Select assembly to export to GFF3"),
|
|
2885
|
+
React.createElement(FormGroup, null,
|
|
2886
|
+
React.createElement(FormControlLabel, { "data-testid": "include-fasta-checkbox", control: React.createElement(Checkbox, { checked: includeFASTA, onChange: () => {
|
|
2887
|
+
setincludeFASTA(!includeFASTA);
|
|
2888
|
+
} }), label: "Include fasta sequence in GFF output" }))),
|
|
2439
2889
|
React.createElement(DialogActions, null,
|
|
2440
2890
|
React.createElement(Button, { disabled: !selectedAssembly, variant: "contained", type: "submit" }, "Download"),
|
|
2441
2891
|
React.createElement(Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
@@ -2914,70 +3364,254 @@ function ManageUsers({ changeManager, handleClose, session, }) {
|
|
|
2914
3364
|
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
2915
3365
|
}
|
|
2916
3366
|
|
|
2917
|
-
/* eslint-disable @typescript-eslint/
|
|
2918
|
-
function
|
|
2919
|
-
const
|
|
2920
|
-
const
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3367
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
3368
|
+
function getNeighboringExons(referenceExon) {
|
|
3369
|
+
const neighboringExons = {};
|
|
3370
|
+
const tx = referenceExon.parent;
|
|
3371
|
+
if (!tx) {
|
|
3372
|
+
throw new Error('Unable to find parent of reference exon');
|
|
3373
|
+
}
|
|
3374
|
+
let exons = [];
|
|
3375
|
+
if (tx.children) {
|
|
3376
|
+
for (const [, feature] of tx.children) {
|
|
3377
|
+
if (feature.type === 'exon') {
|
|
3378
|
+
exons.push(feature);
|
|
3379
|
+
}
|
|
2930
3380
|
}
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
if (
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3381
|
+
}
|
|
3382
|
+
exons = exons.sort((a, b) => {
|
|
3383
|
+
if (a.min === b.min) {
|
|
3384
|
+
return a.max - b.max;
|
|
3385
|
+
}
|
|
3386
|
+
return a.min - b.min;
|
|
3387
|
+
});
|
|
3388
|
+
if (tx.strand && tx.strand === -1) {
|
|
3389
|
+
exons = exons.reverse();
|
|
3390
|
+
}
|
|
3391
|
+
let i = 0;
|
|
3392
|
+
for (const x of exons) {
|
|
3393
|
+
if (x._id === referenceExon._id) {
|
|
3394
|
+
if (exons.length > i + 1) {
|
|
3395
|
+
neighboringExons.three_prime = exons[i + 1];
|
|
2938
3396
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
3397
|
+
if (i > 0) {
|
|
3398
|
+
neighboringExons.five_prime = exons[i - 1];
|
|
2941
3399
|
}
|
|
3400
|
+
break;
|
|
2942
3401
|
}
|
|
3402
|
+
i++;
|
|
2943
3403
|
}
|
|
2944
|
-
|
|
3404
|
+
return neighboringExons;
|
|
3405
|
+
}
|
|
3406
|
+
function makeRadioButtonName$1(key, neighboringExons) {
|
|
3407
|
+
const neighboringExon = neighboringExons[key];
|
|
3408
|
+
let name;
|
|
3409
|
+
if (key === 'three_prime') {
|
|
3410
|
+
name = `3'end (coords: ${neighboringExon.min + 1}-${neighboringExon.max})`;
|
|
3411
|
+
}
|
|
3412
|
+
else if (key === 'five_prime') {
|
|
3413
|
+
name = `5'end (coords: ${neighboringExon.min + 1}-${neighboringExon.max})`;
|
|
3414
|
+
}
|
|
3415
|
+
else {
|
|
3416
|
+
throw new Error(`Unexpected direction: "${key}"`);
|
|
3417
|
+
}
|
|
3418
|
+
return name;
|
|
3419
|
+
}
|
|
3420
|
+
function MergeExons({ changeManager, handleClose, selectedFeature, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
3421
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
3422
|
+
const [selectedExon, setSelectedExon] = useState();
|
|
3423
|
+
function onSubmit(event) {
|
|
2945
3424
|
event.preventDefault();
|
|
2946
3425
|
setErrorMessage('');
|
|
2947
|
-
|
|
2948
|
-
if (!
|
|
2949
|
-
throw new Error('No file selected');
|
|
2950
|
-
}
|
|
2951
|
-
// Right now we are not using stream because there was a problem with 'pipe' in ReadStream
|
|
2952
|
-
const fileData = await new Response(file).text();
|
|
2953
|
-
const assemblyId = `${assemblyName}-${file.name}-${nanoid(8)}`;
|
|
2954
|
-
try {
|
|
2955
|
-
await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore);
|
|
2956
|
-
}
|
|
2957
|
-
catch (error) {
|
|
2958
|
-
console.error(error);
|
|
2959
|
-
notify(`Error loading GFF3 ${file.name}, ${String(error)}`, 'error');
|
|
2960
|
-
handleClose();
|
|
3426
|
+
const { parent } = sourceFeature;
|
|
3427
|
+
if (!(selectedExon && parent)) {
|
|
2961
3428
|
return;
|
|
2962
3429
|
}
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3430
|
+
if (selectedFeature?._id === sourceFeature._id) {
|
|
3431
|
+
setSelectedFeature();
|
|
3432
|
+
}
|
|
3433
|
+
const change = new MergeExonsChange({
|
|
3434
|
+
changedIds: [sourceFeature._id],
|
|
3435
|
+
typeName: 'MergeExonsChange',
|
|
3436
|
+
assembly: sourceAssemblyId,
|
|
3437
|
+
firstExon: getSnapshot(sourceFeature),
|
|
3438
|
+
secondExon: getSnapshot(selectedExon),
|
|
3439
|
+
parentFeatureId: parent._id,
|
|
3440
|
+
});
|
|
3441
|
+
void changeManager.submit(change);
|
|
3442
|
+
handleClose();
|
|
3443
|
+
event.preventDefault();
|
|
3444
|
+
}
|
|
3445
|
+
const handleTypeChange = (e) => {
|
|
3446
|
+
setErrorMessage('');
|
|
3447
|
+
const { value } = e.target;
|
|
3448
|
+
setSelectedExon(neighboringExons[value]);
|
|
3449
|
+
};
|
|
3450
|
+
const neighboringExons = getNeighboringExons(sourceFeature);
|
|
3451
|
+
return (React.createElement(Dialog, { open: true, title: "Merge exons", handleClose: handleClose, maxWidth: false, "data-testid": "merge-exons" },
|
|
3452
|
+
React.createElement("form", { onSubmit: onSubmit },
|
|
3453
|
+
React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
3454
|
+
Object.keys(neighboringExons).length === 0
|
|
3455
|
+
? 'There are no neighbouring exons to merge with'
|
|
3456
|
+
: 'Merge with exon on:',
|
|
3457
|
+
React.createElement(FormControl, { style: { marginTop: 5 } },
|
|
3458
|
+
React.createElement(RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", name: "radio-buttons-group", value: selectedExon, onChange: handleTypeChange }, Object.keys(neighboringExons).map((key) => (React.createElement(FormControlLabel, { value: key, key: key, control: React.createElement(Radio, null), label: React.createElement(Box, { display: "flex", alignItems: "center" }, makeRadioButtonName$1(key, neighboringExons)) })))))),
|
|
3459
|
+
React.createElement(DialogActions, null,
|
|
3460
|
+
React.createElement(Button, { variant: "contained", type: "submit", disabled: Object.keys(neighboringExons).length === 0 ||
|
|
3461
|
+
selectedExon === undefined }, "Submit"),
|
|
3462
|
+
React.createElement(Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
3463
|
+
errorMessage ? (React.createElement(DialogContent, null,
|
|
3464
|
+
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
function getTranscripts(referenceTranscript, session) {
|
|
3468
|
+
const gene = referenceTranscript.parent;
|
|
3469
|
+
if (!gene) {
|
|
3470
|
+
throw new Error('Unable to find parent of reference transcript');
|
|
3471
|
+
}
|
|
3472
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
3473
|
+
if (!featureTypeOntology) {
|
|
3474
|
+
throw new Error('featureTypeOntology is undefined');
|
|
3475
|
+
}
|
|
3476
|
+
const transcripts = {};
|
|
3477
|
+
if (gene.children) {
|
|
3478
|
+
for (const [, feature] of gene.children) {
|
|
3479
|
+
if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
|
|
3480
|
+
feature._id !== referenceTranscript._id) {
|
|
3481
|
+
transcripts[feature._id] = feature;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
return transcripts;
|
|
3486
|
+
}
|
|
3487
|
+
function makeRadioButtonName(transcript) {
|
|
3488
|
+
let id;
|
|
3489
|
+
if (transcript.attributes.get('gff_name')) {
|
|
3490
|
+
id = transcript.attributes.get('gff_name')?.join(',');
|
|
3491
|
+
}
|
|
3492
|
+
else if (transcript.attributes.get('gff_id')) {
|
|
3493
|
+
id = transcript.attributes.get('gff_id')?.join(',');
|
|
3494
|
+
}
|
|
3495
|
+
else {
|
|
3496
|
+
id = transcript._id;
|
|
3497
|
+
}
|
|
3498
|
+
return `${id} [${transcript.min + 1}-${transcript.max}]`;
|
|
3499
|
+
}
|
|
3500
|
+
function MergeTranscripts({ changeManager, handleClose, selectedFeature, session, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
3501
|
+
const { notify } = session;
|
|
3502
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
3503
|
+
const [selectedTranscript, setSelectedTranscript] = useState();
|
|
3504
|
+
async function onSubmit(event) {
|
|
3505
|
+
event.preventDefault();
|
|
3506
|
+
setErrorMessage('');
|
|
3507
|
+
if (!selectedTranscript) {
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
if (selectedFeature?._id === sourceFeature._id) {
|
|
3511
|
+
setSelectedFeature();
|
|
3512
|
+
}
|
|
3513
|
+
if (!sourceFeature.parent) {
|
|
3514
|
+
throw new Error('Cannot find parent');
|
|
3515
|
+
}
|
|
3516
|
+
const change = new MergeTranscriptsChange({
|
|
3517
|
+
changedIds: [sourceFeature._id],
|
|
3518
|
+
typeName: 'MergeTranscriptsChange',
|
|
3519
|
+
assembly: sourceAssemblyId,
|
|
3520
|
+
firstTranscript: getSnapshot(sourceFeature),
|
|
3521
|
+
secondTranscript: getSnapshot(selectedTranscript),
|
|
3522
|
+
parentFeatureId: sourceFeature.parent._id,
|
|
3523
|
+
});
|
|
3524
|
+
await changeManager.submit(change);
|
|
3525
|
+
notify('Transcripts successfully merged', 'success');
|
|
3526
|
+
handleClose();
|
|
3527
|
+
event.preventDefault();
|
|
3528
|
+
}
|
|
3529
|
+
const handleTypeChange = (e) => {
|
|
3530
|
+
setErrorMessage('');
|
|
3531
|
+
const { value } = e.target;
|
|
3532
|
+
setSelectedTranscript(transcripts[value]);
|
|
3533
|
+
};
|
|
3534
|
+
const transcripts = getTranscripts(sourceFeature, session);
|
|
3535
|
+
return (React.createElement(Dialog, { open: true, title: "Merge transcripts", handleClose: handleClose, maxWidth: false, "data-testid": "merge-transcripts" },
|
|
3536
|
+
React.createElement("form", { onSubmit: onSubmit },
|
|
3537
|
+
React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
3538
|
+
Object.keys(transcripts).length === 0
|
|
3539
|
+
? 'There are no transcripts to merge with'
|
|
3540
|
+
: 'Merge with transcript:',
|
|
3541
|
+
React.createElement(FormControl, { style: { marginTop: 5 } },
|
|
3542
|
+
React.createElement(RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", name: "radio-buttons-group", value: selectedTranscript, onChange: handleTypeChange }, Object.keys(transcripts).map((key) => (React.createElement(FormControlLabel, { value: key, key: key, control: React.createElement(Radio, null), label: React.createElement(Box, { display: "flex", alignItems: "center" }, makeRadioButtonName(transcripts[key])) })))))),
|
|
3543
|
+
React.createElement(DialogActions, null,
|
|
3544
|
+
React.createElement(Button, { variant: "contained", type: "submit", disabled: Object.keys(transcripts).length === 0 ||
|
|
3545
|
+
selectedTranscript === undefined }, "Submit"),
|
|
3546
|
+
React.createElement(Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
3547
|
+
errorMessage ? (React.createElement(DialogContent, null,
|
|
3548
|
+
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
3552
|
+
function OpenLocalFile({ handleClose, session }) {
|
|
3553
|
+
const { apolloDataStore } = session;
|
|
3554
|
+
const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
|
|
3555
|
+
const [file, setFile] = useState(null);
|
|
3556
|
+
const [assemblyName, setAssemblyName] = useState('');
|
|
3557
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
3558
|
+
const [submitted, setSubmitted] = useState(false);
|
|
3559
|
+
const theme = useTheme();
|
|
3560
|
+
function handleChangeFile(e) {
|
|
3561
|
+
const selectedFile = e.target.files?.item(0);
|
|
3562
|
+
if (!selectedFile) {
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
setErrorMessage('');
|
|
3566
|
+
setFile(selectedFile);
|
|
3567
|
+
if (!assemblyName) {
|
|
3568
|
+
const fileName = selectedFile.name;
|
|
3569
|
+
const lastDotIndex = fileName.lastIndexOf('.');
|
|
3570
|
+
if (lastDotIndex === -1) {
|
|
3571
|
+
setAssemblyName(fileName);
|
|
3572
|
+
}
|
|
3573
|
+
else {
|
|
3574
|
+
setAssemblyName(fileName.slice(0, lastDotIndex));
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
async function onSubmit(event) {
|
|
3579
|
+
event.preventDefault();
|
|
3580
|
+
setErrorMessage('');
|
|
3581
|
+
setSubmitted(true);
|
|
3582
|
+
if (!file) {
|
|
3583
|
+
throw new Error('No file selected');
|
|
3584
|
+
}
|
|
3585
|
+
// Right now we are not using stream because there was a problem with 'pipe' in ReadStream
|
|
3586
|
+
const fileData = await new Response(file).text();
|
|
3587
|
+
const assemblyId = `${assemblyName}-${file.name}-${nanoid(8)}`;
|
|
3588
|
+
try {
|
|
3589
|
+
await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore);
|
|
3590
|
+
}
|
|
3591
|
+
catch (error) {
|
|
3592
|
+
console.error(error);
|
|
3593
|
+
notify(`Error loading GFF3 ${file.name}, ${String(error)}`, 'error');
|
|
3594
|
+
handleClose();
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
const assemblyConfig = {
|
|
3598
|
+
name: assemblyId,
|
|
3599
|
+
aliases: [assemblyName],
|
|
3600
|
+
displayName: assemblyName,
|
|
3601
|
+
sequence: {
|
|
3602
|
+
trackId: `sequenceConfigId-${assemblyName}`,
|
|
3603
|
+
type: 'ReferenceSequenceTrack',
|
|
3604
|
+
adapter: { type: 'ApolloSequenceAdapter', assemblyId },
|
|
3605
|
+
metadata: {
|
|
3606
|
+
apollo: true,
|
|
3607
|
+
...(isElectron
|
|
3608
|
+
? { file: file.path }
|
|
3609
|
+
: {}),
|
|
3610
|
+
},
|
|
3611
|
+
},
|
|
3612
|
+
};
|
|
3613
|
+
// Save assembly into session
|
|
3614
|
+
await (addSessionAssembly || addAssembly)(assemblyConfig);
|
|
2981
3615
|
const a = await assemblyManager.waitForAssembly(assemblyConfig.name);
|
|
2982
3616
|
if (a) {
|
|
2983
3617
|
// @ts-expect-error MST type coercion problem?
|
|
@@ -3133,7 +3767,7 @@ function ViewChangeLog({ handleClose, session }) {
|
|
|
3133
3767
|
}
|
|
3134
3768
|
|
|
3135
3769
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
3136
|
-
const columns = [
|
|
3770
|
+
const columns$1 = [
|
|
3137
3771
|
{ field: 'refName', headerName: 'Ref Name' },
|
|
3138
3772
|
{ field: 'aliases', headerName: 'Aliases', editable: true },
|
|
3139
3773
|
];
|
|
@@ -3282,7 +3916,7 @@ function AddRefSeqAliases({ changeManager, handleClose, session, }) {
|
|
|
3282
3916
|
React.createElement("input", { type: "file", onChange: handleChangeFileHandler, ref: fileRef, disabled: (enableSubmit && !errorMessage) || !selectedAssembly }))),
|
|
3283
3917
|
selectedAssembly && refNameAliasMap.size > 0 ? (React.createElement("div", { style: { height: 200, width: '100%', marginTop: 20 } },
|
|
3284
3918
|
React.createElement(InputLabel, null, "Refname aliases found for selected assembly."),
|
|
3285
|
-
React.createElement(DataGrid, { rows: getTableRows(), columns: columns, initialState: {
|
|
3919
|
+
React.createElement(DataGrid, { rows: getTableRows(), columns: columns$1, initialState: {
|
|
3286
3920
|
pagination: {
|
|
3287
3921
|
paginationModel: { page: 0, pageSize: 5 },
|
|
3288
3922
|
},
|
|
@@ -3372,6 +4006,116 @@ function ViewCheckResults({ handleClose, session, }) {
|
|
|
3372
4006
|
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
3373
4007
|
}
|
|
3374
4008
|
|
|
4009
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
4010
|
+
function exonIsSplittable(exonToBeSplit) {
|
|
4011
|
+
if (exonToBeSplit.max - exonToBeSplit.min < 2) {
|
|
4012
|
+
return {
|
|
4013
|
+
isSplittable: false,
|
|
4014
|
+
comment: 'This exon is too short to be split',
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
return { isSplittable: true, comment: '' };
|
|
4018
|
+
}
|
|
4019
|
+
function makeDialogText(splitExon) {
|
|
4020
|
+
const splittable = exonIsSplittable(splitExon);
|
|
4021
|
+
if (splittable.isSplittable) {
|
|
4022
|
+
return 'Are you sure you want to split the selected exon?';
|
|
4023
|
+
}
|
|
4024
|
+
return splittable.comment;
|
|
4025
|
+
}
|
|
4026
|
+
function SplitExon({ changeManager, handleClose, selectedFeature, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
4027
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
4028
|
+
const exonToBeSplit = getSnapshot(sourceFeature);
|
|
4029
|
+
function onSubmit(event) {
|
|
4030
|
+
event.preventDefault();
|
|
4031
|
+
setErrorMessage('');
|
|
4032
|
+
if (selectedFeature?._id === sourceFeature._id) {
|
|
4033
|
+
setSelectedFeature();
|
|
4034
|
+
}
|
|
4035
|
+
const midpoint = exonToBeSplit.min + (exonToBeSplit.max - exonToBeSplit.min) / 2;
|
|
4036
|
+
const upstreamCut = Math.floor(midpoint);
|
|
4037
|
+
const downstreamCut = Math.ceil(midpoint);
|
|
4038
|
+
if (!sourceFeature.parent?._id) {
|
|
4039
|
+
throw new Error('Splitting an exon without parent is not possible yet');
|
|
4040
|
+
}
|
|
4041
|
+
const change = new SplitExonChange({
|
|
4042
|
+
changedIds: [sourceFeature._id],
|
|
4043
|
+
typeName: 'SplitExonChange',
|
|
4044
|
+
assembly: sourceAssemblyId,
|
|
4045
|
+
exonToBeSplit,
|
|
4046
|
+
parentFeatureId: sourceFeature.parent._id,
|
|
4047
|
+
upstreamCut,
|
|
4048
|
+
downstreamCut,
|
|
4049
|
+
leftExonId: new ObjectID().toHexString(),
|
|
4050
|
+
rightExonId: new ObjectID().toHexString(),
|
|
4051
|
+
});
|
|
4052
|
+
void changeManager.submit(change);
|
|
4053
|
+
handleClose();
|
|
4054
|
+
event.preventDefault();
|
|
4055
|
+
}
|
|
4056
|
+
return (React.createElement(Dialog, { open: true, title: "Split exon", handleClose: handleClose, maxWidth: false, "data-testid": "split-exon" },
|
|
4057
|
+
React.createElement("form", { onSubmit: onSubmit },
|
|
4058
|
+
React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
4059
|
+
React.createElement(DialogContentText, null, makeDialogText(exonToBeSplit))),
|
|
4060
|
+
React.createElement(DialogActions, null,
|
|
4061
|
+
React.createElement(Button, { variant: "contained", type: "submit", disabled: !exonIsSplittable(exonToBeSplit).isSplittable }, "Yes"),
|
|
4062
|
+
React.createElement(Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
4063
|
+
errorMessage ? (React.createElement(DialogContent, null,
|
|
4064
|
+
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
const columns = [
|
|
4068
|
+
{
|
|
4069
|
+
field: 'name',
|
|
4070
|
+
headerName: 'Assembly Name',
|
|
4071
|
+
width: 150,
|
|
4072
|
+
editable: false,
|
|
4073
|
+
},
|
|
4074
|
+
{
|
|
4075
|
+
field: 'aliases',
|
|
4076
|
+
headerName: 'Aliases',
|
|
4077
|
+
width: 300,
|
|
4078
|
+
editable: true,
|
|
4079
|
+
},
|
|
4080
|
+
];
|
|
4081
|
+
function AddAssemblyAliases({ changeManager, handleClose, session, }) {
|
|
4082
|
+
const { apolloDataStore } = session;
|
|
4083
|
+
const { collaborationServerDriver } = apolloDataStore;
|
|
4084
|
+
const assemblies = collaborationServerDriver.getAssemblies();
|
|
4085
|
+
const rows = assemblies.map((assembly) => {
|
|
4086
|
+
return {
|
|
4087
|
+
id: assembly.name,
|
|
4088
|
+
name: assembly.displayName ?? assembly.name,
|
|
4089
|
+
aliases: assembly.aliases.join(', '),
|
|
4090
|
+
};
|
|
4091
|
+
});
|
|
4092
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
4093
|
+
const processRowUpdate = (newRow, _oldRow) => {
|
|
4094
|
+
const change = new AddAssemblyAliasesChange({
|
|
4095
|
+
typeName: 'AddAssemblyAliasesChange',
|
|
4096
|
+
assembly: newRow.id,
|
|
4097
|
+
aliases: newRow.aliases.split(','),
|
|
4098
|
+
});
|
|
4099
|
+
void changeManager.submit(change).catch(() => {
|
|
4100
|
+
setErrorMessage('Error submitting change');
|
|
4101
|
+
});
|
|
4102
|
+
handleClose();
|
|
4103
|
+
return newRow;
|
|
4104
|
+
};
|
|
4105
|
+
return (React.createElement(Dialog, { open: true, title: "Add assembly aliases", handleClose: handleClose, maxWidth: 'sm', "data-testid": "add-assembly-alias", fullWidth: true },
|
|
4106
|
+
React.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
4107
|
+
React.createElement(Box, { sx: { height: 400, width: '100%' } },
|
|
4108
|
+
React.createElement(DataGrid, { rows: rows, columns: columns, initialState: {
|
|
4109
|
+
pagination: {
|
|
4110
|
+
paginationModel: {
|
|
4111
|
+
pageSize: 5,
|
|
4112
|
+
},
|
|
4113
|
+
},
|
|
4114
|
+
}, pageSizeOptions: [5], processRowUpdate: processRowUpdate, disableRowSelectionOnClick: true }))),
|
|
4115
|
+
errorMessage ? (React.createElement(DialogContent, null,
|
|
4116
|
+
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
4117
|
+
}
|
|
4118
|
+
|
|
3375
4119
|
function addMenuItems(rootModel) {
|
|
3376
4120
|
rootModel.appendToMenu('Apollo', {
|
|
3377
4121
|
label: 'Add Assembly',
|
|
@@ -3433,6 +4177,21 @@ function addMenuItems(rootModel) {
|
|
|
3433
4177
|
]);
|
|
3434
4178
|
},
|
|
3435
4179
|
});
|
|
4180
|
+
rootModel.appendToMenu('Apollo', {
|
|
4181
|
+
label: 'Add Assembly aliases',
|
|
4182
|
+
onClick: (session) => {
|
|
4183
|
+
session.queueDialog((doneCallback) => [
|
|
4184
|
+
AddAssemblyAliases,
|
|
4185
|
+
{
|
|
4186
|
+
session,
|
|
4187
|
+
handleClose: () => {
|
|
4188
|
+
doneCallback();
|
|
4189
|
+
},
|
|
4190
|
+
changeManager: session.apolloDataStore.changeManager,
|
|
4191
|
+
},
|
|
4192
|
+
]);
|
|
4193
|
+
},
|
|
4194
|
+
});
|
|
3436
4195
|
rootModel.appendToMenu('Apollo', {
|
|
3437
4196
|
label: 'Manage Users',
|
|
3438
4197
|
onClick: (session) => {
|
|
@@ -4448,12 +5207,14 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
|
|
|
4448
5207
|
typeName: 'FeatureAttributeChange',
|
|
4449
5208
|
assembly,
|
|
4450
5209
|
featureId: _id,
|
|
4451
|
-
|
|
5210
|
+
oldAttributes: attributesSerialized,
|
|
5211
|
+
newAttributes: remainingAttributes,
|
|
4452
5212
|
});
|
|
4453
5213
|
void changeManager.submit(change);
|
|
4454
5214
|
}
|
|
4455
5215
|
function modifyFeatureAttribute(key, attribute) {
|
|
4456
5216
|
const serializedAttributes = { ...getSnapshot(attributes) };
|
|
5217
|
+
const oldAttributes = structuredClone(serializedAttributes);
|
|
4457
5218
|
if (!(key in serializedAttributes)) {
|
|
4458
5219
|
notify(`"${key}" not found in feature attributes`, 'error');
|
|
4459
5220
|
return;
|
|
@@ -4468,12 +5229,14 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
|
|
|
4468
5229
|
typeName: 'FeatureAttributeChange',
|
|
4469
5230
|
assembly,
|
|
4470
5231
|
featureId: feature._id,
|
|
4471
|
-
|
|
5232
|
+
oldAttributes,
|
|
5233
|
+
newAttributes: serializedAttributes,
|
|
4472
5234
|
});
|
|
4473
5235
|
void changeManager.submit(change);
|
|
4474
5236
|
}
|
|
4475
5237
|
function addFeatureAttribute(key, attribute) {
|
|
4476
5238
|
const serializedAttributes = { ...getSnapshot(attributes) };
|
|
5239
|
+
const oldAttributes = structuredClone(serializedAttributes);
|
|
4477
5240
|
if (key in serializedAttributes) {
|
|
4478
5241
|
notify(`Feature already has attribute "${key}"`, 'error');
|
|
4479
5242
|
return;
|
|
@@ -4484,7 +5247,8 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
|
|
|
4484
5247
|
typeName: 'FeatureAttributeChange',
|
|
4485
5248
|
assembly,
|
|
4486
5249
|
featureId: feature._id,
|
|
4487
|
-
|
|
5250
|
+
oldAttributes,
|
|
5251
|
+
newAttributes: serializedAttributes,
|
|
4488
5252
|
});
|
|
4489
5253
|
void changeManager.submit(change);
|
|
4490
5254
|
}
|
|
@@ -4887,6 +5651,28 @@ const ApolloTranscriptDetailsModel = types
|
|
|
4887
5651
|
},
|
|
4888
5652
|
}));
|
|
4889
5653
|
|
|
5654
|
+
async function copyToClipboard(element) {
|
|
5655
|
+
if (isSecureContext) {
|
|
5656
|
+
const textBlob = new Blob([element.outerText], { type: 'text/plain' });
|
|
5657
|
+
const htmlBlob = new Blob([element.outerHTML], { type: 'text/html' });
|
|
5658
|
+
const clipboardItem = new ClipboardItem({
|
|
5659
|
+
[textBlob.type]: textBlob,
|
|
5660
|
+
[htmlBlob.type]: htmlBlob,
|
|
5661
|
+
});
|
|
5662
|
+
return navigator.clipboard.write([clipboardItem]);
|
|
5663
|
+
}
|
|
5664
|
+
const copyCallback = (event) => {
|
|
5665
|
+
event.clipboardData?.setData('text/plain', element.outerText);
|
|
5666
|
+
event.clipboardData?.setData('text/html', element.outerHTML);
|
|
5667
|
+
event.preventDefault();
|
|
5668
|
+
};
|
|
5669
|
+
document.addEventListener('copy', copyCallback);
|
|
5670
|
+
// fall back to deprecated only in non-secure contexts
|
|
5671
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
5672
|
+
document.execCommand('copy');
|
|
5673
|
+
document.removeEventListener('copy', copyCallback);
|
|
5674
|
+
}
|
|
5675
|
+
|
|
4890
5676
|
const SEQUENCE_WRAP_LENGTH = 60;
|
|
4891
5677
|
function getSequenceSegments(segmentType, feature, getSequence) {
|
|
4892
5678
|
const segments = [];
|
|
@@ -4946,13 +5732,12 @@ function getSequenceSegments(segmentType, feature, getSequence) {
|
|
|
4946
5732
|
const [firstLocation] = cdsLocations;
|
|
4947
5733
|
const locs = [];
|
|
4948
5734
|
for (const loc of firstLocation) {
|
|
4949
|
-
|
|
4950
|
-
if (strand === -1) {
|
|
4951
|
-
sequence = revcom(sequence);
|
|
4952
|
-
}
|
|
4953
|
-
wholeSequence += sequence;
|
|
5735
|
+
wholeSequence += getSequence(loc.min, loc.max);
|
|
4954
5736
|
locs.push({ min: loc.min, max: loc.max });
|
|
4955
5737
|
}
|
|
5738
|
+
if (strand === -1) {
|
|
5739
|
+
wholeSequence = revcom(wholeSequence);
|
|
5740
|
+
}
|
|
4956
5741
|
const sequenceLines = splitStringIntoChunks(wholeSequence, SEQUENCE_WRAP_LENGTH);
|
|
4957
5742
|
segments.push({ type: 'CDS', sequenceLines, locs });
|
|
4958
5743
|
return segments;
|
|
@@ -4962,13 +5747,12 @@ function getSequenceSegments(segmentType, feature, getSequence) {
|
|
|
4962
5747
|
const [firstLocation] = cdsLocations;
|
|
4963
5748
|
const locs = [];
|
|
4964
5749
|
for (const loc of firstLocation) {
|
|
4965
|
-
|
|
4966
|
-
if (strand === -1) {
|
|
4967
|
-
sequence = revcom(sequence);
|
|
4968
|
-
}
|
|
4969
|
-
wholeSequence += sequence;
|
|
5750
|
+
wholeSequence += getSequence(loc.min, loc.max);
|
|
4970
5751
|
locs.push({ min: loc.min, max: loc.max });
|
|
4971
5752
|
}
|
|
5753
|
+
if (strand === -1) {
|
|
5754
|
+
wholeSequence = revcom(wholeSequence);
|
|
5755
|
+
}
|
|
4972
5756
|
let protein = '';
|
|
4973
5757
|
for (let i = 0; i < wholeSequence.length; i += 3) {
|
|
4974
5758
|
const codonSeq = wholeSequence.slice(i, i + 3).toUpperCase();
|
|
@@ -5072,23 +5856,16 @@ const TranscriptSequence = observer(function TranscriptSequence({ assembly, feat
|
|
|
5072
5856
|
setSequenceSegments(seqSegments);
|
|
5073
5857
|
setLocationIntervals(locIntervals);
|
|
5074
5858
|
}
|
|
5075
|
-
|
|
5076
|
-
const copyToClipboard = () => {
|
|
5859
|
+
const onCopyClick = () => {
|
|
5077
5860
|
const seqDiv = seqRef.current;
|
|
5078
5861
|
if (!seqDiv) {
|
|
5079
5862
|
return;
|
|
5080
5863
|
}
|
|
5081
|
-
|
|
5082
|
-
const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
|
|
5083
|
-
const clipboardItem = new ClipboardItem({
|
|
5084
|
-
[textBlob.type]: textBlob,
|
|
5085
|
-
[htmlBlob.type]: htmlBlob,
|
|
5086
|
-
});
|
|
5087
|
-
void navigator.clipboard.write([clipboardItem]);
|
|
5864
|
+
void copyToClipboard(seqDiv);
|
|
5088
5865
|
};
|
|
5089
5866
|
return (React.createElement(React.Fragment, null,
|
|
5090
5867
|
React.createElement(Select, { defaultValue: "genomic", value: selectedOption, onChange: handleChangeSeqOption, size: "small" }, sequenceOptions.map((option) => (React.createElement(MenuItem, { key: option, value: option }, option)))),
|
|
5091
|
-
React.createElement(Button, { variant: "contained", onClick:
|
|
5868
|
+
React.createElement(Button, { variant: "contained", onClick: onCopyClick, style: { marginLeft: 10 }, size: "medium" }, "Copy sequence"),
|
|
5092
5869
|
React.createElement(Paper, { style: {
|
|
5093
5870
|
fontFamily: 'monospace',
|
|
5094
5871
|
padding: theme.spacing(),
|
|
@@ -5145,107 +5922,512 @@ const Strand = (props) => {
|
|
|
5145
5922
|
const { strand } = props;
|
|
5146
5923
|
return (React.createElement("div", null, strand === 1 ? (React.createElement(AddIcon, null)) : strand === -1 ? (React.createElement(RemoveIcon, null)) : (React.createElement(Typography, { component: 'span' }, "N/A"))));
|
|
5147
5924
|
};
|
|
5925
|
+
const minMaxExonTranscriptLocation = (transcript, featureTypeOntology) => {
|
|
5926
|
+
const { transcriptExonParts } = transcript;
|
|
5927
|
+
const exonParts = transcriptExonParts
|
|
5928
|
+
.filter((part) => featureTypeOntology.isTypeOf(part.type, 'exon'))
|
|
5929
|
+
.sort(({ min: a }, { min: b }) => a - b);
|
|
5930
|
+
const exonMin = exonParts[0]?.min;
|
|
5931
|
+
const exonMax = exonParts[exonParts.length - 1]?.max;
|
|
5932
|
+
return [exonMin, exonMax];
|
|
5933
|
+
};
|
|
5148
5934
|
const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocation({ assembly, feature, refName, session, }) {
|
|
5149
5935
|
const { notify } = session;
|
|
5150
5936
|
const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
|
|
5151
5937
|
const refData = currentAssembly?.getByRefName(refName);
|
|
5152
5938
|
const { changeManager } = session.apolloDataStore;
|
|
5153
5939
|
const seqRef = useRef(null);
|
|
5154
|
-
|
|
5155
|
-
|
|
5940
|
+
if (!refData) {
|
|
5941
|
+
return null;
|
|
5942
|
+
}
|
|
5943
|
+
const { apolloDataStore } = session;
|
|
5944
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
5945
|
+
if (!featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
|
|
5946
|
+
!featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) {
|
|
5947
|
+
throw new Error('Feature is not a transcript or equivalent');
|
|
5948
|
+
}
|
|
5949
|
+
const { cdsLocations, transcriptExonParts, strand } = feature;
|
|
5950
|
+
const [firstCDSLocation] = cdsLocations;
|
|
5951
|
+
const [exonMin, exonMax] = minMaxExonTranscriptLocation(feature, featureTypeOntology);
|
|
5952
|
+
let cdsMin = exonMin;
|
|
5953
|
+
let cdsMax = exonMax;
|
|
5954
|
+
const cdsPresent = firstCDSLocation.length > 0;
|
|
5955
|
+
if (cdsPresent) {
|
|
5956
|
+
const sortedCDSLocations = firstCDSLocation.toSorted(({ min: a }, { min: b }) => a - b);
|
|
5957
|
+
cdsMin = sortedCDSLocations[0].min;
|
|
5958
|
+
cdsMax = sortedCDSLocations[sortedCDSLocations.length - 1].max;
|
|
5959
|
+
}
|
|
5156
5960
|
function handleCDSLocationChange(oldLocation, newLocation, feature, isMin) {
|
|
5157
5961
|
if (!feature.children) {
|
|
5158
5962
|
throw new Error('Transcript should have child features');
|
|
5159
5963
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5964
|
+
const overlappingExon = getOverlappingExonForCDS(feature, featureTypeOntology, oldLocation, isMin);
|
|
5965
|
+
if (!overlappingExon) {
|
|
5966
|
+
notify('No matching exon found', 'error');
|
|
5967
|
+
return;
|
|
5968
|
+
}
|
|
5969
|
+
const oldExonLocation = isMin ? overlappingExon.min : overlappingExon.max;
|
|
5970
|
+
const { prevExon, nextExon } = getNeighboringExonParts(feature, featureTypeOntology, oldExonLocation, isMin);
|
|
5971
|
+
// Start location should be less than end location
|
|
5972
|
+
if (isMin && newLocation >= overlappingExon.max) {
|
|
5973
|
+
notify('Start location should be less than overlapping exon end location', 'error');
|
|
5974
|
+
return;
|
|
5975
|
+
}
|
|
5976
|
+
// End location should be greater than start location
|
|
5977
|
+
if (!isMin && newLocation <= overlappingExon.min) {
|
|
5978
|
+
notify('End location should be greater than overlapping exon start location', 'error');
|
|
5979
|
+
return;
|
|
5980
|
+
}
|
|
5981
|
+
// Changed location should be greater than end location of previous exon - give 2bp buffer
|
|
5982
|
+
if (prevExon && prevExon.max + 2 > newLocation) {
|
|
5983
|
+
notify('Start location should be greater than previous exon end location', 'error');
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
// Changed location should be less than start location of next exon
|
|
5987
|
+
if (nextExon && nextExon.min - 2 < newLocation) {
|
|
5988
|
+
notify('End location should be less than next exon start location', 'error');
|
|
5989
|
+
return;
|
|
5990
|
+
}
|
|
5991
|
+
const cdsFeature = getMatchingCDSFeature(feature, featureTypeOntology, oldLocation, isMin);
|
|
5992
|
+
if (!cdsFeature) {
|
|
5993
|
+
notify('No matching CDS feature found', 'error');
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
if (!isMin && newLocation <= cdsFeature.min) {
|
|
5997
|
+
notify('End location should be greater than CDS start location', 'error');
|
|
5998
|
+
return;
|
|
5999
|
+
}
|
|
6000
|
+
if (isMin && newLocation >= cdsFeature.max) {
|
|
6001
|
+
notify('Start location should be less than CDS end location', 'error');
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
const overlappingExonFeature = getExonFeature(feature, overlappingExon.min, overlappingExon.max, featureTypeOntology);
|
|
6005
|
+
if (!overlappingExonFeature) {
|
|
6006
|
+
notify('No matching exon feature found', 'error');
|
|
6007
|
+
return;
|
|
6008
|
+
}
|
|
6009
|
+
if (isMin && newLocation !== cdsFeature.min) {
|
|
6010
|
+
const startChange = new LocationStartChange({
|
|
6011
|
+
typeName: 'LocationStartChange',
|
|
6012
|
+
changedIds: [],
|
|
6013
|
+
changes: [],
|
|
6014
|
+
assembly,
|
|
6015
|
+
});
|
|
6016
|
+
if (newLocation < overlappingExon.min) {
|
|
6017
|
+
if (prevExon) {
|
|
6018
|
+
// update exon start location
|
|
6019
|
+
appendStartLocationChange(overlappingExonFeature, startChange, newLocation);
|
|
6020
|
+
// update CDS start location
|
|
6021
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
6022
|
+
}
|
|
6023
|
+
else {
|
|
6024
|
+
const transcriptStart = feature.min;
|
|
6025
|
+
const gene = feature.parent;
|
|
6026
|
+
if (newLocation < transcriptStart) {
|
|
6027
|
+
if (gene && newLocation < gene.min) {
|
|
6028
|
+
// update gene start location
|
|
6029
|
+
appendStartLocationChange(gene, startChange, newLocation);
|
|
6030
|
+
}
|
|
6031
|
+
// update transcript start location
|
|
6032
|
+
appendStartLocationChange(feature, startChange, newLocation);
|
|
6033
|
+
// update exon start location
|
|
6034
|
+
appendStartLocationChange(overlappingExonFeature, startChange, newLocation);
|
|
6035
|
+
// update CDS start location
|
|
6036
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
else {
|
|
6041
|
+
// update CDS start location
|
|
6042
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
6043
|
+
}
|
|
6044
|
+
void changeManager.submit(startChange).catch(() => {
|
|
6045
|
+
notify('Error updating feature CDS start position', 'error');
|
|
6046
|
+
});
|
|
6047
|
+
}
|
|
6048
|
+
if (!isMin && newLocation !== cdsFeature.max) {
|
|
6049
|
+
const endChange = new LocationEndChange({
|
|
6050
|
+
typeName: 'LocationEndChange',
|
|
6051
|
+
changedIds: [],
|
|
6052
|
+
changes: [],
|
|
6053
|
+
assembly,
|
|
6054
|
+
});
|
|
6055
|
+
if (newLocation > overlappingExon.max) {
|
|
6056
|
+
if (nextExon) {
|
|
6057
|
+
// update exon end location
|
|
6058
|
+
appendEndLocationChange(overlappingExonFeature, endChange, newLocation);
|
|
6059
|
+
// update CDS end location
|
|
6060
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
6061
|
+
}
|
|
6062
|
+
else {
|
|
6063
|
+
const transcriptEnd = feature.max;
|
|
6064
|
+
const gene = feature.parent;
|
|
6065
|
+
if (newLocation > transcriptEnd) {
|
|
6066
|
+
if (gene && newLocation > gene.max) {
|
|
6067
|
+
// update gene end location
|
|
6068
|
+
appendEndLocationChange(gene, endChange, newLocation);
|
|
6069
|
+
}
|
|
6070
|
+
// update transcript end location
|
|
6071
|
+
appendEndLocationChange(feature, endChange, newLocation);
|
|
6072
|
+
// update exon end location
|
|
6073
|
+
appendEndLocationChange(overlappingExonFeature, endChange, newLocation);
|
|
6074
|
+
// update CDS end location
|
|
6075
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
else {
|
|
6080
|
+
// update CDS end location
|
|
6081
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
5191
6082
|
}
|
|
6083
|
+
void changeManager.submit(endChange).catch(() => {
|
|
6084
|
+
notify('Error updating feature CDS end position', 'error');
|
|
6085
|
+
});
|
|
5192
6086
|
}
|
|
5193
6087
|
}
|
|
6088
|
+
const updateCDSLocation = (oldLocation, newLocation, feature, isMin, onComplete) => {
|
|
6089
|
+
if (!feature.children) {
|
|
6090
|
+
throw new Error('Transcript should have child features');
|
|
6091
|
+
}
|
|
6092
|
+
if (oldLocation === newLocation) {
|
|
6093
|
+
return;
|
|
6094
|
+
}
|
|
6095
|
+
const cdsFeature = getMatchingCDSFeature(feature, featureTypeOntology, oldLocation, isMin);
|
|
6096
|
+
if (!cdsFeature) {
|
|
6097
|
+
notify('No matching CDS feature found', 'error');
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
6100
|
+
const change = isMin
|
|
6101
|
+
? new LocationStartChange({
|
|
6102
|
+
typeName: 'LocationStartChange',
|
|
6103
|
+
changedIds: [cdsFeature._id],
|
|
6104
|
+
featureId: cdsFeature._id,
|
|
6105
|
+
oldStart: cdsFeature.min,
|
|
6106
|
+
newStart: newLocation,
|
|
6107
|
+
assembly,
|
|
6108
|
+
})
|
|
6109
|
+
: new LocationEndChange({
|
|
6110
|
+
typeName: 'LocationEndChange',
|
|
6111
|
+
changedIds: [cdsFeature._id],
|
|
6112
|
+
featureId: cdsFeature._id,
|
|
6113
|
+
oldEnd: cdsFeature.max,
|
|
6114
|
+
newEnd: newLocation,
|
|
6115
|
+
assembly,
|
|
6116
|
+
});
|
|
6117
|
+
void changeManager
|
|
6118
|
+
.submit(change)
|
|
6119
|
+
.then(() => {
|
|
6120
|
+
if (onComplete) {
|
|
6121
|
+
onComplete();
|
|
6122
|
+
}
|
|
6123
|
+
})
|
|
6124
|
+
.catch(() => {
|
|
6125
|
+
notify('Error updating feature CDS position', 'error');
|
|
6126
|
+
});
|
|
6127
|
+
};
|
|
5194
6128
|
function handleExonLocationChange(oldLocation, newLocation, feature, isMin) {
|
|
5195
6129
|
if (!feature.children) {
|
|
5196
6130
|
throw new Error('Transcript should have child features');
|
|
5197
6131
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
6132
|
+
const { matchingExon, prevExon, nextExon } = getNeighboringExonParts(feature, featureTypeOntology, oldLocation, isMin);
|
|
6133
|
+
if (!matchingExon) {
|
|
6134
|
+
notify('No matching exon found', 'error');
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
// Start location should be less than end location
|
|
6138
|
+
if (isMin && newLocation >= matchingExon.max) {
|
|
6139
|
+
notify(`Start location should be less than end location`, 'error');
|
|
6140
|
+
return;
|
|
6141
|
+
}
|
|
6142
|
+
// End location should be greater than start location
|
|
6143
|
+
if (!isMin && newLocation <= matchingExon.min) {
|
|
6144
|
+
notify(`End location should be greater than start location`, 'error');
|
|
6145
|
+
return;
|
|
6146
|
+
}
|
|
6147
|
+
// Changed location should be greater than end location of previous exon - give 2bp buffer
|
|
6148
|
+
if (prevExon && prevExon.max + 2 > newLocation) {
|
|
6149
|
+
notify(`Error while changing start location`, 'error');
|
|
6150
|
+
return;
|
|
6151
|
+
}
|
|
6152
|
+
// Changed location should be less than start location of next exon - give 2bp buffer
|
|
6153
|
+
if (nextExon && nextExon.min - 2 < newLocation) {
|
|
6154
|
+
notify(`Error while changing end location`, 'error');
|
|
6155
|
+
return;
|
|
6156
|
+
}
|
|
6157
|
+
const exonFeature = getExonFeature(feature, matchingExon.min, matchingExon.max, featureTypeOntology);
|
|
6158
|
+
if (!exonFeature) {
|
|
6159
|
+
notify('No matching exon feature found', 'error');
|
|
6160
|
+
return;
|
|
6161
|
+
}
|
|
6162
|
+
const cdsFeature = getFirstCDSFeature(feature, featureTypeOntology);
|
|
6163
|
+
// START LOCATION CHANGE
|
|
6164
|
+
if (isMin && newLocation !== matchingExon.min) {
|
|
6165
|
+
const startChange = new LocationStartChange({
|
|
6166
|
+
typeName: 'LocationStartChange',
|
|
6167
|
+
changedIds: [],
|
|
6168
|
+
changes: [],
|
|
6169
|
+
assembly,
|
|
6170
|
+
});
|
|
6171
|
+
if (prevExon) {
|
|
6172
|
+
// update exon start location
|
|
6173
|
+
appendStartLocationChange(exonFeature, startChange, newLocation);
|
|
5201
6174
|
}
|
|
5202
|
-
|
|
5203
|
-
const
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
6175
|
+
else {
|
|
6176
|
+
const transcriptStart = feature.min;
|
|
6177
|
+
const gene = feature.parent;
|
|
6178
|
+
if (newLocation < transcriptStart) {
|
|
6179
|
+
if (gene && newLocation < gene.min) {
|
|
6180
|
+
// update gene start location
|
|
6181
|
+
appendStartLocationChange(gene, startChange, newLocation);
|
|
6182
|
+
}
|
|
6183
|
+
// update transcript start location
|
|
6184
|
+
appendStartLocationChange(feature, startChange, newLocation);
|
|
6185
|
+
// update exon start location
|
|
6186
|
+
appendStartLocationChange(exonFeature, startChange, newLocation);
|
|
6187
|
+
}
|
|
6188
|
+
else if (newLocation > transcriptStart) {
|
|
6189
|
+
// update exon start location
|
|
6190
|
+
appendStartLocationChange(exonFeature, startChange, newLocation);
|
|
6191
|
+
// update transcript start location
|
|
6192
|
+
appendStartLocationChange(feature, startChange, newLocation);
|
|
6193
|
+
if (gene) {
|
|
6194
|
+
const [geneMinWithNewLoc] = geneMinMaxWithNewLocation(gene, feature, newLocation, featureTypeOntology, isMin);
|
|
6195
|
+
if (gene.min != geneMinWithNewLoc) {
|
|
6196
|
+
// update gene start location
|
|
6197
|
+
appendStartLocationChange(gene, startChange, geneMinWithNewLoc);
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
5215
6201
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
});
|
|
5225
|
-
changeManager.submit(change).catch(() => {
|
|
5226
|
-
notify('Error updating feature start position', 'error');
|
|
5227
|
-
});
|
|
5228
|
-
return;
|
|
6202
|
+
// When we change the start location of the exon overlapping with start location of the CDS
|
|
6203
|
+
// and the new start location is greater than the CDS start location then we need to update the CDS start location
|
|
6204
|
+
if (cdsFeature &&
|
|
6205
|
+
cdsFeature.min >= matchingExon.min &&
|
|
6206
|
+
cdsFeature.min <= matchingExon.max &&
|
|
6207
|
+
newLocation > cdsFeature.min) {
|
|
6208
|
+
// update CDS start location
|
|
6209
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
5229
6210
|
}
|
|
6211
|
+
void changeManager.submit(startChange).catch(() => {
|
|
6212
|
+
notify('Error updating feature exon start position', 'error');
|
|
6213
|
+
});
|
|
6214
|
+
}
|
|
6215
|
+
// END LOCATION CHANGE
|
|
6216
|
+
if (!isMin && newLocation !== matchingExon.max) {
|
|
6217
|
+
const endChange = new LocationEndChange({
|
|
6218
|
+
typeName: 'LocationEndChange',
|
|
6219
|
+
changedIds: [],
|
|
6220
|
+
changes: [],
|
|
6221
|
+
assembly,
|
|
6222
|
+
});
|
|
6223
|
+
if (nextExon) {
|
|
6224
|
+
// update exon end location
|
|
6225
|
+
appendEndLocationChange(exonFeature, endChange, newLocation);
|
|
6226
|
+
}
|
|
6227
|
+
else {
|
|
6228
|
+
const transcriptEnd = feature.max;
|
|
6229
|
+
const gene = feature.parent;
|
|
6230
|
+
if (newLocation > transcriptEnd) {
|
|
6231
|
+
if (gene && newLocation > gene.max) {
|
|
6232
|
+
// update gene end location
|
|
6233
|
+
appendEndLocationChange(gene, endChange, newLocation);
|
|
6234
|
+
}
|
|
6235
|
+
// update transcript end location
|
|
6236
|
+
appendEndLocationChange(feature, endChange, newLocation);
|
|
6237
|
+
// update exon end location
|
|
6238
|
+
appendEndLocationChange(exonFeature, endChange, newLocation);
|
|
6239
|
+
}
|
|
6240
|
+
else if (newLocation < transcriptEnd) {
|
|
6241
|
+
// update exon end location
|
|
6242
|
+
appendEndLocationChange(exonFeature, endChange, newLocation);
|
|
6243
|
+
// update transcript end location
|
|
6244
|
+
appendEndLocationChange(feature, endChange, newLocation);
|
|
6245
|
+
if (gene) {
|
|
6246
|
+
const [, geneMaxWithNewLoc] = geneMinMaxWithNewLocation(gene, feature, newLocation, featureTypeOntology, isMin);
|
|
6247
|
+
if (gene.max != geneMaxWithNewLoc) {
|
|
6248
|
+
// update gene end location
|
|
6249
|
+
appendEndLocationChange(gene, endChange, geneMaxWithNewLoc);
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
// When we change the end location of the exon overlapping with end location of the CDS
|
|
6255
|
+
// and the new end location is less than the CDS end location then we need to update the CDS end location
|
|
6256
|
+
if (cdsFeature &&
|
|
6257
|
+
cdsFeature.max >= matchingExon.min &&
|
|
6258
|
+
cdsFeature.max <= matchingExon.max &&
|
|
6259
|
+
newLocation < cdsFeature.max) {
|
|
6260
|
+
// update CDS end location
|
|
6261
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
6262
|
+
}
|
|
6263
|
+
void changeManager.submit(endChange).catch(() => {
|
|
6264
|
+
notify('Error updating feature exon end position', 'error');
|
|
6265
|
+
});
|
|
5230
6266
|
}
|
|
5231
6267
|
}
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
const
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
6268
|
+
const appendEndLocationChange = (feature, change, newLocation) => {
|
|
6269
|
+
change.changedIds.push(feature._id);
|
|
6270
|
+
change.changes.push({
|
|
6271
|
+
featureId: feature._id,
|
|
6272
|
+
oldEnd: feature.max,
|
|
6273
|
+
newEnd: newLocation,
|
|
6274
|
+
});
|
|
6275
|
+
};
|
|
6276
|
+
const appendStartLocationChange = (feature, change, newLocation) => {
|
|
6277
|
+
change.changedIds.push(feature._id);
|
|
6278
|
+
change.changes.push({
|
|
6279
|
+
featureId: feature._id,
|
|
6280
|
+
oldStart: feature.min,
|
|
6281
|
+
newStart: newLocation,
|
|
6282
|
+
});
|
|
6283
|
+
};
|
|
6284
|
+
const getMatchingCDSFeature = (feature, featureTypeOntology, oldCDSLocation, isMin) => {
|
|
6285
|
+
let cdsFeature;
|
|
6286
|
+
for (const [, child] of feature.children ?? []) {
|
|
6287
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
|
|
6288
|
+
continue;
|
|
6289
|
+
}
|
|
6290
|
+
if (isMin && oldCDSLocation === child.min) {
|
|
6291
|
+
cdsFeature = child;
|
|
6292
|
+
break;
|
|
6293
|
+
}
|
|
6294
|
+
if (!isMin && oldCDSLocation === child.max) {
|
|
6295
|
+
cdsFeature = child;
|
|
6296
|
+
break;
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
return cdsFeature;
|
|
6300
|
+
};
|
|
6301
|
+
const getFirstCDSFeature = (feature, featureTypeOntology) => {
|
|
6302
|
+
let cdsFeature;
|
|
6303
|
+
for (const [, child] of feature.children ?? []) {
|
|
6304
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
|
|
6305
|
+
continue;
|
|
6306
|
+
}
|
|
6307
|
+
cdsFeature = child;
|
|
6308
|
+
break;
|
|
6309
|
+
}
|
|
6310
|
+
return cdsFeature;
|
|
6311
|
+
};
|
|
6312
|
+
const getExonFeature = (feature, exonMin, exonMax, featureTypeOntology) => {
|
|
6313
|
+
let exonFeature;
|
|
6314
|
+
for (const [, child] of feature.children ?? []) {
|
|
6315
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
6316
|
+
continue;
|
|
6317
|
+
}
|
|
6318
|
+
if (exonMin === child.min && exonMax === child.max) {
|
|
6319
|
+
exonFeature = child;
|
|
6320
|
+
break;
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
return exonFeature;
|
|
6324
|
+
};
|
|
6325
|
+
const geneMinMaxWithNewLocation = (gene, transcript, newLocation, featureTypeOntology, isMin) => {
|
|
6326
|
+
const mins = [];
|
|
6327
|
+
const maxs = [];
|
|
6328
|
+
for (const [, t] of gene.children?.entries() ?? []) {
|
|
6329
|
+
if (!featureTypeOntology.isTypeOf(t.type, 'transcript')) {
|
|
6330
|
+
continue;
|
|
6331
|
+
}
|
|
6332
|
+
if (t._id === transcript._id) {
|
|
6333
|
+
if (isMin) {
|
|
6334
|
+
mins.push(newLocation);
|
|
6335
|
+
maxs.push(t.max);
|
|
6336
|
+
}
|
|
6337
|
+
else {
|
|
6338
|
+
maxs.push(newLocation);
|
|
6339
|
+
mins.push(t.min);
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6342
|
+
else {
|
|
6343
|
+
mins.push(t.min);
|
|
6344
|
+
maxs.push(t.max);
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
const newMin = Math.min(...mins);
|
|
6348
|
+
const newMax = Math.max(...maxs);
|
|
6349
|
+
return [newMin, newMax];
|
|
6350
|
+
};
|
|
6351
|
+
const getOverlappingExonForCDS = (transcript, featureTypeOntology, oldCDSLocation, isMin) => {
|
|
6352
|
+
const { transcriptExonParts } = transcript;
|
|
6353
|
+
let overlappingExonPart;
|
|
6354
|
+
for (const [, exonPart] of transcriptExonParts.entries()) {
|
|
6355
|
+
if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
|
|
6356
|
+
continue;
|
|
6357
|
+
}
|
|
6358
|
+
if (!isMin &&
|
|
6359
|
+
oldCDSLocation >= exonPart.min &&
|
|
6360
|
+
oldCDSLocation <= exonPart.max) {
|
|
6361
|
+
overlappingExonPart = exonPart;
|
|
6362
|
+
break;
|
|
6363
|
+
}
|
|
6364
|
+
if (isMin &&
|
|
6365
|
+
oldCDSLocation >= exonPart.min &&
|
|
6366
|
+
oldCDSLocation <= exonPart.max) {
|
|
6367
|
+
overlappingExonPart = exonPart;
|
|
6368
|
+
break;
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6371
|
+
return overlappingExonPart;
|
|
6372
|
+
};
|
|
6373
|
+
const getNeighboringExonParts = (transcript, featureTypeOntology, oldExonLoc, isMin) => {
|
|
6374
|
+
const { transcriptExonParts, strand } = transcript;
|
|
6375
|
+
let matchingExon, matchingExonIdx, prevExon, nextExon;
|
|
6376
|
+
for (const [i, exonPart] of transcriptExonParts.entries()) {
|
|
6377
|
+
if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
|
|
6378
|
+
continue;
|
|
6379
|
+
}
|
|
6380
|
+
if (isMin && exonPart.min === oldExonLoc) {
|
|
6381
|
+
matchingExon = exonPart;
|
|
6382
|
+
matchingExonIdx = i;
|
|
6383
|
+
break;
|
|
6384
|
+
}
|
|
6385
|
+
if (!isMin && exonPart.max === oldExonLoc) {
|
|
6386
|
+
matchingExon = exonPart;
|
|
6387
|
+
matchingExonIdx = i;
|
|
6388
|
+
break;
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
if (matchingExon && matchingExonIdx !== undefined) {
|
|
6392
|
+
if (strand === 1 && matchingExonIdx > 0) {
|
|
6393
|
+
for (let i = matchingExonIdx - 1; i >= 0; i--) {
|
|
6394
|
+
const prevLoc = transcriptExonParts[i];
|
|
6395
|
+
if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
|
|
6396
|
+
prevExon = prevLoc;
|
|
6397
|
+
break;
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
if (strand === -1 && matchingExonIdx < transcriptExonParts.length - 1) {
|
|
6402
|
+
for (let i = matchingExonIdx + 1; i < transcriptExonParts.length; i++) {
|
|
6403
|
+
const prevLoc = transcriptExonParts[i];
|
|
6404
|
+
if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
|
|
6405
|
+
prevExon = prevLoc;
|
|
6406
|
+
break;
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
if (strand === 1 && matchingExonIdx < transcriptExonParts.length - 1) {
|
|
6411
|
+
for (let i = matchingExonIdx + 1; i < transcriptExonParts.length; i++) {
|
|
6412
|
+
const nextLoc = transcriptExonParts[i];
|
|
6413
|
+
if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
|
|
6414
|
+
nextExon = nextLoc;
|
|
6415
|
+
break;
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
if (strand === -1 && matchingExonIdx > 0) {
|
|
6420
|
+
for (let i = matchingExonIdx - 1; i >= 0; i--) {
|
|
6421
|
+
const nextLoc = transcriptExonParts[i];
|
|
6422
|
+
if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
|
|
6423
|
+
nextExon = nextLoc;
|
|
6424
|
+
break;
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
return { matchingExon, prevExon, nextExon };
|
|
6430
|
+
};
|
|
5249
6431
|
const getFivePrimeSpliceSite = (loc, prevLocIdx) => {
|
|
5250
6432
|
let spliceSite = '';
|
|
5251
6433
|
if (prevLocIdx > 0) {
|
|
@@ -5293,12 +6475,15 @@ const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocat
|
|
|
5293
6475
|
const getTranslationSequence = () => {
|
|
5294
6476
|
let wholeSequence = '';
|
|
5295
6477
|
const [firstLocation] = cdsLocations;
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
6478
|
+
const sortedCDSLocations = firstLocation.toSorted(({ min: a }, { min: b }) => a - b);
|
|
6479
|
+
for (const loc of sortedCDSLocations) {
|
|
6480
|
+
wholeSequence += refData.getSequence(loc.min, loc.max);
|
|
6481
|
+
}
|
|
6482
|
+
if (strand === -1) {
|
|
6483
|
+
// Original: ACGCAT
|
|
6484
|
+
// Complement: TGCGTA
|
|
6485
|
+
// Reverse complement: ATGCGT
|
|
6486
|
+
wholeSequence = revcom(wholeSequence);
|
|
5302
6487
|
}
|
|
5303
6488
|
const elements = [];
|
|
5304
6489
|
for (let codonGenomicPos = 0; codonGenomicPos < wholeSequence.length; codonGenomicPos += 3) {
|
|
@@ -5316,9 +6501,12 @@ const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocat
|
|
|
5316
6501
|
// NOTE: codonGenomicPos is important here for calculating the genomic location
|
|
5317
6502
|
// of the start codon. We are using the codonGenomicPos as the key in the typography
|
|
5318
6503
|
// elements to maintain the genomic postion of the codon start
|
|
5319
|
-
const startCodonGenomicLocation =
|
|
5320
|
-
if (startCodonGenomicLocation !== cdsMin) {
|
|
5321
|
-
|
|
6504
|
+
const startCodonGenomicLocation = getCodonGenomicLocation(codonGenomicPos);
|
|
6505
|
+
if (startCodonGenomicLocation !== cdsMin && strand === 1) {
|
|
6506
|
+
updateCDSLocation(cdsMin, startCodonGenomicLocation, feature, true);
|
|
6507
|
+
}
|
|
6508
|
+
if (startCodonGenomicLocation !== cdsMax && strand === -1) {
|
|
6509
|
+
updateCDSLocation(cdsMax, startCodonGenomicLocation, feature, false);
|
|
5322
6510
|
}
|
|
5323
6511
|
} }, protein));
|
|
5324
6512
|
}
|
|
@@ -5337,32 +6525,36 @@ const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocat
|
|
|
5337
6525
|
};
|
|
5338
6526
|
// Codon position is the index of the start codon in the CDS genomic sequence
|
|
5339
6527
|
// Calculate the genomic location of the start codon based on the codon position in the CDS
|
|
5340
|
-
const
|
|
6528
|
+
const getCodonGenomicLocation = (codonGenomicPosition) => {
|
|
5341
6529
|
const [firstLocation] = cdsLocations;
|
|
5342
6530
|
let cdsLen = 0;
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
6531
|
+
const sortedCDSLocations = firstLocation.toSorted(({ min: a }, { min: b }) => a - b);
|
|
6532
|
+
// Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
|
|
6533
|
+
// and codonGenomicPosition is 25
|
|
6534
|
+
// ((10 - 0) + (30 - 20) + (50 - 40)) > 25
|
|
6535
|
+
// So, start codon is in (40, 50)
|
|
6536
|
+
// 40 + (25-20) = 45 is the genomic location of the start codon
|
|
6537
|
+
if (strand === 1) {
|
|
6538
|
+
for (const loc of sortedCDSLocations) {
|
|
6539
|
+
const locLength = loc.max - loc.min;
|
|
6540
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
6541
|
+
return loc.min + (codonGenomicPosition - cdsLen);
|
|
6542
|
+
}
|
|
6543
|
+
cdsLen += locLength;
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
else if (strand === -1) {
|
|
6547
|
+
for (let i = sortedCDSLocations.length - 1; i >= 0; i--) {
|
|
6548
|
+
const loc = sortedCDSLocations[i];
|
|
6549
|
+
const locLength = loc.max - loc.min;
|
|
6550
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
6551
|
+
return loc.max - (codonGenomicPosition - cdsLen);
|
|
6552
|
+
}
|
|
6553
|
+
cdsLen += locLength;
|
|
5364
6554
|
}
|
|
5365
|
-
|
|
6555
|
+
}
|
|
6556
|
+
if (strand === 1) {
|
|
6557
|
+
return cdsMin;
|
|
5366
6558
|
}
|
|
5367
6559
|
return cdsMax;
|
|
5368
6560
|
};
|
|
@@ -5387,48 +6579,82 @@ const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocat
|
|
|
5387
6579
|
if (translSeqCodonStartGenomicPosArr.length === 0) {
|
|
5388
6580
|
return;
|
|
5389
6581
|
}
|
|
5390
|
-
// Trim any sequence before first start codon and after
|
|
6582
|
+
// Trim any sequence before first start codon and after stop codon
|
|
5391
6583
|
const startCodonIndex = translationSequence.indexOf('M');
|
|
5392
|
-
const stopCodonIndex = translationSequence.
|
|
6584
|
+
const stopCodonIndex = translationSequence.indexOf('*') + 1;
|
|
5393
6585
|
const startCodonPos = translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos;
|
|
5394
6586
|
const stopCodonPos = translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos;
|
|
5395
6587
|
if (!startCodonPos || !stopCodonPos) {
|
|
5396
6588
|
return;
|
|
5397
6589
|
}
|
|
5398
|
-
const startCodonGenomicLoc =
|
|
5399
|
-
const stopCodonGenomicLoc =
|
|
5400
|
-
if (
|
|
5401
|
-
|
|
6590
|
+
const startCodonGenomicLoc = getCodonGenomicLocation(startCodonPos);
|
|
6591
|
+
const stopCodonGenomicLoc = getCodonGenomicLocation(stopCodonPos);
|
|
6592
|
+
if (strand === 1) {
|
|
6593
|
+
if (startCodonGenomicLoc > stopCodonGenomicLoc) {
|
|
6594
|
+
notify('Start codon genomic location should be less than stop codon genomic location', 'error');
|
|
6595
|
+
return;
|
|
6596
|
+
}
|
|
6597
|
+
let promise;
|
|
6598
|
+
if (startCodonGenomicLoc !== cdsMin) {
|
|
6599
|
+
promise = new Promise((resolve) => {
|
|
6600
|
+
updateCDSLocation(cdsMin, startCodonGenomicLoc, feature, true, () => {
|
|
6601
|
+
resolve(true);
|
|
6602
|
+
});
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
if (stopCodonGenomicLoc !== cdsMax) {
|
|
6606
|
+
if (promise) {
|
|
6607
|
+
void promise.then(() => {
|
|
6608
|
+
updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false);
|
|
6609
|
+
});
|
|
6610
|
+
}
|
|
6611
|
+
else {
|
|
6612
|
+
updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false);
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
5402
6615
|
}
|
|
5403
|
-
if (
|
|
5404
|
-
//
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
}
|
|
6616
|
+
if (strand === -1) {
|
|
6617
|
+
// reverse strand
|
|
6618
|
+
if (startCodonGenomicLoc < stopCodonGenomicLoc) {
|
|
6619
|
+
notify('Start codon genomic location should be less than stop codon genomic location', 'error');
|
|
6620
|
+
return;
|
|
6621
|
+
}
|
|
6622
|
+
let promise;
|
|
6623
|
+
if (startCodonGenomicLoc !== cdsMax) {
|
|
6624
|
+
promise = new Promise((resolve) => {
|
|
6625
|
+
updateCDSLocation(cdsMax, startCodonGenomicLoc, feature, false, () => {
|
|
6626
|
+
resolve(true);
|
|
6627
|
+
});
|
|
6628
|
+
});
|
|
6629
|
+
}
|
|
6630
|
+
if (stopCodonGenomicLoc !== cdsMin) {
|
|
6631
|
+
if (promise) {
|
|
6632
|
+
void promise.then(() => {
|
|
6633
|
+
updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true);
|
|
6634
|
+
});
|
|
6635
|
+
}
|
|
6636
|
+
else {
|
|
6637
|
+
updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true);
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
5409
6640
|
}
|
|
6641
|
+
notify('Translation sequence trimmed to start and stop codons', 'success');
|
|
5410
6642
|
};
|
|
5411
|
-
const
|
|
6643
|
+
const onCopyClick = () => {
|
|
5412
6644
|
const seqDiv = seqRef.current;
|
|
5413
6645
|
if (!seqDiv) {
|
|
5414
6646
|
return;
|
|
5415
6647
|
}
|
|
5416
|
-
|
|
5417
|
-
const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
|
|
5418
|
-
const clipboardItem = new ClipboardItem({
|
|
5419
|
-
[textBlob.type]: textBlob,
|
|
5420
|
-
[htmlBlob.type]: htmlBlob,
|
|
5421
|
-
});
|
|
5422
|
-
void navigator.clipboard.write([clipboardItem]);
|
|
6648
|
+
void copyToClipboard(seqDiv);
|
|
5423
6649
|
};
|
|
5424
6650
|
return (React.createElement("div", null,
|
|
5425
6651
|
cdsPresent && (React.createElement("div", null,
|
|
5426
|
-
React.createElement(Accordion,
|
|
6652
|
+
React.createElement(Accordion, null,
|
|
5427
6653
|
React.createElement(StyledAccordionSummary, { expandIcon: React.createElement(ExpandMoreIcon, { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
|
|
5428
6654
|
React.createElement(Typography, { component: "span", fontWeight: 'bold' }, "Translation")),
|
|
5429
6655
|
React.createElement(AccordionDetails, null,
|
|
5430
6656
|
React.createElement(SequenceContainer, null,
|
|
5431
|
-
React.createElement(Typography, { component: 'span', ref: seqRef }, getTranslationSequence())),
|
|
6657
|
+
React.createElement(Typography, { component: 'span', ref: seqRef, style: { maxHeight: 120, overflowY: 'scroll' } }, getTranslationSequence())),
|
|
5432
6658
|
React.createElement("div", { style: {
|
|
5433
6659
|
marginTop: 10,
|
|
5434
6660
|
display: 'flex',
|
|
@@ -5437,36 +6663,48 @@ const TranscriptWidgetEditLocation = observer(function TranscriptWidgetEditLocat
|
|
|
5437
6663
|
gap: 10,
|
|
5438
6664
|
} },
|
|
5439
6665
|
React.createElement(Tooltip, { title: "Copy" },
|
|
5440
|
-
React.createElement(ContentCopyIcon, { style: { fontSize: 15, cursor: 'pointer' }, onClick:
|
|
6666
|
+
React.createElement(ContentCopyIcon, { style: { fontSize: 15, cursor: 'pointer' }, onClick: onCopyClick })),
|
|
5441
6667
|
React.createElement(Tooltip, { title: "Trim" },
|
|
5442
6668
|
React.createElement(ContentCutIcon, { style: { fontSize: 15, cursor: 'pointer' }, onClick: trimTranslationSequence }))))),
|
|
5443
6669
|
React.createElement(Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center', marginTop: 10 } },
|
|
5444
6670
|
React.createElement(Grid2, { size: 1 }),
|
|
5445
|
-
React.createElement(Grid2, { size: 4 },
|
|
5446
|
-
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin, onChangeCommitted: (newLocation) => {
|
|
5447
|
-
handleCDSLocationChange(cdsMin, newLocation, feature, true);
|
|
5448
|
-
} })),
|
|
6671
|
+
strand === 1 ? (React.createElement(Grid2, { size: 4 },
|
|
6672
|
+
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin + 1, onChangeCommitted: (newLocation) => {
|
|
6673
|
+
handleCDSLocationChange(cdsMin, newLocation - 1, feature, true);
|
|
6674
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))) : (React.createElement(Grid2, { size: 4 },
|
|
6675
|
+
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
|
|
6676
|
+
handleCDSLocationChange(cdsMax, newLocation, feature, false);
|
|
6677
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))),
|
|
5449
6678
|
React.createElement(Grid2, { size: 2 },
|
|
5450
6679
|
React.createElement(Typography, { component: 'span' }, "CDS")),
|
|
5451
|
-
React.createElement(Grid2, { size: 4 },
|
|
6680
|
+
strand === 1 ? (React.createElement(Grid2, { size: 4 },
|
|
5452
6681
|
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
|
|
5453
6682
|
handleCDSLocationChange(cdsMax, newLocation, feature, false);
|
|
5454
|
-
} })),
|
|
6683
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))) : (React.createElement(Grid2, { size: 4 },
|
|
6684
|
+
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin + 1, onChangeCommitted: (newLocation) => {
|
|
6685
|
+
handleCDSLocationChange(cdsMin, newLocation - 1, feature, true);
|
|
6686
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))),
|
|
5455
6687
|
React.createElement(Grid2, { size: 1 })))),
|
|
5456
6688
|
React.createElement("div", { style: { marginTop: 5 } }, transcriptExonParts.map((loc, index) => {
|
|
5457
6689
|
return (React.createElement("div", { key: index }, loc.type === 'exon' && (React.createElement(Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center' } },
|
|
5458
6690
|
React.createElement(Grid2, { size: 1 }, index !== 0 &&
|
|
5459
6691
|
getFivePrimeSpliceSite(loc, index).map((site, idx) => (React.createElement(Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite)))),
|
|
5460
|
-
React.createElement(Grid2, { size: 4, style: { padding: 0 } },
|
|
5461
|
-
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min, onChangeCommitted: (newLocation) => {
|
|
5462
|
-
handleExonLocationChange(loc.min, newLocation, feature, true);
|
|
5463
|
-
} })),
|
|
6692
|
+
strand === 1 ? (React.createElement(Grid2, { size: 4, style: { padding: 0 } },
|
|
6693
|
+
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min + 1, onChangeCommitted: (newLocation) => {
|
|
6694
|
+
handleExonLocationChange(loc.min, newLocation - 1, feature, true);
|
|
6695
|
+
} }))) : (React.createElement(Grid2, { size: 4, style: { padding: 0 } },
|
|
6696
|
+
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
|
|
6697
|
+
handleExonLocationChange(loc.max, newLocation, feature, false);
|
|
6698
|
+
} }))),
|
|
5464
6699
|
React.createElement(Grid2, { size: 2 },
|
|
5465
6700
|
React.createElement(Strand, { strand: feature.strand })),
|
|
5466
|
-
React.createElement(Grid2, { size: 4, style: { padding: 0 } },
|
|
6701
|
+
strand === 1 ? (React.createElement(Grid2, { size: 4, style: { padding: 0 } },
|
|
5467
6702
|
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
|
|
5468
6703
|
handleExonLocationChange(loc.max, newLocation, feature, false);
|
|
5469
|
-
} })),
|
|
6704
|
+
} }))) : (React.createElement(Grid2, { size: 4, style: { padding: 0 } },
|
|
6705
|
+
React.createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min + 1, onChangeCommitted: (newLocation) => {
|
|
6706
|
+
handleExonLocationChange(loc.min, newLocation - 1, feature, true);
|
|
6707
|
+
} }))),
|
|
5470
6708
|
React.createElement(Grid2, { size: 1 }, index !== transcriptExonParts.length - 1 &&
|
|
5471
6709
|
getThreePrimeSpliceSite(loc, index).map((site, idx) => (React.createElement(Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite))))))));
|
|
5472
6710
|
}))));
|
|
@@ -5477,16 +6715,19 @@ const HeaderTableCell = styled(TableCell)(() => ({
|
|
|
5477
6715
|
}));
|
|
5478
6716
|
const TranscriptWidgetSummary = observer(function TranscriptWidgetSummary(props) {
|
|
5479
6717
|
const { feature } = props;
|
|
5480
|
-
const name = getFeatureName(feature);
|
|
6718
|
+
const name = getFeatureName$1(feature);
|
|
5481
6719
|
const id = getFeatureId$1(feature);
|
|
5482
6720
|
return (React.createElement(Table, { size: "small", sx: { fontSize: '0.75rem', '& .MuiTableCell-root': { padding: '4px' } } },
|
|
5483
6721
|
React.createElement(TableBody, null,
|
|
5484
6722
|
name !== '' && (React.createElement(TableRow, null,
|
|
5485
6723
|
React.createElement(HeaderTableCell, null, "Name"),
|
|
5486
|
-
React.createElement(TableCell, null, getFeatureName(feature)))),
|
|
6724
|
+
React.createElement(TableCell, null, getFeatureName$1(feature)))),
|
|
5487
6725
|
id !== '' && (React.createElement(TableRow, null,
|
|
5488
6726
|
React.createElement(HeaderTableCell, null, "ID"),
|
|
5489
6727
|
React.createElement(TableCell, null, getFeatureId$1(feature)))),
|
|
6728
|
+
React.createElement(TableRow, null,
|
|
6729
|
+
React.createElement(HeaderTableCell, null, "Type"),
|
|
6730
|
+
React.createElement(TableCell, null, feature.type)),
|
|
5490
6731
|
React.createElement(TableRow, null,
|
|
5491
6732
|
React.createElement(HeaderTableCell, null, "Location"),
|
|
5492
6733
|
React.createElement(TableCell, null,
|
|
@@ -5734,13 +6975,14 @@ const NumberCell = observer(function NumberCell({ initialValue, notifyError, onC
|
|
|
5734
6975
|
} })));
|
|
5735
6976
|
});
|
|
5736
6977
|
|
|
5737
|
-
function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager) {
|
|
6978
|
+
function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager, filteredTranscripts, updateFilteredTranscripts) {
|
|
5738
6979
|
const internetAccount = getApolloInternetAccount(session);
|
|
5739
6980
|
const role = internetAccount ? internetAccount.role : 'admin';
|
|
5740
6981
|
const admin = role === 'admin';
|
|
5741
6982
|
const readOnly = !(role && ['admin', 'user'].includes(role));
|
|
5742
6983
|
const menuItems = [];
|
|
5743
6984
|
if (feature) {
|
|
6985
|
+
const featureID = feature.attributes.get('gff_id')?.toString();
|
|
5744
6986
|
const sourceAssemblyId = getAssemblyId(region.assemblyName);
|
|
5745
6987
|
const currentAssemblyId = getAssemblyId(region.assemblyName);
|
|
5746
6988
|
menuItems.push({
|
|
@@ -5807,6 +7049,63 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
|
|
|
5807
7049
|
},
|
|
5808
7050
|
]);
|
|
5809
7051
|
},
|
|
7052
|
+
}, {
|
|
7053
|
+
label: 'Merge transcripts',
|
|
7054
|
+
disabled: !admin,
|
|
7055
|
+
onClick: () => {
|
|
7056
|
+
session.queueDialog((doneCallback) => [
|
|
7057
|
+
MergeTranscripts,
|
|
7058
|
+
{
|
|
7059
|
+
session,
|
|
7060
|
+
handleClose: () => {
|
|
7061
|
+
doneCallback();
|
|
7062
|
+
},
|
|
7063
|
+
changeManager,
|
|
7064
|
+
sourceFeature: feature,
|
|
7065
|
+
sourceAssemblyId: currentAssemblyId,
|
|
7066
|
+
selectedFeature,
|
|
7067
|
+
setSelectedFeature,
|
|
7068
|
+
},
|
|
7069
|
+
]);
|
|
7070
|
+
},
|
|
7071
|
+
}, {
|
|
7072
|
+
label: 'Merge exons',
|
|
7073
|
+
disabled: !admin,
|
|
7074
|
+
onClick: () => {
|
|
7075
|
+
session.queueDialog((doneCallback) => [
|
|
7076
|
+
MergeExons,
|
|
7077
|
+
{
|
|
7078
|
+
session,
|
|
7079
|
+
handleClose: () => {
|
|
7080
|
+
doneCallback();
|
|
7081
|
+
},
|
|
7082
|
+
changeManager,
|
|
7083
|
+
sourceFeature: feature,
|
|
7084
|
+
sourceAssemblyId: currentAssemblyId,
|
|
7085
|
+
selectedFeature,
|
|
7086
|
+
setSelectedFeature,
|
|
7087
|
+
},
|
|
7088
|
+
]);
|
|
7089
|
+
},
|
|
7090
|
+
}, {
|
|
7091
|
+
label: 'Split exon',
|
|
7092
|
+
disabled: !admin,
|
|
7093
|
+
onClick: () => {
|
|
7094
|
+
session.queueDialog((doneCallback) => [
|
|
7095
|
+
SplitExon,
|
|
7096
|
+
{
|
|
7097
|
+
session,
|
|
7098
|
+
handleClose: () => {
|
|
7099
|
+
doneCallback();
|
|
7100
|
+
},
|
|
7101
|
+
changeManager,
|
|
7102
|
+
sourceFeature: feature,
|
|
7103
|
+
sourceAssemblyId: currentAssemblyId,
|
|
7104
|
+
selectedFeature,
|
|
7105
|
+
setSelectedFeature,
|
|
7106
|
+
},
|
|
7107
|
+
]);
|
|
7108
|
+
},
|
|
5810
7109
|
});
|
|
5811
7110
|
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
5812
7111
|
if (!featureTypeOntology) {
|
|
@@ -5826,6 +7125,18 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
|
|
|
5826
7125
|
});
|
|
5827
7126
|
session.showWidget(apolloTranscriptWidget);
|
|
5828
7127
|
},
|
|
7128
|
+
}, {
|
|
7129
|
+
label: 'Visible',
|
|
7130
|
+
type: 'checkbox',
|
|
7131
|
+
checked: featureID && filteredTranscripts.includes(featureID) ? false : true,
|
|
7132
|
+
onClick: () => {
|
|
7133
|
+
if (featureID) {
|
|
7134
|
+
const newForms = filteredTranscripts.includes(featureID)
|
|
7135
|
+
? filteredTranscripts.filter((form) => form !== featureID)
|
|
7136
|
+
: [...filteredTranscripts, featureID];
|
|
7137
|
+
updateFilteredTranscripts(newForms);
|
|
7138
|
+
}
|
|
7139
|
+
},
|
|
5829
7140
|
});
|
|
5830
7141
|
}
|
|
5831
7142
|
}
|
|
@@ -5868,8 +7179,8 @@ const useStyles$5 = makeStyles()((theme) => ({
|
|
|
5868
7179
|
},
|
|
5869
7180
|
}));
|
|
5870
7181
|
function makeContextMenuItems(display, feature) {
|
|
5871
|
-
const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
|
|
5872
|
-
return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
|
|
7182
|
+
const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, filteredTranscripts, updateFilteredTranscripts, } = display;
|
|
7183
|
+
return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager, filteredTranscripts, updateFilteredTranscripts);
|
|
5873
7184
|
}
|
|
5874
7185
|
function getTopLevelFeature(feature) {
|
|
5875
7186
|
let cur = feature;
|
|
@@ -6171,7 +7482,7 @@ function draw$3(ctx, feature, row, stateModel, displayedRegionIndex) {
|
|
|
6171
7482
|
const widthPx = feature.length / bpPerPx;
|
|
6172
7483
|
const startPx = reversed ? minX - widthPx : minX;
|
|
6173
7484
|
const top = row * heightPx;
|
|
6174
|
-
const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
|
|
7485
|
+
const isSelected = isSelectedFeature$1(feature, apolloSelectedFeature);
|
|
6175
7486
|
const backgroundColor = getBackgroundColor(theme, isSelected);
|
|
6176
7487
|
const textColor = getTextColor(theme, isSelected);
|
|
6177
7488
|
const featureBox = [
|
|
@@ -6290,7 +7601,7 @@ function drawTooltip$3(display, context) {
|
|
|
6290
7601
|
textTop = textTop + 12;
|
|
6291
7602
|
context.fillText(location, startPx + 2, textTop);
|
|
6292
7603
|
}
|
|
6293
|
-
function isSelectedFeature(feature, selectedFeature) {
|
|
7604
|
+
function isSelectedFeature$1(feature, selectedFeature) {
|
|
6294
7605
|
return Boolean(selectedFeature && feature._id === selectedFeature._id);
|
|
6295
7606
|
}
|
|
6296
7607
|
function getBackgroundColor(theme, selected) {
|
|
@@ -6309,19 +7620,51 @@ function drawBox(ctx, x, y, width, height, color) {
|
|
|
6309
7620
|
ctx.fillRect(x, y, width, height);
|
|
6310
7621
|
}
|
|
6311
7622
|
function getContextMenuItems$3(display) {
|
|
6312
|
-
const { apolloHover
|
|
6313
|
-
const menuItems = [];
|
|
7623
|
+
const { apolloHover } = display;
|
|
6314
7624
|
if (!apolloHover) {
|
|
6315
|
-
return
|
|
7625
|
+
return [];
|
|
6316
7626
|
}
|
|
6317
7627
|
const { feature: sourceFeature } = apolloHover;
|
|
7628
|
+
return getContextMenuItemsForFeature$2(display, sourceFeature);
|
|
7629
|
+
}
|
|
7630
|
+
function makeFeatureLabel(feature) {
|
|
7631
|
+
let name;
|
|
7632
|
+
if (feature.attributes.get('gff_name')) {
|
|
7633
|
+
name = feature.attributes.get('gff_name')?.join(',');
|
|
7634
|
+
}
|
|
7635
|
+
else if (feature.attributes.get('gff_id')) {
|
|
7636
|
+
name = feature.attributes.get('gff_id')?.join(',');
|
|
7637
|
+
}
|
|
7638
|
+
else {
|
|
7639
|
+
name = feature._id;
|
|
7640
|
+
}
|
|
7641
|
+
const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`;
|
|
7642
|
+
const maxLen = 60;
|
|
7643
|
+
if (name && name.length + coords.length > maxLen + 5) {
|
|
7644
|
+
const trim = maxLen - coords.length;
|
|
7645
|
+
name = trim > 0 ? name.slice(0, trim) : '';
|
|
7646
|
+
name = `${name}[...]`;
|
|
7647
|
+
}
|
|
7648
|
+
return `${name} ${coords}`;
|
|
7649
|
+
}
|
|
7650
|
+
function getContextMenuItemsForFeature$2(display, sourceFeature) {
|
|
7651
|
+
const { apolloInternetAccount: internetAccount, changeManager, regions, selectedFeature, session, } = display;
|
|
7652
|
+
const menuItems = [];
|
|
6318
7653
|
const role = internetAccount ? internetAccount.role : 'admin';
|
|
6319
7654
|
const admin = role === 'admin';
|
|
6320
7655
|
const readOnly = !(role && ['admin', 'user'].includes(role));
|
|
6321
7656
|
const [region] = regions;
|
|
6322
7657
|
const sourceAssemblyId = display.getAssemblyId(region.assemblyName);
|
|
6323
7658
|
const currentAssemblyId = display.getAssemblyId(region.assemblyName);
|
|
7659
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
7660
|
+
if (!featureTypeOntology) {
|
|
7661
|
+
throw new Error('featureTypeOntology is undefined');
|
|
7662
|
+
}
|
|
7663
|
+
// Add only relevant options
|
|
6324
7664
|
menuItems.push({
|
|
7665
|
+
label: makeFeatureLabel(sourceFeature),
|
|
7666
|
+
type: 'subHeader',
|
|
7667
|
+
}, {
|
|
6325
7668
|
label: 'Add child feature',
|
|
6326
7669
|
disabled: readOnly,
|
|
6327
7670
|
onClick: () => {
|
|
@@ -6377,37 +7720,7 @@ function getContextMenuItems$3(display) {
|
|
|
6377
7720
|
},
|
|
6378
7721
|
]);
|
|
6379
7722
|
},
|
|
6380
|
-
}, {
|
|
6381
|
-
label: 'Edit feature details',
|
|
6382
|
-
onClick: () => {
|
|
6383
|
-
const apolloFeatureWidget = session.addWidget('ApolloFeatureDetailsWidget', 'apolloFeatureDetailsWidget', {
|
|
6384
|
-
feature: sourceFeature,
|
|
6385
|
-
assembly: currentAssemblyId,
|
|
6386
|
-
refName: region.refName,
|
|
6387
|
-
});
|
|
6388
|
-
session.showWidget(apolloFeatureWidget);
|
|
6389
|
-
},
|
|
6390
7723
|
});
|
|
6391
|
-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
6392
|
-
if (!featureTypeOntology) {
|
|
6393
|
-
throw new Error('featureTypeOntology is undefined');
|
|
6394
|
-
}
|
|
6395
|
-
if ((featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
|
|
6396
|
-
featureTypeOntology.isTypeOf(sourceFeature.type, 'pseudogenic_transcript')) &&
|
|
6397
|
-
isSessionModelWithWidgets(session)) {
|
|
6398
|
-
menuItems.push({
|
|
6399
|
-
label: 'Edit transcript details',
|
|
6400
|
-
onClick: () => {
|
|
6401
|
-
const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
|
|
6402
|
-
feature: sourceFeature,
|
|
6403
|
-
assembly: currentAssemblyId,
|
|
6404
|
-
changeManager,
|
|
6405
|
-
refName: region.refName,
|
|
6406
|
-
});
|
|
6407
|
-
session.showWidget(apolloTranscriptWidget);
|
|
6408
|
-
},
|
|
6409
|
-
});
|
|
6410
|
-
}
|
|
6411
7724
|
return menuItems;
|
|
6412
7725
|
}
|
|
6413
7726
|
function getFeatureFromLayout$2(feature, _bp, _row) {
|
|
@@ -6451,9 +7764,12 @@ function onMouseUp$3(stateModel, mousePosition) {
|
|
|
6451
7764
|
return;
|
|
6452
7765
|
}
|
|
6453
7766
|
const { featureAndGlyphUnderMouse } = mousePosition;
|
|
6454
|
-
if (featureAndGlyphUnderMouse
|
|
6455
|
-
|
|
7767
|
+
if (!featureAndGlyphUnderMouse) {
|
|
7768
|
+
return;
|
|
6456
7769
|
}
|
|
7770
|
+
const { feature } = featureAndGlyphUnderMouse;
|
|
7771
|
+
stateModel.setSelectedFeature(feature);
|
|
7772
|
+
stateModel.showFeatureDetailsWidget(feature);
|
|
6457
7773
|
}
|
|
6458
7774
|
/** @returns undefined if mouse not on the edge of this feature, otherwise 'start' or 'end' depending on which edge */
|
|
6459
7775
|
function isMouseOnFeatureEdge(mousePosition, feature, stateModel) {
|
|
@@ -6482,6 +7798,7 @@ const boxGlyph = {
|
|
|
6482
7798
|
drawDragPreview: drawDragPreview$3,
|
|
6483
7799
|
drawHover: drawHover$3,
|
|
6484
7800
|
drawTooltip: drawTooltip$3,
|
|
7801
|
+
getContextMenuItemsForFeature: getContextMenuItemsForFeature$2,
|
|
6485
7802
|
getContextMenuItems: getContextMenuItems$3,
|
|
6486
7803
|
getFeatureFromLayout: getFeatureFromLayout$2,
|
|
6487
7804
|
getRowCount: getRowCount$2,
|
|
@@ -6496,7 +7813,10 @@ let forwardFillLight$1 = null;
|
|
|
6496
7813
|
let backwardFillLight$1 = null;
|
|
6497
7814
|
let forwardFillDark$1 = null;
|
|
6498
7815
|
let backwardFillDark$1 = null;
|
|
6499
|
-
|
|
7816
|
+
const canvas$1 = globalThis.document.createElement('canvas');
|
|
7817
|
+
// @ts-expect-error getContext is undefined in the web worker
|
|
7818
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
7819
|
+
if (canvas$1?.getContext) {
|
|
6500
7820
|
for (const direction of ['forward', 'backward']) {
|
|
6501
7821
|
for (const themeMode of ['light', 'dark']) {
|
|
6502
7822
|
const canvas = document.createElement('canvas');
|
|
@@ -6909,7 +8229,7 @@ function onMouseDown$2(stateModel, currentMousePosition, event) {
|
|
|
6909
8229
|
const draggableFeature = getDraggableFeatureInfo$1(currentMousePosition, feature, stateModel);
|
|
6910
8230
|
if (draggableFeature) {
|
|
6911
8231
|
event.stopPropagation();
|
|
6912
|
-
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge);
|
|
8232
|
+
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge, true);
|
|
6913
8233
|
}
|
|
6914
8234
|
}
|
|
6915
8235
|
function onMouseMove$2(stateModel, mousePosition) {
|
|
@@ -6930,8 +8250,35 @@ function onMouseUp$2(stateModel, mousePosition) {
|
|
|
6930
8250
|
return;
|
|
6931
8251
|
}
|
|
6932
8252
|
const { featureAndGlyphUnderMouse } = mousePosition;
|
|
6933
|
-
if (featureAndGlyphUnderMouse
|
|
6934
|
-
|
|
8253
|
+
if (!featureAndGlyphUnderMouse) {
|
|
8254
|
+
return;
|
|
8255
|
+
}
|
|
8256
|
+
const { feature } = featureAndGlyphUnderMouse;
|
|
8257
|
+
stateModel.setSelectedFeature(feature);
|
|
8258
|
+
const { session } = stateModel;
|
|
8259
|
+
const { apolloDataStore } = session;
|
|
8260
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
8261
|
+
if (!featureTypeOntology) {
|
|
8262
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8263
|
+
}
|
|
8264
|
+
let containsCDSOrExon = false;
|
|
8265
|
+
for (const [, child] of feature.children ?? []) {
|
|
8266
|
+
if (featureTypeOntology.isTypeOf(child.type, 'CDS') ||
|
|
8267
|
+
featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
8268
|
+
containsCDSOrExon = true;
|
|
8269
|
+
break;
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
if ((featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
8273
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
|
|
8274
|
+
containsCDSOrExon) {
|
|
8275
|
+
stateModel.showFeatureDetailsWidget(feature, [
|
|
8276
|
+
'ApolloTranscriptDetails',
|
|
8277
|
+
'apolloTranscriptDetails',
|
|
8278
|
+
]);
|
|
8279
|
+
}
|
|
8280
|
+
else {
|
|
8281
|
+
stateModel.showFeatureDetailsWidget(feature);
|
|
6935
8282
|
}
|
|
6936
8283
|
}
|
|
6937
8284
|
function getDraggableFeatureInfo$1(mousePosition, feature, stateModel) {
|
|
@@ -6945,30 +8292,18 @@ function getDraggableFeatureInfo$1(mousePosition, feature, stateModel) {
|
|
|
6945
8292
|
featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
|
|
6946
8293
|
const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
6947
8294
|
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript');
|
|
6948
|
-
const
|
|
8295
|
+
const isCDS = featureTypeOntology.isTypeOf(feature.type, 'CDS');
|
|
6949
8296
|
if (isGene || isTranscript) {
|
|
8297
|
+
// For gene glyphs, the sizes of genes and transcripts are determined by
|
|
8298
|
+
// their child exons, so we don't make them draggable
|
|
6950
8299
|
return;
|
|
6951
8300
|
}
|
|
8301
|
+
// So now the type of feature is either CDS or exon. If an exon and CDS edge
|
|
8302
|
+
// are in the same place, we want to prioritize dragging the exon. If the
|
|
8303
|
+
// feature we're on is a CDS, let's find any exon it may overlap.
|
|
6952
8304
|
const { bp, refName, regionNumber, x } = mousePosition;
|
|
6953
8305
|
const { lgv } = stateModel;
|
|
6954
|
-
|
|
6955
|
-
const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber });
|
|
6956
|
-
const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber });
|
|
6957
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
6958
|
-
return;
|
|
6959
|
-
}
|
|
6960
|
-
const minPx = minPxInfo.offsetPx - offsetPx;
|
|
6961
|
-
const maxPx = maxPxInfo.offsetPx - offsetPx;
|
|
6962
|
-
if (Math.abs(maxPx - minPx) < 8) {
|
|
6963
|
-
return;
|
|
6964
|
-
}
|
|
6965
|
-
if (Math.abs(minPx - x) < 4) {
|
|
6966
|
-
return { feature, edge: 'min' };
|
|
6967
|
-
}
|
|
6968
|
-
if (Math.abs(maxPx - x) < 4) {
|
|
6969
|
-
return { feature, edge: 'max' };
|
|
6970
|
-
}
|
|
6971
|
-
if (isCds) {
|
|
8306
|
+
if (isCDS) {
|
|
6972
8307
|
const transcript = feature.parent;
|
|
6973
8308
|
if (!transcript?.children) {
|
|
6974
8309
|
return;
|
|
@@ -6984,39 +8319,157 @@ function getDraggableFeatureInfo$1(mousePosition, feature, stateModel) {
|
|
|
6984
8319
|
const [start, end] = intersection2(bp - 1, bp, child.min, child.max);
|
|
6985
8320
|
return start !== undefined && end !== undefined;
|
|
6986
8321
|
});
|
|
6987
|
-
if (
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
refName,
|
|
6997
|
-
coord: overlappingExon.max,
|
|
6998
|
-
regionNumber,
|
|
6999
|
-
});
|
|
7000
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
7001
|
-
return;
|
|
8322
|
+
if (overlappingExon) {
|
|
8323
|
+
// We are on an exon, are we on the edge of it?
|
|
8324
|
+
const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv);
|
|
8325
|
+
if (minMax) {
|
|
8326
|
+
const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax);
|
|
8327
|
+
if (overlappingEdge) {
|
|
8328
|
+
return overlappingEdge;
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
7002
8331
|
}
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
8332
|
+
}
|
|
8333
|
+
// End of special cases, let's see if we're on the edge of this CDS or exon
|
|
8334
|
+
const minMax = getMinAndMaxPx(feature, refName, regionNumber, lgv);
|
|
8335
|
+
if (minMax) {
|
|
8336
|
+
const overlappingEdge = getOverlappingEdge(feature, x, minMax);
|
|
8337
|
+
if (overlappingEdge) {
|
|
8338
|
+
return overlappingEdge;
|
|
7007
8339
|
}
|
|
7008
|
-
|
|
7009
|
-
|
|
8340
|
+
}
|
|
8341
|
+
return;
|
|
8342
|
+
}
|
|
8343
|
+
function isTranscriptFeature(feature, session) {
|
|
8344
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8345
|
+
if (!featureTypeOntology) {
|
|
8346
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8347
|
+
}
|
|
8348
|
+
return (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
8349
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript'));
|
|
8350
|
+
}
|
|
8351
|
+
function isExonFeature(feature, session) {
|
|
8352
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8353
|
+
if (!featureTypeOntology) {
|
|
8354
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8355
|
+
}
|
|
8356
|
+
return featureTypeOntology.isTypeOf(feature.type, 'exon');
|
|
8357
|
+
}
|
|
8358
|
+
function isCDSFeature(feature, session) {
|
|
8359
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8360
|
+
if (!featureTypeOntology) {
|
|
8361
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8362
|
+
}
|
|
8363
|
+
return featureTypeOntology.isTypeOf(feature.type, 'CDS');
|
|
8364
|
+
}
|
|
8365
|
+
function getContextMenuItems$2(display, mousePosition) {
|
|
8366
|
+
const { apolloInternetAccount: internetAccount, apolloHover, changeManager, regions, selectedFeature, session, } = display;
|
|
8367
|
+
const [region] = regions;
|
|
8368
|
+
const currentAssemblyId = display.getAssemblyId(region.assemblyName);
|
|
8369
|
+
const menuItems = [];
|
|
8370
|
+
const role = internetAccount ? internetAccount.role : 'admin';
|
|
8371
|
+
const admin = role === 'admin';
|
|
8372
|
+
if (!apolloHover) {
|
|
8373
|
+
return menuItems;
|
|
8374
|
+
}
|
|
8375
|
+
let featuresUnderClick = getFeaturesUnderClick(mousePosition);
|
|
8376
|
+
if (isCDSFeature(mousePosition.featureAndGlyphUnderMouse.feature, session)) {
|
|
8377
|
+
featuresUnderClick = getFeaturesUnderClick(mousePosition, true);
|
|
8378
|
+
}
|
|
8379
|
+
for (const feature of featuresUnderClick) {
|
|
8380
|
+
const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(display, feature);
|
|
8381
|
+
if (isExonFeature(feature, session)) {
|
|
8382
|
+
contextMenuItemsForFeature.push({
|
|
8383
|
+
label: 'Merge exons',
|
|
8384
|
+
disabled: !admin,
|
|
8385
|
+
onClick: () => {
|
|
8386
|
+
session.queueDialog((doneCallback) => [
|
|
8387
|
+
MergeExons,
|
|
8388
|
+
{
|
|
8389
|
+
session,
|
|
8390
|
+
handleClose: () => {
|
|
8391
|
+
doneCallback();
|
|
8392
|
+
},
|
|
8393
|
+
changeManager,
|
|
8394
|
+
sourceFeature: feature,
|
|
8395
|
+
sourceAssemblyId: currentAssemblyId,
|
|
8396
|
+
selectedFeature,
|
|
8397
|
+
setSelectedFeature: (feature) => {
|
|
8398
|
+
display.setSelectedFeature(feature);
|
|
8399
|
+
},
|
|
8400
|
+
},
|
|
8401
|
+
]);
|
|
8402
|
+
},
|
|
8403
|
+
}, {
|
|
8404
|
+
label: 'Split exon',
|
|
8405
|
+
disabled: !admin,
|
|
8406
|
+
onClick: () => {
|
|
8407
|
+
session.queueDialog((doneCallback) => [
|
|
8408
|
+
SplitExon,
|
|
8409
|
+
{
|
|
8410
|
+
session,
|
|
8411
|
+
handleClose: () => {
|
|
8412
|
+
doneCallback();
|
|
8413
|
+
},
|
|
8414
|
+
changeManager,
|
|
8415
|
+
sourceFeature: feature,
|
|
8416
|
+
sourceAssemblyId: currentAssemblyId,
|
|
8417
|
+
selectedFeature,
|
|
8418
|
+
setSelectedFeature: (feature) => {
|
|
8419
|
+
display.setSelectedFeature(feature);
|
|
8420
|
+
},
|
|
8421
|
+
},
|
|
8422
|
+
]);
|
|
8423
|
+
},
|
|
8424
|
+
});
|
|
7010
8425
|
}
|
|
7011
|
-
if (
|
|
7012
|
-
|
|
8426
|
+
if (isTranscriptFeature(feature, session)) {
|
|
8427
|
+
contextMenuItemsForFeature.push({
|
|
8428
|
+
label: 'Merge transcript',
|
|
8429
|
+
onClick: () => {
|
|
8430
|
+
session.queueDialog((doneCallback) => [
|
|
8431
|
+
MergeTranscripts,
|
|
8432
|
+
{
|
|
8433
|
+
session,
|
|
8434
|
+
handleClose: () => {
|
|
8435
|
+
doneCallback();
|
|
8436
|
+
},
|
|
8437
|
+
changeManager,
|
|
8438
|
+
sourceFeature: feature,
|
|
8439
|
+
sourceAssemblyId: currentAssemblyId,
|
|
8440
|
+
selectedFeature,
|
|
8441
|
+
setSelectedFeature: (feature) => {
|
|
8442
|
+
display.setSelectedFeature(feature);
|
|
8443
|
+
},
|
|
8444
|
+
},
|
|
8445
|
+
]);
|
|
8446
|
+
},
|
|
8447
|
+
});
|
|
8448
|
+
if (isSessionModelWithWidgets(session)) {
|
|
8449
|
+
contextMenuItemsForFeature.push({
|
|
8450
|
+
label: 'Open transcript details',
|
|
8451
|
+
onClick: () => {
|
|
8452
|
+
const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
|
|
8453
|
+
feature,
|
|
8454
|
+
assembly: currentAssemblyId,
|
|
8455
|
+
changeManager,
|
|
8456
|
+
refName: region.refName,
|
|
8457
|
+
});
|
|
8458
|
+
session.showWidget(apolloTranscriptWidget);
|
|
8459
|
+
},
|
|
8460
|
+
});
|
|
8461
|
+
}
|
|
7013
8462
|
}
|
|
8463
|
+
menuItems.push({
|
|
8464
|
+
label: feature.type,
|
|
8465
|
+
subMenu: contextMenuItemsForFeature,
|
|
8466
|
+
});
|
|
7014
8467
|
}
|
|
7015
|
-
return;
|
|
8468
|
+
return menuItems;
|
|
7016
8469
|
}
|
|
7017
8470
|
// False positive here, none of these functions use "this"
|
|
7018
8471
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
7019
|
-
const { drawTooltip: drawTooltip$2,
|
|
8472
|
+
const { drawTooltip: drawTooltip$2, getContextMenuItemsForFeature: getContextMenuItemsForFeature$1, onMouseLeave: onMouseLeave$2 } = boxGlyph;
|
|
7020
8473
|
/* eslint-enable @typescript-eslint/unbound-method */
|
|
7021
8474
|
const geneGlyph$1 = {
|
|
7022
8475
|
draw: draw$2,
|
|
@@ -7024,6 +8477,7 @@ const geneGlyph$1 = {
|
|
|
7024
8477
|
drawHover: drawHover$2,
|
|
7025
8478
|
drawTooltip: drawTooltip$2,
|
|
7026
8479
|
getContextMenuItems: getContextMenuItems$2,
|
|
8480
|
+
getContextMenuItemsForFeature: getContextMenuItemsForFeature$1,
|
|
7027
8481
|
getFeatureFromLayout: getFeatureFromLayout$1,
|
|
7028
8482
|
getRowCount: getRowCount$1,
|
|
7029
8483
|
getRowForFeature: getRowForFeature$1,
|
|
@@ -7071,7 +8525,7 @@ function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
|
|
|
7071
8525
|
const startPx = reversed ? minX - widthPx : minX;
|
|
7072
8526
|
const top = row * heightPx;
|
|
7073
8527
|
const rowCount = getRowCount(feature);
|
|
7074
|
-
const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
|
|
8528
|
+
const isSelected = isSelectedFeature$1(feature, apolloSelectedFeature);
|
|
7075
8529
|
const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
|
|
7076
8530
|
if (rowCount > 1) {
|
|
7077
8531
|
// draw background that encapsulates all child features
|
|
@@ -7118,15 +8572,44 @@ function getRowForFeature(feature, childFeature) {
|
|
|
7118
8572
|
}
|
|
7119
8573
|
return;
|
|
7120
8574
|
}
|
|
8575
|
+
function getContextMenuItems$1(display, mousePosition) {
|
|
8576
|
+
const { apolloHover, session } = display;
|
|
8577
|
+
const menuItems = [];
|
|
8578
|
+
if (!apolloHover) {
|
|
8579
|
+
return menuItems;
|
|
8580
|
+
}
|
|
8581
|
+
const { feature: sourceFeature } = apolloHover;
|
|
8582
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8583
|
+
if (!featureTypeOntology) {
|
|
8584
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8585
|
+
}
|
|
8586
|
+
const sourceFeatureMenuItems = boxGlyph.getContextMenuItems(display, mousePosition);
|
|
8587
|
+
menuItems.push({
|
|
8588
|
+
label: sourceFeature.type,
|
|
8589
|
+
subMenu: sourceFeatureMenuItems,
|
|
8590
|
+
});
|
|
8591
|
+
for (const relative of getFeaturesUnderClick(mousePosition)) {
|
|
8592
|
+
if (relative._id === sourceFeature._id) {
|
|
8593
|
+
continue;
|
|
8594
|
+
}
|
|
8595
|
+
const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(display, relative);
|
|
8596
|
+
menuItems.push({
|
|
8597
|
+
label: relative.type,
|
|
8598
|
+
subMenu: contextMenuItemsForFeature,
|
|
8599
|
+
});
|
|
8600
|
+
}
|
|
8601
|
+
return menuItems;
|
|
8602
|
+
}
|
|
7121
8603
|
// False positive here, none of these functions use "this"
|
|
7122
8604
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
7123
|
-
const { drawDragPreview: drawDragPreview$1, drawTooltip: drawTooltip$1,
|
|
8605
|
+
const { drawDragPreview: drawDragPreview$1, drawTooltip: drawTooltip$1, getContextMenuItemsForFeature, onMouseDown: onMouseDown$1, onMouseLeave: onMouseLeave$1, onMouseMove: onMouseMove$1, onMouseUp: onMouseUp$1, } = boxGlyph;
|
|
7124
8606
|
/* eslint-enable @typescript-eslint/unbound-method */
|
|
7125
8607
|
const genericChildGlyph = {
|
|
7126
8608
|
draw: draw$1,
|
|
7127
8609
|
drawDragPreview: drawDragPreview$1,
|
|
7128
8610
|
drawHover: drawHover$1,
|
|
7129
8611
|
drawTooltip: drawTooltip$1,
|
|
8612
|
+
getContextMenuItemsForFeature,
|
|
7130
8613
|
getContextMenuItems: getContextMenuItems$1,
|
|
7131
8614
|
getFeatureFromLayout,
|
|
7132
8615
|
getRowCount,
|
|
@@ -7413,6 +8896,27 @@ function baseModelFactory$1(_pluginManager, configSchema) {
|
|
|
7413
8896
|
setSelectedFeature(feature) {
|
|
7414
8897
|
self.session.apolloSetSelectedFeature(feature);
|
|
7415
8898
|
},
|
|
8899
|
+
showFeatureDetailsWidget(feature, customWidgetNameAndId) {
|
|
8900
|
+
const [region] = self.regions;
|
|
8901
|
+
const { assemblyName, refName } = region;
|
|
8902
|
+
const assembly = self.getAssemblyId(assemblyName);
|
|
8903
|
+
if (!assembly) {
|
|
8904
|
+
return;
|
|
8905
|
+
}
|
|
8906
|
+
const { session } = self;
|
|
8907
|
+
const { changeManager } = session.apolloDataStore;
|
|
8908
|
+
const [widgetName, widgetId] = customWidgetNameAndId ?? [
|
|
8909
|
+
'ApolloFeatureDetailsWidget',
|
|
8910
|
+
'apolloFeatureDetailsWidget',
|
|
8911
|
+
];
|
|
8912
|
+
const apolloFeatureWidget = session.addWidget(widgetName, widgetId, {
|
|
8913
|
+
feature,
|
|
8914
|
+
assembly,
|
|
8915
|
+
refName,
|
|
8916
|
+
changeManager,
|
|
8917
|
+
});
|
|
8918
|
+
session.showWidget(apolloFeatureWidget);
|
|
8919
|
+
},
|
|
7416
8920
|
afterAttach() {
|
|
7417
8921
|
addDisposer(self, autorun(() => {
|
|
7418
8922
|
if (!self.lgv.initialized || self.regionCannotBeRendered()) {
|
|
@@ -7641,6 +9145,7 @@ function renderingModelIntermediateFactory$1(pluginManager, configSchema) {
|
|
|
7641
9145
|
detailsHeight: 200,
|
|
7642
9146
|
lastRowTooltipBufferHeight: 40,
|
|
7643
9147
|
isShown: true,
|
|
9148
|
+
filteredTranscripts: types.array(types.string),
|
|
7644
9149
|
})
|
|
7645
9150
|
.volatile(() => ({
|
|
7646
9151
|
canvas: null,
|
|
@@ -8032,6 +9537,10 @@ function mouseEventsModelIntermediateFactory$1(pluginManager, configSchema) {
|
|
|
8032
9537
|
self.cursor = cursor;
|
|
8033
9538
|
}
|
|
8034
9539
|
},
|
|
9540
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
9541
|
+
updateFilteredTranscripts(forms) {
|
|
9542
|
+
return;
|
|
9543
|
+
},
|
|
8035
9544
|
}))
|
|
8036
9545
|
.actions(() => ({
|
|
8037
9546
|
// onClick(event: CanvasMouseEvent) {
|
|
@@ -8111,32 +9620,37 @@ function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
|
|
|
8111
9620
|
function mouseEventsModelFactory$1(pluginManager, configSchema) {
|
|
8112
9621
|
const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
|
|
8113
9622
|
return LinearApolloDisplayMouseEvents.views((self) => ({
|
|
8114
|
-
contextMenuItems(
|
|
9623
|
+
contextMenuItems(event) {
|
|
8115
9624
|
const { apolloHover } = self;
|
|
8116
|
-
if (!
|
|
9625
|
+
if (!apolloHover) {
|
|
8117
9626
|
return [];
|
|
8118
9627
|
}
|
|
9628
|
+
const mousePosition = self.getMousePosition(event);
|
|
8119
9629
|
const { topLevelFeature } = apolloHover;
|
|
8120
9630
|
const glyph = self.getGlyph(topLevelFeature);
|
|
8121
|
-
|
|
9631
|
+
if (isMousePositionWithFeatureAndGlyph$1(mousePosition)) {
|
|
9632
|
+
return glyph.getContextMenuItems(self, mousePosition);
|
|
9633
|
+
}
|
|
9634
|
+
return [];
|
|
8122
9635
|
},
|
|
8123
9636
|
}))
|
|
8124
9637
|
.actions((self) => ({
|
|
8125
9638
|
// explicitly pass in a feature in case it's not the same as the one in
|
|
8126
9639
|
// mousePosition (e.g. if features are drawn overlapping).
|
|
8127
|
-
startDrag(mousePosition, feature, edge) {
|
|
9640
|
+
startDrag(mousePosition, feature, edge, shrinkParent = false) {
|
|
8128
9641
|
self.apolloDragging = {
|
|
8129
9642
|
start: mousePosition,
|
|
8130
9643
|
current: mousePosition,
|
|
8131
9644
|
feature,
|
|
8132
9645
|
edge,
|
|
9646
|
+
shrinkParent,
|
|
8133
9647
|
};
|
|
8134
9648
|
},
|
|
8135
9649
|
endDrag() {
|
|
8136
9650
|
if (!self.apolloDragging) {
|
|
8137
9651
|
throw new Error('endDrag() called with no current drag in progress');
|
|
8138
9652
|
}
|
|
8139
|
-
const { current, edge, feature, start } = self.apolloDragging;
|
|
9653
|
+
const { current, edge, feature, start, shrinkParent } = self.apolloDragging;
|
|
8140
9654
|
// don't do anything if it was only dragged a tiny bit
|
|
8141
9655
|
if (Math.abs(current.x - start.x) <= 4) {
|
|
8142
9656
|
self.setDragging();
|
|
@@ -8146,33 +9660,28 @@ function mouseEventsModelFactory$1(pluginManager, configSchema) {
|
|
|
8146
9660
|
const { displayedRegions } = self.lgv;
|
|
8147
9661
|
const region = displayedRegions[start.regionNumber];
|
|
8148
9662
|
const assembly = self.getAssemblyId(region.assemblyName);
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
const oldEnd = feature.max;
|
|
8153
|
-
const newEnd = current.bp;
|
|
8154
|
-
change = new LocationEndChange({
|
|
9663
|
+
const changes = getPropagatedLocationChanges(feature, current.bp, edge, shrinkParent);
|
|
9664
|
+
const change = edge === 'max'
|
|
9665
|
+
? new LocationEndChange({
|
|
8155
9666
|
typeName: 'LocationEndChange',
|
|
8156
|
-
changedIds:
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
9667
|
+
changedIds: changes.map((c) => c.featureId),
|
|
9668
|
+
changes: changes.map((c) => ({
|
|
9669
|
+
featureId: c.featureId,
|
|
9670
|
+
oldEnd: c.oldLocation,
|
|
9671
|
+
newEnd: c.newLocation,
|
|
9672
|
+
})),
|
|
8160
9673
|
assembly,
|
|
8161
|
-
})
|
|
8162
|
-
|
|
8163
|
-
else {
|
|
8164
|
-
const featureId = feature._id;
|
|
8165
|
-
const oldStart = feature.min;
|
|
8166
|
-
const newStart = current.bp;
|
|
8167
|
-
change = new LocationStartChange({
|
|
9674
|
+
})
|
|
9675
|
+
: new LocationStartChange({
|
|
8168
9676
|
typeName: 'LocationStartChange',
|
|
8169
|
-
changedIds:
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
9677
|
+
changedIds: changes.map((c) => c.featureId),
|
|
9678
|
+
changes: changes.map((c) => ({
|
|
9679
|
+
featureId: c.featureId,
|
|
9680
|
+
oldStart: c.oldLocation,
|
|
9681
|
+
newStart: c.newLocation,
|
|
9682
|
+
})),
|
|
8173
9683
|
assembly,
|
|
8174
9684
|
});
|
|
8175
|
-
}
|
|
8176
9685
|
void self.changeManager.submit(change);
|
|
8177
9686
|
self.setDragging();
|
|
8178
9687
|
self.setCursor();
|
|
@@ -8213,6 +9722,9 @@ function mouseEventsModelFactory$1(pluginManager, configSchema) {
|
|
|
8213
9722
|
if (isMousePositionWithFeatureAndGlyph$1(mousePosition)) {
|
|
8214
9723
|
mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
|
|
8215
9724
|
}
|
|
9725
|
+
else {
|
|
9726
|
+
self.setSelectedFeature();
|
|
9727
|
+
}
|
|
8216
9728
|
if (self.apolloDragging) {
|
|
8217
9729
|
self.endDrag();
|
|
8218
9730
|
}
|
|
@@ -8262,11 +9774,46 @@ function stateModelFactory$1(pluginManager, configSchema) {
|
|
|
8262
9774
|
|
|
8263
9775
|
const configSchema = ConfigurationSchema('LinearApolloSixFrameDisplay', {}, { explicitIdentifier: 'displayId', explicitlyTyped: true });
|
|
8264
9776
|
|
|
9777
|
+
const FilterTranscripts = observer(function FilterTranscripts({ sourceFeature, filteredTranscripts, handleClose, onUpdate, }) {
|
|
9778
|
+
const allTranscripts = [];
|
|
9779
|
+
if (sourceFeature.children) {
|
|
9780
|
+
for (const [, child] of sourceFeature.children) {
|
|
9781
|
+
const childID = child.attributes
|
|
9782
|
+
.get('gff_id')
|
|
9783
|
+
?.toString();
|
|
9784
|
+
if (childID) {
|
|
9785
|
+
allTranscripts.push(childID);
|
|
9786
|
+
}
|
|
9787
|
+
}
|
|
9788
|
+
}
|
|
9789
|
+
const [excludedTranscripts, setExcludedTranscripts] = useState(filteredTranscripts);
|
|
9790
|
+
const handleChange = (value) => {
|
|
9791
|
+
const newForms = excludedTranscripts.includes(value)
|
|
9792
|
+
? excludedTranscripts.filter((form) => form !== value)
|
|
9793
|
+
: [...excludedTranscripts, value];
|
|
9794
|
+
onUpdate(newForms);
|
|
9795
|
+
setExcludedTranscripts(newForms);
|
|
9796
|
+
};
|
|
9797
|
+
return (React.createElement(Dialog, { open: true, maxWidth: false, "data-testid": "filter-transcripts-dialog", title: "Filter transcripts by ID", handleClose: handleClose },
|
|
9798
|
+
React.createElement(DialogContent, null,
|
|
9799
|
+
React.createElement(DialogContentText, null, "Select the alternate transcripts you want to display in the apollo track"),
|
|
9800
|
+
React.createElement(Grid2, { container: true, spacing: 2 },
|
|
9801
|
+
React.createElement(Grid2, { size: 8 },
|
|
9802
|
+
React.createElement(FormGroup, null, allTranscripts.map((item) => (
|
|
9803
|
+
// eslint-disable-next-line react/jsx-key
|
|
9804
|
+
React.createElement(FormControlLabel, { control: React.createElement(Checkbox, { checked: !excludedTranscripts.includes(item), onChange: () => {
|
|
9805
|
+
handleChange(item);
|
|
9806
|
+
}, inputProps: { 'aria-label': 'controlled' } }), label: item })))))))));
|
|
9807
|
+
});
|
|
9808
|
+
|
|
8265
9809
|
let forwardFillLight = null;
|
|
8266
9810
|
let backwardFillLight = null;
|
|
8267
9811
|
let forwardFillDark = null;
|
|
8268
9812
|
let backwardFillDark = null;
|
|
8269
|
-
|
|
9813
|
+
const canvas = globalThis.document.createElement('canvas');
|
|
9814
|
+
// @ts-expect-error getContext is undefined in the web worker
|
|
9815
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
9816
|
+
if (canvas?.getContext) {
|
|
8270
9817
|
for (const direction of ['forward', 'backward']) {
|
|
8271
9818
|
for (const themeMode of ['light', 'dark']) {
|
|
8272
9819
|
const canvas = document.createElement('canvas');
|
|
@@ -8317,15 +9864,35 @@ function deepSetHas(set, item) {
|
|
|
8317
9864
|
}
|
|
8318
9865
|
return false;
|
|
8319
9866
|
}
|
|
9867
|
+
function drawTextLabels(ctx, labelArray, font = '10px sans-serif') {
|
|
9868
|
+
for (let i = labelArray.length - 1; i >= 0; --i) {
|
|
9869
|
+
const label = labelArray[i];
|
|
9870
|
+
ctx.fillStyle = label.color;
|
|
9871
|
+
const labelRowX = Math.max(label.x + 1, 0);
|
|
9872
|
+
const labelRowY = label.y + label.h;
|
|
9873
|
+
const textWidth = measureText(label.text, 10);
|
|
9874
|
+
if (label.isSelected) {
|
|
9875
|
+
ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h);
|
|
9876
|
+
ctx.font = 'bold '.concat(font);
|
|
9877
|
+
}
|
|
9878
|
+
if (label.text) {
|
|
9879
|
+
ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth);
|
|
9880
|
+
ctx.font = font;
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
8320
9884
|
function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
8321
|
-
const { apolloRowHeight, lgv, session, theme, highestRow } = stateModel;
|
|
9885
|
+
const { apolloRowHeight, lgv, session, theme, highestRow, filteredTranscripts, showFeatureLabels, } = stateModel;
|
|
8322
9886
|
const { bpPerPx, displayedRegions, offsetPx } = lgv;
|
|
8323
9887
|
const displayedRegion = displayedRegions[displayedRegionIndex];
|
|
8324
9888
|
const { refName, reversed } = displayedRegion;
|
|
8325
9889
|
const rowHeight = apolloRowHeight;
|
|
8326
|
-
const exonHeight =
|
|
8327
|
-
const cdsHeight =
|
|
8328
|
-
const
|
|
9890
|
+
const exonHeight = rowHeight;
|
|
9891
|
+
const cdsHeight = rowHeight;
|
|
9892
|
+
const topLevelFeatureHeight = rowHeight;
|
|
9893
|
+
const featureLabelSpacer = showFeatureLabels ? 2 : 1;
|
|
9894
|
+
const textColor = theme?.palette.text.primary ?? 'black';
|
|
9895
|
+
const { attributes, children, min, strand } = topLevelFeature;
|
|
8329
9896
|
if (!children) {
|
|
8330
9897
|
return;
|
|
8331
9898
|
}
|
|
@@ -8335,6 +9902,7 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8335
9902
|
if (!featureTypeOntology) {
|
|
8336
9903
|
throw new Error('featureTypeOntology is undefined');
|
|
8337
9904
|
}
|
|
9905
|
+
const labelArray = [];
|
|
8338
9906
|
// Draw background for gene
|
|
8339
9907
|
const topLevelFeatureMinX = (lgv.bpToPx({
|
|
8340
9908
|
refName,
|
|
@@ -8345,16 +9913,29 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8345
9913
|
const topLevelFeatureStartPx = reversed
|
|
8346
9914
|
? topLevelFeatureMinX - topLevelFeatureWidthPx
|
|
8347
9915
|
: topLevelFeatureMinX;
|
|
8348
|
-
const topLevelRow = strand == 1 ? 3 : 4;
|
|
9916
|
+
const topLevelRow = (strand == 1 ? 3 : 4) * featureLabelSpacer;
|
|
8349
9917
|
const topLevelFeatureTop = topLevelRow * rowHeight;
|
|
8350
|
-
const topLevelFeatureHeight = Math.round(0.7 * rowHeight);
|
|
8351
9918
|
ctx.fillStyle = theme?.palette.text.primary ?? 'black';
|
|
8352
9919
|
ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
|
|
8353
|
-
ctx.fillStyle =
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
: alpha(theme?.palette.background.paper ?? '#ffffff', 0.7);
|
|
9920
|
+
ctx.fillStyle = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
|
|
9921
|
+
? alpha('rgb(0,0,0)', 0.7)
|
|
9922
|
+
: alpha(theme?.palette.background.paper ?? '#ffffff', 0.7);
|
|
8357
9923
|
ctx.fillRect(topLevelFeatureStartPx + 1, topLevelFeatureTop + 1, topLevelFeatureWidthPx - 2, topLevelFeatureHeight - 2);
|
|
9924
|
+
const isSelected = isSelectedFeature(topLevelFeature, apolloSelectedFeature);
|
|
9925
|
+
const label = {
|
|
9926
|
+
x: topLevelFeatureStartPx,
|
|
9927
|
+
y: topLevelFeatureTop,
|
|
9928
|
+
h: topLevelFeatureHeight,
|
|
9929
|
+
text: attributes.get('gff_id')?.toString(),
|
|
9930
|
+
color: textColor,
|
|
9931
|
+
isSelected,
|
|
9932
|
+
};
|
|
9933
|
+
if (isSelected) {
|
|
9934
|
+
labelArray.unshift(label);
|
|
9935
|
+
}
|
|
9936
|
+
else {
|
|
9937
|
+
labelArray.push(label);
|
|
9938
|
+
}
|
|
8358
9939
|
const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
|
|
8359
9940
|
const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
|
|
8360
9941
|
const reversal = reversed ? -1 : 1;
|
|
@@ -8378,10 +9959,16 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8378
9959
|
featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript'))) {
|
|
8379
9960
|
continue;
|
|
8380
9961
|
}
|
|
8381
|
-
const { children: childrenOfmRNA, cdsLocations
|
|
9962
|
+
const { children: childrenOfmRNA, cdsLocations } = child;
|
|
8382
9963
|
if (!childrenOfmRNA) {
|
|
8383
9964
|
continue;
|
|
8384
9965
|
}
|
|
9966
|
+
const childID = child.attributes
|
|
9967
|
+
.get('gff_id')
|
|
9968
|
+
?.toString();
|
|
9969
|
+
if (childID && filteredTranscripts.includes(childID)) {
|
|
9970
|
+
continue;
|
|
9971
|
+
}
|
|
8385
9972
|
for (const [, exon] of childrenOfmRNA) {
|
|
8386
9973
|
if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
|
|
8387
9974
|
continue;
|
|
@@ -8394,14 +9981,12 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8394
9981
|
const widthPx = exon.length / bpPerPx;
|
|
8395
9982
|
const startPx = reversed ? minX - widthPx : minX;
|
|
8396
9983
|
const exonTop = topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2;
|
|
9984
|
+
const isSelected = isSelectedFeature(exon, apolloSelectedFeature);
|
|
8397
9985
|
ctx.fillStyle = theme?.palette.text.primary ?? 'black';
|
|
8398
9986
|
ctx.fillRect(startPx, exonTop, widthPx, exonHeight);
|
|
8399
9987
|
if (widthPx > 2) {
|
|
8400
9988
|
ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2);
|
|
8401
|
-
ctx.fillStyle =
|
|
8402
|
-
apolloSelectedFeature && exon._id === apolloSelectedFeature._id
|
|
8403
|
-
? 'rgb(0,0,0)'
|
|
8404
|
-
: alpha('#f5f500', 0.6);
|
|
9989
|
+
ctx.fillStyle = isSelected ? 'rgb(0,0,0)' : alpha('#f5f500', 0.6);
|
|
8405
9990
|
ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2);
|
|
8406
9991
|
if (topFill && bottomFill) {
|
|
8407
9992
|
ctx.fillStyle = topFill;
|
|
@@ -8409,16 +9994,33 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8409
9994
|
ctx.fillStyle = bottomFill;
|
|
8410
9995
|
ctx.fillRect(startPx + 1, exonTop + 1 + (exonHeight - 2) / 2, widthPx - 2, (exonHeight - 2) / 2);
|
|
8411
9996
|
}
|
|
9997
|
+
const label = {
|
|
9998
|
+
x: startPx,
|
|
9999
|
+
y: exonTop,
|
|
10000
|
+
h: exonHeight,
|
|
10001
|
+
text: exon.attributes.get('gff_id')?.toString(),
|
|
10002
|
+
color: textColor,
|
|
10003
|
+
isSelected,
|
|
10004
|
+
};
|
|
10005
|
+
if (isSelected) {
|
|
10006
|
+
labelArray.unshift(label);
|
|
10007
|
+
}
|
|
10008
|
+
else {
|
|
10009
|
+
labelArray.push(label);
|
|
10010
|
+
}
|
|
8412
10011
|
}
|
|
8413
10012
|
}
|
|
10013
|
+
const isSelected = isSelectedFeature(child, apolloSelectedFeature?.parent);
|
|
10014
|
+
let cdsStartPx = 0;
|
|
10015
|
+
let cdsTop = 0;
|
|
8414
10016
|
for (const cdsRow of cdsLocations) {
|
|
8415
10017
|
let prevCDSTop = 0;
|
|
8416
10018
|
let prevCDSEndPx = 0;
|
|
8417
10019
|
let counter = 1;
|
|
8418
10020
|
for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
|
|
8419
10021
|
if ((apolloSelectedFeature &&
|
|
8420
|
-
|
|
8421
|
-
|
|
10022
|
+
isSelected &&
|
|
10023
|
+
featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')) ||
|
|
8422
10024
|
!deepSetHas(renderedCDS, cds)) {
|
|
8423
10025
|
const cdsWidthPx = (cds.max - cds.min) / bpPerPx;
|
|
8424
10026
|
const minX = (lgv.bpToPx({
|
|
@@ -8426,11 +10028,11 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8426
10028
|
coord: cds.min,
|
|
8427
10029
|
regionNumber: displayedRegionIndex,
|
|
8428
10030
|
})?.offsetPx ?? 0) - offsetPx;
|
|
8429
|
-
|
|
10031
|
+
cdsStartPx = reversed ? minX - cdsWidthPx : minX;
|
|
8430
10032
|
ctx.fillStyle = theme?.palette.text.primary ?? 'black';
|
|
8431
10033
|
const frame = getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase);
|
|
8432
|
-
const frameAdjust = frame < 0 ? -1 * frame + 5 : frame;
|
|
8433
|
-
|
|
10034
|
+
const frameAdjust = (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer;
|
|
10035
|
+
cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight;
|
|
8434
10036
|
ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight);
|
|
8435
10037
|
if (cdsWidthPx > 2) {
|
|
8436
10038
|
ctx.clearRect(cdsStartPx + 1, cdsTop + 1, cdsWidthPx - 2, cdsHeight - 2);
|
|
@@ -8439,8 +10041,8 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8439
10041
|
ctx.fillStyle = cdsColorCode;
|
|
8440
10042
|
ctx.fillStyle =
|
|
8441
10043
|
apolloSelectedFeature &&
|
|
8442
|
-
|
|
8443
|
-
|
|
10044
|
+
isSelected &&
|
|
10045
|
+
featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')
|
|
8444
10046
|
? 'rgb(0,0,0)'
|
|
8445
10047
|
: cdsColorCode;
|
|
8446
10048
|
ctx.fillRect(cdsStartPx + 1, cdsTop + 1, cdsWidthPx - 2, cdsHeight - 2);
|
|
@@ -8449,7 +10051,9 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8449
10051
|
// Mid-point for intron line "hat"
|
|
8450
10052
|
const midPoint = [
|
|
8451
10053
|
(cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
|
|
8452
|
-
Math.max(frame < 0
|
|
10054
|
+
Math.max(frame < 0
|
|
10055
|
+
? rowHeight * featureLabelSpacer * highestRow + 1
|
|
10056
|
+
: 1, // Avoid render ceiling
|
|
8453
10057
|
Math.min(prevCDSTop, cdsTop) - rowHeight / 2),
|
|
8454
10058
|
];
|
|
8455
10059
|
ctx.strokeStyle = 'rgb(0, 128, 128)';
|
|
@@ -8475,6 +10079,23 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8475
10079
|
}
|
|
8476
10080
|
}
|
|
8477
10081
|
}
|
|
10082
|
+
const label = {
|
|
10083
|
+
x: cdsStartPx,
|
|
10084
|
+
y: cdsTop,
|
|
10085
|
+
h: cdsHeight,
|
|
10086
|
+
text: child.attributes.get('gff_id')?.toString(),
|
|
10087
|
+
color: textColor,
|
|
10088
|
+
isSelected,
|
|
10089
|
+
};
|
|
10090
|
+
if (isSelected) {
|
|
10091
|
+
labelArray.unshift(label);
|
|
10092
|
+
}
|
|
10093
|
+
else {
|
|
10094
|
+
labelArray.push(label);
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
if (showFeatureLabels) {
|
|
10098
|
+
drawTextLabels(ctx, labelArray);
|
|
8478
10099
|
}
|
|
8479
10100
|
}
|
|
8480
10101
|
function drawDragPreview(stateModel, overlayCtx) {
|
|
@@ -8502,7 +10123,7 @@ function drawDragPreview(stateModel, overlayCtx) {
|
|
|
8502
10123
|
overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight);
|
|
8503
10124
|
}
|
|
8504
10125
|
function drawHover(stateModel, ctx) {
|
|
8505
|
-
const { apolloHover, apolloRowHeight, lgv, highestRow, session } = stateModel;
|
|
10126
|
+
const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, highestRow, session, showFeatureLabels, } = stateModel;
|
|
8506
10127
|
if (!apolloHover) {
|
|
8507
10128
|
return;
|
|
8508
10129
|
}
|
|
@@ -8515,6 +10136,12 @@ function drawHover(stateModel, ctx) {
|
|
|
8515
10136
|
if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
|
|
8516
10137
|
return;
|
|
8517
10138
|
}
|
|
10139
|
+
const featureID = feature.attributes
|
|
10140
|
+
.get('gff_id')
|
|
10141
|
+
?.toString();
|
|
10142
|
+
if (featureID && filteredTranscripts.includes(featureID)) {
|
|
10143
|
+
return;
|
|
10144
|
+
}
|
|
8518
10145
|
const position = stateModel.getFeatureLayoutPosition(feature);
|
|
8519
10146
|
if (!position) {
|
|
8520
10147
|
return;
|
|
@@ -8524,7 +10151,8 @@ function drawHover(stateModel, ctx) {
|
|
|
8524
10151
|
const displayedRegion = displayedRegions[layoutIndex];
|
|
8525
10152
|
const { refName, reversed } = displayedRegion;
|
|
8526
10153
|
const rowHeight = apolloRowHeight;
|
|
8527
|
-
const cdsHeight =
|
|
10154
|
+
const cdsHeight = rowHeight;
|
|
10155
|
+
const featureLabelSpacer = showFeatureLabels ? 2 : 1;
|
|
8528
10156
|
const { cdsLocations, strand } = feature;
|
|
8529
10157
|
for (const cdsRow of cdsLocations) {
|
|
8530
10158
|
let prevCDSTop = 0;
|
|
@@ -8540,15 +10168,15 @@ function drawHover(stateModel, ctx) {
|
|
|
8540
10168
|
})?.offsetPx ?? 0) - offsetPx;
|
|
8541
10169
|
const cdsStartPx = reversed ? minX - cdsWidthPx : minX;
|
|
8542
10170
|
const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase);
|
|
8543
|
-
const frameAdjust = frame < 0 ? -1 * frame + 5 : frame;
|
|
8544
|
-
const cdsTop = (frameAdjust -
|
|
10171
|
+
const frameAdjust = (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer;
|
|
10172
|
+
const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight;
|
|
8545
10173
|
ctx.fillStyle = 'rgba(255,0,0,0.6)';
|
|
8546
10174
|
ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight);
|
|
8547
10175
|
if (counter > 1) {
|
|
8548
10176
|
// Mid-point for intron line "hat"
|
|
8549
10177
|
const midPoint = [
|
|
8550
10178
|
(cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
|
|
8551
|
-
Math.max(frame < 0 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
|
|
10179
|
+
Math.max(frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
|
|
8552
10180
|
Math.min(prevCDSTop, cdsTop) - rowHeight / 2),
|
|
8553
10181
|
];
|
|
8554
10182
|
ctx.strokeStyle = 'rgb(0, 0, 0)';
|
|
@@ -8576,7 +10204,7 @@ function onMouseDown(stateModel, currentMousePosition, event) {
|
|
|
8576
10204
|
const draggableFeature = getDraggableFeatureInfo(currentMousePosition, cds, feature, stateModel);
|
|
8577
10205
|
if (draggableFeature) {
|
|
8578
10206
|
event.stopPropagation();
|
|
8579
|
-
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge);
|
|
10207
|
+
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge, true);
|
|
8580
10208
|
}
|
|
8581
10209
|
}
|
|
8582
10210
|
function onMouseMove(stateModel, mousePosition) {
|
|
@@ -8600,28 +10228,39 @@ function onMouseUp(stateModel, mousePosition) {
|
|
|
8600
10228
|
const { session } = stateModel;
|
|
8601
10229
|
const { apolloDataStore } = session;
|
|
8602
10230
|
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
8603
|
-
if (featureAndGlyphUnderMouse
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
10231
|
+
if (!featureAndGlyphUnderMouse) {
|
|
10232
|
+
return;
|
|
10233
|
+
}
|
|
10234
|
+
const { feature } = featureAndGlyphUnderMouse;
|
|
10235
|
+
stateModel.setSelectedFeature(feature);
|
|
10236
|
+
if (!featureTypeOntology) {
|
|
10237
|
+
throw new Error('featureTypeOntology is undefined');
|
|
10238
|
+
}
|
|
10239
|
+
let containsCDSOrExon = false;
|
|
10240
|
+
for (const [, child] of feature.children ?? []) {
|
|
10241
|
+
if (featureTypeOntology.isTypeOf(child.type, 'CDS') ||
|
|
10242
|
+
featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
10243
|
+
containsCDSOrExon = true;
|
|
10244
|
+
break;
|
|
8617
10245
|
}
|
|
8618
10246
|
}
|
|
8619
|
-
|
|
8620
|
-
|
|
10247
|
+
if ((featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
10248
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
|
|
10249
|
+
containsCDSOrExon) {
|
|
10250
|
+
stateModel.showFeatureDetailsWidget(feature, [
|
|
10251
|
+
'ApolloTranscriptDetails',
|
|
10252
|
+
'apolloTranscriptDetails',
|
|
10253
|
+
]);
|
|
10254
|
+
}
|
|
10255
|
+
else {
|
|
10256
|
+
stateModel.showFeatureDetailsWidget(feature);
|
|
8621
10257
|
}
|
|
8622
10258
|
}
|
|
10259
|
+
function isSelectedFeature(feature, selectedFeature) {
|
|
10260
|
+
return Boolean(selectedFeature && feature._id === selectedFeature._id);
|
|
10261
|
+
}
|
|
8623
10262
|
function getDraggableFeatureInfo(mousePosition, cds, feature, stateModel) {
|
|
8624
|
-
const { session } = stateModel;
|
|
10263
|
+
const { filteredTranscripts, session } = stateModel;
|
|
8625
10264
|
const { apolloDataStore } = session;
|
|
8626
10265
|
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
8627
10266
|
if (!featureTypeOntology) {
|
|
@@ -8631,67 +10270,64 @@ function getDraggableFeatureInfo(mousePosition, cds, feature, stateModel) {
|
|
|
8631
10270
|
if (cds === null) {
|
|
8632
10271
|
return;
|
|
8633
10272
|
}
|
|
8634
|
-
const
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
const maxPxInfo = lgv.bpToPx({ refName, coord: cds.max, regionNumber });
|
|
8639
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
8640
|
-
return;
|
|
8641
|
-
}
|
|
8642
|
-
const minPx = minPxInfo.offsetPx - offsetPx;
|
|
8643
|
-
const maxPx = maxPxInfo.offsetPx - offsetPx;
|
|
8644
|
-
if (Math.abs(maxPx - minPx) < 8) {
|
|
10273
|
+
const featureID = feature.attributes
|
|
10274
|
+
.get('gff_id')
|
|
10275
|
+
?.toString();
|
|
10276
|
+
if (featureID && filteredTranscripts.includes(featureID)) {
|
|
8645
10277
|
return;
|
|
8646
10278
|
}
|
|
10279
|
+
const { bp, refName, regionNumber, x } = mousePosition;
|
|
10280
|
+
const { lgv } = stateModel;
|
|
8647
10281
|
if (isTranscript) {
|
|
8648
10282
|
const transcript = feature;
|
|
8649
10283
|
if (!transcript.children) {
|
|
8650
10284
|
return;
|
|
8651
10285
|
}
|
|
8652
10286
|
const exonChildren = [];
|
|
10287
|
+
const cdsChildren = [];
|
|
8653
10288
|
for (const child of transcript.children.values()) {
|
|
8654
10289
|
const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon');
|
|
10290
|
+
const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS');
|
|
8655
10291
|
if (childIsExon) {
|
|
8656
10292
|
exonChildren.push(child);
|
|
8657
10293
|
}
|
|
10294
|
+
else if (childIsCDS) {
|
|
10295
|
+
cdsChildren.push(child);
|
|
10296
|
+
}
|
|
8658
10297
|
}
|
|
8659
10298
|
const overlappingExon = exonChildren.find((child) => {
|
|
8660
10299
|
const [start, end] = intersection2(bp, bp + 1, child.min, child.max);
|
|
8661
10300
|
return start !== undefined && end !== undefined;
|
|
8662
10301
|
});
|
|
8663
|
-
if (
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
}
|
|
8687
|
-
if (Math.abs(maxPx - x) < 4) {
|
|
8688
|
-
return { feature: overlappingExon, edge: 'max' };
|
|
10302
|
+
if (overlappingExon) {
|
|
10303
|
+
// We are on an exon, are we on the edge of it?
|
|
10304
|
+
const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv);
|
|
10305
|
+
if (minMax) {
|
|
10306
|
+
const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax);
|
|
10307
|
+
if (overlappingEdge) {
|
|
10308
|
+
return overlappingEdge;
|
|
10309
|
+
}
|
|
10310
|
+
}
|
|
10311
|
+
}
|
|
10312
|
+
// End of special cases, let's see if we're on the edge of this CDS or exon
|
|
10313
|
+
const minMax = getMinAndMaxPx(cds, refName, regionNumber, lgv);
|
|
10314
|
+
if (minMax) {
|
|
10315
|
+
const overlappingCDS = cdsChildren.find((child) => {
|
|
10316
|
+
const [start, end] = intersection2(bp, bp + 1, child.min, child.max);
|
|
10317
|
+
return start !== undefined && end !== undefined;
|
|
10318
|
+
});
|
|
10319
|
+
if (overlappingCDS) {
|
|
10320
|
+
const overlappingEdge = getOverlappingEdge(overlappingCDS, x, minMax);
|
|
10321
|
+
if (overlappingEdge) {
|
|
10322
|
+
return overlappingEdge;
|
|
10323
|
+
}
|
|
10324
|
+
}
|
|
8689
10325
|
}
|
|
8690
10326
|
}
|
|
8691
10327
|
return;
|
|
8692
10328
|
}
|
|
8693
10329
|
function drawTooltip(display, context) {
|
|
8694
|
-
const { apolloHover, apolloRowHeight, lgv, theme } = display;
|
|
10330
|
+
const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, theme } = display;
|
|
8695
10331
|
if (!apolloHover) {
|
|
8696
10332
|
return;
|
|
8697
10333
|
}
|
|
@@ -8703,6 +10339,12 @@ function drawTooltip(display, context) {
|
|
|
8703
10339
|
if (!position) {
|
|
8704
10340
|
return;
|
|
8705
10341
|
}
|
|
10342
|
+
const featureID = feature.attributes
|
|
10343
|
+
.get('gff_id')
|
|
10344
|
+
?.toString();
|
|
10345
|
+
if (featureID && filteredTranscripts.includes(featureID)) {
|
|
10346
|
+
return;
|
|
10347
|
+
}
|
|
8706
10348
|
const { layoutIndex } = position;
|
|
8707
10349
|
const { bpPerPx, displayedRegions, offsetPx } = lgv;
|
|
8708
10350
|
const displayedRegion = displayedRegions[layoutIndex];
|
|
@@ -8754,7 +10396,7 @@ function drawTooltip(display, context) {
|
|
|
8754
10396
|
context.fillText(location, startPx + 2, textTop);
|
|
8755
10397
|
}
|
|
8756
10398
|
function getContextMenuItems(display) {
|
|
8757
|
-
const { apolloHover, apolloInternetAccount: internetAccount, changeManager, regions, selectedFeature, session, } = display;
|
|
10399
|
+
const { apolloHover, apolloInternetAccount: internetAccount, changeManager, filteredTranscripts, regions, selectedFeature, session, } = display;
|
|
8758
10400
|
const menuItems = [];
|
|
8759
10401
|
if (!apolloHover) {
|
|
8760
10402
|
return menuItems;
|
|
@@ -8822,33 +10464,28 @@ function getContextMenuItems(display) {
|
|
|
8822
10464
|
},
|
|
8823
10465
|
]);
|
|
8824
10466
|
},
|
|
8825
|
-
}, {
|
|
8826
|
-
label: 'Edit feature details',
|
|
8827
|
-
onClick: () => {
|
|
8828
|
-
const apolloFeatureWidget = session.addWidget('ApolloFeatureDetailsWidget', 'apolloFeatureDetailsWidget', {
|
|
8829
|
-
feature: sourceFeature,
|
|
8830
|
-
assembly: currentAssemblyId,
|
|
8831
|
-
refName: region.refName,
|
|
8832
|
-
});
|
|
8833
|
-
session.showWidget(apolloFeatureWidget);
|
|
8834
|
-
},
|
|
8835
10467
|
});
|
|
8836
10468
|
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8837
10469
|
if (!featureTypeOntology) {
|
|
8838
10470
|
throw new Error('featureTypeOntology is undefined');
|
|
8839
10471
|
}
|
|
8840
|
-
if (featureTypeOntology.isTypeOf(sourceFeature.type, '
|
|
8841
|
-
isSessionModelWithWidgets(session)) {
|
|
10472
|
+
if (featureTypeOntology.isTypeOf(sourceFeature.type, 'gene')) {
|
|
8842
10473
|
menuItems.push({
|
|
8843
|
-
label: '
|
|
10474
|
+
label: 'Filter alternate transcripts',
|
|
8844
10475
|
onClick: () => {
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
10476
|
+
session.queueDialog((doneCallback) => [
|
|
10477
|
+
FilterTranscripts,
|
|
10478
|
+
{
|
|
10479
|
+
handleClose: () => {
|
|
10480
|
+
doneCallback();
|
|
10481
|
+
},
|
|
10482
|
+
sourceFeature,
|
|
10483
|
+
filteredTranscripts: getSnapshot(filteredTranscripts),
|
|
10484
|
+
onUpdate: (forms) => {
|
|
10485
|
+
display.updateFilteredTranscripts(forms);
|
|
10486
|
+
},
|
|
10487
|
+
},
|
|
10488
|
+
]);
|
|
8852
10489
|
},
|
|
8853
10490
|
});
|
|
8854
10491
|
}
|
|
@@ -8877,6 +10514,7 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
8877
10514
|
configuration: ConfigurationReference(configSchema),
|
|
8878
10515
|
graphical: true,
|
|
8879
10516
|
table: false,
|
|
10517
|
+
showFeatureLabels: true,
|
|
8880
10518
|
heightPreConfig: types.maybe(types.refinement('displayHeight', types.number, (n) => n >= minDisplayHeight)),
|
|
8881
10519
|
filteredFeatureTypes: types.array(types.string),
|
|
8882
10520
|
})
|
|
@@ -8993,6 +10631,9 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
8993
10631
|
self.graphical = true;
|
|
8994
10632
|
self.table = true;
|
|
8995
10633
|
},
|
|
10634
|
+
toggleShowFeatureLabels() {
|
|
10635
|
+
self.showFeatureLabels = !self.showFeatureLabels;
|
|
10636
|
+
},
|
|
8996
10637
|
updateFilteredFeatureTypes(types) {
|
|
8997
10638
|
self.filteredFeatureTypes = cast(types);
|
|
8998
10639
|
},
|
|
@@ -9001,7 +10642,7 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9001
10642
|
const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self;
|
|
9002
10643
|
return {
|
|
9003
10644
|
trackMenuItems() {
|
|
9004
|
-
const { graphical, table } = self;
|
|
10645
|
+
const { graphical, table, showFeatureLabels } = self;
|
|
9005
10646
|
return [
|
|
9006
10647
|
...superTrackMenuItems(),
|
|
9007
10648
|
{
|
|
@@ -9032,6 +10673,14 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9032
10673
|
self.showGraphicalAndTable();
|
|
9033
10674
|
},
|
|
9034
10675
|
},
|
|
10676
|
+
{
|
|
10677
|
+
label: 'Feature Labels',
|
|
10678
|
+
type: 'checkbox',
|
|
10679
|
+
checked: showFeatureLabels,
|
|
10680
|
+
onClick: () => {
|
|
10681
|
+
self.toggleShowFeatureLabels();
|
|
10682
|
+
},
|
|
10683
|
+
},
|
|
9035
10684
|
],
|
|
9036
10685
|
},
|
|
9037
10686
|
{
|
|
@@ -9061,6 +10710,27 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9061
10710
|
setSelectedFeature(feature) {
|
|
9062
10711
|
self.session.apolloSetSelectedFeature(feature);
|
|
9063
10712
|
},
|
|
10713
|
+
showFeatureDetailsWidget(feature, customWidgetNameAndId) {
|
|
10714
|
+
const [region] = self.regions;
|
|
10715
|
+
const { assemblyName, refName } = region;
|
|
10716
|
+
const assembly = self.getAssemblyId(assemblyName);
|
|
10717
|
+
if (!assembly) {
|
|
10718
|
+
return;
|
|
10719
|
+
}
|
|
10720
|
+
const { session } = self;
|
|
10721
|
+
const { changeManager } = session.apolloDataStore;
|
|
10722
|
+
const [widgetName, widgetId] = customWidgetNameAndId ?? [
|
|
10723
|
+
'ApolloFeatureDetailsWidget',
|
|
10724
|
+
'apolloFeatureDetailsWidget',
|
|
10725
|
+
];
|
|
10726
|
+
const apolloFeatureWidget = session.addWidget(widgetName, widgetId, {
|
|
10727
|
+
feature,
|
|
10728
|
+
assembly,
|
|
10729
|
+
refName,
|
|
10730
|
+
changeManager,
|
|
10731
|
+
});
|
|
10732
|
+
session.showWidget(apolloFeatureWidget);
|
|
10733
|
+
},
|
|
9064
10734
|
afterAttach() {
|
|
9065
10735
|
addDisposer(self, autorun(() => {
|
|
9066
10736
|
if (!self.lgv.initialized || self.regionCannotBeRendered()) {
|
|
@@ -9121,6 +10791,9 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9121
10791
|
getGlyph(_feature) {
|
|
9122
10792
|
return geneGlyph;
|
|
9123
10793
|
},
|
|
10794
|
+
featureLabelSpacer(elem) {
|
|
10795
|
+
return self.showFeatureLabels ? elem * 2 - 1 : elem;
|
|
10796
|
+
},
|
|
9124
10797
|
}))
|
|
9125
10798
|
.actions((self) => ({
|
|
9126
10799
|
addSeenFeature(feature) {
|
|
@@ -9129,6 +10802,11 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9129
10802
|
deleteSeenFeature(featureId) {
|
|
9130
10803
|
self.seenFeatures.delete(featureId);
|
|
9131
10804
|
},
|
|
10805
|
+
}))
|
|
10806
|
+
.views((self) => ({
|
|
10807
|
+
get geneTrackRowNums() {
|
|
10808
|
+
return [4, 5].map((elem) => self.featureLabelSpacer(elem));
|
|
10809
|
+
},
|
|
9132
10810
|
}))
|
|
9133
10811
|
.views((self) => ({
|
|
9134
10812
|
get featureLayouts() {
|
|
@@ -9155,7 +10833,9 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9155
10833
|
throw new Error('featureTypeOntology is undefined');
|
|
9156
10834
|
}
|
|
9157
10835
|
if (feature.looksLikeGene) {
|
|
9158
|
-
const rowNum = feature.strand == 1
|
|
10836
|
+
const rowNum = feature.strand == 1
|
|
10837
|
+
? self.geneTrackRowNums[0]
|
|
10838
|
+
: self.geneTrackRowNums[1];
|
|
9159
10839
|
if (!featureLayout.get(rowNum)) {
|
|
9160
10840
|
featureLayout.set(rowNum, []);
|
|
9161
10841
|
}
|
|
@@ -9173,7 +10853,9 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9173
10853
|
if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
|
|
9174
10854
|
continue;
|
|
9175
10855
|
}
|
|
9176
|
-
const rowNum = exon.strand == 1
|
|
10856
|
+
const rowNum = exon.strand == 1
|
|
10857
|
+
? self.geneTrackRowNums[0]
|
|
10858
|
+
: self.geneTrackRowNums[1];
|
|
9177
10859
|
const layoutRow = featureLayout.get(rowNum);
|
|
9178
10860
|
layoutRow?.push({ rowNum, feature: exon, cds: null });
|
|
9179
10861
|
}
|
|
@@ -9181,7 +10863,7 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9181
10863
|
for (const cdsRow of cdsLocations) {
|
|
9182
10864
|
for (const cds of cdsRow) {
|
|
9183
10865
|
let rowNum = getFrame(cds.min, cds.max, strand ?? 1, cds.phase);
|
|
9184
|
-
rowNum = rowNum < 0 ? -1 * rowNum + 5 : rowNum;
|
|
10866
|
+
rowNum = self.featureLabelSpacer(rowNum < 0 ? -1 * rowNum + 5 : rowNum);
|
|
9185
10867
|
if (!featureLayout.get(rowNum)) {
|
|
9186
10868
|
featureLayout.set(rowNum, []);
|
|
9187
10869
|
}
|
|
@@ -9257,6 +10939,7 @@ function renderingModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9257
10939
|
detailsHeight: 200,
|
|
9258
10940
|
lastRowTooltipBufferHeight: 80,
|
|
9259
10941
|
isShown: true,
|
|
10942
|
+
filteredTranscripts: types.array(types.string),
|
|
9260
10943
|
})
|
|
9261
10944
|
.volatile(() => ({
|
|
9262
10945
|
canvas: null,
|
|
@@ -9266,7 +10949,8 @@ function renderingModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9266
10949
|
}))
|
|
9267
10950
|
.views((self) => ({
|
|
9268
10951
|
get featuresHeight() {
|
|
9269
|
-
|
|
10952
|
+
const featureLabelSpacer = self.showFeatureLabels ? 2 : 1;
|
|
10953
|
+
return (featureLabelSpacer * ((self.highestRow + 1) * self.apolloRowHeight) +
|
|
9270
10954
|
self.lastRowTooltipBufferHeight);
|
|
9271
10955
|
},
|
|
9272
10956
|
}))
|
|
@@ -9404,7 +11088,7 @@ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9404
11088
|
return mousePosition;
|
|
9405
11089
|
}
|
|
9406
11090
|
let foundFeature;
|
|
9407
|
-
if (
|
|
11091
|
+
if (self.geneTrackRowNums.includes(row)) {
|
|
9408
11092
|
foundFeature = layoutRow.find((f) => f.feature.type == 'exon' &&
|
|
9409
11093
|
bp >= f.feature.min &&
|
|
9410
11094
|
bp <= f.feature.max);
|
|
@@ -9413,7 +11097,14 @@ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9413
11097
|
}
|
|
9414
11098
|
}
|
|
9415
11099
|
else {
|
|
9416
|
-
foundFeature = layoutRow.find((f) =>
|
|
11100
|
+
foundFeature = layoutRow.find((f) => {
|
|
11101
|
+
const featureID = f.feature.attributes.get('gff_id')?.toString();
|
|
11102
|
+
return (f.cds != null &&
|
|
11103
|
+
bp >= f.cds.min &&
|
|
11104
|
+
bp <= f.cds.max &&
|
|
11105
|
+
(featureID === undefined ||
|
|
11106
|
+
!self.filteredTranscripts.includes(featureID)));
|
|
11107
|
+
});
|
|
9417
11108
|
}
|
|
9418
11109
|
if (!foundFeature) {
|
|
9419
11110
|
return mousePosition;
|
|
@@ -9448,6 +11139,9 @@ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9448
11139
|
self.cursor = cursor;
|
|
9449
11140
|
}
|
|
9450
11141
|
},
|
|
11142
|
+
updateFilteredTranscripts(forms) {
|
|
11143
|
+
self.filteredTranscripts = cast(forms);
|
|
11144
|
+
},
|
|
9451
11145
|
}))
|
|
9452
11146
|
.actions(() => ({
|
|
9453
11147
|
// onClick(event: CanvasMouseEvent) {
|
|
@@ -9472,19 +11166,20 @@ function mouseEventsModelFactory(pluginManager, configSchema) {
|
|
|
9472
11166
|
.actions((self) => ({
|
|
9473
11167
|
// explicitly pass in a feature in case it's not the same as the one in
|
|
9474
11168
|
// mousePosition (e.g. if features are drawn overlapping).
|
|
9475
|
-
startDrag(mousePosition, feature, edge) {
|
|
11169
|
+
startDrag(mousePosition, feature, edge, shrinkParent = false) {
|
|
9476
11170
|
self.apolloDragging = {
|
|
9477
11171
|
start: mousePosition,
|
|
9478
11172
|
current: mousePosition,
|
|
9479
11173
|
feature,
|
|
9480
11174
|
edge,
|
|
11175
|
+
shrinkParent,
|
|
9481
11176
|
};
|
|
9482
11177
|
},
|
|
9483
11178
|
endDrag() {
|
|
9484
11179
|
if (!self.apolloDragging) {
|
|
9485
11180
|
throw new Error('endDrag() called with no current drag in progress');
|
|
9486
11181
|
}
|
|
9487
|
-
const { current, edge, feature, start } = self.apolloDragging;
|
|
11182
|
+
const { current, edge, feature, start, shrinkParent } = self.apolloDragging;
|
|
9488
11183
|
// don't do anything if it was only dragged a tiny bit
|
|
9489
11184
|
if (Math.abs(current.x - start.x) <= 4) {
|
|
9490
11185
|
self.setDragging();
|
|
@@ -9494,33 +11189,28 @@ function mouseEventsModelFactory(pluginManager, configSchema) {
|
|
|
9494
11189
|
const { displayedRegions } = self.lgv;
|
|
9495
11190
|
const region = displayedRegions[start.regionNumber];
|
|
9496
11191
|
const assembly = self.getAssemblyId(region.assemblyName);
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
const oldEnd = feature.max;
|
|
9501
|
-
const newEnd = current.bp;
|
|
9502
|
-
change = new LocationEndChange({
|
|
11192
|
+
const changes = getPropagatedLocationChanges(feature, current.bp, edge, shrinkParent);
|
|
11193
|
+
const change = edge === 'max'
|
|
11194
|
+
? new LocationEndChange({
|
|
9503
11195
|
typeName: 'LocationEndChange',
|
|
9504
|
-
changedIds:
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
11196
|
+
changedIds: changes.map((c) => c.featureId),
|
|
11197
|
+
changes: changes.map((c) => ({
|
|
11198
|
+
featureId: c.featureId,
|
|
11199
|
+
oldEnd: c.oldLocation,
|
|
11200
|
+
newEnd: c.newLocation,
|
|
11201
|
+
})),
|
|
9508
11202
|
assembly,
|
|
9509
|
-
})
|
|
9510
|
-
|
|
9511
|
-
else {
|
|
9512
|
-
const featureId = feature._id;
|
|
9513
|
-
const oldStart = feature.min;
|
|
9514
|
-
const newStart = current.bp;
|
|
9515
|
-
change = new LocationStartChange({
|
|
11203
|
+
})
|
|
11204
|
+
: new LocationStartChange({
|
|
9516
11205
|
typeName: 'LocationStartChange',
|
|
9517
|
-
changedIds:
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
11206
|
+
changedIds: changes.map((c) => c.featureId),
|
|
11207
|
+
changes: changes.map((c) => ({
|
|
11208
|
+
featureId: c.featureId,
|
|
11209
|
+
oldStart: c.oldLocation,
|
|
11210
|
+
newStart: c.newLocation,
|
|
11211
|
+
})),
|
|
9521
11212
|
assembly,
|
|
9522
11213
|
});
|
|
9523
|
-
}
|
|
9524
11214
|
void self.changeManager.submit(change);
|
|
9525
11215
|
self.setDragging();
|
|
9526
11216
|
self.setCursor();
|
|
@@ -9561,6 +11251,9 @@ function mouseEventsModelFactory(pluginManager, configSchema) {
|
|
|
9561
11251
|
if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
|
|
9562
11252
|
mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
|
|
9563
11253
|
}
|
|
11254
|
+
else {
|
|
11255
|
+
self.setSelectedFeature();
|
|
11256
|
+
}
|
|
9564
11257
|
if (self.apolloDragging) {
|
|
9565
11258
|
self.endDrag();
|
|
9566
11259
|
}
|
|
@@ -9611,185 +11304,16 @@ function stateModelFactory(pluginManager, configSchema) {
|
|
|
9611
11304
|
const ApolloPluginConfigurationSchema = ConfigurationSchema('ApolloPlugin', {
|
|
9612
11305
|
ontologies: types.array(OntologyRecordConfiguration),
|
|
9613
11306
|
featureTypeOntologyName: {
|
|
9614
|
-
description: 'Name of the feature type ontology',
|
|
9615
|
-
type: 'string',
|
|
9616
|
-
defaultValue: 'Sequence Ontology',
|
|
9617
|
-
},
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
}
|
|
9625
|
-
function annotationFromPileup(pluggableElement) {
|
|
9626
|
-
if (pluggableElement.name !== 'LinearPileupDisplay') {
|
|
9627
|
-
return pluggableElement;
|
|
9628
|
-
}
|
|
9629
|
-
const { stateModel } = pluggableElement;
|
|
9630
|
-
const newStateModel = stateModel
|
|
9631
|
-
.views((self) => ({
|
|
9632
|
-
getFirstRegion() {
|
|
9633
|
-
const lgv = getContainingView(self);
|
|
9634
|
-
return lgv.dynamicBlocks.contentBlocks[0];
|
|
9635
|
-
},
|
|
9636
|
-
getAssembly() {
|
|
9637
|
-
const firstRegion = self.getFirstRegion();
|
|
9638
|
-
const session = getSession(self);
|
|
9639
|
-
const { assemblyManager } = session;
|
|
9640
|
-
const { assemblyName } = firstRegion;
|
|
9641
|
-
const assembly = assemblyManager.get(assemblyName);
|
|
9642
|
-
if (!assembly) {
|
|
9643
|
-
throw new Error(`Could not find assembly named ${assemblyName}`);
|
|
9644
|
-
}
|
|
9645
|
-
return assembly;
|
|
9646
|
-
},
|
|
9647
|
-
getRefSeqId(assembly) {
|
|
9648
|
-
const firstRegion = self.getFirstRegion();
|
|
9649
|
-
const { refName } = firstRegion;
|
|
9650
|
-
const { refNameAliases } = assembly;
|
|
9651
|
-
if (!refNameAliases) {
|
|
9652
|
-
throw new Error(`Could not find aliases for ${assembly.name}`);
|
|
9653
|
-
}
|
|
9654
|
-
const newRefNames = [...Object.entries(refNameAliases)]
|
|
9655
|
-
.filter(([id, refName]) => id !== refName)
|
|
9656
|
-
.map(([id, refName]) => ({
|
|
9657
|
-
_id: id,
|
|
9658
|
-
name: refName,
|
|
9659
|
-
}));
|
|
9660
|
-
const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
|
|
9661
|
-
if (!refSeqId) {
|
|
9662
|
-
throw new Error(`Could not find refSeqId named ${refName}`);
|
|
9663
|
-
}
|
|
9664
|
-
return refSeqId;
|
|
9665
|
-
},
|
|
9666
|
-
createFeature() {
|
|
9667
|
-
const feature = self.contextMenuFeature;
|
|
9668
|
-
const assembly = self.getAssembly();
|
|
9669
|
-
const refSeqId = self.getRefSeqId(assembly);
|
|
9670
|
-
const cigarData = feature.get('CIGAR');
|
|
9671
|
-
const ops = parseCigar(cigarData);
|
|
9672
|
-
let currOffset = 0;
|
|
9673
|
-
const start = feature.get('start');
|
|
9674
|
-
let openStart;
|
|
9675
|
-
const exons = [];
|
|
9676
|
-
for (const [op, len] of ops) {
|
|
9677
|
-
// open or continue open
|
|
9678
|
-
if (op === 'M' || op === '=') {
|
|
9679
|
-
// if it was closed, then open with start, strand, type
|
|
9680
|
-
if (openStart === undefined) {
|
|
9681
|
-
// add subfeature
|
|
9682
|
-
openStart = currOffset + start;
|
|
9683
|
-
}
|
|
9684
|
-
}
|
|
9685
|
-
else if (op === 'N' && openStart !== undefined) {
|
|
9686
|
-
// if it was open, then close and add the subfeature
|
|
9687
|
-
exons.push({
|
|
9688
|
-
start: openStart,
|
|
9689
|
-
end: currOffset + openStart,
|
|
9690
|
-
});
|
|
9691
|
-
openStart = undefined;
|
|
9692
|
-
}
|
|
9693
|
-
if (op !== 'I') {
|
|
9694
|
-
// we ignore insertions when calculating potential exon length
|
|
9695
|
-
currOffset += len;
|
|
9696
|
-
}
|
|
9697
|
-
}
|
|
9698
|
-
// if we are still open, then close with the final length and add subfeature
|
|
9699
|
-
if (openStart !== undefined) {
|
|
9700
|
-
exons.push({
|
|
9701
|
-
start: openStart,
|
|
9702
|
-
end: currOffset + start,
|
|
9703
|
-
});
|
|
9704
|
-
}
|
|
9705
|
-
const newFeature = {
|
|
9706
|
-
_id: ObjectID().toHexString(),
|
|
9707
|
-
refSeq: refSeqId,
|
|
9708
|
-
min: feature.get('start'),
|
|
9709
|
-
max: feature.get('end'),
|
|
9710
|
-
type: 'mRNA',
|
|
9711
|
-
strand: feature.get('strand'),
|
|
9712
|
-
};
|
|
9713
|
-
if (exons.length === 0) {
|
|
9714
|
-
return newFeature;
|
|
9715
|
-
}
|
|
9716
|
-
const children = {};
|
|
9717
|
-
newFeature.children = children;
|
|
9718
|
-
const [firstExon] = exons;
|
|
9719
|
-
const cdsFeature = {
|
|
9720
|
-
_id: ObjectID().toHexString(),
|
|
9721
|
-
refSeq: refSeqId,
|
|
9722
|
-
min: firstExon.start,
|
|
9723
|
-
max: firstExon.end,
|
|
9724
|
-
type: 'CDS',
|
|
9725
|
-
strand: feature.get('strand'),
|
|
9726
|
-
};
|
|
9727
|
-
newFeature.children[cdsFeature._id] = cdsFeature;
|
|
9728
|
-
if (exons.length === 1) {
|
|
9729
|
-
const exon = {
|
|
9730
|
-
_id: ObjectID().toHexString(),
|
|
9731
|
-
refSeq: refSeqId,
|
|
9732
|
-
min: firstExon.start,
|
|
9733
|
-
max: firstExon.end,
|
|
9734
|
-
type: 'exon',
|
|
9735
|
-
strand: feature.get('strand'),
|
|
9736
|
-
};
|
|
9737
|
-
newFeature.children[exon._id] = exon;
|
|
9738
|
-
return newFeature;
|
|
9739
|
-
}
|
|
9740
|
-
for (const exon of exons) {
|
|
9741
|
-
cdsFeature.min = Math.min(cdsFeature.min, exon.start);
|
|
9742
|
-
cdsFeature.max = Math.max(cdsFeature.max, exon.end);
|
|
9743
|
-
const { end, start } = exon;
|
|
9744
|
-
const newExon = {
|
|
9745
|
-
_id: ObjectID().toHexString(),
|
|
9746
|
-
refSeq: refSeqId,
|
|
9747
|
-
min: start,
|
|
9748
|
-
max: end,
|
|
9749
|
-
type: 'exon',
|
|
9750
|
-
strand: feature.get('strand'),
|
|
9751
|
-
};
|
|
9752
|
-
newFeature.children[newExon._id] = newExon;
|
|
9753
|
-
}
|
|
9754
|
-
return newFeature;
|
|
9755
|
-
},
|
|
9756
|
-
async onPileupFeatureContext() {
|
|
9757
|
-
const newFeature = self.createFeature();
|
|
9758
|
-
const assembly = self.getAssembly();
|
|
9759
|
-
const assemblyId = assembly.name;
|
|
9760
|
-
const change = new AddFeatureChange({
|
|
9761
|
-
changedIds: [newFeature._id],
|
|
9762
|
-
typeName: 'AddFeatureChange',
|
|
9763
|
-
assembly: assemblyId,
|
|
9764
|
-
addedFeature: newFeature,
|
|
9765
|
-
});
|
|
9766
|
-
const session = getSession(self);
|
|
9767
|
-
await session.apolloDataStore.changeManager.submit(change);
|
|
9768
|
-
session.notify('Annotation added successfully', 'success');
|
|
9769
|
-
},
|
|
9770
|
-
}))
|
|
9771
|
-
.views((self) => {
|
|
9772
|
-
const superContextMenuItems = self.contextMenuItems;
|
|
9773
|
-
return {
|
|
9774
|
-
contextMenuItems() {
|
|
9775
|
-
const feature = self.contextMenuFeature;
|
|
9776
|
-
if (!feature) {
|
|
9777
|
-
return superContextMenuItems();
|
|
9778
|
-
}
|
|
9779
|
-
return [
|
|
9780
|
-
...superContextMenuItems(),
|
|
9781
|
-
{
|
|
9782
|
-
label: 'Create Apollo annotation',
|
|
9783
|
-
icon: AddIcon,
|
|
9784
|
-
onClick: self.onPileupFeatureContext,
|
|
9785
|
-
},
|
|
9786
|
-
];
|
|
9787
|
-
},
|
|
9788
|
-
};
|
|
9789
|
-
});
|
|
9790
|
-
pluggableElement.stateModel = newStateModel;
|
|
9791
|
-
return pluggableElement;
|
|
9792
|
-
}
|
|
11307
|
+
description: 'Name of the feature type ontology',
|
|
11308
|
+
type: 'string',
|
|
11309
|
+
defaultValue: 'Sequence Ontology',
|
|
11310
|
+
},
|
|
11311
|
+
hasRole: {
|
|
11312
|
+
description: 'Flag used internally by jbrowse-plugin-apollo',
|
|
11313
|
+
type: 'boolean',
|
|
11314
|
+
defaultValue: false,
|
|
11315
|
+
},
|
|
11316
|
+
});
|
|
9793
11317
|
|
|
9794
11318
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
9795
11319
|
const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
|
|
@@ -9818,61 +11342,80 @@ const isTranscript = (annotationFeature, apolloSessionModel) => {
|
|
|
9818
11342
|
return (featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript') ||
|
|
9819
11343
|
featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogenic_transcript'));
|
|
9820
11344
|
};
|
|
9821
|
-
|
|
11345
|
+
function getFeatureName(feature) {
|
|
9822
11346
|
const { attributes } = feature;
|
|
9823
|
-
const
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
}
|
|
9829
|
-
const getFeatureNameOrId = (feature, apolloSessionModel) => {
|
|
9830
|
-
const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
|
|
9831
|
-
if (!featureTypeOntology) {
|
|
9832
|
-
return getFeatureId(feature);
|
|
9833
|
-
}
|
|
9834
|
-
let attrName = '';
|
|
9835
|
-
if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
|
|
9836
|
-
attrName = 'gene_name';
|
|
11347
|
+
const keys = ['name', 'gff_name', 'transcript_name', 'gene_name'];
|
|
11348
|
+
for (const key of keys) {
|
|
11349
|
+
const value = attributes?.[key];
|
|
11350
|
+
if (value?.[0]) {
|
|
11351
|
+
return value[0];
|
|
11352
|
+
}
|
|
9837
11353
|
}
|
|
9838
|
-
|
|
9839
|
-
|
|
11354
|
+
return '';
|
|
11355
|
+
}
|
|
11356
|
+
function getGeneNameOrId(feature) {
|
|
11357
|
+
const { attributes } = feature;
|
|
11358
|
+
const keys = ['gene_name', 'gene_id', 'gene_stable_id'];
|
|
11359
|
+
for (const key of keys) {
|
|
11360
|
+
const value = attributes?.[key];
|
|
11361
|
+
if (value?.[0]) {
|
|
11362
|
+
return value[0];
|
|
11363
|
+
}
|
|
9840
11364
|
}
|
|
11365
|
+
return '';
|
|
11366
|
+
}
|
|
11367
|
+
function getFeatureId(feature) {
|
|
9841
11368
|
const { attributes } = feature;
|
|
9842
|
-
const
|
|
11369
|
+
const keys = [
|
|
11370
|
+
'id',
|
|
11371
|
+
'gff_id',
|
|
11372
|
+
'transcript_id',
|
|
11373
|
+
'gene_id',
|
|
11374
|
+
'gene_stable_id',
|
|
11375
|
+
'stable_id',
|
|
11376
|
+
];
|
|
11377
|
+
for (const key of keys) {
|
|
11378
|
+
const value = attributes?.[key];
|
|
11379
|
+
if (value?.[0]) {
|
|
11380
|
+
return value[0];
|
|
11381
|
+
}
|
|
11382
|
+
}
|
|
11383
|
+
return '';
|
|
11384
|
+
}
|
|
11385
|
+
const getFeatureNameOrId = (feature) => {
|
|
11386
|
+
const name = getFeatureName(feature);
|
|
11387
|
+
const id = getFeatureId(feature);
|
|
9843
11388
|
if (name) {
|
|
9844
|
-
return name
|
|
11389
|
+
return `${feature.type} - ${name}`;
|
|
11390
|
+
}
|
|
11391
|
+
if (id) {
|
|
11392
|
+
return `${feature.type} - ${id}`;
|
|
9845
11393
|
}
|
|
9846
|
-
return
|
|
11394
|
+
return feature.type;
|
|
9847
11395
|
};
|
|
9848
|
-
function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
|
|
11396
|
+
function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, region, }) {
|
|
9849
11397
|
const apolloSessionModel = session;
|
|
11398
|
+
const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
|
|
9850
11399
|
const childIds = useMemo(() => Object.keys(annotationFeature.children ?? {}), [annotationFeature]);
|
|
9851
|
-
const features = useMemo(() => {
|
|
9852
|
-
for (const [, asm] of apolloSessionModel.apolloDataStore.assemblies) {
|
|
9853
|
-
if (asm._id === assembly.name) {
|
|
9854
|
-
for (const [, refSeq] of asm.refSeqs) {
|
|
9855
|
-
if (refSeq._id === refSeqId) {
|
|
9856
|
-
return refSeq.features;
|
|
9857
|
-
}
|
|
9858
|
-
}
|
|
9859
|
-
}
|
|
9860
|
-
}
|
|
9861
|
-
return [];
|
|
9862
|
-
}, []);
|
|
9863
11400
|
const [parentFeatureChecked, setParentFeatureChecked] = useState(true);
|
|
9864
11401
|
const [checkedChildrens, setCheckedChildrens] = useState(childIds);
|
|
9865
11402
|
const [errorMessage, setErrorMessage] = useState('');
|
|
9866
11403
|
const [destinationFeatures, setDestinationFeatures] = useState([]);
|
|
11404
|
+
const [createNewGene, setCreateNewGene] = useState(false);
|
|
9867
11405
|
const [selectedDestinationFeature, setSelectedDestinationFeature] = useState();
|
|
9868
|
-
const
|
|
11406
|
+
const apolloAssembly = apolloSessionModel.apolloDataStore.assemblies.get(assembly.name);
|
|
11407
|
+
const refSeq = apolloAssembly?.refSeqs.get(refSeqId);
|
|
11408
|
+
const features = refSeq?.getFeatures(region.start, region.end);
|
|
11409
|
+
const getDestinationFeatures = () => {
|
|
9869
11410
|
const filteredFeatures = [];
|
|
9870
|
-
for (const
|
|
9871
|
-
if (f.
|
|
11411
|
+
for (const f of features ?? []) {
|
|
11412
|
+
if (f.min > region.end || f.max < region.start) {
|
|
9872
11413
|
continue;
|
|
9873
11414
|
}
|
|
9874
|
-
|
|
9875
|
-
if (
|
|
11415
|
+
// Destination feature should be of type gene amd should be on the same strand as the source feature
|
|
11416
|
+
if (featureTypeOntology?.isTypeOf(f.type, 'gene') &&
|
|
11417
|
+
f.strand === annotationFeature.strand) {
|
|
11418
|
+
const featureSnapshot = getSnapshot(f);
|
|
9876
11419
|
filteredFeatures.push(featureSnapshot);
|
|
9877
11420
|
}
|
|
9878
11421
|
}
|
|
@@ -9880,27 +11423,10 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
|
|
|
9880
11423
|
};
|
|
9881
11424
|
useEffect(() => {
|
|
9882
11425
|
setErrorMessage('');
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
.filter((child) => isTranscript(child, apolloSessionModel))
|
|
9888
|
-
.filter((child) => checkedChildrens.includes(child._id));
|
|
9889
|
-
mins = checkedAnnotationFeatureChildren.map((f) => f.min);
|
|
9890
|
-
maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
|
|
9891
|
-
}
|
|
9892
|
-
const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
|
|
9893
|
-
if (featureTypeOntology &&
|
|
9894
|
-
featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript')) {
|
|
9895
|
-
mins = [annotationFeature.min, ...mins];
|
|
9896
|
-
maxes = [annotationFeature.max, ...maxes];
|
|
9897
|
-
}
|
|
9898
|
-
const min = Math.min(...mins);
|
|
9899
|
-
const max = Math.max(...maxes);
|
|
9900
|
-
const filteredFeatures = getFeatures(min, max);
|
|
9901
|
-
setDestinationFeatures(filteredFeatures);
|
|
9902
|
-
setSelectedDestinationFeature(filteredFeatures[0]);
|
|
9903
|
-
}, [checkedChildrens, parentFeatureChecked]);
|
|
11426
|
+
const features = getDestinationFeatures();
|
|
11427
|
+
setDestinationFeatures(features);
|
|
11428
|
+
setSelectedDestinationFeature(features[0]);
|
|
11429
|
+
}, [checkedChildrens, parentFeatureChecked, region]);
|
|
9904
11430
|
const handleParentFeatureCheck = (event) => {
|
|
9905
11431
|
const isChecked = event.target.checked;
|
|
9906
11432
|
setParentFeatureChecked(isChecked);
|
|
@@ -9917,95 +11443,211 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
|
|
|
9917
11443
|
};
|
|
9918
11444
|
const handleCreateApolloAnnotation = async () => {
|
|
9919
11445
|
if (parentFeatureChecked) {
|
|
9920
|
-
|
|
11446
|
+
// IF SOURCE FEATURE IS GENE
|
|
9921
11447
|
if (isGene(annotationFeature, apolloSessionModel)) {
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
Object.values(annotationFeature.children).length) {
|
|
9925
|
-
const childrens = {};
|
|
9926
|
-
for (const childId of checkedChildrens) {
|
|
9927
|
-
childrens[childId] = annotationFeature.children[childId];
|
|
9928
|
-
}
|
|
9929
|
-
change = new AddFeatureChange({
|
|
9930
|
-
changedIds: [annotationFeature._id],
|
|
9931
|
-
typeName: 'AddFeatureChange',
|
|
9932
|
-
assembly: assembly.name,
|
|
9933
|
-
addedFeature: {
|
|
9934
|
-
...annotationFeature,
|
|
9935
|
-
children: childrens,
|
|
9936
|
-
},
|
|
9937
|
-
});
|
|
9938
|
-
}
|
|
9939
|
-
else {
|
|
9940
|
-
change = new AddFeatureChange({
|
|
9941
|
-
changedIds: [annotationFeature._id],
|
|
9942
|
-
typeName: 'AddFeatureChange',
|
|
9943
|
-
assembly: assembly.name,
|
|
9944
|
-
addedFeature: annotationFeature,
|
|
9945
|
-
});
|
|
9946
|
-
}
|
|
11448
|
+
await copyGeneFeature();
|
|
11449
|
+
session.notify('Successfully copied selected gene and transcript(s)', 'success');
|
|
9947
11450
|
}
|
|
9948
11451
|
if (isTranscript(annotationFeature, apolloSessionModel)) {
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
11452
|
+
// IF THE SOURCE IS TRANSCRIPT AND THE DESTINATION IS SELECTED AND CREATE NEW GENE IS NOT CHECKED
|
|
11453
|
+
if (selectedDestinationFeature && !createNewGene) {
|
|
11454
|
+
const transcripts = {};
|
|
11455
|
+
transcripts[annotationFeature._id] = annotationFeature;
|
|
11456
|
+
// If source trancript doesn't overlap with destination gene
|
|
11457
|
+
// If not overlapping, then extend the destination gene to include the transcript
|
|
11458
|
+
if (selectedDestinationFeature.max < annotationFeature.max ||
|
|
11459
|
+
selectedDestinationFeature.min > annotationFeature.min) {
|
|
11460
|
+
const newMin = Math.min(selectedDestinationFeature.min, annotationFeature.min);
|
|
11461
|
+
const newMax = Math.max(selectedDestinationFeature.max, annotationFeature.max);
|
|
11462
|
+
await extendSelectedDestinationFeatureLocation(newMin, newMax);
|
|
11463
|
+
await copyTranscriptsToDestinationGene(transcripts);
|
|
11464
|
+
}
|
|
11465
|
+
else {
|
|
11466
|
+
await copyTranscriptsToDestinationGene(transcripts);
|
|
11467
|
+
}
|
|
11468
|
+
session.notify('Successfully copied selected transcripts to destination gene', 'success');
|
|
9957
11469
|
}
|
|
9958
11470
|
else {
|
|
9959
|
-
|
|
9960
|
-
|
|
11471
|
+
// IF THERE IS NO DESTINATION GENE SELECTED AND CREATE NEW GENE IS CHECKED
|
|
11472
|
+
const childrens = {};
|
|
11473
|
+
childrens[annotationFeature._id] = annotationFeature;
|
|
11474
|
+
await createNewGeneFeatureWithTranscripts(childrens);
|
|
11475
|
+
session.notify('Successfully created a new gene with selected transcripts', 'success');
|
|
9961
11476
|
}
|
|
9962
11477
|
}
|
|
9963
|
-
if (!change) {
|
|
9964
|
-
return;
|
|
9965
|
-
}
|
|
9966
|
-
await apolloSessionModel.apolloDataStore.changeManager.submit(change);
|
|
9967
|
-
session.notify('Annotation added successfully', 'success');
|
|
9968
|
-
handleClose();
|
|
9969
11478
|
}
|
|
9970
11479
|
else {
|
|
11480
|
+
// IF PARENT (GENE) FEATURE IS NOT CHECKED AND WE ARE COPYING CHILDREN (TRANSCRIPTS)
|
|
9971
11481
|
if (!annotationFeature.children) {
|
|
9972
11482
|
return;
|
|
9973
11483
|
}
|
|
9974
|
-
|
|
9975
|
-
|
|
11484
|
+
// IF DESTINATION IS SELECTED AND CREATE NEW GENE IS NOT CHECKED
|
|
11485
|
+
if (selectedDestinationFeature && !createNewGene) {
|
|
11486
|
+
const childrens = {};
|
|
11487
|
+
for (const childId of checkedChildrens) {
|
|
11488
|
+
childrens[childId] = annotationFeature.children[childId];
|
|
11489
|
+
}
|
|
11490
|
+
const min = Math.min(...Object.values(childrens).map((child) => child.min));
|
|
11491
|
+
const max = Math.max(...Object.values(childrens).map((child) => child.max));
|
|
11492
|
+
// If source trancript doesn't overlap with destination gene
|
|
11493
|
+
// If not overlapping, then extend the destination gene to include the transcript
|
|
11494
|
+
if (selectedDestinationFeature.min > min ||
|
|
11495
|
+
selectedDestinationFeature.max < max) {
|
|
11496
|
+
const newMin = Math.min(selectedDestinationFeature.min, min);
|
|
11497
|
+
const newMax = Math.max(selectedDestinationFeature.max, max);
|
|
11498
|
+
await extendSelectedDestinationFeatureLocation(newMin, newMax);
|
|
11499
|
+
await copyTranscriptsToDestinationGene(childrens);
|
|
11500
|
+
}
|
|
11501
|
+
else {
|
|
11502
|
+
await copyTranscriptsToDestinationGene(childrens);
|
|
11503
|
+
}
|
|
11504
|
+
session.notify('Successfully copied transcript to destination gene', 'success');
|
|
9976
11505
|
}
|
|
11506
|
+
else {
|
|
11507
|
+
// IF THERE IS NO DESTINATION GENE SELECTED AND CREATE NEW GENE IS CHECKED
|
|
11508
|
+
const childrens = {};
|
|
11509
|
+
for (const childId of checkedChildrens) {
|
|
11510
|
+
childrens[childId] = annotationFeature.children[childId];
|
|
11511
|
+
}
|
|
11512
|
+
await createNewGeneFeatureWithTranscripts(childrens);
|
|
11513
|
+
session.notify('Successfully created a new gene with selected transcript', 'success');
|
|
11514
|
+
}
|
|
11515
|
+
}
|
|
11516
|
+
handleClose();
|
|
11517
|
+
};
|
|
11518
|
+
// Copies gene feature along with its selected children
|
|
11519
|
+
const copyGeneFeature = async () => {
|
|
11520
|
+
let change;
|
|
11521
|
+
if (annotationFeature.children &&
|
|
11522
|
+
checkedChildrens.length !==
|
|
11523
|
+
Object.values(annotationFeature.children).length) {
|
|
11524
|
+
// IF SOME CHILDREN ARE CHECKED
|
|
11525
|
+
const childrens = {};
|
|
9977
11526
|
for (const childId of checkedChildrens) {
|
|
9978
|
-
|
|
9979
|
-
const change = new AddFeatureChange({
|
|
9980
|
-
parentFeatureId: selectedDestinationFeature._id,
|
|
9981
|
-
changedIds: [selectedDestinationFeature._id],
|
|
9982
|
-
typeName: 'AddFeatureChange',
|
|
9983
|
-
assembly: assembly.name,
|
|
9984
|
-
addedFeature: child,
|
|
9985
|
-
});
|
|
9986
|
-
await apolloSessionModel.apolloDataStore.changeManager.submit(change);
|
|
11527
|
+
childrens[childId] = annotationFeature.children[childId];
|
|
9987
11528
|
}
|
|
9988
|
-
|
|
9989
|
-
|
|
11529
|
+
change = new AddFeatureChange({
|
|
11530
|
+
changedIds: [annotationFeature._id],
|
|
11531
|
+
typeName: 'AddFeatureChange',
|
|
11532
|
+
assembly: assembly.name,
|
|
11533
|
+
addedFeature: {
|
|
11534
|
+
...annotationFeature,
|
|
11535
|
+
children: childrens,
|
|
11536
|
+
},
|
|
11537
|
+
});
|
|
11538
|
+
}
|
|
11539
|
+
else {
|
|
11540
|
+
// IF PARENT AND ALL CHILDREN ARE CHECKED
|
|
11541
|
+
change = new AddFeatureChange({
|
|
11542
|
+
changedIds: [annotationFeature._id],
|
|
11543
|
+
typeName: 'AddFeatureChange',
|
|
11544
|
+
assembly: assembly.name,
|
|
11545
|
+
addedFeature: annotationFeature,
|
|
11546
|
+
});
|
|
11547
|
+
}
|
|
11548
|
+
await submitChange(change);
|
|
11549
|
+
};
|
|
11550
|
+
const copyTranscriptsToDestinationGene = async (transcripts) => {
|
|
11551
|
+
if (!selectedDestinationFeature) {
|
|
11552
|
+
return;
|
|
11553
|
+
}
|
|
11554
|
+
for (const transcriptId of Object.keys(transcripts)) {
|
|
11555
|
+
const transcript = transcripts[transcriptId];
|
|
11556
|
+
const change = new AddFeatureChange({
|
|
11557
|
+
parentFeatureId: selectedDestinationFeature._id,
|
|
11558
|
+
changedIds: [selectedDestinationFeature._id],
|
|
11559
|
+
typeName: 'AddFeatureChange',
|
|
11560
|
+
assembly: assembly.name,
|
|
11561
|
+
addedFeature: transcript,
|
|
11562
|
+
});
|
|
11563
|
+
await submitChange(change);
|
|
11564
|
+
}
|
|
11565
|
+
};
|
|
11566
|
+
const createNewGeneFeatureWithTranscripts = async (childrens) => {
|
|
11567
|
+
const newGeneId = new ObjectID().toHexString();
|
|
11568
|
+
const min = Math.min(...Object.values(childrens).map((child) => child.min));
|
|
11569
|
+
const max = Math.max(...Object.values(childrens).map((child) => child.max));
|
|
11570
|
+
const change = new AddFeatureChange({
|
|
11571
|
+
changedIds: [newGeneId],
|
|
11572
|
+
typeName: 'AddFeatureChange',
|
|
11573
|
+
assembly: assembly.name,
|
|
11574
|
+
addedFeature: {
|
|
11575
|
+
_id: newGeneId,
|
|
11576
|
+
refSeq: refSeqId,
|
|
11577
|
+
min,
|
|
11578
|
+
max,
|
|
11579
|
+
strand: annotationFeature.strand,
|
|
11580
|
+
type: 'gene',
|
|
11581
|
+
children: childrens,
|
|
11582
|
+
attributes: {
|
|
11583
|
+
name: [getGeneNameOrId(annotationFeature)],
|
|
11584
|
+
gene_name: [getGeneNameOrId(annotationFeature)],
|
|
11585
|
+
},
|
|
11586
|
+
},
|
|
11587
|
+
});
|
|
11588
|
+
await submitChange(change);
|
|
11589
|
+
};
|
|
11590
|
+
const extendSelectedDestinationFeatureLocation = async (newMin, newMax) => {
|
|
11591
|
+
if (!selectedDestinationFeature) {
|
|
11592
|
+
return;
|
|
11593
|
+
}
|
|
11594
|
+
const changes = [];
|
|
11595
|
+
if (newMin !== selectedDestinationFeature.min) {
|
|
11596
|
+
changes.push(new LocationStartChange({
|
|
11597
|
+
typeName: 'LocationStartChange',
|
|
11598
|
+
changedIds: [selectedDestinationFeature._id],
|
|
11599
|
+
featureId: selectedDestinationFeature._id,
|
|
11600
|
+
assembly: assembly.name,
|
|
11601
|
+
oldStart: selectedDestinationFeature.min,
|
|
11602
|
+
newStart: newMin,
|
|
11603
|
+
}));
|
|
11604
|
+
}
|
|
11605
|
+
if (newMax !== selectedDestinationFeature.max) {
|
|
11606
|
+
changes.push(new LocationEndChange({
|
|
11607
|
+
typeName: 'LocationEndChange',
|
|
11608
|
+
changedIds: [selectedDestinationFeature._id],
|
|
11609
|
+
featureId: selectedDestinationFeature._id,
|
|
11610
|
+
assembly: assembly.name,
|
|
11611
|
+
oldEnd: selectedDestinationFeature.max,
|
|
11612
|
+
newEnd: newMax,
|
|
11613
|
+
}));
|
|
11614
|
+
}
|
|
11615
|
+
for (const change of changes) {
|
|
11616
|
+
await submitChange(change);
|
|
9990
11617
|
}
|
|
9991
11618
|
};
|
|
11619
|
+
const submitChange = async (change) => {
|
|
11620
|
+
await apolloSessionModel.apolloDataStore.changeManager.submit(change);
|
|
11621
|
+
};
|
|
11622
|
+
const handleCreateNewGeneChange = (e) => {
|
|
11623
|
+
setCreateNewGene(e.target.checked);
|
|
11624
|
+
};
|
|
9992
11625
|
return (React.createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
|
|
9993
11626
|
React.createElement(DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
|
|
9994
11627
|
React.createElement(DialogContent, null,
|
|
9995
11628
|
React.createElement(Box, { sx: { ml: 3 } },
|
|
9996
|
-
isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React.createElement(FormControlLabel, { control: React.createElement(Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${getFeatureNameOrId(annotationFeature
|
|
11629
|
+
isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React.createElement(FormControlLabel, { control: React.createElement(Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${getFeatureNameOrId(annotationFeature)} (${annotationFeature.min + 1}..${annotationFeature.max})` })),
|
|
9997
11630
|
annotationFeature.children && (React.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
|
|
9998
11631
|
.filter((child) => isTranscript(child, apolloSessionModel))
|
|
9999
11632
|
.map((child) => (React.createElement(FormControlLabel, { key: child._id, control: React.createElement(Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
|
|
10000
11633
|
handleChildFeatureCheck(e, child);
|
|
10001
|
-
} }), label: `${getFeatureNameOrId(child
|
|
11634
|
+
} }), label: `${getFeatureNameOrId(child)} (${child.min + 1}..${child.max})` })))))),
|
|
10002
11635
|
destinationFeatures.length > 0 &&
|
|
10003
11636
|
((!parentFeatureChecked && checkedChildrens.length > 0) ||
|
|
10004
11637
|
(parentFeatureChecked &&
|
|
10005
|
-
isTranscript(annotationFeature, apolloSessionModel))) && (React.createElement(
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
11638
|
+
isTranscript(annotationFeature, apolloSessionModel))) && (React.createElement("div", { style: {
|
|
11639
|
+
border: '1px solid #ccc',
|
|
11640
|
+
marginTop: 20,
|
|
11641
|
+
padding: 10,
|
|
11642
|
+
borderRadius: 5,
|
|
11643
|
+
} },
|
|
11644
|
+
React.createElement(Box, { sx: { ml: 3 } },
|
|
11645
|
+
React.createElement(Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
|
|
11646
|
+
React.createElement(Box, { sx: { mt: 1 } },
|
|
11647
|
+
React.createElement(Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange, disabled: createNewGene }, destinationFeatures.map((f) => (React.createElement(MenuItem, { key: f._id, value: f._id }, `${getFeatureNameOrId(f)} (${f.min + 1}..${f.max})`)))))),
|
|
11648
|
+
React.createElement(Box, { sx: { ml: 3 } },
|
|
11649
|
+
React.createElement(FormGroup, null,
|
|
11650
|
+
React.createElement(FormControlLabel, { control: React.createElement(Checkbox, { checked: createNewGene, onChange: handleCreateNewGeneChange }), label: "Create new gene" })))))),
|
|
10009
11651
|
React.createElement(DialogActions, null,
|
|
10010
11652
|
React.createElement(Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
|
|
10011
11653
|
(!parentFeatureChecked &&
|
|
@@ -10016,6 +11658,189 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
|
|
|
10016
11658
|
React.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
10017
11659
|
}
|
|
10018
11660
|
|
|
11661
|
+
function parseCigar(cigar) {
|
|
11662
|
+
const regex = /(\d+)([MIDNSHPX=])/g;
|
|
11663
|
+
const result = [];
|
|
11664
|
+
let match;
|
|
11665
|
+
while ((match = regex.exec(cigar)) !== null) {
|
|
11666
|
+
result.push([match[2], Number.parseInt(match[1], 10)]);
|
|
11667
|
+
}
|
|
11668
|
+
return result;
|
|
11669
|
+
}
|
|
11670
|
+
function annotationFromPileup(pluggableElement) {
|
|
11671
|
+
if (pluggableElement.name !== 'LinearPileupDisplay') {
|
|
11672
|
+
return pluggableElement;
|
|
11673
|
+
}
|
|
11674
|
+
const { stateModel } = pluggableElement;
|
|
11675
|
+
const newStateModel = stateModel
|
|
11676
|
+
.views((self) => ({
|
|
11677
|
+
getFirstRegion() {
|
|
11678
|
+
const lgv = getContainingView(self);
|
|
11679
|
+
return lgv.dynamicBlocks.contentBlocks[0];
|
|
11680
|
+
},
|
|
11681
|
+
getAssembly() {
|
|
11682
|
+
const firstRegion = self.getFirstRegion();
|
|
11683
|
+
const session = getSession(self);
|
|
11684
|
+
const { assemblyManager } = session;
|
|
11685
|
+
const { assemblyName } = firstRegion;
|
|
11686
|
+
const assembly = assemblyManager.get(assemblyName);
|
|
11687
|
+
if (!assembly) {
|
|
11688
|
+
throw new Error(`Could not find assembly named ${assemblyName}`);
|
|
11689
|
+
}
|
|
11690
|
+
return assembly;
|
|
11691
|
+
},
|
|
11692
|
+
getRefSeqId(assembly) {
|
|
11693
|
+
const firstRegion = self.getFirstRegion();
|
|
11694
|
+
const { refName } = firstRegion;
|
|
11695
|
+
const { refNameAliases } = assembly;
|
|
11696
|
+
if (!refNameAliases) {
|
|
11697
|
+
throw new Error(`Could not find aliases for ${assembly.name}`);
|
|
11698
|
+
}
|
|
11699
|
+
const newRefNames = [...Object.entries(refNameAliases)]
|
|
11700
|
+
.filter(([id, refName]) => id !== refName)
|
|
11701
|
+
.map(([id, refName]) => ({
|
|
11702
|
+
_id: id,
|
|
11703
|
+
name: refName,
|
|
11704
|
+
}));
|
|
11705
|
+
const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
|
|
11706
|
+
if (!refSeqId) {
|
|
11707
|
+
throw new Error(`Could not find refSeqId named ${refName}`);
|
|
11708
|
+
}
|
|
11709
|
+
return refSeqId;
|
|
11710
|
+
},
|
|
11711
|
+
getAnnotationFeature() {
|
|
11712
|
+
const feature = self.contextMenuFeature;
|
|
11713
|
+
const assembly = self.getAssembly();
|
|
11714
|
+
const refSeqId = self.getRefSeqId(assembly);
|
|
11715
|
+
const start = feature.get('start');
|
|
11716
|
+
const end = feature.get('end');
|
|
11717
|
+
const strand = feature.get('strand');
|
|
11718
|
+
const name = feature.get('name');
|
|
11719
|
+
const cigarData = feature.get('CIGAR');
|
|
11720
|
+
const ops = parseCigar(cigarData);
|
|
11721
|
+
let position = start;
|
|
11722
|
+
let currentExonStart;
|
|
11723
|
+
const exons = [];
|
|
11724
|
+
// Example: [[96,S], [4,M], [4216,N], [357,M], [1,I], [628,M], [94,S]]
|
|
11725
|
+
// Results in 2 exons
|
|
11726
|
+
// M, = and X are matches -> exon
|
|
11727
|
+
// N is a gap in the reference sequence -> intron
|
|
11728
|
+
// I, S, H and P -> not counted in reference position
|
|
11729
|
+
for (const [op, len] of ops) {
|
|
11730
|
+
switch (op) {
|
|
11731
|
+
case 'M':
|
|
11732
|
+
case '=':
|
|
11733
|
+
case 'X': {
|
|
11734
|
+
if (currentExonStart === undefined) {
|
|
11735
|
+
currentExonStart = position;
|
|
11736
|
+
}
|
|
11737
|
+
position += len;
|
|
11738
|
+
break;
|
|
11739
|
+
}
|
|
11740
|
+
case 'N': {
|
|
11741
|
+
if (currentExonStart !== undefined) {
|
|
11742
|
+
exons.push({
|
|
11743
|
+
start: currentExonStart,
|
|
11744
|
+
end: position,
|
|
11745
|
+
});
|
|
11746
|
+
currentExonStart = undefined;
|
|
11747
|
+
}
|
|
11748
|
+
position += len;
|
|
11749
|
+
break;
|
|
11750
|
+
}
|
|
11751
|
+
case 'D': {
|
|
11752
|
+
position += len;
|
|
11753
|
+
break;
|
|
11754
|
+
}
|
|
11755
|
+
case 'I':
|
|
11756
|
+
case 'S':
|
|
11757
|
+
case 'H':
|
|
11758
|
+
case 'P': {
|
|
11759
|
+
// These operations do not affect the position in the reference sequence
|
|
11760
|
+
break;
|
|
11761
|
+
}
|
|
11762
|
+
default: {
|
|
11763
|
+
throw new Error(`Unknown CIGAR operation: ${op}`);
|
|
11764
|
+
}
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
// If still in exon at end
|
|
11768
|
+
if (currentExonStart !== undefined) {
|
|
11769
|
+
exons.push({
|
|
11770
|
+
start: currentExonStart,
|
|
11771
|
+
end: position,
|
|
11772
|
+
});
|
|
11773
|
+
}
|
|
11774
|
+
const newFeature = {
|
|
11775
|
+
_id: ObjectID().toHexString(),
|
|
11776
|
+
refSeq: refSeqId,
|
|
11777
|
+
min: start,
|
|
11778
|
+
max: end,
|
|
11779
|
+
type: 'mRNA',
|
|
11780
|
+
strand,
|
|
11781
|
+
attributes: {
|
|
11782
|
+
name: [name],
|
|
11783
|
+
},
|
|
11784
|
+
};
|
|
11785
|
+
if (exons.length === 0) {
|
|
11786
|
+
return newFeature;
|
|
11787
|
+
}
|
|
11788
|
+
const children = {};
|
|
11789
|
+
newFeature.children = children;
|
|
11790
|
+
for (const exon of exons) {
|
|
11791
|
+
const newExon = {
|
|
11792
|
+
_id: ObjectID().toHexString(),
|
|
11793
|
+
refSeq: refSeqId,
|
|
11794
|
+
min: exon.start,
|
|
11795
|
+
max: exon.end,
|
|
11796
|
+
type: 'exon',
|
|
11797
|
+
strand,
|
|
11798
|
+
};
|
|
11799
|
+
newFeature.children[newExon._id] = newExon;
|
|
11800
|
+
}
|
|
11801
|
+
return newFeature;
|
|
11802
|
+
},
|
|
11803
|
+
}))
|
|
11804
|
+
.views((self) => {
|
|
11805
|
+
const superContextMenuItems = self.contextMenuItems;
|
|
11806
|
+
return {
|
|
11807
|
+
contextMenuItems() {
|
|
11808
|
+
const session = getSession(self);
|
|
11809
|
+
const assembly = self.getAssembly();
|
|
11810
|
+
const region = self.getFirstRegion();
|
|
11811
|
+
const feature = self.contextMenuFeature;
|
|
11812
|
+
if (!feature) {
|
|
11813
|
+
return superContextMenuItems();
|
|
11814
|
+
}
|
|
11815
|
+
return [
|
|
11816
|
+
...superContextMenuItems(),
|
|
11817
|
+
{
|
|
11818
|
+
label: 'Create Apollo annotation',
|
|
11819
|
+
icon: AddIcon,
|
|
11820
|
+
onClick: () => {
|
|
11821
|
+
session.queueDialog((doneCallback) => [
|
|
11822
|
+
CreateApolloAnnotation,
|
|
11823
|
+
{
|
|
11824
|
+
session,
|
|
11825
|
+
handleClose: () => {
|
|
11826
|
+
doneCallback();
|
|
11827
|
+
},
|
|
11828
|
+
annotationFeature: self.getAnnotationFeature(assembly),
|
|
11829
|
+
assembly,
|
|
11830
|
+
refSeqId: self.getRefSeqId(assembly),
|
|
11831
|
+
region,
|
|
11832
|
+
},
|
|
11833
|
+
]);
|
|
11834
|
+
},
|
|
11835
|
+
},
|
|
11836
|
+
];
|
|
11837
|
+
},
|
|
11838
|
+
};
|
|
11839
|
+
});
|
|
11840
|
+
pluggableElement.stateModel = newStateModel;
|
|
11841
|
+
return pluggableElement;
|
|
11842
|
+
}
|
|
11843
|
+
|
|
10019
11844
|
function simpleFeatureToGFF3Feature(feature, refSeqId) {
|
|
10020
11845
|
// eslint-disable-next-line unicorn/prefer-structured-clone
|
|
10021
11846
|
const xfeature = JSON.parse(JSON.stringify(feature));
|
|
@@ -10119,6 +11944,7 @@ function annotationFromJBrowseFeature(pluggableElement) {
|
|
|
10119
11944
|
contextMenuItems() {
|
|
10120
11945
|
const session = getSession(self);
|
|
10121
11946
|
const assembly = self.getAssembly();
|
|
11947
|
+
const region = self.getFirstRegion();
|
|
10122
11948
|
const feature = self.contextMenuFeature;
|
|
10123
11949
|
if (!feature) {
|
|
10124
11950
|
return superContextMenuItems();
|
|
@@ -10139,6 +11965,7 @@ function annotationFromJBrowseFeature(pluggableElement) {
|
|
|
10139
11965
|
annotationFeature: self.getAnnotationFeature(assembly),
|
|
10140
11966
|
assembly,
|
|
10141
11967
|
refSeqId: self.getRefSeqId(assembly),
|
|
11968
|
+
region,
|
|
10142
11969
|
},
|
|
10143
11970
|
]);
|
|
10144
11971
|
},
|
|
@@ -10219,7 +12046,7 @@ const LinearApolloDisplay = observer(function LinearApolloDisplay(props) {
|
|
|
10219
12046
|
else {
|
|
10220
12047
|
const coord = [event.clientX, event.clientY];
|
|
10221
12048
|
setContextCoord(coord);
|
|
10222
|
-
setContextMenuItems(getContextMenuItems(
|
|
12049
|
+
setContextMenuItems(getContextMenuItems(event));
|
|
10223
12050
|
}
|
|
10224
12051
|
} },
|
|
10225
12052
|
loading ? (React.createElement("div", { className: classes.loading },
|
|
@@ -10291,23 +12118,17 @@ const LinearApolloDisplay = observer(function LinearApolloDisplay(props) {
|
|
|
10291
12118
|
: undefined, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenuItems }))))));
|
|
10292
12119
|
});
|
|
10293
12120
|
|
|
10294
|
-
const TrackLines = observer(function TrackLines({ model,
|
|
10295
|
-
const { apolloRowHeight, highestRow,
|
|
10296
|
-
|
|
12121
|
+
const TrackLines = observer(function TrackLines({ model, hrStyle = { margin: 0, top: 0, color: 'black' }, idx = 0, }) {
|
|
12122
|
+
const { apolloRowHeight, highestRow, showFeatureLabels } = model;
|
|
12123
|
+
const featureLabelSpacer = showFeatureLabels ? 2 : 1;
|
|
12124
|
+
return (React.createElement("div", { style: {
|
|
10297
12125
|
position: 'absolute',
|
|
10298
12126
|
left: 0,
|
|
10299
|
-
top: (apolloRowHeight * (highestRow + 1)) / 2
|
|
12127
|
+
top: (apolloRowHeight * featureLabelSpacer * (highestRow + 1)) / 2 +
|
|
12128
|
+
idx * featureLabelSpacer * apolloRowHeight,
|
|
10300
12129
|
width: '100%',
|
|
10301
12130
|
} },
|
|
10302
|
-
React.createElement("hr", { style:
|
|
10303
|
-
position: 'absolute',
|
|
10304
|
-
left: 0,
|
|
10305
|
-
bottom: (apolloRowHeight * (highestRow + 1) + lastRowTooltipBufferHeight) /
|
|
10306
|
-
2 +
|
|
10307
|
-
3,
|
|
10308
|
-
width: '100%',
|
|
10309
|
-
} },
|
|
10310
|
-
React.createElement("hr", { style: { margin: 0, top: 0, color: 'black' } })));
|
|
12131
|
+
React.createElement("hr", { style: hrStyle })));
|
|
10311
12132
|
});
|
|
10312
12133
|
|
|
10313
12134
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
@@ -10367,8 +12188,9 @@ const LinearApolloSixFrameDisplay = observer(function LinearApolloSixFrameDispla
|
|
|
10367
12188
|
// Promise.resolve() in these 3 callbacks is to avoid infinite rendering loop
|
|
10368
12189
|
// https://github.com/mobxjs/mobx/issues/3728#issuecomment-1715400931
|
|
10369
12190
|
React.createElement(React.Fragment, null,
|
|
10370
|
-
React.createElement(TrackLines, { model: model,
|
|
10371
|
-
React.createElement(TrackLines, { model: model,
|
|
12191
|
+
React.createElement(TrackLines, { model: model, idx: 0 }),
|
|
12192
|
+
React.createElement(TrackLines, { model: model, hrStyle: { margin: 0, top: 0, color: 'grey', opacity: 0.4 }, idx: 1 }),
|
|
12193
|
+
React.createElement(TrackLines, { model: model, idx: 2 }),
|
|
10372
12194
|
React.createElement("canvas", { ref: async (node) => {
|
|
10373
12195
|
await Promise.resolve();
|
|
10374
12196
|
setCollaboratorCanvas(node);
|
|
@@ -10716,7 +12538,7 @@ class ChangeManager {
|
|
|
10716
12538
|
jobsManager.abortJob(job.name, String(error));
|
|
10717
12539
|
}
|
|
10718
12540
|
console.error(error);
|
|
10719
|
-
session.notify(String(error), 'error');
|
|
12541
|
+
session.notify(`Error encountered in client: ${String(error)}. Data may be out of sync, please refresh the page`, 'error');
|
|
10720
12542
|
return;
|
|
10721
12543
|
}
|
|
10722
12544
|
// post-validate
|
|
@@ -10759,10 +12581,10 @@ class ChangeManager {
|
|
|
10759
12581
|
if (change.notification) {
|
|
10760
12582
|
session.notify(change.notification, 'success');
|
|
10761
12583
|
}
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
12584
|
+
if (addToRecents) {
|
|
12585
|
+
// Push the change into array
|
|
12586
|
+
this.recentChanges.push(change);
|
|
12587
|
+
}
|
|
10766
12588
|
}
|
|
10767
12589
|
if (updateJobsManager) {
|
|
10768
12590
|
jobsManager.done(job);
|
|
@@ -10770,7 +12592,8 @@ class ChangeManager {
|
|
|
10770
12592
|
}
|
|
10771
12593
|
async revert(change, submitToBackend = true) {
|
|
10772
12594
|
const inverseChange = change.getInverse();
|
|
10773
|
-
|
|
12595
|
+
const opts = { submitToBackend, addToRecents: false };
|
|
12596
|
+
return this.submit(inverseChange, opts);
|
|
10774
12597
|
}
|
|
10775
12598
|
/**
|
|
10776
12599
|
* Undo the last change
|
|
@@ -11596,15 +13419,6 @@ function extendSession(pluginManager, sessionModel) {
|
|
|
11596
13419
|
}))
|
|
11597
13420
|
.actions((self) => ({
|
|
11598
13421
|
afterCreate: flow(function* afterCreate() {
|
|
11599
|
-
// When the initial config.json loads, it doesn't include the Apollo
|
|
11600
|
-
// tracks, which would result in a potentially invalid session snapshot
|
|
11601
|
-
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
11602
|
-
// empty session snapshot, and then restore the original session
|
|
11603
|
-
// snapshot after the updated config.json loads.
|
|
11604
|
-
const sessionSnapshot = getSnapshot(self);
|
|
11605
|
-
const { id, name } = sessionSnapshot;
|
|
11606
|
-
applySnapshot(self, { name, id });
|
|
11607
|
-
const { internetAccounts, jbrowse } = getRoot(self);
|
|
11608
13422
|
autorun(() => {
|
|
11609
13423
|
// broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
|
|
11610
13424
|
const locations = [];
|
|
@@ -11653,7 +13467,23 @@ function extendSession(pluginManager, sessionModel) {
|
|
|
11653
13467
|
}
|
|
11654
13468
|
}
|
|
11655
13469
|
}, { name: 'ApolloSession' });
|
|
11656
|
-
//
|
|
13470
|
+
// When the initial config.json loads, it doesn't include the Apollo
|
|
13471
|
+
// tracks, which would result in a potentially invalid session snapshot
|
|
13472
|
+
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
13473
|
+
// empty session snapshot, and then restore the original session
|
|
13474
|
+
// snapshot after the updated config.json loads.
|
|
13475
|
+
// @ts-expect-error type is missing on ApolloRootModel
|
|
13476
|
+
const { internetAccounts, jbrowse, reloadPluginManagerCallback } = getRoot(self);
|
|
13477
|
+
const pluginConfiguration =
|
|
13478
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
13479
|
+
jbrowse.configuration.ApolloPlugin;
|
|
13480
|
+
const hasRole = readConfObject(pluginConfiguration, 'hasRole');
|
|
13481
|
+
if (hasRole) {
|
|
13482
|
+
return;
|
|
13483
|
+
}
|
|
13484
|
+
const sessionSnapshot = getSnapshot(self);
|
|
13485
|
+
const { id, name } = sessionSnapshot;
|
|
13486
|
+
applySnapshot(self, { name, id });
|
|
11657
13487
|
// fetch and initialize assemblies for each of our Apollo internet accounts
|
|
11658
13488
|
for (const internetAccount of internetAccounts) {
|
|
11659
13489
|
if (internetAccount.type !== 'ApolloInternetAccount') {
|
|
@@ -11686,9 +13516,8 @@ function extendSession(pluginManager, sessionModel) {
|
|
|
11686
13516
|
console.error(error);
|
|
11687
13517
|
continue;
|
|
11688
13518
|
}
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
applySnapshot(self, sessionSnapshot);
|
|
13519
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
13520
|
+
reloadPluginManagerCallback(jbrowseConfig, sessionSnapshot);
|
|
11692
13521
|
}
|
|
11693
13522
|
}),
|
|
11694
13523
|
beforeDestroy() {
|