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