@apollo-annotation/jbrowse-plugin-apollo 0.1.21 → 0.2.1

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 (57) hide show
  1. package/dist/index.esm.js +431 -570
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +439 -578
  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 +11064 -1091
  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 -5
  12. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +4 -2
  13. package/src/ApolloInternetAccount/configSchema.ts +1 -1
  14. package/src/ApolloInternetAccount/model.ts +5 -10
  15. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +1 -1
  16. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +4 -5
  17. package/src/BackendDrivers/DesktopFileDriver.ts +3 -2
  18. package/src/FeatureDetailsWidget/Attributes.tsx +1 -6
  19. package/src/FeatureDetailsWidget/NumberTextField.tsx +1 -0
  20. package/src/FeatureDetailsWidget/StringTextField.tsx +1 -0
  21. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +131 -382
  22. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +209 -284
  23. package/src/FeatureDetailsWidget/model.ts +4 -4
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -0
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +25 -3
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +95 -32
  27. package/src/LinearApolloDisplay/stateModel/base.ts +5 -3
  28. package/src/LinearApolloDisplay/stateModel/index.ts +1 -1
  29. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +1 -1
  30. package/src/LinearApolloDisplay/stateModel/rendering.ts +1 -1
  31. package/src/OntologyManager/OntologyStore/fulltext.ts +5 -2
  32. package/src/OntologyManager/OntologyStore/index.ts +25 -22
  33. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +8 -3
  34. package/src/OntologyManager/index.ts +31 -8
  35. package/src/SixFrameFeatureDisplay/stateModel.ts +1 -1
  36. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +1 -0
  37. package/src/TabularEditor/HybridGrid/NumberCell.tsx +1 -0
  38. package/src/TabularEditor/model.ts +1 -1
  39. package/src/components/AddChildFeature.tsx +1 -0
  40. package/src/components/AddFeature.tsx +1 -1
  41. package/src/components/AddRefSeqAliases.tsx +1 -0
  42. package/src/components/CopyFeature.tsx +1 -0
  43. package/src/components/DeleteAssembly.tsx +1 -0
  44. package/src/components/DeleteFeature.tsx +1 -0
  45. package/src/components/LogOut.tsx +2 -1
  46. package/src/components/ManageChecks.tsx +1 -1
  47. package/src/components/ManageUsers.tsx +2 -1
  48. package/src/components/OntologyTermAutocomplete.tsx +7 -9
  49. package/src/components/OntologyTermMultiSelect.tsx +2 -1
  50. package/src/components/OpenLocalFile.tsx +3 -1
  51. package/src/components/ViewChangeLog.tsx +1 -0
  52. package/src/components/ViewCheckResults.tsx +1 -0
  53. package/src/config.ts +5 -0
  54. package/src/extensions/annotationFromPileup.ts +1 -1
  55. package/src/makeDisplayComponent.tsx +28 -7
  56. package/src/session/ClientDataStore.ts +1 -1
  57. package/src/session/session.ts +2 -1
@@ -24,10 +24,11 @@ var CloseIcon = require('@mui/icons-material/Close');
24
24
  var mobxReact = require('mobx-react');
25
25
  var mui = require('tss-react/mui');
26
26
  var mst = require('@jbrowse/core/util/types/mst');
27
+ var withAsyncIttr = require('idb/with-async-ittr');
28
+ var aborting = require('@jbrowse/core/util/aborting');
27
29
  var jsonpath = require('jsonpath');
28
30
  var io = require('@jbrowse/core/util/io');
29
31
  var equal = require('fast-deep-equal/es6');
30
- var withAsyncIttr = require('idb/with-async-ittr');
31
32
  var fileSaver = require('file-saver');
32
33
  var Checkbox = require('@mui/material/Checkbox');
33
34
  var FormControlLabel = require('@mui/material/FormControlLabel');
@@ -100,7 +101,7 @@ var ExpandMoreIcon__default = /*#__PURE__*/_interopDefaultLegacy(ExpandMoreIcon)
100
101
  var ErrorIcon__default = /*#__PURE__*/_interopDefaultLegacy(ErrorIcon);
101
102
  var SaveIcon__default = /*#__PURE__*/_interopDefaultLegacy(SaveIcon);
102
103
 
103
- var version = "0.1.21";
104
+ var version = "0.2.1";
104
105
 
