@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
|
@@ -95,7 +95,7 @@ var ExpandLessIcon__default = /*#__PURE__*/_interopDefaultLegacy(ExpandLessIcon)
|
|
|
95
95
|
var ErrorIcon__default = /*#__PURE__*/_interopDefaultLegacy(ErrorIcon);
|
|
96
96
|
var SaveIcon__default = /*#__PURE__*/_interopDefaultLegacy(SaveIcon);
|
|
97
97
|
|
|
98
|
-
var version = "0.3.
|
|
98
|
+
var version = "0.3.7";
|
|
99
99
|
|
|
100
100
|
const ApolloConfigSchema = configuration.ConfigurationSchema('ApolloInternetAccount', {
|
|
101
101
|
baseURL: {
|
|
@@ -175,7 +175,7 @@ async function checkFeatures(assembly) {
|
|
|
175
175
|
return checkResults;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
function getFeatureName(feature) {
|
|
178
|
+
function getFeatureName$1(feature) {
|
|
179
179
|
const { attributes } = feature;
|
|
180
180
|
const name = attributes.get('gff_name');
|
|
181
181
|
if (name) {
|
|
@@ -204,7 +204,7 @@ function getFeatureId$1(feature) {
|
|
|
204
204
|
return '';
|
|
205
205
|
}
|
|
206
206
|
function getFeatureNameOrId$1(feature) {
|
|
207
|
-
const name = getFeatureName(feature);
|
|
207
|
+
const name = getFeatureName$1(feature);
|
|
208
208
|
const id = getFeatureId$1(feature);
|
|
209
209
|
if (name) {
|
|
210
210
|
return `: ${name}`;
|
|
@@ -223,6 +223,164 @@ function getStrand(strand) {
|
|
|
223
223
|
}
|
|
224
224
|
return '';
|
|
225
225
|
}
|
|
226
|
+
function getChildren(feature) {
|
|
227
|
+
const children = [];
|
|
228
|
+
//
|
|
229
|
+
if (feature.children) {
|
|
230
|
+
for (const [, ff] of feature.children) {
|
|
231
|
+
children.push(ff);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return children;
|
|
235
|
+
}
|
|
236
|
+
function getParents(feature) {
|
|
237
|
+
const parents = [];
|
|
238
|
+
let { parent } = feature;
|
|
239
|
+
while (parent) {
|
|
240
|
+
parents.push(parent);
|
|
241
|
+
({ parent } = parent);
|
|
242
|
+
}
|
|
243
|
+
return parents;
|
|
244
|
+
}
|
|
245
|
+
function getFeaturesUnderClick(mousePosition, includeSiblings = false) {
|
|
246
|
+
const clickedFeatures = [];
|
|
247
|
+
if (!mousePosition.featureAndGlyphUnderMouse) {
|
|
248
|
+
return clickedFeatures;
|
|
249
|
+
}
|
|
250
|
+
clickedFeatures.push(mousePosition.featureAndGlyphUnderMouse.feature);
|
|
251
|
+
for (const x of getParents(mousePosition.featureAndGlyphUnderMouse.feature)) {
|
|
252
|
+
clickedFeatures.push(x);
|
|
253
|
+
}
|
|
254
|
+
const { bp } = mousePosition;
|
|
255
|
+
const children = getChildren(mousePosition.featureAndGlyphUnderMouse.feature);
|
|
256
|
+
for (const child of children) {
|
|
257
|
+
if (child.min < bp && child.max >= bp) {
|
|
258
|
+
clickedFeatures.push(child);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!includeSiblings) {
|
|
262
|
+
return clickedFeatures;
|
|
263
|
+
}
|
|
264
|
+
// Also add siblings , i.e. features having the same parent as the clicked
|
|
265
|
+
// one and intersecting the click position
|
|
266
|
+
if (mousePosition.featureAndGlyphUnderMouse.feature.parent) {
|
|
267
|
+
const siblings = mousePosition.featureAndGlyphUnderMouse.feature.parent.children;
|
|
268
|
+
if (siblings) {
|
|
269
|
+
for (const [, sib] of siblings) {
|
|
270
|
+
if (sib._id == mousePosition.featureAndGlyphUnderMouse.feature._id) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (sib.min < bp && sib.max >= bp) {
|
|
274
|
+
clickedFeatures.push(sib);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return clickedFeatures;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getMinAndMaxPx(feature, refName, regionNumber, lgv) {
|
|
283
|
+
const minPxInfo = lgv.bpToPx({
|
|
284
|
+
refName,
|
|
285
|
+
coord: feature.min,
|
|
286
|
+
regionNumber,
|
|
287
|
+
});
|
|
288
|
+
const maxPxInfo = lgv.bpToPx({
|
|
289
|
+
refName,
|
|
290
|
+
coord: feature.max,
|
|
291
|
+
regionNumber,
|
|
292
|
+
});
|
|
293
|
+
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const { offsetPx } = lgv;
|
|
297
|
+
const minPx = minPxInfo.offsetPx - offsetPx;
|
|
298
|
+
const maxPx = maxPxInfo.offsetPx - offsetPx;
|
|
299
|
+
return [minPx, maxPx];
|
|
300
|
+
}
|
|
301
|
+
function getOverlappingEdge(feature, x, minMax) {
|
|
302
|
+
const [minPx, maxPx] = minMax;
|
|
303
|
+
// Feature is too small to tell if we're overlapping an edge
|
|
304
|
+
if (Math.abs(maxPx - minPx) < 8) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (Math.abs(minPx - x) < 4) {
|
|
308
|
+
return { feature, edge: 'min' };
|
|
309
|
+
}
|
|
310
|
+
if (Math.abs(maxPx - x) < 4) {
|
|
311
|
+
return { feature, edge: 'max' };
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function expandFeatures(feature, newLocation, edge) {
|
|
317
|
+
const featureId = feature._id;
|
|
318
|
+
const oldLocation = feature[edge];
|
|
319
|
+
const changes = [{ featureId, oldLocation, newLocation }];
|
|
320
|
+
const { parent } = feature;
|
|
321
|
+
if (parent &&
|
|
322
|
+
((edge === 'min' && parent[edge] > newLocation) ||
|
|
323
|
+
(edge === 'max' && parent[edge] < newLocation))) {
|
|
324
|
+
changes.push(...expandFeatures(parent, newLocation, edge));
|
|
325
|
+
}
|
|
326
|
+
return changes;
|
|
327
|
+
}
|
|
328
|
+
function shrinkFeatures(feature, newLocation, edge, shrinkParent, childIdToSkip) {
|
|
329
|
+
const featureId = feature._id;
|
|
330
|
+
const oldLocation = feature[edge];
|
|
331
|
+
const changes = [{ featureId, oldLocation, newLocation }];
|
|
332
|
+
const { parent, children } = feature;
|
|
333
|
+
if (children) {
|
|
334
|
+
for (const [, child] of children) {
|
|
335
|
+
if (child._id === childIdToSkip) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if ((edge === 'min' && child[edge] < newLocation) ||
|
|
339
|
+
(edge === 'max' && child[edge] > newLocation)) {
|
|
340
|
+
changes.push(...shrinkFeatures(child, newLocation, edge, shrinkParent));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (parent && shrinkParent) {
|
|
345
|
+
const siblings = [];
|
|
346
|
+
if (parent.children) {
|
|
347
|
+
for (const [, c] of parent.children) {
|
|
348
|
+
if (c._id === featureId) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
siblings.push(c);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (siblings.length === 0) {
|
|
355
|
+
changes.push(...shrinkFeatures(parent, newLocation, edge, shrinkParent, featureId));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const oldLocation = parent[edge];
|
|
359
|
+
const boundedLocation = Math[edge](...siblings.map((s) => s[edge]), newLocation);
|
|
360
|
+
if (boundedLocation !== oldLocation) {
|
|
361
|
+
changes.push(...shrinkFeatures(parent, boundedLocation, edge, shrinkParent, featureId));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return changes;
|
|
366
|
+
}
|
|
367
|
+
function getPropagatedLocationChanges(feature, newLocation, edge, shrinkParent = false) {
|
|
368
|
+
const oldLocation = feature[edge];
|
|
369
|
+
if (newLocation === oldLocation) {
|
|
370
|
+
throw new Error(`New and existing locations are the same: "${newLocation}"`);
|
|
371
|
+
}
|
|
372
|
+
if (edge === 'min') {
|
|
373
|
+
if (newLocation > oldLocation) {
|
|
374
|
+
// shrinking feature, may need to shrink children and/or parents
|
|
375
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent);
|
|
376
|
+
}
|
|
377
|
+
return expandFeatures(feature, newLocation, edge);
|
|
378
|
+
}
|
|
379
|
+
if (newLocation < oldLocation) {
|
|
380
|
+
return shrinkFeatures(feature, newLocation, edge, shrinkParent);
|
|
381
|
+
}
|
|
382
|
+
return expandFeatures(feature, newLocation, edge);
|
|
383
|
+
}
|
|
226
384
|
|
|
227
385
|
async function createFetchErrorMessage(response, additionalText) {
|
|
228
386
|
let errorMessage;
|
|
@@ -1541,7 +1699,9 @@ const OntologyRecordType = mobxStateTree.types
|
|
|
1541
1699
|
const equivalents = terms
|
|
1542
1700
|
.map((term) => term.lbl)
|
|
1543
1701
|
.filter((term) => term != undefined);
|
|
1544
|
-
|
|
1702
|
+
if (mobxStateTree.isAlive(self)) {
|
|
1703
|
+
self.setEquivalentTypes(type, equivalents);
|
|
1704
|
+
}
|
|
1545
1705
|
}),
|
|
1546
1706
|
}))
|
|
1547
1707
|
.actions((self) => ({
|
|
@@ -1822,8 +1982,8 @@ async function getValidTerms(ontologyStore, fetchValidTerms, filterTerms, signal
|
|
|
1822
1982
|
return filterTerms ? result.filter((element) => filterTerms(element)) : result;
|
|
1823
1983
|
}
|
|
1824
1984
|
|
|
1985
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1825
1986
|
function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId, sourceFeature, }) {
|
|
1826
|
-
const { notify } = session;
|
|
1827
1987
|
const [end, setEnd] = React.useState(String(sourceFeature.max));
|
|
1828
1988
|
const [start, setStart] = React.useState(String(sourceFeature.min + 1));
|
|
1829
1989
|
const [type, setType] = React.useState('');
|
|
@@ -1837,7 +1997,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
|
|
|
1837
1997
|
}
|
|
1838
1998
|
return terms;
|
|
1839
1999
|
}
|
|
1840
|
-
|
|
2000
|
+
function onSubmit(event) {
|
|
1841
2001
|
event.preventDefault();
|
|
1842
2002
|
setErrorMessage('');
|
|
1843
2003
|
const change = new shared.AddFeatureChange({
|
|
@@ -1853,8 +2013,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
|
|
|
1853
2013
|
},
|
|
1854
2014
|
parentFeatureId: sourceFeature._id,
|
|
1855
2015
|
});
|
|
1856
|
-
|
|
1857
|
-
notify('Feature added successfully', 'success');
|
|
2016
|
+
void changeManager.submit(change);
|
|
1858
2017
|
handleClose();
|
|
1859
2018
|
event.preventDefault();
|
|
1860
2019
|
}
|
|
@@ -1884,6 +2043,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
|
|
|
1884
2043
|
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
1885
2044
|
}
|
|
1886
2045
|
|
|
2046
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1887
2047
|
var NewFeature;
|
|
1888
2048
|
(function (NewFeature) {
|
|
1889
2049
|
NewFeature["GENE_AND_SUBFEATURES"] = "GENE_AND_SUBFEATURES";
|
|
@@ -1922,14 +2082,13 @@ function makeCodingMrna(refSeqId, strand, min, max) {
|
|
|
1922
2082
|
return mRNA;
|
|
1923
2083
|
}
|
|
1924
2084
|
function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
1925
|
-
const { notify } = session;
|
|
1926
2085
|
const [end, setEnd] = React.useState(String(region.end));
|
|
1927
2086
|
const [start, setStart] = React.useState(String(region.start + 1));
|
|
1928
2087
|
const [type, setType] = React.useState(NewFeature.GENE_AND_SUBFEATURES);
|
|
1929
2088
|
const [customType, setCustomType] = React.useState();
|
|
1930
2089
|
const [strand, setStrand] = React.useState();
|
|
1931
2090
|
const [errorMessage, setErrorMessage] = React.useState('');
|
|
1932
|
-
|
|
2091
|
+
function onSubmit(event) {
|
|
1933
2092
|
event.preventDefault();
|
|
1934
2093
|
setErrorMessage('');
|
|
1935
2094
|
let refSeqId;
|
|
@@ -1943,7 +2102,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1943
2102
|
}
|
|
1944
2103
|
}
|
|
1945
2104
|
if (!refSeqId) {
|
|
1946
|
-
setErrorMessage('Invalid refseq id');
|
|
2105
|
+
setErrorMessage('Invalid refseq id. Make sure you have the Apollo annotation track open');
|
|
1947
2106
|
return;
|
|
1948
2107
|
}
|
|
1949
2108
|
if (type === NewFeature.GENE_AND_SUBFEATURES) {
|
|
@@ -1965,8 +2124,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1965
2124
|
children,
|
|
1966
2125
|
},
|
|
1967
2126
|
});
|
|
1968
|
-
|
|
1969
|
-
notify('Feature added successfully', 'success');
|
|
2127
|
+
void changeManager.submit(change);
|
|
1970
2128
|
handleClose();
|
|
1971
2129
|
return;
|
|
1972
2130
|
}
|
|
@@ -1978,8 +2136,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
1978
2136
|
assembly: region.assemblyName,
|
|
1979
2137
|
addedFeature: mRNA,
|
|
1980
2138
|
});
|
|
1981
|
-
|
|
1982
|
-
notify('Feature added successfully', 'success');
|
|
2139
|
+
void changeManager.submit(change);
|
|
1983
2140
|
handleClose();
|
|
1984
2141
|
return;
|
|
1985
2142
|
}
|
|
@@ -2001,8 +2158,7 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
2001
2158
|
strand,
|
|
2002
2159
|
},
|
|
2003
2160
|
});
|
|
2004
|
-
|
|
2005
|
-
notify('Feature added successfully', 'success');
|
|
2161
|
+
void changeManager.submit(change);
|
|
2006
2162
|
handleClose();
|
|
2007
2163
|
return;
|
|
2008
2164
|
}
|
|
@@ -2035,7 +2191,9 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
|
|
|
2035
2191
|
}
|
|
2036
2192
|
};
|
|
2037
2193
|
let submitDisabled = Boolean(error) || !(start && end && type);
|
|
2038
|
-
if (type === NewFeature.CUSTOM && !customType)
|
|
2194
|
+
if ((type === NewFeature.CUSTOM && !customType) ||
|
|
2195
|
+
(!strand && type === NewFeature.GENE_AND_SUBFEATURES) ||
|
|
2196
|
+
(!strand && type === NewFeature.TRANSCRIPT_AND_SUBFEATURES)) {
|
|
2039
2197
|
submitDisabled = true;
|
|
2040
2198
|
}
|
|
2041
2199
|
return (React__default["default"].createElement(Dialog, { open: true, title: "Add new feature", handleClose: handleClose, maxWidth: false, "data-testid": "add-feature-dialog" },
|
|
@@ -2107,7 +2265,7 @@ feature, featureIds) {
|
|
|
2107
2265
|
};
|
|
2108
2266
|
}
|
|
2109
2267
|
function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, sourceFeature, }) {
|
|
2110
|
-
const { assemblyManager
|
|
2268
|
+
const { assemblyManager } = session;
|
|
2111
2269
|
const assemblies = assemblyManager.assemblyList;
|
|
2112
2270
|
const [selectedAssemblyId, setSelectedAssemblyId] = React.useState(assemblies.find((a) => a.name !== sourceAssemblyId)?.name);
|
|
2113
2271
|
const [refNames, setRefNames] = React.useState([]);
|
|
@@ -2206,8 +2364,7 @@ function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, so
|
|
|
2206
2364
|
copyFeature: true,
|
|
2207
2365
|
allIds: featureIds,
|
|
2208
2366
|
});
|
|
2209
|
-
|
|
2210
|
-
notify('Feature copied successfully', 'success');
|
|
2367
|
+
void changeManager.submit(change);
|
|
2211
2368
|
handleClose();
|
|
2212
2369
|
event.preventDefault();
|
|
2213
2370
|
}
|
|
@@ -2331,30 +2488,318 @@ function DeleteAssembly({ changeManager, handleClose, session, }) {
|
|
|
2331
2488
|
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
2332
2489
|
}
|
|
2333
2490
|
|
|
2491
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
2492
|
+
function lumpLocationChanges(changes, assembly) {
|
|
2493
|
+
if (changes.length === 0) {
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
const locationStartChange = new shared.LocationStartChange({
|
|
2497
|
+
typeName: 'LocationStartChange',
|
|
2498
|
+
changedIds: [],
|
|
2499
|
+
changes: [],
|
|
2500
|
+
assembly,
|
|
2501
|
+
});
|
|
2502
|
+
const locationEndChange = new shared.LocationEndChange({
|
|
2503
|
+
typeName: 'LocationEndChange',
|
|
2504
|
+
changedIds: [],
|
|
2505
|
+
changes: [],
|
|
2506
|
+
assembly,
|
|
2507
|
+
});
|
|
2508
|
+
for (const change of changes) {
|
|
2509
|
+
if (change.typeName === 'LocationStartChange') {
|
|
2510
|
+
locationStartChange.changedIds.push(change.changedId);
|
|
2511
|
+
const cc = {
|
|
2512
|
+
featureId: change.featureId,
|
|
2513
|
+
oldStart: change.oldLocation,
|
|
2514
|
+
newStart: change.newLocation,
|
|
2515
|
+
};
|
|
2516
|
+
locationStartChange.changes.push(cc);
|
|
2517
|
+
}
|
|
2518
|
+
if (change.typeName === 'LocationEndChange') {
|
|
2519
|
+
locationEndChange.changedIds.push(change.changedId);
|
|
2520
|
+
const cc = {
|
|
2521
|
+
featureId: change.featureId,
|
|
2522
|
+
oldEnd: change.oldLocation,
|
|
2523
|
+
newEnd: change.newLocation,
|
|
2524
|
+
};
|
|
2525
|
+
locationEndChange.changes.push(cc);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (locationStartChange.changedIds.length > 0 &&
|
|
2529
|
+
locationEndChange.changedIds.length === 0) {
|
|
2530
|
+
return locationStartChange;
|
|
2531
|
+
}
|
|
2532
|
+
if (locationEndChange.changedIds.length > 0 &&
|
|
2533
|
+
locationStartChange.changedIds.length === 0) {
|
|
2534
|
+
return locationEndChange;
|
|
2535
|
+
}
|
|
2536
|
+
throw new Error('Unexpected list of changes');
|
|
2537
|
+
}
|
|
2334
2538
|
function DeleteFeature({ changeManager, handleClose, selectedFeature, session, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
2335
|
-
const { notify } = session;
|
|
2336
2539
|
const [errorMessage, setErrorMessage] = React.useState('');
|
|
2540
|
+
const { ontologyManager } = session.apolloDataStore;
|
|
2541
|
+
const { featureTypeOntology } = ontologyManager;
|
|
2542
|
+
function trimCDS(sourceFeature) {
|
|
2543
|
+
if (!featureTypeOntology) {
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (!featureTypeOntology.isTypeOf(sourceFeature.type, 'exon')) {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
if (!sourceFeature.parent?.cdsLocations ||
|
|
2550
|
+
sourceFeature.parent.cdsLocations.length === 0 ||
|
|
2551
|
+
sourceFeature.parent.cdsLocations[0].length === 0) {
|
|
2552
|
+
// No CDS - parent of this exon is a non-coding transcript
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
if (!sourceFeature.parent.children) {
|
|
2556
|
+
throw new Error('Unable to find parent of CDS');
|
|
2557
|
+
}
|
|
2558
|
+
if (sourceFeature.parent.cdsLocations.length != 1) {
|
|
2559
|
+
throw new Error('Unable to handle a transcript with multiple CDSs');
|
|
2560
|
+
}
|
|
2561
|
+
const _cdsLocations = sourceFeature.parent.cdsLocations.at(0) ?? [];
|
|
2562
|
+
const cdsLocations = _cdsLocations.sort(({ min: a }, { min: b }) => a - b);
|
|
2563
|
+
let cdsFeature;
|
|
2564
|
+
for (const child of sourceFeature.parent.children.values()) {
|
|
2565
|
+
if (child.type === cdsLocations[0].type) {
|
|
2566
|
+
cdsFeature = child;
|
|
2567
|
+
break;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
if (!cdsFeature) {
|
|
2571
|
+
throw new Error('Unable to find CDS');
|
|
2572
|
+
}
|
|
2573
|
+
const cdsStart = cdsLocations[0].min;
|
|
2574
|
+
// eslint-disable-next-line unicorn/prefer-at
|
|
2575
|
+
const cdsEnd = cdsLocations[cdsLocations.length - 1].max;
|
|
2576
|
+
if ((sourceFeature.min > cdsStart && sourceFeature.max < cdsEnd) ||
|
|
2577
|
+
sourceFeature.max < cdsStart ||
|
|
2578
|
+
sourceFeature.min > cdsEnd) {
|
|
2579
|
+
// No adjustment if the exon being deleted is fully contained in the CDS
|
|
2580
|
+
// or completely outside of the CDS
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
if (sourceFeature.min <= cdsStart && sourceFeature.max >= cdsEnd) {
|
|
2584
|
+
// CDS is fully contained in the exon, delete CDS
|
|
2585
|
+
return new shared.DeleteFeatureChange({
|
|
2586
|
+
changedIds: [cdsFeature._id],
|
|
2587
|
+
typeName: 'DeleteFeatureChange',
|
|
2588
|
+
assembly: sourceAssemblyId,
|
|
2589
|
+
changes: [
|
|
2590
|
+
{
|
|
2591
|
+
deletedFeature: mobxStateTree.getSnapshot(cdsFeature),
|
|
2592
|
+
parentFeatureId: cdsFeature.parent?._id,
|
|
2593
|
+
},
|
|
2594
|
+
],
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
if (sourceFeature.min <= cdsStart && sourceFeature.max > cdsStart) {
|
|
2598
|
+
// Exon overlaps the start of the CDS so we need to move the CDS start
|
|
2599
|
+
let newCdsStart;
|
|
2600
|
+
for (const cdsLocation of cdsLocations) {
|
|
2601
|
+
if (cdsLocation.min > sourceFeature.max) {
|
|
2602
|
+
newCdsStart = cdsLocation.min;
|
|
2603
|
+
break;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (!newCdsStart) {
|
|
2607
|
+
throw new Error('Error setting new CDS start');
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
typeName: 'LocationStartChange',
|
|
2611
|
+
changedId: cdsFeature._id,
|
|
2612
|
+
featureId: cdsFeature._id,
|
|
2613
|
+
oldLocation: cdsFeature.min,
|
|
2614
|
+
newLocation: newCdsStart,
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
if (sourceFeature.min < cdsEnd && sourceFeature.max >= cdsEnd) {
|
|
2618
|
+
// Exon overlaps the end of the CDS so we need to move the CDS end
|
|
2619
|
+
let newCdsEnd;
|
|
2620
|
+
for (const cdsLocation of cdsLocations.reverse()) {
|
|
2621
|
+
if (cdsLocation.max < sourceFeature.min) {
|
|
2622
|
+
newCdsEnd = cdsLocation.max;
|
|
2623
|
+
break;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (!newCdsEnd) {
|
|
2627
|
+
throw new Error('Error setting new CDS end');
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
typeName: 'LocationEndChange',
|
|
2631
|
+
changedId: cdsFeature._id,
|
|
2632
|
+
featureId: cdsFeature._id,
|
|
2633
|
+
oldLocation: cdsFeature.max,
|
|
2634
|
+
newLocation: newCdsEnd,
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
throw new Error('Unexpected relationship between exon and CDS');
|
|
2638
|
+
}
|
|
2639
|
+
function trimParent(featureToDelete) {
|
|
2640
|
+
if (!featureToDelete.parent?.children ||
|
|
2641
|
+
featureToDelete.parent.children.size === 1) {
|
|
2642
|
+
// Do not resize if this parent has only one child (i.e. the feature being deleted)
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const childrenByStart = [];
|
|
2646
|
+
for (const x of featureToDelete.parent.children.values()) {
|
|
2647
|
+
if (!featureTypeOntology?.isTypeOf(x.type, 'CDS')) {
|
|
2648
|
+
// CDS has been already handled so don't use it to resize parent
|
|
2649
|
+
childrenByStart.push(x);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
childrenByStart.sort((a, b) => a.min - b.min);
|
|
2653
|
+
const childrenByEnd = [];
|
|
2654
|
+
for (const x of featureToDelete.parent.children.values()) {
|
|
2655
|
+
if (!featureTypeOntology?.isTypeOf(x.type, 'CDS')) {
|
|
2656
|
+
// CDS has been already handled so don't use it to resize parent
|
|
2657
|
+
childrenByEnd.push(x);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
childrenByEnd.sort((a, b) => b.max - a.max);
|
|
2661
|
+
if (featureToDelete.min === childrenByStart[0].min) {
|
|
2662
|
+
// The feature to delete has the lowest start coordinate of all children
|
|
2663
|
+
// Find the next lowest coordinate and reset parent to this new start
|
|
2664
|
+
let newParentFeatureStart;
|
|
2665
|
+
for (const child of childrenByStart) {
|
|
2666
|
+
if (child._id !== featureToDelete._id &&
|
|
2667
|
+
child.min >= featureToDelete.min) {
|
|
2668
|
+
newParentFeatureStart = child.min;
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
if (newParentFeatureStart &&
|
|
2673
|
+
newParentFeatureStart != featureToDelete.parent.min) {
|
|
2674
|
+
return {
|
|
2675
|
+
typeName: 'LocationStartChange',
|
|
2676
|
+
changedId: featureToDelete.parent._id,
|
|
2677
|
+
featureId: featureToDelete.parent._id,
|
|
2678
|
+
oldLocation: featureToDelete.parent.min,
|
|
2679
|
+
newLocation: newParentFeatureStart,
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (featureToDelete.max === childrenByEnd[0].max) {
|
|
2684
|
+
// The feature to delete has the highest end coordinate of all children
|
|
2685
|
+
// Find the next highest coordinate and reset parent to this new end
|
|
2686
|
+
let newParentFeatureEnd;
|
|
2687
|
+
for (const child of childrenByEnd) {
|
|
2688
|
+
if (child._id != featureToDelete._id &&
|
|
2689
|
+
child.max <= featureToDelete.max) {
|
|
2690
|
+
newParentFeatureEnd = child.max;
|
|
2691
|
+
break;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
if (newParentFeatureEnd &&
|
|
2695
|
+
newParentFeatureEnd != featureToDelete.parent.max) {
|
|
2696
|
+
return {
|
|
2697
|
+
typeName: 'LocationEndChange',
|
|
2698
|
+
changedId: featureToDelete.parent._id,
|
|
2699
|
+
featureId: featureToDelete.parent._id,
|
|
2700
|
+
oldLocation: featureToDelete.parent.max,
|
|
2701
|
+
newLocation: newParentFeatureEnd,
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2337
2707
|
async function onSubmit(event) {
|
|
2338
2708
|
event.preventDefault();
|
|
2339
2709
|
setErrorMessage('');
|
|
2340
2710
|
if (selectedFeature?._id === sourceFeature._id) {
|
|
2341
2711
|
setSelectedFeature();
|
|
2342
2712
|
}
|
|
2343
|
-
|
|
2344
|
-
const
|
|
2713
|
+
const locationChanges = [];
|
|
2714
|
+
// const deleteChanges: DeleteFeatureChange = []
|
|
2715
|
+
const deleteChanges = new shared.DeleteFeatureChange({
|
|
2345
2716
|
changedIds: [sourceFeature._id],
|
|
2346
2717
|
typeName: 'DeleteFeatureChange',
|
|
2347
2718
|
assembly: sourceAssemblyId,
|
|
2348
|
-
|
|
2349
|
-
|
|
2719
|
+
changes: [
|
|
2720
|
+
{
|
|
2721
|
+
deletedFeature: mobxStateTree.getSnapshot(sourceFeature),
|
|
2722
|
+
parentFeatureId: sourceFeature.parent?._id,
|
|
2723
|
+
},
|
|
2724
|
+
],
|
|
2350
2725
|
});
|
|
2351
|
-
|
|
2352
|
-
|
|
2726
|
+
if (featureTypeOntology &&
|
|
2727
|
+
(featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
|
|
2728
|
+
featureTypeOntology.isTypeOf(sourceFeature.type, 'pseudogenic_transcript'))) {
|
|
2729
|
+
const geneChange = trimParent(sourceFeature);
|
|
2730
|
+
if (geneChange) {
|
|
2731
|
+
locationChanges.push(geneChange);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
if (featureTypeOntology &&
|
|
2735
|
+
featureTypeOntology.isTypeOf(sourceFeature.type, 'exon')) {
|
|
2736
|
+
const cdsChange = trimCDS(sourceFeature);
|
|
2737
|
+
if (cdsChange) {
|
|
2738
|
+
if (cdsChange.typeName === 'DeleteFeatureChange') {
|
|
2739
|
+
deleteChanges.changedIds.push(...cdsChange.changedIds);
|
|
2740
|
+
deleteChanges.changes.push(...cdsChange.changes);
|
|
2741
|
+
}
|
|
2742
|
+
else {
|
|
2743
|
+
locationChanges.push(cdsChange);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
const txChange = trimParent(sourceFeature);
|
|
2747
|
+
if (txChange) {
|
|
2748
|
+
locationChanges.push(txChange);
|
|
2749
|
+
// Parent transcript has changed. See if we need to resize the parent gene
|
|
2750
|
+
const gene = sourceFeature.parent?.parent;
|
|
2751
|
+
if (gene?.children) {
|
|
2752
|
+
if (txChange.typeName === 'LocationStartChange') {
|
|
2753
|
+
let newGeneStart = txChange.newLocation;
|
|
2754
|
+
for (const [, tx] of gene.children) {
|
|
2755
|
+
if (tx._id != txChange.featureId && tx.min < newGeneStart) {
|
|
2756
|
+
// Reset to longest child (tx)
|
|
2757
|
+
newGeneStart = tx.min;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
if (newGeneStart != gene.min) {
|
|
2761
|
+
locationChanges.push({
|
|
2762
|
+
typeName: txChange.typeName,
|
|
2763
|
+
changedId: gene._id,
|
|
2764
|
+
featureId: gene._id,
|
|
2765
|
+
oldLocation: gene.min,
|
|
2766
|
+
newLocation: newGeneStart,
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
else {
|
|
2771
|
+
let newGeneEnd = txChange.newLocation;
|
|
2772
|
+
for (const [, tx] of gene.children) {
|
|
2773
|
+
if (tx._id != txChange.featureId && tx.max > newGeneEnd) {
|
|
2774
|
+
// Reset to longest child (tx)
|
|
2775
|
+
newGeneEnd = tx.max;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
if (newGeneEnd != gene.max) {
|
|
2779
|
+
locationChanges.push({
|
|
2780
|
+
typeName: txChange.typeName,
|
|
2781
|
+
changedId: gene._id,
|
|
2782
|
+
featureId: gene._id,
|
|
2783
|
+
oldLocation: gene.max,
|
|
2784
|
+
newLocation: newGeneEnd,
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const lumpedLocChanges = lumpLocationChanges(locationChanges, sourceAssemblyId);
|
|
2792
|
+
await changeManager.submit(deleteChanges);
|
|
2793
|
+
if (lumpedLocChanges) {
|
|
2794
|
+
await changeManager.submit(lumpedLocChanges);
|
|
2795
|
+
}
|
|
2353
2796
|
handleClose();
|
|
2354
2797
|
event.preventDefault();
|
|
2355
2798
|
}
|
|
2356
2799
|
return (React__default["default"].createElement(Dialog, { open: true, title: "Delete feature", handleClose: handleClose, maxWidth: false, "data-testid": "delete-feature" },
|
|
2357
|
-
React__default["default"].createElement("form", { onSubmit:
|
|
2800
|
+
React__default["default"].createElement("form", { onSubmit: (event) => {
|
|
2801
|
+
void onSubmit(event);
|
|
2802
|
+
} },
|
|
2358
2803
|
React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
2359
2804
|
React__default["default"].createElement(material.DialogContentText, null, "Are you sure you want to delete the selected feature?")),
|
|
2360
2805
|
React__default["default"].createElement(material.DialogActions, null,
|
|
@@ -2365,6 +2810,7 @@ function DeleteFeature({ changeManager, handleClose, selectedFeature, session, s
|
|
|
2365
2810
|
}
|
|
2366
2811
|
|
|
2367
2812
|
function DownloadGFF3({ handleClose, session }) {
|
|
2813
|
+
const [includeFASTA, setincludeFASTA] = React.useState(false);
|
|
2368
2814
|
const [selectedAssembly, setSelectedAssembly] = React.useState();
|
|
2369
2815
|
const [errorMessage, setErrorMessage] = React.useState('');
|
|
2370
2816
|
const { collaborationServerDriver, getInternetAccount, inMemoryFileDriver } = session.apolloDataStore;
|
|
@@ -2421,7 +2867,7 @@ function DownloadGFF3({ handleClose, session }) {
|
|
|
2421
2867
|
const exportURL = new URL('export', internetAccount.baseURL);
|
|
2422
2868
|
const params = {
|
|
2423
2869
|
exportID,
|
|
2424
|
-
includeFASTA: 'true',
|
|
2870
|
+
includeFASTA: includeFASTA ? 'true' : 'false',
|
|
2425
2871
|
};
|
|
2426
2872
|
const exportSearchParams = new URLSearchParams(params);
|
|
2427
2873
|
exportURL.search = exportSearchParams.toString();
|
|
@@ -2475,7 +2921,11 @@ function DownloadGFF3({ handleClose, session }) {
|
|
|
2475
2921
|
React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
2476
2922
|
React__default["default"].createElement(material.DialogContentText, null, "Select assembly"),
|
|
2477
2923
|
React__default["default"].createElement(material.Select, { labelId: "label", value: selectedAssembly?.name ?? '', onChange: handleChangeAssembly, disabled: assemblies.length === 0 }, assemblies.map((option) => (React__default["default"].createElement(material.MenuItem, { key: option.name, value: option.name }, option.displayName ?? option.name)))),
|
|
2478
|
-
React__default["default"].createElement(material.DialogContentText, null, "Select assembly to export to GFF3")
|
|
2924
|
+
React__default["default"].createElement(material.DialogContentText, null, "Select assembly to export to GFF3"),
|
|
2925
|
+
React__default["default"].createElement(material.FormGroup, null,
|
|
2926
|
+
React__default["default"].createElement(material.FormControlLabel, { "data-testid": "include-fasta-checkbox", control: React__default["default"].createElement(material.Checkbox, { checked: includeFASTA, onChange: () => {
|
|
2927
|
+
setincludeFASTA(!includeFASTA);
|
|
2928
|
+
} }), label: "Include fasta sequence in GFF output" }))),
|
|
2479
2929
|
React__default["default"].createElement(material.DialogActions, null,
|
|
2480
2930
|
React__default["default"].createElement(material.Button, { disabled: !selectedAssembly, variant: "contained", type: "submit" }, "Download"),
|
|
2481
2931
|
React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
@@ -2954,70 +3404,254 @@ function ManageUsers({ changeManager, handleClose, session, }) {
|
|
|
2954
3404
|
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
2955
3405
|
}
|
|
2956
3406
|
|
|
2957
|
-
/* eslint-disable @typescript-eslint/
|
|
2958
|
-
function
|
|
2959
|
-
const
|
|
2960
|
-
const
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3407
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
3408
|
+
function getNeighboringExons(referenceExon) {
|
|
3409
|
+
const neighboringExons = {};
|
|
3410
|
+
const tx = referenceExon.parent;
|
|
3411
|
+
if (!tx) {
|
|
3412
|
+
throw new Error('Unable to find parent of reference exon');
|
|
3413
|
+
}
|
|
3414
|
+
let exons = [];
|
|
3415
|
+
if (tx.children) {
|
|
3416
|
+
for (const [, feature] of tx.children) {
|
|
3417
|
+
if (feature.type === 'exon') {
|
|
3418
|
+
exons.push(feature);
|
|
3419
|
+
}
|
|
2970
3420
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
if (
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3421
|
+
}
|
|
3422
|
+
exons = exons.sort((a, b) => {
|
|
3423
|
+
if (a.min === b.min) {
|
|
3424
|
+
return a.max - b.max;
|
|
3425
|
+
}
|
|
3426
|
+
return a.min - b.min;
|
|
3427
|
+
});
|
|
3428
|
+
if (tx.strand && tx.strand === -1) {
|
|
3429
|
+
exons = exons.reverse();
|
|
3430
|
+
}
|
|
3431
|
+
let i = 0;
|
|
3432
|
+
for (const x of exons) {
|
|
3433
|
+
if (x._id === referenceExon._id) {
|
|
3434
|
+
if (exons.length > i + 1) {
|
|
3435
|
+
neighboringExons.three_prime = exons[i + 1];
|
|
2978
3436
|
}
|
|
2979
|
-
|
|
2980
|
-
|
|
3437
|
+
if (i > 0) {
|
|
3438
|
+
neighboringExons.five_prime = exons[i - 1];
|
|
2981
3439
|
}
|
|
3440
|
+
break;
|
|
2982
3441
|
}
|
|
3442
|
+
i++;
|
|
2983
3443
|
}
|
|
2984
|
-
|
|
3444
|
+
return neighboringExons;
|
|
3445
|
+
}
|
|
3446
|
+
function makeRadioButtonName$1(key, neighboringExons) {
|
|
3447
|
+
const neighboringExon = neighboringExons[key];
|
|
3448
|
+
let name;
|
|
3449
|
+
if (key === 'three_prime') {
|
|
3450
|
+
name = `3'end (coords: ${neighboringExon.min + 1}-${neighboringExon.max})`;
|
|
3451
|
+
}
|
|
3452
|
+
else if (key === 'five_prime') {
|
|
3453
|
+
name = `5'end (coords: ${neighboringExon.min + 1}-${neighboringExon.max})`;
|
|
3454
|
+
}
|
|
3455
|
+
else {
|
|
3456
|
+
throw new Error(`Unexpected direction: "${key}"`);
|
|
3457
|
+
}
|
|
3458
|
+
return name;
|
|
3459
|
+
}
|
|
3460
|
+
function MergeExons({ changeManager, handleClose, selectedFeature, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
3461
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
3462
|
+
const [selectedExon, setSelectedExon] = React.useState();
|
|
3463
|
+
function onSubmit(event) {
|
|
2985
3464
|
event.preventDefault();
|
|
2986
3465
|
setErrorMessage('');
|
|
2987
|
-
|
|
2988
|
-
if (!
|
|
2989
|
-
throw new Error('No file selected');
|
|
2990
|
-
}
|
|
2991
|
-
// Right now we are not using stream because there was a problem with 'pipe' in ReadStream
|
|
2992
|
-
const fileData = await new Response(file).text();
|
|
2993
|
-
const assemblyId = `${assemblyName}-${file.name}-${nanoid.nanoid(8)}`;
|
|
2994
|
-
try {
|
|
2995
|
-
await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore);
|
|
2996
|
-
}
|
|
2997
|
-
catch (error) {
|
|
2998
|
-
console.error(error);
|
|
2999
|
-
notify(`Error loading GFF3 ${file.name}, ${String(error)}`, 'error');
|
|
3000
|
-
handleClose();
|
|
3466
|
+
const { parent } = sourceFeature;
|
|
3467
|
+
if (!(selectedExon && parent)) {
|
|
3001
3468
|
return;
|
|
3002
3469
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3470
|
+
if (selectedFeature?._id === sourceFeature._id) {
|
|
3471
|
+
setSelectedFeature();
|
|
3472
|
+
}
|
|
3473
|
+
const change = new shared.MergeExonsChange({
|
|
3474
|
+
changedIds: [sourceFeature._id],
|
|
3475
|
+
typeName: 'MergeExonsChange',
|
|
3476
|
+
assembly: sourceAssemblyId,
|
|
3477
|
+
firstExon: mobxStateTree.getSnapshot(sourceFeature),
|
|
3478
|
+
secondExon: mobxStateTree.getSnapshot(selectedExon),
|
|
3479
|
+
parentFeatureId: parent._id,
|
|
3480
|
+
});
|
|
3481
|
+
void changeManager.submit(change);
|
|
3482
|
+
handleClose();
|
|
3483
|
+
event.preventDefault();
|
|
3484
|
+
}
|
|
3485
|
+
const handleTypeChange = (e) => {
|
|
3486
|
+
setErrorMessage('');
|
|
3487
|
+
const { value } = e.target;
|
|
3488
|
+
setSelectedExon(neighboringExons[value]);
|
|
3489
|
+
};
|
|
3490
|
+
const neighboringExons = getNeighboringExons(sourceFeature);
|
|
3491
|
+
return (React__default["default"].createElement(Dialog, { open: true, title: "Merge exons", handleClose: handleClose, maxWidth: false, "data-testid": "merge-exons" },
|
|
3492
|
+
React__default["default"].createElement("form", { onSubmit: onSubmit },
|
|
3493
|
+
React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
3494
|
+
Object.keys(neighboringExons).length === 0
|
|
3495
|
+
? 'There are no neighbouring exons to merge with'
|
|
3496
|
+
: 'Merge with exon on:',
|
|
3497
|
+
React__default["default"].createElement(material.FormControl, { style: { marginTop: 5 } },
|
|
3498
|
+
React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", name: "radio-buttons-group", value: selectedExon, onChange: handleTypeChange }, Object.keys(neighboringExons).map((key) => (React__default["default"].createElement(material.FormControlLabel, { value: key, key: key, control: React__default["default"].createElement(material.Radio, null), label: React__default["default"].createElement(material.Box, { display: "flex", alignItems: "center" }, makeRadioButtonName$1(key, neighboringExons)) })))))),
|
|
3499
|
+
React__default["default"].createElement(material.DialogActions, null,
|
|
3500
|
+
React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: Object.keys(neighboringExons).length === 0 ||
|
|
3501
|
+
selectedExon === undefined }, "Submit"),
|
|
3502
|
+
React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
3503
|
+
errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
|
|
3504
|
+
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
function getTranscripts(referenceTranscript, session) {
|
|
3508
|
+
const gene = referenceTranscript.parent;
|
|
3509
|
+
if (!gene) {
|
|
3510
|
+
throw new Error('Unable to find parent of reference transcript');
|
|
3511
|
+
}
|
|
3512
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
3513
|
+
if (!featureTypeOntology) {
|
|
3514
|
+
throw new Error('featureTypeOntology is undefined');
|
|
3515
|
+
}
|
|
3516
|
+
const transcripts = {};
|
|
3517
|
+
if (gene.children) {
|
|
3518
|
+
for (const [, feature] of gene.children) {
|
|
3519
|
+
if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
|
|
3520
|
+
feature._id !== referenceTranscript._id) {
|
|
3521
|
+
transcripts[feature._id] = feature;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
return transcripts;
|
|
3526
|
+
}
|
|
3527
|
+
function makeRadioButtonName(transcript) {
|
|
3528
|
+
let id;
|
|
3529
|
+
if (transcript.attributes.get('gff_name')) {
|
|
3530
|
+
id = transcript.attributes.get('gff_name')?.join(',');
|
|
3531
|
+
}
|
|
3532
|
+
else if (transcript.attributes.get('gff_id')) {
|
|
3533
|
+
id = transcript.attributes.get('gff_id')?.join(',');
|
|
3534
|
+
}
|
|
3535
|
+
else {
|
|
3536
|
+
id = transcript._id;
|
|
3537
|
+
}
|
|
3538
|
+
return `${id} [${transcript.min + 1}-${transcript.max}]`;
|
|
3539
|
+
}
|
|
3540
|
+
function MergeTranscripts({ changeManager, handleClose, selectedFeature, session, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
3541
|
+
const { notify } = session;
|
|
3542
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
3543
|
+
const [selectedTranscript, setSelectedTranscript] = React.useState();
|
|
3544
|
+
async function onSubmit(event) {
|
|
3545
|
+
event.preventDefault();
|
|
3546
|
+
setErrorMessage('');
|
|
3547
|
+
if (!selectedTranscript) {
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
if (selectedFeature?._id === sourceFeature._id) {
|
|
3551
|
+
setSelectedFeature();
|
|
3552
|
+
}
|
|
3553
|
+
if (!sourceFeature.parent) {
|
|
3554
|
+
throw new Error('Cannot find parent');
|
|
3555
|
+
}
|
|
3556
|
+
const change = new shared.MergeTranscriptsChange({
|
|
3557
|
+
changedIds: [sourceFeature._id],
|
|
3558
|
+
typeName: 'MergeTranscriptsChange',
|
|
3559
|
+
assembly: sourceAssemblyId,
|
|
3560
|
+
firstTranscript: mobxStateTree.getSnapshot(sourceFeature),
|
|
3561
|
+
secondTranscript: mobxStateTree.getSnapshot(selectedTranscript),
|
|
3562
|
+
parentFeatureId: sourceFeature.parent._id,
|
|
3563
|
+
});
|
|
3564
|
+
await changeManager.submit(change);
|
|
3565
|
+
notify('Transcripts successfully merged', 'success');
|
|
3566
|
+
handleClose();
|
|
3567
|
+
event.preventDefault();
|
|
3568
|
+
}
|
|
3569
|
+
const handleTypeChange = (e) => {
|
|
3570
|
+
setErrorMessage('');
|
|
3571
|
+
const { value } = e.target;
|
|
3572
|
+
setSelectedTranscript(transcripts[value]);
|
|
3573
|
+
};
|
|
3574
|
+
const transcripts = getTranscripts(sourceFeature, session);
|
|
3575
|
+
return (React__default["default"].createElement(Dialog, { open: true, title: "Merge transcripts", handleClose: handleClose, maxWidth: false, "data-testid": "merge-transcripts" },
|
|
3576
|
+
React__default["default"].createElement("form", { onSubmit: onSubmit },
|
|
3577
|
+
React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
3578
|
+
Object.keys(transcripts).length === 0
|
|
3579
|
+
? 'There are no transcripts to merge with'
|
|
3580
|
+
: 'Merge with transcript:',
|
|
3581
|
+
React__default["default"].createElement(material.FormControl, { style: { marginTop: 5 } },
|
|
3582
|
+
React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", name: "radio-buttons-group", value: selectedTranscript, onChange: handleTypeChange }, Object.keys(transcripts).map((key) => (React__default["default"].createElement(material.FormControlLabel, { value: key, key: key, control: React__default["default"].createElement(material.Radio, null), label: React__default["default"].createElement(material.Box, { display: "flex", alignItems: "center" }, makeRadioButtonName(transcripts[key])) })))))),
|
|
3583
|
+
React__default["default"].createElement(material.DialogActions, null,
|
|
3584
|
+
React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: Object.keys(transcripts).length === 0 ||
|
|
3585
|
+
selectedTranscript === undefined }, "Submit"),
|
|
3586
|
+
React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
3587
|
+
errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
|
|
3588
|
+
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
3592
|
+
function OpenLocalFile({ handleClose, session }) {
|
|
3593
|
+
const { apolloDataStore } = session;
|
|
3594
|
+
const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
|
|
3595
|
+
const [file, setFile] = React.useState(null);
|
|
3596
|
+
const [assemblyName, setAssemblyName] = React.useState('');
|
|
3597
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
3598
|
+
const [submitted, setSubmitted] = React.useState(false);
|
|
3599
|
+
const theme = material.useTheme();
|
|
3600
|
+
function handleChangeFile(e) {
|
|
3601
|
+
const selectedFile = e.target.files?.item(0);
|
|
3602
|
+
if (!selectedFile) {
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
setErrorMessage('');
|
|
3606
|
+
setFile(selectedFile);
|
|
3607
|
+
if (!assemblyName) {
|
|
3608
|
+
const fileName = selectedFile.name;
|
|
3609
|
+
const lastDotIndex = fileName.lastIndexOf('.');
|
|
3610
|
+
if (lastDotIndex === -1) {
|
|
3611
|
+
setAssemblyName(fileName);
|
|
3612
|
+
}
|
|
3613
|
+
else {
|
|
3614
|
+
setAssemblyName(fileName.slice(0, lastDotIndex));
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
async function onSubmit(event) {
|
|
3619
|
+
event.preventDefault();
|
|
3620
|
+
setErrorMessage('');
|
|
3621
|
+
setSubmitted(true);
|
|
3622
|
+
if (!file) {
|
|
3623
|
+
throw new Error('No file selected');
|
|
3624
|
+
}
|
|
3625
|
+
// Right now we are not using stream because there was a problem with 'pipe' in ReadStream
|
|
3626
|
+
const fileData = await new Response(file).text();
|
|
3627
|
+
const assemblyId = `${assemblyName}-${file.name}-${nanoid.nanoid(8)}`;
|
|
3628
|
+
try {
|
|
3629
|
+
await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore);
|
|
3630
|
+
}
|
|
3631
|
+
catch (error) {
|
|
3632
|
+
console.error(error);
|
|
3633
|
+
notify(`Error loading GFF3 ${file.name}, ${String(error)}`, 'error');
|
|
3634
|
+
handleClose();
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
const assemblyConfig = {
|
|
3638
|
+
name: assemblyId,
|
|
3639
|
+
aliases: [assemblyName],
|
|
3640
|
+
displayName: assemblyName,
|
|
3641
|
+
sequence: {
|
|
3642
|
+
trackId: `sequenceConfigId-${assemblyName}`,
|
|
3643
|
+
type: 'ReferenceSequenceTrack',
|
|
3644
|
+
adapter: { type: 'ApolloSequenceAdapter', assemblyId },
|
|
3645
|
+
metadata: {
|
|
3646
|
+
apollo: true,
|
|
3647
|
+
...(util.isElectron
|
|
3648
|
+
? { file: file.path }
|
|
3649
|
+
: {}),
|
|
3650
|
+
},
|
|
3651
|
+
},
|
|
3652
|
+
};
|
|
3653
|
+
// Save assembly into session
|
|
3654
|
+
await (addSessionAssembly || addAssembly)(assemblyConfig);
|
|
3021
3655
|
const a = await assemblyManager.waitForAssembly(assemblyConfig.name);
|
|
3022
3656
|
if (a) {
|
|
3023
3657
|
// @ts-expect-error MST type coercion problem?
|
|
@@ -3173,7 +3807,7 @@ function ViewChangeLog({ handleClose, session }) {
|
|
|
3173
3807
|
}
|
|
3174
3808
|
|
|
3175
3809
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
3176
|
-
const columns = [
|
|
3810
|
+
const columns$1 = [
|
|
3177
3811
|
{ field: 'refName', headerName: 'Ref Name' },
|
|
3178
3812
|
{ field: 'aliases', headerName: 'Aliases', editable: true },
|
|
3179
3813
|
];
|
|
@@ -3322,7 +3956,7 @@ function AddRefSeqAliases({ changeManager, handleClose, session, }) {
|
|
|
3322
3956
|
React__default["default"].createElement("input", { type: "file", onChange: handleChangeFileHandler, ref: fileRef, disabled: (enableSubmit && !errorMessage) || !selectedAssembly }))),
|
|
3323
3957
|
selectedAssembly && refNameAliasMap.size > 0 ? (React__default["default"].createElement("div", { style: { height: 200, width: '100%', marginTop: 20 } },
|
|
3324
3958
|
React__default["default"].createElement(material.InputLabel, null, "Refname aliases found for selected assembly."),
|
|
3325
|
-
React__default["default"].createElement(xDataGrid.DataGrid, { rows: getTableRows(), columns: columns, initialState: {
|
|
3959
|
+
React__default["default"].createElement(xDataGrid.DataGrid, { rows: getTableRows(), columns: columns$1, initialState: {
|
|
3326
3960
|
pagination: {
|
|
3327
3961
|
paginationModel: { page: 0, pageSize: 5 },
|
|
3328
3962
|
},
|
|
@@ -3412,6 +4046,116 @@ function ViewCheckResults({ handleClose, session, }) {
|
|
|
3412
4046
|
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
3413
4047
|
}
|
|
3414
4048
|
|
|
4049
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
4050
|
+
function exonIsSplittable(exonToBeSplit) {
|
|
4051
|
+
if (exonToBeSplit.max - exonToBeSplit.min < 2) {
|
|
4052
|
+
return {
|
|
4053
|
+
isSplittable: false,
|
|
4054
|
+
comment: 'This exon is too short to be split',
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
return { isSplittable: true, comment: '' };
|
|
4058
|
+
}
|
|
4059
|
+
function makeDialogText(splitExon) {
|
|
4060
|
+
const splittable = exonIsSplittable(splitExon);
|
|
4061
|
+
if (splittable.isSplittable) {
|
|
4062
|
+
return 'Are you sure you want to split the selected exon?';
|
|
4063
|
+
}
|
|
4064
|
+
return splittable.comment;
|
|
4065
|
+
}
|
|
4066
|
+
function SplitExon({ changeManager, handleClose, selectedFeature, setSelectedFeature, sourceAssemblyId, sourceFeature, }) {
|
|
4067
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
4068
|
+
const exonToBeSplit = mobxStateTree.getSnapshot(sourceFeature);
|
|
4069
|
+
function onSubmit(event) {
|
|
4070
|
+
event.preventDefault();
|
|
4071
|
+
setErrorMessage('');
|
|
4072
|
+
if (selectedFeature?._id === sourceFeature._id) {
|
|
4073
|
+
setSelectedFeature();
|
|
4074
|
+
}
|
|
4075
|
+
const midpoint = exonToBeSplit.min + (exonToBeSplit.max - exonToBeSplit.min) / 2;
|
|
4076
|
+
const upstreamCut = Math.floor(midpoint);
|
|
4077
|
+
const downstreamCut = Math.ceil(midpoint);
|
|
4078
|
+
if (!sourceFeature.parent?._id) {
|
|
4079
|
+
throw new Error('Splitting an exon without parent is not possible yet');
|
|
4080
|
+
}
|
|
4081
|
+
const change = new shared.SplitExonChange({
|
|
4082
|
+
changedIds: [sourceFeature._id],
|
|
4083
|
+
typeName: 'SplitExonChange',
|
|
4084
|
+
assembly: sourceAssemblyId,
|
|
4085
|
+
exonToBeSplit,
|
|
4086
|
+
parentFeatureId: sourceFeature.parent._id,
|
|
4087
|
+
upstreamCut,
|
|
4088
|
+
downstreamCut,
|
|
4089
|
+
leftExonId: new ObjectID__default["default"]().toHexString(),
|
|
4090
|
+
rightExonId: new ObjectID__default["default"]().toHexString(),
|
|
4091
|
+
});
|
|
4092
|
+
void changeManager.submit(change);
|
|
4093
|
+
handleClose();
|
|
4094
|
+
event.preventDefault();
|
|
4095
|
+
}
|
|
4096
|
+
return (React__default["default"].createElement(Dialog, { open: true, title: "Split exon", handleClose: handleClose, maxWidth: false, "data-testid": "split-exon" },
|
|
4097
|
+
React__default["default"].createElement("form", { onSubmit: onSubmit },
|
|
4098
|
+
React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
4099
|
+
React__default["default"].createElement(material.DialogContentText, null, makeDialogText(exonToBeSplit))),
|
|
4100
|
+
React__default["default"].createElement(material.DialogActions, null,
|
|
4101
|
+
React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: !exonIsSplittable(exonToBeSplit).isSplittable }, "Yes"),
|
|
4102
|
+
React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel"))),
|
|
4103
|
+
errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
|
|
4104
|
+
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
const columns = [
|
|
4108
|
+
{
|
|
4109
|
+
field: 'name',
|
|
4110
|
+
headerName: 'Assembly Name',
|
|
4111
|
+
width: 150,
|
|
4112
|
+
editable: false,
|
|
4113
|
+
},
|
|
4114
|
+
{
|
|
4115
|
+
field: 'aliases',
|
|
4116
|
+
headerName: 'Aliases',
|
|
4117
|
+
width: 300,
|
|
4118
|
+
editable: true,
|
|
4119
|
+
},
|
|
4120
|
+
];
|
|
4121
|
+
function AddAssemblyAliases({ changeManager, handleClose, session, }) {
|
|
4122
|
+
const { apolloDataStore } = session;
|
|
4123
|
+
const { collaborationServerDriver } = apolloDataStore;
|
|
4124
|
+
const assemblies = collaborationServerDriver.getAssemblies();
|
|
4125
|
+
const rows = assemblies.map((assembly) => {
|
|
4126
|
+
return {
|
|
4127
|
+
id: assembly.name,
|
|
4128
|
+
name: assembly.displayName ?? assembly.name,
|
|
4129
|
+
aliases: assembly.aliases.join(', '),
|
|
4130
|
+
};
|
|
4131
|
+
});
|
|
4132
|
+
const [errorMessage, setErrorMessage] = React__default["default"].useState('');
|
|
4133
|
+
const processRowUpdate = (newRow, _oldRow) => {
|
|
4134
|
+
const change = new shared.AddAssemblyAliasesChange({
|
|
4135
|
+
typeName: 'AddAssemblyAliasesChange',
|
|
4136
|
+
assembly: newRow.id,
|
|
4137
|
+
aliases: newRow.aliases.split(','),
|
|
4138
|
+
});
|
|
4139
|
+
void changeManager.submit(change).catch(() => {
|
|
4140
|
+
setErrorMessage('Error submitting change');
|
|
4141
|
+
});
|
|
4142
|
+
handleClose();
|
|
4143
|
+
return newRow;
|
|
4144
|
+
};
|
|
4145
|
+
return (React__default["default"].createElement(Dialog, { open: true, title: "Add assembly aliases", handleClose: handleClose, maxWidth: 'sm', "data-testid": "add-assembly-alias", fullWidth: true },
|
|
4146
|
+
React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
|
|
4147
|
+
React__default["default"].createElement(material.Box, { sx: { height: 400, width: '100%' } },
|
|
4148
|
+
React__default["default"].createElement(xDataGrid.DataGrid, { rows: rows, columns: columns, initialState: {
|
|
4149
|
+
pagination: {
|
|
4150
|
+
paginationModel: {
|
|
4151
|
+
pageSize: 5,
|
|
4152
|
+
},
|
|
4153
|
+
},
|
|
4154
|
+
}, pageSizeOptions: [5], processRowUpdate: processRowUpdate, disableRowSelectionOnClick: true }))),
|
|
4155
|
+
errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
|
|
4156
|
+
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
4157
|
+
}
|
|
4158
|
+
|
|
3415
4159
|
function addMenuItems(rootModel) {
|
|
3416
4160
|
rootModel.appendToMenu('Apollo', {
|
|
3417
4161
|
label: 'Add Assembly',
|
|
@@ -3473,6 +4217,21 @@ function addMenuItems(rootModel) {
|
|
|
3473
4217
|
]);
|
|
3474
4218
|
},
|
|
3475
4219
|
});
|
|
4220
|
+
rootModel.appendToMenu('Apollo', {
|
|
4221
|
+
label: 'Add Assembly aliases',
|
|
4222
|
+
onClick: (session) => {
|
|
4223
|
+
session.queueDialog((doneCallback) => [
|
|
4224
|
+
AddAssemblyAliases,
|
|
4225
|
+
{
|
|
4226
|
+
session,
|
|
4227
|
+
handleClose: () => {
|
|
4228
|
+
doneCallback();
|
|
4229
|
+
},
|
|
4230
|
+
changeManager: session.apolloDataStore.changeManager,
|
|
4231
|
+
},
|
|
4232
|
+
]);
|
|
4233
|
+
},
|
|
4234
|
+
});
|
|
3476
4235
|
rootModel.appendToMenu('Apollo', {
|
|
3477
4236
|
label: 'Manage Users',
|
|
3478
4237
|
onClick: (session) => {
|
|
@@ -4488,12 +5247,14 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
|
|
|
4488
5247
|
typeName: 'FeatureAttributeChange',
|
|
4489
5248
|
assembly,
|
|
4490
5249
|
featureId: _id,
|
|
4491
|
-
|
|
5250
|
+
oldAttributes: attributesSerialized,
|
|
5251
|
+
newAttributes: remainingAttributes,
|
|
4492
5252
|
});
|
|
4493
5253
|
void changeManager.submit(change);
|
|
4494
5254
|
}
|
|
4495
5255
|
function modifyFeatureAttribute(key, attribute) {
|
|
4496
5256
|
const serializedAttributes = { ...mobxStateTree.getSnapshot(attributes) };
|
|
5257
|
+
const oldAttributes = structuredClone(serializedAttributes);
|
|
4497
5258
|
if (!(key in serializedAttributes)) {
|
|
4498
5259
|
notify(`"${key}" not found in feature attributes`, 'error');
|
|
4499
5260
|
return;
|
|
@@ -4508,12 +5269,14 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
|
|
|
4508
5269
|
typeName: 'FeatureAttributeChange',
|
|
4509
5270
|
assembly,
|
|
4510
5271
|
featureId: feature._id,
|
|
4511
|
-
|
|
5272
|
+
oldAttributes,
|
|
5273
|
+
newAttributes: serializedAttributes,
|
|
4512
5274
|
});
|
|
4513
5275
|
void changeManager.submit(change);
|
|
4514
5276
|
}
|
|
4515
5277
|
function addFeatureAttribute(key, attribute) {
|
|
4516
5278
|
const serializedAttributes = { ...mobxStateTree.getSnapshot(attributes) };
|
|
5279
|
+
const oldAttributes = structuredClone(serializedAttributes);
|
|
4517
5280
|
if (key in serializedAttributes) {
|
|
4518
5281
|
notify(`Feature already has attribute "${key}"`, 'error');
|
|
4519
5282
|
return;
|
|
@@ -4524,7 +5287,8 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
|
|
|
4524
5287
|
typeName: 'FeatureAttributeChange',
|
|
4525
5288
|
assembly,
|
|
4526
5289
|
featureId: feature._id,
|
|
4527
|
-
|
|
5290
|
+
oldAttributes,
|
|
5291
|
+
newAttributes: serializedAttributes,
|
|
4528
5292
|
});
|
|
4529
5293
|
void changeManager.submit(change);
|
|
4530
5294
|
}
|
|
@@ -4927,6 +5691,28 @@ const ApolloTranscriptDetailsModel = mobxStateTree.types
|
|
|
4927
5691
|
},
|
|
4928
5692
|
}));
|
|
4929
5693
|
|
|
5694
|
+
async function copyToClipboard(element) {
|
|
5695
|
+
if (isSecureContext) {
|
|
5696
|
+
const textBlob = new Blob([element.outerText], { type: 'text/plain' });
|
|
5697
|
+
const htmlBlob = new Blob([element.outerHTML], { type: 'text/html' });
|
|
5698
|
+
const clipboardItem = new ClipboardItem({
|
|
5699
|
+
[textBlob.type]: textBlob,
|
|
5700
|
+
[htmlBlob.type]: htmlBlob,
|
|
5701
|
+
});
|
|
5702
|
+
return navigator.clipboard.write([clipboardItem]);
|
|
5703
|
+
}
|
|
5704
|
+
const copyCallback = (event) => {
|
|
5705
|
+
event.clipboardData?.setData('text/plain', element.outerText);
|
|
5706
|
+
event.clipboardData?.setData('text/html', element.outerHTML);
|
|
5707
|
+
event.preventDefault();
|
|
5708
|
+
};
|
|
5709
|
+
document.addEventListener('copy', copyCallback);
|
|
5710
|
+
// fall back to deprecated only in non-secure contexts
|
|
5711
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
5712
|
+
document.execCommand('copy');
|
|
5713
|
+
document.removeEventListener('copy', copyCallback);
|
|
5714
|
+
}
|
|
5715
|
+
|
|
4930
5716
|
const SEQUENCE_WRAP_LENGTH = 60;
|
|
4931
5717
|
function getSequenceSegments(segmentType, feature, getSequence) {
|
|
4932
5718
|
const segments = [];
|
|
@@ -4986,13 +5772,12 @@ function getSequenceSegments(segmentType, feature, getSequence) {
|
|
|
4986
5772
|
const [firstLocation] = cdsLocations;
|
|
4987
5773
|
const locs = [];
|
|
4988
5774
|
for (const loc of firstLocation) {
|
|
4989
|
-
|
|
4990
|
-
if (strand === -1) {
|
|
4991
|
-
sequence = util.revcom(sequence);
|
|
4992
|
-
}
|
|
4993
|
-
wholeSequence += sequence;
|
|
5775
|
+
wholeSequence += getSequence(loc.min, loc.max);
|
|
4994
5776
|
locs.push({ min: loc.min, max: loc.max });
|
|
4995
5777
|
}
|
|
5778
|
+
if (strand === -1) {
|
|
5779
|
+
wholeSequence = util.revcom(wholeSequence);
|
|
5780
|
+
}
|
|
4996
5781
|
const sequenceLines = shared.splitStringIntoChunks(wholeSequence, SEQUENCE_WRAP_LENGTH);
|
|
4997
5782
|
segments.push({ type: 'CDS', sequenceLines, locs });
|
|
4998
5783
|
return segments;
|
|
@@ -5002,13 +5787,12 @@ function getSequenceSegments(segmentType, feature, getSequence) {
|
|
|
5002
5787
|
const [firstLocation] = cdsLocations;
|
|
5003
5788
|
const locs = [];
|
|
5004
5789
|
for (const loc of firstLocation) {
|
|
5005
|
-
|
|
5006
|
-
if (strand === -1) {
|
|
5007
|
-
sequence = util.revcom(sequence);
|
|
5008
|
-
}
|
|
5009
|
-
wholeSequence += sequence;
|
|
5790
|
+
wholeSequence += getSequence(loc.min, loc.max);
|
|
5010
5791
|
locs.push({ min: loc.min, max: loc.max });
|
|
5011
5792
|
}
|
|
5793
|
+
if (strand === -1) {
|
|
5794
|
+
wholeSequence = util.revcom(wholeSequence);
|
|
5795
|
+
}
|
|
5012
5796
|
let protein = '';
|
|
5013
5797
|
for (let i = 0; i < wholeSequence.length; i += 3) {
|
|
5014
5798
|
const codonSeq = wholeSequence.slice(i, i + 3).toUpperCase();
|
|
@@ -5112,23 +5896,16 @@ const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ asse
|
|
|
5112
5896
|
setSequenceSegments(seqSegments);
|
|
5113
5897
|
setLocationIntervals(locIntervals);
|
|
5114
5898
|
}
|
|
5115
|
-
|
|
5116
|
-
const copyToClipboard = () => {
|
|
5899
|
+
const onCopyClick = () => {
|
|
5117
5900
|
const seqDiv = seqRef.current;
|
|
5118
5901
|
if (!seqDiv) {
|
|
5119
5902
|
return;
|
|
5120
5903
|
}
|
|
5121
|
-
|
|
5122
|
-
const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
|
|
5123
|
-
const clipboardItem = new ClipboardItem({
|
|
5124
|
-
[textBlob.type]: textBlob,
|
|
5125
|
-
[htmlBlob.type]: htmlBlob,
|
|
5126
|
-
});
|
|
5127
|
-
void navigator.clipboard.write([clipboardItem]);
|
|
5904
|
+
void copyToClipboard(seqDiv);
|
|
5128
5905
|
};
|
|
5129
5906
|
return (React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
5130
5907
|
React__default["default"].createElement(material.Select, { defaultValue: "genomic", value: selectedOption, onChange: handleChangeSeqOption, size: "small" }, sequenceOptions.map((option) => (React__default["default"].createElement(material.MenuItem, { key: option, value: option }, option)))),
|
|
5131
|
-
React__default["default"].createElement(material.Button, { variant: "contained", onClick:
|
|
5908
|
+
React__default["default"].createElement(material.Button, { variant: "contained", onClick: onCopyClick, style: { marginLeft: 10 }, size: "medium" }, "Copy sequence"),
|
|
5132
5909
|
React__default["default"].createElement(material.Paper, { style: {
|
|
5133
5910
|
fontFamily: 'monospace',
|
|
5134
5911
|
padding: theme.spacing(),
|
|
@@ -5185,107 +5962,512 @@ const Strand = (props) => {
|
|
|
5185
5962
|
const { strand } = props;
|
|
5186
5963
|
return (React__default["default"].createElement("div", null, strand === 1 ? (React__default["default"].createElement(AddIcon__default["default"], null)) : strand === -1 ? (React__default["default"].createElement(RemoveIcon__default["default"], null)) : (React__default["default"].createElement(material.Typography, { component: 'span' }, "N/A"))));
|
|
5187
5964
|
};
|
|
5965
|
+
const minMaxExonTranscriptLocation = (transcript, featureTypeOntology) => {
|
|
5966
|
+
const { transcriptExonParts } = transcript;
|
|
5967
|
+
const exonParts = transcriptExonParts
|
|
5968
|
+
.filter((part) => featureTypeOntology.isTypeOf(part.type, 'exon'))
|
|
5969
|
+
.sort(({ min: a }, { min: b }) => a - b);
|
|
5970
|
+
const exonMin = exonParts[0]?.min;
|
|
5971
|
+
const exonMax = exonParts[exonParts.length - 1]?.max;
|
|
5972
|
+
return [exonMin, exonMax];
|
|
5973
|
+
};
|
|
5188
5974
|
const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidgetEditLocation({ assembly, feature, refName, session, }) {
|
|
5189
5975
|
const { notify } = session;
|
|
5190
5976
|
const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
|
|
5191
5977
|
const refData = currentAssembly?.getByRefName(refName);
|
|
5192
5978
|
const { changeManager } = session.apolloDataStore;
|
|
5193
5979
|
const seqRef = React.useRef(null);
|
|
5194
|
-
|
|
5195
|
-
|
|
5980
|
+
if (!refData) {
|
|
5981
|
+
return null;
|
|
5982
|
+
}
|
|
5983
|
+
const { apolloDataStore } = session;
|
|
5984
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
5985
|
+
if (!featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
|
|
5986
|
+
!featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) {
|
|
5987
|
+
throw new Error('Feature is not a transcript or equivalent');
|
|
5988
|
+
}
|
|
5989
|
+
const { cdsLocations, transcriptExonParts, strand } = feature;
|
|
5990
|
+
const [firstCDSLocation] = cdsLocations;
|
|
5991
|
+
const [exonMin, exonMax] = minMaxExonTranscriptLocation(feature, featureTypeOntology);
|
|
5992
|
+
let cdsMin = exonMin;
|
|
5993
|
+
let cdsMax = exonMax;
|
|
5994
|
+
const cdsPresent = firstCDSLocation.length > 0;
|
|
5995
|
+
if (cdsPresent) {
|
|
5996
|
+
const sortedCDSLocations = firstCDSLocation.toSorted(({ min: a }, { min: b }) => a - b);
|
|
5997
|
+
cdsMin = sortedCDSLocations[0].min;
|
|
5998
|
+
cdsMax = sortedCDSLocations[sortedCDSLocations.length - 1].max;
|
|
5999
|
+
}
|
|
5196
6000
|
function handleCDSLocationChange(oldLocation, newLocation, feature, isMin) {
|
|
5197
6001
|
if (!feature.children) {
|
|
5198
6002
|
throw new Error('Transcript should have child features');
|
|
5199
6003
|
}
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
6004
|
+
const overlappingExon = getOverlappingExonForCDS(feature, featureTypeOntology, oldLocation, isMin);
|
|
6005
|
+
if (!overlappingExon) {
|
|
6006
|
+
notify('No matching exon found', 'error');
|
|
6007
|
+
return;
|
|
6008
|
+
}
|
|
6009
|
+
const oldExonLocation = isMin ? overlappingExon.min : overlappingExon.max;
|
|
6010
|
+
const { prevExon, nextExon } = getNeighboringExonParts(feature, featureTypeOntology, oldExonLocation, isMin);
|
|
6011
|
+
// Start location should be less than end location
|
|
6012
|
+
if (isMin && newLocation >= overlappingExon.max) {
|
|
6013
|
+
notify('Start location should be less than overlapping exon end location', 'error');
|
|
6014
|
+
return;
|
|
6015
|
+
}
|
|
6016
|
+
// End location should be greater than start location
|
|
6017
|
+
if (!isMin && newLocation <= overlappingExon.min) {
|
|
6018
|
+
notify('End location should be greater than overlapping exon start location', 'error');
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
// Changed location should be greater than end location of previous exon - give 2bp buffer
|
|
6022
|
+
if (prevExon && prevExon.max + 2 > newLocation) {
|
|
6023
|
+
notify('Start location should be greater than previous exon end location', 'error');
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
// Changed location should be less than start location of next exon
|
|
6027
|
+
if (nextExon && nextExon.min - 2 < newLocation) {
|
|
6028
|
+
notify('End location should be less than next exon start location', 'error');
|
|
6029
|
+
return;
|
|
6030
|
+
}
|
|
6031
|
+
const cdsFeature = getMatchingCDSFeature(feature, featureTypeOntology, oldLocation, isMin);
|
|
6032
|
+
if (!cdsFeature) {
|
|
6033
|
+
notify('No matching CDS feature found', 'error');
|
|
6034
|
+
return;
|
|
6035
|
+
}
|
|
6036
|
+
if (!isMin && newLocation <= cdsFeature.min) {
|
|
6037
|
+
notify('End location should be greater than CDS start location', 'error');
|
|
6038
|
+
return;
|
|
6039
|
+
}
|
|
6040
|
+
if (isMin && newLocation >= cdsFeature.max) {
|
|
6041
|
+
notify('Start location should be less than CDS end location', 'error');
|
|
6042
|
+
return;
|
|
6043
|
+
}
|
|
6044
|
+
const overlappingExonFeature = getExonFeature(feature, overlappingExon.min, overlappingExon.max, featureTypeOntology);
|
|
6045
|
+
if (!overlappingExonFeature) {
|
|
6046
|
+
notify('No matching exon feature found', 'error');
|
|
6047
|
+
return;
|
|
6048
|
+
}
|
|
6049
|
+
if (isMin && newLocation !== cdsFeature.min) {
|
|
6050
|
+
const startChange = new shared.LocationStartChange({
|
|
6051
|
+
typeName: 'LocationStartChange',
|
|
6052
|
+
changedIds: [],
|
|
6053
|
+
changes: [],
|
|
6054
|
+
assembly,
|
|
6055
|
+
});
|
|
6056
|
+
if (newLocation < overlappingExon.min) {
|
|
6057
|
+
if (prevExon) {
|
|
6058
|
+
// update exon start location
|
|
6059
|
+
appendStartLocationChange(overlappingExonFeature, startChange, newLocation);
|
|
6060
|
+
// update CDS start location
|
|
6061
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
6062
|
+
}
|
|
6063
|
+
else {
|
|
6064
|
+
const transcriptStart = feature.min;
|
|
6065
|
+
const gene = feature.parent;
|
|
6066
|
+
if (newLocation < transcriptStart) {
|
|
6067
|
+
if (gene && newLocation < gene.min) {
|
|
6068
|
+
// update gene start location
|
|
6069
|
+
appendStartLocationChange(gene, startChange, newLocation);
|
|
6070
|
+
}
|
|
6071
|
+
// update transcript start location
|
|
6072
|
+
appendStartLocationChange(feature, startChange, newLocation);
|
|
6073
|
+
// update exon start location
|
|
6074
|
+
appendStartLocationChange(overlappingExonFeature, startChange, newLocation);
|
|
6075
|
+
// update CDS start location
|
|
6076
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
else {
|
|
6081
|
+
// update CDS start location
|
|
6082
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
6083
|
+
}
|
|
6084
|
+
void changeManager.submit(startChange).catch(() => {
|
|
6085
|
+
notify('Error updating feature CDS start position', 'error');
|
|
6086
|
+
});
|
|
6087
|
+
}
|
|
6088
|
+
if (!isMin && newLocation !== cdsFeature.max) {
|
|
6089
|
+
const endChange = new shared.LocationEndChange({
|
|
6090
|
+
typeName: 'LocationEndChange',
|
|
6091
|
+
changedIds: [],
|
|
6092
|
+
changes: [],
|
|
6093
|
+
assembly,
|
|
6094
|
+
});
|
|
6095
|
+
if (newLocation > overlappingExon.max) {
|
|
6096
|
+
if (nextExon) {
|
|
6097
|
+
// update exon end location
|
|
6098
|
+
appendEndLocationChange(overlappingExonFeature, endChange, newLocation);
|
|
6099
|
+
// update CDS end location
|
|
6100
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
6101
|
+
}
|
|
6102
|
+
else {
|
|
6103
|
+
const transcriptEnd = feature.max;
|
|
6104
|
+
const gene = feature.parent;
|
|
6105
|
+
if (newLocation > transcriptEnd) {
|
|
6106
|
+
if (gene && newLocation > gene.max) {
|
|
6107
|
+
// update gene end location
|
|
6108
|
+
appendEndLocationChange(gene, endChange, newLocation);
|
|
6109
|
+
}
|
|
6110
|
+
// update transcript end location
|
|
6111
|
+
appendEndLocationChange(feature, endChange, newLocation);
|
|
6112
|
+
// update exon end location
|
|
6113
|
+
appendEndLocationChange(overlappingExonFeature, endChange, newLocation);
|
|
6114
|
+
// update CDS end location
|
|
6115
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
else {
|
|
6120
|
+
// update CDS end location
|
|
6121
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
5231
6122
|
}
|
|
6123
|
+
void changeManager.submit(endChange).catch(() => {
|
|
6124
|
+
notify('Error updating feature CDS end position', 'error');
|
|
6125
|
+
});
|
|
5232
6126
|
}
|
|
5233
6127
|
}
|
|
6128
|
+
const updateCDSLocation = (oldLocation, newLocation, feature, isMin, onComplete) => {
|
|
6129
|
+
if (!feature.children) {
|
|
6130
|
+
throw new Error('Transcript should have child features');
|
|
6131
|
+
}
|
|
6132
|
+
if (oldLocation === newLocation) {
|
|
6133
|
+
return;
|
|
6134
|
+
}
|
|
6135
|
+
const cdsFeature = getMatchingCDSFeature(feature, featureTypeOntology, oldLocation, isMin);
|
|
6136
|
+
if (!cdsFeature) {
|
|
6137
|
+
notify('No matching CDS feature found', 'error');
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
const change = isMin
|
|
6141
|
+
? new shared.LocationStartChange({
|
|
6142
|
+
typeName: 'LocationStartChange',
|
|
6143
|
+
changedIds: [cdsFeature._id],
|
|
6144
|
+
featureId: cdsFeature._id,
|
|
6145
|
+
oldStart: cdsFeature.min,
|
|
6146
|
+
newStart: newLocation,
|
|
6147
|
+
assembly,
|
|
6148
|
+
})
|
|
6149
|
+
: new shared.LocationEndChange({
|
|
6150
|
+
typeName: 'LocationEndChange',
|
|
6151
|
+
changedIds: [cdsFeature._id],
|
|
6152
|
+
featureId: cdsFeature._id,
|
|
6153
|
+
oldEnd: cdsFeature.max,
|
|
6154
|
+
newEnd: newLocation,
|
|
6155
|
+
assembly,
|
|
6156
|
+
});
|
|
6157
|
+
void changeManager
|
|
6158
|
+
.submit(change)
|
|
6159
|
+
.then(() => {
|
|
6160
|
+
if (onComplete) {
|
|
6161
|
+
onComplete();
|
|
6162
|
+
}
|
|
6163
|
+
})
|
|
6164
|
+
.catch(() => {
|
|
6165
|
+
notify('Error updating feature CDS position', 'error');
|
|
6166
|
+
});
|
|
6167
|
+
};
|
|
5234
6168
|
function handleExonLocationChange(oldLocation, newLocation, feature, isMin) {
|
|
5235
6169
|
if (!feature.children) {
|
|
5236
6170
|
throw new Error('Transcript should have child features');
|
|
5237
6171
|
}
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
6172
|
+
const { matchingExon, prevExon, nextExon } = getNeighboringExonParts(feature, featureTypeOntology, oldLocation, isMin);
|
|
6173
|
+
if (!matchingExon) {
|
|
6174
|
+
notify('No matching exon found', 'error');
|
|
6175
|
+
return;
|
|
6176
|
+
}
|
|
6177
|
+
// Start location should be less than end location
|
|
6178
|
+
if (isMin && newLocation >= matchingExon.max) {
|
|
6179
|
+
notify(`Start location should be less than end location`, 'error');
|
|
6180
|
+
return;
|
|
6181
|
+
}
|
|
6182
|
+
// End location should be greater than start location
|
|
6183
|
+
if (!isMin && newLocation <= matchingExon.min) {
|
|
6184
|
+
notify(`End location should be greater than start location`, 'error');
|
|
6185
|
+
return;
|
|
6186
|
+
}
|
|
6187
|
+
// Changed location should be greater than end location of previous exon - give 2bp buffer
|
|
6188
|
+
if (prevExon && prevExon.max + 2 > newLocation) {
|
|
6189
|
+
notify(`Error while changing start location`, 'error');
|
|
6190
|
+
return;
|
|
6191
|
+
}
|
|
6192
|
+
// Changed location should be less than start location of next exon - give 2bp buffer
|
|
6193
|
+
if (nextExon && nextExon.min - 2 < newLocation) {
|
|
6194
|
+
notify(`Error while changing end location`, 'error');
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
const exonFeature = getExonFeature(feature, matchingExon.min, matchingExon.max, featureTypeOntology);
|
|
6198
|
+
if (!exonFeature) {
|
|
6199
|
+
notify('No matching exon feature found', 'error');
|
|
6200
|
+
return;
|
|
6201
|
+
}
|
|
6202
|
+
const cdsFeature = getFirstCDSFeature(feature, featureTypeOntology);
|
|
6203
|
+
// START LOCATION CHANGE
|
|
6204
|
+
if (isMin && newLocation !== matchingExon.min) {
|
|
6205
|
+
const startChange = new shared.LocationStartChange({
|
|
6206
|
+
typeName: 'LocationStartChange',
|
|
6207
|
+
changedIds: [],
|
|
6208
|
+
changes: [],
|
|
6209
|
+
assembly,
|
|
6210
|
+
});
|
|
6211
|
+
if (prevExon) {
|
|
6212
|
+
// update exon start location
|
|
6213
|
+
appendStartLocationChange(exonFeature, startChange, newLocation);
|
|
5241
6214
|
}
|
|
5242
|
-
|
|
5243
|
-
const
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
6215
|
+
else {
|
|
6216
|
+
const transcriptStart = feature.min;
|
|
6217
|
+
const gene = feature.parent;
|
|
6218
|
+
if (newLocation < transcriptStart) {
|
|
6219
|
+
if (gene && newLocation < gene.min) {
|
|
6220
|
+
// update gene start location
|
|
6221
|
+
appendStartLocationChange(gene, startChange, newLocation);
|
|
6222
|
+
}
|
|
6223
|
+
// update transcript start location
|
|
6224
|
+
appendStartLocationChange(feature, startChange, newLocation);
|
|
6225
|
+
// update exon start location
|
|
6226
|
+
appendStartLocationChange(exonFeature, startChange, newLocation);
|
|
6227
|
+
}
|
|
6228
|
+
else if (newLocation > transcriptStart) {
|
|
6229
|
+
// update exon start location
|
|
6230
|
+
appendStartLocationChange(exonFeature, startChange, newLocation);
|
|
6231
|
+
// update transcript start location
|
|
6232
|
+
appendStartLocationChange(feature, startChange, newLocation);
|
|
6233
|
+
if (gene) {
|
|
6234
|
+
const [geneMinWithNewLoc] = geneMinMaxWithNewLocation(gene, feature, newLocation, featureTypeOntology, isMin);
|
|
6235
|
+
if (gene.min != geneMinWithNewLoc) {
|
|
6236
|
+
// update gene start location
|
|
6237
|
+
appendStartLocationChange(gene, startChange, geneMinWithNewLoc);
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
5255
6241
|
}
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
});
|
|
5265
|
-
changeManager.submit(change).catch(() => {
|
|
5266
|
-
notify('Error updating feature start position', 'error');
|
|
5267
|
-
});
|
|
5268
|
-
return;
|
|
6242
|
+
// When we change the start location of the exon overlapping with start location of the CDS
|
|
6243
|
+
// and the new start location is greater than the CDS start location then we need to update the CDS start location
|
|
6244
|
+
if (cdsFeature &&
|
|
6245
|
+
cdsFeature.min >= matchingExon.min &&
|
|
6246
|
+
cdsFeature.min <= matchingExon.max &&
|
|
6247
|
+
newLocation > cdsFeature.min) {
|
|
6248
|
+
// update CDS start location
|
|
6249
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation);
|
|
5269
6250
|
}
|
|
6251
|
+
void changeManager.submit(startChange).catch(() => {
|
|
6252
|
+
notify('Error updating feature exon start position', 'error');
|
|
6253
|
+
});
|
|
6254
|
+
}
|
|
6255
|
+
// END LOCATION CHANGE
|
|
6256
|
+
if (!isMin && newLocation !== matchingExon.max) {
|
|
6257
|
+
const endChange = new shared.LocationEndChange({
|
|
6258
|
+
typeName: 'LocationEndChange',
|
|
6259
|
+
changedIds: [],
|
|
6260
|
+
changes: [],
|
|
6261
|
+
assembly,
|
|
6262
|
+
});
|
|
6263
|
+
if (nextExon) {
|
|
6264
|
+
// update exon end location
|
|
6265
|
+
appendEndLocationChange(exonFeature, endChange, newLocation);
|
|
6266
|
+
}
|
|
6267
|
+
else {
|
|
6268
|
+
const transcriptEnd = feature.max;
|
|
6269
|
+
const gene = feature.parent;
|
|
6270
|
+
if (newLocation > transcriptEnd) {
|
|
6271
|
+
if (gene && newLocation > gene.max) {
|
|
6272
|
+
// update gene end location
|
|
6273
|
+
appendEndLocationChange(gene, endChange, newLocation);
|
|
6274
|
+
}
|
|
6275
|
+
// update transcript end location
|
|
6276
|
+
appendEndLocationChange(feature, endChange, newLocation);
|
|
6277
|
+
// update exon end location
|
|
6278
|
+
appendEndLocationChange(exonFeature, endChange, newLocation);
|
|
6279
|
+
}
|
|
6280
|
+
else if (newLocation < transcriptEnd) {
|
|
6281
|
+
// update exon end location
|
|
6282
|
+
appendEndLocationChange(exonFeature, endChange, newLocation);
|
|
6283
|
+
// update transcript end location
|
|
6284
|
+
appendEndLocationChange(feature, endChange, newLocation);
|
|
6285
|
+
if (gene) {
|
|
6286
|
+
const [, geneMaxWithNewLoc] = geneMinMaxWithNewLocation(gene, feature, newLocation, featureTypeOntology, isMin);
|
|
6287
|
+
if (gene.max != geneMaxWithNewLoc) {
|
|
6288
|
+
// update gene end location
|
|
6289
|
+
appendEndLocationChange(gene, endChange, geneMaxWithNewLoc);
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
// When we change the end location of the exon overlapping with end location of the CDS
|
|
6295
|
+
// and the new end location is less than the CDS end location then we need to update the CDS end location
|
|
6296
|
+
if (cdsFeature &&
|
|
6297
|
+
cdsFeature.max >= matchingExon.min &&
|
|
6298
|
+
cdsFeature.max <= matchingExon.max &&
|
|
6299
|
+
newLocation < cdsFeature.max) {
|
|
6300
|
+
// update CDS end location
|
|
6301
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation);
|
|
6302
|
+
}
|
|
6303
|
+
void changeManager.submit(endChange).catch(() => {
|
|
6304
|
+
notify('Error updating feature exon end position', 'error');
|
|
6305
|
+
});
|
|
5270
6306
|
}
|
|
5271
6307
|
}
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
const
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
6308
|
+
const appendEndLocationChange = (feature, change, newLocation) => {
|
|
6309
|
+
change.changedIds.push(feature._id);
|
|
6310
|
+
change.changes.push({
|
|
6311
|
+
featureId: feature._id,
|
|
6312
|
+
oldEnd: feature.max,
|
|
6313
|
+
newEnd: newLocation,
|
|
6314
|
+
});
|
|
6315
|
+
};
|
|
6316
|
+
const appendStartLocationChange = (feature, change, newLocation) => {
|
|
6317
|
+
change.changedIds.push(feature._id);
|
|
6318
|
+
change.changes.push({
|
|
6319
|
+
featureId: feature._id,
|
|
6320
|
+
oldStart: feature.min,
|
|
6321
|
+
newStart: newLocation,
|
|
6322
|
+
});
|
|
6323
|
+
};
|
|
6324
|
+
const getMatchingCDSFeature = (feature, featureTypeOntology, oldCDSLocation, isMin) => {
|
|
6325
|
+
let cdsFeature;
|
|
6326
|
+
for (const [, child] of feature.children ?? []) {
|
|
6327
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
|
|
6328
|
+
continue;
|
|
6329
|
+
}
|
|
6330
|
+
if (isMin && oldCDSLocation === child.min) {
|
|
6331
|
+
cdsFeature = child;
|
|
6332
|
+
break;
|
|
6333
|
+
}
|
|
6334
|
+
if (!isMin && oldCDSLocation === child.max) {
|
|
6335
|
+
cdsFeature = child;
|
|
6336
|
+
break;
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
return cdsFeature;
|
|
6340
|
+
};
|
|
6341
|
+
const getFirstCDSFeature = (feature, featureTypeOntology) => {
|
|
6342
|
+
let cdsFeature;
|
|
6343
|
+
for (const [, child] of feature.children ?? []) {
|
|
6344
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
|
|
6345
|
+
continue;
|
|
6346
|
+
}
|
|
6347
|
+
cdsFeature = child;
|
|
6348
|
+
break;
|
|
6349
|
+
}
|
|
6350
|
+
return cdsFeature;
|
|
6351
|
+
};
|
|
6352
|
+
const getExonFeature = (feature, exonMin, exonMax, featureTypeOntology) => {
|
|
6353
|
+
let exonFeature;
|
|
6354
|
+
for (const [, child] of feature.children ?? []) {
|
|
6355
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
6356
|
+
continue;
|
|
6357
|
+
}
|
|
6358
|
+
if (exonMin === child.min && exonMax === child.max) {
|
|
6359
|
+
exonFeature = child;
|
|
6360
|
+
break;
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
return exonFeature;
|
|
6364
|
+
};
|
|
6365
|
+
const geneMinMaxWithNewLocation = (gene, transcript, newLocation, featureTypeOntology, isMin) => {
|
|
6366
|
+
const mins = [];
|
|
6367
|
+
const maxs = [];
|
|
6368
|
+
for (const [, t] of gene.children?.entries() ?? []) {
|
|
6369
|
+
if (!featureTypeOntology.isTypeOf(t.type, 'transcript')) {
|
|
6370
|
+
continue;
|
|
6371
|
+
}
|
|
6372
|
+
if (t._id === transcript._id) {
|
|
6373
|
+
if (isMin) {
|
|
6374
|
+
mins.push(newLocation);
|
|
6375
|
+
maxs.push(t.max);
|
|
6376
|
+
}
|
|
6377
|
+
else {
|
|
6378
|
+
maxs.push(newLocation);
|
|
6379
|
+
mins.push(t.min);
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
else {
|
|
6383
|
+
mins.push(t.min);
|
|
6384
|
+
maxs.push(t.max);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
const newMin = Math.min(...mins);
|
|
6388
|
+
const newMax = Math.max(...maxs);
|
|
6389
|
+
return [newMin, newMax];
|
|
6390
|
+
};
|
|
6391
|
+
const getOverlappingExonForCDS = (transcript, featureTypeOntology, oldCDSLocation, isMin) => {
|
|
6392
|
+
const { transcriptExonParts } = transcript;
|
|
6393
|
+
let overlappingExonPart;
|
|
6394
|
+
for (const [, exonPart] of transcriptExonParts.entries()) {
|
|
6395
|
+
if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
|
|
6396
|
+
continue;
|
|
6397
|
+
}
|
|
6398
|
+
if (!isMin &&
|
|
6399
|
+
oldCDSLocation >= exonPart.min &&
|
|
6400
|
+
oldCDSLocation <= exonPart.max) {
|
|
6401
|
+
overlappingExonPart = exonPart;
|
|
6402
|
+
break;
|
|
6403
|
+
}
|
|
6404
|
+
if (isMin &&
|
|
6405
|
+
oldCDSLocation >= exonPart.min &&
|
|
6406
|
+
oldCDSLocation <= exonPart.max) {
|
|
6407
|
+
overlappingExonPart = exonPart;
|
|
6408
|
+
break;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
return overlappingExonPart;
|
|
6412
|
+
};
|
|
6413
|
+
const getNeighboringExonParts = (transcript, featureTypeOntology, oldExonLoc, isMin) => {
|
|
6414
|
+
const { transcriptExonParts, strand } = transcript;
|
|
6415
|
+
let matchingExon, matchingExonIdx, prevExon, nextExon;
|
|
6416
|
+
for (const [i, exonPart] of transcriptExonParts.entries()) {
|
|
6417
|
+
if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
|
|
6418
|
+
continue;
|
|
6419
|
+
}
|
|
6420
|
+
if (isMin && exonPart.min === oldExonLoc) {
|
|
6421
|
+
matchingExon = exonPart;
|
|
6422
|
+
matchingExonIdx = i;
|
|
6423
|
+
break;
|
|
6424
|
+
}
|
|
6425
|
+
if (!isMin && exonPart.max === oldExonLoc) {
|
|
6426
|
+
matchingExon = exonPart;
|
|
6427
|
+
matchingExonIdx = i;
|
|
6428
|
+
break;
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
if (matchingExon && matchingExonIdx !== undefined) {
|
|
6432
|
+
if (strand === 1 && matchingExonIdx > 0) {
|
|
6433
|
+
for (let i = matchingExonIdx - 1; i >= 0; i--) {
|
|
6434
|
+
const prevLoc = transcriptExonParts[i];
|
|
6435
|
+
if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
|
|
6436
|
+
prevExon = prevLoc;
|
|
6437
|
+
break;
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
if (strand === -1 && matchingExonIdx < transcriptExonParts.length - 1) {
|
|
6442
|
+
for (let i = matchingExonIdx + 1; i < transcriptExonParts.length; i++) {
|
|
6443
|
+
const prevLoc = transcriptExonParts[i];
|
|
6444
|
+
if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
|
|
6445
|
+
prevExon = prevLoc;
|
|
6446
|
+
break;
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
if (strand === 1 && matchingExonIdx < transcriptExonParts.length - 1) {
|
|
6451
|
+
for (let i = matchingExonIdx + 1; i < transcriptExonParts.length; i++) {
|
|
6452
|
+
const nextLoc = transcriptExonParts[i];
|
|
6453
|
+
if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
|
|
6454
|
+
nextExon = nextLoc;
|
|
6455
|
+
break;
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
if (strand === -1 && matchingExonIdx > 0) {
|
|
6460
|
+
for (let i = matchingExonIdx - 1; i >= 0; i--) {
|
|
6461
|
+
const nextLoc = transcriptExonParts[i];
|
|
6462
|
+
if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
|
|
6463
|
+
nextExon = nextLoc;
|
|
6464
|
+
break;
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
return { matchingExon, prevExon, nextExon };
|
|
6470
|
+
};
|
|
5289
6471
|
const getFivePrimeSpliceSite = (loc, prevLocIdx) => {
|
|
5290
6472
|
let spliceSite = '';
|
|
5291
6473
|
if (prevLocIdx > 0) {
|
|
@@ -5333,12 +6515,15 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
|
|
|
5333
6515
|
const getTranslationSequence = () => {
|
|
5334
6516
|
let wholeSequence = '';
|
|
5335
6517
|
const [firstLocation] = cdsLocations;
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
6518
|
+
const sortedCDSLocations = firstLocation.toSorted(({ min: a }, { min: b }) => a - b);
|
|
6519
|
+
for (const loc of sortedCDSLocations) {
|
|
6520
|
+
wholeSequence += refData.getSequence(loc.min, loc.max);
|
|
6521
|
+
}
|
|
6522
|
+
if (strand === -1) {
|
|
6523
|
+
// Original: ACGCAT
|
|
6524
|
+
// Complement: TGCGTA
|
|
6525
|
+
// Reverse complement: ATGCGT
|
|
6526
|
+
wholeSequence = util.revcom(wholeSequence);
|
|
5342
6527
|
}
|
|
5343
6528
|
const elements = [];
|
|
5344
6529
|
for (let codonGenomicPos = 0; codonGenomicPos < wholeSequence.length; codonGenomicPos += 3) {
|
|
@@ -5356,9 +6541,12 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
|
|
|
5356
6541
|
// NOTE: codonGenomicPos is important here for calculating the genomic location
|
|
5357
6542
|
// of the start codon. We are using the codonGenomicPos as the key in the typography
|
|
5358
6543
|
// elements to maintain the genomic postion of the codon start
|
|
5359
|
-
const startCodonGenomicLocation =
|
|
5360
|
-
if (startCodonGenomicLocation !== cdsMin) {
|
|
5361
|
-
|
|
6544
|
+
const startCodonGenomicLocation = getCodonGenomicLocation(codonGenomicPos);
|
|
6545
|
+
if (startCodonGenomicLocation !== cdsMin && strand === 1) {
|
|
6546
|
+
updateCDSLocation(cdsMin, startCodonGenomicLocation, feature, true);
|
|
6547
|
+
}
|
|
6548
|
+
if (startCodonGenomicLocation !== cdsMax && strand === -1) {
|
|
6549
|
+
updateCDSLocation(cdsMax, startCodonGenomicLocation, feature, false);
|
|
5362
6550
|
}
|
|
5363
6551
|
} }, protein));
|
|
5364
6552
|
}
|
|
@@ -5377,32 +6565,36 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
|
|
|
5377
6565
|
};
|
|
5378
6566
|
// Codon position is the index of the start codon in the CDS genomic sequence
|
|
5379
6567
|
// Calculate the genomic location of the start codon based on the codon position in the CDS
|
|
5380
|
-
const
|
|
6568
|
+
const getCodonGenomicLocation = (codonGenomicPosition) => {
|
|
5381
6569
|
const [firstLocation] = cdsLocations;
|
|
5382
6570
|
let cdsLen = 0;
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
6571
|
+
const sortedCDSLocations = firstLocation.toSorted(({ min: a }, { min: b }) => a - b);
|
|
6572
|
+
// Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
|
|
6573
|
+
// and codonGenomicPosition is 25
|
|
6574
|
+
// ((10 - 0) + (30 - 20) + (50 - 40)) > 25
|
|
6575
|
+
// So, start codon is in (40, 50)
|
|
6576
|
+
// 40 + (25-20) = 45 is the genomic location of the start codon
|
|
6577
|
+
if (strand === 1) {
|
|
6578
|
+
for (const loc of sortedCDSLocations) {
|
|
6579
|
+
const locLength = loc.max - loc.min;
|
|
6580
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
6581
|
+
return loc.min + (codonGenomicPosition - cdsLen);
|
|
6582
|
+
}
|
|
6583
|
+
cdsLen += locLength;
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
else if (strand === -1) {
|
|
6587
|
+
for (let i = sortedCDSLocations.length - 1; i >= 0; i--) {
|
|
6588
|
+
const loc = sortedCDSLocations[i];
|
|
6589
|
+
const locLength = loc.max - loc.min;
|
|
6590
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
6591
|
+
return loc.max - (codonGenomicPosition - cdsLen);
|
|
6592
|
+
}
|
|
6593
|
+
cdsLen += locLength;
|
|
5404
6594
|
}
|
|
5405
|
-
|
|
6595
|
+
}
|
|
6596
|
+
if (strand === 1) {
|
|
6597
|
+
return cdsMin;
|
|
5406
6598
|
}
|
|
5407
6599
|
return cdsMax;
|
|
5408
6600
|
};
|
|
@@ -5427,48 +6619,82 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
|
|
|
5427
6619
|
if (translSeqCodonStartGenomicPosArr.length === 0) {
|
|
5428
6620
|
return;
|
|
5429
6621
|
}
|
|
5430
|
-
// Trim any sequence before first start codon and after
|
|
6622
|
+
// Trim any sequence before first start codon and after stop codon
|
|
5431
6623
|
const startCodonIndex = translationSequence.indexOf('M');
|
|
5432
|
-
const stopCodonIndex = translationSequence.
|
|
6624
|
+
const stopCodonIndex = translationSequence.indexOf('*') + 1;
|
|
5433
6625
|
const startCodonPos = translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos;
|
|
5434
6626
|
const stopCodonPos = translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos;
|
|
5435
6627
|
if (!startCodonPos || !stopCodonPos) {
|
|
5436
6628
|
return;
|
|
5437
6629
|
}
|
|
5438
|
-
const startCodonGenomicLoc =
|
|
5439
|
-
const stopCodonGenomicLoc =
|
|
5440
|
-
if (
|
|
5441
|
-
|
|
6630
|
+
const startCodonGenomicLoc = getCodonGenomicLocation(startCodonPos);
|
|
6631
|
+
const stopCodonGenomicLoc = getCodonGenomicLocation(stopCodonPos);
|
|
6632
|
+
if (strand === 1) {
|
|
6633
|
+
if (startCodonGenomicLoc > stopCodonGenomicLoc) {
|
|
6634
|
+
notify('Start codon genomic location should be less than stop codon genomic location', 'error');
|
|
6635
|
+
return;
|
|
6636
|
+
}
|
|
6637
|
+
let promise;
|
|
6638
|
+
if (startCodonGenomicLoc !== cdsMin) {
|
|
6639
|
+
promise = new Promise((resolve) => {
|
|
6640
|
+
updateCDSLocation(cdsMin, startCodonGenomicLoc, feature, true, () => {
|
|
6641
|
+
resolve(true);
|
|
6642
|
+
});
|
|
6643
|
+
});
|
|
6644
|
+
}
|
|
6645
|
+
if (stopCodonGenomicLoc !== cdsMax) {
|
|
6646
|
+
if (promise) {
|
|
6647
|
+
void promise.then(() => {
|
|
6648
|
+
updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false);
|
|
6649
|
+
});
|
|
6650
|
+
}
|
|
6651
|
+
else {
|
|
6652
|
+
updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false);
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
5442
6655
|
}
|
|
5443
|
-
if (
|
|
5444
|
-
//
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
}
|
|
6656
|
+
if (strand === -1) {
|
|
6657
|
+
// reverse strand
|
|
6658
|
+
if (startCodonGenomicLoc < stopCodonGenomicLoc) {
|
|
6659
|
+
notify('Start codon genomic location should be less than stop codon genomic location', 'error');
|
|
6660
|
+
return;
|
|
6661
|
+
}
|
|
6662
|
+
let promise;
|
|
6663
|
+
if (startCodonGenomicLoc !== cdsMax) {
|
|
6664
|
+
promise = new Promise((resolve) => {
|
|
6665
|
+
updateCDSLocation(cdsMax, startCodonGenomicLoc, feature, false, () => {
|
|
6666
|
+
resolve(true);
|
|
6667
|
+
});
|
|
6668
|
+
});
|
|
6669
|
+
}
|
|
6670
|
+
if (stopCodonGenomicLoc !== cdsMin) {
|
|
6671
|
+
if (promise) {
|
|
6672
|
+
void promise.then(() => {
|
|
6673
|
+
updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true);
|
|
6674
|
+
});
|
|
6675
|
+
}
|
|
6676
|
+
else {
|
|
6677
|
+
updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true);
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
5449
6680
|
}
|
|
6681
|
+
notify('Translation sequence trimmed to start and stop codons', 'success');
|
|
5450
6682
|
};
|
|
5451
|
-
const
|
|
6683
|
+
const onCopyClick = () => {
|
|
5452
6684
|
const seqDiv = seqRef.current;
|
|
5453
6685
|
if (!seqDiv) {
|
|
5454
6686
|
return;
|
|
5455
6687
|
}
|
|
5456
|
-
|
|
5457
|
-
const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
|
|
5458
|
-
const clipboardItem = new ClipboardItem({
|
|
5459
|
-
[textBlob.type]: textBlob,
|
|
5460
|
-
[htmlBlob.type]: htmlBlob,
|
|
5461
|
-
});
|
|
5462
|
-
void navigator.clipboard.write([clipboardItem]);
|
|
6688
|
+
void copyToClipboard(seqDiv);
|
|
5463
6689
|
};
|
|
5464
6690
|
return (React__default["default"].createElement("div", null,
|
|
5465
6691
|
cdsPresent && (React__default["default"].createElement("div", null,
|
|
5466
|
-
React__default["default"].createElement(material.Accordion,
|
|
6692
|
+
React__default["default"].createElement(material.Accordion, null,
|
|
5467
6693
|
React__default["default"].createElement(StyledAccordionSummary, { expandIcon: React__default["default"].createElement(ExpandMoreIcon__default["default"], { style: { color: 'white' } }), "aria-controls": "panel1-content", id: "panel1-header" },
|
|
5468
6694
|
React__default["default"].createElement(material.Typography, { component: "span", fontWeight: 'bold' }, "Translation")),
|
|
5469
6695
|
React__default["default"].createElement(material.AccordionDetails, null,
|
|
5470
6696
|
React__default["default"].createElement(SequenceContainer, null,
|
|
5471
|
-
React__default["default"].createElement(material.Typography, { component: 'span', ref: seqRef }, getTranslationSequence())),
|
|
6697
|
+
React__default["default"].createElement(material.Typography, { component: 'span', ref: seqRef, style: { maxHeight: 120, overflowY: 'scroll' } }, getTranslationSequence())),
|
|
5472
6698
|
React__default["default"].createElement("div", { style: {
|
|
5473
6699
|
marginTop: 10,
|
|
5474
6700
|
display: 'flex',
|
|
@@ -5477,36 +6703,48 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
|
|
|
5477
6703
|
gap: 10,
|
|
5478
6704
|
} },
|
|
5479
6705
|
React__default["default"].createElement(material.Tooltip, { title: "Copy" },
|
|
5480
|
-
React__default["default"].createElement(ContentCopyIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick:
|
|
6706
|
+
React__default["default"].createElement(ContentCopyIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick: onCopyClick })),
|
|
5481
6707
|
React__default["default"].createElement(material.Tooltip, { title: "Trim" },
|
|
5482
6708
|
React__default["default"].createElement(ContentCutIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick: trimTranslationSequence }))))),
|
|
5483
6709
|
React__default["default"].createElement(material.Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center', marginTop: 10 } },
|
|
5484
6710
|
React__default["default"].createElement(material.Grid2, { size: 1 }),
|
|
5485
|
-
React__default["default"].createElement(material.Grid2, { size: 4 },
|
|
5486
|
-
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin, onChangeCommitted: (newLocation) => {
|
|
5487
|
-
handleCDSLocationChange(cdsMin, newLocation, feature, true);
|
|
5488
|
-
} })),
|
|
6711
|
+
strand === 1 ? (React__default["default"].createElement(material.Grid2, { size: 4 },
|
|
6712
|
+
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin + 1, onChangeCommitted: (newLocation) => {
|
|
6713
|
+
handleCDSLocationChange(cdsMin, newLocation - 1, feature, true);
|
|
6714
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))) : (React__default["default"].createElement(material.Grid2, { size: 4 },
|
|
6715
|
+
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
|
|
6716
|
+
handleCDSLocationChange(cdsMax, newLocation, feature, false);
|
|
6717
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))),
|
|
5489
6718
|
React__default["default"].createElement(material.Grid2, { size: 2 },
|
|
5490
6719
|
React__default["default"].createElement(material.Typography, { component: 'span' }, "CDS")),
|
|
5491
|
-
React__default["default"].createElement(material.Grid2, { size: 4 },
|
|
6720
|
+
strand === 1 ? (React__default["default"].createElement(material.Grid2, { size: 4 },
|
|
5492
6721
|
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
|
|
5493
6722
|
handleCDSLocationChange(cdsMax, newLocation, feature, false);
|
|
5494
|
-
} })),
|
|
6723
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))) : (React__default["default"].createElement(material.Grid2, { size: 4 },
|
|
6724
|
+
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin + 1, onChangeCommitted: (newLocation) => {
|
|
6725
|
+
handleCDSLocationChange(cdsMin, newLocation - 1, feature, true);
|
|
6726
|
+
}, style: { border: '1px solid black', borderRadius: 5 } }))),
|
|
5495
6727
|
React__default["default"].createElement(material.Grid2, { size: 1 })))),
|
|
5496
6728
|
React__default["default"].createElement("div", { style: { marginTop: 5 } }, transcriptExonParts.map((loc, index) => {
|
|
5497
6729
|
return (React__default["default"].createElement("div", { key: index }, loc.type === 'exon' && (React__default["default"].createElement(material.Grid2, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center' } },
|
|
5498
6730
|
React__default["default"].createElement(material.Grid2, { size: 1 }, index !== 0 &&
|
|
5499
6731
|
getFivePrimeSpliceSite(loc, index).map((site, idx) => (React__default["default"].createElement(material.Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite)))),
|
|
5500
|
-
React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
|
|
5501
|
-
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min, onChangeCommitted: (newLocation) => {
|
|
5502
|
-
handleExonLocationChange(loc.min, newLocation, feature, true);
|
|
5503
|
-
} })),
|
|
6732
|
+
strand === 1 ? (React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
|
|
6733
|
+
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min + 1, onChangeCommitted: (newLocation) => {
|
|
6734
|
+
handleExonLocationChange(loc.min, newLocation - 1, feature, true);
|
|
6735
|
+
} }))) : (React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
|
|
6736
|
+
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
|
|
6737
|
+
handleExonLocationChange(loc.max, newLocation, feature, false);
|
|
6738
|
+
} }))),
|
|
5504
6739
|
React__default["default"].createElement(material.Grid2, { size: 2 },
|
|
5505
6740
|
React__default["default"].createElement(Strand, { strand: feature.strand })),
|
|
5506
|
-
React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
|
|
6741
|
+
strand === 1 ? (React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
|
|
5507
6742
|
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
|
|
5508
6743
|
handleExonLocationChange(loc.max, newLocation, feature, false);
|
|
5509
|
-
} })),
|
|
6744
|
+
} }))) : (React__default["default"].createElement(material.Grid2, { size: 4, style: { padding: 0 } },
|
|
6745
|
+
React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min + 1, onChangeCommitted: (newLocation) => {
|
|
6746
|
+
handleExonLocationChange(loc.min, newLocation - 1, feature, true);
|
|
6747
|
+
} }))),
|
|
5510
6748
|
React__default["default"].createElement(material.Grid2, { size: 1 }, index !== transcriptExonParts.length - 1 &&
|
|
5511
6749
|
getThreePrimeSpliceSite(loc, index).map((site, idx) => (React__default["default"].createElement(material.Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite))))))));
|
|
5512
6750
|
}))));
|
|
@@ -5517,16 +6755,19 @@ const HeaderTableCell = styled__default["default"](material.TableCell)(() => ({
|
|
|
5517
6755
|
}));
|
|
5518
6756
|
const TranscriptWidgetSummary = mobxReact.observer(function TranscriptWidgetSummary(props) {
|
|
5519
6757
|
const { feature } = props;
|
|
5520
|
-
const name = getFeatureName(feature);
|
|
6758
|
+
const name = getFeatureName$1(feature);
|
|
5521
6759
|
const id = getFeatureId$1(feature);
|
|
5522
6760
|
return (React__default["default"].createElement(material.Table, { size: "small", sx: { fontSize: '0.75rem', '& .MuiTableCell-root': { padding: '4px' } } },
|
|
5523
6761
|
React__default["default"].createElement(material.TableBody, null,
|
|
5524
6762
|
name !== '' && (React__default["default"].createElement(material.TableRow, null,
|
|
5525
6763
|
React__default["default"].createElement(HeaderTableCell, null, "Name"),
|
|
5526
|
-
React__default["default"].createElement(material.TableCell, null, getFeatureName(feature)))),
|
|
6764
|
+
React__default["default"].createElement(material.TableCell, null, getFeatureName$1(feature)))),
|
|
5527
6765
|
id !== '' && (React__default["default"].createElement(material.TableRow, null,
|
|
5528
6766
|
React__default["default"].createElement(HeaderTableCell, null, "ID"),
|
|
5529
6767
|
React__default["default"].createElement(material.TableCell, null, getFeatureId$1(feature)))),
|
|
6768
|
+
React__default["default"].createElement(material.TableRow, null,
|
|
6769
|
+
React__default["default"].createElement(HeaderTableCell, null, "Type"),
|
|
6770
|
+
React__default["default"].createElement(material.TableCell, null, feature.type)),
|
|
5530
6771
|
React__default["default"].createElement(material.TableRow, null,
|
|
5531
6772
|
React__default["default"].createElement(HeaderTableCell, null, "Location"),
|
|
5532
6773
|
React__default["default"].createElement(material.TableCell, null,
|
|
@@ -5774,13 +7015,14 @@ const NumberCell = mobxReact.observer(function NumberCell({ initialValue, notify
|
|
|
5774
7015
|
} })));
|
|
5775
7016
|
});
|
|
5776
7017
|
|
|
5777
|
-
function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager) {
|
|
7018
|
+
function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager, filteredTranscripts, updateFilteredTranscripts) {
|
|
5778
7019
|
const internetAccount = getApolloInternetAccount(session);
|
|
5779
7020
|
const role = internetAccount ? internetAccount.role : 'admin';
|
|
5780
7021
|
const admin = role === 'admin';
|
|
5781
7022
|
const readOnly = !(role && ['admin', 'user'].includes(role));
|
|
5782
7023
|
const menuItems = [];
|
|
5783
7024
|
if (feature) {
|
|
7025
|
+
const featureID = feature.attributes.get('gff_id')?.toString();
|
|
5784
7026
|
const sourceAssemblyId = getAssemblyId(region.assemblyName);
|
|
5785
7027
|
const currentAssemblyId = getAssemblyId(region.assemblyName);
|
|
5786
7028
|
menuItems.push({
|
|
@@ -5847,6 +7089,63 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
|
|
|
5847
7089
|
},
|
|
5848
7090
|
]);
|
|
5849
7091
|
},
|
|
7092
|
+
}, {
|
|
7093
|
+
label: 'Merge transcripts',
|
|
7094
|
+
disabled: !admin,
|
|
7095
|
+
onClick: () => {
|
|
7096
|
+
session.queueDialog((doneCallback) => [
|
|
7097
|
+
MergeTranscripts,
|
|
7098
|
+
{
|
|
7099
|
+
session,
|
|
7100
|
+
handleClose: () => {
|
|
7101
|
+
doneCallback();
|
|
7102
|
+
},
|
|
7103
|
+
changeManager,
|
|
7104
|
+
sourceFeature: feature,
|
|
7105
|
+
sourceAssemblyId: currentAssemblyId,
|
|
7106
|
+
selectedFeature,
|
|
7107
|
+
setSelectedFeature,
|
|
7108
|
+
},
|
|
7109
|
+
]);
|
|
7110
|
+
},
|
|
7111
|
+
}, {
|
|
7112
|
+
label: 'Merge exons',
|
|
7113
|
+
disabled: !admin,
|
|
7114
|
+
onClick: () => {
|
|
7115
|
+
session.queueDialog((doneCallback) => [
|
|
7116
|
+
MergeExons,
|
|
7117
|
+
{
|
|
7118
|
+
session,
|
|
7119
|
+
handleClose: () => {
|
|
7120
|
+
doneCallback();
|
|
7121
|
+
},
|
|
7122
|
+
changeManager,
|
|
7123
|
+
sourceFeature: feature,
|
|
7124
|
+
sourceAssemblyId: currentAssemblyId,
|
|
7125
|
+
selectedFeature,
|
|
7126
|
+
setSelectedFeature,
|
|
7127
|
+
},
|
|
7128
|
+
]);
|
|
7129
|
+
},
|
|
7130
|
+
}, {
|
|
7131
|
+
label: 'Split exon',
|
|
7132
|
+
disabled: !admin,
|
|
7133
|
+
onClick: () => {
|
|
7134
|
+
session.queueDialog((doneCallback) => [
|
|
7135
|
+
SplitExon,
|
|
7136
|
+
{
|
|
7137
|
+
session,
|
|
7138
|
+
handleClose: () => {
|
|
7139
|
+
doneCallback();
|
|
7140
|
+
},
|
|
7141
|
+
changeManager,
|
|
7142
|
+
sourceFeature: feature,
|
|
7143
|
+
sourceAssemblyId: currentAssemblyId,
|
|
7144
|
+
selectedFeature,
|
|
7145
|
+
setSelectedFeature,
|
|
7146
|
+
},
|
|
7147
|
+
]);
|
|
7148
|
+
},
|
|
5850
7149
|
});
|
|
5851
7150
|
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
5852
7151
|
if (!featureTypeOntology) {
|
|
@@ -5866,6 +7165,18 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
|
|
|
5866
7165
|
});
|
|
5867
7166
|
session.showWidget(apolloTranscriptWidget);
|
|
5868
7167
|
},
|
|
7168
|
+
}, {
|
|
7169
|
+
label: 'Visible',
|
|
7170
|
+
type: 'checkbox',
|
|
7171
|
+
checked: featureID && filteredTranscripts.includes(featureID) ? false : true,
|
|
7172
|
+
onClick: () => {
|
|
7173
|
+
if (featureID) {
|
|
7174
|
+
const newForms = filteredTranscripts.includes(featureID)
|
|
7175
|
+
? filteredTranscripts.filter((form) => form !== featureID)
|
|
7176
|
+
: [...filteredTranscripts, featureID];
|
|
7177
|
+
updateFilteredTranscripts(newForms);
|
|
7178
|
+
}
|
|
7179
|
+
},
|
|
5869
7180
|
});
|
|
5870
7181
|
}
|
|
5871
7182
|
}
|
|
@@ -5908,8 +7219,8 @@ const useStyles$5 = mui.makeStyles()((theme) => ({
|
|
|
5908
7219
|
},
|
|
5909
7220
|
}));
|
|
5910
7221
|
function makeContextMenuItems(display, feature) {
|
|
5911
|
-
const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
|
|
5912
|
-
return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
|
|
7222
|
+
const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, filteredTranscripts, updateFilteredTranscripts, } = display;
|
|
7223
|
+
return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager, filteredTranscripts, updateFilteredTranscripts);
|
|
5913
7224
|
}
|
|
5914
7225
|
function getTopLevelFeature(feature) {
|
|
5915
7226
|
let cur = feature;
|
|
@@ -6211,7 +7522,7 @@ function draw$3(ctx, feature, row, stateModel, displayedRegionIndex) {
|
|
|
6211
7522
|
const widthPx = feature.length / bpPerPx;
|
|
6212
7523
|
const startPx = reversed ? minX - widthPx : minX;
|
|
6213
7524
|
const top = row * heightPx;
|
|
6214
|
-
const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
|
|
7525
|
+
const isSelected = isSelectedFeature$1(feature, apolloSelectedFeature);
|
|
6215
7526
|
const backgroundColor = getBackgroundColor(theme, isSelected);
|
|
6216
7527
|
const textColor = getTextColor(theme, isSelected);
|
|
6217
7528
|
const featureBox = [
|
|
@@ -6330,7 +7641,7 @@ function drawTooltip$3(display, context) {
|
|
|
6330
7641
|
textTop = textTop + 12;
|
|
6331
7642
|
context.fillText(location, startPx + 2, textTop);
|
|
6332
7643
|
}
|
|
6333
|
-
function isSelectedFeature(feature, selectedFeature) {
|
|
7644
|
+
function isSelectedFeature$1(feature, selectedFeature) {
|
|
6334
7645
|
return Boolean(selectedFeature && feature._id === selectedFeature._id);
|
|
6335
7646
|
}
|
|
6336
7647
|
function getBackgroundColor(theme, selected) {
|
|
@@ -6349,19 +7660,51 @@ function drawBox(ctx, x, y, width, height, color) {
|
|
|
6349
7660
|
ctx.fillRect(x, y, width, height);
|
|
6350
7661
|
}
|
|
6351
7662
|
function getContextMenuItems$3(display) {
|
|
6352
|
-
const { apolloHover
|
|
6353
|
-
const menuItems = [];
|
|
7663
|
+
const { apolloHover } = display;
|
|
6354
7664
|
if (!apolloHover) {
|
|
6355
|
-
return
|
|
7665
|
+
return [];
|
|
6356
7666
|
}
|
|
6357
7667
|
const { feature: sourceFeature } = apolloHover;
|
|
7668
|
+
return getContextMenuItemsForFeature$2(display, sourceFeature);
|
|
7669
|
+
}
|
|
7670
|
+
function makeFeatureLabel(feature) {
|
|
7671
|
+
let name;
|
|
7672
|
+
if (feature.attributes.get('gff_name')) {
|
|
7673
|
+
name = feature.attributes.get('gff_name')?.join(',');
|
|
7674
|
+
}
|
|
7675
|
+
else if (feature.attributes.get('gff_id')) {
|
|
7676
|
+
name = feature.attributes.get('gff_id')?.join(',');
|
|
7677
|
+
}
|
|
7678
|
+
else {
|
|
7679
|
+
name = feature._id;
|
|
7680
|
+
}
|
|
7681
|
+
const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`;
|
|
7682
|
+
const maxLen = 60;
|
|
7683
|
+
if (name && name.length + coords.length > maxLen + 5) {
|
|
7684
|
+
const trim = maxLen - coords.length;
|
|
7685
|
+
name = trim > 0 ? name.slice(0, trim) : '';
|
|
7686
|
+
name = `${name}[...]`;
|
|
7687
|
+
}
|
|
7688
|
+
return `${name} ${coords}`;
|
|
7689
|
+
}
|
|
7690
|
+
function getContextMenuItemsForFeature$2(display, sourceFeature) {
|
|
7691
|
+
const { apolloInternetAccount: internetAccount, changeManager, regions, selectedFeature, session, } = display;
|
|
7692
|
+
const menuItems = [];
|
|
6358
7693
|
const role = internetAccount ? internetAccount.role : 'admin';
|
|
6359
7694
|
const admin = role === 'admin';
|
|
6360
7695
|
const readOnly = !(role && ['admin', 'user'].includes(role));
|
|
6361
7696
|
const [region] = regions;
|
|
6362
7697
|
const sourceAssemblyId = display.getAssemblyId(region.assemblyName);
|
|
6363
7698
|
const currentAssemblyId = display.getAssemblyId(region.assemblyName);
|
|
7699
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
7700
|
+
if (!featureTypeOntology) {
|
|
7701
|
+
throw new Error('featureTypeOntology is undefined');
|
|
7702
|
+
}
|
|
7703
|
+
// Add only relevant options
|
|
6364
7704
|
menuItems.push({
|
|
7705
|
+
label: makeFeatureLabel(sourceFeature),
|
|
7706
|
+
type: 'subHeader',
|
|
7707
|
+
}, {
|
|
6365
7708
|
label: 'Add child feature',
|
|
6366
7709
|
disabled: readOnly,
|
|
6367
7710
|
onClick: () => {
|
|
@@ -6417,37 +7760,7 @@ function getContextMenuItems$3(display) {
|
|
|
6417
7760
|
},
|
|
6418
7761
|
]);
|
|
6419
7762
|
},
|
|
6420
|
-
}, {
|
|
6421
|
-
label: 'Edit feature details',
|
|
6422
|
-
onClick: () => {
|
|
6423
|
-
const apolloFeatureWidget = session.addWidget('ApolloFeatureDetailsWidget', 'apolloFeatureDetailsWidget', {
|
|
6424
|
-
feature: sourceFeature,
|
|
6425
|
-
assembly: currentAssemblyId,
|
|
6426
|
-
refName: region.refName,
|
|
6427
|
-
});
|
|
6428
|
-
session.showWidget(apolloFeatureWidget);
|
|
6429
|
-
},
|
|
6430
7763
|
});
|
|
6431
|
-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
6432
|
-
if (!featureTypeOntology) {
|
|
6433
|
-
throw new Error('featureTypeOntology is undefined');
|
|
6434
|
-
}
|
|
6435
|
-
if ((featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
|
|
6436
|
-
featureTypeOntology.isTypeOf(sourceFeature.type, 'pseudogenic_transcript')) &&
|
|
6437
|
-
util.isSessionModelWithWidgets(session)) {
|
|
6438
|
-
menuItems.push({
|
|
6439
|
-
label: 'Edit transcript details',
|
|
6440
|
-
onClick: () => {
|
|
6441
|
-
const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
|
|
6442
|
-
feature: sourceFeature,
|
|
6443
|
-
assembly: currentAssemblyId,
|
|
6444
|
-
changeManager,
|
|
6445
|
-
refName: region.refName,
|
|
6446
|
-
});
|
|
6447
|
-
session.showWidget(apolloTranscriptWidget);
|
|
6448
|
-
},
|
|
6449
|
-
});
|
|
6450
|
-
}
|
|
6451
7764
|
return menuItems;
|
|
6452
7765
|
}
|
|
6453
7766
|
function getFeatureFromLayout$2(feature, _bp, _row) {
|
|
@@ -6491,9 +7804,12 @@ function onMouseUp$3(stateModel, mousePosition) {
|
|
|
6491
7804
|
return;
|
|
6492
7805
|
}
|
|
6493
7806
|
const { featureAndGlyphUnderMouse } = mousePosition;
|
|
6494
|
-
if (featureAndGlyphUnderMouse
|
|
6495
|
-
|
|
7807
|
+
if (!featureAndGlyphUnderMouse) {
|
|
7808
|
+
return;
|
|
6496
7809
|
}
|
|
7810
|
+
const { feature } = featureAndGlyphUnderMouse;
|
|
7811
|
+
stateModel.setSelectedFeature(feature);
|
|
7812
|
+
stateModel.showFeatureDetailsWidget(feature);
|
|
6497
7813
|
}
|
|
6498
7814
|
/** @returns undefined if mouse not on the edge of this feature, otherwise 'start' or 'end' depending on which edge */
|
|
6499
7815
|
function isMouseOnFeatureEdge(mousePosition, feature, stateModel) {
|
|
@@ -6522,6 +7838,7 @@ const boxGlyph = {
|
|
|
6522
7838
|
drawDragPreview: drawDragPreview$3,
|
|
6523
7839
|
drawHover: drawHover$3,
|
|
6524
7840
|
drawTooltip: drawTooltip$3,
|
|
7841
|
+
getContextMenuItemsForFeature: getContextMenuItemsForFeature$2,
|
|
6525
7842
|
getContextMenuItems: getContextMenuItems$3,
|
|
6526
7843
|
getFeatureFromLayout: getFeatureFromLayout$2,
|
|
6527
7844
|
getRowCount: getRowCount$2,
|
|
@@ -6536,7 +7853,10 @@ let forwardFillLight$1 = null;
|
|
|
6536
7853
|
let backwardFillLight$1 = null;
|
|
6537
7854
|
let forwardFillDark$1 = null;
|
|
6538
7855
|
let backwardFillDark$1 = null;
|
|
6539
|
-
|
|
7856
|
+
const canvas$1 = globalThis.document.createElement('canvas');
|
|
7857
|
+
// @ts-expect-error getContext is undefined in the web worker
|
|
7858
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
7859
|
+
if (canvas$1?.getContext) {
|
|
6540
7860
|
for (const direction of ['forward', 'backward']) {
|
|
6541
7861
|
for (const themeMode of ['light', 'dark']) {
|
|
6542
7862
|
const canvas = document.createElement('canvas');
|
|
@@ -6949,7 +8269,7 @@ function onMouseDown$2(stateModel, currentMousePosition, event) {
|
|
|
6949
8269
|
const draggableFeature = getDraggableFeatureInfo$1(currentMousePosition, feature, stateModel);
|
|
6950
8270
|
if (draggableFeature) {
|
|
6951
8271
|
event.stopPropagation();
|
|
6952
|
-
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge);
|
|
8272
|
+
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge, true);
|
|
6953
8273
|
}
|
|
6954
8274
|
}
|
|
6955
8275
|
function onMouseMove$2(stateModel, mousePosition) {
|
|
@@ -6970,8 +8290,35 @@ function onMouseUp$2(stateModel, mousePosition) {
|
|
|
6970
8290
|
return;
|
|
6971
8291
|
}
|
|
6972
8292
|
const { featureAndGlyphUnderMouse } = mousePosition;
|
|
6973
|
-
if (featureAndGlyphUnderMouse
|
|
6974
|
-
|
|
8293
|
+
if (!featureAndGlyphUnderMouse) {
|
|
8294
|
+
return;
|
|
8295
|
+
}
|
|
8296
|
+
const { feature } = featureAndGlyphUnderMouse;
|
|
8297
|
+
stateModel.setSelectedFeature(feature);
|
|
8298
|
+
const { session } = stateModel;
|
|
8299
|
+
const { apolloDataStore } = session;
|
|
8300
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
8301
|
+
if (!featureTypeOntology) {
|
|
8302
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8303
|
+
}
|
|
8304
|
+
let containsCDSOrExon = false;
|
|
8305
|
+
for (const [, child] of feature.children ?? []) {
|
|
8306
|
+
if (featureTypeOntology.isTypeOf(child.type, 'CDS') ||
|
|
8307
|
+
featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
8308
|
+
containsCDSOrExon = true;
|
|
8309
|
+
break;
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
if ((featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
8313
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
|
|
8314
|
+
containsCDSOrExon) {
|
|
8315
|
+
stateModel.showFeatureDetailsWidget(feature, [
|
|
8316
|
+
'ApolloTranscriptDetails',
|
|
8317
|
+
'apolloTranscriptDetails',
|
|
8318
|
+
]);
|
|
8319
|
+
}
|
|
8320
|
+
else {
|
|
8321
|
+
stateModel.showFeatureDetailsWidget(feature);
|
|
6975
8322
|
}
|
|
6976
8323
|
}
|
|
6977
8324
|
function getDraggableFeatureInfo$1(mousePosition, feature, stateModel) {
|
|
@@ -6985,30 +8332,18 @@ function getDraggableFeatureInfo$1(mousePosition, feature, stateModel) {
|
|
|
6985
8332
|
featureTypeOntology.isTypeOf(feature.type, 'pseudogene');
|
|
6986
8333
|
const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
6987
8334
|
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript');
|
|
6988
|
-
const
|
|
8335
|
+
const isCDS = featureTypeOntology.isTypeOf(feature.type, 'CDS');
|
|
6989
8336
|
if (isGene || isTranscript) {
|
|
8337
|
+
// For gene glyphs, the sizes of genes and transcripts are determined by
|
|
8338
|
+
// their child exons, so we don't make them draggable
|
|
6990
8339
|
return;
|
|
6991
8340
|
}
|
|
8341
|
+
// So now the type of feature is either CDS or exon. If an exon and CDS edge
|
|
8342
|
+
// are in the same place, we want to prioritize dragging the exon. If the
|
|
8343
|
+
// feature we're on is a CDS, let's find any exon it may overlap.
|
|
6992
8344
|
const { bp, refName, regionNumber, x } = mousePosition;
|
|
6993
8345
|
const { lgv } = stateModel;
|
|
6994
|
-
|
|
6995
|
-
const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber });
|
|
6996
|
-
const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber });
|
|
6997
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
6998
|
-
return;
|
|
6999
|
-
}
|
|
7000
|
-
const minPx = minPxInfo.offsetPx - offsetPx;
|
|
7001
|
-
const maxPx = maxPxInfo.offsetPx - offsetPx;
|
|
7002
|
-
if (Math.abs(maxPx - minPx) < 8) {
|
|
7003
|
-
return;
|
|
7004
|
-
}
|
|
7005
|
-
if (Math.abs(minPx - x) < 4) {
|
|
7006
|
-
return { feature, edge: 'min' };
|
|
7007
|
-
}
|
|
7008
|
-
if (Math.abs(maxPx - x) < 4) {
|
|
7009
|
-
return { feature, edge: 'max' };
|
|
7010
|
-
}
|
|
7011
|
-
if (isCds) {
|
|
8346
|
+
if (isCDS) {
|
|
7012
8347
|
const transcript = feature.parent;
|
|
7013
8348
|
if (!transcript?.children) {
|
|
7014
8349
|
return;
|
|
@@ -7024,39 +8359,157 @@ function getDraggableFeatureInfo$1(mousePosition, feature, stateModel) {
|
|
|
7024
8359
|
const [start, end] = util.intersection2(bp - 1, bp, child.min, child.max);
|
|
7025
8360
|
return start !== undefined && end !== undefined;
|
|
7026
8361
|
});
|
|
7027
|
-
if (
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
refName,
|
|
7037
|
-
coord: overlappingExon.max,
|
|
7038
|
-
regionNumber,
|
|
7039
|
-
});
|
|
7040
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
7041
|
-
return;
|
|
8362
|
+
if (overlappingExon) {
|
|
8363
|
+
// We are on an exon, are we on the edge of it?
|
|
8364
|
+
const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv);
|
|
8365
|
+
if (minMax) {
|
|
8366
|
+
const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax);
|
|
8367
|
+
if (overlappingEdge) {
|
|
8368
|
+
return overlappingEdge;
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
7042
8371
|
}
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
8372
|
+
}
|
|
8373
|
+
// End of special cases, let's see if we're on the edge of this CDS or exon
|
|
8374
|
+
const minMax = getMinAndMaxPx(feature, refName, regionNumber, lgv);
|
|
8375
|
+
if (minMax) {
|
|
8376
|
+
const overlappingEdge = getOverlappingEdge(feature, x, minMax);
|
|
8377
|
+
if (overlappingEdge) {
|
|
8378
|
+
return overlappingEdge;
|
|
7047
8379
|
}
|
|
7048
|
-
|
|
7049
|
-
|
|
8380
|
+
}
|
|
8381
|
+
return;
|
|
8382
|
+
}
|
|
8383
|
+
function isTranscriptFeature(feature, session) {
|
|
8384
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8385
|
+
if (!featureTypeOntology) {
|
|
8386
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8387
|
+
}
|
|
8388
|
+
return (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
8389
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript'));
|
|
8390
|
+
}
|
|
8391
|
+
function isExonFeature(feature, session) {
|
|
8392
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8393
|
+
if (!featureTypeOntology) {
|
|
8394
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8395
|
+
}
|
|
8396
|
+
return featureTypeOntology.isTypeOf(feature.type, 'exon');
|
|
8397
|
+
}
|
|
8398
|
+
function isCDSFeature(feature, session) {
|
|
8399
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8400
|
+
if (!featureTypeOntology) {
|
|
8401
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8402
|
+
}
|
|
8403
|
+
return featureTypeOntology.isTypeOf(feature.type, 'CDS');
|
|
8404
|
+
}
|
|
8405
|
+
function getContextMenuItems$2(display, mousePosition) {
|
|
8406
|
+
const { apolloInternetAccount: internetAccount, apolloHover, changeManager, regions, selectedFeature, session, } = display;
|
|
8407
|
+
const [region] = regions;
|
|
8408
|
+
const currentAssemblyId = display.getAssemblyId(region.assemblyName);
|
|
8409
|
+
const menuItems = [];
|
|
8410
|
+
const role = internetAccount ? internetAccount.role : 'admin';
|
|
8411
|
+
const admin = role === 'admin';
|
|
8412
|
+
if (!apolloHover) {
|
|
8413
|
+
return menuItems;
|
|
8414
|
+
}
|
|
8415
|
+
let featuresUnderClick = getFeaturesUnderClick(mousePosition);
|
|
8416
|
+
if (isCDSFeature(mousePosition.featureAndGlyphUnderMouse.feature, session)) {
|
|
8417
|
+
featuresUnderClick = getFeaturesUnderClick(mousePosition, true);
|
|
8418
|
+
}
|
|
8419
|
+
for (const feature of featuresUnderClick) {
|
|
8420
|
+
const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(display, feature);
|
|
8421
|
+
if (isExonFeature(feature, session)) {
|
|
8422
|
+
contextMenuItemsForFeature.push({
|
|
8423
|
+
label: 'Merge exons',
|
|
8424
|
+
disabled: !admin,
|
|
8425
|
+
onClick: () => {
|
|
8426
|
+
session.queueDialog((doneCallback) => [
|
|
8427
|
+
MergeExons,
|
|
8428
|
+
{
|
|
8429
|
+
session,
|
|
8430
|
+
handleClose: () => {
|
|
8431
|
+
doneCallback();
|
|
8432
|
+
},
|
|
8433
|
+
changeManager,
|
|
8434
|
+
sourceFeature: feature,
|
|
8435
|
+
sourceAssemblyId: currentAssemblyId,
|
|
8436
|
+
selectedFeature,
|
|
8437
|
+
setSelectedFeature: (feature) => {
|
|
8438
|
+
display.setSelectedFeature(feature);
|
|
8439
|
+
},
|
|
8440
|
+
},
|
|
8441
|
+
]);
|
|
8442
|
+
},
|
|
8443
|
+
}, {
|
|
8444
|
+
label: 'Split exon',
|
|
8445
|
+
disabled: !admin,
|
|
8446
|
+
onClick: () => {
|
|
8447
|
+
session.queueDialog((doneCallback) => [
|
|
8448
|
+
SplitExon,
|
|
8449
|
+
{
|
|
8450
|
+
session,
|
|
8451
|
+
handleClose: () => {
|
|
8452
|
+
doneCallback();
|
|
8453
|
+
},
|
|
8454
|
+
changeManager,
|
|
8455
|
+
sourceFeature: feature,
|
|
8456
|
+
sourceAssemblyId: currentAssemblyId,
|
|
8457
|
+
selectedFeature,
|
|
8458
|
+
setSelectedFeature: (feature) => {
|
|
8459
|
+
display.setSelectedFeature(feature);
|
|
8460
|
+
},
|
|
8461
|
+
},
|
|
8462
|
+
]);
|
|
8463
|
+
},
|
|
8464
|
+
});
|
|
7050
8465
|
}
|
|
7051
|
-
if (
|
|
7052
|
-
|
|
8466
|
+
if (isTranscriptFeature(feature, session)) {
|
|
8467
|
+
contextMenuItemsForFeature.push({
|
|
8468
|
+
label: 'Merge transcript',
|
|
8469
|
+
onClick: () => {
|
|
8470
|
+
session.queueDialog((doneCallback) => [
|
|
8471
|
+
MergeTranscripts,
|
|
8472
|
+
{
|
|
8473
|
+
session,
|
|
8474
|
+
handleClose: () => {
|
|
8475
|
+
doneCallback();
|
|
8476
|
+
},
|
|
8477
|
+
changeManager,
|
|
8478
|
+
sourceFeature: feature,
|
|
8479
|
+
sourceAssemblyId: currentAssemblyId,
|
|
8480
|
+
selectedFeature,
|
|
8481
|
+
setSelectedFeature: (feature) => {
|
|
8482
|
+
display.setSelectedFeature(feature);
|
|
8483
|
+
},
|
|
8484
|
+
},
|
|
8485
|
+
]);
|
|
8486
|
+
},
|
|
8487
|
+
});
|
|
8488
|
+
if (util.isSessionModelWithWidgets(session)) {
|
|
8489
|
+
contextMenuItemsForFeature.push({
|
|
8490
|
+
label: 'Open transcript details',
|
|
8491
|
+
onClick: () => {
|
|
8492
|
+
const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
|
|
8493
|
+
feature,
|
|
8494
|
+
assembly: currentAssemblyId,
|
|
8495
|
+
changeManager,
|
|
8496
|
+
refName: region.refName,
|
|
8497
|
+
});
|
|
8498
|
+
session.showWidget(apolloTranscriptWidget);
|
|
8499
|
+
},
|
|
8500
|
+
});
|
|
8501
|
+
}
|
|
7053
8502
|
}
|
|
8503
|
+
menuItems.push({
|
|
8504
|
+
label: feature.type,
|
|
8505
|
+
subMenu: contextMenuItemsForFeature,
|
|
8506
|
+
});
|
|
7054
8507
|
}
|
|
7055
|
-
return;
|
|
8508
|
+
return menuItems;
|
|
7056
8509
|
}
|
|
7057
8510
|
// False positive here, none of these functions use "this"
|
|
7058
8511
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
7059
|
-
const { drawTooltip: drawTooltip$2,
|
|
8512
|
+
const { drawTooltip: drawTooltip$2, getContextMenuItemsForFeature: getContextMenuItemsForFeature$1, onMouseLeave: onMouseLeave$2 } = boxGlyph;
|
|
7060
8513
|
/* eslint-enable @typescript-eslint/unbound-method */
|
|
7061
8514
|
const geneGlyph$1 = {
|
|
7062
8515
|
draw: draw$2,
|
|
@@ -7064,6 +8517,7 @@ const geneGlyph$1 = {
|
|
|
7064
8517
|
drawHover: drawHover$2,
|
|
7065
8518
|
drawTooltip: drawTooltip$2,
|
|
7066
8519
|
getContextMenuItems: getContextMenuItems$2,
|
|
8520
|
+
getContextMenuItemsForFeature: getContextMenuItemsForFeature$1,
|
|
7067
8521
|
getFeatureFromLayout: getFeatureFromLayout$1,
|
|
7068
8522
|
getRowCount: getRowCount$1,
|
|
7069
8523
|
getRowForFeature: getRowForFeature$1,
|
|
@@ -7111,7 +8565,7 @@ function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
|
|
|
7111
8565
|
const startPx = reversed ? minX - widthPx : minX;
|
|
7112
8566
|
const top = row * heightPx;
|
|
7113
8567
|
const rowCount = getRowCount(feature);
|
|
7114
|
-
const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
|
|
8568
|
+
const isSelected = isSelectedFeature$1(feature, apolloSelectedFeature);
|
|
7115
8569
|
const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
|
|
7116
8570
|
if (rowCount > 1) {
|
|
7117
8571
|
// draw background that encapsulates all child features
|
|
@@ -7158,15 +8612,44 @@ function getRowForFeature(feature, childFeature) {
|
|
|
7158
8612
|
}
|
|
7159
8613
|
return;
|
|
7160
8614
|
}
|
|
8615
|
+
function getContextMenuItems$1(display, mousePosition) {
|
|
8616
|
+
const { apolloHover, session } = display;
|
|
8617
|
+
const menuItems = [];
|
|
8618
|
+
if (!apolloHover) {
|
|
8619
|
+
return menuItems;
|
|
8620
|
+
}
|
|
8621
|
+
const { feature: sourceFeature } = apolloHover;
|
|
8622
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8623
|
+
if (!featureTypeOntology) {
|
|
8624
|
+
throw new Error('featureTypeOntology is undefined');
|
|
8625
|
+
}
|
|
8626
|
+
const sourceFeatureMenuItems = boxGlyph.getContextMenuItems(display, mousePosition);
|
|
8627
|
+
menuItems.push({
|
|
8628
|
+
label: sourceFeature.type,
|
|
8629
|
+
subMenu: sourceFeatureMenuItems,
|
|
8630
|
+
});
|
|
8631
|
+
for (const relative of getFeaturesUnderClick(mousePosition)) {
|
|
8632
|
+
if (relative._id === sourceFeature._id) {
|
|
8633
|
+
continue;
|
|
8634
|
+
}
|
|
8635
|
+
const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(display, relative);
|
|
8636
|
+
menuItems.push({
|
|
8637
|
+
label: relative.type,
|
|
8638
|
+
subMenu: contextMenuItemsForFeature,
|
|
8639
|
+
});
|
|
8640
|
+
}
|
|
8641
|
+
return menuItems;
|
|
8642
|
+
}
|
|
7161
8643
|
// False positive here, none of these functions use "this"
|
|
7162
8644
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
7163
|
-
const { drawDragPreview: drawDragPreview$1, drawTooltip: drawTooltip$1,
|
|
8645
|
+
const { drawDragPreview: drawDragPreview$1, drawTooltip: drawTooltip$1, getContextMenuItemsForFeature, onMouseDown: onMouseDown$1, onMouseLeave: onMouseLeave$1, onMouseMove: onMouseMove$1, onMouseUp: onMouseUp$1, } = boxGlyph;
|
|
7164
8646
|
/* eslint-enable @typescript-eslint/unbound-method */
|
|
7165
8647
|
const genericChildGlyph = {
|
|
7166
8648
|
draw: draw$1,
|
|
7167
8649
|
drawDragPreview: drawDragPreview$1,
|
|
7168
8650
|
drawHover: drawHover$1,
|
|
7169
8651
|
drawTooltip: drawTooltip$1,
|
|
8652
|
+
getContextMenuItemsForFeature,
|
|
7170
8653
|
getContextMenuItems: getContextMenuItems$1,
|
|
7171
8654
|
getFeatureFromLayout,
|
|
7172
8655
|
getRowCount,
|
|
@@ -7453,6 +8936,27 @@ function baseModelFactory$1(_pluginManager, configSchema) {
|
|
|
7453
8936
|
setSelectedFeature(feature) {
|
|
7454
8937
|
self.session.apolloSetSelectedFeature(feature);
|
|
7455
8938
|
},
|
|
8939
|
+
showFeatureDetailsWidget(feature, customWidgetNameAndId) {
|
|
8940
|
+
const [region] = self.regions;
|
|
8941
|
+
const { assemblyName, refName } = region;
|
|
8942
|
+
const assembly = self.getAssemblyId(assemblyName);
|
|
8943
|
+
if (!assembly) {
|
|
8944
|
+
return;
|
|
8945
|
+
}
|
|
8946
|
+
const { session } = self;
|
|
8947
|
+
const { changeManager } = session.apolloDataStore;
|
|
8948
|
+
const [widgetName, widgetId] = customWidgetNameAndId ?? [
|
|
8949
|
+
'ApolloFeatureDetailsWidget',
|
|
8950
|
+
'apolloFeatureDetailsWidget',
|
|
8951
|
+
];
|
|
8952
|
+
const apolloFeatureWidget = session.addWidget(widgetName, widgetId, {
|
|
8953
|
+
feature,
|
|
8954
|
+
assembly,
|
|
8955
|
+
refName,
|
|
8956
|
+
changeManager,
|
|
8957
|
+
});
|
|
8958
|
+
session.showWidget(apolloFeatureWidget);
|
|
8959
|
+
},
|
|
7456
8960
|
afterAttach() {
|
|
7457
8961
|
mobxStateTree.addDisposer(self, mobx.autorun(() => {
|
|
7458
8962
|
if (!self.lgv.initialized || self.regionCannotBeRendered()) {
|
|
@@ -7681,6 +9185,7 @@ function renderingModelIntermediateFactory$1(pluginManager, configSchema) {
|
|
|
7681
9185
|
detailsHeight: 200,
|
|
7682
9186
|
lastRowTooltipBufferHeight: 40,
|
|
7683
9187
|
isShown: true,
|
|
9188
|
+
filteredTranscripts: mobxStateTree.types.array(mobxStateTree.types.string),
|
|
7684
9189
|
})
|
|
7685
9190
|
.volatile(() => ({
|
|
7686
9191
|
canvas: null,
|
|
@@ -8072,6 +9577,10 @@ function mouseEventsModelIntermediateFactory$1(pluginManager, configSchema) {
|
|
|
8072
9577
|
self.cursor = cursor;
|
|
8073
9578
|
}
|
|
8074
9579
|
},
|
|
9580
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
9581
|
+
updateFilteredTranscripts(forms) {
|
|
9582
|
+
return;
|
|
9583
|
+
},
|
|
8075
9584
|
}))
|
|
8076
9585
|
.actions(() => ({
|
|
8077
9586
|
// onClick(event: CanvasMouseEvent) {
|
|
@@ -8151,32 +9660,37 @@ function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
|
|
|
8151
9660
|
function mouseEventsModelFactory$1(pluginManager, configSchema) {
|
|
8152
9661
|
const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
|
|
8153
9662
|
return LinearApolloDisplayMouseEvents.views((self) => ({
|
|
8154
|
-
contextMenuItems(
|
|
9663
|
+
contextMenuItems(event) {
|
|
8155
9664
|
const { apolloHover } = self;
|
|
8156
|
-
if (!
|
|
9665
|
+
if (!apolloHover) {
|
|
8157
9666
|
return [];
|
|
8158
9667
|
}
|
|
9668
|
+
const mousePosition = self.getMousePosition(event);
|
|
8159
9669
|
const { topLevelFeature } = apolloHover;
|
|
8160
9670
|
const glyph = self.getGlyph(topLevelFeature);
|
|
8161
|
-
|
|
9671
|
+
if (isMousePositionWithFeatureAndGlyph$1(mousePosition)) {
|
|
9672
|
+
return glyph.getContextMenuItems(self, mousePosition);
|
|
9673
|
+
}
|
|
9674
|
+
return [];
|
|
8162
9675
|
},
|
|
8163
9676
|
}))
|
|
8164
9677
|
.actions((self) => ({
|
|
8165
9678
|
// explicitly pass in a feature in case it's not the same as the one in
|
|
8166
9679
|
// mousePosition (e.g. if features are drawn overlapping).
|
|
8167
|
-
startDrag(mousePosition, feature, edge) {
|
|
9680
|
+
startDrag(mousePosition, feature, edge, shrinkParent = false) {
|
|
8168
9681
|
self.apolloDragging = {
|
|
8169
9682
|
start: mousePosition,
|
|
8170
9683
|
current: mousePosition,
|
|
8171
9684
|
feature,
|
|
8172
9685
|
edge,
|
|
9686
|
+
shrinkParent,
|
|
8173
9687
|
};
|
|
8174
9688
|
},
|
|
8175
9689
|
endDrag() {
|
|
8176
9690
|
if (!self.apolloDragging) {
|
|
8177
9691
|
throw new Error('endDrag() called with no current drag in progress');
|
|
8178
9692
|
}
|
|
8179
|
-
const { current, edge, feature, start } = self.apolloDragging;
|
|
9693
|
+
const { current, edge, feature, start, shrinkParent } = self.apolloDragging;
|
|
8180
9694
|
// don't do anything if it was only dragged a tiny bit
|
|
8181
9695
|
if (Math.abs(current.x - start.x) <= 4) {
|
|
8182
9696
|
self.setDragging();
|
|
@@ -8186,33 +9700,28 @@ function mouseEventsModelFactory$1(pluginManager, configSchema) {
|
|
|
8186
9700
|
const { displayedRegions } = self.lgv;
|
|
8187
9701
|
const region = displayedRegions[start.regionNumber];
|
|
8188
9702
|
const assembly = self.getAssemblyId(region.assemblyName);
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
const oldEnd = feature.max;
|
|
8193
|
-
const newEnd = current.bp;
|
|
8194
|
-
change = new shared.LocationEndChange({
|
|
9703
|
+
const changes = getPropagatedLocationChanges(feature, current.bp, edge, shrinkParent);
|
|
9704
|
+
const change = edge === 'max'
|
|
9705
|
+
? new shared.LocationEndChange({
|
|
8195
9706
|
typeName: 'LocationEndChange',
|
|
8196
|
-
changedIds:
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
9707
|
+
changedIds: changes.map((c) => c.featureId),
|
|
9708
|
+
changes: changes.map((c) => ({
|
|
9709
|
+
featureId: c.featureId,
|
|
9710
|
+
oldEnd: c.oldLocation,
|
|
9711
|
+
newEnd: c.newLocation,
|
|
9712
|
+
})),
|
|
8200
9713
|
assembly,
|
|
8201
|
-
})
|
|
8202
|
-
|
|
8203
|
-
else {
|
|
8204
|
-
const featureId = feature._id;
|
|
8205
|
-
const oldStart = feature.min;
|
|
8206
|
-
const newStart = current.bp;
|
|
8207
|
-
change = new shared.LocationStartChange({
|
|
9714
|
+
})
|
|
9715
|
+
: new shared.LocationStartChange({
|
|
8208
9716
|
typeName: 'LocationStartChange',
|
|
8209
|
-
changedIds:
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
9717
|
+
changedIds: changes.map((c) => c.featureId),
|
|
9718
|
+
changes: changes.map((c) => ({
|
|
9719
|
+
featureId: c.featureId,
|
|
9720
|
+
oldStart: c.oldLocation,
|
|
9721
|
+
newStart: c.newLocation,
|
|
9722
|
+
})),
|
|
8213
9723
|
assembly,
|
|
8214
9724
|
});
|
|
8215
|
-
}
|
|
8216
9725
|
void self.changeManager.submit(change);
|
|
8217
9726
|
self.setDragging();
|
|
8218
9727
|
self.setCursor();
|
|
@@ -8253,6 +9762,9 @@ function mouseEventsModelFactory$1(pluginManager, configSchema) {
|
|
|
8253
9762
|
if (isMousePositionWithFeatureAndGlyph$1(mousePosition)) {
|
|
8254
9763
|
mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
|
|
8255
9764
|
}
|
|
9765
|
+
else {
|
|
9766
|
+
self.setSelectedFeature();
|
|
9767
|
+
}
|
|
8256
9768
|
if (self.apolloDragging) {
|
|
8257
9769
|
self.endDrag();
|
|
8258
9770
|
}
|
|
@@ -8302,11 +9814,46 @@ function stateModelFactory$1(pluginManager, configSchema) {
|
|
|
8302
9814
|
|
|
8303
9815
|
const configSchema = configuration.ConfigurationSchema('LinearApolloSixFrameDisplay', {}, { explicitIdentifier: 'displayId', explicitlyTyped: true });
|
|
8304
9816
|
|
|
9817
|
+
const FilterTranscripts = mobxReact.observer(function FilterTranscripts({ sourceFeature, filteredTranscripts, handleClose, onUpdate, }) {
|
|
9818
|
+
const allTranscripts = [];
|
|
9819
|
+
if (sourceFeature.children) {
|
|
9820
|
+
for (const [, child] of sourceFeature.children) {
|
|
9821
|
+
const childID = child.attributes
|
|
9822
|
+
.get('gff_id')
|
|
9823
|
+
?.toString();
|
|
9824
|
+
if (childID) {
|
|
9825
|
+
allTranscripts.push(childID);
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9829
|
+
const [excludedTranscripts, setExcludedTranscripts] = React.useState(filteredTranscripts);
|
|
9830
|
+
const handleChange = (value) => {
|
|
9831
|
+
const newForms = excludedTranscripts.includes(value)
|
|
9832
|
+
? excludedTranscripts.filter((form) => form !== value)
|
|
9833
|
+
: [...excludedTranscripts, value];
|
|
9834
|
+
onUpdate(newForms);
|
|
9835
|
+
setExcludedTranscripts(newForms);
|
|
9836
|
+
};
|
|
9837
|
+
return (React__default["default"].createElement(Dialog, { open: true, maxWidth: false, "data-testid": "filter-transcripts-dialog", title: "Filter transcripts by ID", handleClose: handleClose },
|
|
9838
|
+
React__default["default"].createElement(material.DialogContent, null,
|
|
9839
|
+
React__default["default"].createElement(material.DialogContentText, null, "Select the alternate transcripts you want to display in the apollo track"),
|
|
9840
|
+
React__default["default"].createElement(material.Grid2, { container: true, spacing: 2 },
|
|
9841
|
+
React__default["default"].createElement(material.Grid2, { size: 8 },
|
|
9842
|
+
React__default["default"].createElement(material.FormGroup, null, allTranscripts.map((item) => (
|
|
9843
|
+
// eslint-disable-next-line react/jsx-key
|
|
9844
|
+
React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { checked: !excludedTranscripts.includes(item), onChange: () => {
|
|
9845
|
+
handleChange(item);
|
|
9846
|
+
}, inputProps: { 'aria-label': 'controlled' } }), label: item })))))))));
|
|
9847
|
+
});
|
|
9848
|
+
|
|
8305
9849
|
let forwardFillLight = null;
|
|
8306
9850
|
let backwardFillLight = null;
|
|
8307
9851
|
let forwardFillDark = null;
|
|
8308
9852
|
let backwardFillDark = null;
|
|
8309
|
-
|
|
9853
|
+
const canvas = globalThis.document.createElement('canvas');
|
|
9854
|
+
// @ts-expect-error getContext is undefined in the web worker
|
|
9855
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
9856
|
+
if (canvas?.getContext) {
|
|
8310
9857
|
for (const direction of ['forward', 'backward']) {
|
|
8311
9858
|
for (const themeMode of ['light', 'dark']) {
|
|
8312
9859
|
const canvas = document.createElement('canvas');
|
|
@@ -8357,15 +9904,35 @@ function deepSetHas(set, item) {
|
|
|
8357
9904
|
}
|
|
8358
9905
|
return false;
|
|
8359
9906
|
}
|
|
9907
|
+
function drawTextLabels(ctx, labelArray, font = '10px sans-serif') {
|
|
9908
|
+
for (let i = labelArray.length - 1; i >= 0; --i) {
|
|
9909
|
+
const label = labelArray[i];
|
|
9910
|
+
ctx.fillStyle = label.color;
|
|
9911
|
+
const labelRowX = Math.max(label.x + 1, 0);
|
|
9912
|
+
const labelRowY = label.y + label.h;
|
|
9913
|
+
const textWidth = util.measureText(label.text, 10);
|
|
9914
|
+
if (label.isSelected) {
|
|
9915
|
+
ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h);
|
|
9916
|
+
ctx.font = 'bold '.concat(font);
|
|
9917
|
+
}
|
|
9918
|
+
if (label.text) {
|
|
9919
|
+
ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth);
|
|
9920
|
+
ctx.font = font;
|
|
9921
|
+
}
|
|
9922
|
+
}
|
|
9923
|
+
}
|
|
8360
9924
|
function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
8361
|
-
const { apolloRowHeight, lgv, session, theme, highestRow } = stateModel;
|
|
9925
|
+
const { apolloRowHeight, lgv, session, theme, highestRow, filteredTranscripts, showFeatureLabels, } = stateModel;
|
|
8362
9926
|
const { bpPerPx, displayedRegions, offsetPx } = lgv;
|
|
8363
9927
|
const displayedRegion = displayedRegions[displayedRegionIndex];
|
|
8364
9928
|
const { refName, reversed } = displayedRegion;
|
|
8365
9929
|
const rowHeight = apolloRowHeight;
|
|
8366
|
-
const exonHeight =
|
|
8367
|
-
const cdsHeight =
|
|
8368
|
-
const
|
|
9930
|
+
const exonHeight = rowHeight;
|
|
9931
|
+
const cdsHeight = rowHeight;
|
|
9932
|
+
const topLevelFeatureHeight = rowHeight;
|
|
9933
|
+
const featureLabelSpacer = showFeatureLabels ? 2 : 1;
|
|
9934
|
+
const textColor = theme?.palette.text.primary ?? 'black';
|
|
9935
|
+
const { attributes, children, min, strand } = topLevelFeature;
|
|
8369
9936
|
if (!children) {
|
|
8370
9937
|
return;
|
|
8371
9938
|
}
|
|
@@ -8375,6 +9942,7 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8375
9942
|
if (!featureTypeOntology) {
|
|
8376
9943
|
throw new Error('featureTypeOntology is undefined');
|
|
8377
9944
|
}
|
|
9945
|
+
const labelArray = [];
|
|
8378
9946
|
// Draw background for gene
|
|
8379
9947
|
const topLevelFeatureMinX = (lgv.bpToPx({
|
|
8380
9948
|
refName,
|
|
@@ -8385,16 +9953,29 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8385
9953
|
const topLevelFeatureStartPx = reversed
|
|
8386
9954
|
? topLevelFeatureMinX - topLevelFeatureWidthPx
|
|
8387
9955
|
: topLevelFeatureMinX;
|
|
8388
|
-
const topLevelRow = strand == 1 ? 3 : 4;
|
|
9956
|
+
const topLevelRow = (strand == 1 ? 3 : 4) * featureLabelSpacer;
|
|
8389
9957
|
const topLevelFeatureTop = topLevelRow * rowHeight;
|
|
8390
|
-
const topLevelFeatureHeight = Math.round(0.7 * rowHeight);
|
|
8391
9958
|
ctx.fillStyle = theme?.palette.text.primary ?? 'black';
|
|
8392
9959
|
ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
|
|
8393
|
-
ctx.fillStyle =
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
: material.alpha(theme?.palette.background.paper ?? '#ffffff', 0.7);
|
|
9960
|
+
ctx.fillStyle = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
|
|
9961
|
+
? material.alpha('rgb(0,0,0)', 0.7)
|
|
9962
|
+
: material.alpha(theme?.palette.background.paper ?? '#ffffff', 0.7);
|
|
8397
9963
|
ctx.fillRect(topLevelFeatureStartPx + 1, topLevelFeatureTop + 1, topLevelFeatureWidthPx - 2, topLevelFeatureHeight - 2);
|
|
9964
|
+
const isSelected = isSelectedFeature(topLevelFeature, apolloSelectedFeature);
|
|
9965
|
+
const label = {
|
|
9966
|
+
x: topLevelFeatureStartPx,
|
|
9967
|
+
y: topLevelFeatureTop,
|
|
9968
|
+
h: topLevelFeatureHeight,
|
|
9969
|
+
text: attributes.get('gff_id')?.toString(),
|
|
9970
|
+
color: textColor,
|
|
9971
|
+
isSelected,
|
|
9972
|
+
};
|
|
9973
|
+
if (isSelected) {
|
|
9974
|
+
labelArray.unshift(label);
|
|
9975
|
+
}
|
|
9976
|
+
else {
|
|
9977
|
+
labelArray.push(label);
|
|
9978
|
+
}
|
|
8398
9979
|
const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
|
|
8399
9980
|
const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
|
|
8400
9981
|
const reversal = reversed ? -1 : 1;
|
|
@@ -8418,10 +9999,16 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8418
9999
|
featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript'))) {
|
|
8419
10000
|
continue;
|
|
8420
10001
|
}
|
|
8421
|
-
const { children: childrenOfmRNA, cdsLocations
|
|
10002
|
+
const { children: childrenOfmRNA, cdsLocations } = child;
|
|
8422
10003
|
if (!childrenOfmRNA) {
|
|
8423
10004
|
continue;
|
|
8424
10005
|
}
|
|
10006
|
+
const childID = child.attributes
|
|
10007
|
+
.get('gff_id')
|
|
10008
|
+
?.toString();
|
|
10009
|
+
if (childID && filteredTranscripts.includes(childID)) {
|
|
10010
|
+
continue;
|
|
10011
|
+
}
|
|
8425
10012
|
for (const [, exon] of childrenOfmRNA) {
|
|
8426
10013
|
if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
|
|
8427
10014
|
continue;
|
|
@@ -8434,14 +10021,12 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8434
10021
|
const widthPx = exon.length / bpPerPx;
|
|
8435
10022
|
const startPx = reversed ? minX - widthPx : minX;
|
|
8436
10023
|
const exonTop = topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2;
|
|
10024
|
+
const isSelected = isSelectedFeature(exon, apolloSelectedFeature);
|
|
8437
10025
|
ctx.fillStyle = theme?.palette.text.primary ?? 'black';
|
|
8438
10026
|
ctx.fillRect(startPx, exonTop, widthPx, exonHeight);
|
|
8439
10027
|
if (widthPx > 2) {
|
|
8440
10028
|
ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2);
|
|
8441
|
-
ctx.fillStyle =
|
|
8442
|
-
apolloSelectedFeature && exon._id === apolloSelectedFeature._id
|
|
8443
|
-
? 'rgb(0,0,0)'
|
|
8444
|
-
: material.alpha('#f5f500', 0.6);
|
|
10029
|
+
ctx.fillStyle = isSelected ? 'rgb(0,0,0)' : material.alpha('#f5f500', 0.6);
|
|
8445
10030
|
ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2);
|
|
8446
10031
|
if (topFill && bottomFill) {
|
|
8447
10032
|
ctx.fillStyle = topFill;
|
|
@@ -8449,16 +10034,33 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8449
10034
|
ctx.fillStyle = bottomFill;
|
|
8450
10035
|
ctx.fillRect(startPx + 1, exonTop + 1 + (exonHeight - 2) / 2, widthPx - 2, (exonHeight - 2) / 2);
|
|
8451
10036
|
}
|
|
10037
|
+
const label = {
|
|
10038
|
+
x: startPx,
|
|
10039
|
+
y: exonTop,
|
|
10040
|
+
h: exonHeight,
|
|
10041
|
+
text: exon.attributes.get('gff_id')?.toString(),
|
|
10042
|
+
color: textColor,
|
|
10043
|
+
isSelected,
|
|
10044
|
+
};
|
|
10045
|
+
if (isSelected) {
|
|
10046
|
+
labelArray.unshift(label);
|
|
10047
|
+
}
|
|
10048
|
+
else {
|
|
10049
|
+
labelArray.push(label);
|
|
10050
|
+
}
|
|
8452
10051
|
}
|
|
8453
10052
|
}
|
|
10053
|
+
const isSelected = isSelectedFeature(child, apolloSelectedFeature?.parent);
|
|
10054
|
+
let cdsStartPx = 0;
|
|
10055
|
+
let cdsTop = 0;
|
|
8454
10056
|
for (const cdsRow of cdsLocations) {
|
|
8455
10057
|
let prevCDSTop = 0;
|
|
8456
10058
|
let prevCDSEndPx = 0;
|
|
8457
10059
|
let counter = 1;
|
|
8458
10060
|
for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
|
|
8459
10061
|
if ((apolloSelectedFeature &&
|
|
8460
|
-
|
|
8461
|
-
|
|
10062
|
+
isSelected &&
|
|
10063
|
+
featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')) ||
|
|
8462
10064
|
!deepSetHas(renderedCDS, cds)) {
|
|
8463
10065
|
const cdsWidthPx = (cds.max - cds.min) / bpPerPx;
|
|
8464
10066
|
const minX = (lgv.bpToPx({
|
|
@@ -8466,11 +10068,11 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8466
10068
|
coord: cds.min,
|
|
8467
10069
|
regionNumber: displayedRegionIndex,
|
|
8468
10070
|
})?.offsetPx ?? 0) - offsetPx;
|
|
8469
|
-
|
|
10071
|
+
cdsStartPx = reversed ? minX - cdsWidthPx : minX;
|
|
8470
10072
|
ctx.fillStyle = theme?.palette.text.primary ?? 'black';
|
|
8471
10073
|
const frame = util.getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase);
|
|
8472
|
-
const frameAdjust = frame < 0 ? -1 * frame + 5 : frame;
|
|
8473
|
-
|
|
10074
|
+
const frameAdjust = (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer;
|
|
10075
|
+
cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight;
|
|
8474
10076
|
ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight);
|
|
8475
10077
|
if (cdsWidthPx > 2) {
|
|
8476
10078
|
ctx.clearRect(cdsStartPx + 1, cdsTop + 1, cdsWidthPx - 2, cdsHeight - 2);
|
|
@@ -8479,8 +10081,8 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8479
10081
|
ctx.fillStyle = cdsColorCode;
|
|
8480
10082
|
ctx.fillStyle =
|
|
8481
10083
|
apolloSelectedFeature &&
|
|
8482
|
-
|
|
8483
|
-
|
|
10084
|
+
isSelected &&
|
|
10085
|
+
featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')
|
|
8484
10086
|
? 'rgb(0,0,0)'
|
|
8485
10087
|
: cdsColorCode;
|
|
8486
10088
|
ctx.fillRect(cdsStartPx + 1, cdsTop + 1, cdsWidthPx - 2, cdsHeight - 2);
|
|
@@ -8489,7 +10091,9 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8489
10091
|
// Mid-point for intron line "hat"
|
|
8490
10092
|
const midPoint = [
|
|
8491
10093
|
(cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
|
|
8492
|
-
Math.max(frame < 0
|
|
10094
|
+
Math.max(frame < 0
|
|
10095
|
+
? rowHeight * featureLabelSpacer * highestRow + 1
|
|
10096
|
+
: 1, // Avoid render ceiling
|
|
8493
10097
|
Math.min(prevCDSTop, cdsTop) - rowHeight / 2),
|
|
8494
10098
|
];
|
|
8495
10099
|
ctx.strokeStyle = 'rgb(0, 128, 128)';
|
|
@@ -8515,6 +10119,23 @@ function draw(ctx, topLevelFeature, _row, stateModel, displayedRegionIndex) {
|
|
|
8515
10119
|
}
|
|
8516
10120
|
}
|
|
8517
10121
|
}
|
|
10122
|
+
const label = {
|
|
10123
|
+
x: cdsStartPx,
|
|
10124
|
+
y: cdsTop,
|
|
10125
|
+
h: cdsHeight,
|
|
10126
|
+
text: child.attributes.get('gff_id')?.toString(),
|
|
10127
|
+
color: textColor,
|
|
10128
|
+
isSelected,
|
|
10129
|
+
};
|
|
10130
|
+
if (isSelected) {
|
|
10131
|
+
labelArray.unshift(label);
|
|
10132
|
+
}
|
|
10133
|
+
else {
|
|
10134
|
+
labelArray.push(label);
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
10137
|
+
if (showFeatureLabels) {
|
|
10138
|
+
drawTextLabels(ctx, labelArray);
|
|
8518
10139
|
}
|
|
8519
10140
|
}
|
|
8520
10141
|
function drawDragPreview(stateModel, overlayCtx) {
|
|
@@ -8542,7 +10163,7 @@ function drawDragPreview(stateModel, overlayCtx) {
|
|
|
8542
10163
|
overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight);
|
|
8543
10164
|
}
|
|
8544
10165
|
function drawHover(stateModel, ctx) {
|
|
8545
|
-
const { apolloHover, apolloRowHeight, lgv, highestRow, session } = stateModel;
|
|
10166
|
+
const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, highestRow, session, showFeatureLabels, } = stateModel;
|
|
8546
10167
|
if (!apolloHover) {
|
|
8547
10168
|
return;
|
|
8548
10169
|
}
|
|
@@ -8555,6 +10176,12 @@ function drawHover(stateModel, ctx) {
|
|
|
8555
10176
|
if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
|
|
8556
10177
|
return;
|
|
8557
10178
|
}
|
|
10179
|
+
const featureID = feature.attributes
|
|
10180
|
+
.get('gff_id')
|
|
10181
|
+
?.toString();
|
|
10182
|
+
if (featureID && filteredTranscripts.includes(featureID)) {
|
|
10183
|
+
return;
|
|
10184
|
+
}
|
|
8558
10185
|
const position = stateModel.getFeatureLayoutPosition(feature);
|
|
8559
10186
|
if (!position) {
|
|
8560
10187
|
return;
|
|
@@ -8564,7 +10191,8 @@ function drawHover(stateModel, ctx) {
|
|
|
8564
10191
|
const displayedRegion = displayedRegions[layoutIndex];
|
|
8565
10192
|
const { refName, reversed } = displayedRegion;
|
|
8566
10193
|
const rowHeight = apolloRowHeight;
|
|
8567
|
-
const cdsHeight =
|
|
10194
|
+
const cdsHeight = rowHeight;
|
|
10195
|
+
const featureLabelSpacer = showFeatureLabels ? 2 : 1;
|
|
8568
10196
|
const { cdsLocations, strand } = feature;
|
|
8569
10197
|
for (const cdsRow of cdsLocations) {
|
|
8570
10198
|
let prevCDSTop = 0;
|
|
@@ -8580,15 +10208,15 @@ function drawHover(stateModel, ctx) {
|
|
|
8580
10208
|
})?.offsetPx ?? 0) - offsetPx;
|
|
8581
10209
|
const cdsStartPx = reversed ? minX - cdsWidthPx : minX;
|
|
8582
10210
|
const frame = util.getFrame(cds.min, cds.max, strand ?? 1, cds.phase);
|
|
8583
|
-
const frameAdjust = frame < 0 ? -1 * frame + 5 : frame;
|
|
8584
|
-
const cdsTop = (frameAdjust -
|
|
10211
|
+
const frameAdjust = (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer;
|
|
10212
|
+
const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight;
|
|
8585
10213
|
ctx.fillStyle = 'rgba(255,0,0,0.6)';
|
|
8586
10214
|
ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight);
|
|
8587
10215
|
if (counter > 1) {
|
|
8588
10216
|
// Mid-point for intron line "hat"
|
|
8589
10217
|
const midPoint = [
|
|
8590
10218
|
(cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
|
|
8591
|
-
Math.max(frame < 0 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
|
|
10219
|
+
Math.max(frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
|
|
8592
10220
|
Math.min(prevCDSTop, cdsTop) - rowHeight / 2),
|
|
8593
10221
|
];
|
|
8594
10222
|
ctx.strokeStyle = 'rgb(0, 0, 0)';
|
|
@@ -8616,7 +10244,7 @@ function onMouseDown(stateModel, currentMousePosition, event) {
|
|
|
8616
10244
|
const draggableFeature = getDraggableFeatureInfo(currentMousePosition, cds, feature, stateModel);
|
|
8617
10245
|
if (draggableFeature) {
|
|
8618
10246
|
event.stopPropagation();
|
|
8619
|
-
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge);
|
|
10247
|
+
stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge, true);
|
|
8620
10248
|
}
|
|
8621
10249
|
}
|
|
8622
10250
|
function onMouseMove(stateModel, mousePosition) {
|
|
@@ -8640,28 +10268,39 @@ function onMouseUp(stateModel, mousePosition) {
|
|
|
8640
10268
|
const { session } = stateModel;
|
|
8641
10269
|
const { apolloDataStore } = session;
|
|
8642
10270
|
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
8643
|
-
if (featureAndGlyphUnderMouse
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
10271
|
+
if (!featureAndGlyphUnderMouse) {
|
|
10272
|
+
return;
|
|
10273
|
+
}
|
|
10274
|
+
const { feature } = featureAndGlyphUnderMouse;
|
|
10275
|
+
stateModel.setSelectedFeature(feature);
|
|
10276
|
+
if (!featureTypeOntology) {
|
|
10277
|
+
throw new Error('featureTypeOntology is undefined');
|
|
10278
|
+
}
|
|
10279
|
+
let containsCDSOrExon = false;
|
|
10280
|
+
for (const [, child] of feature.children ?? []) {
|
|
10281
|
+
if (featureTypeOntology.isTypeOf(child.type, 'CDS') ||
|
|
10282
|
+
featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
10283
|
+
containsCDSOrExon = true;
|
|
10284
|
+
break;
|
|
8657
10285
|
}
|
|
8658
10286
|
}
|
|
8659
|
-
|
|
8660
|
-
|
|
10287
|
+
if ((featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
10288
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
|
|
10289
|
+
containsCDSOrExon) {
|
|
10290
|
+
stateModel.showFeatureDetailsWidget(feature, [
|
|
10291
|
+
'ApolloTranscriptDetails',
|
|
10292
|
+
'apolloTranscriptDetails',
|
|
10293
|
+
]);
|
|
10294
|
+
}
|
|
10295
|
+
else {
|
|
10296
|
+
stateModel.showFeatureDetailsWidget(feature);
|
|
8661
10297
|
}
|
|
8662
10298
|
}
|
|
10299
|
+
function isSelectedFeature(feature, selectedFeature) {
|
|
10300
|
+
return Boolean(selectedFeature && feature._id === selectedFeature._id);
|
|
10301
|
+
}
|
|
8663
10302
|
function getDraggableFeatureInfo(mousePosition, cds, feature, stateModel) {
|
|
8664
|
-
const { session } = stateModel;
|
|
10303
|
+
const { filteredTranscripts, session } = stateModel;
|
|
8665
10304
|
const { apolloDataStore } = session;
|
|
8666
10305
|
const { featureTypeOntology } = apolloDataStore.ontologyManager;
|
|
8667
10306
|
if (!featureTypeOntology) {
|
|
@@ -8671,67 +10310,64 @@ function getDraggableFeatureInfo(mousePosition, cds, feature, stateModel) {
|
|
|
8671
10310
|
if (cds === null) {
|
|
8672
10311
|
return;
|
|
8673
10312
|
}
|
|
8674
|
-
const
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
const maxPxInfo = lgv.bpToPx({ refName, coord: cds.max, regionNumber });
|
|
8679
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
8680
|
-
return;
|
|
8681
|
-
}
|
|
8682
|
-
const minPx = minPxInfo.offsetPx - offsetPx;
|
|
8683
|
-
const maxPx = maxPxInfo.offsetPx - offsetPx;
|
|
8684
|
-
if (Math.abs(maxPx - minPx) < 8) {
|
|
10313
|
+
const featureID = feature.attributes
|
|
10314
|
+
.get('gff_id')
|
|
10315
|
+
?.toString();
|
|
10316
|
+
if (featureID && filteredTranscripts.includes(featureID)) {
|
|
8685
10317
|
return;
|
|
8686
10318
|
}
|
|
10319
|
+
const { bp, refName, regionNumber, x } = mousePosition;
|
|
10320
|
+
const { lgv } = stateModel;
|
|
8687
10321
|
if (isTranscript) {
|
|
8688
10322
|
const transcript = feature;
|
|
8689
10323
|
if (!transcript.children) {
|
|
8690
10324
|
return;
|
|
8691
10325
|
}
|
|
8692
10326
|
const exonChildren = [];
|
|
10327
|
+
const cdsChildren = [];
|
|
8693
10328
|
for (const child of transcript.children.values()) {
|
|
8694
10329
|
const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon');
|
|
10330
|
+
const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS');
|
|
8695
10331
|
if (childIsExon) {
|
|
8696
10332
|
exonChildren.push(child);
|
|
8697
10333
|
}
|
|
10334
|
+
else if (childIsCDS) {
|
|
10335
|
+
cdsChildren.push(child);
|
|
10336
|
+
}
|
|
8698
10337
|
}
|
|
8699
10338
|
const overlappingExon = exonChildren.find((child) => {
|
|
8700
10339
|
const [start, end] = util.intersection2(bp, bp + 1, child.min, child.max);
|
|
8701
10340
|
return start !== undefined && end !== undefined;
|
|
8702
10341
|
});
|
|
8703
|
-
if (
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
}
|
|
8727
|
-
if (Math.abs(maxPx - x) < 4) {
|
|
8728
|
-
return { feature: overlappingExon, edge: 'max' };
|
|
10342
|
+
if (overlappingExon) {
|
|
10343
|
+
// We are on an exon, are we on the edge of it?
|
|
10344
|
+
const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv);
|
|
10345
|
+
if (minMax) {
|
|
10346
|
+
const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax);
|
|
10347
|
+
if (overlappingEdge) {
|
|
10348
|
+
return overlappingEdge;
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
}
|
|
10352
|
+
// End of special cases, let's see if we're on the edge of this CDS or exon
|
|
10353
|
+
const minMax = getMinAndMaxPx(cds, refName, regionNumber, lgv);
|
|
10354
|
+
if (minMax) {
|
|
10355
|
+
const overlappingCDS = cdsChildren.find((child) => {
|
|
10356
|
+
const [start, end] = util.intersection2(bp, bp + 1, child.min, child.max);
|
|
10357
|
+
return start !== undefined && end !== undefined;
|
|
10358
|
+
});
|
|
10359
|
+
if (overlappingCDS) {
|
|
10360
|
+
const overlappingEdge = getOverlappingEdge(overlappingCDS, x, minMax);
|
|
10361
|
+
if (overlappingEdge) {
|
|
10362
|
+
return overlappingEdge;
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
8729
10365
|
}
|
|
8730
10366
|
}
|
|
8731
10367
|
return;
|
|
8732
10368
|
}
|
|
8733
10369
|
function drawTooltip(display, context) {
|
|
8734
|
-
const { apolloHover, apolloRowHeight, lgv, theme } = display;
|
|
10370
|
+
const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, theme } = display;
|
|
8735
10371
|
if (!apolloHover) {
|
|
8736
10372
|
return;
|
|
8737
10373
|
}
|
|
@@ -8743,6 +10379,12 @@ function drawTooltip(display, context) {
|
|
|
8743
10379
|
if (!position) {
|
|
8744
10380
|
return;
|
|
8745
10381
|
}
|
|
10382
|
+
const featureID = feature.attributes
|
|
10383
|
+
.get('gff_id')
|
|
10384
|
+
?.toString();
|
|
10385
|
+
if (featureID && filteredTranscripts.includes(featureID)) {
|
|
10386
|
+
return;
|
|
10387
|
+
}
|
|
8746
10388
|
const { layoutIndex } = position;
|
|
8747
10389
|
const { bpPerPx, displayedRegions, offsetPx } = lgv;
|
|
8748
10390
|
const displayedRegion = displayedRegions[layoutIndex];
|
|
@@ -8794,7 +10436,7 @@ function drawTooltip(display, context) {
|
|
|
8794
10436
|
context.fillText(location, startPx + 2, textTop);
|
|
8795
10437
|
}
|
|
8796
10438
|
function getContextMenuItems(display) {
|
|
8797
|
-
const { apolloHover, apolloInternetAccount: internetAccount, changeManager, regions, selectedFeature, session, } = display;
|
|
10439
|
+
const { apolloHover, apolloInternetAccount: internetAccount, changeManager, filteredTranscripts, regions, selectedFeature, session, } = display;
|
|
8798
10440
|
const menuItems = [];
|
|
8799
10441
|
if (!apolloHover) {
|
|
8800
10442
|
return menuItems;
|
|
@@ -8862,33 +10504,28 @@ function getContextMenuItems(display) {
|
|
|
8862
10504
|
},
|
|
8863
10505
|
]);
|
|
8864
10506
|
},
|
|
8865
|
-
}, {
|
|
8866
|
-
label: 'Edit feature details',
|
|
8867
|
-
onClick: () => {
|
|
8868
|
-
const apolloFeatureWidget = session.addWidget('ApolloFeatureDetailsWidget', 'apolloFeatureDetailsWidget', {
|
|
8869
|
-
feature: sourceFeature,
|
|
8870
|
-
assembly: currentAssemblyId,
|
|
8871
|
-
refName: region.refName,
|
|
8872
|
-
});
|
|
8873
|
-
session.showWidget(apolloFeatureWidget);
|
|
8874
|
-
},
|
|
8875
10507
|
});
|
|
8876
10508
|
const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
|
|
8877
10509
|
if (!featureTypeOntology) {
|
|
8878
10510
|
throw new Error('featureTypeOntology is undefined');
|
|
8879
10511
|
}
|
|
8880
|
-
if (featureTypeOntology.isTypeOf(sourceFeature.type, '
|
|
8881
|
-
util.isSessionModelWithWidgets(session)) {
|
|
10512
|
+
if (featureTypeOntology.isTypeOf(sourceFeature.type, 'gene')) {
|
|
8882
10513
|
menuItems.push({
|
|
8883
|
-
label: '
|
|
10514
|
+
label: 'Filter alternate transcripts',
|
|
8884
10515
|
onClick: () => {
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
10516
|
+
session.queueDialog((doneCallback) => [
|
|
10517
|
+
FilterTranscripts,
|
|
10518
|
+
{
|
|
10519
|
+
handleClose: () => {
|
|
10520
|
+
doneCallback();
|
|
10521
|
+
},
|
|
10522
|
+
sourceFeature,
|
|
10523
|
+
filteredTranscripts: mobxStateTree.getSnapshot(filteredTranscripts),
|
|
10524
|
+
onUpdate: (forms) => {
|
|
10525
|
+
display.updateFilteredTranscripts(forms);
|
|
10526
|
+
},
|
|
10527
|
+
},
|
|
10528
|
+
]);
|
|
8892
10529
|
},
|
|
8893
10530
|
});
|
|
8894
10531
|
}
|
|
@@ -8917,6 +10554,7 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
8917
10554
|
configuration: configuration.ConfigurationReference(configSchema),
|
|
8918
10555
|
graphical: true,
|
|
8919
10556
|
table: false,
|
|
10557
|
+
showFeatureLabels: true,
|
|
8920
10558
|
heightPreConfig: mobxStateTree.types.maybe(mobxStateTree.types.refinement('displayHeight', mobxStateTree.types.number, (n) => n >= minDisplayHeight)),
|
|
8921
10559
|
filteredFeatureTypes: mobxStateTree.types.array(mobxStateTree.types.string),
|
|
8922
10560
|
})
|
|
@@ -9033,6 +10671,9 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9033
10671
|
self.graphical = true;
|
|
9034
10672
|
self.table = true;
|
|
9035
10673
|
},
|
|
10674
|
+
toggleShowFeatureLabels() {
|
|
10675
|
+
self.showFeatureLabels = !self.showFeatureLabels;
|
|
10676
|
+
},
|
|
9036
10677
|
updateFilteredFeatureTypes(types) {
|
|
9037
10678
|
self.filteredFeatureTypes = mobxStateTree.cast(types);
|
|
9038
10679
|
},
|
|
@@ -9041,7 +10682,7 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9041
10682
|
const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self;
|
|
9042
10683
|
return {
|
|
9043
10684
|
trackMenuItems() {
|
|
9044
|
-
const { graphical, table } = self;
|
|
10685
|
+
const { graphical, table, showFeatureLabels } = self;
|
|
9045
10686
|
return [
|
|
9046
10687
|
...superTrackMenuItems(),
|
|
9047
10688
|
{
|
|
@@ -9072,6 +10713,14 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9072
10713
|
self.showGraphicalAndTable();
|
|
9073
10714
|
},
|
|
9074
10715
|
},
|
|
10716
|
+
{
|
|
10717
|
+
label: 'Feature Labels',
|
|
10718
|
+
type: 'checkbox',
|
|
10719
|
+
checked: showFeatureLabels,
|
|
10720
|
+
onClick: () => {
|
|
10721
|
+
self.toggleShowFeatureLabels();
|
|
10722
|
+
},
|
|
10723
|
+
},
|
|
9075
10724
|
],
|
|
9076
10725
|
},
|
|
9077
10726
|
{
|
|
@@ -9101,6 +10750,27 @@ function baseModelFactory(_pluginManager, configSchema) {
|
|
|
9101
10750
|
setSelectedFeature(feature) {
|
|
9102
10751
|
self.session.apolloSetSelectedFeature(feature);
|
|
9103
10752
|
},
|
|
10753
|
+
showFeatureDetailsWidget(feature, customWidgetNameAndId) {
|
|
10754
|
+
const [region] = self.regions;
|
|
10755
|
+
const { assemblyName, refName } = region;
|
|
10756
|
+
const assembly = self.getAssemblyId(assemblyName);
|
|
10757
|
+
if (!assembly) {
|
|
10758
|
+
return;
|
|
10759
|
+
}
|
|
10760
|
+
const { session } = self;
|
|
10761
|
+
const { changeManager } = session.apolloDataStore;
|
|
10762
|
+
const [widgetName, widgetId] = customWidgetNameAndId ?? [
|
|
10763
|
+
'ApolloFeatureDetailsWidget',
|
|
10764
|
+
'apolloFeatureDetailsWidget',
|
|
10765
|
+
];
|
|
10766
|
+
const apolloFeatureWidget = session.addWidget(widgetName, widgetId, {
|
|
10767
|
+
feature,
|
|
10768
|
+
assembly,
|
|
10769
|
+
refName,
|
|
10770
|
+
changeManager,
|
|
10771
|
+
});
|
|
10772
|
+
session.showWidget(apolloFeatureWidget);
|
|
10773
|
+
},
|
|
9104
10774
|
afterAttach() {
|
|
9105
10775
|
mobxStateTree.addDisposer(self, mobx.autorun(() => {
|
|
9106
10776
|
if (!self.lgv.initialized || self.regionCannotBeRendered()) {
|
|
@@ -9161,6 +10831,9 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9161
10831
|
getGlyph(_feature) {
|
|
9162
10832
|
return geneGlyph;
|
|
9163
10833
|
},
|
|
10834
|
+
featureLabelSpacer(elem) {
|
|
10835
|
+
return self.showFeatureLabels ? elem * 2 - 1 : elem;
|
|
10836
|
+
},
|
|
9164
10837
|
}))
|
|
9165
10838
|
.actions((self) => ({
|
|
9166
10839
|
addSeenFeature(feature) {
|
|
@@ -9169,6 +10842,11 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9169
10842
|
deleteSeenFeature(featureId) {
|
|
9170
10843
|
self.seenFeatures.delete(featureId);
|
|
9171
10844
|
},
|
|
10845
|
+
}))
|
|
10846
|
+
.views((self) => ({
|
|
10847
|
+
get geneTrackRowNums() {
|
|
10848
|
+
return [4, 5].map((elem) => self.featureLabelSpacer(elem));
|
|
10849
|
+
},
|
|
9172
10850
|
}))
|
|
9173
10851
|
.views((self) => ({
|
|
9174
10852
|
get featureLayouts() {
|
|
@@ -9195,7 +10873,9 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9195
10873
|
throw new Error('featureTypeOntology is undefined');
|
|
9196
10874
|
}
|
|
9197
10875
|
if (feature.looksLikeGene) {
|
|
9198
|
-
const rowNum = feature.strand == 1
|
|
10876
|
+
const rowNum = feature.strand == 1
|
|
10877
|
+
? self.geneTrackRowNums[0]
|
|
10878
|
+
: self.geneTrackRowNums[1];
|
|
9199
10879
|
if (!featureLayout.get(rowNum)) {
|
|
9200
10880
|
featureLayout.set(rowNum, []);
|
|
9201
10881
|
}
|
|
@@ -9213,7 +10893,9 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9213
10893
|
if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
|
|
9214
10894
|
continue;
|
|
9215
10895
|
}
|
|
9216
|
-
const rowNum = exon.strand == 1
|
|
10896
|
+
const rowNum = exon.strand == 1
|
|
10897
|
+
? self.geneTrackRowNums[0]
|
|
10898
|
+
: self.geneTrackRowNums[1];
|
|
9217
10899
|
const layoutRow = featureLayout.get(rowNum);
|
|
9218
10900
|
layoutRow?.push({ rowNum, feature: exon, cds: null });
|
|
9219
10901
|
}
|
|
@@ -9221,7 +10903,7 @@ function layoutsModelFactory(pluginManager, configSchema) {
|
|
|
9221
10903
|
for (const cdsRow of cdsLocations) {
|
|
9222
10904
|
for (const cds of cdsRow) {
|
|
9223
10905
|
let rowNum = util.getFrame(cds.min, cds.max, strand ?? 1, cds.phase);
|
|
9224
|
-
rowNum = rowNum < 0 ? -1 * rowNum + 5 : rowNum;
|
|
10906
|
+
rowNum = self.featureLabelSpacer(rowNum < 0 ? -1 * rowNum + 5 : rowNum);
|
|
9225
10907
|
if (!featureLayout.get(rowNum)) {
|
|
9226
10908
|
featureLayout.set(rowNum, []);
|
|
9227
10909
|
}
|
|
@@ -9297,6 +10979,7 @@ function renderingModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9297
10979
|
detailsHeight: 200,
|
|
9298
10980
|
lastRowTooltipBufferHeight: 80,
|
|
9299
10981
|
isShown: true,
|
|
10982
|
+
filteredTranscripts: mobxStateTree.types.array(mobxStateTree.types.string),
|
|
9300
10983
|
})
|
|
9301
10984
|
.volatile(() => ({
|
|
9302
10985
|
canvas: null,
|
|
@@ -9306,7 +10989,8 @@ function renderingModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9306
10989
|
}))
|
|
9307
10990
|
.views((self) => ({
|
|
9308
10991
|
get featuresHeight() {
|
|
9309
|
-
|
|
10992
|
+
const featureLabelSpacer = self.showFeatureLabels ? 2 : 1;
|
|
10993
|
+
return (featureLabelSpacer * ((self.highestRow + 1) * self.apolloRowHeight) +
|
|
9310
10994
|
self.lastRowTooltipBufferHeight);
|
|
9311
10995
|
},
|
|
9312
10996
|
}))
|
|
@@ -9444,7 +11128,7 @@ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9444
11128
|
return mousePosition;
|
|
9445
11129
|
}
|
|
9446
11130
|
let foundFeature;
|
|
9447
|
-
if (
|
|
11131
|
+
if (self.geneTrackRowNums.includes(row)) {
|
|
9448
11132
|
foundFeature = layoutRow.find((f) => f.feature.type == 'exon' &&
|
|
9449
11133
|
bp >= f.feature.min &&
|
|
9450
11134
|
bp <= f.feature.max);
|
|
@@ -9453,7 +11137,14 @@ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9453
11137
|
}
|
|
9454
11138
|
}
|
|
9455
11139
|
else {
|
|
9456
|
-
foundFeature = layoutRow.find((f) =>
|
|
11140
|
+
foundFeature = layoutRow.find((f) => {
|
|
11141
|
+
const featureID = f.feature.attributes.get('gff_id')?.toString();
|
|
11142
|
+
return (f.cds != null &&
|
|
11143
|
+
bp >= f.cds.min &&
|
|
11144
|
+
bp <= f.cds.max &&
|
|
11145
|
+
(featureID === undefined ||
|
|
11146
|
+
!self.filteredTranscripts.includes(featureID)));
|
|
11147
|
+
});
|
|
9457
11148
|
}
|
|
9458
11149
|
if (!foundFeature) {
|
|
9459
11150
|
return mousePosition;
|
|
@@ -9488,6 +11179,9 @@ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
|
|
|
9488
11179
|
self.cursor = cursor;
|
|
9489
11180
|
}
|
|
9490
11181
|
},
|
|
11182
|
+
updateFilteredTranscripts(forms) {
|
|
11183
|
+
self.filteredTranscripts = mobxStateTree.cast(forms);
|
|
11184
|
+
},
|
|
9491
11185
|
}))
|
|
9492
11186
|
.actions(() => ({
|
|
9493
11187
|
// onClick(event: CanvasMouseEvent) {
|
|
@@ -9512,19 +11206,20 @@ function mouseEventsModelFactory(pluginManager, configSchema) {
|
|
|
9512
11206
|
.actions((self) => ({
|
|
9513
11207
|
// explicitly pass in a feature in case it's not the same as the one in
|
|
9514
11208
|
// mousePosition (e.g. if features are drawn overlapping).
|
|
9515
|
-
startDrag(mousePosition, feature, edge) {
|
|
11209
|
+
startDrag(mousePosition, feature, edge, shrinkParent = false) {
|
|
9516
11210
|
self.apolloDragging = {
|
|
9517
11211
|
start: mousePosition,
|
|
9518
11212
|
current: mousePosition,
|
|
9519
11213
|
feature,
|
|
9520
11214
|
edge,
|
|
11215
|
+
shrinkParent,
|
|
9521
11216
|
};
|
|
9522
11217
|
},
|
|
9523
11218
|
endDrag() {
|
|
9524
11219
|
if (!self.apolloDragging) {
|
|
9525
11220
|
throw new Error('endDrag() called with no current drag in progress');
|
|
9526
11221
|
}
|
|
9527
|
-
const { current, edge, feature, start } = self.apolloDragging;
|
|
11222
|
+
const { current, edge, feature, start, shrinkParent } = self.apolloDragging;
|
|
9528
11223
|
// don't do anything if it was only dragged a tiny bit
|
|
9529
11224
|
if (Math.abs(current.x - start.x) <= 4) {
|
|
9530
11225
|
self.setDragging();
|
|
@@ -9534,33 +11229,28 @@ function mouseEventsModelFactory(pluginManager, configSchema) {
|
|
|
9534
11229
|
const { displayedRegions } = self.lgv;
|
|
9535
11230
|
const region = displayedRegions[start.regionNumber];
|
|
9536
11231
|
const assembly = self.getAssemblyId(region.assemblyName);
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
const oldEnd = feature.max;
|
|
9541
|
-
const newEnd = current.bp;
|
|
9542
|
-
change = new shared.LocationEndChange({
|
|
11232
|
+
const changes = getPropagatedLocationChanges(feature, current.bp, edge, shrinkParent);
|
|
11233
|
+
const change = edge === 'max'
|
|
11234
|
+
? new shared.LocationEndChange({
|
|
9543
11235
|
typeName: 'LocationEndChange',
|
|
9544
|
-
changedIds:
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
11236
|
+
changedIds: changes.map((c) => c.featureId),
|
|
11237
|
+
changes: changes.map((c) => ({
|
|
11238
|
+
featureId: c.featureId,
|
|
11239
|
+
oldEnd: c.oldLocation,
|
|
11240
|
+
newEnd: c.newLocation,
|
|
11241
|
+
})),
|
|
9548
11242
|
assembly,
|
|
9549
|
-
})
|
|
9550
|
-
|
|
9551
|
-
else {
|
|
9552
|
-
const featureId = feature._id;
|
|
9553
|
-
const oldStart = feature.min;
|
|
9554
|
-
const newStart = current.bp;
|
|
9555
|
-
change = new shared.LocationStartChange({
|
|
11243
|
+
})
|
|
11244
|
+
: new shared.LocationStartChange({
|
|
9556
11245
|
typeName: 'LocationStartChange',
|
|
9557
|
-
changedIds:
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
11246
|
+
changedIds: changes.map((c) => c.featureId),
|
|
11247
|
+
changes: changes.map((c) => ({
|
|
11248
|
+
featureId: c.featureId,
|
|
11249
|
+
oldStart: c.oldLocation,
|
|
11250
|
+
newStart: c.newLocation,
|
|
11251
|
+
})),
|
|
9561
11252
|
assembly,
|
|
9562
11253
|
});
|
|
9563
|
-
}
|
|
9564
11254
|
void self.changeManager.submit(change);
|
|
9565
11255
|
self.setDragging();
|
|
9566
11256
|
self.setCursor();
|
|
@@ -9601,6 +11291,9 @@ function mouseEventsModelFactory(pluginManager, configSchema) {
|
|
|
9601
11291
|
if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
|
|
9602
11292
|
mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
|
|
9603
11293
|
}
|
|
11294
|
+
else {
|
|
11295
|
+
self.setSelectedFeature();
|
|
11296
|
+
}
|
|
9604
11297
|
if (self.apolloDragging) {
|
|
9605
11298
|
self.endDrag();
|
|
9606
11299
|
}
|
|
@@ -9651,185 +11344,16 @@ function stateModelFactory(pluginManager, configSchema) {
|
|
|
9651
11344
|
const ApolloPluginConfigurationSchema = configuration.ConfigurationSchema('ApolloPlugin', {
|
|
9652
11345
|
ontologies: mobxStateTree.types.array(OntologyRecordConfiguration),
|
|
9653
11346
|
featureTypeOntologyName: {
|
|
9654
|
-
description: 'Name of the feature type ontology',
|
|
9655
|
-
type: 'string',
|
|
9656
|
-
defaultValue: 'Sequence Ontology',
|
|
9657
|
-
},
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
}
|
|
9665
|
-
function annotationFromPileup(pluggableElement) {
|
|
9666
|
-
if (pluggableElement.name !== 'LinearPileupDisplay') {
|
|
9667
|
-
return pluggableElement;
|
|
9668
|
-
}
|
|
9669
|
-
const { stateModel } = pluggableElement;
|
|
9670
|
-
const newStateModel = stateModel
|
|
9671
|
-
.views((self) => ({
|
|
9672
|
-
getFirstRegion() {
|
|
9673
|
-
const lgv = util.getContainingView(self);
|
|
9674
|
-
return lgv.dynamicBlocks.contentBlocks[0];
|
|
9675
|
-
},
|
|
9676
|
-
getAssembly() {
|
|
9677
|
-
const firstRegion = self.getFirstRegion();
|
|
9678
|
-
const session = util.getSession(self);
|
|
9679
|
-
const { assemblyManager } = session;
|
|
9680
|
-
const { assemblyName } = firstRegion;
|
|
9681
|
-
const assembly = assemblyManager.get(assemblyName);
|
|
9682
|
-
if (!assembly) {
|
|
9683
|
-
throw new Error(`Could not find assembly named ${assemblyName}`);
|
|
9684
|
-
}
|
|
9685
|
-
return assembly;
|
|
9686
|
-
},
|
|
9687
|
-
getRefSeqId(assembly) {
|
|
9688
|
-
const firstRegion = self.getFirstRegion();
|
|
9689
|
-
const { refName } = firstRegion;
|
|
9690
|
-
const { refNameAliases } = assembly;
|
|
9691
|
-
if (!refNameAliases) {
|
|
9692
|
-
throw new Error(`Could not find aliases for ${assembly.name}`);
|
|
9693
|
-
}
|
|
9694
|
-
const newRefNames = [...Object.entries(refNameAliases)]
|
|
9695
|
-
.filter(([id, refName]) => id !== refName)
|
|
9696
|
-
.map(([id, refName]) => ({
|
|
9697
|
-
_id: id,
|
|
9698
|
-
name: refName,
|
|
9699
|
-
}));
|
|
9700
|
-
const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
|
|
9701
|
-
if (!refSeqId) {
|
|
9702
|
-
throw new Error(`Could not find refSeqId named ${refName}`);
|
|
9703
|
-
}
|
|
9704
|
-
return refSeqId;
|
|
9705
|
-
},
|
|
9706
|
-
createFeature() {
|
|
9707
|
-
const feature = self.contextMenuFeature;
|
|
9708
|
-
const assembly = self.getAssembly();
|
|
9709
|
-
const refSeqId = self.getRefSeqId(assembly);
|
|
9710
|
-
const cigarData = feature.get('CIGAR');
|
|
9711
|
-
const ops = parseCigar(cigarData);
|
|
9712
|
-
let currOffset = 0;
|
|
9713
|
-
const start = feature.get('start');
|
|
9714
|
-
let openStart;
|
|
9715
|
-
const exons = [];
|
|
9716
|
-
for (const [op, len] of ops) {
|
|
9717
|
-
// open or continue open
|
|
9718
|
-
if (op === 'M' || op === '=') {
|
|
9719
|
-
// if it was closed, then open with start, strand, type
|
|
9720
|
-
if (openStart === undefined) {
|
|
9721
|
-
// add subfeature
|
|
9722
|
-
openStart = currOffset + start;
|
|
9723
|
-
}
|
|
9724
|
-
}
|
|
9725
|
-
else if (op === 'N' && openStart !== undefined) {
|
|
9726
|
-
// if it was open, then close and add the subfeature
|
|
9727
|
-
exons.push({
|
|
9728
|
-
start: openStart,
|
|
9729
|
-
end: currOffset + openStart,
|
|
9730
|
-
});
|
|
9731
|
-
openStart = undefined;
|
|
9732
|
-
}
|
|
9733
|
-
if (op !== 'I') {
|
|
9734
|
-
// we ignore insertions when calculating potential exon length
|
|
9735
|
-
currOffset += len;
|
|
9736
|
-
}
|
|
9737
|
-
}
|
|
9738
|
-
// if we are still open, then close with the final length and add subfeature
|
|
9739
|
-
if (openStart !== undefined) {
|
|
9740
|
-
exons.push({
|
|
9741
|
-
start: openStart,
|
|
9742
|
-
end: currOffset + start,
|
|
9743
|
-
});
|
|
9744
|
-
}
|
|
9745
|
-
const newFeature = {
|
|
9746
|
-
_id: ObjectID__default["default"]().toHexString(),
|
|
9747
|
-
refSeq: refSeqId,
|
|
9748
|
-
min: feature.get('start'),
|
|
9749
|
-
max: feature.get('end'),
|
|
9750
|
-
type: 'mRNA',
|
|
9751
|
-
strand: feature.get('strand'),
|
|
9752
|
-
};
|
|
9753
|
-
if (exons.length === 0) {
|
|
9754
|
-
return newFeature;
|
|
9755
|
-
}
|
|
9756
|
-
const children = {};
|
|
9757
|
-
newFeature.children = children;
|
|
9758
|
-
const [firstExon] = exons;
|
|
9759
|
-
const cdsFeature = {
|
|
9760
|
-
_id: ObjectID__default["default"]().toHexString(),
|
|
9761
|
-
refSeq: refSeqId,
|
|
9762
|
-
min: firstExon.start,
|
|
9763
|
-
max: firstExon.end,
|
|
9764
|
-
type: 'CDS',
|
|
9765
|
-
strand: feature.get('strand'),
|
|
9766
|
-
};
|
|
9767
|
-
newFeature.children[cdsFeature._id] = cdsFeature;
|
|
9768
|
-
if (exons.length === 1) {
|
|
9769
|
-
const exon = {
|
|
9770
|
-
_id: ObjectID__default["default"]().toHexString(),
|
|
9771
|
-
refSeq: refSeqId,
|
|
9772
|
-
min: firstExon.start,
|
|
9773
|
-
max: firstExon.end,
|
|
9774
|
-
type: 'exon',
|
|
9775
|
-
strand: feature.get('strand'),
|
|
9776
|
-
};
|
|
9777
|
-
newFeature.children[exon._id] = exon;
|
|
9778
|
-
return newFeature;
|
|
9779
|
-
}
|
|
9780
|
-
for (const exon of exons) {
|
|
9781
|
-
cdsFeature.min = Math.min(cdsFeature.min, exon.start);
|
|
9782
|
-
cdsFeature.max = Math.max(cdsFeature.max, exon.end);
|
|
9783
|
-
const { end, start } = exon;
|
|
9784
|
-
const newExon = {
|
|
9785
|
-
_id: ObjectID__default["default"]().toHexString(),
|
|
9786
|
-
refSeq: refSeqId,
|
|
9787
|
-
min: start,
|
|
9788
|
-
max: end,
|
|
9789
|
-
type: 'exon',
|
|
9790
|
-
strand: feature.get('strand'),
|
|
9791
|
-
};
|
|
9792
|
-
newFeature.children[newExon._id] = newExon;
|
|
9793
|
-
}
|
|
9794
|
-
return newFeature;
|
|
9795
|
-
},
|
|
9796
|
-
async onPileupFeatureContext() {
|
|
9797
|
-
const newFeature = self.createFeature();
|
|
9798
|
-
const assembly = self.getAssembly();
|
|
9799
|
-
const assemblyId = assembly.name;
|
|
9800
|
-
const change = new shared.AddFeatureChange({
|
|
9801
|
-
changedIds: [newFeature._id],
|
|
9802
|
-
typeName: 'AddFeatureChange',
|
|
9803
|
-
assembly: assemblyId,
|
|
9804
|
-
addedFeature: newFeature,
|
|
9805
|
-
});
|
|
9806
|
-
const session = util.getSession(self);
|
|
9807
|
-
await session.apolloDataStore.changeManager.submit(change);
|
|
9808
|
-
session.notify('Annotation added successfully', 'success');
|
|
9809
|
-
},
|
|
9810
|
-
}))
|
|
9811
|
-
.views((self) => {
|
|
9812
|
-
const superContextMenuItems = self.contextMenuItems;
|
|
9813
|
-
return {
|
|
9814
|
-
contextMenuItems() {
|
|
9815
|
-
const feature = self.contextMenuFeature;
|
|
9816
|
-
if (!feature) {
|
|
9817
|
-
return superContextMenuItems();
|
|
9818
|
-
}
|
|
9819
|
-
return [
|
|
9820
|
-
...superContextMenuItems(),
|
|
9821
|
-
{
|
|
9822
|
-
label: 'Create Apollo annotation',
|
|
9823
|
-
icon: AddIcon__default["default"],
|
|
9824
|
-
onClick: self.onPileupFeatureContext,
|
|
9825
|
-
},
|
|
9826
|
-
];
|
|
9827
|
-
},
|
|
9828
|
-
};
|
|
9829
|
-
});
|
|
9830
|
-
pluggableElement.stateModel = newStateModel;
|
|
9831
|
-
return pluggableElement;
|
|
9832
|
-
}
|
|
11347
|
+
description: 'Name of the feature type ontology',
|
|
11348
|
+
type: 'string',
|
|
11349
|
+
defaultValue: 'Sequence Ontology',
|
|
11350
|
+
},
|
|
11351
|
+
hasRole: {
|
|
11352
|
+
description: 'Flag used internally by jbrowse-plugin-apollo',
|
|
11353
|
+
type: 'boolean',
|
|
11354
|
+
defaultValue: false,
|
|
11355
|
+
},
|
|
11356
|
+
});
|
|
9833
11357
|
|
|
9834
11358
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
9835
11359
|
const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
|
|
@@ -9858,61 +11382,80 @@ const isTranscript = (annotationFeature, apolloSessionModel) => {
|
|
|
9858
11382
|
return (featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript') ||
|
|
9859
11383
|
featureTypeOntology.isTypeOf(annotationFeature.type, 'pseudogenic_transcript'));
|
|
9860
11384
|
};
|
|
9861
|
-
|
|
11385
|
+
function getFeatureName(feature) {
|
|
9862
11386
|
const { attributes } = feature;
|
|
9863
|
-
const
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
}
|
|
9869
|
-
const getFeatureNameOrId = (feature, apolloSessionModel) => {
|
|
9870
|
-
const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
|
|
9871
|
-
if (!featureTypeOntology) {
|
|
9872
|
-
return getFeatureId(feature);
|
|
9873
|
-
}
|
|
9874
|
-
let attrName = '';
|
|
9875
|
-
if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
|
|
9876
|
-
attrName = 'gene_name';
|
|
11387
|
+
const keys = ['name', 'gff_name', 'transcript_name', 'gene_name'];
|
|
11388
|
+
for (const key of keys) {
|
|
11389
|
+
const value = attributes?.[key];
|
|
11390
|
+
if (value?.[0]) {
|
|
11391
|
+
return value[0];
|
|
11392
|
+
}
|
|
9877
11393
|
}
|
|
9878
|
-
|
|
9879
|
-
|
|
11394
|
+
return '';
|
|
11395
|
+
}
|
|
11396
|
+
function getGeneNameOrId(feature) {
|
|
11397
|
+
const { attributes } = feature;
|
|
11398
|
+
const keys = ['gene_name', 'gene_id', 'gene_stable_id'];
|
|
11399
|
+
for (const key of keys) {
|
|
11400
|
+
const value = attributes?.[key];
|
|
11401
|
+
if (value?.[0]) {
|
|
11402
|
+
return value[0];
|
|
11403
|
+
}
|
|
9880
11404
|
}
|
|
11405
|
+
return '';
|
|
11406
|
+
}
|
|
11407
|
+
function getFeatureId(feature) {
|
|
9881
11408
|
const { attributes } = feature;
|
|
9882
|
-
const
|
|
11409
|
+
const keys = [
|
|
11410
|
+
'id',
|
|
11411
|
+
'gff_id',
|
|
11412
|
+
'transcript_id',
|
|
11413
|
+
'gene_id',
|
|
11414
|
+
'gene_stable_id',
|
|
11415
|
+
'stable_id',
|
|
11416
|
+
];
|
|
11417
|
+
for (const key of keys) {
|
|
11418
|
+
const value = attributes?.[key];
|
|
11419
|
+
if (value?.[0]) {
|
|
11420
|
+
return value[0];
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
return '';
|
|
11424
|
+
}
|
|
11425
|
+
const getFeatureNameOrId = (feature) => {
|
|
11426
|
+
const name = getFeatureName(feature);
|
|
11427
|
+
const id = getFeatureId(feature);
|
|
9883
11428
|
if (name) {
|
|
9884
|
-
return name
|
|
11429
|
+
return `${feature.type} - ${name}`;
|
|
11430
|
+
}
|
|
11431
|
+
if (id) {
|
|
11432
|
+
return `${feature.type} - ${id}`;
|
|
9885
11433
|
}
|
|
9886
|
-
return
|
|
11434
|
+
return feature.type;
|
|
9887
11435
|
};
|
|
9888
|
-
function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
|
|
11436
|
+
function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, region, }) {
|
|
9889
11437
|
const apolloSessionModel = session;
|
|
11438
|
+
const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
|
|
9890
11439
|
const childIds = React.useMemo(() => Object.keys(annotationFeature.children ?? {}), [annotationFeature]);
|
|
9891
|
-
const features = React.useMemo(() => {
|
|
9892
|
-
for (const [, asm] of apolloSessionModel.apolloDataStore.assemblies) {
|
|
9893
|
-
if (asm._id === assembly.name) {
|
|
9894
|
-
for (const [, refSeq] of asm.refSeqs) {
|
|
9895
|
-
if (refSeq._id === refSeqId) {
|
|
9896
|
-
return refSeq.features;
|
|
9897
|
-
}
|
|
9898
|
-
}
|
|
9899
|
-
}
|
|
9900
|
-
}
|
|
9901
|
-
return [];
|
|
9902
|
-
}, []);
|
|
9903
11440
|
const [parentFeatureChecked, setParentFeatureChecked] = React.useState(true);
|
|
9904
11441
|
const [checkedChildrens, setCheckedChildrens] = React.useState(childIds);
|
|
9905
11442
|
const [errorMessage, setErrorMessage] = React.useState('');
|
|
9906
11443
|
const [destinationFeatures, setDestinationFeatures] = React.useState([]);
|
|
11444
|
+
const [createNewGene, setCreateNewGene] = React.useState(false);
|
|
9907
11445
|
const [selectedDestinationFeature, setSelectedDestinationFeature] = React.useState();
|
|
9908
|
-
const
|
|
11446
|
+
const apolloAssembly = apolloSessionModel.apolloDataStore.assemblies.get(assembly.name);
|
|
11447
|
+
const refSeq = apolloAssembly?.refSeqs.get(refSeqId);
|
|
11448
|
+
const features = refSeq?.getFeatures(region.start, region.end);
|
|
11449
|
+
const getDestinationFeatures = () => {
|
|
9909
11450
|
const filteredFeatures = [];
|
|
9910
|
-
for (const
|
|
9911
|
-
if (f.
|
|
11451
|
+
for (const f of features ?? []) {
|
|
11452
|
+
if (f.min > region.end || f.max < region.start) {
|
|
9912
11453
|
continue;
|
|
9913
11454
|
}
|
|
9914
|
-
|
|
9915
|
-
if (
|
|
11455
|
+
// Destination feature should be of type gene amd should be on the same strand as the source feature
|
|
11456
|
+
if (featureTypeOntology?.isTypeOf(f.type, 'gene') &&
|
|
11457
|
+
f.strand === annotationFeature.strand) {
|
|
11458
|
+
const featureSnapshot = mobxStateTree.getSnapshot(f);
|
|
9916
11459
|
filteredFeatures.push(featureSnapshot);
|
|
9917
11460
|
}
|
|
9918
11461
|
}
|
|
@@ -9920,27 +11463,10 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
|
|
|
9920
11463
|
};
|
|
9921
11464
|
React.useEffect(() => {
|
|
9922
11465
|
setErrorMessage('');
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
.filter((child) => isTranscript(child, apolloSessionModel))
|
|
9928
|
-
.filter((child) => checkedChildrens.includes(child._id));
|
|
9929
|
-
mins = checkedAnnotationFeatureChildren.map((f) => f.min);
|
|
9930
|
-
maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
|
|
9931
|
-
}
|
|
9932
|
-
const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
|
|
9933
|
-
if (featureTypeOntology &&
|
|
9934
|
-
featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript')) {
|
|
9935
|
-
mins = [annotationFeature.min, ...mins];
|
|
9936
|
-
maxes = [annotationFeature.max, ...maxes];
|
|
9937
|
-
}
|
|
9938
|
-
const min = Math.min(...mins);
|
|
9939
|
-
const max = Math.max(...maxes);
|
|
9940
|
-
const filteredFeatures = getFeatures(min, max);
|
|
9941
|
-
setDestinationFeatures(filteredFeatures);
|
|
9942
|
-
setSelectedDestinationFeature(filteredFeatures[0]);
|
|
9943
|
-
}, [checkedChildrens, parentFeatureChecked]);
|
|
11466
|
+
const features = getDestinationFeatures();
|
|
11467
|
+
setDestinationFeatures(features);
|
|
11468
|
+
setSelectedDestinationFeature(features[0]);
|
|
11469
|
+
}, [checkedChildrens, parentFeatureChecked, region]);
|
|
9944
11470
|
const handleParentFeatureCheck = (event) => {
|
|
9945
11471
|
const isChecked = event.target.checked;
|
|
9946
11472
|
setParentFeatureChecked(isChecked);
|
|
@@ -9957,95 +11483,211 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
|
|
|
9957
11483
|
};
|
|
9958
11484
|
const handleCreateApolloAnnotation = async () => {
|
|
9959
11485
|
if (parentFeatureChecked) {
|
|
9960
|
-
|
|
11486
|
+
// IF SOURCE FEATURE IS GENE
|
|
9961
11487
|
if (isGene(annotationFeature, apolloSessionModel)) {
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
Object.values(annotationFeature.children).length) {
|
|
9965
|
-
const childrens = {};
|
|
9966
|
-
for (const childId of checkedChildrens) {
|
|
9967
|
-
childrens[childId] = annotationFeature.children[childId];
|
|
9968
|
-
}
|
|
9969
|
-
change = new shared.AddFeatureChange({
|
|
9970
|
-
changedIds: [annotationFeature._id],
|
|
9971
|
-
typeName: 'AddFeatureChange',
|
|
9972
|
-
assembly: assembly.name,
|
|
9973
|
-
addedFeature: {
|
|
9974
|
-
...annotationFeature,
|
|
9975
|
-
children: childrens,
|
|
9976
|
-
},
|
|
9977
|
-
});
|
|
9978
|
-
}
|
|
9979
|
-
else {
|
|
9980
|
-
change = new shared.AddFeatureChange({
|
|
9981
|
-
changedIds: [annotationFeature._id],
|
|
9982
|
-
typeName: 'AddFeatureChange',
|
|
9983
|
-
assembly: assembly.name,
|
|
9984
|
-
addedFeature: annotationFeature,
|
|
9985
|
-
});
|
|
9986
|
-
}
|
|
11488
|
+
await copyGeneFeature();
|
|
11489
|
+
session.notify('Successfully copied selected gene and transcript(s)', 'success');
|
|
9987
11490
|
}
|
|
9988
11491
|
if (isTranscript(annotationFeature, apolloSessionModel)) {
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
11492
|
+
// IF THE SOURCE IS TRANSCRIPT AND THE DESTINATION IS SELECTED AND CREATE NEW GENE IS NOT CHECKED
|
|
11493
|
+
if (selectedDestinationFeature && !createNewGene) {
|
|
11494
|
+
const transcripts = {};
|
|
11495
|
+
transcripts[annotationFeature._id] = annotationFeature;
|
|
11496
|
+
// If source trancript doesn't overlap with destination gene
|
|
11497
|
+
// If not overlapping, then extend the destination gene to include the transcript
|
|
11498
|
+
if (selectedDestinationFeature.max < annotationFeature.max ||
|
|
11499
|
+
selectedDestinationFeature.min > annotationFeature.min) {
|
|
11500
|
+
const newMin = Math.min(selectedDestinationFeature.min, annotationFeature.min);
|
|
11501
|
+
const newMax = Math.max(selectedDestinationFeature.max, annotationFeature.max);
|
|
11502
|
+
await extendSelectedDestinationFeatureLocation(newMin, newMax);
|
|
11503
|
+
await copyTranscriptsToDestinationGene(transcripts);
|
|
11504
|
+
}
|
|
11505
|
+
else {
|
|
11506
|
+
await copyTranscriptsToDestinationGene(transcripts);
|
|
11507
|
+
}
|
|
11508
|
+
session.notify('Successfully copied selected transcripts to destination gene', 'success');
|
|
9997
11509
|
}
|
|
9998
11510
|
else {
|
|
9999
|
-
|
|
10000
|
-
|
|
11511
|
+
// IF THERE IS NO DESTINATION GENE SELECTED AND CREATE NEW GENE IS CHECKED
|
|
11512
|
+
const childrens = {};
|
|
11513
|
+
childrens[annotationFeature._id] = annotationFeature;
|
|
11514
|
+
await createNewGeneFeatureWithTranscripts(childrens);
|
|
11515
|
+
session.notify('Successfully created a new gene with selected transcripts', 'success');
|
|
10001
11516
|
}
|
|
10002
11517
|
}
|
|
10003
|
-
if (!change) {
|
|
10004
|
-
return;
|
|
10005
|
-
}
|
|
10006
|
-
await apolloSessionModel.apolloDataStore.changeManager.submit(change);
|
|
10007
|
-
session.notify('Annotation added successfully', 'success');
|
|
10008
|
-
handleClose();
|
|
10009
11518
|
}
|
|
10010
11519
|
else {
|
|
11520
|
+
// IF PARENT (GENE) FEATURE IS NOT CHECKED AND WE ARE COPYING CHILDREN (TRANSCRIPTS)
|
|
10011
11521
|
if (!annotationFeature.children) {
|
|
10012
11522
|
return;
|
|
10013
11523
|
}
|
|
10014
|
-
|
|
10015
|
-
|
|
11524
|
+
// IF DESTINATION IS SELECTED AND CREATE NEW GENE IS NOT CHECKED
|
|
11525
|
+
if (selectedDestinationFeature && !createNewGene) {
|
|
11526
|
+
const childrens = {};
|
|
11527
|
+
for (const childId of checkedChildrens) {
|
|
11528
|
+
childrens[childId] = annotationFeature.children[childId];
|
|
11529
|
+
}
|
|
11530
|
+
const min = Math.min(...Object.values(childrens).map((child) => child.min));
|
|
11531
|
+
const max = Math.max(...Object.values(childrens).map((child) => child.max));
|
|
11532
|
+
// If source trancript doesn't overlap with destination gene
|
|
11533
|
+
// If not overlapping, then extend the destination gene to include the transcript
|
|
11534
|
+
if (selectedDestinationFeature.min > min ||
|
|
11535
|
+
selectedDestinationFeature.max < max) {
|
|
11536
|
+
const newMin = Math.min(selectedDestinationFeature.min, min);
|
|
11537
|
+
const newMax = Math.max(selectedDestinationFeature.max, max);
|
|
11538
|
+
await extendSelectedDestinationFeatureLocation(newMin, newMax);
|
|
11539
|
+
await copyTranscriptsToDestinationGene(childrens);
|
|
11540
|
+
}
|
|
11541
|
+
else {
|
|
11542
|
+
await copyTranscriptsToDestinationGene(childrens);
|
|
11543
|
+
}
|
|
11544
|
+
session.notify('Successfully copied transcript to destination gene', 'success');
|
|
10016
11545
|
}
|
|
11546
|
+
else {
|
|
11547
|
+
// IF THERE IS NO DESTINATION GENE SELECTED AND CREATE NEW GENE IS CHECKED
|
|
11548
|
+
const childrens = {};
|
|
11549
|
+
for (const childId of checkedChildrens) {
|
|
11550
|
+
childrens[childId] = annotationFeature.children[childId];
|
|
11551
|
+
}
|
|
11552
|
+
await createNewGeneFeatureWithTranscripts(childrens);
|
|
11553
|
+
session.notify('Successfully created a new gene with selected transcript', 'success');
|
|
11554
|
+
}
|
|
11555
|
+
}
|
|
11556
|
+
handleClose();
|
|
11557
|
+
};
|
|
11558
|
+
// Copies gene feature along with its selected children
|
|
11559
|
+
const copyGeneFeature = async () => {
|
|
11560
|
+
let change;
|
|
11561
|
+
if (annotationFeature.children &&
|
|
11562
|
+
checkedChildrens.length !==
|
|
11563
|
+
Object.values(annotationFeature.children).length) {
|
|
11564
|
+
// IF SOME CHILDREN ARE CHECKED
|
|
11565
|
+
const childrens = {};
|
|
10017
11566
|
for (const childId of checkedChildrens) {
|
|
10018
|
-
|
|
10019
|
-
const change = new shared.AddFeatureChange({
|
|
10020
|
-
parentFeatureId: selectedDestinationFeature._id,
|
|
10021
|
-
changedIds: [selectedDestinationFeature._id],
|
|
10022
|
-
typeName: 'AddFeatureChange',
|
|
10023
|
-
assembly: assembly.name,
|
|
10024
|
-
addedFeature: child,
|
|
10025
|
-
});
|
|
10026
|
-
await apolloSessionModel.apolloDataStore.changeManager.submit(change);
|
|
11567
|
+
childrens[childId] = annotationFeature.children[childId];
|
|
10027
11568
|
}
|
|
10028
|
-
|
|
10029
|
-
|
|
11569
|
+
change = new shared.AddFeatureChange({
|
|
11570
|
+
changedIds: [annotationFeature._id],
|
|
11571
|
+
typeName: 'AddFeatureChange',
|
|
11572
|
+
assembly: assembly.name,
|
|
11573
|
+
addedFeature: {
|
|
11574
|
+
...annotationFeature,
|
|
11575
|
+
children: childrens,
|
|
11576
|
+
},
|
|
11577
|
+
});
|
|
11578
|
+
}
|
|
11579
|
+
else {
|
|
11580
|
+
// IF PARENT AND ALL CHILDREN ARE CHECKED
|
|
11581
|
+
change = new shared.AddFeatureChange({
|
|
11582
|
+
changedIds: [annotationFeature._id],
|
|
11583
|
+
typeName: 'AddFeatureChange',
|
|
11584
|
+
assembly: assembly.name,
|
|
11585
|
+
addedFeature: annotationFeature,
|
|
11586
|
+
});
|
|
11587
|
+
}
|
|
11588
|
+
await submitChange(change);
|
|
11589
|
+
};
|
|
11590
|
+
const copyTranscriptsToDestinationGene = async (transcripts) => {
|
|
11591
|
+
if (!selectedDestinationFeature) {
|
|
11592
|
+
return;
|
|
11593
|
+
}
|
|
11594
|
+
for (const transcriptId of Object.keys(transcripts)) {
|
|
11595
|
+
const transcript = transcripts[transcriptId];
|
|
11596
|
+
const change = new shared.AddFeatureChange({
|
|
11597
|
+
parentFeatureId: selectedDestinationFeature._id,
|
|
11598
|
+
changedIds: [selectedDestinationFeature._id],
|
|
11599
|
+
typeName: 'AddFeatureChange',
|
|
11600
|
+
assembly: assembly.name,
|
|
11601
|
+
addedFeature: transcript,
|
|
11602
|
+
});
|
|
11603
|
+
await submitChange(change);
|
|
11604
|
+
}
|
|
11605
|
+
};
|
|
11606
|
+
const createNewGeneFeatureWithTranscripts = async (childrens) => {
|
|
11607
|
+
const newGeneId = new ObjectID__default["default"]().toHexString();
|
|
11608
|
+
const min = Math.min(...Object.values(childrens).map((child) => child.min));
|
|
11609
|
+
const max = Math.max(...Object.values(childrens).map((child) => child.max));
|
|
11610
|
+
const change = new shared.AddFeatureChange({
|
|
11611
|
+
changedIds: [newGeneId],
|
|
11612
|
+
typeName: 'AddFeatureChange',
|
|
11613
|
+
assembly: assembly.name,
|
|
11614
|
+
addedFeature: {
|
|
11615
|
+
_id: newGeneId,
|
|
11616
|
+
refSeq: refSeqId,
|
|
11617
|
+
min,
|
|
11618
|
+
max,
|
|
11619
|
+
strand: annotationFeature.strand,
|
|
11620
|
+
type: 'gene',
|
|
11621
|
+
children: childrens,
|
|
11622
|
+
attributes: {
|
|
11623
|
+
name: [getGeneNameOrId(annotationFeature)],
|
|
11624
|
+
gene_name: [getGeneNameOrId(annotationFeature)],
|
|
11625
|
+
},
|
|
11626
|
+
},
|
|
11627
|
+
});
|
|
11628
|
+
await submitChange(change);
|
|
11629
|
+
};
|
|
11630
|
+
const extendSelectedDestinationFeatureLocation = async (newMin, newMax) => {
|
|
11631
|
+
if (!selectedDestinationFeature) {
|
|
11632
|
+
return;
|
|
11633
|
+
}
|
|
11634
|
+
const changes = [];
|
|
11635
|
+
if (newMin !== selectedDestinationFeature.min) {
|
|
11636
|
+
changes.push(new shared.LocationStartChange({
|
|
11637
|
+
typeName: 'LocationStartChange',
|
|
11638
|
+
changedIds: [selectedDestinationFeature._id],
|
|
11639
|
+
featureId: selectedDestinationFeature._id,
|
|
11640
|
+
assembly: assembly.name,
|
|
11641
|
+
oldStart: selectedDestinationFeature.min,
|
|
11642
|
+
newStart: newMin,
|
|
11643
|
+
}));
|
|
11644
|
+
}
|
|
11645
|
+
if (newMax !== selectedDestinationFeature.max) {
|
|
11646
|
+
changes.push(new shared.LocationEndChange({
|
|
11647
|
+
typeName: 'LocationEndChange',
|
|
11648
|
+
changedIds: [selectedDestinationFeature._id],
|
|
11649
|
+
featureId: selectedDestinationFeature._id,
|
|
11650
|
+
assembly: assembly.name,
|
|
11651
|
+
oldEnd: selectedDestinationFeature.max,
|
|
11652
|
+
newEnd: newMax,
|
|
11653
|
+
}));
|
|
11654
|
+
}
|
|
11655
|
+
for (const change of changes) {
|
|
11656
|
+
await submitChange(change);
|
|
10030
11657
|
}
|
|
10031
11658
|
};
|
|
11659
|
+
const submitChange = async (change) => {
|
|
11660
|
+
await apolloSessionModel.apolloDataStore.changeManager.submit(change);
|
|
11661
|
+
};
|
|
11662
|
+
const handleCreateNewGeneChange = (e) => {
|
|
11663
|
+
setCreateNewGene(e.target.checked);
|
|
11664
|
+
};
|
|
10032
11665
|
return (React__default["default"].createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
|
|
10033
11666
|
React__default["default"].createElement(material.DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
|
|
10034
11667
|
React__default["default"].createElement(material.DialogContent, null,
|
|
10035
11668
|
React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
|
|
10036
|
-
isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${getFeatureNameOrId(annotationFeature
|
|
11669
|
+
isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${getFeatureNameOrId(annotationFeature)} (${annotationFeature.min + 1}..${annotationFeature.max})` })),
|
|
10037
11670
|
annotationFeature.children && (React__default["default"].createElement(material.Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
|
|
10038
11671
|
.filter((child) => isTranscript(child, apolloSessionModel))
|
|
10039
11672
|
.map((child) => (React__default["default"].createElement(material.FormControlLabel, { key: child._id, control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
|
|
10040
11673
|
handleChildFeatureCheck(e, child);
|
|
10041
|
-
} }), label: `${getFeatureNameOrId(child
|
|
11674
|
+
} }), label: `${getFeatureNameOrId(child)} (${child.min + 1}..${child.max})` })))))),
|
|
10042
11675
|
destinationFeatures.length > 0 &&
|
|
10043
11676
|
((!parentFeatureChecked && checkedChildrens.length > 0) ||
|
|
10044
11677
|
(parentFeatureChecked &&
|
|
10045
|
-
isTranscript(annotationFeature, apolloSessionModel))) && (React__default["default"].createElement(
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
11678
|
+
isTranscript(annotationFeature, apolloSessionModel))) && (React__default["default"].createElement("div", { style: {
|
|
11679
|
+
border: '1px solid #ccc',
|
|
11680
|
+
marginTop: 20,
|
|
11681
|
+
padding: 10,
|
|
11682
|
+
borderRadius: 5,
|
|
11683
|
+
} },
|
|
11684
|
+
React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
|
|
11685
|
+
React__default["default"].createElement(material.Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
|
|
11686
|
+
React__default["default"].createElement(material.Box, { sx: { mt: 1 } },
|
|
11687
|
+
React__default["default"].createElement(material.Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange, disabled: createNewGene }, destinationFeatures.map((f) => (React__default["default"].createElement(material.MenuItem, { key: f._id, value: f._id }, `${getFeatureNameOrId(f)} (${f.min + 1}..${f.max})`)))))),
|
|
11688
|
+
React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
|
|
11689
|
+
React__default["default"].createElement(material.FormGroup, null,
|
|
11690
|
+
React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { checked: createNewGene, onChange: handleCreateNewGeneChange }), label: "Create new gene" })))))),
|
|
10049
11691
|
React__default["default"].createElement(material.DialogActions, null,
|
|
10050
11692
|
React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
|
|
10051
11693
|
(!parentFeatureChecked &&
|
|
@@ -10056,6 +11698,189 @@ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refS
|
|
|
10056
11698
|
React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
|
|
10057
11699
|
}
|
|
10058
11700
|
|
|
11701
|
+
function parseCigar(cigar) {
|
|
11702
|
+
const regex = /(\d+)([MIDNSHPX=])/g;
|
|
11703
|
+
const result = [];
|
|
11704
|
+
let match;
|
|
11705
|
+
while ((match = regex.exec(cigar)) !== null) {
|
|
11706
|
+
result.push([match[2], Number.parseInt(match[1], 10)]);
|
|
11707
|
+
}
|
|
11708
|
+
return result;
|
|
11709
|
+
}
|
|
11710
|
+
function annotationFromPileup(pluggableElement) {
|
|
11711
|
+
if (pluggableElement.name !== 'LinearPileupDisplay') {
|
|
11712
|
+
return pluggableElement;
|
|
11713
|
+
}
|
|
11714
|
+
const { stateModel } = pluggableElement;
|
|
11715
|
+
const newStateModel = stateModel
|
|
11716
|
+
.views((self) => ({
|
|
11717
|
+
getFirstRegion() {
|
|
11718
|
+
const lgv = util.getContainingView(self);
|
|
11719
|
+
return lgv.dynamicBlocks.contentBlocks[0];
|
|
11720
|
+
},
|
|
11721
|
+
getAssembly() {
|
|
11722
|
+
const firstRegion = self.getFirstRegion();
|
|
11723
|
+
const session = util.getSession(self);
|
|
11724
|
+
const { assemblyManager } = session;
|
|
11725
|
+
const { assemblyName } = firstRegion;
|
|
11726
|
+
const assembly = assemblyManager.get(assemblyName);
|
|
11727
|
+
if (!assembly) {
|
|
11728
|
+
throw new Error(`Could not find assembly named ${assemblyName}`);
|
|
11729
|
+
}
|
|
11730
|
+
return assembly;
|
|
11731
|
+
},
|
|
11732
|
+
getRefSeqId(assembly) {
|
|
11733
|
+
const firstRegion = self.getFirstRegion();
|
|
11734
|
+
const { refName } = firstRegion;
|
|
11735
|
+
const { refNameAliases } = assembly;
|
|
11736
|
+
if (!refNameAliases) {
|
|
11737
|
+
throw new Error(`Could not find aliases for ${assembly.name}`);
|
|
11738
|
+
}
|
|
11739
|
+
const newRefNames = [...Object.entries(refNameAliases)]
|
|
11740
|
+
.filter(([id, refName]) => id !== refName)
|
|
11741
|
+
.map(([id, refName]) => ({
|
|
11742
|
+
_id: id,
|
|
11743
|
+
name: refName,
|
|
11744
|
+
}));
|
|
11745
|
+
const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
|
|
11746
|
+
if (!refSeqId) {
|
|
11747
|
+
throw new Error(`Could not find refSeqId named ${refName}`);
|
|
11748
|
+
}
|
|
11749
|
+
return refSeqId;
|
|
11750
|
+
},
|
|
11751
|
+
getAnnotationFeature() {
|
|
11752
|
+
const feature = self.contextMenuFeature;
|
|
11753
|
+
const assembly = self.getAssembly();
|
|
11754
|
+
const refSeqId = self.getRefSeqId(assembly);
|
|
11755
|
+
const start = feature.get('start');
|
|
11756
|
+
const end = feature.get('end');
|
|
11757
|
+
const strand = feature.get('strand');
|
|
11758
|
+
const name = feature.get('name');
|
|
11759
|
+
const cigarData = feature.get('CIGAR');
|
|
11760
|
+
const ops = parseCigar(cigarData);
|
|
11761
|
+
let position = start;
|
|
11762
|
+
let currentExonStart;
|
|
11763
|
+
const exons = [];
|
|
11764
|
+
// Example: [[96,S], [4,M], [4216,N], [357,M], [1,I], [628,M], [94,S]]
|
|
11765
|
+
// Results in 2 exons
|
|
11766
|
+
// M, = and X are matches -> exon
|
|
11767
|
+
// N is a gap in the reference sequence -> intron
|
|
11768
|
+
// I, S, H and P -> not counted in reference position
|
|
11769
|
+
for (const [op, len] of ops) {
|
|
11770
|
+
switch (op) {
|
|
11771
|
+
case 'M':
|
|
11772
|
+
case '=':
|
|
11773
|
+
case 'X': {
|
|
11774
|
+
if (currentExonStart === undefined) {
|
|
11775
|
+
currentExonStart = position;
|
|
11776
|
+
}
|
|
11777
|
+
position += len;
|
|
11778
|
+
break;
|
|
11779
|
+
}
|
|
11780
|
+
case 'N': {
|
|
11781
|
+
if (currentExonStart !== undefined) {
|
|
11782
|
+
exons.push({
|
|
11783
|
+
start: currentExonStart,
|
|
11784
|
+
end: position,
|
|
11785
|
+
});
|
|
11786
|
+
currentExonStart = undefined;
|
|
11787
|
+
}
|
|
11788
|
+
position += len;
|
|
11789
|
+
break;
|
|
11790
|
+
}
|
|
11791
|
+
case 'D': {
|
|
11792
|
+
position += len;
|
|
11793
|
+
break;
|
|
11794
|
+
}
|
|
11795
|
+
case 'I':
|
|
11796
|
+
case 'S':
|
|
11797
|
+
case 'H':
|
|
11798
|
+
case 'P': {
|
|
11799
|
+
// These operations do not affect the position in the reference sequence
|
|
11800
|
+
break;
|
|
11801
|
+
}
|
|
11802
|
+
default: {
|
|
11803
|
+
throw new Error(`Unknown CIGAR operation: ${op}`);
|
|
11804
|
+
}
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11807
|
+
// If still in exon at end
|
|
11808
|
+
if (currentExonStart !== undefined) {
|
|
11809
|
+
exons.push({
|
|
11810
|
+
start: currentExonStart,
|
|
11811
|
+
end: position,
|
|
11812
|
+
});
|
|
11813
|
+
}
|
|
11814
|
+
const newFeature = {
|
|
11815
|
+
_id: ObjectID__default["default"]().toHexString(),
|
|
11816
|
+
refSeq: refSeqId,
|
|
11817
|
+
min: start,
|
|
11818
|
+
max: end,
|
|
11819
|
+
type: 'mRNA',
|
|
11820
|
+
strand,
|
|
11821
|
+
attributes: {
|
|
11822
|
+
name: [name],
|
|
11823
|
+
},
|
|
11824
|
+
};
|
|
11825
|
+
if (exons.length === 0) {
|
|
11826
|
+
return newFeature;
|
|
11827
|
+
}
|
|
11828
|
+
const children = {};
|
|
11829
|
+
newFeature.children = children;
|
|
11830
|
+
for (const exon of exons) {
|
|
11831
|
+
const newExon = {
|
|
11832
|
+
_id: ObjectID__default["default"]().toHexString(),
|
|
11833
|
+
refSeq: refSeqId,
|
|
11834
|
+
min: exon.start,
|
|
11835
|
+
max: exon.end,
|
|
11836
|
+
type: 'exon',
|
|
11837
|
+
strand,
|
|
11838
|
+
};
|
|
11839
|
+
newFeature.children[newExon._id] = newExon;
|
|
11840
|
+
}
|
|
11841
|
+
return newFeature;
|
|
11842
|
+
},
|
|
11843
|
+
}))
|
|
11844
|
+
.views((self) => {
|
|
11845
|
+
const superContextMenuItems = self.contextMenuItems;
|
|
11846
|
+
return {
|
|
11847
|
+
contextMenuItems() {
|
|
11848
|
+
const session = util.getSession(self);
|
|
11849
|
+
const assembly = self.getAssembly();
|
|
11850
|
+
const region = self.getFirstRegion();
|
|
11851
|
+
const feature = self.contextMenuFeature;
|
|
11852
|
+
if (!feature) {
|
|
11853
|
+
return superContextMenuItems();
|
|
11854
|
+
}
|
|
11855
|
+
return [
|
|
11856
|
+
...superContextMenuItems(),
|
|
11857
|
+
{
|
|
11858
|
+
label: 'Create Apollo annotation',
|
|
11859
|
+
icon: AddIcon__default["default"],
|
|
11860
|
+
onClick: () => {
|
|
11861
|
+
session.queueDialog((doneCallback) => [
|
|
11862
|
+
CreateApolloAnnotation,
|
|
11863
|
+
{
|
|
11864
|
+
session,
|
|
11865
|
+
handleClose: () => {
|
|
11866
|
+
doneCallback();
|
|
11867
|
+
},
|
|
11868
|
+
annotationFeature: self.getAnnotationFeature(assembly),
|
|
11869
|
+
assembly,
|
|
11870
|
+
refSeqId: self.getRefSeqId(assembly),
|
|
11871
|
+
region,
|
|
11872
|
+
},
|
|
11873
|
+
]);
|
|
11874
|
+
},
|
|
11875
|
+
},
|
|
11876
|
+
];
|
|
11877
|
+
},
|
|
11878
|
+
};
|
|
11879
|
+
});
|
|
11880
|
+
pluggableElement.stateModel = newStateModel;
|
|
11881
|
+
return pluggableElement;
|
|
11882
|
+
}
|
|
11883
|
+
|
|
10059
11884
|
function simpleFeatureToGFF3Feature(feature, refSeqId) {
|
|
10060
11885
|
// eslint-disable-next-line unicorn/prefer-structured-clone
|
|
10061
11886
|
const xfeature = JSON.parse(JSON.stringify(feature));
|
|
@@ -10159,6 +11984,7 @@ function annotationFromJBrowseFeature(pluggableElement) {
|
|
|
10159
11984
|
contextMenuItems() {
|
|
10160
11985
|
const session = util.getSession(self);
|
|
10161
11986
|
const assembly = self.getAssembly();
|
|
11987
|
+
const region = self.getFirstRegion();
|
|
10162
11988
|
const feature = self.contextMenuFeature;
|
|
10163
11989
|
if (!feature) {
|
|
10164
11990
|
return superContextMenuItems();
|
|
@@ -10179,6 +12005,7 @@ function annotationFromJBrowseFeature(pluggableElement) {
|
|
|
10179
12005
|
annotationFeature: self.getAnnotationFeature(assembly),
|
|
10180
12006
|
assembly,
|
|
10181
12007
|
refSeqId: self.getRefSeqId(assembly),
|
|
12008
|
+
region,
|
|
10182
12009
|
},
|
|
10183
12010
|
]);
|
|
10184
12011
|
},
|
|
@@ -10259,7 +12086,7 @@ const LinearApolloDisplay = mobxReact.observer(function LinearApolloDisplay(prop
|
|
|
10259
12086
|
else {
|
|
10260
12087
|
const coord = [event.clientX, event.clientY];
|
|
10261
12088
|
setContextCoord(coord);
|
|
10262
|
-
setContextMenuItems(getContextMenuItems(
|
|
12089
|
+
setContextMenuItems(getContextMenuItems(event));
|
|
10263
12090
|
}
|
|
10264
12091
|
} },
|
|
10265
12092
|
loading ? (React__default["default"].createElement("div", { className: classes.loading },
|
|
@@ -10331,23 +12158,17 @@ const LinearApolloDisplay = mobxReact.observer(function LinearApolloDisplay(prop
|
|
|
10331
12158
|
: undefined, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenuItems }))))));
|
|
10332
12159
|
});
|
|
10333
12160
|
|
|
10334
|
-
const TrackLines = mobxReact.observer(function TrackLines({ model,
|
|
10335
|
-
const { apolloRowHeight, highestRow,
|
|
10336
|
-
|
|
12161
|
+
const TrackLines = mobxReact.observer(function TrackLines({ model, hrStyle = { margin: 0, top: 0, color: 'black' }, idx = 0, }) {
|
|
12162
|
+
const { apolloRowHeight, highestRow, showFeatureLabels } = model;
|
|
12163
|
+
const featureLabelSpacer = showFeatureLabels ? 2 : 1;
|
|
12164
|
+
return (React__default["default"].createElement("div", { style: {
|
|
10337
12165
|
position: 'absolute',
|
|
10338
12166
|
left: 0,
|
|
10339
|
-
top: (apolloRowHeight * (highestRow + 1)) / 2
|
|
12167
|
+
top: (apolloRowHeight * featureLabelSpacer * (highestRow + 1)) / 2 +
|
|
12168
|
+
idx * featureLabelSpacer * apolloRowHeight,
|
|
10340
12169
|
width: '100%',
|
|
10341
12170
|
} },
|
|
10342
|
-
React__default["default"].createElement("hr", { style:
|
|
10343
|
-
position: 'absolute',
|
|
10344
|
-
left: 0,
|
|
10345
|
-
bottom: (apolloRowHeight * (highestRow + 1) + lastRowTooltipBufferHeight) /
|
|
10346
|
-
2 +
|
|
10347
|
-
3,
|
|
10348
|
-
width: '100%',
|
|
10349
|
-
} },
|
|
10350
|
-
React__default["default"].createElement("hr", { style: { margin: 0, top: 0, color: 'black' } })));
|
|
12171
|
+
React__default["default"].createElement("hr", { style: hrStyle })));
|
|
10351
12172
|
});
|
|
10352
12173
|
|
|
10353
12174
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
@@ -10407,8 +12228,9 @@ const LinearApolloSixFrameDisplay = mobxReact.observer(function LinearApolloSixF
|
|
|
10407
12228
|
// Promise.resolve() in these 3 callbacks is to avoid infinite rendering loop
|
|
10408
12229
|
// https://github.com/mobxjs/mobx/issues/3728#issuecomment-1715400931
|
|
10409
12230
|
React__default["default"].createElement(React__default["default"].Fragment, null,
|
|
10410
|
-
React__default["default"].createElement(TrackLines, { model: model,
|
|
10411
|
-
React__default["default"].createElement(TrackLines, { model: model,
|
|
12231
|
+
React__default["default"].createElement(TrackLines, { model: model, idx: 0 }),
|
|
12232
|
+
React__default["default"].createElement(TrackLines, { model: model, hrStyle: { margin: 0, top: 0, color: 'grey', opacity: 0.4 }, idx: 1 }),
|
|
12233
|
+
React__default["default"].createElement(TrackLines, { model: model, idx: 2 }),
|
|
10412
12234
|
React__default["default"].createElement("canvas", { ref: async (node) => {
|
|
10413
12235
|
await Promise.resolve();
|
|
10414
12236
|
setCollaboratorCanvas(node);
|
|
@@ -10756,7 +12578,7 @@ class ChangeManager {
|
|
|
10756
12578
|
jobsManager.abortJob(job.name, String(error));
|
|
10757
12579
|
}
|
|
10758
12580
|
console.error(error);
|
|
10759
|
-
session.notify(String(error), 'error');
|
|
12581
|
+
session.notify(`Error encountered in client: ${String(error)}. Data may be out of sync, please refresh the page`, 'error');
|
|
10760
12582
|
return;
|
|
10761
12583
|
}
|
|
10762
12584
|
// post-validate
|
|
@@ -10799,10 +12621,10 @@ class ChangeManager {
|
|
|
10799
12621
|
if (change.notification) {
|
|
10800
12622
|
session.notify(change.notification, 'success');
|
|
10801
12623
|
}
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
12624
|
+
if (addToRecents) {
|
|
12625
|
+
// Push the change into array
|
|
12626
|
+
this.recentChanges.push(change);
|
|
12627
|
+
}
|
|
10806
12628
|
}
|
|
10807
12629
|
if (updateJobsManager) {
|
|
10808
12630
|
jobsManager.done(job);
|
|
@@ -10810,7 +12632,8 @@ class ChangeManager {
|
|
|
10810
12632
|
}
|
|
10811
12633
|
async revert(change, submitToBackend = true) {
|
|
10812
12634
|
const inverseChange = change.getInverse();
|
|
10813
|
-
|
|
12635
|
+
const opts = { submitToBackend, addToRecents: false };
|
|
12636
|
+
return this.submit(inverseChange, opts);
|
|
10814
12637
|
}
|
|
10815
12638
|
/**
|
|
10816
12639
|
* Undo the last change
|
|
@@ -11636,15 +13459,6 @@ function extendSession(pluginManager, sessionModel) {
|
|
|
11636
13459
|
}))
|
|
11637
13460
|
.actions((self) => ({
|
|
11638
13461
|
afterCreate: mobxStateTree.flow(function* afterCreate() {
|
|
11639
|
-
// When the initial config.json loads, it doesn't include the Apollo
|
|
11640
|
-
// tracks, which would result in a potentially invalid session snapshot
|
|
11641
|
-
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
11642
|
-
// empty session snapshot, and then restore the original session
|
|
11643
|
-
// snapshot after the updated config.json loads.
|
|
11644
|
-
const sessionSnapshot = mobxStateTree.getSnapshot(self);
|
|
11645
|
-
const { id, name } = sessionSnapshot;
|
|
11646
|
-
mobxStateTree.applySnapshot(self, { name, id });
|
|
11647
|
-
const { internetAccounts, jbrowse } = mobxStateTree.getRoot(self);
|
|
11648
13462
|
mobx.autorun(() => {
|
|
11649
13463
|
// broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
|
|
11650
13464
|
const locations = [];
|
|
@@ -11693,7 +13507,23 @@ function extendSession(pluginManager, sessionModel) {
|
|
|
11693
13507
|
}
|
|
11694
13508
|
}
|
|
11695
13509
|
}, { name: 'ApolloSession' });
|
|
11696
|
-
//
|
|
13510
|
+
// When the initial config.json loads, it doesn't include the Apollo
|
|
13511
|
+
// tracks, which would result in a potentially invalid session snapshot
|
|
13512
|
+
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
13513
|
+
// empty session snapshot, and then restore the original session
|
|
13514
|
+
// snapshot after the updated config.json loads.
|
|
13515
|
+
// @ts-expect-error type is missing on ApolloRootModel
|
|
13516
|
+
const { internetAccounts, jbrowse, reloadPluginManagerCallback } = mobxStateTree.getRoot(self);
|
|
13517
|
+
const pluginConfiguration =
|
|
13518
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
13519
|
+
jbrowse.configuration.ApolloPlugin;
|
|
13520
|
+
const hasRole = configuration.readConfObject(pluginConfiguration, 'hasRole');
|
|
13521
|
+
if (hasRole) {
|
|
13522
|
+
return;
|
|
13523
|
+
}
|
|
13524
|
+
const sessionSnapshot = mobxStateTree.getSnapshot(self);
|
|
13525
|
+
const { id, name } = sessionSnapshot;
|
|
13526
|
+
mobxStateTree.applySnapshot(self, { name, id });
|
|
11697
13527
|
// fetch and initialize assemblies for each of our Apollo internet accounts
|
|
11698
13528
|
for (const internetAccount of internetAccounts) {
|
|
11699
13529
|
if (internetAccount.type !== 'ApolloInternetAccount') {
|
|
@@ -11726,9 +13556,8 @@ function extendSession(pluginManager, sessionModel) {
|
|
|
11726
13556
|
console.error(error);
|
|
11727
13557
|
continue;
|
|
11728
13558
|
}
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
mobxStateTree.applySnapshot(self, sessionSnapshot);
|
|
13559
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
13560
|
+
reloadPluginManagerCallback(jbrowseConfig, sessionSnapshot);
|
|
11732
13561
|
}
|
|
11733
13562
|
}),
|
|
11734
13563
|
beforeDestroy() {
|