@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.
Files changed (56) hide show
  1. package/dist/index.esm.js +2679 -850
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2676 -847
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +5194 -1258
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +18 -0
  13. package/src/ChangeManager.ts +10 -6
  14. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  15. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +12 -20
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +929 -175
  17. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +48 -60
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +244 -51
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +46 -1
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +9 -1
  23. package/src/LinearApolloDisplay/stateModel/base.ts +29 -0
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +51 -35
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +2 -1
  26. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +7 -2
  27. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  28. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +243 -124
  29. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -1
  30. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +19 -3
  31. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +53 -34
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +4 -2
  33. package/src/OntologyManager/index.ts +4 -1
  34. package/src/TabularEditor/HybridGrid/Feature.tsx +4 -0
  35. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  36. package/src/components/AddAssemblyAliases.tsx +114 -0
  37. package/src/components/AddChildFeature.tsx +3 -6
  38. package/src/components/AddFeature.tsx +14 -15
  39. package/src/components/CopyFeature.tsx +2 -4
  40. package/src/components/CreateApolloAnnotation.tsx +334 -151
  41. package/src/components/DeleteFeature.tsx +358 -11
  42. package/src/components/DownloadGFF3.tsx +20 -1
  43. package/src/components/FilterTranscripts.tsx +86 -0
  44. package/src/components/MergeExons.tsx +193 -0
  45. package/src/components/MergeTranscripts.tsx +185 -0
  46. package/src/components/SplitExon.tsx +134 -0
  47. package/src/components/index.ts +3 -0
  48. package/src/config.ts +5 -0
  49. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  50. package/src/extensions/annotationFromPileup.ts +99 -89
  51. package/src/session/session.ts +26 -13
  52. package/src/util/annotationFeatureUtils.ts +65 -0
  53. package/src/util/copyToClipboard.ts +21 -0
  54. package/src/util/glyphUtils.ts +49 -0
  55. package/src/util/index.ts +2 -0
  56. 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.6";
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
- self.setEquivalentTypes(type, equivalents);
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
- async function onSubmit(event) {
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
- await changeManager.submit(change);
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
- async function onSubmit(event) {
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
- await changeManager.submit(change);
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
- await changeManager.submit(change);
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
- await changeManager.submit(change);
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, notify } = session;
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
- await changeManager.submit(change);
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
- // Delete features
2344
- const change = new shared.DeleteFeatureChange({
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
- deletedFeature: mobxStateTree.getSnapshot(sourceFeature),
2349
- parentFeatureId: sourceFeature.parent?._id,
2719
+ changes: [
2720
+ {
2721
+ deletedFeature: mobxStateTree.getSnapshot(sourceFeature),
2722
+ parentFeatureId: sourceFeature.parent?._id,
2723
+ },
2724
+ ],
2350
2725
  });
2351
- await changeManager.submit(change);
2352
- notify('Feature deleted successfully', 'success');
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: 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/no-unsafe-call */
2958
- function OpenLocalFile({ handleClose, session }) {
2959
- const { apolloDataStore } = session;
2960
- const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
2961
- const [file, setFile] = React.useState(null);
2962
- const [assemblyName, setAssemblyName] = React.useState('');
2963
- const [errorMessage, setErrorMessage] = React.useState('');
2964
- const [submitted, setSubmitted] = React.useState(false);
2965
- const theme = material.useTheme();
2966
- function handleChangeFile(e) {
2967
- const selectedFile = e.target.files?.item(0);
2968
- if (!selectedFile) {
2969
- return;
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
- setErrorMessage('');
2972
- setFile(selectedFile);
2973
- if (!assemblyName) {
2974
- const fileName = selectedFile.name;
2975
- const lastDotIndex = fileName.lastIndexOf('.');
2976
- if (lastDotIndex === -1) {
2977
- setAssemblyName(fileName);
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
- else {
2980
- setAssemblyName(fileName.slice(0, lastDotIndex));
3437
+ if (i > 0) {
3438
+ neighboringExons.five_prime = exons[i - 1];
2981
3439
  }
3440
+ break;
2982
3441
  }
3442
+ i++;
2983
3443
  }
2984
- async function onSubmit(event) {
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
- setSubmitted(true);
2988
- if (!file) {
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
- const assemblyConfig = {
3004
- name: assemblyId,
3005
- aliases: [assemblyName],
3006
- displayName: assemblyName,
3007
- sequence: {
3008
- trackId: `sequenceConfigId-${assemblyName}`,
3009
- type: 'ReferenceSequenceTrack',
3010
- adapter: { type: 'ApolloSequenceAdapter', assemblyId },
3011
- metadata: {
3012
- apollo: true,
3013
- ...(util.isElectron
3014
- ? { file: file.path }
3015
- : {}),
3016
- },
3017
- },
3018
- };
3019
- // Save assembly into session
3020
- await (addSessionAssembly || addAssembly)(assemblyConfig);
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
- attributes: remainingAttributes,
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
- attributes: serializedAttributes,
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
- attributes: serializedAttributes,
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
- let sequence = getSequence(loc.min, loc.max);
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
- let sequence = getSequence(loc.min, loc.max);
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
- // Function to copy text to clipboard
5116
- const copyToClipboard = () => {
5899
+ const onCopyClick = () => {
5117
5900
  const seqDiv = seqRef.current;
5118
5901
  if (!seqDiv) {
5119
5902
  return;
5120
5903
  }
5121
- const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' });
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: copyToClipboard, style: { marginLeft: 10 }, size: "medium" }, "Copy sequence"),
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
- // Separate function to handle CDS location change
5195
- // because start of CDS and exon might be same
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
- for (const [, child] of feature.children) {
5201
- if (child.type !== 'CDS') {
5202
- continue;
5203
- }
5204
- if (isMin && oldLocation === child.min) {
5205
- const change = new shared.LocationStartChange({
5206
- typeName: 'LocationStartChange',
5207
- changedIds: [child._id],
5208
- featureId: feature._id,
5209
- oldStart: child.min,
5210
- newStart: newLocation,
5211
- assembly,
5212
- });
5213
- changeManager.submit(change).catch(() => {
5214
- notify('Error updating feature start position', 'error');
5215
- });
5216
- return;
5217
- }
5218
- if (!isMin && oldLocation === child.max) {
5219
- const change = new shared.LocationEndChange({
5220
- typeName: 'LocationEndChange',
5221
- changedIds: [child._id],
5222
- featureId: feature._id,
5223
- oldEnd: child.max,
5224
- newEnd: newLocation,
5225
- assembly,
5226
- });
5227
- changeManager.submit(change).catch(() => {
5228
- notify('Error updating feature start position', 'error');
5229
- });
5230
- return;
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
- for (const [, child] of feature.children) {
5239
- if (child.type !== 'exon') {
5240
- continue;
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
- if (isMin && oldLocation === child.min) {
5243
- const change = new shared.LocationStartChange({
5244
- typeName: 'LocationStartChange',
5245
- changedIds: [child._id],
5246
- featureId: feature._id,
5247
- oldStart: child.min,
5248
- newStart: newLocation,
5249
- assembly,
5250
- });
5251
- changeManager.submit(change).catch(() => {
5252
- notify('Error updating feature start position', 'error');
5253
- });
5254
- return;
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
- if (!isMin && oldLocation === child.max) {
5257
- const change = new shared.LocationEndChange({
5258
- typeName: 'LocationEndChange',
5259
- changedIds: [child._id],
5260
- featureId: feature._id,
5261
- oldEnd: child.max,
5262
- newEnd: newLocation,
5263
- assembly,
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
- if (!refData) {
5273
- return null;
5274
- }
5275
- const { cdsLocations, transcriptExonParts, strand } = feature;
5276
- const [firstCDSLocation] = cdsLocations;
5277
- const exonParts = transcriptExonParts
5278
- .filter((part) => part.type === 'exon')
5279
- .sort(({ min: a }, { min: b }) => a - b);
5280
- const exonMin = exonParts[0]?.min;
5281
- const exonMax = exonParts[exonParts.length - 1]?.max;
5282
- let cdsMin = exonMin;
5283
- let cdsMax = exonMax;
5284
- const cdsPresent = firstCDSLocation.length > 0;
5285
- if (cdsPresent) {
5286
- cdsMin = firstCDSLocation[0].min;
5287
- cdsMax = firstCDSLocation[firstCDSLocation.length - 1].max;
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
- for (const loc of firstLocation) {
5337
- let sequence = refData.getSequence(loc.min, loc.max);
5338
- if (strand === -1) {
5339
- sequence = util.revcom(sequence);
5340
- }
5341
- wholeSequence += sequence;
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 = getStartCodonGenomicLocation(codonGenomicPos);
5360
- if (startCodonGenomicLocation !== cdsMin) {
5361
- handleCDSLocationChange(cdsMin, startCodonGenomicLocation, feature, true);
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 getStartCodonGenomicLocation = (codonGenomicPosition) => {
6568
+ const getCodonGenomicLocation = (codonGenomicPosition) => {
5381
6569
  const [firstLocation] = cdsLocations;
5382
6570
  let cdsLen = 0;
5383
- for (const loc of firstLocation) {
5384
- const locLength = loc.max - loc.min;
5385
- // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
5386
- // and codonGenomicPosition is 25
5387
- // (((10 - 0) + (30 - 20)) + 10) > 25
5388
- // 40 + (25-20) = 45 is the genomic location of the start codon
5389
- if (cdsLen + locLength > codonGenomicPosition) {
5390
- return loc.min + (codonGenomicPosition - cdsLen);
5391
- }
5392
- cdsLen += locLength;
5393
- }
5394
- return cdsMin;
5395
- };
5396
- const getStopCodonGenomicLocation = (codonGenomicPosition) => {
5397
- const [firstLocation] = cdsLocations;
5398
- let cdsLen = 0;
5399
- for (const loc of firstLocation) {
5400
- const locLength = loc.max - loc.min;
5401
- // Check if the codonPosition is within the current location
5402
- if (cdsLen + locLength > codonGenomicPosition) {
5403
- return loc.min + (codonGenomicPosition - cdsLen);
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
- cdsLen += locLength;
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 last stop codon
6622
+ // Trim any sequence before first start codon and after stop codon
5431
6623
  const startCodonIndex = translationSequence.indexOf('M');
5432
- const stopCodonIndex = translationSequence.lastIndexOf('*') + 1;
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 = getStartCodonGenomicLocation(startCodonPos);
5439
- const stopCodonGenomicLoc = getStopCodonGenomicLocation(stopCodonPos);
5440
- if (startCodonGenomicLoc !== cdsMin) {
5441
- handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true);
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 (stopCodonGenomicLoc !== cdsMax) {
5444
- // TODO: getting error when trying to change the CDS start and end location at the same time
5445
- // Need to fix this
5446
- setTimeout(() => {
5447
- handleCDSLocationChange(cdsMax, stopCodonGenomicLoc, feature, false);
5448
- }, 1000);
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 copyToClipboard = () => {
6683
+ const onCopyClick = () => {
5452
6684
  const seqDiv = seqRef.current;
5453
6685
  if (!seqDiv) {
5454
6686
  return;
5455
6687
  }
5456
- const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' });
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, { defaultExpanded: true },
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: copyToClipboard })),
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, apolloInternetAccount: internetAccount, changeManager, regions, selectedFeature, session, } = display;
6353
- const menuItems = [];
7663
+ const { apolloHover } = display;
6354
7664
  if (!apolloHover) {
6355
- return menuItems;
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?.feature) {
6495
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature);
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
- if ('document' in globalThis) {
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?.feature) {
6974
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature);
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 isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS');
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
- const { offsetPx } = lgv;
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 (!overlappingExon) {
7028
- return;
7029
- }
7030
- const minPxInfo = lgv.bpToPx({
7031
- refName,
7032
- coord: overlappingExon.min,
7033
- regionNumber,
7034
- });
7035
- const maxPxInfo = lgv.bpToPx({
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
- const minPx = minPxInfo.offsetPx - offsetPx;
7044
- const maxPx = maxPxInfo.offsetPx - offsetPx;
7045
- if (Math.abs(maxPx - minPx) < 8) {
7046
- return;
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
- if (Math.abs(minPx - x) < 4) {
7049
- return { feature: overlappingExon, edge: 'min' };
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 (Math.abs(maxPx - x) < 4) {
7052
- return { feature: overlappingExon, edge: 'max' };
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, getContextMenuItems: getContextMenuItems$2, onMouseLeave: onMouseLeave$2 } = boxGlyph;
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, getContextMenuItems: getContextMenuItems$1, onMouseDown: onMouseDown$1, onMouseLeave: onMouseLeave$1, onMouseMove: onMouseMove$1, onMouseUp: onMouseUp$1, } = boxGlyph;
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(contextCoord) {
9663
+ contextMenuItems(event) {
8155
9664
  const { apolloHover } = self;
8156
- if (!(apolloHover && contextCoord)) {
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
- return glyph.getContextMenuItems(self);
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
- let change;
8190
- if (edge === 'max') {
8191
- const featureId = feature._id;
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: [featureId],
8197
- featureId,
8198
- oldEnd,
8199
- newEnd,
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: [featureId],
8210
- featureId,
8211
- oldStart,
8212
- newStart,
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
- if ('document' in globalThis) {
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 = Math.round(0.6 * rowHeight);
8367
- const cdsHeight = Math.round(0.7 * rowHeight);
8368
- const { children, min, strand, _id } = topLevelFeature;
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
- apolloSelectedFeature && _id === apolloSelectedFeature._id
8395
- ? material.alpha('rgb(0,0,0)', 0.7)
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, _id } = child;
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
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS') &&
8461
- _id === apolloSelectedFeature.parent?._id) ||
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
- const cdsStartPx = reversed ? minX - cdsWidthPx : minX;
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
- const cdsTop = (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2;
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
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS') &&
8483
- _id === apolloSelectedFeature.parent?._id
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 ? rowHeight * highestRow + 1 : 1, // Avoid render ceiling
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 = Math.round(0.7 * rowHeight);
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 - 1) * rowHeight + (rowHeight - cdsHeight) / 2;
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?.cds) {
8644
- const { cds, feature } = featureAndGlyphUnderMouse;
8645
- if (!featureTypeOntology) {
8646
- throw new Error('featureTypeOntology is undefined');
8647
- }
8648
- if (!feature.children) {
8649
- return;
8650
- }
8651
- for (const child of feature.children.values()) {
8652
- const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS');
8653
- if (childIsCDS && cds.max <= child.max && cds.min >= child.min) {
8654
- stateModel.setSelectedFeature(child);
8655
- break;
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
- else if (featureAndGlyphUnderMouse?.feature) {
8660
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature);
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 { bp, refName, regionNumber, x } = mousePosition;
8675
- const { lgv } = stateModel;
8676
- const { offsetPx } = lgv;
8677
- const minPxInfo = lgv.bpToPx({ refName, coord: cds.min, regionNumber });
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 (!overlappingExon) {
8704
- return;
8705
- }
8706
- const minPxInfo = lgv.bpToPx({
8707
- refName,
8708
- coord: overlappingExon.min,
8709
- regionNumber,
8710
- });
8711
- const maxPxInfo = lgv.bpToPx({
8712
- refName,
8713
- coord: overlappingExon.max,
8714
- regionNumber,
8715
- });
8716
- if (minPxInfo === undefined || maxPxInfo === undefined) {
8717
- return;
8718
- }
8719
- const minPx = minPxInfo.offsetPx - offsetPx;
8720
- const maxPx = maxPxInfo.offsetPx - offsetPx;
8721
- if (Math.abs(maxPx - minPx) < 8) {
8722
- return;
8723
- }
8724
- if (Math.abs(minPx - x) < 4) {
8725
- return { feature: overlappingExon, edge: 'min' };
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, 'transcript') &&
8881
- util.isSessionModelWithWidgets(session)) {
10512
+ if (featureTypeOntology.isTypeOf(sourceFeature.type, 'gene')) {
8882
10513
  menuItems.push({
8883
- label: 'Edit transcript details',
10514
+ label: 'Filter alternate transcripts',
8884
10515
  onClick: () => {
8885
- const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
8886
- feature: sourceFeature,
8887
- assembly: currentAssemblyId,
8888
- changeManager,
8889
- refName: region.refName,
8890
- });
8891
- session.showWidget(apolloTranscriptWidget);
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 ? 4 : 5;
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 ? 4 : 5;
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
- return ((self.highestRow + 1) * self.apolloRowHeight +
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 ([4, 5].includes(row)) {
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) => f.cds != null && bp >= f.cds.min && bp <= f.cds.max);
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
- let change;
9538
- if (edge === 'max') {
9539
- const featureId = feature._id;
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: [featureId],
9545
- featureId,
9546
- oldEnd,
9547
- newEnd,
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: [featureId],
9558
- featureId,
9559
- oldStart,
9560
- newStart,
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
- function parseCigar(cigar) {
9661
- return (cigar.toUpperCase().match(/\d+\D/g) ?? []).map((op) => {
9662
- return [(/\D/.exec(op) ?? [])[0], Number.parseInt(op, 10)];
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
- const getFeatureId = (feature) => {
11385
+ function getFeatureName(feature) {
9862
11386
  const { attributes } = feature;
9863
- const id = attributes?.id;
9864
- if (id) {
9865
- return id[0];
9866
- }
9867
- return feature.type;
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
- if (featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
9879
- attrName = 'transcript_name';
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 name = attributes?.[attrName];
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[0];
11429
+ return `${feature.type} - ${name}`;
11430
+ }
11431
+ if (id) {
11432
+ return `${feature.type} - ${id}`;
9885
11433
  }
9886
- return getFeatureId(feature);
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 getFeatures = (min, max) => {
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 [, f] of features) {
9911
- if (f.type === 'chromosome') {
11451
+ for (const f of features ?? []) {
11452
+ if (f.min > region.end || f.max < region.start) {
9912
11453
  continue;
9913
11454
  }
9914
- const featureSnapshot = mobxStateTree.getSnapshot(f);
9915
- if (min >= featureSnapshot.min && max <= featureSnapshot.max) {
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
- let mins = [];
9924
- let maxes = [];
9925
- if (annotationFeature.children) {
9926
- const checkedAnnotationFeatureChildren = Object.values(annotationFeature.children)
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
- let change;
11486
+ // IF SOURCE FEATURE IS GENE
9961
11487
  if (isGene(annotationFeature, apolloSessionModel)) {
9962
- if (annotationFeature.children &&
9963
- checkedChildrens.length !==
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
- if (selectedDestinationFeature) {
9990
- change = new shared.AddFeatureChange({
9991
- parentFeatureId: selectedDestinationFeature._id,
9992
- changedIds: [selectedDestinationFeature._id],
9993
- typeName: 'AddFeatureChange',
9994
- assembly: assembly.name,
9995
- addedFeature: annotationFeature,
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
- setErrorMessage('There is no destination gene for this transcript');
10000
- return;
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
- if (!selectedDestinationFeature) {
10015
- return;
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
- const child = annotationFeature.children[childId];
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
- session.notify('Annotation added successfully', 'success');
10029
- handleClose();
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, apolloSessionModel)} (${annotationFeature.min + 1}..${annotationFeature.max})` })),
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, apolloSessionModel)} (${child.min + 1}..${child.max})` })))))),
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(material.Box, { sx: { ml: 3 } },
10046
- React__default["default"].createElement(material.Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
10047
- React__default["default"].createElement(material.Box, { sx: { mt: 1 } },
10048
- React__default["default"].createElement(material.Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default["default"].createElement(material.MenuItem, { key: f._id, value: f._id }, `${getFeatureNameOrId(f, apolloSessionModel)} (${f.min}..${f.max})`)))))))),
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(coord));
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, strand, }) {
10335
- const { apolloRowHeight, highestRow, lastRowTooltipBufferHeight } = model;
10336
- return strand == 1 ? (React__default["default"].createElement("div", { style: {
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 - 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: { margin: 0, top: 0, color: 'black' } }))) : (React__default["default"].createElement("div", { 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, strand: 1 }),
10411
- React__default["default"].createElement(TrackLines, { model: model, strand: -1 }),
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
- if (addToRecents) {
10804
- // Push the change into array
10805
- this.recentChanges.push(change);
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
- return this.submit(inverseChange, { submitToBackend, addToRecents: false });
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
- // END AUTORUN
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
- mobxStateTree.applySnapshot(jbrowse, jbrowseConfig);
11730
- // @ts-expect-error snapshot seems to get wrong type?
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() {