@apollo-annotation/jbrowse-plugin-apollo 0.3.1 → 0.3.2

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 (47) hide show
  1. package/dist/index.esm.js +2072 -1496
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2069 -1493
  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 +2256 -1533
  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 +13 -11
  12. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +7 -10
  13. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +3 -0
  14. package/src/FeatureDetailsWidget/Attributes.tsx +27 -27
  15. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +65 -0
  16. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +6 -1
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +25 -2
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +0 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +8 -1
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +88 -40
  21. package/src/LinearApolloDisplay/glyphs/Glyph.ts +8 -1
  22. package/src/LinearApolloDisplay/stateModel/base.ts +28 -2
  23. package/src/LinearApolloDisplay/stateModel/layouts.ts +65 -11
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +25 -6
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +1 -2
  26. package/src/OntologyManager/OntologyStore/index.ts +6 -2
  27. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +41 -13
  28. package/src/OntologyManager/index.ts +35 -0
  29. package/src/SixFrameFeatureDisplay/stateModel.ts +11 -2
  30. package/src/TabularEditor/HybridGrid/Feature.tsx +1 -2
  31. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +0 -1
  32. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +8 -1
  33. package/src/components/AddRefSeqAliases.tsx +7 -8
  34. package/src/components/CopyFeature.tsx +1 -1
  35. package/src/components/CreateApolloAnnotation.tsx +304 -0
  36. package/src/components/DownloadGFF3.tsx +5 -1
  37. package/src/components/FilterFeatures.tsx +120 -0
  38. package/src/components/ModifyFeatureAttribute.tsx +27 -27
  39. package/src/components/OntologyTermMultiSelect.tsx +5 -5
  40. package/src/extensions/annotationFromJBrowseFeature.test.ts +119 -0
  41. package/src/extensions/annotationFromJBrowseFeature.ts +171 -0
  42. package/src/extensions/annotationFromPileup.ts +1 -1
  43. package/src/extensions/index.ts +1 -0
  44. package/src/index.ts +8 -2
  45. package/src/session/ClientDataStore.ts +29 -0
  46. package/src/session/session.ts +2 -5
  47. package/src/LinearApolloDisplay/stateModel/getGlyph.ts +0 -40
@@ -45,9 +45,9 @@ var rxjs = require('@jbrowse/core/util/rxjs');
45
45
  var SimpleFeature = require('@jbrowse/core/util/simpleFeature');
46
46
  var BaseResult = require('@jbrowse/core/TextSearch/BaseResults');
47
47
  var mst$1 = require('@apollo-annotation/mst');
48
- var tracks = require('@jbrowse/core/util/tracks');
49
48
  var ClearIcon = require('@mui/icons-material/Clear');
50
49
  var UnfoldLessIcon = require('@mui/icons-material/UnfoldLess');
50
+ var tracks = require('@jbrowse/core/util/tracks');
51
51
  var ExpandLessIcon = require('@mui/icons-material/ExpandLess');
52
52
  var ExpandMoreIcon = require('@mui/icons-material/ExpandMore');
53
53
  var ErrorIcon = require('@mui/icons-material/Error');
@@ -101,7 +101,7 @@ var ExpandMoreIcon__default = /*#__PURE__*/_interopDefaultLegacy(ExpandMoreIcon)
101
101
  var ErrorIcon__default = /*#__PURE__*/_interopDefaultLegacy(ErrorIcon);
102
102
  var SaveIcon__default = /*#__PURE__*/_interopDefaultLegacy(SaveIcon);
103
103
 
104
- var version = "0.3.1";
104
+ var version = "0.3.2";
105
105
 