105
106
  const ApolloConfigSchema = configuration.ConfigurationSchema('ApolloInternetAccount', {
106
107
  baseURL: {
@@ -696,7 +697,7 @@ function* getWords(node, jsonPaths, prefixes) {
696
697
  async function textSearch(text, tx, signal) {
697
698
  const db = await this.db;
698
699
  const myTx = tx ?? db.transaction(['nodes']);
699
- util.checkAbortSignal(signal);
700
+ aborting.checkAbortSignal(signal);
700
701
  const queryWords = [...wordsInString(text)];
701
702
  const queries = [];
702
703
  /**
@@ -706,10 +707,10 @@ async function textSearch(text, tx, signal) {
706
707
  const initialMatches = new Map();
707
708
  // find startsWith and complete matches
708
709
  queries.push(...queryWords.map(async (queryWord, queryWordIndex) => {
709
- util.checkAbortSignal(signal);
710
+ aborting.checkAbortSignal(signal);
710
711
  const idx = myTx.objectStore('nodes').index('full-text-words');
711
712
  for await (const cursor of idx.iterate(IDBKeyRange.bound(queryWord, `${queryWord}\uFFFF`, false, false))) {
712
- util.checkAbortSignal(signal);
713
+ aborting.checkAbortSignal(signal);
713
714
  const term = cursor.value;
714
715
  const termMatches = initialMatches.get(term.id) ?? [
715
716
  term,
@@ -720,11 +721,11 @@ async function textSearch(text, tx, signal) {
720
721
  }
721
722
  }));
722
723
  await Promise.all(queries);
723
- util.checkAbortSignal(signal);
724
+ aborting.checkAbortSignal(signal);
724
725
  // now rank the term matches and add some detail
725
726
  const results = [];
726
727
  for (const [, [term, wordIndexes]] of initialMatches) {
727
- util.checkAbortSignal(signal);
728
+ aborting.checkAbortSignal(signal);
728
729
  results.push(...elaborateMatch(this.textIndexFields, term, wordIndexes, queryWords, this.prefixes));
729
730
  }
730
731
  // sort the terms by score descending
@@ -736,7 +737,7 @@ function elaborateMatch(textIndexPaths, term, queryWordIndexes, queryWords, pref
736
737
  const sortedWordIndexes = [...queryWordIndexes].sort();
737
738
  const matchedQueryWords = sortedWordIndexes.map((i) => queryWords[i]);
738
739
  const queryWordRegexps = matchedQueryWords.map((queryWord) => {
739
- const escaped = queryWord.replaceAll(/[$()*+./?[\\\]^{|}-]/g, '\\$&');
740
+ const escaped = queryWord.replaceAll(/[$()*+./?[\\\]^{|}-]/g, String.raw `\$&`);
740
741
  return new RegExp(`\\b${escaped}`, 'gi');
741
742
  });
742
743
  // const needle = matchedQueryWords.join(' ')
@@ -883,7 +884,13 @@ async function loadOboGraphJson(db) {
883
884
  // TODO: using file streaming along with an event-based json parser
884
885
  // instead of JSON.parse and .readFile could probably make this faster
885
886
  // and less memory intensive
886
- const oboGraph = JSON.parse(await io.openLocation(this.sourceLocation).readFile('utf8'));
887
+ let oboGraph;
888
+ try {
889
+ oboGraph = JSON.parse(await io.openLocation(this.sourceLocation).readFile('utf8'));
890
+ }
891
+ catch {
892
+ throw new Error('Error in loading ontology');
893
+ }
887
894
  const parseTime = Date.now();
888
895
  const [graph, ...additionalGraphs] = oboGraph.graphs ?? [];
889
896
  if (!graph) {
@@ -962,12 +969,6 @@ async function isDatabaseCurrent(db) {
962
969
  }
963
970
 
964
971
  /* eslint-disable @typescript-eslint/only-throw-error */
965
- /**
966
- * @deprecated use the one from jbrowse core when it is published
967
- **/
968
- function isLocalPathLocation(location) {
969
- return (typeof location === 'object' && location !== null && 'localPath' in location);
970
- }
971
972
  async function arrayFromAsync(iter) {
972
973
  const a = [];
973
974
  for await (const i of iter) {
@@ -1022,7 +1023,7 @@ class OntologyStore {
1022
1023
  return 'obo-graph-json';
1023
1024
  }
1024
1025
  }
1025
- else if (isLocalPathLocation(this.sourceLocation) &&
1026
+ else if (util.isLocalPathLocation(this.sourceLocation) &&
1026
1027
  this.sourceLocation.localPath.endsWith('.json')) {
1027
1028
  return 'obo-graph-json';
1028
1029
  }
@@ -1042,14 +1043,21 @@ class OntologyStore {
1042
1043
  if (await this.isDatabaseCurrent(db)) {
1043
1044
  return db;
1044
1045
  }
1045
- const { sourceLocation, sourceType } = this;
1046
- if (sourceType === 'obo-graph-json') {
1047
- await this.loadOboGraphJson(db);
1046
+ try {
1047
+ const { sourceLocation, sourceType } = this;
1048
+ if (sourceType === 'obo-graph-json') {
1049
+ await this.loadOboGraphJson(db);
1050
+ }
1051
+ else {
1052
+ throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
1053
+ }
1054
+ return db;
1048
1055
  }
1049
- else {
1050
- throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
1056
+ catch (error) {
1057
+ db.close();
1058
+ await withAsyncIttr.deleteDB(this.dbName);
1059
+ throw error;
1051
1060
  }
1052
- return db;
1053
1061
  }
1054
1062
  async termCount(tx) {
1055
1063
  const db = await this.db;
@@ -1227,7 +1235,7 @@ class OntologyStore {
1227
1235
  }
1228
1236
  // fetch the full nodes and filter out deprecated ones
1229
1237
  const terms = [];
1230
- for await (const termId of termIds) {
1238
+ for (const termId of termIds) {
1231
1239
  const node = await myTx.objectStore('nodes').get(termId);
1232
1240
  if (node && isOntologyClass(node) && !isDeprecated(node)) {
1233
1241
  terms.push(node);
@@ -1285,15 +1293,22 @@ const OntologyManagerType = mobxStateTree.types
1285
1293
  'SO:': 'http://purl.obolibrary.org/obo/SO_',
1286
1294
  }),
1287
1295
  })
1296
+ .views((self) => ({
1297
+ get featureTypeOntologyName() {
1298
+ const jbConfig = mobxStateTree.getRoot(self).jbrowse
1299
+ .configuration;
1300
+ const pluginConfiguration = jbConfig.ApolloPlugin;
1301
+ const featureTypeOntologyName = configuration.readConfObject(pluginConfiguration, 'featureTypeOntologyName');
1302
+ return featureTypeOntologyName;
1303
+ },
1304
+ }))
1288
1305
  .views((self) => ({
1289
1306
  /**
1290
1307
  * gets the OntologyRecord for the ontology we should be
1291
1308
  * using for feature types (e.g. SO or maybe biotypes)
1292
1309
  **/
1293
1310
  get featureTypeOntology() {
1294
- // TODO: change this to read some configuration for which feature type ontology
1295
- // we should be using. currently hardcoded to use SO.
1296
- return this.findOntology('Sequence Ontology');
1311
+ return this.findOntology(self.featureTypeOntologyName);
1297
1312
  },
1298
1313
  findOntology(name, version) {
1299
1314
  return self.ontologies.find((record) => {
@@ -1405,7 +1420,6 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1405
1420
  // effect for clearing choices when not open
1406
1421
  React.useEffect(() => {
1407
1422
  if (!open) {
1408
- // eslint-disable-next-line unicorn/no-useless-undefined
1409
1423
  setTermChoices(undefined);
1410
1424
  }
1411
1425
  }, [open]);
@@ -1420,7 +1434,7 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1420
1434
  setCurrentOntologyTerm(term);
1421
1435
  }
1422
1436
  }, (error) => {
1423
- if (!signal.aborted && !util.isAbortException(error)) {
1437
+ if (!signal.aborted && !aborting.isAbortException(error)) {
1424
1438
  setCurrentOntologyTermInvalid(String(error));
1425
1439
  }
1426
1440
  });
@@ -1439,8 +1453,8 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1439
1453
  setTermChoices(soTerms);
1440
1454
  }
1441
1455
  }, (error) => {
1442
- if (!signal.aborted && !util.isAbortException(error)) {
1443
- session.notify(error.message, 'error');
1456
+ if (!signal.aborted && !aborting.isAbortException(error)) {
1457
+ session.notify(error instanceof Error ? error.message : String(error), 'error');
1444
1458
  }
1445
1459
  });
1446
1460
  }
@@ -1459,7 +1473,6 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
1459
1473
  return;
1460
1474
  }
1461
1475
  if (typeof newValue === 'string') {
1462
- // eslint-disable-next-line unicorn/no-useless-undefined
1463
1476
  setCurrentOntologyTerm(undefined);
1464
1477
  onChange(valueString, newValue);
1465
1478
  }
@@ -1494,7 +1507,7 @@ async function getCurrentTerm(ontologyStore, currentTermLabel, filterTerms, _sig
1494
1507
  }
1495
1508
  // TODO: support prefixed IDs as ontology terms here (e.g. SO:001234)
1496
1509
  const terms = await ontologyStore.getTermsWithLabelOrSynonym(currentTermLabel, { includeSubclasses: false });
1497
- const term = terms.find(filterTerms ?? (() => true));
1510
+ const term = terms.find((term) => (filterTerms ?? (() => true))(term));
1498
1511
  if (!term) {
1499
1512
  throw new Error(`not a valid ${ontologyStore.ontologyName} term`);
1500
1513
  }
@@ -1576,7 +1589,7 @@ function AddChildFeature({ changeManager, handleClose, session, sourceAssemblyId
1576
1589
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
1577
1590
  }
1578
1591
 
1579
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
1592
+ /* eslint-disable @typescript-eslint/unbound-method */
1580
1593
  function AddFeature({ changeManager, handleClose, region, session, }) {
1581
1594
  const { notify } = session;
1582
1595
  const [end, setEnd] = React.useState(String(region.end));
@@ -1636,7 +1649,6 @@ function AddFeature({ changeManager, handleClose, region, session, }) {
1636
1649
  break;
1637
1650
  }
1638
1651
  default: {
1639
- // eslint-disable-next-line unicorn/no-useless-undefined
1640
1652
  setStrand(undefined);
1641
1653
  }
1642
1654
  }
@@ -1851,7 +1863,7 @@ function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, so
1851
1863
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
1852
1864
  }
1853
1865
 
1854
- /* eslint-disable @typescript-eslint/no-misused-promises */
1866
+ /* eslint-disable @typescript-eslint/unbound-method */
1855
1867
  function DeleteAssembly({ changeManager, handleClose, session, }) {
1856
1868
  const { internetAccounts } = mobxStateTree.getRoot(session);
1857
1869
  const [selectedAssembly, setSelectedAssembly] = React.useState();
@@ -2234,6 +2246,7 @@ function ImportFeatures({ changeManager, handleClose, session, }) {
2234
2246
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
2235
2247
  }
2236
2248
 
2249
+ /* eslint-disable @typescript-eslint/unbound-method */
2237
2250
  function LogOut({ handleClose, session }) {
2238
2251
  const { internetAccounts } = mobxStateTree.getRoot(session);
2239
2252
  const [errorMessage, setErrorMessage] = React.useState('');
@@ -2253,7 +2266,7 @@ function LogOut({ handleClose, session }) {
2253
2266
  event.preventDefault();
2254
2267
  setErrorMessage('');
2255
2268
  selectedInternetAccount.removeToken();
2256
- window.location.reload();
2269
+ globalThis.location.reload();
2257
2270
  }
2258
2271
  return (React__default["default"].createElement(Dialog, { open: true, title: "Log out", handleClose: handleClose, maxWidth: false, "data-testid": "log-out" },
2259
2272
  React__default["default"].createElement("form", { onSubmit: onSubmit },
@@ -2374,7 +2387,7 @@ function ManageChecks({ handleClose, session }) {
2374
2387
  }
2375
2388
  else {
2376
2389
  const index = checks.indexOf(_id, 0);
2377
- if (index > -1) {
2390
+ if (index !== -1) {
2378
2391
  checks.splice(index, 1);
2379
2392
  }
2380
2393
  setSelectedChecks(checks);
@@ -2415,7 +2428,7 @@ function ManageChecks({ handleClose, session }) {
2415
2428
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
2416
2429
  }
2417
2430
 
2418
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
2431
+ /* eslint-disable @typescript-eslint/unbound-method */
2419
2432
  function ManageUsers({ changeManager, handleClose, session, }) {
2420
2433
  const { internetAccounts } = mobxStateTree.getRoot(session);
2421
2434
  const apolloInternetAccounts = internetAccounts.filter((ia) => ia.type === 'ApolloInternetAccount' && ia.role?.includes('admin'));
@@ -2499,7 +2512,7 @@ function ManageUsers({ changeManager, handleClose, session, }) {
2499
2512
  type: 'actions',
2500
2513
  getActions: (params) => [
2501
2514
  React__default["default"].createElement(xDataGrid.GridActionsCellItem, { key: `delete-${params.id}`, icon: React__default["default"].createElement(DeleteIcon__default["default"], null), onClick: async () => {
2502
- if (window.confirm('Delete this user?')) {
2515
+ if (globalThis.confirm('Delete this user?')) {
2503
2516
  await deleteUser(params.id);
2504
2517
  }
2505
2518
  }, disabled: isCurrentUser(params.id), label: "Delete" }),
@@ -2539,7 +2552,7 @@ function ManageUsers({ changeManager, handleClose, session, }) {
2539
2552
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
2540
2553
  }
2541
2554
 
2542
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
2555
+ /* eslint-disable @typescript-eslint/unbound-method */
2543
2556
  // interface TermAutocompleteResult extends TermValue {
2544
2557
  // label: string[]
2545
2558
  // match: string
@@ -2625,7 +2638,7 @@ function OntologyTermMultiSelect({ includeDeprecated, onChange, ontologyName, on
2625
2638
  callback(options);
2626
2639
  }
2627
2640
  catch (error) {
2628
- if (!util.isAbortException(error)) {
2641
+ if (!aborting.isAbortException(error)) {
2629
2642
  setErrorMessage(String(error));
2630
2643
  }
2631
2644
  }
@@ -2951,7 +2964,7 @@ function ModifyFeatureAttribute({ changeManager, handleClose, session, sourceAss
2951
2964
  React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", disabled: showAddNewForm, onClick: handleClose }, "Cancel")))));
2952
2965
  }
2953
2966
 
2954
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2967
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2955
2968
  function OpenLocalFile({ handleClose, session }) {
2956
2969
  const { apolloDataStore } = session;
2957
2970
  const { addAssembly, addSessionAssembly, assemblyManager, notify } = session;
@@ -3047,7 +3060,7 @@ function OpenLocalFile({ handleClose, session }) {
3047
3060
  React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
3048
3061
  }
3049
3062
 
3050
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
3063
+ /* eslint-disable @typescript-eslint/unbound-method */
3051
3064
  const useStyles$d = mui.makeStyles()((theme) => ({
3052
3065
  changeTextarea: {
3053
3066
  fontFamily: 'monospace',
@@ -3571,7 +3584,9 @@ const AuthTypeSelector = ({ baseURL, handleClose, name, }) => {
3571
3584
  setLoginTypes(data);
3572
3585
  }
3573
3586
  getAuthTypes().catch((error) => {
3574
- util.isAbortException(error) ? '' : setErrorMessage(String(error));
3587
+ if (!aborting.isAbortException(error)) {
3588
+ setErrorMessage(String(error));
3589
+ }
3575
3590
  });
3576
3591
  return () => {
3577
3592
  controller.abort();
@@ -3694,7 +3709,7 @@ const stateModelFactory$2 = (configSchema) => {
3694
3709
  async openAuthWindow(type, resolve, reject) {
3695
3710
  const redirectUri = util.isElectron
3696
3711
  ? 'http://localhost/auth'
3697
- : window.location.origin + window.location.pathname;
3712
+ : globalThis.location.origin + globalThis.location.pathname;
3698
3713
  const url = new URL('auth/login', self.baseURL);
3699
3714
  const params = new URLSearchParams({
3700
3715
  type,
@@ -3703,7 +3718,7 @@ const stateModelFactory$2 = (configSchema) => {
3703
3718
  url.search = params.toString();
3704
3719
  const eventName = `JBrowseAuthWindow-${self.internetAccountId}`;
3705
3720
  if (util.isElectron) {
3706
- const { ipcRenderer } = window.require('electron');
3721
+ const { ipcRenderer } = globalThis.require('electron');
3707
3722
  const redirectUriFromElectron = await ipcRenderer.invoke('openAuthWindow', {
3708
3723
  internetAccountId: self.internetAccountId,
3709
3724
  data: { redirect_uri: redirectUri },
@@ -3877,14 +3892,9 @@ const stateModelFactory$2 = (configSchema) => {
3877
3892
  }
3878
3893
  });
3879
3894
  socket.on('REQUEST_INFORMATION', (message) => {
3880
- const { channel, reqType, userSessionId } = message;
3895
+ const { channel, userSessionId } = message;
3881
3896
  if (channel === 'REQUEST_INFORMATION' && userSessionId !== token) {
3882
- switch (reqType) {
3883
- case 'CURRENT_LOCATION': {
3884
- session.broadcastLocations();
3885
- break;
3886
- }
3887
- }
3897
+ session.broadcastLocations();
3888
3898
  }
3889
3899
  });
3890
3900
  },
@@ -4556,7 +4566,6 @@ function ApolloRendering(props) {
4556
4566
  }
4557
4567
  await changeManager.submit(change);
4558
4568
  }
4559
- // eslint-disable-next-line unicorn/no-useless-undefined
4560
4569
  setDragging(undefined);
4561
4570
  setMovedDuringLastMouseDown(false);
4562
4571
  }
@@ -4569,7 +4578,6 @@ function ApolloRendering(props) {
4569
4578
  React__default["default"].createElement(material.Menu, { open: Boolean(contextMenuFeature), anchorReference: "anchorPosition", anchorPosition: contextCoord
4570
4579
  ? { left: contextCoord[0], top: contextCoord[1] }
4571
4580
  : undefined, "data-testid": "base_linear_display_context_menu", onClose: () => {
4572
- // eslint-disable-next-line unicorn/no-useless-undefined
4573
4581
  setContextMenuFeature(undefined);
4574
4582
  } },
4575
4583
  React__default["default"].createElement(material.MenuItem, { disabled: isReadOnly, key: 1, value: 1, onClick: () => {
@@ -4583,7 +4591,6 @@ function ApolloRendering(props) {
4583
4591
  session,
4584
4592
  handleClose: () => {
4585
4593
  doneCallback();
4586
- // eslint-disable-next-line unicorn/no-useless-undefined
4587
4594
  setContextMenuFeature(undefined);
4588
4595
  },
4589
4596
  changeManager,
@@ -4604,7 +4611,6 @@ function ApolloRendering(props) {
4604
4611
  session,
4605
4612
  handleClose: () => {
4606
4613
  doneCallback();
4607
- // eslint-disable-next-line unicorn/no-useless-undefined
4608
4614
  setContextMenuFeature(undefined);
4609
4615
  },
4610
4616
  changeManager,
@@ -4624,7 +4630,6 @@ function ApolloRendering(props) {
4624
4630
  session,
4625
4631
  handleClose: () => {
4626
4632
  doneCallback();
4627
- // eslint-disable-next-line unicorn/no-useless-undefined
4628
4633
  setContextMenuFeature(undefined);
4629
4634
  },
4630
4635
  changeManager,
@@ -4742,11 +4747,16 @@ function installApolloTextSearchAdapter(pluginManager) {
4742
4747
 
4743
4748
  const ApolloPluginConfigurationSchema = configuration.ConfigurationSchema('ApolloPlugin', {
4744
4749
  ontologies: mobxStateTree.types.array(OntologyRecordConfiguration),
4750
+ featureTypeOntologyName: {
4751
+ description: 'Name of the feature type ontology',
4752
+ type: 'string',
4753
+ defaultValue: 'Sequence Ontology',
4754
+ },
4745
4755
  });
4746
4756
 
4747
4757
  function parseCigar(cigar) {
4748
4758
  return (cigar.toUpperCase().match(/\d+\D/g) ?? []).map((op) => {
4749
- return [(op.match(/\D/) ?? [])[0], Number.parseInt(op, 10)];
4759
+ return [(/\D/.exec(op) ?? [])[0], Number.parseInt(op, 10)];
4750
4760
  });
4751
4761
  }
4752
4762
  function annotationFromPileup(pluggableElement) {
@@ -4918,7 +4928,7 @@ function annotationFromPileup(pluggableElement) {
4918
4928
  return pluggableElement;
4919
4929
  }
4920
4930
 
4921
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
4931
+ /* eslint-disable @typescript-eslint/unbound-method */
4922
4932
  const StringTextField = mobxReact.observer(function StringTextField({ onChangeCommitted, value: initialValue, ...props }) {
4923
4933
  const [value, setValue] = React.useState(String(initialValue));
4924
4934
  const [blur, setBlur] = React.useState(false);
@@ -5123,7 +5133,7 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
5123
5133
  }
5124
5134
  }
5125
5135
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
5126
- React__default["default"].createElement(material.Typography, { style: { display: 'inline', marginLeft: '15px' }, variant: "h5" }, "Attributes"),
5136
+ React__default["default"].createElement(material.Typography, { variant: "h5" }, "Attributes"),
5127
5137
  React__default["default"].createElement(material.Grid, { container: true, direction: "column", spacing: 1 },
5128
5138
  Object.entries(attributes).map(([key, value]) => {
5129
5139
  if (key === '') {
@@ -5172,7 +5182,7 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
5172
5182
  errorMessage ? (React__default["default"].createElement(material.Typography, { color: "error" }, errorMessage)) : null));
5173
5183
  });
5174
5184
 
5175
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
5185
+ /* eslint-disable @typescript-eslint/unbound-method */
5176
5186
  const NumberTextField = mobxReact.observer(function NumberTextField({ onChangeCommitted, value: initialValue, ...props }) {
5177
5187
  const [value, setValue] = React.useState(String(initialValue));
5178
5188
  const [blur, setBlur] = React.useState(false);
@@ -5299,7 +5309,7 @@ const BasicInformation = mobxReact.observer(function BasicInformation({ assembly
5299
5309
  errorMessage ? (React__default["default"].createElement(material.Typography, { color: "error" }, errorMessage)) : null));
5300
5310
  });
5301
5311
 
5302
- function formatSequence$1(seq, refName, start, end, wrap) {
5312
+ function formatSequence(seq, refName, start, end, wrap) {
5303
5313
  const header = `>${refName}:${start + 1}–${end}\n`;
5304
5314
  const body = wrap === undefined ? seq : shared.splitStringIntoChunks(seq, wrap).join('\n');
5305
5315
  return `${header}${body}`;
@@ -5329,7 +5339,7 @@ const Sequence = mobxReact.observer(function Sequence({ assembly, feature, refNa
5329
5339
  if (showSequence) {
5330
5340
  sequence = refSeq.getSequence(min, max);
5331
5341
  if (sequence) {
5332
- sequence = formatSequence$1(sequence, refName, min, max);
5342
+ sequence = formatSequence(sequence, refName, min, max);
5333
5343
  }
5334
5344
  else {
5335
5345
  void session.apolloDataStore.loadRefSeq([
@@ -5469,383 +5479,219 @@ const ApolloTranscriptDetailsModel = mobxStateTree.types
5469
5479
  },
5470
5480
  }));
5471
5481
 
5472
- /**
5473
- * Get single feature by featureId
5474
- * @param feature -
5475
- * @param featureId -
5476
- * @returns
5477
- */
5478
- function getFeatureFromId(feature, featureId) {
5479
- if (feature._id === featureId) {
5480
- return feature;
5481
- }
5482
- // Check if there is also childFeatures in parent feature and it's not empty
5483
- // Let's get featureId from recursive method
5484
- if (!feature.children) {
5485
- return;
5486
- }
5487
- for (const [, childFeature] of feature.children) {
5488
- const subFeature = getFeatureFromId(childFeature, featureId);
5489
- if (subFeature) {
5490
- return subFeature;
5491
- }
5492
- }
5493
- return;
5494
- }
5495
- function findExonInRange(exons, pairStart, pairEnd) {
5496
- for (const exon of exons) {
5497
- if (Number(exon.min) <= pairStart && Number(exon.max) >= pairEnd) {
5498
- return exon;
5499
- }
5500
- }
5501
- return null;
5502
- }
5503
- function removeMatchingExon(exons, matchStart, matchEnd) {
5504
- // Filter the array to remove elements matching the specified start and end
5505
- return exons.filter((exon) => !(exon.min === matchStart && exon.max === matchEnd));
5506
- }
5507
5482
  const TranscriptBasicInformation = mobxReact.observer(function TranscriptBasicInformation({ assembly, feature, refName, session, }) {
5508
5483
  const { notify } = session;
5509
5484
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5510
5485
  const refData = currentAssembly?.getByRefName(refName);
5511
5486
  const { changeManager } = session.apolloDataStore;
5512
- function handleStartChange(newStart, featureId, oldStart) {
5513
- newStart--;
5514
- oldStart--;
5515
- if (newStart < feature.min) {
5516
- notify('Feature start cannot be less than parent starts', 'error');
5517
- return;
5518
- }
5519
- const subFeature = getFeatureFromId(feature, featureId);
5520
- if (!subFeature?.children) {
5521
- return;
5487
+ const theme = material.useTheme();
5488
+ function handleLocationChange(oldLocation, newLocation, feature, isMin) {
5489
+ if (!feature.children) {
5490
+ throw new Error('Transcript should have child features');
5522
5491
  }
5523
- // Let's check CDS start and end values. And possibly update those too
5524
- for (const child of subFeature.children) {
5525
- if ((child[1].type === 'CDS' || child[1].type === 'exon') &&
5526
- child[1].min === oldStart) {
5492
+ for (const [, child] of feature.children) {
5493
+ if (isMin && oldLocation - 1 === child.min) {
5527
5494
  const change = new shared.LocationStartChange({
5528
5495
  typeName: 'LocationStartChange',
5529
- changedIds: [child[1]._id],
5530
- featureId,
5531
- oldStart,
5532
- newStart,
5496
+ changedIds: [child._id],
5497
+ featureId: feature._id,
5498
+ oldStart: oldLocation - 1,
5499
+ newStart: newLocation - 1,
5533
5500
  assembly,
5534
5501
  });
5535
5502
  changeManager.submit(change).catch(() => {
5536
5503
  notify('Error updating feature start position', 'error');
5537
5504
  });
5505
+ return;
5538
5506
  }
5539
- }
5540
- return;
5541
- }
5542
- function handleEndChange(newEnd, featureId, oldEnd) {
5543
- const subFeature = getFeatureFromId(feature, featureId);
5544
- if (newEnd > feature.max) {
5545
- notify('Feature start cannot be greater than parent end', 'error');
5546
- return;
5547
- }
5548
- if (!subFeature?.children) {
5549
- return;
5550
- }
5551
- // Let's check CDS start and end values. And possibly update those too
5552
- for (const child of subFeature.children) {
5553
- if ((child[1].type === 'CDS' || child[1].type === 'exon') &&
5554
- child[1].max === oldEnd) {
5507
+ if (!isMin && newLocation === child.max) {
5555
5508
  const change = new shared.LocationEndChange({
5556
5509
  typeName: 'LocationEndChange',
5557
- changedIds: [child[1]._id],
5558
- featureId,
5559
- oldEnd,
5560
- newEnd,
5510
+ changedIds: [child._id],
5511
+ featureId: feature._id,
5512
+ oldEnd: child.max,
5513
+ newEnd: newLocation,
5561
5514
  assembly,
5562
5515
  });
5563
5516
  changeManager.submit(change).catch(() => {
5564
- notify('Error updating feature end position', 'error');
5517
+ notify('Error updating feature start position', 'error');
5565
5518
  });
5519
+ return;
5566
5520
  }
5567
5521
  }
5568
- return;
5569
5522
  }
5570
- const featureNew = feature;
5571
- let exonsArray = [];
5572
- const traverse = (currentFeature) => {
5573
- if (currentFeature.type === 'exon') {
5574
- exonsArray.push({
5575
- min: currentFeature.min + 1,
5576
- max: currentFeature.max,
5577
- });
5578
- }
5579
- if (currentFeature.children) {
5580
- for (const child of currentFeature.children) {
5581
- traverse(child[1]);
5582
- }
5583
- }
5584
- };
5585
- traverse(featureNew);
5586
- const CDSresult = [];
5587
- const CDSData = featureNew.cdsLocations;
5588
- if (refData) {
5589
- for (const CDSDatum of CDSData) {
5590
- for (const dataPoint of CDSDatum) {
5591
- let startSeq = refData.getSequence(Number(dataPoint.min) - 2, Number(dataPoint.min));
5592
- let endSeq = refData.getSequence(Number(dataPoint.max), Number(dataPoint.max) + 2);
5593
- if (featureNew.strand === -1 && startSeq && endSeq) {
5594
- startSeq = util.revcom(startSeq);
5595
- endSeq = util.revcom(endSeq);
5596
- }
5597
- const oneCDS = {
5598
- id: featureNew._id,
5599
- type: 'CDS',
5600
- strand: Number(featureNew.strand),
5601
- min: dataPoint.min + 1,
5602
- max: dataPoint.max,
5603
- oldMin: dataPoint.min + 1,
5604
- oldMax: dataPoint.max,
5605
- startSeq,
5606
- endSeq,
5607
- };
5608
- // CDSresult.push(oneCDS)
5609
- // Check if there is already an object with the same start and end
5610
- const exists = CDSresult.some((obj) => obj.min === oneCDS.min &&
5611
- obj.max === oneCDS.max &&
5612
- obj.type === oneCDS.type);
5613
- // If no such object exists, add the new object to the array
5614
- if (!exists) {
5615
- CDSresult.push(oneCDS);
5616
- }
5617
- // Add possible UTRs
5618
- const foundExon = findExonInRange(exonsArray, dataPoint.min + 1, dataPoint.max);
5619
- if (foundExon && Number(foundExon.min) < dataPoint.min) {
5620
- if (feature.strand === 1) {
5621
- const oneCDS = {
5622
- id: feature._id,
5623
- type: 'five_prime_UTR',
5624
- strand: Number(feature.strand),
5625
- min: foundExon.min,
5626
- max: dataPoint.min,
5627
- oldMin: foundExon.min,
5628
- oldMax: dataPoint.min,
5629
- startSeq: '',
5630
- endSeq: '',
5631
- };
5632
- CDSresult.push(oneCDS);
5633
- }
5634
- else {
5635
- const oneCDS = {
5636
- id: feature._id,
5637
- type: 'three_prime_UTR',
5638
- strand: Number(feature.strand),
5639
- min: dataPoint.min + 1,
5640
- max: foundExon.min + 1,
5641
- oldMin: dataPoint.min + 1,
5642
- oldMax: foundExon.min + 1,
5643
- startSeq: '',
5644
- endSeq: '',
5645
- };
5646
- CDSresult.push(oneCDS);
5647
- }
5648
- exonsArray = removeMatchingExon(exonsArray, foundExon.min, foundExon.max);
5649
- }
5650
- if (foundExon && Number(foundExon.max) > dataPoint.max) {
5651
- if (feature.strand === 1) {
5652
- const oneCDS = {
5653
- id: feature._id,
5654
- type: 'three_prime_UTR',
5655
- strand: Number(feature.strand),
5656
- min: dataPoint.max + 1,
5657
- max: foundExon.max,
5658
- oldMin: dataPoint.max + 1,
5659
- oldMax: foundExon.max,
5660
- startSeq: '',
5661
- endSeq: '',
5662
- };
5663
- CDSresult.push(oneCDS);
5664
- }
5665
- else {
5666
- const oneCDS = {
5667
- id: feature._id,
5668
- type: 'five_prime_UTR',
5669
- strand: Number(feature.strand),
5670
- min: dataPoint.min + 1,
5671
- max: foundExon.max,
5672
- oldMin: dataPoint.min + 1,
5673
- oldMax: foundExon.max,
5674
- startSeq: '',
5675
- endSeq: '',
5676
- };
5677
- CDSresult.push(oneCDS);
5678
- }
5679
- exonsArray = removeMatchingExon(exonsArray, foundExon.min, foundExon.max);
5680
- }
5681
- if (dataPoint.min + 1 === foundExon?.min &&
5682
- dataPoint.max === foundExon.max) {
5683
- exonsArray = removeMatchingExon(exonsArray, foundExon.min, foundExon.max);
5684
- }
5685
- }
5686
- }
5687
- }
5688
- // Add remaining UTRs if any
5689
- if (exonsArray.length > 0) {
5690
- // eslint-disable-next-line unicorn/no-array-for-each
5691
- exonsArray.forEach((element) => {
5692
- if (featureNew.strand === 1) {
5693
- const oneCDS = {
5694
- id: featureNew._id,
5695
- type: 'five_prime_UTR',
5696
- strand: Number(featureNew.strand),
5697
- min: element.min + 1,
5698
- max: element.max,
5699
- oldMin: element.min + 1,
5700
- oldMax: element.max,
5701
- startSeq: '',
5702
- endSeq: '',
5703
- };
5704
- CDSresult.push(oneCDS);
5705
- }
5706
- else {
5707
- const oneCDS = {
5708
- id: featureNew._id,
5709
- type: 'three_prime_UTR',
5710
- strand: Number(featureNew.strand),
5711
- min: element.min + 1,
5712
- max: element.max + 1,
5713
- oldMin: element.min + 1,
5714
- oldMax: element.max + 1,
5715
- startSeq: '',
5716
- endSeq: '',
5717
- };
5718
- CDSresult.push(oneCDS);
5719
- }
5720
- exonsArray = removeMatchingExon(exonsArray, element.min, element.max);
5721
- });
5523
+ if (!refData) {
5524
+ return null;
5722
5525
  }
5723
- CDSresult.sort((a, b) => {
5724
- // Primary sorting by 'start' property
5725
- const startDifference = Number(a.min) - Number(b.min);
5726
- if (startDifference !== 0) {
5727
- return startDifference;
5728
- }
5729
- return Number(a.max) - Number(b.max);
5730
- });
5731
- if (CDSresult.length > 0) {
5732
- CDSresult[0].startSeq = '';
5733
- // eslint-disable-next-line unicorn/prefer-at
5734
- CDSresult[CDSresult.length - 1].endSeq = '';
5735
- // Loop through the array and clear "startSeq" or "endSeq" based on the conditions
5736
- for (let i = 0; i < CDSresult.length; i++) {
5737
- if (i > 0 && CDSresult[i].min === CDSresult[i - 1].max) {
5738
- // Clear "startSeq" if the current item's "start" is equal to the previous item's "end"
5739
- CDSresult[i].startSeq = '';
5526
+ const { strand, transcriptParts } = feature;
5527
+ const [firstLocation] = transcriptParts;
5528
+ const locationData = firstLocation
5529
+ .map((loc, idx) => {
5530
+ const { max, min, type } = loc;
5531
+ let label = type;
5532
+ if (label === 'threePrimeUTR') {
5533
+ label = '3` UTR';
5534
+ }
5535
+ else if (label === 'fivePrimeUTR') {
5536
+ label = '5` UTR';
5537
+ }
5538
+ let fivePrimeSpliceSite;
5539
+ let threePrimeSpliceSite;
5540
+ let frameColor;
5541
+ if (type === 'CDS') {
5542
+ const { phase } = loc;
5543
+ const frame = util.getFrame(min, max, strand ?? 1, phase);
5544
+ frameColor = theme.palette.framesCDS.at(frame)?.main;
5545
+ const previousLoc = firstLocation.at(idx - 1);
5546
+ const nextLoc = firstLocation.at(idx + 1);
5547
+ if (strand === 1) {
5548
+ if (previousLoc?.type === 'intron') {
5549
+ fivePrimeSpliceSite = refData.getSequence(min - 2, min);
5550
+ }
5551
+ if (nextLoc?.type === 'intron') {
5552
+ threePrimeSpliceSite = refData.getSequence(max, max + 2);
5553
+ }
5740
5554
  }
5741
- if (i < CDSresult.length - 1 &&
5742
- CDSresult[i].max === CDSresult[i + 1].min) {
5743
- // Clear "endSeq" if the next item's "start" is equal to the current item's "end"
5744
- CDSresult[i].endSeq = '';
5555
+ else {
5556
+ if (previousLoc?.type === 'intron') {
5557
+ fivePrimeSpliceSite = util.revcom(refData.getSequence(max, max + 2));
5558
+ }
5559
+ if (nextLoc?.type === 'intron') {
5560
+ threePrimeSpliceSite = util.revcom(refData.getSequence(min - 2, min));
5561
+ }
5745
5562
  }
5746
5563
  }
5747
- }
5748
- const transcriptItems = CDSresult;
5564
+ return {
5565
+ min,
5566
+ max,
5567
+ label,
5568
+ fivePrimeSpliceSite,
5569
+ threePrimeSpliceSite,
5570
+ frameColor,
5571
+ };
5572
+ })
5573
+ .filter((loc) => loc.label !== 'intron');
5749
5574
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
5750
- React__default["default"].createElement(material.Typography, { variant: "h5", style: { marginLeft: '15px', marginBottom: '0' } }, "CDS and UTRs"),
5751
- React__default["default"].createElement("div", null, transcriptItems.map((item, index) => (React__default["default"].createElement("div", { key: index, style: { display: 'flex', alignItems: 'center' } },
5752
- React__default["default"].createElement("span", { style: { marginLeft: '20px', width: '50px' } }, item.type === 'three_prime_UTR'
5753
- ? '3 UTR'
5754
- : // eslint-disable-next-line unicorn/no-nested-ternary
5755
- item.type === 'five_prime_UTR'
5756
- ? '5 UTR'
5757
- : 'CDS'),
5758
- React__default["default"].createElement("span", { style: { fontWeight: 'bold', width: '30px' } }, item.startSeq),
5759
- React__default["default"].createElement(NumberTextField, { margin: "dense", id: item.id, disabled: item.type !== 'CDS', style: {
5760
- width: '150px',
5761
- marginLeft: '8px',
5762
- backgroundColor: item.startSeq.trim() === '' && index !== 0
5763
- ? 'lightblue'
5764
- : 'inherit',
5765
- }, variant: "outlined", value: item.min, onChangeCommitted: (newStart) => {
5766
- handleStartChange(newStart, item.id, Number(item.oldMin));
5767
- } }),
5768
- React__default["default"].createElement("span", { style: { margin: '0 10px' } }, item.strand === -1 ? '-' : item.strand === 1 ? '+' : ''),
5769
- React__default["default"].createElement(NumberTextField, { margin: "dense", id: item.id, disabled: item.type !== 'CDS', style: {
5770
- width: '150px',
5771
- backgroundColor: item.endSeq.trim() === '' &&
5772
- index + 1 !== transcriptItems.length
5773
- ? 'lightblue'
5774
- : 'inherit',
5775
- }, variant: "outlined", value: item.max, onChangeCommitted: (newEnd) => {
5776
- handleEndChange(newEnd, item.id, Number(item.oldMax));
5777
- } }),
5778
- React__default["default"].createElement("span", { style: { marginLeft: '8px', fontWeight: 'bold' } }, item.endSeq)))))));
5575
+ React__default["default"].createElement(material.Typography, { variant: "h5" }, "Structure"),
5576
+ React__default["default"].createElement(material.Typography, { variant: "h6" },
5577
+ strand === 1 ? 'Forward' : 'Reverse',
5578
+ " strand"),
5579
+ React__default["default"].createElement(material.TableContainer, { component: material.Paper },
5580
+ React__default["default"].createElement(material.Table, { size: "small" },
5581
+ React__default["default"].createElement(material.TableBody, null, locationData.map((loc) => (React__default["default"].createElement(material.TableRow, { key: `${loc.label}:${loc.min}-${loc.max}` },
5582
+ React__default["default"].createElement(material.TableCell, { component: "th", scope: "row", style: { background: loc.frameColor } }, loc.label),
5583
+ React__default["default"].createElement(material.TableCell, null, loc.fivePrimeSpliceSite ?? ''),
5584
+ React__default["default"].createElement(material.TableCell, { padding: "none" },
5585
+ React__default["default"].createElement(NumberTextField, { margin: "dense", variant: "outlined", value: strand === 1 ? loc.min + 1 : loc.max, onChangeCommitted: (newLocation) => {
5586
+ handleLocationChange(strand === 1 ? loc.min + 1 : loc.max, newLocation, feature, strand === 1);
5587
+ } })),
5588
+ React__default["default"].createElement(material.TableCell, { padding: "none" },
5589
+ React__default["default"].createElement(NumberTextField, { margin: "dense",
5590
+ // disabled={item.type !== 'CDS'}
5591
+ variant: "outlined", value: strand === 1 ? loc.max : loc.min + 1, onChangeCommitted: (newLocation) => {
5592
+ handleLocationChange(strand === 1 ? loc.max : loc.min + 1, newLocation, feature, strand !== 1);
5593
+ } })),
5594
+ React__default["default"].createElement(material.TableCell, null, loc.threePrimeSpliceSite ?? '')))))))));
5779
5595
  });
5780
5596
 
5781
- const getCDSInfo = (feature, refData) => {
5782
- const CDSresult = [];
5783
- const traverse = (currentFeature, isParentMRNA) => {
5784
- if (isParentMRNA &&
5785
- (currentFeature.type === 'CDS' ||
5786
- currentFeature.type === 'three_prime_UTR' ||
5787
- currentFeature.type === 'five_prime_UTR')) {
5788
- let startSeq = refData.getSequence(Number(currentFeature.min) - 2, Number(currentFeature.min));
5789
- let endSeq = refData.getSequence(Number(currentFeature.max), Number(currentFeature.max) + 2);
5790
- if (currentFeature.strand === -1 && startSeq && endSeq) {
5791
- startSeq = util.revcom(startSeq);
5792
- endSeq = util.revcom(endSeq);
5793
- }
5794
- const oneCDS = {
5795
- id: currentFeature._id,
5796
- type: currentFeature.type,
5797
- strand: Number(currentFeature.strand),
5798
- min: currentFeature.min + 1,
5799
- max: currentFeature.max + 1,
5800
- oldMin: currentFeature.min + 1,
5801
- oldMax: currentFeature.max + 1,
5802
- startSeq: startSeq || '',
5803
- endSeq: endSeq || '',
5804
- };
5805
- CDSresult.push(oneCDS);
5806
- }
5807
- if (currentFeature.children) {
5808
- for (const child of currentFeature.children) {
5809
- traverse(child[1], feature.type === 'mRNA');
5597
+ const SEQUENCE_WRAP_LENGTH = 60;
5598
+ function getSequenceSegments(segmentType, feature, getSequence) {
5599
+ const segments = [];
5600
+ const { cdsLocations, strand, transcriptParts } = feature;
5601
+ switch (segmentType) {
5602
+ case 'genomic':
5603
+ case 'cDNA': {
5604
+ const [firstLocation] = transcriptParts;
5605
+ for (const loc of firstLocation) {
5606
+ if (segmentType === 'cDNA' && loc.type === 'intron') {
5607
+ continue;
5608
+ }
5609
+ let sequence = getSequence(loc.min, loc.max);
5610
+ if (strand === -1) {
5611
+ sequence = util.revcom(sequence);
5612
+ }
5613
+ const type = loc.type === 'fivePrimeUTR' || loc.type === 'threePrimeUTR'
5614
+ ? 'UTR'
5615
+ : loc.type;
5616
+ const previousSegment = segments.at(-1);
5617
+ if (!previousSegment) {
5618
+ const sequenceLines = shared.splitStringIntoChunks(sequence, SEQUENCE_WRAP_LENGTH);
5619
+ segments.push({
5620
+ type,
5621
+ sequenceLines,
5622
+ locs: [{ min: loc.min, max: loc.max }],
5623
+ });
5624
+ continue;
5625
+ }
5626
+ if (previousSegment.type === type) {
5627
+ const [previousSegmentFirstLine, ...previousSegmentFollowingLines] = previousSegment.sequenceLines;
5628
+ const newSequence = previousSegmentFollowingLines.join('') + sequence;
5629
+ previousSegment.sequenceLines = [
5630
+ previousSegmentFirstLine,
5631
+ ...shared.splitStringIntoChunks(newSequence, SEQUENCE_WRAP_LENGTH),
5632
+ ];
5633
+ previousSegment.locs.push({ min: loc.min, max: loc.max });
5634
+ }
5635
+ else {
5636
+ const count = segments.reduce((accumulator, currentSegment) => accumulator +
5637
+ currentSegment.sequenceLines.reduce((subAccumulator, currentLine) => subAccumulator + currentLine.length, 0), 0);
5638
+ const previousLineLength = count % SEQUENCE_WRAP_LENGTH;
5639
+ const newSegmentFirstLineLength = SEQUENCE_WRAP_LENGTH - previousLineLength;
5640
+ const newSegmentFirstLine = sequence.slice(0, newSegmentFirstLineLength);
5641
+ const newSegmentRemainderLines = shared.splitStringIntoChunks(sequence.slice(newSegmentFirstLineLength), SEQUENCE_WRAP_LENGTH);
5642
+ segments.push({
5643
+ type,
5644
+ sequenceLines: [newSegmentFirstLine, ...newSegmentRemainderLines],
5645
+ locs: [{ min: loc.min, max: loc.max }],
5646
+ });
5647
+ }
5810
5648
  }
5649
+ return segments;
5811
5650
  }
5812
- };
5813
- traverse(feature, feature.type === 'mRNA');
5814
- CDSresult.sort((a, b) => {
5815
- return Number(a.min) - Number(b.min);
5816
- });
5817
- if (CDSresult.length > 0) {
5818
- CDSresult[0].startSeq = '';
5819
- // eslint-disable-next-line unicorn/prefer-at
5820
- CDSresult[CDSresult.length - 1].endSeq = '';
5821
- // Loop through the array and clear "startSeq" or "endSeq" based on the conditions
5822
- for (let i = 0; i < CDSresult.length; i++) {
5823
- if (i > 0 && CDSresult[i].min === CDSresult[i - 1].max) {
5824
- // Clear "startSeq" if the current item's "start" is equal to the previous item's "end"
5825
- CDSresult[i].startSeq = '';
5826
- }
5827
- if (i < CDSresult.length - 1 &&
5828
- CDSresult[i].max === CDSresult[i + 1].min) {
5829
- // Clear "endSeq" if the next item's "start" is equal to the current item's "end"
5830
- CDSresult[i].endSeq = '';
5651
+ case 'CDS': {
5652
+ let wholeSequence = '';
5653
+ const [firstLocation] = cdsLocations;
5654
+ const locs = [];
5655
+ for (const loc of firstLocation) {
5656
+ let sequence = getSequence(loc.min, loc.max);
5657
+ if (strand === -1) {
5658
+ sequence = util.revcom(sequence);
5659
+ }
5660
+ wholeSequence += sequence;
5661
+ locs.push({ min: loc.min, max: loc.max });
5831
5662
  }
5663
+ const sequenceLines = shared.splitStringIntoChunks(wholeSequence, SEQUENCE_WRAP_LENGTH);
5664
+ segments.push({ type: 'CDS', sequenceLines, locs });
5665
+ return segments;
5666
+ }
5667
+ }
5668
+ }
5669
+ function getSegmentColor(type) {
5670
+ switch (type) {
5671
+ case 'upOrDownstream': {
5672
+ return 'rgb(255,255,255)';
5673
+ }
5674
+ case 'UTR': {
5675
+ return 'rgb(194,106,119)';
5676
+ }
5677
+ case 'CDS': {
5678
+ return 'rgb(93,168,153)';
5679
+ }
5680
+ case 'intron': {
5681
+ return 'rgb(187,187,187)';
5682
+ }
5683
+ case 'protein': {
5684
+ return 'rgb(148,203,236)';
5832
5685
  }
5833
5686
  }
5834
- return CDSresult;
5835
- };
5836
- function formatSequence(seq, refName, start, end, wrap) {
5837
- const header = `>${refName}:${start + 1}–${end}\n`;
5838
- const body = wrap === undefined ? seq : shared.splitStringIntoChunks(seq, wrap).join('\n');
5839
- return `${header}${body}`;
5840
5687
  }
5841
- const utrColor = 'rgb(20,200,200)'; // Slightly brighter cyan
5842
- const cdsColor = 'rgb(240,200,20)'; // Slightly brighter yellow
5843
- let textSegments = [{ text: '', color: '' }];
5844
5688
  const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ assembly, feature, refName, session, }) {
5845
5689
  const currentAssembly = session.apolloDataStore.assemblies.get(assembly);
5846
5690
  const refData = currentAssembly?.getByRefName(refName);
5847
5691
  const [showSequence, setShowSequence] = React.useState(false);
5848
- const [selectedOption, setSelectedOption] = React.useState('Select');
5692
+ const [selectedOption, setSelectedOption] = React.useState('CDS');
5693
+ const theme = material.useTheme();
5694
+ const seqRef = React.useRef(null);
5849
5695
  if (!(currentAssembly && refData)) {
5850
5696
  return null;
5851
5697
  }
@@ -5853,150 +5699,85 @@ const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ asse
5853
5699
  if (!refSeq) {
5854
5700
  return null;
5855
5701
  }
5856
- const transcriptItems = getCDSInfo(feature, refData);
5857
- const { max, min } = feature;
5858
- let sequence = '';
5859
- if (showSequence) {
5860
- getSequenceAsString(min, max);
5861
- }
5862
- function getSequenceAsString(start, end) {
5863
- sequence = refSeq?.getSequence(start, end) ?? '';
5864
- if (sequence === '') {
5865
- void session.apolloDataStore.loadRefSeq([
5866
- { assemblyName: assembly, refName, start, end },
5867
- ]);
5868
- }
5869
- else {
5870
- sequence = formatSequence(sequence, refName, start, end);
5871
- }
5872
- getSequenceAsTextSegment(selectedOption); // For color coded sequence
5873
- return sequence;
5702
+ if (feature.type !== 'mRNA') {
5703
+ return null;
5874
5704
  }
5875
5705
  const handleSeqButtonClick = () => {
5876
5706
  setShowSequence(!showSequence);
5877
5707
  };
5878
- function getSequenceAsTextSegment(option) {
5879
- let seqData = '';
5880
- textSegments = [];
5881
- if (!refData) {
5882
- return;
5883
- }
5884
- switch (option) {
5885
- case 'CDS': {
5886
- textSegments.push({ text: `>${refName} : CDS\n`, color: 'black' });
5887
- for (const item of transcriptItems) {
5888
- if (item.type === 'CDS') {
5889
- const refSeq = refData.getSequence(Number(item.min + 1), Number(item.max));
5890
- seqData += item.strand === -1 && refSeq ? util.revcom(refSeq) : refSeq;
5891
- textSegments.push({ text: seqData, color: cdsColor });
5892
- }
5893
- }
5894
- break;
5895
- }
5896
- case 'cDNA': {
5897
- textSegments.push({ text: `>${refName} : cDNA\n`, color: 'black' });
5898
- for (const item of transcriptItems) {
5899
- if (item.type === 'CDS' ||
5900
- item.type === 'three_prime_UTR' ||
5901
- item.type === 'five_prime_UTR') {
5902
- const refSeq = refData.getSequence(Number(item.min + 1), Number(item.max));
5903
- seqData += item.strand === -1 && refSeq ? util.revcom(refSeq) : refSeq;
5904
- if (item.type === 'CDS') {
5905
- textSegments.push({ text: seqData, color: cdsColor });
5906
- }
5907
- else {
5908
- textSegments.push({ text: seqData, color: utrColor });
5909
- }
5910
- }
5911
- }
5912
- break;
5913
- }
5914
- case 'Full': {
5915
- textSegments.push({
5916
- text: `>${refName} : Full genomic\n`,
5917
- color: 'black',
5918
- });
5919
- let lastEnd = 0;
5920
- let count = 0;
5921
- for (const item of transcriptItems) {
5922
- count++;
5923
- if (lastEnd != 0 &&
5924
- lastEnd != Number(item.min) &&
5925
- count != transcriptItems.length) {
5926
- // Intron etc. between CDS/UTRs. No need to check this on very last item
5927
- const refSeq = refData.getSequence(lastEnd + 1, Number(item.min) - 1);
5928
- seqData += item.strand === -1 && refSeq ? util.revcom(refSeq) : refSeq;
5929
- textSegments.push({ text: seqData, color: 'black' });
5930
- }
5931
- if (item.type === 'CDS' ||
5932
- item.type === 'three_prime_UTR' ||
5933
- item.type === 'five_prime_UTR') {
5934
- const refSeq = refData.getSequence(Number(item.min + 1), Number(item.max));
5935
- seqData += item.strand === -1 && refSeq ? util.revcom(refSeq) : refSeq;
5936
- switch (item.type) {
5937
- case 'CDS': {
5938
- textSegments.push({ text: seqData, color: cdsColor });
5939
- break;
5940
- }
5941
- case 'three_prime_UTR': {
5942
- textSegments.push({ text: seqData, color: utrColor });
5943
- break;
5944
- }
5945
- case 'five_prime_UTR': {
5946
- textSegments.push({ text: seqData, color: utrColor });
5947
- break;
5948
- }
5949
- default: {
5950
- textSegments.push({ text: seqData, color: 'black' });
5951
- break;
5952
- }
5953
- }
5954
- }
5955
- lastEnd = Number(item.max);
5956
- }
5957
- break;
5958
- }
5959
- }
5960
- }
5961
5708
  function handleChangeSeqOption(e) {
5962
5709
  const option = e.target.value;
5963
5710
  setSelectedOption(option);
5964
- getSequenceAsTextSegment(option);
5965
5711
  }
5966
5712
  // Function to copy text to clipboard
5967
5713
  const copyToClipboard = () => {
5968
- const textToCopy = textSegments.map((segment) => segment.text).join('');
5969
- if (textToCopy) {
5970
- navigator.clipboard
5971
- .writeText(textToCopy)
5972
- .then(() => {
5973
- // console.log('Text copied to clipboard!')
5974
- })
5975
- .catch((error) => {
5976
- console.error('Failed to copy text to clipboard', error);
5977
- });
5714
+ const seqDiv = seqRef.current;
5715
+ if (!seqDiv) {
5716
+ return;
5978
5717
  }
5718
+ const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' });
5719
+ const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' });
5720
+ const clipboardItem = new ClipboardItem({
5721
+ [textBlob.type]: textBlob,
5722
+ [htmlBlob.type]: htmlBlob,
5723
+ });
5724
+ void navigator.clipboard.write([clipboardItem]);
5979
5725
  };
5980
- const ColoredText = ({ textSegments }) => {
5981
- return (React__default["default"].createElement("div", null, textSegments.map((segment, index) => (React__default["default"].createElement("span", { key: index, style: { color: segment.color } }, shared.splitStringIntoChunks(segment.text, 150).join('\n'))))));
5982
- };
5726
+ const sequenceSegments = showSequence
5727
+ ? getSequenceSegments(selectedOption, feature, (min, max) => refData.getSequence(min, max))
5728
+ : [];
5729
+ const locationIntervals = [];
5730
+ if (showSequence) {
5731
+ const allLocs = sequenceSegments.flatMap((segment) => segment.locs);
5732
+ let [previous] = allLocs;
5733
+ for (let i = 1; i < allLocs.length; i++) {
5734
+ if (previous.min === allLocs[i].max || previous.max === allLocs[i].min) {
5735
+ previous = {
5736
+ min: Math.min(previous.min, allLocs[i].min),
5737
+ max: Math.max(previous.max, allLocs[i].max),
5738
+ };
5739
+ }
5740
+ else {
5741
+ locationIntervals.push(previous);
5742
+ previous = allLocs[i];
5743
+ }
5744
+ }
5745
+ locationIntervals.push(previous);
5746
+ }
5983
5747
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
5984
- React__default["default"].createElement(material.Typography, { style: { display: 'inline', marginLeft: '15px' }, variant: "h5" }, "Sequence"),
5748
+ React__default["default"].createElement(material.Typography, { variant: "h5" }, "Sequence"),
5985
5749
  React__default["default"].createElement("div", null,
5986
- React__default["default"].createElement(material.Button, { variant: "contained", style: { marginLeft: '15px' }, onClick: handleSeqButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence')),
5987
- React__default["default"].createElement("div", null, showSequence && (React__default["default"].createElement(material.Select, { value: selectedOption, onChange: handleChangeSeqOption, style: { width: '150px', marginLeft: '15px', height: '25px' } },
5988
- React__default["default"].createElement(material.MenuItem, { value: 'Select' }, "Select"),
5989
- React__default["default"].createElement(material.MenuItem, { value: 'CDS' }, "CDS"),
5990
- React__default["default"].createElement(material.MenuItem, { value: 'cDNA' }, "cDNA"),
5991
- React__default["default"].createElement(material.MenuItem, { value: 'Full' }, "Full genomics")))),
5992
- React__default["default"].createElement("div", { style: {
5993
- width: '500px',
5994
- marginLeft: '15px',
5995
- height: '300px',
5996
- overflowY: 'auto',
5997
- border: '1px solid #ccc',
5998
- } }, showSequence && React__default["default"].createElement(ColoredText, { textSegments: textSegments })),
5999
- showSequence && (React__default["default"].createElement(material.Button, { variant: "contained", style: { marginLeft: '15px' }, onClick: copyToClipboard }, "Copy sequence"))));
5750
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: handleSeqButtonClick }, showSequence ? 'Hide sequence' : 'Show sequence')),
5751
+ showSequence && (React__default["default"].createElement(React__default["default"].Fragment, null,
5752
+ React__default["default"].createElement(material.Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
5753
+ React__default["default"].createElement(material.MenuItem, { value: "CDS" }, "CDS"),
5754
+ React__default["default"].createElement(material.MenuItem, { value: "cDNA" }, "cDNA"),
5755
+ React__default["default"].createElement(material.MenuItem, { value: "genomic" }, "Genomic")),
5756
+ React__default["default"].createElement(material.Paper, { style: {
5757
+ fontFamily: 'monospace',
5758
+ padding: theme.spacing(),
5759
+ overflowX: 'auto',
5760
+ }, ref: seqRef },
5761
+ ">",
5762
+ refSeq.name,
5763
+ ":",
5764
+ locationIntervals
5765
+ .map((interval) => feature.strand === 1
5766
+ ? `${interval.min + 1}-${interval.max}`
5767
+ : `${interval.max}-${interval.min + 1}`)
5768
+ .join(';'),
5769
+ "(",
5770
+ feature.strand === 1 ? '+' : '-',
5771
+ ")",
5772
+ React__default["default"].createElement("br", null),
5773
+ sequenceSegments.map((segment, index) => (React__default["default"].createElement("span", { key: `${segment.type}-${index}`, style: {
5774
+ background: getSegmentColor(segment.type),
5775
+ color: theme.palette.getContrastText(getSegmentColor(segment.type)),
5776
+ } }, segment.sequenceLines.map((sequenceLine, idx) => (React__default["default"].createElement(React__default["default"].Fragment, { key: `${sequenceLine.slice(0, 5)}-${idx}` },
5777
+ sequenceLine,
5778
+ idx === segment.sequenceLines.length - 1 &&
5779
+ sequenceLine.length !== SEQUENCE_WRAP_LENGTH ? null : (React__default["default"].createElement("br", null))))))))),
5780
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: copyToClipboard }, "Copy sequence")))));
6000
5781
  });
6001
5782
 
6002
5783
  const useStyles$7 = mui.makeStyles()((theme) => ({
@@ -6227,7 +6008,7 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
6227
6008
  return menuItems;
6228
6009
  }
6229
6010
 
6230
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
6011
+ /* eslint-disable @typescript-eslint/unbound-method */
6231
6012
  const useStyles$5 = mui.makeStyles()((theme) => ({
6232
6013
  inputWrapper: {
6233
6014
  position: 'relative',
@@ -6456,7 +6237,9 @@ function baseModelFactory(_pluginManager, configSchema) {
6456
6237
  return;
6457
6238
  }
6458
6239
  void self.session.apolloDataStore.loadFeatures(self.regions);
6459
- void self.session.apolloDataStore.loadRefSeq(self.regions);
6240
+ if (self.lgv.bpPerPx <= 3) {
6241
+ void self.session.apolloDataStore.loadRefSeq(self.regions);
6242
+ }
6460
6243
  }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
6461
6244
  },
6462
6245
  }));
@@ -7341,7 +7124,7 @@ function drawTooltip$2(display, context) {
7341
7124
  if (!position) {
7342
7125
  return;
7343
7126
  }
7344
- const { layoutIndex, layoutRow } = position;
7127
+ const { featureRow, layoutIndex, layoutRow } = position;
7345
7128
  const { bpPerPx, displayedRegions, offsetPx } = lgv;
7346
7129
  const displayedRegion = displayedRegions[layoutIndex];
7347
7130
  const { refName, reversed } = displayedRegion;
@@ -7353,7 +7136,7 @@ function drawTooltip$2(display, context) {
7353
7136
  coord: reversed ? max : min,
7354
7137
  regionNumber: layoutIndex,
7355
7138
  })?.offsetPx ?? 0) - offsetPx;
7356
- const top = layoutRow * apolloRowHeight;
7139
+ const top = (layoutRow + featureRow) * apolloRowHeight;
7357
7140
  const widthPx = length / bpPerPx;
7358
7141
  const featureType = `Type: ${feature.type}`;
7359
7142
  const { attributes } = feature;
@@ -7499,6 +7282,20 @@ function getContextMenuItems$2(display) {
7499
7282
  session.showWidget(apolloFeatureWidget);
7500
7283
  },
7501
7284
  });
7285
+ if (sourceFeature.type === 'mRNA' && util.isSessionModelWithWidgets(session)) {
7286
+ menuItems.push({
7287
+ label: 'Edit transcript details',
7288
+ onClick: () => {
7289
+ const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
7290
+ feature: sourceFeature,
7291
+ assembly: currentAssemblyId,
7292
+ changeManager,
7293
+ refName: region.refName,
7294
+ });
7295
+ session.showWidget(apolloTranscriptWidget);
7296
+ },
7297
+ });
7298
+ }
7502
7299
  return menuItems;
7503
7300
  }
7504
7301
  function getFeatureFromLayout$2(feature, _bp, _row) {
@@ -7583,35 +7380,49 @@ const boxGlyph = {
7583
7380
  onMouseUp: onMouseUp$2,
7584
7381
  };
7585
7382
 
7586
- let forwardFill = null;
7587
- let backwardFill = null;
7588
- if ('document' in window) {
7383
+ let forwardFillLight = null;
7384
+ let backwardFillLight = null;
7385
+ let forwardFillDark = null;
7386
+ let backwardFillDark = null;
7387
+ if ('document' in globalThis) {
7589
7388
  for (const direction of ['forward', 'backward']) {
7590
- const canvas = document.createElement('canvas');
7591
- const canvasSize = 10;
7592
- canvas.width = canvas.height = canvasSize;
7593
- const ctx = canvas.getContext('2d');
7594
- if (ctx) {
7595
- const stripeColor1 = 'rgba(0,0,0,0)';
7596
- const stripeColor2 = 'rgba(255,255,255,0.25)';
7597
- const gradient = direction === 'forward'
7598
- ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
7599
- : ctx.createLinearGradient(0, 0, canvasSize, canvasSize);
7600
- gradient.addColorStop(0, stripeColor1);
7601
- gradient.addColorStop(0.25, stripeColor1);
7602
- gradient.addColorStop(0.25, stripeColor2);
7603
- gradient.addColorStop(0.5, stripeColor2);
7604
- gradient.addColorStop(0.5, stripeColor1);
7605
- gradient.addColorStop(0.75, stripeColor1);
7606
- gradient.addColorStop(0.75, stripeColor2);
7607
- gradient.addColorStop(1, stripeColor2);
7608
- ctx.fillStyle = gradient;
7609
- ctx.fillRect(0, 0, 10, 10);
7610
- if (direction === 'forward') {
7611
- forwardFill = ctx.createPattern(canvas, 'repeat');
7612
- }
7613
- else {
7614
- backwardFill = ctx.createPattern(canvas, 'repeat');
7389
+ for (const themeMode of ['light', 'dark']) {
7390
+ const canvas = document.createElement('canvas');
7391
+ const canvasSize = 10;
7392
+ canvas.width = canvas.height = canvasSize;
7393
+ const ctx = canvas.getContext('2d');
7394
+ if (ctx) {
7395
+ const stripeColor1 = themeMode === 'light' ? 'rgba(0,0,0,0)' : 'rgba(0,0,0,0.75)';
7396
+ const stripeColor2 = themeMode === 'light' ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.50)';
7397
+ const gradient = direction === 'forward'
7398
+ ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
7399
+ : ctx.createLinearGradient(0, 0, canvasSize, canvasSize);
7400
+ gradient.addColorStop(0, stripeColor1);
7401
+ gradient.addColorStop(0.25, stripeColor1);
7402
+ gradient.addColorStop(0.25, stripeColor2);
7403
+ gradient.addColorStop(0.5, stripeColor2);
7404
+ gradient.addColorStop(0.5, stripeColor1);
7405
+ gradient.addColorStop(0.75, stripeColor1);
7406
+ gradient.addColorStop(0.75, stripeColor2);
7407
+ gradient.addColorStop(1, stripeColor2);
7408
+ ctx.fillStyle = gradient;
7409
+ ctx.fillRect(0, 0, 10, 10);
7410
+ if (direction === 'forward') {
7411
+ if (themeMode === 'light') {
7412
+ forwardFillLight = ctx.createPattern(canvas, 'repeat');
7413
+ }
7414
+ else {
7415
+ forwardFillDark = ctx.createPattern(canvas, 'repeat');
7416
+ }
7417
+ }
7418
+ else {
7419
+ if (themeMode === 'light') {
7420
+ backwardFillLight = ctx.createPattern(canvas, 'repeat');
7421
+ }
7422
+ else {
7423
+ backwardFillDark = ctx.createPattern(canvas, 'repeat');
7424
+ }
7425
+ }
7615
7426
  }
7616
7427
  }
7617
7428
  }
@@ -7624,12 +7435,25 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7624
7435
  const rowHeight = apolloRowHeight;
7625
7436
  const exonHeight = Math.round(0.6 * rowHeight);
7626
7437
  const cdsHeight = Math.round(0.9 * rowHeight);
7627
- const { strand } = feature;
7628
- const { children } = feature;
7438
+ const { children, min, strand } = feature;
7629
7439
  if (!children) {
7630
7440
  return;
7631
7441
  }
7632
7442
  const { apolloSelectedFeature } = session;
7443
+ // Draw background for gene
7444
+ const topLevelFeatureMinX = (lgv.bpToPx({
7445
+ refName,
7446
+ coord: min,
7447
+ regionNumber: displayedRegionIndex,
7448
+ })?.offsetPx ?? 0) - offsetPx;
7449
+ const topLevelFeatureWidthPx = feature.length / bpPerPx;
7450
+ const topLevelFeatureStartPx = reversed
7451
+ ? topLevelFeatureMinX - topLevelFeatureWidthPx
7452
+ : topLevelFeatureMinX;
7453
+ const topLevelFeatureTop = row * rowHeight;
7454
+ const topLevelFeatureHeight = getRowCount$1(feature) * rowHeight;
7455
+ ctx.fillStyle = material.alpha(theme?.palette.background.paper ?? '#ffffff', 0.6);
7456
+ ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
7633
7457
  // Draw lines on different rows for each mRNA
7634
7458
  let currentRow = 0;
7635
7459
  for (const [, mrna] of children) {
@@ -7661,6 +7485,8 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7661
7485
  currentRow += 1;
7662
7486
  }
7663
7487
  }
7488
+ const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
7489
+ const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
7664
7490
  // Draw exon and CDS for each mRNA
7665
7491
  currentRow = 0;
7666
7492
  for (const [, child] of children) {
@@ -7797,11 +7623,31 @@ function drawHover$1(stateModel, ctx) {
7797
7623
  ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature));
7798
7624
  }
7799
7625
  function getFeatureFromLayout$1(feature, bp, row) {
7800
- const featureInThisRow = featuresForRow$1(feature)[row];
7626
+ const featureInThisRow = featuresForRow$1(feature)[row] || [];
7801
7627
  for (const f of featureInThisRow) {
7628
+ let featureObj;
7802
7629
  if (bp >= f.min && bp <= f.max && f.parent) {
7803
- return f;
7630
+ featureObj = f;
7631
+ }
7632
+ if (!featureObj) {
7633
+ continue;
7634
+ }
7635
+ if (featureObj.type === 'CDS' &&
7636
+ featureObj.parent &&
7637
+ featureObj.parent.type === 'mRNA') {
7638
+ const { cdsLocations } = featureObj.parent;
7639
+ for (const cdsLoc of cdsLocations) {
7640
+ for (const loc of cdsLoc) {
7641
+ if (bp >= loc.min && bp <= loc.max) {
7642
+ return featureObj;
7643
+ }
7644
+ }
7645
+ }
7646
+ // If mouse position is in the intron region, return the mRNA
7647
+ return featureObj.parent;
7804
7648
  }
7649
+ // If mouse position is in a feature that is not a CDS, return the feature
7650
+ return featureObj;
7805
7651
  }
7806
7652
  return feature;
7807
7653
  }
@@ -8264,6 +8110,7 @@ async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
8264
8110
  return;
8265
8111
  }
8266
8112
 
8113
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
8267
8114
  const useStyles$3 = mui.makeStyles()((theme) => ({
8268
8115
  scrollableTable: {
8269
8116
  width: '100%',
@@ -8436,7 +8283,7 @@ function stateModelFactory$1(pluginManager, configSchema) {
8436
8283
  .named('LinearApolloDisplay');
8437
8284
  }
8438
8285
 
8439
- /* eslint-disable @typescript-eslint/unbound-method */
8286
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
8440
8287
  const useStyles$1 = mui.makeStyles()((theme) => ({
8441
8288
  canvasContainer: {
8442
8289
  position: 'relative',
@@ -8603,6 +8450,11 @@ const useStyles = mui.makeStyles()((theme) => ({
8603
8450
  // position: 'relative',
8604
8451
  userSelect: 'none',
8605
8452
  },
8453
+ alertContainer: {
8454
+ display: 'flex',
8455
+ alignItems: 'center',
8456
+ justifyContent: 'center',
8457
+ },
8606
8458
  }));
8607
8459
  function scrollSelectedFeatureIntoView(model, scrollContainerRef) {
8608
8460
  const { apolloRowHeight, selectedFeature } = model;
@@ -8625,18 +8477,18 @@ const ResizeHandle = ({ onResize, }) => {
8625
8477
  const cancelDrag = React.useCallback((event) => {
8626
8478
  event.stopPropagation();
8627
8479
  event.preventDefault();
8628
- window.removeEventListener('mousemove', mouseMove);
8629
- window.removeEventListener('mouseup', cancelDrag);
8630
- window.removeEventListener('mouseleave', cancelDrag);
8480
+ globalThis.removeEventListener('mousemove', mouseMove);
8481
+ globalThis.removeEventListener('mouseup', cancelDrag);
8482
+ globalThis.removeEventListener('mouseleave', cancelDrag);
8631
8483
  }, [mouseMove]);
8632
8484
  return (
8633
8485
  // TODO: a11y
8634
8486
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8635
8487
  React__default["default"].createElement("div", { onMouseDown: (event) => {
8636
8488
  event.stopPropagation();
8637
- window.addEventListener('mousemove', mouseMove);
8638
- window.addEventListener('mouseup', cancelDrag);
8639
- window.addEventListener('mouseleave', cancelDrag);
8489
+ globalThis.addEventListener('mousemove', mouseMove);
8490
+ globalThis.addEventListener('mouseup', cancelDrag);
8491
+ globalThis.addEventListener('mouseleave', cancelDrag);
8640
8492
  }, onClick: (e) => {
8641
8493
  e.stopPropagation();
8642
8494
  e.preventDefault();
@@ -8651,6 +8503,10 @@ const AccordionControl = mobxReact.observer(function AccordionControl({ onClick,
8651
8503
  title ? (React__default["default"].createElement(material.Typography, { className: classes.title, variant: "caption", component: "span" }, title)) : null)));
8652
8504
  });
8653
8505
  const DisplayComponent = mobxReact.observer(function DisplayComponent({ model, ...other }) {
8506
+ const session = util.getSession(model);
8507
+ const { ontologyManager } = session.apolloDataStore;
8508
+ const { featureTypeOntology } = ontologyManager;
8509
+ const ontologyStore = featureTypeOntology?.dataStore;
8654
8510
  const { classes } = useStyles();
8655
8511
  const { detailsHeight, graphical, height: overallHeight, isShown, selectedFeature, table, tabularEditor, toggleShown, } = model;
8656
8512
  const canvasScrollContainerRef = React.useRef(null);
@@ -8660,6 +8516,10 @@ const DisplayComponent = mobxReact.observer(function DisplayComponent({ model, .
8660
8516
  const onDetailsResize = (delta) => {
8661
8517
  model.setDetailsHeight(detailsHeight - delta);
8662
8518
  };
8519
+ if (!ontologyStore) {
8520
+ return (React__default["default"].createElement("div", { className: classes.alertContainer },
8521
+ React__default["default"].createElement(material.Alert, { severity: "error" }, "Could not load feature type ontology.")));
8522
+ }
8663
8523
  if (graphical && table) {
8664
8524
  const tabularHeight = tabularEditor.isShown ? detailsHeight : 0;
8665
8525
  const featureAreaHeight = isShown
@@ -9229,7 +9089,7 @@ class DesktopFileDriver extends BackendDriver {
9229
9089
  throw new Error(`Assembly ${assemblyName} not found`);
9230
9090
  }
9231
9091
  const { file } = configuration.getConf(assembly, ['sequence', 'metadata']);
9232
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9092
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9233
9093
  const fs = require('node:fs');
9234
9094
  const fileContents = await fs.promises.readFile(file, 'utf8');
9235
9095
  return loadAssemblyIntoClient(assemblyName, fileContents, this.clientStore);
@@ -9339,7 +9199,7 @@ class DesktopFileDriver extends BackendDriver {
9339
9199
  });
9340
9200
  }
9341
9201
  const gff3Contents = gff__default["default"].formatSync(gff3Items);
9342
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9202
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9343
9203
  const fs = require('node:fs');
9344
9204
  await fs.promises.writeFile(file, gff3Contents, 'utf8');
9345
9205
  const results = new shared.ValidationResultSet();
@@ -9356,6 +9216,7 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9356
9216
  typeName: mobxStateTree.types.optional(mobxStateTree.types.literal('Client'), 'Client'),
9357
9217
  assemblies: mobxStateTree.types.map(mst$1.ApolloAssembly),
9358
9218
  checkResults: mobxStateTree.types.map(mst$1.CheckResult),
9219
+ ontologyManager: mobxStateTree.types.optional(OntologyManagerType, {}),
9359
9220
  })
9360
9221
  .views((self) => ({
9361
9222
  get internetAccounts() {
@@ -9429,7 +9290,6 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9429
9290
  desktopFileDriver: util.isElectron
9430
9291
  ? new DesktopFileDriver(self)
9431
9292
  : undefined,
9432
- ontologyManager: OntologyManagerType.create(),
9433
9293
  }))
9434
9294
  .actions((self) => ({
9435
9295
  afterCreate() {
@@ -9882,6 +9742,7 @@ function extendSession(pluginManager, sessionModel) {
9882
9742
  postProcessor(snap) {
9883
9743
  snap.apolloSelectedFeature = undefined;
9884
9744
  const assemblies = Object.fromEntries(Object.entries(snap.apolloDataStore.assemblies).filter(([, assembly]) => assembly.backendDriverType === 'InMemoryFileDriver'));
9745
+ // @ts-expect-error ontologyManager isn't actually required
9885
9746
  snap.apolloDataStore = {
9886
9747
  typeName: 'Client',
9887
9748
  assemblies,
@@ -10316,7 +10177,7 @@ class RefNameAliasAdapter extends BaseAdapter.BaseAdapter {
10316
10177
  this.refNameAliases = refNameAliases;
10317
10178
  return refNameAliases;
10318
10179
  }
10319
- async freeResources() {
10180
+ freeResources() {
10320
10181
  // no resources to free
10321
10182
  }
10322
10183
  }