106
106
  const ApolloConfigSchema = configuration.ConfigurationSchema('ApolloInternetAccount', {
107
107
  baseURL: {
@@ -881,6 +881,8 @@ function serializeWords(foundWords) {
881
881
  /** load a OBO Graph JSON file into a database */
882
882
  async function loadOboGraphJson(db) {
883
883
  const startTime = Date.now();
884
+ let percentProgress = 1;
885
+ this.options.update?.('Parsing JSON', percentProgress);
884
886
  // TODO: using file streaming along with an event-based json parser
885
887
  // instead of JSON.parse and .readFile could probably make this faster
886
888
  // and less memory intensive
@@ -891,6 +893,8 @@ async function loadOboGraphJson(db) {
891
893
  catch {
892
894
  throw new Error('Error in loading ontology');
893
895
  }
896
+ percentProgress += 5;
897
+ this.options.update?.('Parsing JSON complete', percentProgress);
894
898
  const parseTime = Date.now();
895
899
  const [graph, ...additionalGraphs] = oboGraph.graphs ?? [];
896
900
  if (!graph) {
@@ -909,31 +913,51 @@ async function loadOboGraphJson(db) {
909
913
  const fullTextIndexPaths = getTextIndexFields
910
914
  .call(this)
911
915
  .map((def) => def.jsonPath);
912
- for (const node of graph.nodes ?? []) {
913
- if (isOntologyDBNode(node)) {
914
- await nodeStore.add({
915
- ...node,
916
- fullTextWords: serializeWords(getWords(node, fullTextIndexPaths, this.prefixes)),
917
- });
916
+ if (graph.nodes) {
917
+ let lastProgress = Math.round(percentProgress);
918
+ for (const [, node] of graph.nodes.entries()) {
919
+ percentProgress += 64 * (1 / graph.nodes.length);
920
+ if (Math.round(percentProgress) != lastProgress &&
921
+ percentProgress < 100) {
922
+ this.options.update?.('Processing nodes', percentProgress);
923
+ lastProgress = Math.round(percentProgress);
924
+ }
925
+ if (isOntologyDBNode(node)) {
926
+ await nodeStore.add({
927
+ ...node,
928
+ fullTextWords: serializeWords(getWords(node, fullTextIndexPaths, this.prefixes)),
929
+ });
930
+ }
918
931
  }
919
932
  }
920
933
  // load edges
921
934
  const edgeStore = tx.objectStore('edges');
922
- for (const edge of graph.edges ?? []) {
923
- if (isOntologyDBEdge(edge)) {
924
- await edgeStore.add(edge);
935
+ if (graph.edges) {
936
+ let lastProgress = Math.round(percentProgress);
937
+ for (const [, edge] of graph.edges.entries()) {
938
+ percentProgress += 30 * (1 / graph.edges.length);
939
+ if (Math.round(percentProgress) != lastProgress &&
940
+ percentProgress < 100) {
941
+ this.options.update?.('Processing edges', percentProgress);
942
+ lastProgress = Math.round(percentProgress);
943
+ }
944
+ if (isOntologyDBEdge(edge)) {
945
+ await edgeStore.add(edge);
946
+ }
925
947
  }
926
948
  }
927
949
  await tx.done;
928
950
  // record some metadata about this ontology and load operation
929
951
  const tx2 = db.transaction('meta', 'readwrite');
952
+ // eslint-disable-next-line @typescript-eslint/unbound-method
953
+ const { update, ...otherOptions } = this.options;
930
954
  await tx2.objectStore('meta').add({
931
955
  ontologyRecord: {
932
956
  name: this.ontologyName,
933
957
  version: this.ontologyVersion,
934
958
  sourceLocation: this.sourceLocation,
935
959
  },
936
- storeOptions: this.options,
960
+ storeOptions: otherOptions,
937
961
  graphMeta: graph.meta,
938
962
  timestamp: String(new Date()),
939
963
  schemaVersion,
@@ -998,8 +1022,8 @@ class OntologyStore {
998
1022
  this.ontologyName = name;
999
1023
  this.ontologyVersion = version;
1000
1024
  this.sourceLocation = source;
1001
- this.db = this.prepareDatabase();
1002
1025
  this.options = options ?? {};
1026
+ this.db = this.prepareDatabase();
1003
1027
  }
1004
1028
  /**
1005
1029
  * check that the configuration of this ontology appears valid. Does not
@@ -1044,9 +1068,12 @@ class OntologyStore {
1044
1068
  return db;
1045
1069
  }
1046
1070
  try {
1047
- const { sourceLocation, sourceType } = this;
1071
+ const { options, sourceLocation, sourceType } = this;
1048
1072
  if (sourceType === 'obo-graph-json') {
1073
+ options.update?.('', 0);
1074
+ // add more updates inside `loadOboGraphJson`
1049
1075
  await this.loadOboGraphJson(db);
1076
+ options.update?.('', 100);
1050
1077
  }
1051
1078
  else {
1052
1079
  throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
@@ -1266,6 +1293,7 @@ const OntologyRecordType = mobxStateTree.types
1266
1293
  version: 'unversioned',
1267
1294
  source: mobxStateTree.types.union(mst.LocalPathLocation, mst.UriLocation, mst.BlobLocation),
1268
1295
  options: mobxStateTree.types.frozen(),
1296
+ equivalentTypes: mobxStateTree.types.map(mobxStateTree.types.array(mobxStateTree.types.string)),
1269
1297
  })
1270
1298
  .volatile((_self) => ({
1271
1299
  dataStore: undefined,
@@ -1283,6 +1311,37 @@ const OntologyRecordType = mobxStateTree.types
1283
1311
  this.initDataStore();
1284
1312
  }));
1285
1313
  },
1314
+ setEquivalentTypes(type, equivalentTypes) {
1315
+ self.equivalentTypes.set(type, equivalentTypes);
1316
+ },
1317
+ }))
1318
+ .actions((self) => ({
1319
+ loadEquivalentTypes: mobxStateTree.flow(function* loadEquivalentTypes(type) {
1320
+ if (!self.dataStore) {
1321
+ return;
1322
+ }
1323
+ const terms = (yield self.dataStore.getTermsWithLabelOrSynonym(type));
1324
+ const equivalents = terms
1325
+ .map((term) => term.lbl)
1326
+ .filter((term) => term != undefined);
1327
+ self.setEquivalentTypes(type, equivalents);
1328
+ }),
1329
+ }))
1330
+ .views((self) => ({
1331
+ isTypeOf(queryType, typeOf) {
1332
+ if (queryType === typeOf) {
1333
+ return true;
1334
+ }
1335
+ if (!self.dataStore) {
1336
+ return false;
1337
+ }
1338
+ const equivalents = self.equivalentTypes.get(typeOf);
1339
+ if (!equivalents) {
1340
+ void self.loadEquivalentTypes(typeOf);
1341
+ return false;
1342
+ }
1343
+ return equivalents.includes(queryType);
1344
+ },
1286
1345
  }));
1287
1346
  const OntologyManagerType = mobxStateTree.types
1288
1347
  .model('OntologyManager', {
@@ -1736,7 +1795,7 @@ function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, so
1736
1795
  }
1737
1796
  const newRefNames = [...Object.entries(refNameAliases)]
1738
1797
  .filter(([id, refName]) => id !== refName)
1739
- .map(([id, refName]) => ({ _id: id, name: refName ?? '' }));
1798
+ .map(([id, refName]) => ({ _id: id, name: refName }));
1740
1799
  setRefNames(newRefNames);
1741
1800
  setSelectedRefSeqId(newRefNames[0]?._id || '');
1742
1801
  }
@@ -2021,7 +2080,11 @@ function DownloadGFF3({ handleClose, session }) {
2021
2080
  }
2022
2081
  const { exportID } = (await response.json());
2023
2082
  const exportURL = new URL('export', internetAccount.baseURL);
2024
- const exportSearchParams = new URLSearchParams({ exportID });
2083
+ const params = {
2084
+ exportID,
2085
+ includeFASTA: 'true',
2086
+ };
2087
+ const exportSearchParams = new URLSearchParams(params);
2025
2088
  exportURL.search = exportSearchParams.toString();
2026
2089
  const exportUri = exportURL.toString();
2027
2090
  window.open(exportUri, '_blank');
@@ -2712,8 +2775,8 @@ function Option(props) {
2712
2775
  // .map((m) => m.score)
2713
2776
  // .join(', ')
2714
2777
  return (React__namespace.createElement("li", { ...other },
2715
- React__namespace.createElement(material.Grid, { container: true },
2716
- React__namespace.createElement(material.Grid, { item: true },
2778
+ React__namespace.createElement(material.Grid2, { container: true },
2779
+ React__namespace.createElement(material.Grid2, null,
2717
2780
  React__namespace.createElement(material.Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
2718
2781
  ' ',
2719
2782
  React__namespace.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
@@ -2914,43 +2977,43 @@ function ModifyFeatureAttribute({ changeManager, handleClose, session, sourceAss
2914
2977
  return (React__default["default"].createElement(Dialog, { open: true, title: "Feature attributes", handleClose: handleClose, maxWidth: false, "data-testid": "modify-feature-attribute" },
2915
2978
  React__default["default"].createElement("form", { onSubmit: onSubmit },
2916
2979
  React__default["default"].createElement(material.DialogContent, null,
2917
- React__default["default"].createElement(material.Grid, { container: true, direction: "column", spacing: 1 },
2980
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column", spacing: 1 },
2918
2981
  Object.entries(attributes).map(([key, value]) => {
2919
2982
  const EditorComponent = reservedKeys$1.get(key) ?? CustomAttributeValueEditor$1;
2920
- return (React__default["default"].createElement(material.Grid, { container: true, item: true, spacing: 3, alignItems: "center", key: key },
2921
- React__default["default"].createElement(material.Grid, { item: true, xs: "auto" },
2983
+ return (React__default["default"].createElement(material.Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
2984
+ React__default["default"].createElement(material.Grid2, null,
2922
2985
  React__default["default"].createElement(material.Paper, { variant: "outlined", className: classes.attributeName },
2923
2986
  React__default["default"].createElement(material.Typography, null, key))),
2924
- React__default["default"].createElement(material.Grid, { item: true, flexGrow: 1 },
2987
+ React__default["default"].createElement(material.Grid2, { flexGrow: 1 },
2925
2988
  React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: makeOnChange(key) })),
2926
- React__default["default"].createElement(material.Grid, { item: true, xs: 1 },
2989
+ React__default["default"].createElement(material.Grid2, null,
2927
2990
  React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
2928
2991
  deleteAttribute(key);
2929
2992
  } },
2930
2993
  React__default["default"].createElement(DeleteIcon__default["default"], { fontSize: "medium", key: key })))));
2931
2994
  }),
2932
- React__default["default"].createElement(material.Grid, { item: true },
2995
+ React__default["default"].createElement(material.Grid2, null,
2933
2996
  React__default["default"].createElement(material.Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
2934
2997
  setShowAddNewForm(true);
2935
2998
  } }, "Add new")),
2936
- showAddNewForm ? (React__default["default"].createElement(material.Grid, { item: true },
2999
+ showAddNewForm ? (React__default["default"].createElement(material.Grid2, null,
2937
3000
  React__default["default"].createElement(material.Paper, { elevation: 8, className: classes.newAttributePaper },
2938
- React__default["default"].createElement(material.Grid, { container: true, direction: "column" },
2939
- React__default["default"].createElement(material.Grid, { item: true },
3001
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column" },
3002
+ React__default["default"].createElement(material.Grid2, null,
2940
3003
  React__default["default"].createElement(material.FormControl, null,
2941
3004
  React__default["default"].createElement(material.FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
2942
3005
  React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
2943
- React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid, { container: true, spacing: 1, alignItems: "center" },
2944
- React__default["default"].createElement(material.Grid, { item: true },
3006
+ React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid2, { container: true, spacing: 1, alignItems: "center" },
3007
+ React__default["default"].createElement(material.Grid2, null,
2945
3008
  React__default["default"].createElement(material.Typography, null, "Custom")),
2946
- React__default["default"].createElement(material.Grid, { item: true },
3009
+ React__default["default"].createElement(material.Grid2, null,
2947
3010
  React__default["default"].createElement(material.TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys$1.has(newAttributeKey)
2948
3011
  ? ''
2949
3012
  : newAttributeKey, disabled: reservedKeys$1.has(newAttributeKey), onChange: (event) => {
2950
3013
  setNewAttributeKey(event.target.value);
2951
3014
  } }))) }),
2952
3015
  [...reservedKeys$1.keys()].map((key) => (React__default["default"].createElement(material.FormControlLabel, { key: key, value: key, control: React__default["default"].createElement(material.Radio, null), label: key })))))),
2953
- React__default["default"].createElement(material.Grid, { item: true },
3016
+ React__default["default"].createElement(material.Grid2, null,
2954
3017
  React__default["default"].createElement(material.DialogActions, null,
2955
3018
  React__default["default"].createElement(material.Button, { key: "addButton", color: "primary", variant: "contained", style: { margin: 2 }, onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
2956
3019
  React__default["default"].createElement(material.Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
@@ -3321,13 +3384,12 @@ function AddRefSeqAliases({ changeManager, handleClose, session, }) {
3321
3384
  };
3322
3385
  return (React__default["default"].createElement(Dialog, { open: true, title: "Add reference sequence aliases", handleClose: handleClose, maxWidth: 'sm', "data-testid": "add-refseq-alias", fullWidth: true },
3323
3386
  React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
3324
- React__default["default"].createElement(material.Grid, { container: true, spacing: 2 },
3325
- React__default["default"].createElement(material.Grid, { item: true, xs: 4 },
3387
+ React__default["default"].createElement(material.Grid2, { container: true, spacing: 2 },
3388
+ React__default["default"].createElement(material.Grid2, null,
3326
3389
  React__default["default"].createElement(material.FormControl, { disabled: enableSubmit && !errorMessage, fullWidth: true },
3327
3390
  React__default["default"].createElement(material.InputLabel, { id: "demo-simple-select-label" }, "Assembly"),
3328
3391
  React__default["default"].createElement(material.Select, { labelId: "demo-simple-select-label", id: "demo-simple-select", label: "Assembly", value: selectedAssembly?.name ?? '', onChange: handleChangeAssembly }, assemblies.map((option) => (React__default["default"].createElement(material.MenuItem, { key: option.name, value: option.name }, option.displayName ?? option.name)))))),
3329
- React__default["default"].createElement(material.Grid, { item: true, xs: 1 }),
3330
- React__default["default"].createElement(material.Grid, { item: true, xs: 7 },
3392
+ React__default["default"].createElement(material.Grid2, null,
3331
3393
  React__default["default"].createElement(material.InputLabel, null, "Load RefName alias"),
3332
3394
  React__default["default"].createElement("input", { type: "file", onChange: handleChangeFileHandler, ref: fileRef, disabled: (enableSubmit && !errorMessage) || !selectedAssembly }))),
3333
3395
  selectedAssembly && refNameAliasMap.size > 0 ? (React__default["default"].createElement("div", { style: { height: 200, width: '100%', marginTop: 20 } },
@@ -4002,11 +4064,11 @@ function isApolloMessageData$1(data) {
4002
4064
  const isInWebWorker$1 = typeof sessionStorage === 'undefined';
4003
4065
  class ApolloSequenceAdapter extends BaseAdapter.BaseSequenceAdapter {
4004
4066
  regions;
4005
- async getRefNames(opts) {
4006
- const regions = await this.getRegions(opts);
4067
+ async getRefNames() {
4068
+ const regions = await this.getRegions();
4007
4069
  return regions.map((regions) => regions.refName);
4008
4070
  }
4009
- async getRegions(opts) {
4071
+ async getRegions() {
4010
4072
  if (this.regions) {
4011
4073
  return this.regions;
4012
4074
  }
@@ -4038,7 +4100,7 @@ class ApolloSequenceAdapter extends BaseAdapter.BaseSequenceAdapter {
4038
4100
  removeEventListener('message', messageListener);
4039
4101
  resolve(data.regions);
4040
4102
  };
4041
- addEventListener('message', messageListener, opts);
4103
+ addEventListener('message', messageListener);
4042
4104
  // @ts-expect-error waiting for types to be published
4043
4105
  globalThis.rpcServer.emit('apollo', {
4044
4106
  apollo: true,
@@ -4055,7 +4117,7 @@ class ApolloSequenceAdapter extends BaseAdapter.BaseSequenceAdapter {
4055
4117
  * @param param -
4056
4118
  * @returns Observable of Feature objects in the region
4057
4119
  */
4058
- getFeatures(region, opts) {
4120
+ getFeatures(region) {
4059
4121
  const { end, refName, start } = region;
4060
4122
  const assemblyId = configuration.readConfObject(this.config, 'assemblyId');
4061
4123
  const regionWithAssemblyName = { ...region, assemblyName: assemblyId };
@@ -4092,7 +4154,7 @@ class ApolloSequenceAdapter extends BaseAdapter.BaseSequenceAdapter {
4092
4154
  removeEventListener('message', messageListener);
4093
4155
  resolve(data.sequence);
4094
4156
  };
4095
- addEventListener('message', messageListener, opts);
4157
+ addEventListener('message', messageListener);
4096
4158
  // @ts-expect-error waiting for types to be published
4097
4159
  globalThis.rpcServer.emit('apollo', {
4098
4160
  apollo: true,
@@ -4792,7 +4854,7 @@ function annotationFromPileup(pluggableElement) {
4792
4854
  .filter(([id, refName]) => id !== refName)
4793
4855
  .map(([id, refName]) => ({
4794
4856
  _id: id,
4795
- name: refName ?? '',
4857
+ name: refName,
4796
4858
  }));
4797
4859
  const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
4798
4860
  if (!refSeqId) {
@@ -4928,6 +4990,285 @@ function annotationFromPileup(pluggableElement) {
4928
4990
  return pluggableElement;
4929
4991
  }
4930
4992
 
4993
+ /* eslint-disable react-hooks/exhaustive-deps */
4994
+ const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
4995
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4996
+ if (!featureTypeOntology) {
4997
+ throw new Error('featureTypeOntology is undefined');
4998
+ }
4999
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
5000
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
5001
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
5002
+ };
5003
+ const isTranscript = (annotationFeature, apolloSessionModel) => {
5004
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
5005
+ if (!featureTypeOntology) {
5006
+ throw new Error('featureTypeOntology is undefined');
5007
+ }
5008
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
5009
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
5010
+ };
5011
+ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
5012
+ const apolloSessionModel = session;
5013
+ const childIds = React.useMemo(() => Object.keys(annotationFeature.children ?? {}), [annotationFeature]);
5014
+ const features = React.useMemo(() => {
5015
+ for (const [, asm] of apolloSessionModel.apolloDataStore.assemblies) {
5016
+ if (asm._id === assembly.name) {
5017
+ for (const [, refSeq] of asm.refSeqs) {
5018
+ if (refSeq._id === refSeqId) {
5019
+ return refSeq.features;
5020
+ }
5021
+ }
5022
+ }
5023
+ }
5024
+ return [];
5025
+ }, []);
5026
+ const [parentFeatureChecked, setParentFeatureChecked] = React.useState(true);
5027
+ const [checkedChildrens, setCheckedChildrens] = React.useState(childIds);
5028
+ const [errorMessage, setErrorMessage] = React.useState('');
5029
+ const [destinationFeatures, setDestinationFeatures] = React.useState([]);
5030
+ const [selectedDestinationFeature, setSelectedDestinationFeature] = React.useState();
5031
+ const getFeatures = (min, max) => {
5032
+ const filteredFeatures = [];
5033
+ for (const [, f] of features) {
5034
+ const featureSnapshot = mobxStateTree.getSnapshot(f);
5035
+ if (min >= featureSnapshot.min && max <= featureSnapshot.max) {
5036
+ filteredFeatures.push(featureSnapshot);
5037
+ }
5038
+ }
5039
+ return filteredFeatures;
5040
+ };
5041
+ React.useEffect(() => {
5042
+ setErrorMessage('');
5043
+ if (checkedChildrens.length === 0) {
5044
+ setParentFeatureChecked(false);
5045
+ return;
5046
+ }
5047
+ if (annotationFeature.children) {
5048
+ const checkedAnnotationFeatureChildren = Object.values(annotationFeature.children)
5049
+ .filter((child) => isTranscript(child, apolloSessionModel))
5050
+ .filter((child) => checkedChildrens.includes(child._id));
5051
+ const mins = checkedAnnotationFeatureChildren.map((f) => f.min);
5052
+ const maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
5053
+ const min = Math.min(...mins);
5054
+ const max = Math.max(...maxes);
5055
+ const filteredFeatures = getFeatures(min, max);
5056
+ setDestinationFeatures(filteredFeatures);
5057
+ if (filteredFeatures.length === 0 &&
5058
+ checkedChildrens.length > 0 &&
5059
+ !parentFeatureChecked) {
5060
+ setErrorMessage('No destination features found');
5061
+ }
5062
+ }
5063
+ }, [checkedChildrens]);
5064
+ const handleParentFeatureCheck = (event) => {
5065
+ const isChecked = event.target.checked;
5066
+ setParentFeatureChecked(isChecked);
5067
+ setCheckedChildrens(isChecked ? childIds : []);
5068
+ };
5069
+ const handleChildFeatureCheck = (event, child) => {
5070
+ setCheckedChildrens((prevChecked) => event.target.checked
5071
+ ? [...prevChecked, child._id]
5072
+ : prevChecked.filter((childId) => childId !== child._id));
5073
+ };
5074
+ const handleDestinationFeatureChange = (e) => {
5075
+ const selectedFeature = destinationFeatures.find((f) => f._id === e.target.value);
5076
+ setSelectedDestinationFeature(selectedFeature);
5077
+ };
5078
+ const handleCreateApolloAnnotation = async () => {
5079
+ if (parentFeatureChecked) {
5080
+ const change = new shared.AddFeatureChange({
5081
+ changedIds: [annotationFeature._id],
5082
+ typeName: 'AddFeatureChange',
5083
+ assembly: assembly.name,
5084
+ addedFeature: annotationFeature,
5085
+ });
5086
+ await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5087
+ session.notify('Annotation added successfully', 'success');
5088
+ handleClose();
5089
+ }
5090
+ else {
5091
+ if (!annotationFeature.children) {
5092
+ return;
5093
+ }
5094
+ if (!selectedDestinationFeature) {
5095
+ return;
5096
+ }
5097
+ for (const childId of checkedChildrens) {
5098
+ const child = annotationFeature.children[childId];
5099
+ const change = new shared.AddFeatureChange({
5100
+ parentFeatureId: selectedDestinationFeature._id,
5101
+ changedIds: [selectedDestinationFeature._id],
5102
+ typeName: 'AddFeatureChange',
5103
+ assembly: assembly.name,
5104
+ addedFeature: child,
5105
+ });
5106
+ await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5107
+ session.notify('Annotation added successfully', 'success');
5108
+ handleClose();
5109
+ }
5110
+ }
5111
+ };
5112
+ return (React__default["default"].createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
5113
+ React__default["default"].createElement(material.DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
5114
+ React__default["default"].createElement(material.DialogContent, null,
5115
+ React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
5116
+ isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${annotationFeature.type}:${annotationFeature.min}..${annotationFeature.max}` })),
5117
+ annotationFeature.children && (React__default["default"].createElement(material.Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
5118
+ .filter((child) => isTranscript(child, apolloSessionModel))
5119
+ .map((child) => (React__default["default"].createElement(material.FormControlLabel, { key: child._id, control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
5120
+ handleChildFeatureCheck(e, child);
5121
+ } }), label: `${child.type}:${child.min}..${child.max}` })))))),
5122
+ !parentFeatureChecked &&
5123
+ checkedChildrens.length > 0 &&
5124
+ destinationFeatures.length > 0 && (React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
5125
+ React__default["default"].createElement(material.Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
5126
+ React__default["default"].createElement(material.Box, { sx: { mt: 1 } },
5127
+ React__default["default"].createElement(material.Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default["default"].createElement(material.MenuItem, { key: f._id, value: f._id }, `${f.type}:${f.min}..${f.max}`)))))))),
5128
+ React__default["default"].createElement(material.DialogActions, null,
5129
+ React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
5130
+ (!parentFeatureChecked &&
5131
+ checkedChildrens.length > 0 &&
5132
+ !selectedDestinationFeature), onClick: handleCreateApolloAnnotation }, "Create"),
5133
+ React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel")),
5134
+ errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
5135
+ React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
5136
+ }
5137
+
5138
+ function simpleFeatureToGFF3Feature(feature, refSeqId) {
5139
+ const xfeature = JSON.parse(JSON.stringify(feature));
5140
+ const children = xfeature.subfeatures;
5141
+ const gff3Feature = [
5142
+ {
5143
+ start: xfeature.start + 1,
5144
+ end: xfeature.end,
5145
+ seq_id: refSeqId,
5146
+ source: xfeature.source ?? null,
5147
+ type: xfeature.type ?? null,
5148
+ score: xfeature.score ?? null,
5149
+ strand: xfeature.strand ? (xfeature.strand === 1 ? '+' : '-') : null,
5150
+ phase: xfeature.phase !== null || xfeature.phase !== undefined
5151
+ ? xfeature.phase
5152
+ : null,
5153
+ attributes: convertFeatureAttributes(xfeature),
5154
+ derived_features: [],
5155
+ child_features: children
5156
+ ? children.map((x) => simpleFeatureToGFF3Feature(x, refSeqId))
5157
+ : [],
5158
+ },
5159
+ ];
5160
+ return gff3Feature;
5161
+ }
5162
+ function jbrowseFeatureToAnnotationFeature(feature, refSeqId) {
5163
+ return shared.gff3ToAnnotationFeature(simpleFeatureToGFF3Feature(feature, refSeqId));
5164
+ }
5165
+ function convertFeatureAttributes(feature) {
5166
+ const attributes = {};
5167
+ const defaultFields = new Set([
5168
+ 'start',
5169
+ 'end',
5170
+ 'type',
5171
+ 'strand',
5172
+ 'refName',
5173
+ 'subfeatures',
5174
+ 'derived_features',
5175
+ 'phase',
5176
+ 'source',
5177
+ 'score',
5178
+ ]);
5179
+ for (const [key, value] of Object.entries(feature)) {
5180
+ if (defaultFields.has(key)) {
5181
+ continue;
5182
+ }
5183
+ attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
5184
+ }
5185
+ return attributes;
5186
+ }
5187
+ function annotationFromJBrowseFeature(pluggableElement) {
5188
+ if (pluggableElement.name !== 'LinearBasicDisplay') {
5189
+ return pluggableElement;
5190
+ }
5191
+ const { stateModel } = pluggableElement;
5192
+ const newStateModel = stateModel
5193
+ .views((self) => ({
5194
+ getFirstRegion() {
5195
+ const lgv = util.getContainingView(self);
5196
+ return lgv.dynamicBlocks.contentBlocks[0];
5197
+ },
5198
+ getAssembly() {
5199
+ const firstRegion = self.getFirstRegion();
5200
+ const session = util.getSession(self);
5201
+ const { assemblyManager } = session;
5202
+ const { assemblyName } = firstRegion;
5203
+ const assembly = assemblyManager.get(assemblyName);
5204
+ if (!assembly) {
5205
+ throw new Error(`Could not find assembly named ${assemblyName}`);
5206
+ }
5207
+ return assembly;
5208
+ },
5209
+ getRefSeqId(assembly) {
5210
+ const firstRegion = self.getFirstRegion();
5211
+ const { refName } = firstRegion;
5212
+ const { refNameAliases } = assembly;
5213
+ if (!refNameAliases) {
5214
+ throw new Error(`Could not find aliases for ${assembly.name}`);
5215
+ }
5216
+ const newRefNames = [...Object.entries(refNameAliases)]
5217
+ .filter(([id, refName]) => id !== refName)
5218
+ .map(([id, refName]) => ({
5219
+ _id: id,
5220
+ name: refName,
5221
+ }));
5222
+ const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
5223
+ if (!refSeqId) {
5224
+ throw new Error(`Could not find refSeqId named ${refName}`);
5225
+ }
5226
+ return refSeqId;
5227
+ },
5228
+ getAnnotationFeature(assembly) {
5229
+ const refSeqId = self.getRefSeqId(assembly);
5230
+ const sfeature = self.contextMenuFeature.data;
5231
+ return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
5232
+ },
5233
+ }))
5234
+ .views((self) => {
5235
+ const superContextMenuItems = self.contextMenuItems;
5236
+ const session = util.getSession(self);
5237
+ const assembly = self.getAssembly();
5238
+ return {
5239
+ contextMenuItems() {
5240
+ const feature = self.contextMenuFeature;
5241
+ if (!feature) {
5242
+ return superContextMenuItems();
5243
+ }
5244
+ return [
5245
+ ...superContextMenuItems(),
5246
+ {
5247
+ label: 'Create Apollo annotation',
5248
+ icon: AddIcon__default["default"],
5249
+ onClick: () => {
5250
+ session.queueDialog((doneCallback) => [
5251
+ CreateApolloAnnotation,
5252
+ {
5253
+ session,
5254
+ handleClose: () => {
5255
+ doneCallback();
5256
+ },
5257
+ annotationFeature: self.getAnnotationFeature(assembly),
5258
+ assembly,
5259
+ refSeqId: self.getRefSeqId(assembly),
5260
+ },
5261
+ ]);
5262
+ },
5263
+ },
5264
+ ];
5265
+ },
5266
+ };
5267
+ });
5268
+ pluggableElement.stateModel = newStateModel;
5269
+ return pluggableElement;
5270
+ }
5271
+
4931
5272
  /* eslint-disable @typescript-eslint/unbound-method */
4932
5273
  const StringTextField = mobxReact.observer(function StringTextField({ onChangeCommitted, value: initialValue, ...props }) {
4933
5274
  const [value, setValue] = React.useState(String(initialValue));
@@ -5134,44 +5475,44 @@ const Attributes = mobxReact.observer(function Attributes({ assembly, editable,
5134
5475
  }
5135
5476
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
5136
5477
  React__default["default"].createElement(material.Typography, { variant: "h5" }, "Attributes"),
5137
- React__default["default"].createElement(material.Grid, { container: true, direction: "column", spacing: 1 },
5478
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column", spacing: 1 },
5138
5479
  Object.entries(attributes).map(([key, value]) => {
5139
5480
  if (key === '') {
5140
5481
  return null;
5141
5482
  }
5142
5483
  const EditorComponent = reservedKeys.get(key) ?? CustomAttributeValueEditor;
5143
- return (React__default["default"].createElement(material.Grid, { container: true, item: true, spacing: 3, alignItems: "center", key: key },
5144
- React__default["default"].createElement(material.Grid, { item: true, xs: "auto" },
5484
+ return (React__default["default"].createElement(material.Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
5485
+ React__default["default"].createElement(material.Grid2, null,
5145
5486
  React__default["default"].createElement(material.Paper, { variant: "outlined", className: classes.attributeName },
5146
5487
  React__default["default"].createElement(material.Typography, null, key))),
5147
- React__default["default"].createElement(material.Grid, { item: true, flexGrow: 1 },
5488
+ React__default["default"].createElement(material.Grid2, { flexGrow: 1 },
5148
5489
  React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => onChangeCommitted(key, newValue) })),
5149
- React__default["default"].createElement(material.Grid, { item: true, xs: 1 },
5490
+ React__default["default"].createElement(material.Grid2, null,
5150
5491
  React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => onChangeCommitted(key) },
5151
5492
  React__default["default"].createElement(DeleteIcon__default["default"], { fontSize: "medium", key: key })))));
5152
5493
  }),
5153
- React__default["default"].createElement(material.Grid, { item: true },
5494
+ React__default["default"].createElement(material.Grid2, null,
5154
5495
  React__default["default"].createElement(material.Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
5155
5496
  setShowAddNewForm(true);
5156
5497
  } }, "Add new")),
5157
- showAddNewForm ? (React__default["default"].createElement(material.Grid, { item: true },
5498
+ showAddNewForm ? (React__default["default"].createElement(material.Grid2, null,
5158
5499
  React__default["default"].createElement(material.Paper, { elevation: 8, className: classes.newAttributePaper },
5159
- React__default["default"].createElement(material.Grid, { container: true, direction: "column" },
5160
- React__default["default"].createElement(material.Grid, { item: true },
5500
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column" },
5501
+ React__default["default"].createElement(material.Grid2, null,
5161
5502
  React__default["default"].createElement(material.FormControl, null,
5162
5503
  React__default["default"].createElement(material.FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
5163
5504
  React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
5164
- React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid, { container: true, spacing: 1, alignItems: "center" },
5165
- React__default["default"].createElement(material.Grid, { item: true },
5505
+ React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid2, { container: true, spacing: 1, alignItems: "center" },
5506
+ React__default["default"].createElement(material.Grid2, null,
5166
5507
  React__default["default"].createElement(material.Typography, null, "Custom")),
5167
- React__default["default"].createElement(material.Grid, { item: true },
5508
+ React__default["default"].createElement(material.Grid2, null,
5168
5509
  React__default["default"].createElement(material.TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys.has(newAttributeKey)
5169
5510
  ? ''
5170
5511
  : newAttributeKey, disabled: reservedKeys.has(newAttributeKey), onChange: (event) => {
5171
5512
  setNewAttributeKey(event.target.value);
5172
5513
  } }))) }),
5173
5514
  [...reservedKeys.keys()].map((key) => (React__default["default"].createElement(material.FormControlLabel, { key: key, value: key, control: React__default["default"].createElement(material.Radio, null), label: key })))))),
5174
- React__default["default"].createElement(material.Grid, { item: true },
5515
+ React__default["default"].createElement(material.Grid2, null,
5175
5516
  React__default["default"].createElement(material.DialogActions, null,
5176
5517
  React__default["default"].createElement(material.Button, { key: "addButton", color: "primary", variant: "contained", onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
5177
5518
  React__default["default"].createElement(material.Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
@@ -5353,6 +5694,47 @@ const Sequence = mobxReact.observer(function Sequence({ assembly, feature, refNa
5353
5694
  React__default["default"].createElement("div", null, showSequence && (React__default["default"].createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })))));
5354
5695
  });
5355
5696
 
5697
+ const FeatureDetailsNavigation = mobxReact.observer(function FeatureDetailsNavigation(props) {
5698
+ const { feature, model } = props;
5699
+ const { children, parent } = feature;
5700
+ const childFeatures = [];
5701
+ if (children) {
5702
+ for (const [, child] of children) {
5703
+ childFeatures.push(child);
5704
+ }
5705
+ }
5706
+ if (!(parent ?? childFeatures.length > 0)) {
5707
+ return null;
5708
+ }
5709
+ return (React__default["default"].createElement("div", null,
5710
+ React__default["default"].createElement(material.Typography, { variant: "h5" }, "Go to related feature"),
5711
+ parent && (React__default["default"].createElement("div", null,
5712
+ React__default["default"].createElement(material.Typography, { variant: "h6" }, "Parent:"),
5713
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: () => {
5714
+ model.setFeature(parent);
5715
+ } },
5716
+ parent.type,
5717
+ " (",
5718
+ parent.min,
5719
+ "..",
5720
+ parent.max,
5721
+ ")"))),
5722
+ childFeatures.length > 0 && (React__default["default"].createElement("div", null,
5723
+ React__default["default"].createElement(material.Typography, { variant: "h6" },
5724
+ childFeatures.length === 1 ? 'Child' : 'Children',
5725
+ ":"),
5726
+ childFeatures.map((child) => (React__default["default"].createElement("div", { key: child._id, style: { marginBottom: 5 } },
5727
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: () => {
5728
+ model.setFeature(child);
5729
+ } },
5730
+ child.type,
5731
+ " (",
5732
+ child.min,
5733
+ "..",
5734
+ child.max,
5735
+ ")"))))))));
5736
+ });
5737
+
5356
5738
  const useStyles$8 = mui.makeStyles()((theme) => ({
5357
5739
  root: {
5358
5740
  padding: theme.spacing(2),
@@ -5383,7 +5765,9 @@ const ApolloFeatureDetailsWidget = mobxReact.observer(function ApolloFeatureDeta
5383
5765
  React__default["default"].createElement("hr", null),
5384
5766
  React__default["default"].createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }),
5385
5767
  React__default["default"].createElement("hr", null),
5386
- React__default["default"].createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName })));
5768
+ React__default["default"].createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName }),
5769
+ React__default["default"].createElement("hr", null),
5770
+ React__default["default"].createElement(FeatureDetailsNavigation, { model: model, feature: feature })));
5387
5771
  });
5388
5772
 
5389
5773
  /* eslint-disable @typescript-eslint/no-unsafe-call */
@@ -5523,7 +5907,14 @@ const TranscriptBasicInformation = mobxReact.observer(function TranscriptBasicIn
5523
5907
  if (!refData) {
5524
5908
  return null;
5525
5909
  }
5526
- const { strand, transcriptParts } = feature;
5910
+ let strand, transcriptParts;
5911
+ try {
5912
+ ;
5913
+ ({ strand, transcriptParts } = feature);
5914
+ }
5915
+ catch {
5916
+ return null;
5917
+ }
5527
5918
  const [firstLocation] = transcriptParts;
5528
5919
  const locationData = firstLocation
5529
5920
  .map((loc, idx) => {
@@ -5664,6 +6055,28 @@ function getSequenceSegments(segmentType, feature, getSequence) {
5664
6055
  segments.push({ type: 'CDS', sequenceLines, locs });
5665
6056
  return segments;
5666
6057
  }
6058
+ case 'protein': {
6059
+ let wholeSequence = '';
6060
+ const [firstLocation] = cdsLocations;
6061
+ const locs = [];
6062
+ for (const loc of firstLocation) {
6063
+ let sequence = getSequence(loc.min, loc.max);
6064
+ if (strand === -1) {
6065
+ sequence = util.revcom(sequence);
6066
+ }
6067
+ wholeSequence += sequence;
6068
+ locs.push({ min: loc.min, max: loc.max });
6069
+ }
6070
+ let protein = '';
6071
+ for (let i = 0; i < wholeSequence.length; i += 3) {
6072
+ const codonSeq = wholeSequence.slice(i, i + 3).toUpperCase();
6073
+ protein +=
6074
+ util.defaultCodonTable[codonSeq] || '&';
6075
+ }
6076
+ const sequenceLines = shared.splitStringIntoChunks(protein, SEQUENCE_WRAP_LENGTH);
6077
+ segments.push({ type: 'protein', sequenceLines, locs });
6078
+ return segments;
6079
+ }
5667
6080
  }
5668
6081
  }
5669
6082
  function getSegmentColor(type) {
@@ -5752,7 +6165,8 @@ const TranscriptSequence = mobxReact.observer(function TranscriptSequence({ asse
5752
6165
  React__default["default"].createElement(material.Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
5753
6166
  React__default["default"].createElement(material.MenuItem, { value: "CDS" }, "CDS"),
5754
6167
  React__default["default"].createElement(material.MenuItem, { value: "cDNA" }, "cDNA"),
5755
- React__default["default"].createElement(material.MenuItem, { value: "genomic" }, "Genomic")),
6168
+ React__default["default"].createElement(material.MenuItem, { value: "genomic" }, "Genomic"),
6169
+ React__default["default"].createElement(material.MenuItem, { value: "protein" }, "Protein")),
5756
6170
  React__default["default"].createElement(material.Paper, { style: {
5757
6171
  fontFamily: 'monospace',
5758
6172
  padding: theme.spacing(),
@@ -5990,7 +6404,12 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
5990
6404
  ]);
5991
6405
  },
5992
6406
  });
5993
- if (feature.type === 'mRNA' && util.isSessionModelWithWidgets(session)) {
6407
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
6408
+ if (!featureTypeOntology) {
6409
+ throw new Error('featureTypeOntology is undefined');
6410
+ }
6411
+ if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
6412
+ util.isSessionModelWithWidgets(session)) {
5994
6413
  menuItems.push({
5995
6414
  label: 'Edit transcript details',
5996
6415
  onClick: () => {
@@ -6062,958 +6481,555 @@ const NumberCell = mobxReact.observer(function NumberCell({ initialValue, notify
6062
6481
  } })));
6063
6482
  });
6064
6483
 
6065
- const minDisplayHeight = 20;
6066
- function baseModelFactory(_pluginManager, configSchema) {
6067
- return pluggableElementTypes.BaseDisplay.named('BaseLinearApolloDisplay')
6068
- .props({
6069
- type: mobxStateTree.types.literal('LinearApolloDisplay'),
6070
- configuration: configuration.ConfigurationReference(configSchema),
6071
- graphical: true,
6072
- table: false,
6073
- heightPreConfig: mobxStateTree.types.maybe(mobxStateTree.types.refinement('displayHeight', mobxStateTree.types.number, (n) => n >= minDisplayHeight)),
6074
- })
6075
- .views((self) => {
6076
- const { configuration, renderProps: superRenderProps } = self;
6077
- return {
6078
- renderProps() {
6079
- return {
6080
- ...superRenderProps(),
6081
- ...tracks.getParentRenderProps(self),
6082
- config: configuration.renderer,
6083
- };
6084
- },
6085
- };
6086
- })
6087
- .volatile(() => ({
6088
- scrollTop: 0,
6089
- }))
6090
- .views((self) => ({
6091
- get lgv() {
6092
- return util.getContainingView(self);
6093
- },
6094
- get height() {
6095
- if (self.heightPreConfig) {
6096
- return self.heightPreConfig;
6097
- }
6098
- if (self.graphical && self.table) {
6099
- return 500;
6100
- }
6101
- if (self.graphical) {
6102
- return 200;
6103
- }
6104
- return 300;
6105
- },
6106
- }))
6107
- .views((self) => ({
6108
- get rendererTypeName() {
6109
- return self.configuration.renderer.type;
6110
- },
6111
- get session() {
6112
- return util.getSession(self);
6113
- },
6114
- get regions() {
6115
- const regions = self.lgv.dynamicBlocks.contentBlocks.map(({ assemblyName, end, refName, start }) => ({
6116
- assemblyName,
6117
- refName,
6118
- start: Math.round(start),
6119
- end: Math.round(end),
6120
- }));
6121
- return regions;
6122
- },
6123
- regionCannotBeRendered( /* region */) {
6124
- if (self.lgv && self.lgv.bpPerPx >= 200) {
6125
- return 'Zoom in to see annotations';
6126
- }
6127
- return;
6484
+ /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
6485
+ const useStyles$4 = mui.makeStyles()((theme) => ({
6486
+ typeContent: {
6487
+ display: 'inline-block',
6488
+ width: '174px',
6489
+ height: '100%',
6490
+ cursor: 'text',
6491
+ },
6492
+ feature: {
6493
+ td: {
6494
+ position: 'relative',
6495
+ verticalAlign: 'top',
6496
+ paddingLeft: '0.5em',
6128
6497
  },
6129
- }))
6130
- .views((self) => ({
6131
- get apolloInternetAccount() {
6132
- const [region] = self.regions;
6133
- const { internetAccounts } = mobxStateTree.getRoot(self);
6134
- const { assemblyName } = region;
6135
- const { assemblyManager } = self.session;
6136
- const assembly = assemblyManager.get(assemblyName);
6137
- if (!assembly) {
6138
- throw new Error(`No assembly found with name ${assemblyName}`);
6139
- }
6140
- const { internetAccountConfigId } = configuration.getConf(assembly, [
6141
- 'sequence',
6142
- 'metadata',
6143
- ]);
6144
- return internetAccounts.find((ia) => configuration.getConf(ia, 'internetAccountId') === internetAccountConfigId);
6145
- },
6146
- get changeManager() {
6147
- return self.session.apolloDataStore
6148
- .changeManager;
6149
- },
6150
- getAssemblyId(assemblyName) {
6151
- const { assemblyManager } = self.session;
6152
- const assembly = assemblyManager.get(assemblyName);
6153
- if (!assembly) {
6154
- throw new Error(`Could not find assembly named ${assemblyName}`);
6155
- }
6156
- return assembly.name;
6157
- },
6158
- get selectedFeature() {
6159
- return self.session
6160
- .apolloSelectedFeature;
6161
- },
6162
- }))
6163
- .actions((self) => ({
6164
- setScrollTop(scrollTop) {
6165
- self.scrollTop = scrollTop;
6166
- },
6167
- setHeight(displayHeight) {
6168
- self.heightPreConfig = Math.max(displayHeight, minDisplayHeight);
6169
- return self.height;
6170
- },
6171
- resizeHeight(distance) {
6172
- const oldHeight = self.height;
6173
- const newHeight = this.setHeight(self.height + distance);
6174
- return newHeight - oldHeight;
6175
- },
6176
- showGraphicalOnly() {
6177
- self.graphical = true;
6178
- self.table = false;
6179
- },
6180
- showTableOnly() {
6181
- self.graphical = false;
6182
- self.table = true;
6183
- },
6184
- showGraphicalAndTable() {
6185
- self.graphical = true;
6186
- self.table = true;
6187
- },
6188
- }))
6189
- .views((self) => {
6190
- const { trackMenuItems: superTrackMenuItems } = self;
6191
- return {
6192
- trackMenuItems() {
6193
- const { graphical, table } = self;
6194
- return [
6195
- ...superTrackMenuItems(),
6196
- {
6197
- type: 'subMenu',
6198
- label: 'Appearance',
6199
- subMenu: [
6200
- {
6201
- label: 'Show graphical display',
6202
- type: 'radio',
6203
- checked: graphical && !table,
6204
- onClick: () => {
6205
- self.showGraphicalOnly();
6206
- },
6207
- },
6208
- {
6209
- label: 'Show table display',
6210
- type: 'radio',
6211
- checked: table && !graphical,
6212
- onClick: () => {
6213
- self.showTableOnly();
6214
- },
6215
- },
6216
- {
6217
- label: 'Show both graphical and table display',
6218
- type: 'radio',
6219
- checked: table && graphical,
6220
- onClick: () => {
6221
- self.showGraphicalAndTable();
6222
- },
6223
- },
6224
- ],
6225
- },
6226
- ];
6227
- },
6228
- };
6229
- })
6230
- .actions((self) => ({
6231
- setSelectedFeature(feature) {
6232
- self.session.apolloSetSelectedFeature(feature);
6233
- },
6234
- afterAttach() {
6235
- mobxStateTree.addDisposer(self, mobx.autorun(() => {
6236
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6237
- return;
6238
- }
6239
- void self.session.apolloDataStore.loadFeatures(self.regions);
6240
- if (self.lgv.bpPerPx <= 3) {
6241
- void self.session.apolloDataStore.loadRefSeq(self.regions);
6242
- }
6243
- }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
6244
- },
6245
- }));
6246
- }
6247
-
6248
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
6249
- function layoutsModelFactory(pluginManager, configSchema) {
6250
- const BaseLinearApolloDisplay = baseModelFactory(pluginManager, configSchema);
6251
- return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
6252
- .props({
6253
- featuresMinMaxLimit: 500_000,
6254
- })
6255
- .volatile(() => ({
6256
- seenFeatures: mobx.observable.map(),
6257
- }))
6258
- .views((self) => ({
6259
- get featuresMinMax() {
6260
- const { assemblyManager } = self.session;
6261
- return self.lgv.displayedRegions.map((region) => {
6262
- const assembly = assemblyManager.get(region.assemblyName);
6263
- let min;
6264
- let max;
6265
- const { end, refName, start } = region;
6266
- for (const [, feature] of self.seenFeatures) {
6267
- if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
6268
- !util.doesIntersect2(start, end, feature.min, feature.max) ||
6269
- feature.length > self.featuresMinMaxLimit) {
6270
- continue;
6271
- }
6272
- if (min === undefined) {
6273
- ({ min } = feature);
6274
- }
6275
- if (max === undefined) {
6276
- ({ max } = feature);
6277
- }
6278
- if (feature.minWithChildren < min) {
6279
- ({ min } = feature);
6280
- }
6281
- if (feature.maxWithChildren > max) {
6282
- ({ max } = feature);
6283
- }
6284
- }
6285
- if (min !== undefined && max !== undefined) {
6286
- return [min, max];
6287
- }
6288
- return;
6289
- });
6290
- },
6291
- }))
6292
- .actions((self) => ({
6293
- addSeenFeature(feature) {
6294
- self.seenFeatures.set(feature._id, feature);
6295
- },
6296
- deleteSeenFeature(featureId) {
6297
- self.seenFeatures.delete(featureId);
6298
- },
6299
- }))
6300
- .views((self) => ({
6301
- get featureLayouts() {
6302
- const { assemblyManager } = self.session;
6303
- return self.lgv.displayedRegions.map((region, idx) => {
6304
- const assembly = assemblyManager.get(region.assemblyName);
6305
- const featureLayout = new Map();
6306
- const minMax = self.featuresMinMax[idx];
6307
- if (!minMax) {
6308
- return featureLayout;
6309
- }
6310
- const [min, max] = minMax;
6311
- const rows = [];
6312
- const { end, refName, start } = region;
6313
- for (const [id, feature] of self.seenFeatures.entries()) {
6314
- if (!mobxStateTree.isAlive(feature)) {
6315
- self.deleteSeenFeature(id);
6316
- continue;
6317
- }
6318
- if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
6319
- !util.doesIntersect2(start, end, feature.min, feature.max)) {
6320
- continue;
6321
- }
6322
- const rowCount = getGlyph(feature).getRowCount(feature, self.lgv.bpPerPx);
6323
- let startingRow = 0;
6324
- let placed = false;
6325
- while (!placed) {
6326
- let rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
6327
- if (rowsForFeature.length < rowCount) {
6328
- for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
6329
- const newRowNumber = rows.length;
6330
- rows[newRowNumber] = Array.from({ length: max - min });
6331
- featureLayout.set(newRowNumber, []);
6332
- }
6333
- rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
6334
- }
6335
- if (rowsForFeature
6336
- .map((rowForFeature) => {
6337
- // zero-length features are allowed in the spec
6338
- const featureMax = feature.max - feature.min === 0
6339
- ? feature.min + 1
6340
- : feature.max;
6341
- let start = feature.min - min, end = featureMax - min;
6342
- if (feature.min - min < 0) {
6343
- start = 0;
6344
- end = featureMax - feature.min;
6345
- }
6346
- return rowForFeature.slice(start, end).some(Boolean);
6347
- })
6348
- .some(Boolean)) {
6349
- startingRow += 1;
6350
- continue;
6351
- }
6352
- for (let rowNum = startingRow; rowNum < startingRow + rowCount; rowNum++) {
6353
- const row = rows[rowNum];
6354
- let start = feature.min - min, end = feature.max - min;
6355
- if (feature.min - min < 0) {
6356
- start = 0;
6357
- end = feature.max - feature.min;
6358
- }
6359
- row.fill(true, start, end);
6360
- const layoutRow = featureLayout.get(rowNum);
6361
- layoutRow?.push([rowNum - startingRow, feature]);
6362
- }
6363
- placed = true;
6364
- }
6365
- }
6366
- return featureLayout;
6367
- });
6368
- },
6369
- getFeatureLayoutPosition(feature) {
6370
- const { featureLayouts } = this;
6371
- for (const [idx, layout] of featureLayouts.entries()) {
6372
- for (const [layoutRowNum, layoutRow] of layout) {
6373
- for (const [featureRowNum, layoutFeature] of layoutRow) {
6374
- if (featureRowNum !== 0) {
6375
- // Same top-level feature in all feature rows, so only need to
6376
- // check the first one
6377
- continue;
6378
- }
6379
- if (feature._id === layoutFeature._id) {
6380
- return {
6381
- layoutIndex: idx,
6382
- layoutRow: layoutRowNum,
6383
- featureRow: featureRowNum,
6384
- };
6385
- }
6386
- if (layoutFeature.hasDescendant(feature._id)) {
6387
- const row = getGlyph(layoutFeature).getRowForFeature(layoutFeature, feature);
6388
- if (row !== undefined) {
6389
- return {
6390
- layoutIndex: idx,
6391
- layoutRow: layoutRowNum,
6392
- featureRow: row,
6393
- };
6394
- }
6395
- }
6396
- }
6397
- }
6398
- }
6399
- return;
6400
- },
6401
- }))
6402
- .views((self) => ({
6403
- get highestRow() {
6404
- return Math.max(0, ...self.featureLayouts.map((layout) => Math.max(...layout.keys())));
6405
- },
6406
- }))
6407
- .actions((self) => ({
6408
- afterAttach() {
6409
- mobxStateTree.addDisposer(self, mobx.autorun(() => {
6410
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6411
- return;
6412
- }
6413
- for (const region of self.regions) {
6414
- const assembly = self.session.apolloDataStore.assemblies.get(region.assemblyName);
6415
- const ref = assembly?.getByRefName(region.refName);
6416
- const features = ref?.features;
6417
- if (!features) {
6418
- continue;
6419
- }
6420
- for (const [, feature] of features) {
6421
- if (util.doesIntersect2(region.start, region.end, feature.min, feature.max) &&
6422
- !self.seenFeatures.has(feature._id)) {
6423
- self.addSeenFeature(feature);
6424
- }
6425
- }
6426
- }
6427
- }, { name: 'LinearApolloDisplaySetSeenFeatures', delay: 1000 }));
6428
- },
6429
- }));
6430
- }
6431
-
6432
- function renderingModelIntermediateFactory(pluginManager, configSchema) {
6433
- const LinearApolloDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
6434
- return LinearApolloDisplayLayouts.named('LinearApolloDisplayRendering')
6435
- .props({
6436
- sequenceRowHeight: 15,
6437
- apolloRowHeight: 20,
6438
- detailsMinHeight: 200,
6439
- detailsHeight: 200,
6440
- lastRowTooltipBufferHeight: 40,
6441
- isShown: true,
6442
- })
6443
- .volatile(() => ({
6444
- canvas: null,
6445
- overlayCanvas: null,
6446
- collaboratorCanvas: null,
6447
- seqTrackCanvas: null,
6448
- seqTrackOverlayCanvas: null,
6449
- theme: undefined,
6450
- }))
6451
- .views((self) => ({
6452
- get featuresHeight() {
6453
- return ((self.highestRow + 1) * self.apolloRowHeight +
6454
- self.lastRowTooltipBufferHeight);
6455
- },
6456
- }))
6457
- .actions((self) => ({
6458
- toggleShown() {
6459
- self.isShown = !self.isShown;
6460
- },
6461
- setDetailsHeight(newHeight) {
6462
- self.detailsHeight = self.isShown
6463
- ? Math.max(Math.min(newHeight, self.height - 100), Math.min(self.height, self.detailsMinHeight))
6464
- : newHeight;
6465
- },
6466
- setCanvas(canvas) {
6467
- self.canvas = canvas;
6468
- },
6469
- setOverlayCanvas(canvas) {
6470
- self.overlayCanvas = canvas;
6471
- },
6472
- setCollaboratorCanvas(canvas) {
6473
- self.collaboratorCanvas = canvas;
6474
- },
6475
- setSeqTrackCanvas(canvas) {
6476
- self.seqTrackCanvas = canvas;
6477
- },
6478
- setSeqTrackOverlayCanvas(canvas) {
6479
- self.seqTrackOverlayCanvas = canvas;
6480
- },
6481
- setTheme(theme) {
6482
- self.theme = theme;
6483
- },
6484
- afterAttach() {
6485
- mobxStateTree.addDisposer(self, mobx.autorun(() => {
6486
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6487
- return;
6488
- }
6489
- const ctx = self.collaboratorCanvas?.getContext('2d');
6490
- if (!ctx) {
6491
- return;
6492
- }
6493
- ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
6494
- for (const collaborator of self.session.collaborators) {
6495
- const { locations } = collaborator;
6496
- if (locations.length === 0) {
6497
- continue;
6498
- }
6499
- let idx = 0;
6500
- for (const displayedRegion of self.lgv.displayedRegions) {
6501
- for (const location of locations) {
6502
- if (location.refSeq !== displayedRegion.refName) {
6503
- continue;
6504
- }
6505
- const { end, refSeq, start } = location;
6506
- const locationStartPxInfo = self.lgv.bpToPx({
6507
- refName: refSeq,
6508
- coord: start,
6509
- regionNumber: idx,
6510
- });
6511
- if (!locationStartPxInfo) {
6512
- continue;
6513
- }
6514
- const locationStartPx = locationStartPxInfo.offsetPx - self.lgv.offsetPx;
6515
- const locationWidthPx = (end - start) / self.lgv.bpPerPx;
6516
- ctx.fillStyle = 'rgba(0,255,0,.2)';
6517
- ctx.fillRect(locationStartPx, 1, locationWidthPx, 100);
6518
- ctx.fillStyle = 'black';
6519
- ctx.fillText(collaborator.name, locationStartPx + 1, 11, locationWidthPx - 2);
6520
- }
6521
- idx++;
6522
- }
6523
- }
6524
- }, { name: 'LinearApolloDisplayRenderCollaborators' }));
6525
- },
6526
- }));
6527
- }
6528
- function colorCode(letter, theme) {
6529
- return (theme?.palette.bases[letter.toUpperCase()].main.toString() ?? 'lightgray');
6530
- }
6531
- function codonColorCode(letter) {
6532
- const colorMap = {
6533
- M: '#33ee33',
6534
- '*': '#f44336',
6535
- };
6536
- return colorMap[letter.toUpperCase()];
6537
- }
6538
- function reverseCodonSeq(seq) {
6539
- return [...seq]
6540
- .map((c) => util.revcom(c))
6541
- .reverse()
6542
- .join('');
6543
- }
6544
- function drawLetter(seqTrackctx, startPx, widthPx, letter, textY) {
6545
- const fontSize = Math.min(widthPx, 10);
6546
- seqTrackctx.fillStyle = '#000';
6547
- seqTrackctx.font = `${fontSize}px`;
6548
- const textWidth = seqTrackctx.measureText(letter).width;
6549
- const textX = startPx + (widthPx - textWidth) / 2;
6550
- seqTrackctx.fillText(letter, textX, textY + 10);
6498
+ },
6499
+ arrow: {
6500
+ display: 'inline-block',
6501
+ width: '1.6em',
6502
+ textAlign: 'center',
6503
+ cursor: 'pointer',
6504
+ },
6505
+ arrowExpanded: {
6506
+ transform: 'rotate(90deg)',
6507
+ },
6508
+ hoveredFeature: {
6509
+ backgroundColor: theme.palette.action.hover,
6510
+ },
6511
+ typeInputElement: {
6512
+ border: 'none',
6513
+ background: 'none',
6514
+ },
6515
+ typeErrorMessage: {
6516
+ color: 'red',
6517
+ },
6518
+ }));
6519
+ function makeContextMenuItems(display, feature) {
6520
+ const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
6521
+ return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
6551
6522
  }
6552
- function drawTranslation(seqTrackctx, bpPerPx, trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight, seq, i, reverse) {
6553
- let codonSeq = seq.slice(i, i + 3).toUpperCase();
6554
- if (reverse) {
6555
- codonSeq = reverseCodonSeq(codonSeq);
6556
- }
6557
- const codonLetter = util.defaultCodonTable[codonSeq];
6558
- if (!codonLetter) {
6559
- return;
6560
- }
6561
- const fillColor = codonColorCode(codonLetter);
6562
- if (fillColor) {
6563
- seqTrackctx.fillStyle = fillColor;
6564
- seqTrackctx.fillRect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
6565
- }
6566
- if (bpPerPx <= 0.1) {
6567
- seqTrackctx.rect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
6568
- seqTrackctx.stroke();
6569
- drawLetter(seqTrackctx, trnslStartPx, trnslWidthPx, codonLetter, trnslY);
6523
+ function getTopLevelFeature(feature) {
6524
+ let cur = feature;
6525
+ while (cur.parent) {
6526
+ cur = cur.parent;
6570
6527
  }
6528
+ return cur;
6571
6529
  }
6572
- function sequenceRenderingModelFactory(pluginManager, configSchema) {
6573
- const LinearApolloDisplayRendering = renderingModelIntermediateFactory(pluginManager, configSchema);
6574
- return LinearApolloDisplayRendering.actions((self) => ({
6575
- afterAttach() {
6576
- mobxStateTree.addDisposer(self, mobx.autorun(async () => {
6577
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6578
- return;
6579
- }
6580
- if (self.lgv.bpPerPx > 3) {
6581
- return;
6582
- }
6583
- const seqTrackctx = self.seqTrackCanvas?.getContext('2d');
6584
- if (!seqTrackctx) {
6585
- return;
6586
- }
6587
- seqTrackctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
6588
- const frames = self.lgv.bpPerPx <= 1
6589
- ? [3, 2, 1, 0, 0, -1, -2, -3]
6590
- : [3, 2, 1, -1, -2, -3];
6591
- let height = 0;
6592
- for (const frame of frames) {
6593
- const frameColor = self.theme?.palette.framesCDS.at(frame)?.main;
6594
- if (frameColor) {
6595
- seqTrackctx.fillStyle = frameColor;
6596
- seqTrackctx.fillRect(0, height, self.lgv.dynamicBlocks.totalWidthPx, self.sequenceRowHeight);
6597
- }
6598
- height += self.sequenceRowHeight;
6599
- }
6600
- for (const [idx, region] of self.regions.entries()) {
6601
- const driver = self.session.apolloDataStore.getBackendDriver(region.assemblyName);
6602
- if (!driver) {
6603
- throw new Error('Failed to get the backend driver');
6604
- }
6605
- const { seq } = await driver.getSequence(region);
6606
- if (!seq) {
6607
- return;
6608
- }
6609
- for (const [i, letter] of [...seq].entries()) {
6610
- const trnslXOffset = (self.lgv.bpToPx({
6611
- refName: region.refName,
6612
- coord: region.start + i,
6613
- regionNumber: idx,
6614
- })?.offsetPx ?? 0) - self.lgv.offsetPx;
6615
- const trnslWidthPx = 3 / self.lgv.bpPerPx;
6616
- const trnslStartPx = self.lgv.displayedRegions[idx].reversed
6617
- ? trnslXOffset - trnslWidthPx
6618
- : trnslXOffset;
6619
- // Draw translation forward
6620
- for (let j = 2; j >= 0; j--) {
6621
- if ((region.start + i) % 3 === j) {
6622
- drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (2 - j), trnslWidthPx, self.sequenceRowHeight, seq, i, false);
6623
- }
6624
- }
6625
- if (self.lgv.bpPerPx <= 1) {
6626
- const xOffset = (self.lgv.bpToPx({
6627
- refName: region.refName,
6628
- coord: region.start + i,
6629
- regionNumber: idx,
6630
- })?.offsetPx ?? 0) - self.lgv.offsetPx;
6631
- const widthPx = 1 / self.lgv.bpPerPx;
6632
- const startPx = self.lgv.displayedRegions[idx].reversed
6633
- ? xOffset - widthPx
6634
- : xOffset;
6635
- // Draw forward
6636
- seqTrackctx.beginPath();
6637
- seqTrackctx.fillStyle = colorCode(letter, self.theme);
6638
- seqTrackctx.rect(startPx, self.sequenceRowHeight * 3, widthPx, self.sequenceRowHeight);
6639
- seqTrackctx.fill();
6640
- if (self.lgv.bpPerPx <= 0.1) {
6641
- seqTrackctx.stroke();
6642
- drawLetter(seqTrackctx, startPx, widthPx, letter, self.sequenceRowHeight * 3);
6643
- }
6644
- // Draw reverse
6645
- const revLetter = util.revcom(letter);
6646
- seqTrackctx.beginPath();
6647
- seqTrackctx.fillStyle = colorCode(revLetter, self.theme);
6648
- seqTrackctx.rect(startPx, self.sequenceRowHeight * 4, widthPx, self.sequenceRowHeight);
6649
- seqTrackctx.fill();
6650
- if (self.lgv.bpPerPx <= 0.1) {
6651
- seqTrackctx.stroke();
6652
- drawLetter(seqTrackctx, startPx, widthPx, revLetter, self.sequenceRowHeight * 4);
6653
- }
6654
- }
6655
- // Draw translation reverse
6656
- for (let k = 0; k <= 2; k++) {
6657
- const rowOffset = self.lgv.bpPerPx <= 1 ? 5 : 3;
6658
- if ((region.start + i) % 3 === k) {
6659
- drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (rowOffset + k), trnslWidthPx, self.sequenceRowHeight, seq, i, true);
6660
- }
6661
- }
6662
- }
6663
- }
6664
- }, { name: 'LinearApolloDisplayRenderSequence' }));
6665
- },
6666
- }));
6667
- }
6668
- function renderingModelFactory(pluginManager, configSchema) {
6669
- const LinearApolloDisplayRendering = sequenceRenderingModelFactory(pluginManager, configSchema);
6670
- return LinearApolloDisplayRendering.actions((self) => ({
6671
- afterAttach() {
6672
- mobxStateTree.addDisposer(self, mobx.autorun(() => {
6673
- const { canvas, featureLayouts, featuresHeight, lgv } = self;
6674
- if (!lgv.initialized || self.regionCannotBeRendered()) {
6675
- return;
6676
- }
6677
- const { displayedRegions, dynamicBlocks } = lgv;
6678
- const ctx = canvas?.getContext('2d');
6679
- if (!ctx) {
6680
- return;
6681
- }
6682
- ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight);
6683
- for (const [idx, featureLayout] of featureLayouts.entries()) {
6684
- const displayedRegion = displayedRegions[idx];
6685
- for (const [row, featureLayoutRow] of featureLayout.entries()) {
6686
- for (const [featureRow, feature] of featureLayoutRow) {
6687
- if (featureRow > 0) {
6688
- continue;
6689
- }
6690
- if (!util.doesIntersect2(displayedRegion.start, displayedRegion.end, feature.min, feature.max)) {
6691
- continue;
6692
- }
6693
- getGlyph(feature).draw(ctx, feature, row, self, idx);
6694
- }
6695
- }
6530
+ const Feature = mobxReact.observer(function Feature({ depth, feature, isHovered, isSelected, model: displayState, selectedFeatureClass, setContextMenu, }) {
6531
+ const { classes } = useStyles$4();
6532
+ const { apolloHover, changeManager, selectedFeature, session, tabularEditor: tabularEditorState, } = displayState;
6533
+ const { featureCollapsed, filterText } = tabularEditorState;
6534
+ const { _id, children, max, min, strand, type } = feature;
6535
+ const expanded = !featureCollapsed.get(_id);
6536
+ const toggleExpanded = (e) => {
6537
+ e.stopPropagation();
6538
+ tabularEditorState.setFeatureCollapsed(_id, expanded);
6539
+ };
6540
+ // pop up a snackbar in the session notifying user of an error
6541
+ const notifyError = (e) => {
6542
+ session.notify(e.message, 'error');
6543
+ };
6544
+ return (React__default["default"].createElement(React__default["default"].Fragment, null,
6545
+ React__default["default"].createElement("tr", { onMouseEnter: (_e) => {
6546
+ displayState.setApolloHover({
6547
+ feature,
6548
+ topLevelFeature: getTopLevelFeature(feature),
6549
+ glyph: displayState.getGlyph(getTopLevelFeature(feature)),
6550
+ });
6551
+ }, className: classes.feature +
6552
+ (isSelected
6553
+ ? ` ${selectedFeatureClass}`
6554
+ : isHovered
6555
+ ? ` ${classes.hoveredFeature}`
6556
+ : ''), onClick: (e) => {
6557
+ e.stopPropagation();
6558
+ displayState.setSelectedFeature(feature);
6559
+ }, onContextMenu: (e) => {
6560
+ e.preventDefault();
6561
+ setContextMenu({
6562
+ position: { left: e.clientX + 2, top: e.clientY - 6 },
6563
+ items: makeContextMenuItems(displayState, feature),
6564
+ });
6565
+ return false;
6566
+ } },
6567
+ React__default["default"].createElement("td", { style: {
6568
+ whiteSpace: 'nowrap',
6569
+ borderLeft: `${depth * 2}em solid transparent`,
6570
+ } },
6571
+ children?.size ? (
6572
+ // TODO: a11y
6573
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
6574
+ React__default["default"].createElement("div", { onClick: toggleExpanded, className: classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '') }, "\u276F")) : null,
6575
+ React__default["default"].createElement("div", { className: classes.typeContent },
6576
+ React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: 170 }, value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTypeTerms.bind(null, feature), renderInput: (params) => {
6577
+ return (React__default["default"].createElement("div", { ref: params.InputProps.ref },
6578
+ React__default["default"].createElement("input", { type: "text", ...params.inputProps, className: classes.typeInputElement, style: { width: 170 } }),
6579
+ params.error ? (React__default["default"].createElement("div", { className: classes.typeErrorMessage }, params.errorMessage ?? 'unknown error')) : null));
6580
+ }, onChange: (oldValue, newValue) => {
6581
+ if (newValue) {
6582
+ handleFeatureTypeChange(changeManager, feature, oldValue, newValue).catch(notifyError);
6583
+ }
6584
+ } }))),
6585
+ React__default["default"].createElement("td", null,
6586
+ React__default["default"].createElement(NumberCell, { initialValue: min + 1, notifyError: notifyError, onChangeCommitted: (newStart) => handleFeatureStartChange(changeManager, feature, min, newStart - 1) })),
6587
+ React__default["default"].createElement("td", null,
6588
+ React__default["default"].createElement(NumberCell, { initialValue: max, notifyError: notifyError, onChangeCommitted: (newEnd) => handleFeatureEndChange(changeManager, feature, max, newEnd) })),
6589
+ React__default["default"].createElement("td", null, strand === 1 ? '+' : strand === -1 ? '-' : undefined),
6590
+ React__default["default"].createElement("td", null,
6591
+ React__default["default"].createElement(FeatureAttributes, { filterText: filterText, feature: feature }))),
6592
+ expanded && children
6593
+ ? [...children.entries()]
6594
+ .filter((entry) => {
6595
+ if (!filterText) {
6596
+ return true;
6696
6597
  }
6697
- }, { name: 'LinearApolloDisplayRenderFeatures' }));
6698
- },
6699
- }));
6700
- }
6701
-
6702
- function isMousePositionWithFeatureAndGlyph(mousePosition) {
6703
- return 'featureAndGlyphUnderMouse' in mousePosition;
6704
- }
6705
- function getMousePosition(event, lgv) {
6706
- const canvas = event.currentTarget;
6707
- const { clientX, clientY } = event;
6708
- const { left, top } = canvas.getBoundingClientRect();
6709
- const x = clientX - left;
6710
- const y = clientY - top;
6711
- const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x);
6712
- return { x, y, refName, bp, regionNumber };
6713
- }
6714
- function getTranslationRow(frame, bpPerPx) {
6715
- const offset = bpPerPx <= 1 ? 2 : 0;
6716
- switch (frame) {
6717
- case 3: {
6718
- return 0;
6719
- }
6720
- case 2: {
6721
- return 1;
6722
- }
6723
- case 1: {
6724
- return 2;
6725
- }
6726
- case -1: {
6727
- return 3 + offset;
6728
- }
6729
- case -2: {
6730
- return 4 + offset;
6731
- }
6732
- case -3: {
6733
- return 5 + offset;
6598
+ const [, childFeature] = entry;
6599
+ // search feature and its subfeatures for the text
6600
+ const text = JSON.stringify(childFeature);
6601
+ return text.includes(filterText);
6602
+ })
6603
+ .map(([featureId, childFeature]) => {
6604
+ const childHovered = apolloHover?.feature._id === childFeature._id;
6605
+ const childSelected = selectedFeature?._id === childFeature._id;
6606
+ return (React__default["default"].createElement(Feature, { isHovered: childHovered, isSelected: childSelected, selectedFeatureClass: selectedFeatureClass, key: featureId, depth: (depth || 0) + 1, feature: childFeature, model: displayState, setContextMenu: setContextMenu }));
6607
+ })
6608
+ : null));
6609
+ });
6610
+ async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
6611
+ const { parent: parentFeature } = feature;
6612
+ if (parentFeature) {
6613
+ // if this is a child of an existing feature, restrict the autocomplete choices to valid
6614
+ // parts of that feature
6615
+ const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(parentFeature.type, { includeSubclasses: false });
6616
+ // eslint-disable-next-line unicorn/no-array-callback-reference
6617
+ const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass);
6618
+ if (parentTypeClassTerms.length > 0) {
6619
+ const subpartTerms = await ontologyStore.getClassesThat('part_of', parentTypeClassTerms);
6620
+ return subpartTerms;
6734
6621
  }
6735
6622
  }
6623
+ return;
6736
6624
  }
6737
- function getSeqRow(strand, bpPerPx) {
6738
- if (bpPerPx > 1 || strand === undefined) {
6739
- return;
6740
- }
6741
- return strand === 1 ? 3 : 4;
6625
+
6626
+ const useStyles$3 = mui.makeStyles()((theme) => ({
6627
+ scrollableTable: {
6628
+ width: '100%',
6629
+ height: '100%',
6630
+ th: {
6631
+ position: 'sticky',
6632
+ top: 0,
6633
+ zIndex: 2,
6634
+ textAlign: 'left',
6635
+ background: theme.palette.background.paper,
6636
+ paddingTop: '3.2em',
6637
+ },
6638
+ td: { whiteSpace: 'normal' },
6639
+ },
6640
+ selectedFeature: {
6641
+ backgroundColor: theme.palette.action.selected,
6642
+ },
6643
+ }));
6644
+ const HybridGrid = mobxReact.observer(function HybridGrid({ model, }) {
6645
+ const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model;
6646
+ const theme = material.useTheme();
6647
+ const { classes } = useStyles$3();
6648
+ const scrollContainerRef = React.useRef(null);
6649
+ const [contextMenu, setContextMenu] = React.useState(null);
6650
+ const { filterText } = tabularEditor;
6651
+ // scrolls to selected feature if one is selected and it's not already visible
6652
+ React.useEffect(() => {
6653
+ const scrollContainer = scrollContainerRef.current;
6654
+ if (scrollContainer && selectedFeature) {
6655
+ const selectedRow = scrollContainer.querySelector(`.${classes.selectedFeature}`);
6656
+ if (selectedRow) {
6657
+ const currScroll = scrollContainer.scrollTop;
6658
+ const newScrollTop = selectedRow.offsetTop - 25;
6659
+ const isVisible = newScrollTop > currScroll &&
6660
+ newScrollTop < currScroll + scrollContainer.offsetHeight;
6661
+ if (!isVisible) {
6662
+ scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' });
6663
+ }
6664
+ }
6665
+ }
6666
+ }, [selectedFeature, seenFeatures, classes.selectedFeature]);
6667
+ return (React__default["default"].createElement("div", { ref: scrollContainerRef, style: { width: '100%', overflowY: 'auto', height: '100%' } },
6668
+ React__default["default"].createElement("table", { className: classes.scrollableTable },
6669
+ React__default["default"].createElement("thead", null,
6670
+ React__default["default"].createElement("tr", null,
6671
+ React__default["default"].createElement("th", null, "Type"),
6672
+ React__default["default"].createElement("th", null, "Start"),
6673
+ React__default["default"].createElement("th", null, "End"),
6674
+ React__default["default"].createElement("th", null, "Strand"),
6675
+ React__default["default"].createElement("th", null, "Attributes"))),
6676
+ React__default["default"].createElement("tbody", null, [...seenFeatures.entries()]
6677
+ .filter((entry) => {
6678
+ if (!filterText) {
6679
+ return true;
6680
+ }
6681
+ const [, feature] = entry;
6682
+ // search feature and its subfeatures for the text
6683
+ const text = JSON.stringify(feature);
6684
+ return text.includes(filterText);
6685
+ })
6686
+ .sort((a, b) => {
6687
+ return a[1].min - b[1].min;
6688
+ })
6689
+ .map(([featureId, feature]) => {
6690
+ const isSelected = selectedFeature?._id === featureId;
6691
+ const isHovered = apolloHover?.feature._id === featureId;
6692
+ return (React__default["default"].createElement(Feature, { key: featureId, isSelected: isSelected, isHovered: isHovered, selectedFeatureClass: classes.selectedFeature, feature: feature, model: model, depth: 0, setContextMenu: setContextMenu }));
6693
+ }))),
6694
+ React__default["default"].createElement(ui.Menu, { open: Boolean(contextMenu), onMenuItemClick: (_, callback) => {
6695
+ callback();
6696
+ setContextMenu(null);
6697
+ }, onClose: () => {
6698
+ setContextMenu(null);
6699
+ }, TransitionProps: {
6700
+ onExit: () => {
6701
+ setContextMenu(null);
6702
+ },
6703
+ }, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenu?.items ?? [], anchorReference: "anchorPosition", anchorPosition: contextMenu?.position })));
6704
+ });
6705
+
6706
+ /* eslint-disable @typescript-eslint/unbound-method */
6707
+ const useStyles$2 = mui.makeStyles()({
6708
+ toolbar: {
6709
+ width: '100%',
6710
+ display: 'flex',
6711
+ paddingRight: '2em',
6712
+ flexDirection: 'row',
6713
+ justifyContent: 'space-between',
6714
+ position: 'absolute',
6715
+ zIndex: 4,
6716
+ },
6717
+ filterText: {},
6718
+ });
6719
+ const ToolBar = mobxReact.observer(function ToolBar({ model: displayState, }) {
6720
+ const model = displayState.tabularEditor;
6721
+ const { classes } = useStyles$2();
6722
+ return (React__default["default"].createElement("div", { className: classes.toolbar },
6723
+ React__default["default"].createElement(material.Tooltip, { title: "Collapse all" },
6724
+ React__default["default"].createElement(material.IconButton, { "aria-label": "collapse", sx: { marginTop: 0 }, onClick: model.collapseAllFeatures },
6725
+ React__default["default"].createElement(UnfoldLessIcon__default["default"], null))),
6726
+ React__default["default"].createElement(material.TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
6727
+ model.setFilterText(event.target.value);
6728
+ }, InputProps: {
6729
+ endAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "end" },
6730
+ React__default["default"].createElement(material.IconButton, { onClick: () => {
6731
+ model.clearFilterText();
6732
+ } },
6733
+ React__default["default"].createElement(ClearIcon__default["default"], null)))),
6734
+ } })));
6735
+ });
6736
+
6737
+ function stopPropagation(e) {
6738
+ e.stopPropagation();
6742
6739
  }
6743
- function highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx) {
6744
- if (row !== undefined) {
6745
- seqTrackOverlayctx.fillStyle =
6746
- theme?.palette.action.focus ?? 'rgba(0,0,0,0.04)';
6747
- seqTrackOverlayctx.fillRect(startPx, sequenceRowHeight * row, widthPx, sequenceRowHeight);
6740
+ const TabularEditorPane = mobxReact.observer(function TabularEditorPane({ model: displayState, }) {
6741
+ const model = displayState.tabularEditor;
6742
+ if (!model.isShown) {
6743
+ return null;
6748
6744
  }
6749
- }
6750
- function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
6751
- const LinearApolloDisplayRendering = renderingModelFactory(pluginManager, configSchema);
6752
- return LinearApolloDisplayRendering.named('LinearApolloDisplayMouseEvents')
6745
+ return (
6746
+ // TODO: a11y
6747
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
6748
+ React__default["default"].createElement("div", { onMouseDown: stopPropagation, onClick: stopPropagation, style: { width: '100%', height: '100%', position: 'relative' } },
6749
+ React__default["default"].createElement(ToolBar, { model: displayState }),
6750
+ React__default["default"].createElement(HybridGrid, { model: displayState })));
6751
+ });
6752
+
6753
+ const TabularEditorStateModelType = mobxStateTree.types
6754
+ .model('TabularEditor', {
6755
+ isShown: true,
6756
+ featureCollapsed: mobxStateTree.types.map(mobxStateTree.types.boolean),
6757
+ filterText: '',
6758
+ })
6759
+ .actions((self) => ({
6760
+ setFeatureCollapsed(id, state) {
6761
+ self.featureCollapsed.set(id, state);
6762
+ },
6763
+ setFilterText(text) {
6764
+ self.filterText = text;
6765
+ },
6766
+ clearFilterText() {
6767
+ self.filterText = '';
6768
+ },
6769
+ collapseAllFeatures() {
6770
+ // iterate over all seen features and set them to collapsed
6771
+ const display = mobxStateTree.getParent(self);
6772
+ for (const [featureId] of display.seenFeatures.entries()) {
6773
+ self.featureCollapsed.set(featureId, true);
6774
+ }
6775
+ },
6776
+ togglePane() {
6777
+ self.isShown = !self.isShown;
6778
+ },
6779
+ hidePane() {
6780
+ self.isShown = false;
6781
+ },
6782
+ showPane() {
6783
+ self.isShown = true;
6784
+ },
6785
+ // onPatch(patch: any) {
6786
+ // console.log(patch)
6787
+ // },
6788
+ }));
6789
+
6790
+ const FilterFeatures = mobxReact.observer(function FilterFeatures({ featureTypes, handleClose, onUpdate, session, }) {
6791
+ const [type, setType] = React.useState('');
6792
+ const [selectedFeatureTypes, setSelectedFeatureTypes] = React.useState(featureTypes);
6793
+ const handleChange = (value) => {
6794
+ setType(value);
6795
+ };
6796
+ const handleAddFeatureType = () => {
6797
+ if (type) {
6798
+ if (selectedFeatureTypes.includes(type)) {
6799
+ return;
6800
+ }
6801
+ onUpdate([...selectedFeatureTypes, type]);
6802
+ setSelectedFeatureTypes([...selectedFeatureTypes, type]);
6803
+ }
6804
+ };
6805
+ const handleFeatureTypeDelete = (value) => {
6806
+ const newTypes = selectedFeatureTypes.filter((type) => type !== value);
6807
+ onUpdate(newTypes);
6808
+ setSelectedFeatureTypes(newTypes);
6809
+ };
6810
+ return (React__default["default"].createElement(Dialog, { open: true, maxWidth: false, "data-testid": "filter-features-dialog", title: "Filter features by type", handleClose: handleClose },
6811
+ React__default["default"].createElement(material.DialogContent, null,
6812
+ React__default["default"].createElement(material.DialogContentText, null, "Select the feature types you want to display in the apollo track"),
6813
+ React__default["default"].createElement(material.Grid2, { container: true, spacing: 2 },
6814
+ React__default["default"].createElement(material.Grid2, null,
6815
+ React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: '100%' }, value: type, filterTerms: isOntologyClass, renderInput: (params) => (React__default["default"].createElement(material.TextField, { ...params, label: "Feature type", variant: "outlined", fullWidth: true })), onChange: (oldValue, newValue) => {
6816
+ if (newValue) {
6817
+ handleChange(newValue);
6818
+ }
6819
+ } })),
6820
+ React__default["default"].createElement(material.Grid2, null,
6821
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: handleAddFeatureType, disabled: !type, style: { marginTop: 9 }, size: "medium" }, "Add"))),
6822
+ selectedFeatureTypes.length > 0 && (React__default["default"].createElement("div", null,
6823
+ React__default["default"].createElement("hr", null),
6824
+ React__default["default"].createElement("div", { style: { width: 300 } },
6825
+ React__default["default"].createElement(material.DialogContentText, null, "Selected feature types:"),
6826
+ React__default["default"].createElement(material.Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 0.5 } }, selectedFeatureTypes.map((value) => (React__default["default"].createElement(material.Chip, { key: value, label: value, onDelete: () => {
6827
+ handleFeatureTypeDelete(value);
6828
+ } }))))))))));
6829
+ });
6830
+
6831
+ const minDisplayHeight = 20;
6832
+ function baseModelFactory(_pluginManager, configSchema) {
6833
+ return pluggableElementTypes.BaseDisplay.named('BaseLinearApolloDisplay')
6834
+ .props({
6835
+ type: mobxStateTree.types.literal('LinearApolloDisplay'),
6836
+ configuration: configuration.ConfigurationReference(configSchema),
6837
+ graphical: true,
6838
+ table: false,
6839
+ heightPreConfig: mobxStateTree.types.maybe(mobxStateTree.types.refinement('displayHeight', mobxStateTree.types.number, (n) => n >= minDisplayHeight)),
6840
+ filteredFeatureTypes: mobxStateTree.types.array(mobxStateTree.types.string),
6841
+ })
6842
+ .views((self) => {
6843
+ const { configuration, renderProps: superRenderProps } = self;
6844
+ return {
6845
+ renderProps() {
6846
+ return {
6847
+ ...superRenderProps(),
6848
+ ...tracks.getParentRenderProps(self),
6849
+ config: configuration.renderer,
6850
+ };
6851
+ },
6852
+ };
6853
+ })
6753
6854
  .volatile(() => ({
6754
- apolloDragging: null,
6755
- cursor: undefined,
6756
- apolloHover: undefined,
6855
+ scrollTop: 0,
6757
6856
  }))
6758
6857
  .views((self) => ({
6759
- getMousePosition(event) {
6760
- const mousePosition = getMousePosition(event, self.lgv);
6761
- const { bp, regionNumber, y } = mousePosition;
6762
- const row = Math.floor(y / self.apolloRowHeight);
6763
- const featureLayout = self.featureLayouts[regionNumber];
6764
- const layoutRow = featureLayout.get(row);
6765
- if (!layoutRow) {
6766
- return mousePosition;
6858
+ get lgv() {
6859
+ return util.getContainingView(self);
6860
+ },
6861
+ get height() {
6862
+ if (self.heightPreConfig) {
6863
+ return self.heightPreConfig;
6767
6864
  }
6768
- const foundFeature = layoutRow.find((f) => bp >= f[1].min && bp <= f[1].max);
6769
- if (!foundFeature) {
6770
- return mousePosition;
6865
+ if (self.graphical && self.table) {
6866
+ return 500;
6771
6867
  }
6772
- const [featureRow, topLevelFeature] = foundFeature;
6773
- const glyph = getGlyph(topLevelFeature);
6774
- const feature = glyph.getFeatureFromLayout(topLevelFeature, bp, featureRow);
6775
- if (!feature) {
6776
- return mousePosition;
6868
+ if (self.graphical) {
6869
+ return 200;
6777
6870
  }
6778
- return {
6779
- ...mousePosition,
6780
- featureAndGlyphUnderMouse: { feature, topLevelFeature, glyph },
6781
- };
6871
+ return 300;
6782
6872
  },
6783
6873
  }))
6784
- .actions((self) => ({
6785
- continueDrag(mousePosition, event) {
6786
- if (!self.apolloDragging) {
6787
- throw new Error('continueDrag() called with no current drag in progress');
6788
- }
6789
- event.stopPropagation();
6790
- self.apolloDragging = { ...self.apolloDragging, current: mousePosition };
6874
+ .views((self) => ({
6875
+ get rendererTypeName() {
6876
+ return self.configuration.renderer.type;
6791
6877
  },
6792
- setDragging(dragInfo) {
6793
- self.apolloDragging = dragInfo ?? null;
6878
+ get session() {
6879
+ return util.getSession(self);
6794
6880
  },
6795
- }))
6796
- .actions((self) => ({
6797
- setApolloHover(n) {
6798
- self.apolloHover = n;
6881
+ get regions() {
6882
+ const regions = self.lgv.dynamicBlocks.contentBlocks.map(({ assemblyName, end, refName, start }) => ({
6883
+ assemblyName,
6884
+ refName,
6885
+ start: Math.round(start),
6886
+ end: Math.round(end),
6887
+ }));
6888
+ return regions;
6799
6889
  },
6800
- setCursor(cursor) {
6801
- if (self.cursor !== cursor) {
6802
- self.cursor = cursor;
6890
+ regionCannotBeRendered( /* region */) {
6891
+ if (self.lgv && self.lgv.bpPerPx >= 200) {
6892
+ return 'Zoom in to see annotations';
6803
6893
  }
6894
+ return;
6804
6895
  },
6805
6896
  }))
6806
- .actions(() => ({
6807
- // onClick(event: CanvasMouseEvent) {
6808
- onClick() {
6809
- // TODO: set the selected feature
6897
+ .views((self) => ({
6898
+ get apolloInternetAccount() {
6899
+ const [region] = self.regions;
6900
+ const { internetAccounts } = mobxStateTree.getRoot(self);
6901
+ const { assemblyName } = region;
6902
+ const { assemblyManager } = self.session;
6903
+ const assembly = assemblyManager.get(assemblyName);
6904
+ if (!assembly) {
6905
+ throw new Error(`No assembly found with name ${assemblyName}`);
6906
+ }
6907
+ const { internetAccountConfigId } = configuration.getConf(assembly, [
6908
+ 'sequence',
6909
+ 'metadata',
6910
+ ]);
6911
+ return internetAccounts.find((ia) => configuration.getConf(ia, 'internetAccountId') === internetAccountConfigId);
6810
6912
  },
6811
- }));
6812
- }
6813
- function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
6814
- const LinearApolloDisplayRendering = mouseEventsModelIntermediateFactory(pluginManager, configSchema);
6815
- return LinearApolloDisplayRendering.actions((self) => ({
6816
- afterAttach() {
6817
- mobxStateTree.addDisposer(self, mobx.autorun(() => {
6818
- // This type is wrong in @jbrowse/core
6819
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6820
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6821
- return;
6822
- }
6823
- const seqTrackOverlayctx = self.seqTrackOverlayCanvas?.getContext('2d');
6824
- if (!seqTrackOverlayctx) {
6825
- return;
6826
- }
6827
- seqTrackOverlayctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
6828
- const { apolloHover, lgv, regions, sequenceRowHeight, theme } = self;
6829
- if (!apolloHover) {
6830
- return;
6831
- }
6832
- const { feature } = apolloHover;
6833
- for (const [idx, region] of regions.entries()) {
6834
- if (feature.type === 'CDS') {
6835
- const parentFeature = feature.parent;
6836
- if (!parentFeature) {
6837
- continue;
6838
- }
6839
- const cdsLocs = parentFeature.cdsLocations.find((loc) => feature.min === loc.at(0)?.min &&
6840
- feature.max === loc.at(-1)?.max);
6841
- if (!cdsLocs) {
6842
- continue;
6843
- }
6844
- for (const dl of cdsLocs) {
6845
- const frame = util.getFrame(dl.min, dl.max, feature.strand ?? 1, dl.phase);
6846
- const row = getTranslationRow(frame, lgv.bpPerPx);
6847
- const offset = (lgv.bpToPx({
6848
- refName: region.refName,
6849
- coord: dl.min,
6850
- regionNumber: idx,
6851
- })?.offsetPx ?? 0) - lgv.offsetPx;
6852
- const widthPx = (dl.max - dl.min) / lgv.bpPerPx;
6853
- const startPx = lgv.displayedRegions[idx].reversed
6854
- ? offset - widthPx
6855
- : offset;
6856
- highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
6857
- }
6858
- }
6859
- else {
6860
- const row = getSeqRow(feature.strand, lgv.bpPerPx);
6861
- const offset = (lgv.bpToPx({
6862
- refName: region.refName,
6863
- coord: feature.min,
6864
- regionNumber: idx,
6865
- })?.offsetPx ?? 0) - lgv.offsetPx;
6866
- const widthPx = feature.length / lgv.bpPerPx;
6867
- const startPx = lgv.displayedRegions[idx].reversed
6868
- ? offset - widthPx
6869
- : offset;
6870
- highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
6871
- }
6872
- }
6873
- }, { name: 'LinearApolloDisplayRenderSeqHighlight' }));
6913
+ get changeManager() {
6914
+ return self.session.apolloDataStore
6915
+ .changeManager;
6874
6916
  },
6875
- }));
6876
- }
6877
- function mouseEventsModelFactory(pluginManager, configSchema) {
6878
- const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
6879
- return LinearApolloDisplayMouseEvents.views((self) => ({
6880
- contextMenuItems(contextCoord) {
6881
- const { apolloHover } = self;
6882
- if (!(apolloHover && contextCoord)) {
6883
- return [];
6917
+ getAssemblyId(assemblyName) {
6918
+ const { assemblyManager } = self.session;
6919
+ const assembly = assemblyManager.get(assemblyName);
6920
+ if (!assembly) {
6921
+ throw new Error(`Could not find assembly named ${assemblyName}`);
6884
6922
  }
6885
- const { topLevelFeature } = apolloHover;
6886
- const glyph = getGlyph(topLevelFeature);
6887
- return glyph.getContextMenuItems(self);
6923
+ return assembly.name;
6924
+ },
6925
+ get selectedFeature() {
6926
+ return self.session
6927
+ .apolloSelectedFeature;
6888
6928
  },
6889
6929
  }))
6890
6930
  .actions((self) => ({
6891
- // explicitly pass in a feature in case it's not the same as the one in
6892
- // mousePosition (e.g. if features are drawn overlapping).
6893
- startDrag(mousePosition, feature, edge) {
6894
- self.apolloDragging = {
6895
- start: mousePosition,
6896
- current: mousePosition,
6897
- feature,
6898
- edge,
6899
- };
6900
- },
6901
- endDrag() {
6902
- if (!self.apolloDragging) {
6903
- throw new Error('endDrag() called with no current drag in progress');
6904
- }
6905
- const { current, edge, feature, start } = self.apolloDragging;
6906
- // don't do anything if it was only dragged a tiny bit
6907
- if (Math.abs(current.x - start.x) <= 4) {
6908
- self.setDragging();
6909
- self.setCursor();
6910
- return;
6911
- }
6912
- const { displayedRegions } = self.lgv;
6913
- const region = displayedRegions[start.regionNumber];
6914
- const assembly = self.getAssemblyId(region.assemblyName);
6915
- let change;
6916
- if (edge === 'max') {
6917
- const featureId = feature._id;
6918
- const oldEnd = feature.max;
6919
- const newEnd = current.bp;
6920
- change = new shared.LocationEndChange({
6921
- typeName: 'LocationEndChange',
6922
- changedIds: [featureId],
6923
- featureId,
6924
- oldEnd,
6925
- newEnd,
6926
- assembly,
6927
- });
6928
- }
6929
- else {
6930
- const featureId = feature._id;
6931
- const oldStart = feature.min;
6932
- const newStart = current.bp;
6933
- change = new shared.LocationStartChange({
6934
- typeName: 'LocationStartChange',
6935
- changedIds: [featureId],
6936
- featureId,
6937
- oldStart,
6938
- newStart,
6939
- assembly,
6940
- });
6941
- }
6942
- void self.changeManager.submit(change);
6943
- self.setDragging();
6944
- self.setCursor();
6931
+ setScrollTop(scrollTop) {
6932
+ self.scrollTop = scrollTop;
6945
6933
  },
6946
- }))
6947
- .actions((self) => ({
6948
- onMouseDown(event) {
6949
- const mousePosition = self.getMousePosition(event);
6950
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6951
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseDown(self, mousePosition, event);
6952
- }
6934
+ setHeight(displayHeight) {
6935
+ self.heightPreConfig = Math.max(displayHeight, minDisplayHeight);
6936
+ return self.height;
6953
6937
  },
6954
- onMouseMove(event) {
6955
- const mousePosition = self.getMousePosition(event);
6956
- if (self.apolloDragging) {
6957
- self.setCursor('col-resize');
6958
- self.continueDrag(mousePosition, event);
6959
- return;
6960
- }
6961
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6962
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseMove(self, mousePosition, event);
6963
- }
6964
- else {
6965
- self.setApolloHover();
6966
- self.setCursor();
6967
- }
6938
+ resizeHeight(distance) {
6939
+ const oldHeight = self.height;
6940
+ const newHeight = this.setHeight(self.height + distance);
6941
+ return newHeight - oldHeight;
6968
6942
  },
6969
- onMouseLeave(event) {
6970
- self.setDragging();
6971
- self.setApolloHover();
6972
- const mousePosition = self.getMousePosition(event);
6973
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6974
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseLeave(self, mousePosition, event);
6975
- }
6943
+ showGraphicalOnly() {
6944
+ self.graphical = true;
6945
+ self.table = false;
6976
6946
  },
6977
- onMouseUp(event) {
6978
- const mousePosition = self.getMousePosition(event);
6979
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6980
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
6981
- }
6982
- if (self.apolloDragging) {
6983
- self.endDrag();
6984
- }
6947
+ showTableOnly() {
6948
+ self.graphical = false;
6949
+ self.table = true;
6950
+ },
6951
+ showGraphicalAndTable() {
6952
+ self.graphical = true;
6953
+ self.table = true;
6954
+ },
6955
+ updateFilteredFeatureTypes(types) {
6956
+ self.filteredFeatureTypes = mobxStateTree.cast(types);
6985
6957
  },
6986
6958
  }))
6959
+ .views((self) => {
6960
+ const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self;
6961
+ return {
6962
+ trackMenuItems() {
6963
+ const { graphical, table } = self;
6964
+ return [
6965
+ ...superTrackMenuItems(),
6966
+ {
6967
+ type: 'subMenu',
6968
+ label: 'Appearance',
6969
+ subMenu: [
6970
+ {
6971
+ label: 'Show graphical display',
6972
+ type: 'radio',
6973
+ checked: graphical && !table,
6974
+ onClick: () => {
6975
+ self.showGraphicalOnly();
6976
+ },
6977
+ },
6978
+ {
6979
+ label: 'Show table display',
6980
+ type: 'radio',
6981
+ checked: table && !graphical,
6982
+ onClick: () => {
6983
+ self.showTableOnly();
6984
+ },
6985
+ },
6986
+ {
6987
+ label: 'Show both graphical and table display',
6988
+ type: 'radio',
6989
+ checked: table && graphical,
6990
+ onClick: () => {
6991
+ self.showGraphicalAndTable();
6992
+ },
6993
+ },
6994
+ ],
6995
+ },
6996
+ {
6997
+ label: 'Filter features by type',
6998
+ onClick: () => {
6999
+ const session = self.session;
7000
+ self.session.queueDialog((doneCallback) => [
7001
+ FilterFeatures,
7002
+ {
7003
+ session,
7004
+ handleClose: () => {
7005
+ doneCallback();
7006
+ },
7007
+ featureTypes: mobxStateTree.getSnapshot(filteredFeatureTypes),
7008
+ onUpdate: (types) => {
7009
+ self.updateFilteredFeatureTypes(types);
7010
+ },
7011
+ },
7012
+ ]);
7013
+ },
7014
+ },
7015
+ ];
7016
+ },
7017
+ };
7018
+ })
6987
7019
  .actions((self) => ({
7020
+ setSelectedFeature(feature) {
7021
+ self.session.apolloSetSelectedFeature(feature);
7022
+ },
6988
7023
  afterAttach() {
6989
7024
  mobxStateTree.addDisposer(self, mobx.autorun(() => {
6990
- // This type is wrong in @jbrowse/core
6991
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6992
7025
  if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6993
7026
  return;
6994
7027
  }
6995
- const ctx = self.overlayCanvas?.getContext('2d');
6996
- if (!ctx) {
6997
- return;
6998
- }
6999
- ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
7000
- const { apolloDragging, apolloHover } = self;
7001
- if (!apolloHover) {
7002
- return;
7003
- }
7004
- const { glyph } = apolloHover;
7005
- // draw mouseover hovers
7006
- glyph.drawHover(self, ctx);
7007
- // draw tooltip on hover
7008
- glyph.drawTooltip(self, ctx);
7009
- // dragging previews
7010
- if (apolloDragging) {
7011
- // NOTE: the glyph where the drag started is responsible for drawing the preview.
7012
- // it can call methods in other glyphs to help with this though.
7013
- const glyph = getGlyph(apolloDragging.feature.topLevelFeature);
7014
- glyph.drawDragPreview(self, ctx);
7028
+ void self.session.apolloDataStore.loadFeatures(self.regions);
7029
+ if (self.lgv.bpPerPx <= 3) {
7030
+ void self.session.apolloDataStore.loadRefSeq(self.regions);
7015
7031
  }
7016
- }, { name: 'LinearApolloDisplayRenderMouseoverAndDrag' }));
7032
+ }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
7017
7033
  },
7018
7034
  }));
7019
7035
  }
@@ -7282,7 +7298,12 @@ function getContextMenuItems$2(display) {
7282
7298
  session.showWidget(apolloFeatureWidget);
7283
7299
  },
7284
7300
  });
7285
- if (sourceFeature.type === 'mRNA' && util.isSessionModelWithWidgets(session)) {
7301
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
7302
+ if (!featureTypeOntology) {
7303
+ throw new Error('featureTypeOntology is undefined');
7304
+ }
7305
+ if (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
7306
+ util.isSessionModelWithWidgets(session)) {
7286
7307
  menuItems.push({
7287
7308
  label: 'Edit transcript details',
7288
7309
  onClick: () => {
@@ -7440,6 +7461,11 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7440
7461
  return;
7441
7462
  }
7442
7463
  const { apolloSelectedFeature } = session;
7464
+ const { apolloDataStore } = session;
7465
+ const { featureTypeOntology } = apolloDataStore.ontologyManager;
7466
+ if (!featureTypeOntology) {
7467
+ throw new Error('featureTypeOntology is undefined');
7468
+ }
7443
7469
  // Draw background for gene
7444
7470
  const topLevelFeatureMinX = (lgv.bpToPx({
7445
7471
  refName,
@@ -7451,22 +7477,23 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7451
7477
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
7452
7478
  : topLevelFeatureMinX;
7453
7479
  const topLevelFeatureTop = row * rowHeight;
7454
- const topLevelFeatureHeight = getRowCount$1(feature) * rowHeight;
7480
+ const topLevelFeatureHeight = getRowCount$1(feature, featureTypeOntology) * rowHeight;
7455
7481
  ctx.fillStyle = material.alpha(theme?.palette.background.paper ?? '#ffffff', 0.6);
7456
7482
  ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
7457
- // Draw lines on different rows for each mRNA
7483
+ // Draw lines on different rows for each transcript
7458
7484
  let currentRow = 0;
7459
- for (const [, mrna] of children) {
7460
- if (mrna.type !== 'mRNA') {
7485
+ for (const [, transcript] of children) {
7486
+ const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript');
7487
+ if (!isTranscript) {
7461
7488
  currentRow += 1;
7462
7489
  continue;
7463
7490
  }
7464
- const { children: childrenOfmRNA, min } = mrna;
7465
- if (!childrenOfmRNA) {
7491
+ const { children: childrenOfTranscript, min } = transcript;
7492
+ if (!childrenOfTranscript) {
7466
7493
  continue;
7467
7494
  }
7468
- for (const [, cds] of childrenOfmRNA) {
7469
- if (cds.type !== 'CDS') {
7495
+ for (const [, cds] of childrenOfTranscript) {
7496
+ if (!featureTypeOntology.isTypeOf(cds.type, 'CDS')) {
7470
7497
  continue;
7471
7498
  }
7472
7499
  const minX = (lgv.bpToPx({
@@ -7474,7 +7501,7 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7474
7501
  coord: min,
7475
7502
  regionNumber: displayedRegionIndex,
7476
7503
  })?.offsetPx ?? 0) - offsetPx;
7477
- const widthPx = mrna.length / bpPerPx;
7504
+ const widthPx = transcript.length / bpPerPx;
7478
7505
  const startPx = reversed ? minX - widthPx : minX;
7479
7506
  const height = Math.round((currentRow + 1 / 2) * rowHeight) + row * rowHeight;
7480
7507
  ctx.strokeStyle = theme?.palette.text.primary ?? 'black';
@@ -7487,21 +7514,21 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7487
7514
  }
7488
7515
  const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
7489
7516
  const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
7490
- // Draw exon and CDS for each mRNA
7517
+ // Draw exon and CDS for each transcript
7491
7518
  currentRow = 0;
7492
7519
  for (const [, child] of children) {
7493
- if (child.type !== 'mRNA') {
7520
+ if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
7494
7521
  boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex);
7495
7522
  currentRow += 1;
7496
7523
  continue;
7497
7524
  }
7498
7525
  for (const cdsRow of child.cdsLocations) {
7499
- const { _id, children: childrenOfmRNA } = child;
7500
- if (!childrenOfmRNA) {
7526
+ const { _id, children: childrenOfTranscript } = child;
7527
+ if (!childrenOfTranscript) {
7501
7528
  continue;
7502
7529
  }
7503
- for (const [, exon] of childrenOfmRNA) {
7504
- if (exon.type !== 'exon') {
7530
+ for (const [, exon] of childrenOfTranscript) {
7531
+ if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
7505
7532
  continue;
7506
7533
  }
7507
7534
  const minX = (lgv.bpToPx({
@@ -7597,7 +7624,8 @@ function drawDragPreview$1(stateModel, overlayCtx) {
7597
7624
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight);
7598
7625
  }
7599
7626
  function drawHover$1(stateModel, ctx) {
7600
- const { apolloHover, apolloRowHeight, lgv, theme } = stateModel;
7627
+ const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel;
7628
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
7601
7629
  if (!apolloHover) {
7602
7630
  return;
7603
7631
  }
@@ -7620,10 +7648,13 @@ function drawHover$1(stateModel, ctx) {
7620
7648
  const top = row * apolloRowHeight;
7621
7649
  const widthPx = length / bpPerPx;
7622
7650
  ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)';
7623
- ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature));
7651
+ if (!featureTypeOntology) {
7652
+ throw new Error('featureTypeOntology is undefined');
7653
+ }
7654
+ ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature, featureTypeOntology));
7624
7655
  }
7625
- function getFeatureFromLayout$1(feature, bp, row) {
7626
- const featureInThisRow = featuresForRow$1(feature)[row] || [];
7656
+ function getFeatureFromLayout$1(feature, bp, row, featureTypeOntology) {
7657
+ const featureInThisRow = featuresForRow$1(feature, featureTypeOntology)[row] || [];
7627
7658
  for (const f of featureInThisRow) {
7628
7659
  let featureObj;
7629
7660
  if (bp >= f.min && bp <= f.max && f.parent) {
@@ -7632,9 +7663,9 @@ function getFeatureFromLayout$1(feature, bp, row) {
7632
7663
  if (!featureObj) {
7633
7664
  continue;
7634
7665
  }
7635
- if (featureObj.type === 'CDS' &&
7666
+ if (featureTypeOntology.isTypeOf(featureObj.type, 'CDS') &&
7636
7667
  featureObj.parent &&
7637
- featureObj.parent.type === 'mRNA') {
7668
+ featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript')) {
7638
7669
  const { cdsLocations } = featureObj.parent;
7639
7670
  for (const cdsLoc of cdsLocations) {
7640
7671
  for (const loc of cdsLoc) {
@@ -7643,7 +7674,7 @@ function getFeatureFromLayout$1(feature, bp, row) {
7643
7674
  }
7644
7675
  }
7645
7676
  }
7646
- // If mouse position is in the intron region, return the mRNA
7677
+ // If mouse position is in the intron region, return the transcript
7647
7678
  return featureObj.parent;
7648
7679
  }
7649
7680
  // If mouse position is in a feature that is not a CDS, return the feature
@@ -7651,33 +7682,36 @@ function getFeatureFromLayout$1(feature, bp, row) {
7651
7682
  }
7652
7683
  return feature;
7653
7684
  }
7654
- function getRowCount$1(feature, _bpPerPx) {
7685
+ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
7655
7686
  const { children, type } = feature;
7656
7687
  if (!children) {
7657
7688
  return 1;
7658
7689
  }
7690
+ const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript');
7659
7691
  let rowCount = 0;
7660
- if (type === 'mRNA') {
7692
+ if (isTranscript) {
7661
7693
  for (const [, child] of children) {
7662
- if (child.type === 'CDS') {
7694
+ const isCds = featureTypeOntology.isTypeOf(child.type, 'CDS');
7695
+ if (isCds) {
7663
7696
  rowCount += 1;
7664
7697
  }
7665
7698
  }
7666
7699
  return rowCount;
7667
7700
  }
7668
7701
  for (const [, child] of children) {
7669
- rowCount += getRowCount$1(child);
7702
+ rowCount += getRowCount$1(child, featureTypeOntology);
7670
7703
  }
7671
7704
  return rowCount;
7672
7705
  }
7673
7706
  /**
7674
7707
  * A list of all the subfeatures for each row for a given feature, as well as
7675
7708
  * the feature itself.
7676
- * If the row contains an mRNA, the order is CDS -\> exon -\> mRNA -\> gene
7677
- * If the row does not contain an mRNA, the order is subfeature -\> gene
7709
+ * If the row contains a transcript, the order is CDS -\> exon -\> transcript -\> gene
7710
+ * If the row does not contain an transcript, the order is subfeature -\> gene
7678
7711
  */
7679
- function featuresForRow$1(feature) {
7680
- if (feature.type !== 'gene') {
7712
+ function featuresForRow$1(feature, featureTypeOntology) {
7713
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
7714
+ if (!isGene) {
7681
7715
  throw new Error('Top level feature for GeneGlyph must have type "gene"');
7682
7716
  }
7683
7717
  const { children } = feature;
@@ -7686,7 +7720,7 @@ function featuresForRow$1(feature) {
7686
7720
  }
7687
7721
  const features = [];
7688
7722
  for (const [, child] of children) {
7689
- if (child.type !== 'mRNA') {
7723
+ if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
7690
7724
  features.push([child, feature]);
7691
7725
  continue;
7692
7726
  }
@@ -7696,10 +7730,10 @@ function featuresForRow$1(feature) {
7696
7730
  const cdss = [];
7697
7731
  const exons = [];
7698
7732
  for (const [, grandchild] of child.children) {
7699
- if (grandchild.type === 'CDS') {
7733
+ if (featureTypeOntology.isTypeOf(grandchild.type, 'CDS')) {
7700
7734
  cdss.push(grandchild);
7701
7735
  }
7702
- else if (grandchild.type === 'exon') {
7736
+ else if (featureTypeOntology.isTypeOf(grandchild.type, 'exon')) {
7703
7737
  exons.push(grandchild);
7704
7738
  }
7705
7739
  }
@@ -7709,8 +7743,8 @@ function featuresForRow$1(feature) {
7709
7743
  }
7710
7744
  return features;
7711
7745
  }
7712
- function getRowForFeature$1(feature, childFeature) {
7713
- const rows = featuresForRow$1(feature);
7746
+ function getRowForFeature$1(feature, childFeature, featureTypeOntology) {
7747
+ const rows = featuresForRow$1(feature, featureTypeOntology);
7714
7748
  for (const [idx, row] of rows.entries()) {
7715
7749
  if (row.some((feature) => feature._id === childFeature._id)) {
7716
7750
  return idx;
@@ -7752,7 +7786,16 @@ function onMouseUp$1(stateModel, mousePosition) {
7752
7786
  }
7753
7787
  }
7754
7788
  function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7755
- if (feature.type === 'gene' || feature.type === 'mRNA') {
7789
+ const { session } = stateModel;
7790
+ const { apolloDataStore } = session;
7791
+ const { featureTypeOntology } = apolloDataStore.ontologyManager;
7792
+ if (!featureTypeOntology) {
7793
+ throw new Error('featureTypeOntology is undefined');
7794
+ }
7795
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
7796
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript');
7797
+ const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS');
7798
+ if (isGene || isTranscript) {
7756
7799
  return;
7757
7800
  }
7758
7801
  const { bp, refName, regionNumber, x } = mousePosition;
@@ -7763,517 +7806,1019 @@ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7763
7806
  if (minPxInfo === undefined || maxPxInfo === undefined) {
7764
7807
  return;
7765
7808
  }
7766
- const minPx = minPxInfo.offsetPx - offsetPx;
7767
- const maxPx = maxPxInfo.offsetPx - offsetPx;
7768
- if (Math.abs(maxPx - minPx) < 8) {
7809
+ const minPx = minPxInfo.offsetPx - offsetPx;
7810
+ const maxPx = maxPxInfo.offsetPx - offsetPx;
7811
+ if (Math.abs(maxPx - minPx) < 8) {
7812
+ return;
7813
+ }
7814
+ if (Math.abs(minPx - x) < 4) {
7815
+ return { feature, edge: 'min' };
7816
+ }
7817
+ if (Math.abs(maxPx - x) < 4) {
7818
+ return { feature, edge: 'max' };
7819
+ }
7820
+ if (isCds) {
7821
+ const transcript = feature.parent;
7822
+ if (!transcript?.children) {
7823
+ return;
7824
+ }
7825
+ const exonChildren = [];
7826
+ for (const child of transcript.children.values()) {
7827
+ const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon');
7828
+ if (childIsExon) {
7829
+ exonChildren.push(child);
7830
+ }
7831
+ }
7832
+ const overlappingExon = exonChildren.find((child) => {
7833
+ const [start, end] = util.intersection2(bp, bp + 1, child.min, child.max);
7834
+ return start !== undefined && end !== undefined;
7835
+ });
7836
+ if (!overlappingExon) {
7837
+ return;
7838
+ }
7839
+ const minPxInfo = lgv.bpToPx({
7840
+ refName,
7841
+ coord: overlappingExon.min,
7842
+ regionNumber,
7843
+ });
7844
+ const maxPxInfo = lgv.bpToPx({
7845
+ refName,
7846
+ coord: overlappingExon.max,
7847
+ regionNumber,
7848
+ });
7849
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
7850
+ return;
7851
+ }
7852
+ const minPx = minPxInfo.offsetPx - offsetPx;
7853
+ const maxPx = maxPxInfo.offsetPx - offsetPx;
7854
+ if (Math.abs(maxPx - minPx) < 8) {
7855
+ return;
7856
+ }
7857
+ if (Math.abs(minPx - x) < 4) {
7858
+ return { feature: overlappingExon, edge: 'min' };
7859
+ }
7860
+ if (Math.abs(maxPx - x) < 4) {
7861
+ return { feature: overlappingExon, edge: 'max' };
7862
+ }
7863
+ }
7864
+ return;
7865
+ }
7866
+ // False positive here, none of these functions use "this"
7867
+ /* eslint-disable @typescript-eslint/unbound-method */
7868
+ const { drawTooltip: drawTooltip$1, getContextMenuItems: getContextMenuItems$1, onMouseLeave: onMouseLeave$1 } = boxGlyph;
7869
+ /* eslint-enable @typescript-eslint/unbound-method */
7870
+ const geneGlyph = {
7871
+ draw: draw$1,
7872
+ drawDragPreview: drawDragPreview$1,
7873
+ drawHover: drawHover$1,
7874
+ drawTooltip: drawTooltip$1,
7875
+ getContextMenuItems: getContextMenuItems$1,
7876
+ getFeatureFromLayout: getFeatureFromLayout$1,
7877
+ getRowCount: getRowCount$1,
7878
+ getRowForFeature: getRowForFeature$1,
7879
+ onMouseDown: onMouseDown$1,
7880
+ onMouseLeave: onMouseLeave$1,
7881
+ onMouseMove: onMouseMove$1,
7882
+ onMouseUp: onMouseUp$1,
7883
+ };
7884
+
7885
+ function featuresForRow(feature) {
7886
+ const features = [[feature]];
7887
+ if (feature.children) {
7888
+ for (const [, child] of feature.children) {
7889
+ features.push(...featuresForRow(child));
7890
+ }
7891
+ }
7892
+ return features;
7893
+ }
7894
+ function getRowCount(feature) {
7895
+ return featuresForRow(feature).length;
7896
+ }
7897
+ function draw(ctx, feature, row, stateModel, displayedRegionIndex) {
7898
+ for (let i = 0; i < getRowCount(feature); i++) {
7899
+ drawRow(ctx, feature, row + i, row, stateModel, displayedRegionIndex);
7900
+ }
7901
+ }
7902
+ function drawRow(ctx, topLevelFeature, row, topRow, stateModel, displayedRegionIndex) {
7903
+ const features = featuresForRow(topLevelFeature)[row - topRow];
7904
+ for (const feature of features) {
7905
+ drawFeature(ctx, feature, row, stateModel, displayedRegionIndex);
7906
+ }
7907
+ }
7908
+ function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
7909
+ const { apolloRowHeight: heightPx, lgv, session } = stateModel;
7910
+ const { bpPerPx, displayedRegions, offsetPx } = lgv;
7911
+ const displayedRegion = displayedRegions[displayedRegionIndex];
7912
+ const minX = (lgv.bpToPx({
7913
+ refName: displayedRegion.refName,
7914
+ coord: feature.min,
7915
+ regionNumber: displayedRegionIndex,
7916
+ })?.offsetPx ?? 0) - offsetPx;
7917
+ const { reversed } = displayedRegion;
7918
+ const { apolloSelectedFeature } = session;
7919
+ const widthPx = feature.length / bpPerPx;
7920
+ const startPx = reversed ? minX - widthPx : minX;
7921
+ const top = row * heightPx;
7922
+ const rowCount = getRowCount(feature);
7923
+ const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
7924
+ const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
7925
+ if (rowCount > 1) {
7926
+ // draw background that encapsulates all child features
7927
+ const featureHeight = rowCount * heightPx;
7928
+ drawBox(ctx, startPx, top, widthPx, featureHeight, groupingColor);
7929
+ }
7930
+ boxGlyph.draw(ctx, feature, row, stateModel, displayedRegionIndex);
7931
+ }
7932
+ function drawHover(stateModel, ctx) {
7933
+ const { apolloHover, apolloRowHeight, lgv } = stateModel;
7934
+ if (!apolloHover) {
7935
+ return;
7936
+ }
7937
+ const { feature } = apolloHover;
7938
+ const position = stateModel.getFeatureLayoutPosition(feature);
7939
+ if (!position) {
7769
7940
  return;
7770
7941
  }
7771
- if (Math.abs(minPx - x) < 4) {
7772
- return { feature, edge: 'min' };
7773
- }
7774
- if (Math.abs(maxPx - x) < 4) {
7775
- return { feature, edge: 'max' };
7776
- }
7777
- if (feature.type === 'CDS') {
7778
- const mRNA = feature.parent;
7779
- if (!mRNA?.children) {
7780
- return;
7781
- }
7782
- const exonChildren = [...mRNA.children.values()].filter((child) => child.type === 'exon');
7783
- const overlappingExon = exonChildren.find((child) => {
7784
- const [start, end] = util.intersection2(bp, bp + 1, child.min, child.max);
7785
- return start !== undefined && end !== undefined;
7786
- });
7787
- if (!overlappingExon) {
7788
- return;
7789
- }
7790
- const minPxInfo = lgv.bpToPx({
7791
- refName,
7792
- coord: overlappingExon.min,
7793
- regionNumber,
7794
- });
7795
- const maxPxInfo = lgv.bpToPx({
7796
- refName,
7797
- coord: overlappingExon.max,
7798
- regionNumber,
7799
- });
7800
- if (minPxInfo === undefined || maxPxInfo === undefined) {
7801
- return;
7802
- }
7803
- const minPx = minPxInfo.offsetPx - offsetPx;
7804
- const maxPx = maxPxInfo.offsetPx - offsetPx;
7805
- if (Math.abs(maxPx - minPx) < 8) {
7806
- return;
7807
- }
7808
- if (Math.abs(minPx - x) < 4) {
7809
- return { feature: overlappingExon, edge: 'min' };
7810
- }
7811
- if (Math.abs(maxPx - x) < 4) {
7812
- return { feature: overlappingExon, edge: 'max' };
7942
+ const { featureRow, layoutIndex, layoutRow } = position;
7943
+ const { bpPerPx, displayedRegions, offsetPx } = lgv;
7944
+ const displayedRegion = displayedRegions[layoutIndex];
7945
+ const { refName, reversed } = displayedRegion;
7946
+ const { length, max, min } = feature;
7947
+ const startPx = (lgv.bpToPx({
7948
+ refName,
7949
+ coord: reversed ? max : min,
7950
+ regionNumber: layoutIndex,
7951
+ })?.offsetPx ?? 0) - offsetPx;
7952
+ const top = (layoutRow + featureRow) * apolloRowHeight;
7953
+ const widthPx = length / bpPerPx;
7954
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
7955
+ ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature));
7956
+ }
7957
+ function getFeatureFromLayout(feature, bp, row) {
7958
+ const layoutRow = featuresForRow(feature)[row];
7959
+ return layoutRow.find((f) => bp >= f.min && bp <= f.max);
7960
+ }
7961
+ function getRowForFeature(feature, childFeature) {
7962
+ const rows = featuresForRow(feature);
7963
+ for (const [idx, row] of rows.entries()) {
7964
+ if (row.some((feature) => feature._id === childFeature._id)) {
7965
+ return idx;
7813
7966
  }
7814
7967
  }
7815
7968
  return;
7816
7969
  }
7817
- // False positive here, none of these functions use "this"
7818
- /* eslint-disable @typescript-eslint/unbound-method */
7819
- const { drawTooltip: drawTooltip$1, getContextMenuItems: getContextMenuItems$1, onMouseLeave: onMouseLeave$1 } = boxGlyph;
7820
- /* eslint-enable @typescript-eslint/unbound-method */
7821
- const geneGlyph = {
7822
- draw: draw$1,
7823
- drawDragPreview: drawDragPreview$1,
7824
- drawHover: drawHover$1,
7825
- drawTooltip: drawTooltip$1,
7826
- getContextMenuItems: getContextMenuItems$1,
7827
- getFeatureFromLayout: getFeatureFromLayout$1,
7828
- getRowCount: getRowCount$1,
7829
- getRowForFeature: getRowForFeature$1,
7830
- onMouseDown: onMouseDown$1,
7831
- onMouseLeave: onMouseLeave$1,
7832
- onMouseMove: onMouseMove$1,
7833
- onMouseUp: onMouseUp$1,
7834
- };
7970
+ // False positive here, none of these functions use "this"
7971
+ /* eslint-disable @typescript-eslint/unbound-method */
7972
+ const { drawDragPreview, drawTooltip, getContextMenuItems, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, } = boxGlyph;
7973
+ /* eslint-enable @typescript-eslint/unbound-method */
7974
+ const genericChildGlyph = {
7975
+ draw,
7976
+ drawDragPreview,
7977
+ drawHover,
7978
+ drawTooltip,
7979
+ getContextMenuItems,
7980
+ getFeatureFromLayout,
7981
+ getRowCount,
7982
+ getRowForFeature,
7983
+ onMouseDown,
7984
+ onMouseLeave,
7985
+ onMouseMove,
7986
+ onMouseUp,
7987
+ };
7988
+
7989
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
7990
+ function layoutsModelFactory(pluginManager, configSchema) {
7991
+ const BaseLinearApolloDisplay = baseModelFactory(pluginManager, configSchema);
7992
+ return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
7993
+ .props({
7994
+ featuresMinMaxLimit: 500_000,
7995
+ })
7996
+ .volatile(() => ({
7997
+ seenFeatures: mobx.observable.map(),
7998
+ }))
7999
+ .views((self) => ({
8000
+ get featuresMinMax() {
8001
+ const { assemblyManager } = self.session;
8002
+ return self.lgv.displayedRegions.map((region) => {
8003
+ const assembly = assemblyManager.get(region.assemblyName);
8004
+ let min;
8005
+ let max;
8006
+ const { end, refName, start } = region;
8007
+ for (const [, feature] of self.seenFeatures) {
8008
+ if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
8009
+ !util.doesIntersect2(start, end, feature.min, feature.max) ||
8010
+ feature.length > self.featuresMinMaxLimit ||
8011
+ (self.filteredFeatureTypes.length > 0 &&
8012
+ !self.filteredFeatureTypes.includes(feature.type))) {
8013
+ continue;
8014
+ }
8015
+ if (min === undefined) {
8016
+ ({ min } = feature);
8017
+ }
8018
+ if (max === undefined) {
8019
+ ({ max } = feature);
8020
+ }
8021
+ if (feature.minWithChildren < min) {
8022
+ ({ min } = feature);
8023
+ }
8024
+ if (feature.maxWithChildren > max) {
8025
+ ({ max } = feature);
8026
+ }
8027
+ }
8028
+ if (min !== undefined && max !== undefined) {
8029
+ return [min, max];
8030
+ }
8031
+ return;
8032
+ });
8033
+ },
8034
+ getGlyph(feature) {
8035
+ if (this.looksLikeGene(feature)) {
8036
+ return geneGlyph;
8037
+ }
8038
+ if (feature.children?.size) {
8039
+ return genericChildGlyph;
8040
+ }
8041
+ return boxGlyph;
8042
+ },
8043
+ looksLikeGene(feature) {
8044
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8045
+ if (!featureTypeOntology) {
8046
+ return false;
8047
+ }
8048
+ const { children } = feature;
8049
+ if (!children?.size) {
8050
+ return false;
8051
+ }
8052
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
8053
+ if (!isGene) {
8054
+ return false;
8055
+ }
8056
+ for (const [, child] of children) {
8057
+ if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
8058
+ const { children: grandChildren } = child;
8059
+ if (!grandChildren?.size) {
8060
+ return false;
8061
+ }
8062
+ const hasCDS = [...grandChildren.values()].some((grandchild) => featureTypeOntology.isTypeOf(grandchild.type, 'CDS'));
8063
+ const hasExon = [...grandChildren.values()].some((grandchild) => featureTypeOntology.isTypeOf(grandchild.type, 'exon'));
8064
+ if (hasCDS && hasExon) {
8065
+ return true;
8066
+ }
8067
+ }
8068
+ }
8069
+ return false;
8070
+ },
8071
+ }))
8072
+ .actions((self) => ({
8073
+ addSeenFeature(feature) {
8074
+ self.seenFeatures.set(feature._id, feature);
8075
+ },
8076
+ deleteSeenFeature(featureId) {
8077
+ self.seenFeatures.delete(featureId);
8078
+ },
8079
+ }))
8080
+ .views((self) => ({
8081
+ get featureLayouts() {
8082
+ const { assemblyManager } = self.session;
8083
+ return self.lgv.displayedRegions.map((region, idx) => {
8084
+ const assembly = assemblyManager.get(region.assemblyName);
8085
+ const featureLayout = new Map();
8086
+ const minMax = self.featuresMinMax[idx];
8087
+ if (!minMax) {
8088
+ return featureLayout;
8089
+ }
8090
+ const [min, max] = minMax;
8091
+ const rows = [];
8092
+ const { end, refName, start } = region;
8093
+ for (const [id, feature] of self.seenFeatures.entries()) {
8094
+ if (!mobxStateTree.isAlive(feature)) {
8095
+ self.deleteSeenFeature(id);
8096
+ continue;
8097
+ }
8098
+ if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
8099
+ !util.doesIntersect2(start, end, feature.min, feature.max) ||
8100
+ (self.filteredFeatureTypes.length > 0 &&
8101
+ !self.filteredFeatureTypes.includes(feature.type))) {
8102
+ continue;
8103
+ }
8104
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8105
+ if (!featureTypeOntology) {
8106
+ throw new Error('featureTypeOntology is undefined');
8107
+ }
8108
+ const rowCount = self
8109
+ .getGlyph(feature)
8110
+ .getRowCount(feature, featureTypeOntology, self.lgv.bpPerPx);
8111
+ let startingRow = 0;
8112
+ let placed = false;
8113
+ while (!placed) {
8114
+ let rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
8115
+ if (rowsForFeature.length < rowCount) {
8116
+ for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
8117
+ const newRowNumber = rows.length;
8118
+ rows[newRowNumber] = Array.from({ length: max - min });
8119
+ featureLayout.set(newRowNumber, []);
8120
+ }
8121
+ rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
8122
+ }
8123
+ if (rowsForFeature
8124
+ .map((rowForFeature) => {
8125
+ // zero-length features are allowed in the spec
8126
+ const featureMax = feature.max - feature.min === 0
8127
+ ? feature.min + 1
8128
+ : feature.max;
8129
+ let start = feature.min - min, end = featureMax - min;
8130
+ if (feature.min - min < 0) {
8131
+ start = 0;
8132
+ end = featureMax - feature.min;
8133
+ }
8134
+ return rowForFeature.slice(start, end).some(Boolean);
8135
+ })
8136
+ .some(Boolean)) {
8137
+ startingRow += 1;
8138
+ continue;
8139
+ }
8140
+ for (let rowNum = startingRow; rowNum < startingRow + rowCount; rowNum++) {
8141
+ const row = rows[rowNum];
8142
+ let start = feature.min - min, end = feature.max - min;
8143
+ if (feature.min - min < 0) {
8144
+ start = 0;
8145
+ end = feature.max - feature.min;
8146
+ }
8147
+ row.fill(true, start, end);
8148
+ const layoutRow = featureLayout.get(rowNum);
8149
+ layoutRow?.push([rowNum - startingRow, feature]);
8150
+ }
8151
+ placed = true;
8152
+ }
8153
+ }
8154
+ return featureLayout;
8155
+ });
8156
+ },
8157
+ getFeatureLayoutPosition(feature) {
8158
+ const { featureLayouts } = this;
8159
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8160
+ for (const [idx, layout] of featureLayouts.entries()) {
8161
+ for (const [layoutRowNum, layoutRow] of layout) {
8162
+ for (const [featureRowNum, layoutFeature] of layoutRow) {
8163
+ if (featureRowNum !== 0) {
8164
+ // Same top-level feature in all feature rows, so only need to
8165
+ // check the first one
8166
+ continue;
8167
+ }
8168
+ if (feature._id === layoutFeature._id) {
8169
+ return {
8170
+ layoutIndex: idx,
8171
+ layoutRow: layoutRowNum,
8172
+ featureRow: featureRowNum,
8173
+ };
8174
+ }
8175
+ if (layoutFeature.hasDescendant(feature._id)) {
8176
+ if (!featureTypeOntology) {
8177
+ throw new Error('featureTypeOntology is undefined');
8178
+ }
8179
+ const row = self
8180
+ .getGlyph(layoutFeature)
8181
+ .getRowForFeature(layoutFeature, feature, featureTypeOntology);
8182
+ if (row !== undefined) {
8183
+ return {
8184
+ layoutIndex: idx,
8185
+ layoutRow: layoutRowNum,
8186
+ featureRow: row,
8187
+ };
8188
+ }
8189
+ }
8190
+ }
8191
+ }
8192
+ }
8193
+ return;
8194
+ },
8195
+ }))
8196
+ .views((self) => ({
8197
+ get highestRow() {
8198
+ return Math.max(0, ...self.featureLayouts.map((layout) => Math.max(...layout.keys())));
8199
+ },
8200
+ }))
8201
+ .actions((self) => ({
8202
+ afterAttach() {
8203
+ mobxStateTree.addDisposer(self, mobx.autorun(() => {
8204
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8205
+ return;
8206
+ }
8207
+ for (const region of self.regions) {
8208
+ const assembly = self.session.apolloDataStore.assemblies.get(region.assemblyName);
8209
+ const ref = assembly?.getByRefName(region.refName);
8210
+ const features = ref?.features;
8211
+ if (!features) {
8212
+ continue;
8213
+ }
8214
+ for (const [, feature] of features) {
8215
+ if (util.doesIntersect2(region.start, region.end, feature.min, feature.max) &&
8216
+ !self.seenFeatures.has(feature._id)) {
8217
+ self.addSeenFeature(feature);
8218
+ }
8219
+ }
8220
+ }
8221
+ }, { name: 'LinearApolloDisplaySetSeenFeatures', delay: 1000 }));
8222
+ },
8223
+ }));
8224
+ }
7835
8225
 
7836
- function featuresForRow(feature) {
7837
- const features = [[feature]];
7838
- if (feature.children) {
7839
- for (const [, child] of feature.children) {
7840
- features.push(...featuresForRow(child));
7841
- }
7842
- }
7843
- return features;
7844
- }
7845
- function getRowCount(feature) {
7846
- return featuresForRow(feature).length;
7847
- }
7848
- function draw(ctx, feature, row, stateModel, displayedRegionIndex) {
7849
- for (let i = 0; i < getRowCount(feature); i++) {
7850
- drawRow(ctx, feature, row + i, row, stateModel, displayedRegionIndex);
7851
- }
7852
- }
7853
- function drawRow(ctx, topLevelFeature, row, topRow, stateModel, displayedRegionIndex) {
7854
- const features = featuresForRow(topLevelFeature)[row - topRow];
7855
- for (const feature of features) {
7856
- drawFeature(ctx, feature, row, stateModel, displayedRegionIndex);
7857
- }
7858
- }
7859
- function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
7860
- const { apolloRowHeight: heightPx, lgv, session } = stateModel;
7861
- const { bpPerPx, displayedRegions, offsetPx } = lgv;
7862
- const displayedRegion = displayedRegions[displayedRegionIndex];
7863
- const minX = (lgv.bpToPx({
7864
- refName: displayedRegion.refName,
7865
- coord: feature.min,
7866
- regionNumber: displayedRegionIndex,
7867
- })?.offsetPx ?? 0) - offsetPx;
7868
- const { reversed } = displayedRegion;
7869
- const { apolloSelectedFeature } = session;
7870
- const widthPx = feature.length / bpPerPx;
7871
- const startPx = reversed ? minX - widthPx : minX;
7872
- const top = row * heightPx;
7873
- const rowCount = getRowCount(feature);
7874
- const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
7875
- const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
7876
- if (rowCount > 1) {
7877
- // draw background that encapsulates all child features
7878
- const featureHeight = rowCount * heightPx;
7879
- drawBox(ctx, startPx, top, widthPx, featureHeight, groupingColor);
7880
- }
7881
- boxGlyph.draw(ctx, feature, row, stateModel, displayedRegionIndex);
8226
+ function renderingModelIntermediateFactory(pluginManager, configSchema) {
8227
+ const LinearApolloDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
8228
+ return LinearApolloDisplayLayouts.named('LinearApolloDisplayRendering')
8229
+ .props({
8230
+ sequenceRowHeight: 15,
8231
+ apolloRowHeight: 20,
8232
+ detailsMinHeight: 200,
8233
+ detailsHeight: 200,
8234
+ lastRowTooltipBufferHeight: 40,
8235
+ isShown: true,
8236
+ })
8237
+ .volatile(() => ({
8238
+ canvas: null,
8239
+ overlayCanvas: null,
8240
+ collaboratorCanvas: null,
8241
+ seqTrackCanvas: null,
8242
+ seqTrackOverlayCanvas: null,
8243
+ theme: undefined,
8244
+ }))
8245
+ .views((self) => ({
8246
+ get featuresHeight() {
8247
+ return ((self.highestRow + 1) * self.apolloRowHeight +
8248
+ self.lastRowTooltipBufferHeight);
8249
+ },
8250
+ }))
8251
+ .actions((self) => ({
8252
+ toggleShown() {
8253
+ self.isShown = !self.isShown;
8254
+ },
8255
+ setDetailsHeight(newHeight) {
8256
+ self.detailsHeight = self.isShown
8257
+ ? Math.max(Math.min(newHeight, self.height - 100), Math.min(self.height, self.detailsMinHeight))
8258
+ : newHeight;
8259
+ },
8260
+ setCanvas(canvas) {
8261
+ self.canvas = canvas;
8262
+ },
8263
+ setOverlayCanvas(canvas) {
8264
+ self.overlayCanvas = canvas;
8265
+ },
8266
+ setCollaboratorCanvas(canvas) {
8267
+ self.collaboratorCanvas = canvas;
8268
+ },
8269
+ setSeqTrackCanvas(canvas) {
8270
+ self.seqTrackCanvas = canvas;
8271
+ },
8272
+ setSeqTrackOverlayCanvas(canvas) {
8273
+ self.seqTrackOverlayCanvas = canvas;
8274
+ },
8275
+ setTheme(theme) {
8276
+ self.theme = theme;
8277
+ },
8278
+ afterAttach() {
8279
+ mobxStateTree.addDisposer(self, mobx.autorun(() => {
8280
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8281
+ return;
8282
+ }
8283
+ const ctx = self.collaboratorCanvas?.getContext('2d');
8284
+ if (!ctx) {
8285
+ return;
8286
+ }
8287
+ ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
8288
+ for (const collaborator of self.session.collaborators) {
8289
+ const { locations } = collaborator;
8290
+ if (locations.length === 0) {
8291
+ continue;
8292
+ }
8293
+ let idx = 0;
8294
+ for (const displayedRegion of self.lgv.displayedRegions) {
8295
+ for (const location of locations) {
8296
+ if (location.refSeq !== displayedRegion.refName) {
8297
+ continue;
8298
+ }
8299
+ const { end, refSeq, start } = location;
8300
+ const locationStartPxInfo = self.lgv.bpToPx({
8301
+ refName: refSeq,
8302
+ coord: start,
8303
+ regionNumber: idx,
8304
+ });
8305
+ if (!locationStartPxInfo) {
8306
+ continue;
8307
+ }
8308
+ const locationStartPx = locationStartPxInfo.offsetPx - self.lgv.offsetPx;
8309
+ const locationWidthPx = (end - start) / self.lgv.bpPerPx;
8310
+ ctx.fillStyle = 'rgba(0,255,0,.2)';
8311
+ ctx.fillRect(locationStartPx, 1, locationWidthPx, 100);
8312
+ ctx.fillStyle = 'black';
8313
+ ctx.fillText(collaborator.name, locationStartPx + 1, 11, locationWidthPx - 2);
8314
+ }
8315
+ idx++;
8316
+ }
8317
+ }
8318
+ }, { name: 'LinearApolloDisplayRenderCollaborators' }));
8319
+ },
8320
+ }));
7882
8321
  }
7883
- function drawHover(stateModel, ctx) {
7884
- const { apolloHover, apolloRowHeight, lgv } = stateModel;
7885
- if (!apolloHover) {
7886
- return;
7887
- }
7888
- const { feature } = apolloHover;
7889
- const position = stateModel.getFeatureLayoutPosition(feature);
7890
- if (!position) {
7891
- return;
7892
- }
7893
- const { featureRow, layoutIndex, layoutRow } = position;
7894
- const { bpPerPx, displayedRegions, offsetPx } = lgv;
7895
- const displayedRegion = displayedRegions[layoutIndex];
7896
- const { refName, reversed } = displayedRegion;
7897
- const { length, max, min } = feature;
7898
- const startPx = (lgv.bpToPx({
7899
- refName,
7900
- coord: reversed ? max : min,
7901
- regionNumber: layoutIndex,
7902
- })?.offsetPx ?? 0) - offsetPx;
7903
- const top = (layoutRow + featureRow) * apolloRowHeight;
7904
- const widthPx = length / bpPerPx;
7905
- ctx.fillStyle = 'rgba(0,0,0,0.2)';
7906
- ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature));
8322
+ function colorCode(letter, theme) {
8323
+ return (theme?.palette.bases[letter.toUpperCase()].main.toString() ?? 'lightgray');
7907
8324
  }
7908
- function getFeatureFromLayout(feature, bp, row) {
7909
- const layoutRow = featuresForRow(feature)[row];
7910
- return layoutRow.find((f) => bp >= f.min && bp <= f.max);
8325
+ function codonColorCode(letter) {
8326
+ const colorMap = {
8327
+ M: '#33ee33',
8328
+ '*': '#f44336',
8329
+ };
8330
+ return colorMap[letter.toUpperCase()];
7911
8331
  }
7912
- function getRowForFeature(feature, childFeature) {
7913
- const rows = featuresForRow(feature);
7914
- for (const [idx, row] of rows.entries()) {
7915
- if (row.some((feature) => feature._id === childFeature._id)) {
7916
- return idx;
7917
- }
7918
- }
7919
- return;
8332
+ function reverseCodonSeq(seq) {
8333
+ return [...seq]
8334
+ .map((c) => util.revcom(c))
8335
+ .reverse()
8336
+ .join('');
7920
8337
  }
7921
- // False positive here, none of these functions use "this"
7922
- /* eslint-disable @typescript-eslint/unbound-method */
7923
- const { drawDragPreview, drawTooltip, getContextMenuItems, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, } = boxGlyph;
7924
- /* eslint-enable @typescript-eslint/unbound-method */
7925
- const genericChildGlyph = {
7926
- draw,
7927
- drawDragPreview,
7928
- drawHover,
7929
- drawTooltip,
7930
- getContextMenuItems,
7931
- getFeatureFromLayout,
7932
- getRowCount,
7933
- getRowForFeature,
7934
- onMouseDown,
7935
- onMouseLeave,
7936
- onMouseMove,
7937
- onMouseUp,
7938
- };
7939
-
7940
- /** get the appropriate glyph for the given top-level feature */
7941
- function getGlyph(feature) {
7942
- if (looksLikeGene(feature)) {
7943
- return geneGlyph;
8338
+ function drawLetter(seqTrackctx, startPx, widthPx, letter, textY) {
8339
+ const fontSize = Math.min(widthPx, 10);
8340
+ seqTrackctx.fillStyle = '#000';
8341
+ seqTrackctx.font = `${fontSize}px`;
8342
+ const textWidth = seqTrackctx.measureText(letter).width;
8343
+ const textX = startPx + (widthPx - textWidth) / 2;
8344
+ seqTrackctx.fillText(letter, textX, textY + 10);
8345
+ }
8346
+ function drawTranslation(seqTrackctx, bpPerPx, trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight, seq, i, reverse) {
8347
+ let codonSeq = seq.slice(i, i + 3).toUpperCase();
8348
+ if (reverse) {
8349
+ codonSeq = reverseCodonSeq(codonSeq);
7944
8350
  }
7945
- if (feature.children?.size) {
7946
- return genericChildGlyph;
8351
+ const codonLetter = util.defaultCodonTable[codonSeq];
8352
+ if (!codonLetter) {
8353
+ return;
7947
8354
  }
7948
- return boxGlyph;
7949
- }
7950
- function looksLikeGene(feature) {
7951
- const { children } = feature;
7952
- if (!children?.size) {
7953
- return false;
8355
+ const fillColor = codonColorCode(codonLetter);
8356
+ if (fillColor) {
8357
+ seqTrackctx.fillStyle = fillColor;
8358
+ seqTrackctx.fillRect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
7954
8359
  }
7955
- for (const [, child] of children) {
7956
- if (child.type === 'mRNA') {
7957
- const { children: grandChildren } = child;
7958
- if (!grandChildren?.size) {
7959
- return false;
7960
- }
7961
- const hasCDS = [...grandChildren.values()].some((grandchild) => grandchild.type === 'CDS');
7962
- const hasExon = [...grandChildren.values()].some((grandchild) => grandchild.type === 'exon');
7963
- if (hasCDS && hasExon) {
7964
- return true;
7965
- }
7966
- }
8360
+ if (bpPerPx <= 0.1) {
8361
+ seqTrackctx.rect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
8362
+ seqTrackctx.stroke();
8363
+ drawLetter(seqTrackctx, trnslStartPx, trnslWidthPx, codonLetter, trnslY);
7967
8364
  }
7968
- return false;
7969
8365
  }
7970
-
7971
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
7972
- const useStyles$4 = mui.makeStyles()((theme) => ({
7973
- typeContent: {
7974
- display: 'inline-block',
7975
- width: '174px',
7976
- height: '100%',
7977
- cursor: 'text',
7978
- },
7979
- feature: {
7980
- td: {
7981
- position: 'relative',
7982
- verticalAlign: 'top',
7983
- paddingLeft: '0.5em',
8366
+ function sequenceRenderingModelFactory(pluginManager, configSchema) {
8367
+ const LinearApolloDisplayRendering = renderingModelIntermediateFactory(pluginManager, configSchema);
8368
+ return LinearApolloDisplayRendering.actions((self) => ({
8369
+ afterAttach() {
8370
+ mobxStateTree.addDisposer(self, mobx.autorun(async () => {
8371
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8372
+ return;
8373
+ }
8374
+ if (self.lgv.bpPerPx > 3) {
8375
+ return;
8376
+ }
8377
+ const seqTrackctx = self.seqTrackCanvas?.getContext('2d');
8378
+ if (!seqTrackctx) {
8379
+ return;
8380
+ }
8381
+ seqTrackctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
8382
+ const frames = self.lgv.bpPerPx <= 1
8383
+ ? [3, 2, 1, 0, 0, -1, -2, -3]
8384
+ : [3, 2, 1, -1, -2, -3];
8385
+ let height = 0;
8386
+ for (const frame of frames) {
8387
+ const frameColor = self.theme?.palette.framesCDS.at(frame)?.main;
8388
+ if (frameColor) {
8389
+ seqTrackctx.fillStyle = frameColor;
8390
+ seqTrackctx.fillRect(0, height, self.lgv.dynamicBlocks.totalWidthPx, self.sequenceRowHeight);
8391
+ }
8392
+ height += self.sequenceRowHeight;
8393
+ }
8394
+ for (const [idx, region] of self.regions.entries()) {
8395
+ const driver = self.session.apolloDataStore.getBackendDriver(region.assemblyName);
8396
+ if (!driver) {
8397
+ throw new Error('Failed to get the backend driver');
8398
+ }
8399
+ const { seq } = await driver.getSequence(region);
8400
+ if (!seq) {
8401
+ return;
8402
+ }
8403
+ for (const [i, letter] of [...seq].entries()) {
8404
+ const trnslXOffset = (self.lgv.bpToPx({
8405
+ refName: region.refName,
8406
+ coord: region.start + i,
8407
+ regionNumber: idx,
8408
+ })?.offsetPx ?? 0) - self.lgv.offsetPx;
8409
+ const trnslWidthPx = 3 / self.lgv.bpPerPx;
8410
+ const trnslStartPx = self.lgv.displayedRegions[idx].reversed
8411
+ ? trnslXOffset - trnslWidthPx
8412
+ : trnslXOffset;
8413
+ // Draw translation forward
8414
+ for (let j = 2; j >= 0; j--) {
8415
+ if ((region.start + i) % 3 === j) {
8416
+ drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (2 - j), trnslWidthPx, self.sequenceRowHeight, seq, i, false);
8417
+ }
8418
+ }
8419
+ if (self.lgv.bpPerPx <= 1) {
8420
+ const xOffset = (self.lgv.bpToPx({
8421
+ refName: region.refName,
8422
+ coord: region.start + i,
8423
+ regionNumber: idx,
8424
+ })?.offsetPx ?? 0) - self.lgv.offsetPx;
8425
+ const widthPx = 1 / self.lgv.bpPerPx;
8426
+ const startPx = self.lgv.displayedRegions[idx].reversed
8427
+ ? xOffset - widthPx
8428
+ : xOffset;
8429
+ // Draw forward
8430
+ seqTrackctx.beginPath();
8431
+ seqTrackctx.fillStyle = colorCode(letter, self.theme);
8432
+ seqTrackctx.rect(startPx, self.sequenceRowHeight * 3, widthPx, self.sequenceRowHeight);
8433
+ seqTrackctx.fill();
8434
+ if (self.lgv.bpPerPx <= 0.1) {
8435
+ seqTrackctx.stroke();
8436
+ drawLetter(seqTrackctx, startPx, widthPx, letter, self.sequenceRowHeight * 3);
8437
+ }
8438
+ // Draw reverse
8439
+ const revLetter = util.revcom(letter);
8440
+ seqTrackctx.beginPath();
8441
+ seqTrackctx.fillStyle = colorCode(revLetter, self.theme);
8442
+ seqTrackctx.rect(startPx, self.sequenceRowHeight * 4, widthPx, self.sequenceRowHeight);
8443
+ seqTrackctx.fill();
8444
+ if (self.lgv.bpPerPx <= 0.1) {
8445
+ seqTrackctx.stroke();
8446
+ drawLetter(seqTrackctx, startPx, widthPx, revLetter, self.sequenceRowHeight * 4);
8447
+ }
8448
+ }
8449
+ // Draw translation reverse
8450
+ for (let k = 0; k <= 2; k++) {
8451
+ const rowOffset = self.lgv.bpPerPx <= 1 ? 5 : 3;
8452
+ if ((region.start + i) % 3 === k) {
8453
+ drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (rowOffset + k), trnslWidthPx, self.sequenceRowHeight, seq, i, true);
8454
+ }
8455
+ }
8456
+ }
8457
+ }
8458
+ }, { name: 'LinearApolloDisplayRenderSequence' }));
7984
8459
  },
7985
- },
7986
- arrow: {
7987
- display: 'inline-block',
7988
- width: '1.6em',
7989
- textAlign: 'center',
7990
- cursor: 'pointer',
7991
- },
7992
- arrowExpanded: {
7993
- transform: 'rotate(90deg)',
7994
- },
7995
- hoveredFeature: {
7996
- backgroundColor: theme.palette.action.hover,
7997
- },
7998
- typeInputElement: {
7999
- border: 'none',
8000
- background: 'none',
8001
- },
8002
- typeErrorMessage: {
8003
- color: 'red',
8004
- },
8005
- }));
8006
- function makeContextMenuItems(display, feature) {
8007
- const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
8008
- return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
8009
- }
8010
- function getTopLevelFeature(feature) {
8011
- let cur = feature;
8012
- while (cur.parent) {
8013
- cur = cur.parent;
8014
- }
8015
- return cur;
8460
+ }));
8016
8461
  }
8017
- const Feature = mobxReact.observer(function Feature({ depth, feature, isHovered, isSelected, model: displayState, selectedFeatureClass, setContextMenu, }) {
8018
- const { classes } = useStyles$4();
8019
- const { apolloHover, changeManager, selectedFeature, session, tabularEditor: tabularEditorState, } = displayState;
8020
- const { featureCollapsed, filterText } = tabularEditorState;
8021
- const { _id, children, max, min, strand, type } = feature;
8022
- const expanded = !featureCollapsed.get(_id);
8023
- const toggleExpanded = (e) => {
8024
- e.stopPropagation();
8025
- tabularEditorState.setFeatureCollapsed(_id, expanded);
8026
- };
8027
- // pop up a snackbar in the session notifying user of an error
8028
- const notifyError = (e) => {
8029
- session.notify(e.message, 'error');
8030
- };
8031
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
8032
- React__default["default"].createElement("tr", { onMouseEnter: (_e) => {
8033
- displayState.setApolloHover({
8034
- feature,
8035
- topLevelFeature: getTopLevelFeature(feature),
8036
- glyph: getGlyph(getTopLevelFeature(feature)),
8037
- });
8038
- }, className: classes.feature +
8039
- (isSelected
8040
- ? ` ${selectedFeatureClass}`
8041
- : isHovered
8042
- ? ` ${classes.hoveredFeature}`
8043
- : ''), onClick: (e) => {
8044
- e.stopPropagation();
8045
- displayState.setSelectedFeature(feature);
8046
- }, onContextMenu: (e) => {
8047
- e.preventDefault();
8048
- setContextMenu({
8049
- position: { left: e.clientX + 2, top: e.clientY - 6 },
8050
- items: makeContextMenuItems(displayState, feature),
8051
- });
8052
- return false;
8053
- } },
8054
- React__default["default"].createElement("td", { style: {
8055
- whiteSpace: 'nowrap',
8056
- borderLeft: `${depth * 2}em solid transparent`,
8057
- } },
8058
- children?.size ? (
8059
- // TODO: a11y
8060
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8061
- React__default["default"].createElement("div", { onClick: toggleExpanded, className: classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '') }, "\u276F")) : null,
8062
- React__default["default"].createElement("div", { className: classes.typeContent },
8063
- React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: 170 }, value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTypeTerms.bind(null, feature), renderInput: (params) => {
8064
- return (React__default["default"].createElement("div", { ref: params.InputProps.ref },
8065
- React__default["default"].createElement("input", { type: "text", ...params.inputProps, className: classes.typeInputElement, style: { width: 170 } }),
8066
- params.error ? (React__default["default"].createElement("div", { className: classes.typeErrorMessage }, params.errorMessage ?? 'unknown error')) : null));
8067
- }, onChange: (oldValue, newValue) => {
8068
- if (newValue) {
8069
- handleFeatureTypeChange(changeManager, feature, oldValue, newValue).catch(notifyError);
8462
+ function renderingModelFactory(pluginManager, configSchema) {
8463
+ const LinearApolloDisplayRendering = sequenceRenderingModelFactory(pluginManager, configSchema);
8464
+ return LinearApolloDisplayRendering.actions((self) => ({
8465
+ afterAttach() {
8466
+ mobxStateTree.addDisposer(self, mobx.autorun(() => {
8467
+ const { canvas, featureLayouts, featuresHeight, lgv } = self;
8468
+ if (!lgv.initialized || self.regionCannotBeRendered()) {
8469
+ return;
8470
+ }
8471
+ const { displayedRegions, dynamicBlocks } = lgv;
8472
+ const ctx = canvas?.getContext('2d');
8473
+ if (!ctx) {
8474
+ return;
8475
+ }
8476
+ ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight);
8477
+ for (const [idx, featureLayout] of featureLayouts.entries()) {
8478
+ const displayedRegion = displayedRegions[idx];
8479
+ for (const [row, featureLayoutRow] of featureLayout.entries()) {
8480
+ for (const [featureRow, feature] of featureLayoutRow) {
8481
+ if (featureRow > 0) {
8482
+ continue;
8070
8483
  }
8071
- } }))),
8072
- React__default["default"].createElement("td", null,
8073
- React__default["default"].createElement(NumberCell, { initialValue: min + 1, notifyError: notifyError, onChangeCommitted: (newStart) => handleFeatureStartChange(changeManager, feature, min, newStart - 1) })),
8074
- React__default["default"].createElement("td", null,
8075
- React__default["default"].createElement(NumberCell, { initialValue: max, notifyError: notifyError, onChangeCommitted: (newEnd) => handleFeatureEndChange(changeManager, feature, max, newEnd) })),
8076
- React__default["default"].createElement("td", null, strand === 1 ? '+' : strand === -1 ? '-' : undefined),
8077
- React__default["default"].createElement("td", null,
8078
- React__default["default"].createElement(FeatureAttributes, { filterText: filterText, feature: feature }))),
8079
- expanded && children
8080
- ? [...children.entries()]
8081
- .filter((entry) => {
8082
- if (!filterText) {
8083
- return true;
8484
+ if (!util.doesIntersect2(displayedRegion.start, displayedRegion.end, feature.min, feature.max)) {
8485
+ continue;
8486
+ }
8487
+ self.getGlyph(feature).draw(ctx, feature, row, self, idx);
8488
+ }
8489
+ }
8084
8490
  }
8085
- const [, childFeature] = entry;
8086
- // search feature and its subfeatures for the text
8087
- const text = JSON.stringify(childFeature);
8088
- return text.includes(filterText);
8089
- })
8090
- .map(([featureId, childFeature]) => {
8091
- const childHovered = apolloHover?.feature._id === childFeature._id;
8092
- const childSelected = selectedFeature?._id === childFeature._id;
8093
- return (React__default["default"].createElement(Feature, { isHovered: childHovered, isSelected: childSelected, selectedFeatureClass: selectedFeatureClass, key: featureId, depth: (depth || 0) + 1, feature: childFeature, model: displayState, setContextMenu: setContextMenu }));
8094
- })
8095
- : null));
8096
- });
8097
- async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
8098
- const { parent: parentFeature } = feature;
8099
- if (parentFeature) {
8100
- // if this is a child of an existing feature, restrict the autocomplete choices to valid
8101
- // parts of that feature
8102
- const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(parentFeature.type, { includeSubclasses: false });
8103
- // eslint-disable-next-line unicorn/no-array-callback-reference
8104
- const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass);
8105
- if (parentTypeClassTerms.length > 0) {
8106
- const subpartTerms = await ontologyStore.getClassesThat('part_of', parentTypeClassTerms);
8107
- return subpartTerms;
8491
+ }, { name: 'LinearApolloDisplayRenderFeatures' }));
8492
+ },
8493
+ }));
8494
+ }
8495
+
8496
+ function isMousePositionWithFeatureAndGlyph(mousePosition) {
8497
+ return 'featureAndGlyphUnderMouse' in mousePosition;
8498
+ }
8499
+ function getMousePosition(event, lgv) {
8500
+ const canvas = event.currentTarget;
8501
+ const { clientX, clientY } = event;
8502
+ const { left, top } = canvas.getBoundingClientRect();
8503
+ const x = clientX - left;
8504
+ const y = clientY - top;
8505
+ const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x);
8506
+ return { x, y, refName, bp, regionNumber };
8507
+ }
8508
+ function getTranslationRow(frame, bpPerPx) {
8509
+ const offset = bpPerPx <= 1 ? 2 : 0;
8510
+ switch (frame) {
8511
+ case 3: {
8512
+ return 0;
8513
+ }
8514
+ case 2: {
8515
+ return 1;
8516
+ }
8517
+ case 1: {
8518
+ return 2;
8519
+ }
8520
+ case -1: {
8521
+ return 3 + offset;
8522
+ }
8523
+ case -2: {
8524
+ return 4 + offset;
8525
+ }
8526
+ case -3: {
8527
+ return 5 + offset;
8108
8528
  }
8109
8529
  }
8110
- return;
8111
8530
  }
8112
-
8113
- /* eslint-disable @typescript-eslint/no-unsafe-call */
8114
- const useStyles$3 = mui.makeStyles()((theme) => ({
8115
- scrollableTable: {
8116
- width: '100%',
8117
- height: '100%',
8118
- th: {
8119
- position: 'sticky',
8120
- top: 0,
8121
- zIndex: 2,
8122
- textAlign: 'left',
8123
- background: theme.palette.background.paper,
8124
- paddingTop: '3.2em',
8531
+ function getSeqRow(strand, bpPerPx) {
8532
+ if (bpPerPx > 1 || strand === undefined) {
8533
+ return;
8534
+ }
8535
+ return strand === 1 ? 3 : 4;
8536
+ }
8537
+ function highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx) {
8538
+ if (row !== undefined) {
8539
+ seqTrackOverlayctx.fillStyle =
8540
+ theme?.palette.action.focus ?? 'rgba(0,0,0,0.04)';
8541
+ seqTrackOverlayctx.fillRect(startPx, sequenceRowHeight * row, widthPx, sequenceRowHeight);
8542
+ }
8543
+ }
8544
+ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
8545
+ const LinearApolloDisplayRendering = renderingModelFactory(pluginManager, configSchema);
8546
+ return LinearApolloDisplayRendering.named('LinearApolloDisplayMouseEvents')
8547
+ .volatile(() => ({
8548
+ apolloDragging: null,
8549
+ cursor: undefined,
8550
+ apolloHover: undefined,
8551
+ }))
8552
+ .views((self) => ({
8553
+ getMousePosition(event) {
8554
+ const mousePosition = getMousePosition(event, self.lgv);
8555
+ const { bp, regionNumber, y } = mousePosition;
8556
+ const row = Math.floor(y / self.apolloRowHeight);
8557
+ const featureLayout = self.featureLayouts[regionNumber];
8558
+ const layoutRow = featureLayout.get(row);
8559
+ if (!layoutRow) {
8560
+ return mousePosition;
8561
+ }
8562
+ const foundFeature = layoutRow.find((f) => bp >= f[1].min && bp <= f[1].max);
8563
+ if (!foundFeature) {
8564
+ return mousePosition;
8565
+ }
8566
+ const [featureRow, topLevelFeature] = foundFeature;
8567
+ const glyph = self.getGlyph(topLevelFeature);
8568
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8569
+ if (!featureTypeOntology) {
8570
+ throw new Error('featureTypeOntology is undefined');
8571
+ }
8572
+ const feature = glyph.getFeatureFromLayout(topLevelFeature, bp, featureRow, featureTypeOntology);
8573
+ if (!feature) {
8574
+ return mousePosition;
8575
+ }
8576
+ return {
8577
+ ...mousePosition,
8578
+ featureAndGlyphUnderMouse: { feature, topLevelFeature, glyph },
8579
+ };
8580
+ },
8581
+ }))
8582
+ .actions((self) => ({
8583
+ continueDrag(mousePosition, event) {
8584
+ if (!self.apolloDragging) {
8585
+ throw new Error('continueDrag() called with no current drag in progress');
8586
+ }
8587
+ event.stopPropagation();
8588
+ self.apolloDragging = { ...self.apolloDragging, current: mousePosition };
8589
+ },
8590
+ setDragging(dragInfo) {
8591
+ self.apolloDragging = dragInfo ?? null;
8592
+ },
8593
+ }))
8594
+ .actions((self) => ({
8595
+ setApolloHover(n) {
8596
+ self.apolloHover = n;
8597
+ },
8598
+ setCursor(cursor) {
8599
+ if (self.cursor !== cursor) {
8600
+ self.cursor = cursor;
8601
+ }
8602
+ },
8603
+ }))
8604
+ .actions(() => ({
8605
+ // onClick(event: CanvasMouseEvent) {
8606
+ onClick() {
8607
+ // TODO: set the selected feature
8608
+ },
8609
+ }));
8610
+ }
8611
+ function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
8612
+ const LinearApolloDisplayRendering = mouseEventsModelIntermediateFactory(pluginManager, configSchema);
8613
+ return LinearApolloDisplayRendering.actions((self) => ({
8614
+ afterAttach() {
8615
+ mobxStateTree.addDisposer(self, mobx.autorun(() => {
8616
+ // This type is wrong in @jbrowse/core
8617
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
8618
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8619
+ return;
8620
+ }
8621
+ const seqTrackOverlayctx = self.seqTrackOverlayCanvas?.getContext('2d');
8622
+ if (!seqTrackOverlayctx) {
8623
+ return;
8624
+ }
8625
+ seqTrackOverlayctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
8626
+ const { apolloHover, lgv, regions, sequenceRowHeight, session, theme, } = self;
8627
+ if (!apolloHover) {
8628
+ return;
8629
+ }
8630
+ const { feature } = apolloHover;
8631
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
8632
+ if (!featureTypeOntology) {
8633
+ throw new Error('featureTypeOntology is undefined');
8634
+ }
8635
+ for (const [idx, region] of regions.entries()) {
8636
+ if (featureTypeOntology.isTypeOf(feature.type, 'CDS')) {
8637
+ const parentFeature = feature.parent;
8638
+ if (!parentFeature) {
8639
+ continue;
8640
+ }
8641
+ const cdsLocs = parentFeature.cdsLocations.find((loc) => feature.min === loc.at(0)?.min &&
8642
+ feature.max === loc.at(-1)?.max);
8643
+ if (!cdsLocs) {
8644
+ continue;
8645
+ }
8646
+ for (const dl of cdsLocs) {
8647
+ const frame = util.getFrame(dl.min, dl.max, feature.strand ?? 1, dl.phase);
8648
+ const row = getTranslationRow(frame, lgv.bpPerPx);
8649
+ const offset = (lgv.bpToPx({
8650
+ refName: region.refName,
8651
+ coord: dl.min,
8652
+ regionNumber: idx,
8653
+ })?.offsetPx ?? 0) - lgv.offsetPx;
8654
+ const widthPx = (dl.max - dl.min) / lgv.bpPerPx;
8655
+ const startPx = lgv.displayedRegions[idx].reversed
8656
+ ? offset - widthPx
8657
+ : offset;
8658
+ highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
8659
+ }
8660
+ }
8661
+ else {
8662
+ const row = getSeqRow(feature.strand, lgv.bpPerPx);
8663
+ const offset = (lgv.bpToPx({
8664
+ refName: region.refName,
8665
+ coord: feature.min,
8666
+ regionNumber: idx,
8667
+ })?.offsetPx ?? 0) - lgv.offsetPx;
8668
+ const widthPx = feature.length / lgv.bpPerPx;
8669
+ const startPx = lgv.displayedRegions[idx].reversed
8670
+ ? offset - widthPx
8671
+ : offset;
8672
+ highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
8673
+ }
8674
+ }
8675
+ }, { name: 'LinearApolloDisplayRenderSeqHighlight' }));
8676
+ },
8677
+ }));
8678
+ }
8679
+ function mouseEventsModelFactory(pluginManager, configSchema) {
8680
+ const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
8681
+ return LinearApolloDisplayMouseEvents.views((self) => ({
8682
+ contextMenuItems(contextCoord) {
8683
+ const { apolloHover } = self;
8684
+ if (!(apolloHover && contextCoord)) {
8685
+ return [];
8686
+ }
8687
+ const { topLevelFeature } = apolloHover;
8688
+ const glyph = self.getGlyph(topLevelFeature);
8689
+ return glyph.getContextMenuItems(self);
8690
+ },
8691
+ }))
8692
+ .actions((self) => ({
8693
+ // explicitly pass in a feature in case it's not the same as the one in
8694
+ // mousePosition (e.g. if features are drawn overlapping).
8695
+ startDrag(mousePosition, feature, edge) {
8696
+ self.apolloDragging = {
8697
+ start: mousePosition,
8698
+ current: mousePosition,
8699
+ feature,
8700
+ edge,
8701
+ };
8702
+ },
8703
+ endDrag() {
8704
+ if (!self.apolloDragging) {
8705
+ throw new Error('endDrag() called with no current drag in progress');
8706
+ }
8707
+ const { current, edge, feature, start } = self.apolloDragging;
8708
+ // don't do anything if it was only dragged a tiny bit
8709
+ if (Math.abs(current.x - start.x) <= 4) {
8710
+ self.setDragging();
8711
+ self.setCursor();
8712
+ return;
8713
+ }
8714
+ const { displayedRegions } = self.lgv;
8715
+ const region = displayedRegions[start.regionNumber];
8716
+ const assembly = self.getAssemblyId(region.assemblyName);
8717
+ let change;
8718
+ if (edge === 'max') {
8719
+ const featureId = feature._id;
8720
+ const oldEnd = feature.max;
8721
+ const newEnd = current.bp;
8722
+ change = new shared.LocationEndChange({
8723
+ typeName: 'LocationEndChange',
8724
+ changedIds: [featureId],
8725
+ featureId,
8726
+ oldEnd,
8727
+ newEnd,
8728
+ assembly,
8729
+ });
8730
+ }
8731
+ else {
8732
+ const featureId = feature._id;
8733
+ const oldStart = feature.min;
8734
+ const newStart = current.bp;
8735
+ change = new shared.LocationStartChange({
8736
+ typeName: 'LocationStartChange',
8737
+ changedIds: [featureId],
8738
+ featureId,
8739
+ oldStart,
8740
+ newStart,
8741
+ assembly,
8742
+ });
8743
+ }
8744
+ void self.changeManager.submit(change);
8745
+ self.setDragging();
8746
+ self.setCursor();
8125
8747
  },
8126
- td: { whiteSpace: 'normal' },
8127
- },
8128
- selectedFeature: {
8129
- backgroundColor: theme.palette.action.selected,
8130
- },
8131
- }));
8132
- const HybridGrid = mobxReact.observer(function HybridGrid({ model, }) {
8133
- const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model;
8134
- const theme = material.useTheme();
8135
- const { classes } = useStyles$3();
8136
- const scrollContainerRef = React.useRef(null);
8137
- const [contextMenu, setContextMenu] = React.useState(null);
8138
- const { filterText } = tabularEditor;
8139
- // scrolls to selected feature if one is selected and it's not already visible
8140
- React.useEffect(() => {
8141
- const scrollContainer = scrollContainerRef.current;
8142
- if (scrollContainer && selectedFeature) {
8143
- const selectedRow = scrollContainer.querySelector(`.${classes.selectedFeature}`);
8144
- if (selectedRow) {
8145
- const currScroll = scrollContainer.scrollTop;
8146
- const newScrollTop = selectedRow.offsetTop - 25;
8147
- const isVisible = newScrollTop > currScroll &&
8148
- newScrollTop < currScroll + scrollContainer.offsetHeight;
8149
- if (!isVisible) {
8150
- scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' });
8151
- }
8748
+ }))
8749
+ .actions((self) => ({
8750
+ onMouseDown(event) {
8751
+ const mousePosition = self.getMousePosition(event);
8752
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8753
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseDown(self, mousePosition, event);
8152
8754
  }
8153
- }
8154
- }, [selectedFeature, seenFeatures, classes.selectedFeature]);
8155
- return (React__default["default"].createElement("div", { ref: scrollContainerRef, style: { width: '100%', overflowY: 'auto', height: '100%' } },
8156
- React__default["default"].createElement("table", { className: classes.scrollableTable },
8157
- React__default["default"].createElement("thead", null,
8158
- React__default["default"].createElement("tr", null,
8159
- React__default["default"].createElement("th", null, "Type"),
8160
- React__default["default"].createElement("th", null, "Start"),
8161
- React__default["default"].createElement("th", null, "End"),
8162
- React__default["default"].createElement("th", null, "Strand"),
8163
- React__default["default"].createElement("th", null, "Attributes"))),
8164
- React__default["default"].createElement("tbody", null, [...seenFeatures.entries()]
8165
- .filter((entry) => {
8166
- if (!filterText) {
8167
- return true;
8755
+ },
8756
+ onMouseMove(event) {
8757
+ const mousePosition = self.getMousePosition(event);
8758
+ if (self.apolloDragging) {
8759
+ self.setCursor('col-resize');
8760
+ self.continueDrag(mousePosition, event);
8761
+ return;
8762
+ }
8763
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8764
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseMove(self, mousePosition, event);
8765
+ }
8766
+ else {
8767
+ self.setApolloHover();
8768
+ self.setCursor();
8769
+ }
8770
+ },
8771
+ onMouseLeave(event) {
8772
+ self.setDragging();
8773
+ self.setApolloHover();
8774
+ const mousePosition = self.getMousePosition(event);
8775
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8776
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseLeave(self, mousePosition, event);
8777
+ }
8778
+ },
8779
+ onMouseUp(event) {
8780
+ const mousePosition = self.getMousePosition(event);
8781
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8782
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
8783
+ }
8784
+ if (self.apolloDragging) {
8785
+ self.endDrag();
8786
+ }
8787
+ },
8788
+ }))
8789
+ .actions((self) => ({
8790
+ afterAttach() {
8791
+ mobxStateTree.addDisposer(self, mobx.autorun(() => {
8792
+ // This type is wrong in @jbrowse/core
8793
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
8794
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8795
+ return;
8168
8796
  }
8169
- const [, feature] = entry;
8170
- // search feature and its subfeatures for the text
8171
- const text = JSON.stringify(feature);
8172
- return text.includes(filterText);
8173
- })
8174
- .sort((a, b) => {
8175
- return a[1].min - b[1].min;
8176
- })
8177
- .map(([featureId, feature]) => {
8178
- const isSelected = selectedFeature?._id === featureId;
8179
- const isHovered = apolloHover?.feature._id === featureId;
8180
- return (React__default["default"].createElement(Feature, { key: featureId, isSelected: isSelected, isHovered: isHovered, selectedFeatureClass: classes.selectedFeature, feature: feature, model: model, depth: 0, setContextMenu: setContextMenu }));
8181
- }))),
8182
- React__default["default"].createElement(ui.Menu, { open: Boolean(contextMenu), onMenuItemClick: (_, callback) => {
8183
- callback();
8184
- setContextMenu(null);
8185
- }, onClose: () => {
8186
- setContextMenu(null);
8187
- }, TransitionProps: {
8188
- onExit: () => {
8189
- setContextMenu(null);
8190
- },
8191
- }, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenu?.items ?? [], anchorReference: "anchorPosition", anchorPosition: contextMenu?.position })));
8192
- });
8193
-
8194
- /* eslint-disable @typescript-eslint/unbound-method */
8195
- const useStyles$2 = mui.makeStyles()({
8196
- toolbar: {
8197
- width: '100%',
8198
- display: 'flex',
8199
- paddingRight: '2em',
8200
- flexDirection: 'row',
8201
- justifyContent: 'space-between',
8202
- position: 'absolute',
8203
- zIndex: 4,
8204
- },
8205
- filterText: {},
8206
- });
8207
- const ToolBar = mobxReact.observer(function ToolBar({ model: displayState, }) {
8208
- const model = displayState.tabularEditor;
8209
- const { classes } = useStyles$2();
8210
- return (React__default["default"].createElement("div", { className: classes.toolbar },
8211
- React__default["default"].createElement(material.Tooltip, { title: "Collapse all" },
8212
- React__default["default"].createElement(material.IconButton, { "aria-label": "collapse", sx: { marginTop: 0 }, onClick: model.collapseAllFeatures },
8213
- React__default["default"].createElement(UnfoldLessIcon__default["default"], null))),
8214
- React__default["default"].createElement(material.TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
8215
- model.setFilterText(event.target.value);
8216
- }, InputProps: {
8217
- endAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "end" },
8218
- React__default["default"].createElement(material.IconButton, { onClick: () => {
8219
- model.clearFilterText();
8220
- } },
8221
- React__default["default"].createElement(ClearIcon__default["default"], null)))),
8222
- } })));
8223
- });
8224
-
8225
- function stopPropagation(e) {
8226
- e.stopPropagation();
8797
+ const ctx = self.overlayCanvas?.getContext('2d');
8798
+ if (!ctx) {
8799
+ return;
8800
+ }
8801
+ ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
8802
+ const { apolloDragging, apolloHover } = self;
8803
+ if (!apolloHover) {
8804
+ return;
8805
+ }
8806
+ const { glyph } = apolloHover;
8807
+ // draw mouseover hovers
8808
+ glyph.drawHover(self, ctx);
8809
+ // draw tooltip on hover
8810
+ glyph.drawTooltip(self, ctx);
8811
+ // dragging previews
8812
+ if (apolloDragging) {
8813
+ // NOTE: the glyph where the drag started is responsible for drawing the preview.
8814
+ // it can call methods in other glyphs to help with this though.
8815
+ const glyph = self.getGlyph(apolloDragging.feature.topLevelFeature);
8816
+ glyph.drawDragPreview(self, ctx);
8817
+ }
8818
+ }, { name: 'LinearApolloDisplayRenderMouseoverAndDrag' }));
8819
+ },
8820
+ }));
8227
8821
  }
8228
- const TabularEditorPane = mobxReact.observer(function TabularEditorPane({ model: displayState, }) {
8229
- const model = displayState.tabularEditor;
8230
- if (!model.isShown) {
8231
- return null;
8232
- }
8233
- return (
8234
- // TODO: a11y
8235
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8236
- React__default["default"].createElement("div", { onMouseDown: stopPropagation, onClick: stopPropagation, style: { width: '100%', height: '100%', position: 'relative' } },
8237
- React__default["default"].createElement(ToolBar, { model: displayState }),
8238
- React__default["default"].createElement(HybridGrid, { model: displayState })));
8239
- });
8240
-
8241
- const TabularEditorStateModelType = mobxStateTree.types
8242
- .model('TabularEditor', {
8243
- isShown: true,
8244
- featureCollapsed: mobxStateTree.types.map(mobxStateTree.types.boolean),
8245
- filterText: '',
8246
- })
8247
- .actions((self) => ({
8248
- setFeatureCollapsed(id, state) {
8249
- self.featureCollapsed.set(id, state);
8250
- },
8251
- setFilterText(text) {
8252
- self.filterText = text;
8253
- },
8254
- clearFilterText() {
8255
- self.filterText = '';
8256
- },
8257
- collapseAllFeatures() {
8258
- // iterate over all seen features and set them to collapsed
8259
- const display = mobxStateTree.getParent(self);
8260
- for (const [featureId] of display.seenFeatures.entries()) {
8261
- self.featureCollapsed.set(featureId, true);
8262
- }
8263
- },
8264
- togglePane() {
8265
- self.isShown = !self.isShown;
8266
- },
8267
- hidePane() {
8268
- self.isShown = false;
8269
- },
8270
- showPane() {
8271
- self.isShown = true;
8272
- },
8273
- // onPatch(patch: any) {
8274
- // console.log(patch)
8275
- // },
8276
- }));
8277
8822
 
8278
8823
  function stateModelFactory$1(pluginManager, configSchema) {
8279
8824
  // TODO: this needs to be refactored so that the final composition of the
@@ -8283,7 +8828,7 @@ function stateModelFactory$1(pluginManager, configSchema) {
8283
8828
  .named('LinearApolloDisplay');
8284
8829
  }
8285
8830
 
8286
- /* eslint-disable @typescript-eslint/no-unsafe-call */
8831
+ /* eslint-disable @typescript-eslint/unbound-method */
8287
8832
  const useStyles$1 = mui.makeStyles()((theme) => ({
8288
8833
  canvasContainer: {
8289
8834
  position: 'relative',
@@ -9319,8 +9864,34 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9319
9864
  configuration.readConfObject(ont, 'textIndexFields'),
9320
9865
  ];
9321
9866
  if (!ontologyManager.findOntology(name)) {
9867
+ const session = util.getSession(self);
9868
+ const { jobsManager } = session;
9869
+ const controller = new AbortController();
9870
+ const jobName = `Loading ontology "${name}"`;
9871
+ const job = {
9872
+ name: jobName,
9873
+ statusMessage: `Loading ontology "${name}", version "${version}", this may take a while`,
9874
+ progressPct: 0,
9875
+ cancelCallback: () => {
9876
+ controller.abort();
9877
+ jobsManager.abortJob(job.name);
9878
+ },
9879
+ };
9880
+ const update = (message, progress) => {
9881
+ if (progress === 0) {
9882
+ jobsManager.runJob(job);
9883
+ return;
9884
+ }
9885
+ if (progress === 100) {
9886
+ jobsManager.done(job);
9887
+ return;
9888
+ }
9889
+ jobsManager.update(jobName, message, progress);
9890
+ return;
9891
+ };
9322
9892
  ontologyManager.addOntology(name, version, source, {
9323
9893
  textIndexing: { indexFields },
9894
+ update,
9324
9895
  });
9325
9896
  }
9326
9897
  }
@@ -10015,6 +10586,10 @@ function stateModelFactory(pluginManager, configSchema) {
10015
10586
  return codonLayout;
10016
10587
  },
10017
10588
  get featureLayout() {
10589
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
10590
+ if (!featureTypeOntology) {
10591
+ throw new Error('featureTypeOntology is undefined');
10592
+ }
10018
10593
  const featureLayout = new Map();
10019
10594
  for (const [refSeq, featuresForRefSeq] of this.features || []) {
10020
10595
  if (!featuresForRefSeq) {
@@ -10038,11 +10613,11 @@ function stateModelFactory(pluginManager, configSchema) {
10038
10613
  return start1 - start2 || end1 - end2;
10039
10614
  })) {
10040
10615
  for (const [, childFeature] of feature.children ?? new Map()) {
10041
- if (childFeature.type === 'mRNA') {
10616
+ if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript')) {
10042
10617
  for (const [, grandChildFeature] of childFeature.children ||
10043
10618
  new Map()) {
10044
10619
  let startingRow;
10045
- if (grandChildFeature.type === 'CDS') {
10620
+ if (featureTypeOntology.isTypeOf(grandChildFeature.type, 'CDS')) {
10046
10621
  let discontinuousLocations;
10047
10622
  if (grandChildFeature.discontinuousLocations.length > 0) {
10048
10623
  ({ discontinuousLocations } = grandChildFeature);
@@ -10231,7 +10806,7 @@ function installApolloRefNameAliasAdapter(pluginManager) {
10231
10806
  }));
10232
10807
  }
10233
10808
 
10234
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
10809
+ /* eslint-disable @typescript-eslint/unbound-method */
10235
10810
  function isApolloMessageData(data) {
10236
10811
  return (typeof data === 'object' &&
10237
10812
  data !== null &&
@@ -10365,6 +10940,7 @@ class ApolloPlugin extends Plugin__default["default"] {
10365
10940
  return pluggableElement;
10366
10941
  });
10367
10942
  pluginManager.addToExtensionPoint('Core-extendPluggableElement', annotationFromPileup);
10943
+ pluginManager.addToExtensionPoint('Core-extendPluggableElement', annotationFromJBrowseFeature);
10368
10944
  if (!inWebWorker) {
10369
10945
  pluginManager.addToExtensionPoint('Core-extendWorker', (handle) => {
10370
10946
  if (!('on' in handle && handle.on)) {