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

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
package/dist/index.esm.js CHANGED
@@ -3,14 +3,14 @@ import { gff3ToAnnotationFeature, AddAssemblyFromExternalChange, AddAssemblyAndF
3
3
  import { ConfigurationSchema, readConfObject, getConf, ConfigurationReference } from '@jbrowse/core/configuration';
4
4
  import { BaseInternetAccountConfig, InternetAccount, RendererType, TextSearchAdapterType, BaseDisplay, WidgetType, createBaseTrackConfig, TrackType, createBaseTrackModel, InternetAccountType, DisplayType } from '@jbrowse/core/pluggableElementTypes';
5
5
  import Plugin from '@jbrowse/core/Plugin';
6
- import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getSession, getContainingView, getFrame, revcom, isSessionModelWithWidgets, doesIntersect2, defaultCodonTable, intersection2, reverse, defaultStarts, defaultStops } from '@jbrowse/core/util';
6
+ import { isUriLocation, isLocalPathLocation, isElectron, isAbstractMenuManager, getSession, getContainingView, getFrame, revcom, defaultCodonTable, isSessionModelWithWidgets, intersection2, doesIntersect2, reverse, defaultStarts, defaultStops } from '@jbrowse/core/util';
7
7
  import AddIcon from '@mui/icons-material/Add';
8
8
  import { autorun, toJS, observable } from 'mobx';
9
- import { getSnapshot, getParent, getRoot, types, addDisposer, flow, isAlive, resolveIdentifier, getParentOfType, applySnapshot } from 'mobx-state-tree';
9
+ import { getSnapshot, getParent, getRoot, types, addDisposer, flow, cast, isAlive, resolveIdentifier, getParentOfType, applySnapshot } from 'mobx-state-tree';
10
10
  import { io } from 'socket.io-client';
11
11
  import gff from '@gmod/gff';
12
12
  import LinkIcon from '@mui/icons-material/Link';
13
- import { DialogTitle, IconButton, DialogContent, DialogContentText, Select, MenuItem, TextField, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio, Box, Typography, FormGroup, Checkbox, DialogActions, Button, Autocomplete, InputLabel, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Grid, Tooltip, Chip, useTheme, FormHelperText, SvgIcon, Divider, Menu, alpha, InputAdornment as InputAdornment$1, Alert, Avatar } from '@mui/material';
13
+ import { DialogTitle, IconButton, DialogContent, DialogContentText, Select, MenuItem, TextField, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio, Box, Typography, FormGroup, Checkbox, DialogActions, Button, Autocomplete, InputLabel, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Grid2, Tooltip, Chip, useTheme, FormHelperText, SvgIcon, Divider, Menu, InputAdornment as InputAdornment$1, alpha, Alert, Avatar } from '@mui/material';
14
14
  import InputAdornment from '@mui/material/InputAdornment';
15
15
  import LinearProgress from '@mui/material/LinearProgress';
16
16
  import ObjectID from 'bson-objectid';
@@ -42,15 +42,15 @@ import { ObservableCreate } from '@jbrowse/core/util/rxjs';
42
42
  import SimpleFeature from '@jbrowse/core/util/simpleFeature';
43
43
  import BaseResult from '@jbrowse/core/TextSearch/BaseResults';
44
44
  import { AnnotationFeatureModel, ApolloAssembly, CheckResult, ApolloRefSeq } from '@apollo-annotation/mst';
45
- import { getParentRenderProps } from '@jbrowse/core/util/tracks';
46
45
  import ClearIcon from '@mui/icons-material/Clear';
47
46
  import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
47
+ import { getParentRenderProps } from '@jbrowse/core/util/tracks';
48
48
  import ExpandLessIcon from '@mui/icons-material/ExpandLess';
49
49
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
50
50
  import ErrorIcon from '@mui/icons-material/Error';
51
51
  import SaveIcon from '@mui/icons-material/Save';
52
52
 
53
- var version = "0.3.1";
53
+ var version = "0.3.3";
54
54
 
55
55
  const ApolloConfigSchema = ConfigurationSchema('ApolloInternetAccount', {
56
56
  baseURL: {
@@ -830,6 +830,8 @@ function serializeWords(foundWords) {
830
830
  /** load a OBO Graph JSON file into a database */
831
831
  async function loadOboGraphJson(db) {
832
832
  const startTime = Date.now();
833
+ let percentProgress = 1;
834
+ this.options.update?.('Parsing JSON', percentProgress);
833
835
  // TODO: using file streaming along with an event-based json parser
834
836
  // instead of JSON.parse and .readFile could probably make this faster
835
837
  // and less memory intensive
@@ -840,6 +842,8 @@ async function loadOboGraphJson(db) {
840
842
  catch {
841
843
  throw new Error('Error in loading ontology');
842
844
  }
845
+ percentProgress += 5;
846
+ this.options.update?.('Parsing JSON complete', percentProgress);
843
847
  const parseTime = Date.now();
844
848
  const [graph, ...additionalGraphs] = oboGraph.graphs ?? [];
845
849
  if (!graph) {
@@ -858,31 +862,51 @@ async function loadOboGraphJson(db) {
858
862
  const fullTextIndexPaths = getTextIndexFields
859
863
  .call(this)
860
864
  .map((def) => def.jsonPath);
861
- for (const node of graph.nodes ?? []) {
862
- if (isOntologyDBNode(node)) {
863
- await nodeStore.add({
864
- ...node,
865
- fullTextWords: serializeWords(getWords(node, fullTextIndexPaths, this.prefixes)),
866
- });
865
+ if (graph.nodes) {
866
+ let lastProgress = Math.round(percentProgress);
867
+ for (const [, node] of graph.nodes.entries()) {
868
+ percentProgress += 64 * (1 / graph.nodes.length);
869
+ if (Math.round(percentProgress) != lastProgress &&
870
+ percentProgress < 100) {
871
+ this.options.update?.('Processing nodes', percentProgress);
872
+ lastProgress = Math.round(percentProgress);
873
+ }
874
+ if (isOntologyDBNode(node)) {
875
+ await nodeStore.add({
876
+ ...node,
877
+ fullTextWords: serializeWords(getWords(node, fullTextIndexPaths, this.prefixes)),
878
+ });
879
+ }
867
880
  }
868
881
  }
869
882
  // load edges
870
883
  const edgeStore = tx.objectStore('edges');
871
- for (const edge of graph.edges ?? []) {
872
- if (isOntologyDBEdge(edge)) {
873
- await edgeStore.add(edge);
884
+ if (graph.edges) {
885
+ let lastProgress = Math.round(percentProgress);
886
+ for (const [, edge] of graph.edges.entries()) {
887
+ percentProgress += 30 * (1 / graph.edges.length);
888
+ if (Math.round(percentProgress) != lastProgress &&
889
+ percentProgress < 100) {
890
+ this.options.update?.('Processing edges', percentProgress);
891
+ lastProgress = Math.round(percentProgress);
892
+ }
893
+ if (isOntologyDBEdge(edge)) {
894
+ await edgeStore.add(edge);
895
+ }
874
896
  }
875
897
  }
876
898
  await tx.done;
877
899
  // record some metadata about this ontology and load operation
878
900
  const tx2 = db.transaction('meta', 'readwrite');
901
+ // eslint-disable-next-line @typescript-eslint/unbound-method
902
+ const { update, ...otherOptions } = this.options;
879
903
  await tx2.objectStore('meta').add({
880
904
  ontologyRecord: {
881
905
  name: this.ontologyName,
882
906
  version: this.ontologyVersion,
883
907
  sourceLocation: this.sourceLocation,
884
908
  },
885
- storeOptions: this.options,
909
+ storeOptions: otherOptions,
886
910
  graphMeta: graph.meta,
887
911
  timestamp: String(new Date()),
888
912
  schemaVersion,
@@ -947,8 +971,8 @@ class OntologyStore {
947
971
  this.ontologyName = name;
948
972
  this.ontologyVersion = version;
949
973
  this.sourceLocation = source;
950
- this.db = this.prepareDatabase();
951
974
  this.options = options ?? {};
975
+ this.db = this.prepareDatabase();
952
976
  }
953
977
  /**
954
978
  * check that the configuration of this ontology appears valid. Does not
@@ -993,9 +1017,12 @@ class OntologyStore {
993
1017
  return db;
994
1018
  }
995
1019
  try {
996
- const { sourceLocation, sourceType } = this;
1020
+ const { options, sourceLocation, sourceType } = this;
997
1021
  if (sourceType === 'obo-graph-json') {
1022
+ options.update?.('', 0);
1023
+ // add more updates inside `loadOboGraphJson`
998
1024
  await this.loadOboGraphJson(db);
1025
+ options.update?.('', 100);
999
1026
  }
1000
1027
  else {
1001
1028
  throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
@@ -1215,6 +1242,7 @@ const OntologyRecordType = types
1215
1242
  version: 'unversioned',
1216
1243
  source: types.union(LocalPathLocation, UriLocation, BlobLocation),
1217
1244
  options: types.frozen(),
1245
+ equivalentTypes: types.map(types.array(types.string)),
1218
1246
  })
1219
1247
  .volatile((_self) => ({
1220
1248
  dataStore: undefined,
@@ -1232,6 +1260,37 @@ const OntologyRecordType = types
1232
1260
  this.initDataStore();
1233
1261
  }));
1234
1262
  },
1263
+ setEquivalentTypes(type, equivalentTypes) {
1264
+ self.equivalentTypes.set(type, equivalentTypes);
1265
+ },
1266
+ }))
1267
+ .actions((self) => ({
1268
+ loadEquivalentTypes: flow(function* loadEquivalentTypes(type) {
1269
+ if (!self.dataStore) {
1270
+ return;
1271
+ }
1272
+ const terms = (yield self.dataStore.getTermsWithLabelOrSynonym(type));
1273
+ const equivalents = terms
1274
+ .map((term) => term.lbl)
1275
+ .filter((term) => term != undefined);
1276
+ self.setEquivalentTypes(type, equivalents);
1277
+ }),
1278
+ }))
1279
+ .views((self) => ({
1280
+ isTypeOf(queryType, typeOf) {
1281
+ if (queryType === typeOf) {
1282
+ return true;
1283
+ }
1284
+ if (!self.dataStore) {
1285
+ return false;
1286
+ }
1287
+ const equivalents = self.equivalentTypes.get(typeOf);
1288
+ if (!equivalents) {
1289
+ void self.loadEquivalentTypes(typeOf);
1290
+ return false;
1291
+ }
1292
+ return equivalents.includes(queryType);
1293
+ },
1235
1294
  }));
1236
1295
  const OntologyManagerType = types
1237
1296
  .model('OntologyManager', {
@@ -1685,7 +1744,7 @@ function CopyFeature({ changeManager, handleClose, session, sourceAssemblyId, so
1685
1744
  }
1686
1745
  const newRefNames = [...Object.entries(refNameAliases)]
1687
1746
  .filter(([id, refName]) => id !== refName)
1688
- .map(([id, refName]) => ({ _id: id, name: refName ?? '' }));
1747
+ .map(([id, refName]) => ({ _id: id, name: refName }));
1689
1748
  setRefNames(newRefNames);
1690
1749
  setSelectedRefSeqId(newRefNames[0]?._id || '');
1691
1750
  }
@@ -1970,7 +2029,11 @@ function DownloadGFF3({ handleClose, session }) {
1970
2029
  }
1971
2030
  const { exportID } = (await response.json());
1972
2031
  const exportURL = new URL('export', internetAccount.baseURL);
1973
- const exportSearchParams = new URLSearchParams({ exportID });
2032
+ const params = {
2033
+ exportID,
2034
+ includeFASTA: 'true',
2035
+ };
2036
+ const exportSearchParams = new URLSearchParams(params);
1974
2037
  exportURL.search = exportSearchParams.toString();
1975
2038
  const exportUri = exportURL.toString();
1976
2039
  window.open(exportUri, '_blank');
@@ -2661,8 +2724,8 @@ function Option(props) {
2661
2724
  // .map((m) => m.score)
2662
2725
  // .join(', ')
2663
2726
  return (React.createElement("li", { ...other },
2664
- React.createElement(Grid, { container: true },
2665
- React.createElement(Grid, { item: true },
2727
+ React.createElement(Grid2, { container: true },
2728
+ React.createElement(Grid2, null,
2666
2729
  React.createElement(Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
2667
2730
  ' ',
2668
2731
  React.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
@@ -2863,43 +2926,43 @@ function ModifyFeatureAttribute({ changeManager, handleClose, session, sourceAss
2863
2926
  return (React__default.createElement(Dialog, { open: true, title: "Feature attributes", handleClose: handleClose, maxWidth: false, "data-testid": "modify-feature-attribute" },
2864
2927
  React__default.createElement("form", { onSubmit: onSubmit },
2865
2928
  React__default.createElement(DialogContent, null,
2866
- React__default.createElement(Grid, { container: true, direction: "column", spacing: 1 },
2929
+ React__default.createElement(Grid2, { container: true, direction: "column", spacing: 1 },
2867
2930
  Object.entries(attributes).map(([key, value]) => {
2868
2931
  const EditorComponent = reservedKeys$1.get(key) ?? CustomAttributeValueEditor$1;
2869
- return (React__default.createElement(Grid, { container: true, item: true, spacing: 3, alignItems: "center", key: key },
2870
- React__default.createElement(Grid, { item: true, xs: "auto" },
2932
+ return (React__default.createElement(Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
2933
+ React__default.createElement(Grid2, null,
2871
2934
  React__default.createElement(Paper, { variant: "outlined", className: classes.attributeName },
2872
2935
  React__default.createElement(Typography, null, key))),
2873
- React__default.createElement(Grid, { item: true, flexGrow: 1 },
2936
+ React__default.createElement(Grid2, { flexGrow: 1 },
2874
2937
  React__default.createElement(EditorComponent, { session: session, value: value, onChange: makeOnChange(key) })),
2875
- React__default.createElement(Grid, { item: true, xs: 1 },
2938
+ React__default.createElement(Grid2, null,
2876
2939
  React__default.createElement(IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
2877
2940
  deleteAttribute(key);
2878
2941
  } },
2879
2942
  React__default.createElement(DeleteIcon, { fontSize: "medium", key: key })))));
2880
2943
  }),
2881
- React__default.createElement(Grid, { item: true },
2944
+ React__default.createElement(Grid2, null,
2882
2945
  React__default.createElement(Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
2883
2946
  setShowAddNewForm(true);
2884
2947
  } }, "Add new")),
2885
- showAddNewForm ? (React__default.createElement(Grid, { item: true },
2948
+ showAddNewForm ? (React__default.createElement(Grid2, null,
2886
2949
  React__default.createElement(Paper, { elevation: 8, className: classes.newAttributePaper },
2887
- React__default.createElement(Grid, { container: true, direction: "column" },
2888
- React__default.createElement(Grid, { item: true },
2950
+ React__default.createElement(Grid2, { container: true, direction: "column" },
2951
+ React__default.createElement(Grid2, null,
2889
2952
  React__default.createElement(FormControl, null,
2890
2953
  React__default.createElement(FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
2891
2954
  React__default.createElement(RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
2892
- React__default.createElement(FormControlLabel, { value: "custom", control: React__default.createElement(Radio, null), disableTypography: true, label: React__default.createElement(Grid, { container: true, spacing: 1, alignItems: "center" },
2893
- React__default.createElement(Grid, { item: true },
2955
+ React__default.createElement(FormControlLabel, { value: "custom", control: React__default.createElement(Radio, null), disableTypography: true, label: React__default.createElement(Grid2, { container: true, spacing: 1, alignItems: "center" },
2956
+ React__default.createElement(Grid2, null,
2894
2957
  React__default.createElement(Typography, null, "Custom")),
2895
- React__default.createElement(Grid, { item: true },
2958
+ React__default.createElement(Grid2, null,
2896
2959
  React__default.createElement(TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys$1.has(newAttributeKey)
2897
2960
  ? ''
2898
2961
  : newAttributeKey, disabled: reservedKeys$1.has(newAttributeKey), onChange: (event) => {
2899
2962
  setNewAttributeKey(event.target.value);
2900
2963
  } }))) }),
2901
2964
  [...reservedKeys$1.keys()].map((key) => (React__default.createElement(FormControlLabel, { key: key, value: key, control: React__default.createElement(Radio, null), label: key })))))),
2902
- React__default.createElement(Grid, { item: true },
2965
+ React__default.createElement(Grid2, null,
2903
2966
  React__default.createElement(DialogActions, null,
2904
2967
  React__default.createElement(Button, { key: "addButton", color: "primary", variant: "contained", style: { margin: 2 }, onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
2905
2968
  React__default.createElement(Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
@@ -3270,13 +3333,12 @@ function AddRefSeqAliases({ changeManager, handleClose, session, }) {
3270
3333
  };
3271
3334
  return (React__default.createElement(Dialog, { open: true, title: "Add reference sequence aliases", handleClose: handleClose, maxWidth: 'sm', "data-testid": "add-refseq-alias", fullWidth: true },
3272
3335
  React__default.createElement(DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
3273
- React__default.createElement(Grid, { container: true, spacing: 2 },
3274
- React__default.createElement(Grid, { item: true, xs: 4 },
3336
+ React__default.createElement(Grid2, { container: true, spacing: 2 },
3337
+ React__default.createElement(Grid2, null,
3275
3338
  React__default.createElement(FormControl, { disabled: enableSubmit && !errorMessage, fullWidth: true },
3276
3339
  React__default.createElement(InputLabel, { id: "demo-simple-select-label" }, "Assembly"),
3277
3340
  React__default.createElement(Select, { labelId: "demo-simple-select-label", id: "demo-simple-select", label: "Assembly", value: selectedAssembly?.name ?? '', onChange: handleChangeAssembly }, assemblies.map((option) => (React__default.createElement(MenuItem, { key: option.name, value: option.name }, option.displayName ?? option.name)))))),
3278
- React__default.createElement(Grid, { item: true, xs: 1 }),
3279
- React__default.createElement(Grid, { item: true, xs: 7 },
3341
+ React__default.createElement(Grid2, null,
3280
3342
  React__default.createElement(InputLabel, null, "Load RefName alias"),
3281
3343
  React__default.createElement("input", { type: "file", onChange: handleChangeFileHandler, ref: fileRef, disabled: (enableSubmit && !errorMessage) || !selectedAssembly }))),
3282
3344
  selectedAssembly && refNameAliasMap.size > 0 ? (React__default.createElement("div", { style: { height: 200, width: '100%', marginTop: 20 } },
@@ -3951,11 +4013,11 @@ function isApolloMessageData$1(data) {
3951
4013
  const isInWebWorker$1 = typeof sessionStorage === 'undefined';
3952
4014
  class ApolloSequenceAdapter extends BaseSequenceAdapter {
3953
4015
  regions;
3954
- async getRefNames(opts) {
3955
- const regions = await this.getRegions(opts);
4016
+ async getRefNames() {
4017
+ const regions = await this.getRegions();
3956
4018
  return regions.map((regions) => regions.refName);
3957
4019
  }
3958
- async getRegions(opts) {
4020
+ async getRegions() {
3959
4021
  if (this.regions) {
3960
4022
  return this.regions;
3961
4023
  }
@@ -3987,7 +4049,7 @@ class ApolloSequenceAdapter extends BaseSequenceAdapter {
3987
4049
  removeEventListener('message', messageListener);
3988
4050
  resolve(data.regions);
3989
4051
  };
3990
- addEventListener('message', messageListener, opts);
4052
+ addEventListener('message', messageListener);
3991
4053
  // @ts-expect-error waiting for types to be published
3992
4054
  globalThis.rpcServer.emit('apollo', {
3993
4055
  apollo: true,
@@ -4004,7 +4066,7 @@ class ApolloSequenceAdapter extends BaseSequenceAdapter {
4004
4066
  * @param param -
4005
4067
  * @returns Observable of Feature objects in the region
4006
4068
  */
4007
- getFeatures(region, opts) {
4069
+ getFeatures(region) {
4008
4070
  const { end, refName, start } = region;
4009
4071
  const assemblyId = readConfObject(this.config, 'assemblyId');
4010
4072
  const regionWithAssemblyName = { ...region, assemblyName: assemblyId };
@@ -4041,7 +4103,7 @@ class ApolloSequenceAdapter extends BaseSequenceAdapter {
4041
4103
  removeEventListener('message', messageListener);
4042
4104
  resolve(data.sequence);
4043
4105
  };
4044
- addEventListener('message', messageListener, opts);
4106
+ addEventListener('message', messageListener);
4045
4107
  // @ts-expect-error waiting for types to be published
4046
4108
  globalThis.rpcServer.emit('apollo', {
4047
4109
  apollo: true,
@@ -4741,7 +4803,7 @@ function annotationFromPileup(pluggableElement) {
4741
4803
  .filter(([id, refName]) => id !== refName)
4742
4804
  .map(([id, refName]) => ({
4743
4805
  _id: id,
4744
- name: refName ?? '',
4806
+ name: refName,
4745
4807
  }));
4746
4808
  const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
4747
4809
  if (!refSeqId) {
@@ -4877,6 +4939,285 @@ function annotationFromPileup(pluggableElement) {
4877
4939
  return pluggableElement;
4878
4940
  }
4879
4941
 
4942
+ /* eslint-disable react-hooks/exhaustive-deps */
4943
+ const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
4944
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4945
+ if (!featureTypeOntology) {
4946
+ throw new Error('featureTypeOntology is undefined');
4947
+ }
4948
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
4949
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
4950
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
4951
+ };
4952
+ const isTranscript = (annotationFeature, apolloSessionModel) => {
4953
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
4954
+ if (!featureTypeOntology) {
4955
+ throw new Error('featureTypeOntology is undefined');
4956
+ }
4957
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
4958
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
4959
+ };
4960
+ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
4961
+ const apolloSessionModel = session;
4962
+ const childIds = useMemo(() => Object.keys(annotationFeature.children ?? {}), [annotationFeature]);
4963
+ const features = useMemo(() => {
4964
+ for (const [, asm] of apolloSessionModel.apolloDataStore.assemblies) {
4965
+ if (asm._id === assembly.name) {
4966
+ for (const [, refSeq] of asm.refSeqs) {
4967
+ if (refSeq._id === refSeqId) {
4968
+ return refSeq.features;
4969
+ }
4970
+ }
4971
+ }
4972
+ }
4973
+ return [];
4974
+ }, []);
4975
+ const [parentFeatureChecked, setParentFeatureChecked] = useState(true);
4976
+ const [checkedChildrens, setCheckedChildrens] = useState(childIds);
4977
+ const [errorMessage, setErrorMessage] = useState('');
4978
+ const [destinationFeatures, setDestinationFeatures] = useState([]);
4979
+ const [selectedDestinationFeature, setSelectedDestinationFeature] = useState();
4980
+ const getFeatures = (min, max) => {
4981
+ const filteredFeatures = [];
4982
+ for (const [, f] of features) {
4983
+ const featureSnapshot = getSnapshot(f);
4984
+ if (min >= featureSnapshot.min && max <= featureSnapshot.max) {
4985
+ filteredFeatures.push(featureSnapshot);
4986
+ }
4987
+ }
4988
+ return filteredFeatures;
4989
+ };
4990
+ useEffect(() => {
4991
+ setErrorMessage('');
4992
+ if (checkedChildrens.length === 0) {
4993
+ setParentFeatureChecked(false);
4994
+ return;
4995
+ }
4996
+ if (annotationFeature.children) {
4997
+ const checkedAnnotationFeatureChildren = Object.values(annotationFeature.children)
4998
+ .filter((child) => isTranscript(child, apolloSessionModel))
4999
+ .filter((child) => checkedChildrens.includes(child._id));
5000
+ const mins = checkedAnnotationFeatureChildren.map((f) => f.min);
5001
+ const maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
5002
+ const min = Math.min(...mins);
5003
+ const max = Math.max(...maxes);
5004
+ const filteredFeatures = getFeatures(min, max);
5005
+ setDestinationFeatures(filteredFeatures);
5006
+ if (filteredFeatures.length === 0 &&
5007
+ checkedChildrens.length > 0 &&
5008
+ !parentFeatureChecked) {
5009
+ setErrorMessage('No destination features found');
5010
+ }
5011
+ }
5012
+ }, [checkedChildrens]);
5013
+ const handleParentFeatureCheck = (event) => {
5014
+ const isChecked = event.target.checked;
5015
+ setParentFeatureChecked(isChecked);
5016
+ setCheckedChildrens(isChecked ? childIds : []);
5017
+ };
5018
+ const handleChildFeatureCheck = (event, child) => {
5019
+ setCheckedChildrens((prevChecked) => event.target.checked
5020
+ ? [...prevChecked, child._id]
5021
+ : prevChecked.filter((childId) => childId !== child._id));
5022
+ };
5023
+ const handleDestinationFeatureChange = (e) => {
5024
+ const selectedFeature = destinationFeatures.find((f) => f._id === e.target.value);
5025
+ setSelectedDestinationFeature(selectedFeature);
5026
+ };
5027
+ const handleCreateApolloAnnotation = async () => {
5028
+ if (parentFeatureChecked) {
5029
+ const change = new AddFeatureChange({
5030
+ changedIds: [annotationFeature._id],
5031
+ typeName: 'AddFeatureChange',
5032
+ assembly: assembly.name,
5033
+ addedFeature: annotationFeature,
5034
+ });
5035
+ await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5036
+ session.notify('Annotation added successfully', 'success');
5037
+ handleClose();
5038
+ }
5039
+ else {
5040
+ if (!annotationFeature.children) {
5041
+ return;
5042
+ }
5043
+ if (!selectedDestinationFeature) {
5044
+ return;
5045
+ }
5046
+ for (const childId of checkedChildrens) {
5047
+ const child = annotationFeature.children[childId];
5048
+ const change = new AddFeatureChange({
5049
+ parentFeatureId: selectedDestinationFeature._id,
5050
+ changedIds: [selectedDestinationFeature._id],
5051
+ typeName: 'AddFeatureChange',
5052
+ assembly: assembly.name,
5053
+ addedFeature: child,
5054
+ });
5055
+ await apolloSessionModel.apolloDataStore.changeManager.submit(change);
5056
+ session.notify('Annotation added successfully', 'success');
5057
+ handleClose();
5058
+ }
5059
+ }
5060
+ };
5061
+ return (React__default.createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
5062
+ React__default.createElement(DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
5063
+ React__default.createElement(DialogContent, null,
5064
+ React__default.createElement(Box, { sx: { ml: 3 } },
5065
+ isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default.createElement(FormControlLabel, { control: React__default.createElement(Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${annotationFeature.type}:${annotationFeature.min}..${annotationFeature.max}` })),
5066
+ annotationFeature.children && (React__default.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
5067
+ .filter((child) => isTranscript(child, apolloSessionModel))
5068
+ .map((child) => (React__default.createElement(FormControlLabel, { key: child._id, control: React__default.createElement(Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
5069
+ handleChildFeatureCheck(e, child);
5070
+ } }), label: `${child.type}:${child.min}..${child.max}` })))))),
5071
+ !parentFeatureChecked &&
5072
+ checkedChildrens.length > 0 &&
5073
+ destinationFeatures.length > 0 && (React__default.createElement(Box, { sx: { ml: 3 } },
5074
+ React__default.createElement(Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
5075
+ React__default.createElement(Box, { sx: { mt: 1 } },
5076
+ React__default.createElement(Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default.createElement(MenuItem, { key: f._id, value: f._id }, `${f.type}:${f.min}..${f.max}`)))))))),
5077
+ React__default.createElement(DialogActions, null,
5078
+ React__default.createElement(Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
5079
+ (!parentFeatureChecked &&
5080
+ checkedChildrens.length > 0 &&
5081
+ !selectedDestinationFeature), onClick: handleCreateApolloAnnotation }, "Create"),
5082
+ React__default.createElement(Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel")),
5083
+ errorMessage ? (React__default.createElement(DialogContent, null,
5084
+ React__default.createElement(DialogContentText, { color: "error" }, errorMessage))) : null));
5085
+ }
5086
+
5087
+ function simpleFeatureToGFF3Feature(feature, refSeqId) {
5088
+ const xfeature = JSON.parse(JSON.stringify(feature));
5089
+ const children = xfeature.subfeatures;
5090
+ const gff3Feature = [
5091
+ {
5092
+ start: xfeature.start + 1,
5093
+ end: xfeature.end,
5094
+ seq_id: refSeqId,
5095
+ source: xfeature.source ?? null,
5096
+ type: xfeature.type ?? null,
5097
+ score: xfeature.score ?? null,
5098
+ strand: xfeature.strand ? (xfeature.strand === 1 ? '+' : '-') : null,
5099
+ phase: xfeature.phase !== null || xfeature.phase !== undefined
5100
+ ? xfeature.phase
5101
+ : null,
5102
+ attributes: convertFeatureAttributes(xfeature),
5103
+ derived_features: [],
5104
+ child_features: children
5105
+ ? children.map((x) => simpleFeatureToGFF3Feature(x, refSeqId))
5106
+ : [],
5107
+ },
5108
+ ];
5109
+ return gff3Feature;
5110
+ }
5111
+ function jbrowseFeatureToAnnotationFeature(feature, refSeqId) {
5112
+ return gff3ToAnnotationFeature(simpleFeatureToGFF3Feature(feature, refSeqId));
5113
+ }
5114
+ function convertFeatureAttributes(feature) {
5115
+ const attributes = {};
5116
+ const defaultFields = new Set([
5117
+ 'start',
5118
+ 'end',
5119
+ 'type',
5120
+ 'strand',
5121
+ 'refName',
5122
+ 'subfeatures',
5123
+ 'derived_features',
5124
+ 'phase',
5125
+ 'source',
5126
+ 'score',
5127
+ ]);
5128
+ for (const [key, value] of Object.entries(feature)) {
5129
+ if (defaultFields.has(key)) {
5130
+ continue;
5131
+ }
5132
+ attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
5133
+ }
5134
+ return attributes;
5135
+ }
5136
+ function annotationFromJBrowseFeature(pluggableElement) {
5137
+ if (pluggableElement.name !== 'LinearBasicDisplay') {
5138
+ return pluggableElement;
5139
+ }
5140
+ const { stateModel } = pluggableElement;
5141
+ const newStateModel = stateModel
5142
+ .views((self) => ({
5143
+ getFirstRegion() {
5144
+ const lgv = getContainingView(self);
5145
+ return lgv.dynamicBlocks.contentBlocks[0];
5146
+ },
5147
+ getAssembly() {
5148
+ const firstRegion = self.getFirstRegion();
5149
+ const session = getSession(self);
5150
+ const { assemblyManager } = session;
5151
+ const { assemblyName } = firstRegion;
5152
+ const assembly = assemblyManager.get(assemblyName);
5153
+ if (!assembly) {
5154
+ throw new Error(`Could not find assembly named ${assemblyName}`);
5155
+ }
5156
+ return assembly;
5157
+ },
5158
+ getRefSeqId(assembly) {
5159
+ const firstRegion = self.getFirstRegion();
5160
+ const { refName } = firstRegion;
5161
+ const { refNameAliases } = assembly;
5162
+ if (!refNameAliases) {
5163
+ throw new Error(`Could not find aliases for ${assembly.name}`);
5164
+ }
5165
+ const newRefNames = [...Object.entries(refNameAliases)]
5166
+ .filter(([id, refName]) => id !== refName)
5167
+ .map(([id, refName]) => ({
5168
+ _id: id,
5169
+ name: refName,
5170
+ }));
5171
+ const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
5172
+ if (!refSeqId) {
5173
+ throw new Error(`Could not find refSeqId named ${refName}`);
5174
+ }
5175
+ return refSeqId;
5176
+ },
5177
+ getAnnotationFeature(assembly) {
5178
+ const refSeqId = self.getRefSeqId(assembly);
5179
+ const sfeature = self.contextMenuFeature.data;
5180
+ return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
5181
+ },
5182
+ }))
5183
+ .views((self) => {
5184
+ const superContextMenuItems = self.contextMenuItems;
5185
+ const session = getSession(self);
5186
+ const assembly = self.getAssembly();
5187
+ return {
5188
+ contextMenuItems() {
5189
+ const feature = self.contextMenuFeature;
5190
+ if (!feature) {
5191
+ return superContextMenuItems();
5192
+ }
5193
+ return [
5194
+ ...superContextMenuItems(),
5195
+ {
5196
+ label: 'Create Apollo annotation',
5197
+ icon: AddIcon,
5198
+ onClick: () => {
5199
+ session.queueDialog((doneCallback) => [
5200
+ CreateApolloAnnotation,
5201
+ {
5202
+ session,
5203
+ handleClose: () => {
5204
+ doneCallback();
5205
+ },
5206
+ annotationFeature: self.getAnnotationFeature(assembly),
5207
+ assembly,
5208
+ refSeqId: self.getRefSeqId(assembly),
5209
+ },
5210
+ ]);
5211
+ },
5212
+ },
5213
+ ];
5214
+ },
5215
+ };
5216
+ });
5217
+ pluggableElement.stateModel = newStateModel;
5218
+ return pluggableElement;
5219
+ }
5220
+
4880
5221
  /* eslint-disable @typescript-eslint/unbound-method */
4881
5222
  const StringTextField = observer(function StringTextField({ onChangeCommitted, value: initialValue, ...props }) {
4882
5223
  const [value, setValue] = useState(String(initialValue));
@@ -5083,44 +5424,44 @@ const Attributes = observer(function Attributes({ assembly, editable, feature, s
5083
5424
  }
5084
5425
  return (React__default.createElement(React__default.Fragment, null,
5085
5426
  React__default.createElement(Typography, { variant: "h5" }, "Attributes"),
5086
- React__default.createElement(Grid, { container: true, direction: "column", spacing: 1 },
5427
+ React__default.createElement(Grid2, { container: true, direction: "column", spacing: 1 },
5087
5428
  Object.entries(attributes).map(([key, value]) => {
5088
5429
  if (key === '') {
5089
5430
  return null;
5090
5431
  }
5091
5432
  const EditorComponent = reservedKeys.get(key) ?? CustomAttributeValueEditor;
5092
- return (React__default.createElement(Grid, { container: true, item: true, spacing: 3, alignItems: "center", key: key },
5093
- React__default.createElement(Grid, { item: true, xs: "auto" },
5433
+ return (React__default.createElement(Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
5434
+ React__default.createElement(Grid2, null,
5094
5435
  React__default.createElement(Paper, { variant: "outlined", className: classes.attributeName },
5095
5436
  React__default.createElement(Typography, null, key))),
5096
- React__default.createElement(Grid, { item: true, flexGrow: 1 },
5437
+ React__default.createElement(Grid2, { flexGrow: 1 },
5097
5438
  React__default.createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => onChangeCommitted(key, newValue) })),
5098
- React__default.createElement(Grid, { item: true, xs: 1 },
5439
+ React__default.createElement(Grid2, null,
5099
5440
  React__default.createElement(IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => onChangeCommitted(key) },
5100
5441
  React__default.createElement(DeleteIcon, { fontSize: "medium", key: key })))));
5101
5442
  }),
5102
- React__default.createElement(Grid, { item: true },
5443
+ React__default.createElement(Grid2, null,
5103
5444
  React__default.createElement(Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
5104
5445
  setShowAddNewForm(true);
5105
5446
  } }, "Add new")),
5106
- showAddNewForm ? (React__default.createElement(Grid, { item: true },
5447
+ showAddNewForm ? (React__default.createElement(Grid2, null,
5107
5448
  React__default.createElement(Paper, { elevation: 8, className: classes.newAttributePaper },
5108
- React__default.createElement(Grid, { container: true, direction: "column" },
5109
- React__default.createElement(Grid, { item: true },
5449
+ React__default.createElement(Grid2, { container: true, direction: "column" },
5450
+ React__default.createElement(Grid2, null,
5110
5451
  React__default.createElement(FormControl, null,
5111
5452
  React__default.createElement(FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
5112
5453
  React__default.createElement(RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
5113
- React__default.createElement(FormControlLabel, { value: "custom", control: React__default.createElement(Radio, null), disableTypography: true, label: React__default.createElement(Grid, { container: true, spacing: 1, alignItems: "center" },
5114
- React__default.createElement(Grid, { item: true },
5454
+ React__default.createElement(FormControlLabel, { value: "custom", control: React__default.createElement(Radio, null), disableTypography: true, label: React__default.createElement(Grid2, { container: true, spacing: 1, alignItems: "center" },
5455
+ React__default.createElement(Grid2, null,
5115
5456
  React__default.createElement(Typography, null, "Custom")),
5116
- React__default.createElement(Grid, { item: true },
5457
+ React__default.createElement(Grid2, null,
5117
5458
  React__default.createElement(TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys.has(newAttributeKey)
5118
5459
  ? ''
5119
5460
  : newAttributeKey, disabled: reservedKeys.has(newAttributeKey), onChange: (event) => {
5120
5461
  setNewAttributeKey(event.target.value);
5121
5462
  } }))) }),
5122
5463
  [...reservedKeys.keys()].map((key) => (React__default.createElement(FormControlLabel, { key: key, value: key, control: React__default.createElement(Radio, null), label: key })))))),
5123
- React__default.createElement(Grid, { item: true },
5464
+ React__default.createElement(Grid2, null,
5124
5465
  React__default.createElement(DialogActions, null,
5125
5466
  React__default.createElement(Button, { key: "addButton", color: "primary", variant: "contained", onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
5126
5467
  React__default.createElement(Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
@@ -5302,6 +5643,47 @@ const Sequence = observer(function Sequence({ assembly, feature, refName, sessio
5302
5643
  React__default.createElement("div", null, showSequence && (React__default.createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })))));
5303
5644
  });
5304
5645
 
5646
+ const FeatureDetailsNavigation = observer(function FeatureDetailsNavigation(props) {
5647
+ const { feature, model } = props;
5648
+ const { children, parent } = feature;
5649
+ const childFeatures = [];
5650
+ if (children) {
5651
+ for (const [, child] of children) {
5652
+ childFeatures.push(child);
5653
+ }
5654
+ }
5655
+ if (!(parent ?? childFeatures.length > 0)) {
5656
+ return null;
5657
+ }
5658
+ return (React__default.createElement("div", null,
5659
+ React__default.createElement(Typography, { variant: "h5" }, "Go to related feature"),
5660
+ parent && (React__default.createElement("div", null,
5661
+ React__default.createElement(Typography, { variant: "h6" }, "Parent:"),
5662
+ React__default.createElement(Button, { variant: "contained", onClick: () => {
5663
+ model.setFeature(parent);
5664
+ } },
5665
+ parent.type,
5666
+ " (",
5667
+ parent.min,
5668
+ "..",
5669
+ parent.max,
5670
+ ")"))),
5671
+ childFeatures.length > 0 && (React__default.createElement("div", null,
5672
+ React__default.createElement(Typography, { variant: "h6" },
5673
+ childFeatures.length === 1 ? 'Child' : 'Children',
5674
+ ":"),
5675
+ childFeatures.map((child) => (React__default.createElement("div", { key: child._id, style: { marginBottom: 5 } },
5676
+ React__default.createElement(Button, { variant: "contained", onClick: () => {
5677
+ model.setFeature(child);
5678
+ } },
5679
+ child.type,
5680
+ " (",
5681
+ child.min,
5682
+ "..",
5683
+ child.max,
5684
+ ")"))))))));
5685
+ });
5686
+
5305
5687
  const useStyles$8 = makeStyles()((theme) => ({
5306
5688
  root: {
5307
5689
  padding: theme.spacing(2),
@@ -5332,7 +5714,9 @@ const ApolloFeatureDetailsWidget = observer(function ApolloFeatureDetailsWidget(
5332
5714
  React__default.createElement("hr", null),
5333
5715
  React__default.createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }),
5334
5716
  React__default.createElement("hr", null),
5335
- React__default.createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName })));
5717
+ React__default.createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName }),
5718
+ React__default.createElement("hr", null),
5719
+ React__default.createElement(FeatureDetailsNavigation, { model: model, feature: feature })));
5336
5720
  });
5337
5721
 
5338
5722
  /* eslint-disable @typescript-eslint/no-unsafe-call */
@@ -5472,7 +5856,14 @@ const TranscriptBasicInformation = observer(function TranscriptBasicInformation(
5472
5856
  if (!refData) {
5473
5857
  return null;
5474
5858
  }
5475
- const { strand, transcriptParts } = feature;
5859
+ let strand, transcriptParts;
5860
+ try {
5861
+ ;
5862
+ ({ strand, transcriptParts } = feature);
5863
+ }
5864
+ catch {
5865
+ return null;
5866
+ }
5476
5867
  const [firstLocation] = transcriptParts;
5477
5868
  const locationData = firstLocation
5478
5869
  .map((loc, idx) => {
@@ -5613,6 +6004,28 @@ function getSequenceSegments(segmentType, feature, getSequence) {
5613
6004
  segments.push({ type: 'CDS', sequenceLines, locs });
5614
6005
  return segments;
5615
6006
  }
6007
+ case 'protein': {
6008
+ let wholeSequence = '';
6009
+ const [firstLocation] = cdsLocations;
6010
+ const locs = [];
6011
+ for (const loc of firstLocation) {
6012
+ let sequence = getSequence(loc.min, loc.max);
6013
+ if (strand === -1) {
6014
+ sequence = revcom(sequence);
6015
+ }
6016
+ wholeSequence += sequence;
6017
+ locs.push({ min: loc.min, max: loc.max });
6018
+ }
6019
+ let protein = '';
6020
+ for (let i = 0; i < wholeSequence.length; i += 3) {
6021
+ const codonSeq = wholeSequence.slice(i, i + 3).toUpperCase();
6022
+ protein +=
6023
+ defaultCodonTable[codonSeq] || '&';
6024
+ }
6025
+ const sequenceLines = splitStringIntoChunks(protein, SEQUENCE_WRAP_LENGTH);
6026
+ segments.push({ type: 'protein', sequenceLines, locs });
6027
+ return segments;
6028
+ }
5616
6029
  }
5617
6030
  }
5618
6031
  function getSegmentColor(type) {
@@ -5701,7 +6114,8 @@ const TranscriptSequence = observer(function TranscriptSequence({ assembly, feat
5701
6114
  React__default.createElement(Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
5702
6115
  React__default.createElement(MenuItem, { value: "CDS" }, "CDS"),
5703
6116
  React__default.createElement(MenuItem, { value: "cDNA" }, "cDNA"),
5704
- React__default.createElement(MenuItem, { value: "genomic" }, "Genomic")),
6117
+ React__default.createElement(MenuItem, { value: "genomic" }, "Genomic"),
6118
+ React__default.createElement(MenuItem, { value: "protein" }, "Protein")),
5705
6119
  React__default.createElement(Paper, { style: {
5706
6120
  fontFamily: 'monospace',
5707
6121
  padding: theme.spacing(),
@@ -5939,7 +6353,12 @@ function featureContextMenuItems(feature, region, getAssemblyId, selectedFeature
5939
6353
  ]);
5940
6354
  },
5941
6355
  });
5942
- if (feature.type === 'mRNA' && isSessionModelWithWidgets(session)) {
6356
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
6357
+ if (!featureTypeOntology) {
6358
+ throw new Error('featureTypeOntology is undefined');
6359
+ }
6360
+ if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
6361
+ isSessionModelWithWidgets(session)) {
5943
6362
  menuItems.push({
5944
6363
  label: 'Edit transcript details',
5945
6364
  onClick: () => {
@@ -6011,958 +6430,555 @@ const NumberCell = observer(function NumberCell({ initialValue, notifyError, onC
6011
6430
  } })));
6012
6431
  });
6013
6432
 
6014
- const minDisplayHeight = 20;
6015
- function baseModelFactory(_pluginManager, configSchema) {
6016
- return BaseDisplay.named('BaseLinearApolloDisplay')
6017
- .props({
6018
- type: types.literal('LinearApolloDisplay'),
6019
- configuration: ConfigurationReference(configSchema),
6020
- graphical: true,
6021
- table: false,
6022
- heightPreConfig: types.maybe(types.refinement('displayHeight', types.number, (n) => n >= minDisplayHeight)),
6023
- })
6024
- .views((self) => {
6025
- const { configuration, renderProps: superRenderProps } = self;
6026
- return {
6027
- renderProps() {
6028
- return {
6029
- ...superRenderProps(),
6030
- ...getParentRenderProps(self),
6031
- config: configuration.renderer,
6032
- };
6033
- },
6034
- };
6035
- })
6036
- .volatile(() => ({
6037
- scrollTop: 0,
6038
- }))
6039
- .views((self) => ({
6040
- get lgv() {
6041
- return getContainingView(self);
6042
- },
6043
- get height() {
6044
- if (self.heightPreConfig) {
6045
- return self.heightPreConfig;
6046
- }
6047
- if (self.graphical && self.table) {
6048
- return 500;
6049
- }
6050
- if (self.graphical) {
6051
- return 200;
6052
- }
6053
- return 300;
6054
- },
6055
- }))
6056
- .views((self) => ({
6057
- get rendererTypeName() {
6058
- return self.configuration.renderer.type;
6059
- },
6060
- get session() {
6061
- return getSession(self);
6062
- },
6063
- get regions() {
6064
- const regions = self.lgv.dynamicBlocks.contentBlocks.map(({ assemblyName, end, refName, start }) => ({
6065
- assemblyName,
6066
- refName,
6067
- start: Math.round(start),
6068
- end: Math.round(end),
6069
- }));
6070
- return regions;
6071
- },
6072
- regionCannotBeRendered( /* region */) {
6073
- if (self.lgv && self.lgv.bpPerPx >= 200) {
6074
- return 'Zoom in to see annotations';
6075
- }
6076
- return;
6433
+ /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
6434
+ const useStyles$4 = makeStyles()((theme) => ({
6435
+ typeContent: {
6436
+ display: 'inline-block',
6437
+ width: '174px',
6438
+ height: '100%',
6439
+ cursor: 'text',
6440
+ },
6441
+ feature: {
6442
+ td: {
6443
+ position: 'relative',
6444
+ verticalAlign: 'top',
6445
+ paddingLeft: '0.5em',
6077
6446
  },
6078
- }))
6079
- .views((self) => ({
6080
- get apolloInternetAccount() {
6081
- const [region] = self.regions;
6082
- const { internetAccounts } = getRoot(self);
6083
- const { assemblyName } = region;
6084
- const { assemblyManager } = self.session;
6085
- const assembly = assemblyManager.get(assemblyName);
6086
- if (!assembly) {
6087
- throw new Error(`No assembly found with name ${assemblyName}`);
6088
- }
6089
- const { internetAccountConfigId } = getConf(assembly, [
6090
- 'sequence',
6091
- 'metadata',
6092
- ]);
6093
- return internetAccounts.find((ia) => getConf(ia, 'internetAccountId') === internetAccountConfigId);
6094
- },
6095
- get changeManager() {
6096
- return self.session.apolloDataStore
6097
- .changeManager;
6098
- },
6099
- getAssemblyId(assemblyName) {
6100
- const { assemblyManager } = self.session;
6101
- const assembly = assemblyManager.get(assemblyName);
6102
- if (!assembly) {
6103
- throw new Error(`Could not find assembly named ${assemblyName}`);
6104
- }
6105
- return assembly.name;
6106
- },
6107
- get selectedFeature() {
6108
- return self.session
6109
- .apolloSelectedFeature;
6110
- },
6111
- }))
6112
- .actions((self) => ({
6113
- setScrollTop(scrollTop) {
6114
- self.scrollTop = scrollTop;
6115
- },
6116
- setHeight(displayHeight) {
6117
- self.heightPreConfig = Math.max(displayHeight, minDisplayHeight);
6118
- return self.height;
6119
- },
6120
- resizeHeight(distance) {
6121
- const oldHeight = self.height;
6122
- const newHeight = this.setHeight(self.height + distance);
6123
- return newHeight - oldHeight;
6124
- },
6125
- showGraphicalOnly() {
6126
- self.graphical = true;
6127
- self.table = false;
6128
- },
6129
- showTableOnly() {
6130
- self.graphical = false;
6131
- self.table = true;
6132
- },
6133
- showGraphicalAndTable() {
6134
- self.graphical = true;
6135
- self.table = true;
6136
- },
6137
- }))
6138
- .views((self) => {
6139
- const { trackMenuItems: superTrackMenuItems } = self;
6140
- return {
6141
- trackMenuItems() {
6142
- const { graphical, table } = self;
6143
- return [
6144
- ...superTrackMenuItems(),
6145
- {
6146
- type: 'subMenu',
6147
- label: 'Appearance',
6148
- subMenu: [
6149
- {
6150
- label: 'Show graphical display',
6151
- type: 'radio',
6152
- checked: graphical && !table,
6153
- onClick: () => {
6154
- self.showGraphicalOnly();
6155
- },
6156
- },
6157
- {
6158
- label: 'Show table display',
6159
- type: 'radio',
6160
- checked: table && !graphical,
6161
- onClick: () => {
6162
- self.showTableOnly();
6163
- },
6164
- },
6165
- {
6166
- label: 'Show both graphical and table display',
6167
- type: 'radio',
6168
- checked: table && graphical,
6169
- onClick: () => {
6170
- self.showGraphicalAndTable();
6171
- },
6172
- },
6173
- ],
6174
- },
6175
- ];
6176
- },
6177
- };
6178
- })
6179
- .actions((self) => ({
6180
- setSelectedFeature(feature) {
6181
- self.session.apolloSetSelectedFeature(feature);
6182
- },
6183
- afterAttach() {
6184
- addDisposer(self, autorun(() => {
6185
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6186
- return;
6187
- }
6188
- void self.session.apolloDataStore.loadFeatures(self.regions);
6189
- if (self.lgv.bpPerPx <= 3) {
6190
- void self.session.apolloDataStore.loadRefSeq(self.regions);
6191
- }
6192
- }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
6193
- },
6194
- }));
6195
- }
6196
-
6197
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
6198
- function layoutsModelFactory(pluginManager, configSchema) {
6199
- const BaseLinearApolloDisplay = baseModelFactory(pluginManager, configSchema);
6200
- return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
6201
- .props({
6202
- featuresMinMaxLimit: 500_000,
6203
- })
6204
- .volatile(() => ({
6205
- seenFeatures: observable.map(),
6206
- }))
6207
- .views((self) => ({
6208
- get featuresMinMax() {
6209
- const { assemblyManager } = self.session;
6210
- return self.lgv.displayedRegions.map((region) => {
6211
- const assembly = assemblyManager.get(region.assemblyName);
6212
- let min;
6213
- let max;
6214
- const { end, refName, start } = region;
6215
- for (const [, feature] of self.seenFeatures) {
6216
- if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
6217
- !doesIntersect2(start, end, feature.min, feature.max) ||
6218
- feature.length > self.featuresMinMaxLimit) {
6219
- continue;
6220
- }
6221
- if (min === undefined) {
6222
- ({ min } = feature);
6223
- }
6224
- if (max === undefined) {
6225
- ({ max } = feature);
6226
- }
6227
- if (feature.minWithChildren < min) {
6228
- ({ min } = feature);
6229
- }
6230
- if (feature.maxWithChildren > max) {
6231
- ({ max } = feature);
6232
- }
6233
- }
6234
- if (min !== undefined && max !== undefined) {
6235
- return [min, max];
6236
- }
6237
- return;
6238
- });
6239
- },
6240
- }))
6241
- .actions((self) => ({
6242
- addSeenFeature(feature) {
6243
- self.seenFeatures.set(feature._id, feature);
6244
- },
6245
- deleteSeenFeature(featureId) {
6246
- self.seenFeatures.delete(featureId);
6247
- },
6248
- }))
6249
- .views((self) => ({
6250
- get featureLayouts() {
6251
- const { assemblyManager } = self.session;
6252
- return self.lgv.displayedRegions.map((region, idx) => {
6253
- const assembly = assemblyManager.get(region.assemblyName);
6254
- const featureLayout = new Map();
6255
- const minMax = self.featuresMinMax[idx];
6256
- if (!minMax) {
6257
- return featureLayout;
6258
- }
6259
- const [min, max] = minMax;
6260
- const rows = [];
6261
- const { end, refName, start } = region;
6262
- for (const [id, feature] of self.seenFeatures.entries()) {
6263
- if (!isAlive(feature)) {
6264
- self.deleteSeenFeature(id);
6265
- continue;
6266
- }
6267
- if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
6268
- !doesIntersect2(start, end, feature.min, feature.max)) {
6269
- continue;
6270
- }
6271
- const rowCount = getGlyph(feature).getRowCount(feature, self.lgv.bpPerPx);
6272
- let startingRow = 0;
6273
- let placed = false;
6274
- while (!placed) {
6275
- let rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
6276
- if (rowsForFeature.length < rowCount) {
6277
- for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
6278
- const newRowNumber = rows.length;
6279
- rows[newRowNumber] = Array.from({ length: max - min });
6280
- featureLayout.set(newRowNumber, []);
6281
- }
6282
- rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
6283
- }
6284
- if (rowsForFeature
6285
- .map((rowForFeature) => {
6286
- // zero-length features are allowed in the spec
6287
- const featureMax = feature.max - feature.min === 0
6288
- ? feature.min + 1
6289
- : feature.max;
6290
- let start = feature.min - min, end = featureMax - min;
6291
- if (feature.min - min < 0) {
6292
- start = 0;
6293
- end = featureMax - feature.min;
6294
- }
6295
- return rowForFeature.slice(start, end).some(Boolean);
6296
- })
6297
- .some(Boolean)) {
6298
- startingRow += 1;
6299
- continue;
6300
- }
6301
- for (let rowNum = startingRow; rowNum < startingRow + rowCount; rowNum++) {
6302
- const row = rows[rowNum];
6303
- let start = feature.min - min, end = feature.max - min;
6304
- if (feature.min - min < 0) {
6305
- start = 0;
6306
- end = feature.max - feature.min;
6307
- }
6308
- row.fill(true, start, end);
6309
- const layoutRow = featureLayout.get(rowNum);
6310
- layoutRow?.push([rowNum - startingRow, feature]);
6311
- }
6312
- placed = true;
6313
- }
6314
- }
6315
- return featureLayout;
6316
- });
6317
- },
6318
- getFeatureLayoutPosition(feature) {
6319
- const { featureLayouts } = this;
6320
- for (const [idx, layout] of featureLayouts.entries()) {
6321
- for (const [layoutRowNum, layoutRow] of layout) {
6322
- for (const [featureRowNum, layoutFeature] of layoutRow) {
6323
- if (featureRowNum !== 0) {
6324
- // Same top-level feature in all feature rows, so only need to
6325
- // check the first one
6326
- continue;
6327
- }
6328
- if (feature._id === layoutFeature._id) {
6329
- return {
6330
- layoutIndex: idx,
6331
- layoutRow: layoutRowNum,
6332
- featureRow: featureRowNum,
6333
- };
6334
- }
6335
- if (layoutFeature.hasDescendant(feature._id)) {
6336
- const row = getGlyph(layoutFeature).getRowForFeature(layoutFeature, feature);
6337
- if (row !== undefined) {
6338
- return {
6339
- layoutIndex: idx,
6340
- layoutRow: layoutRowNum,
6341
- featureRow: row,
6342
- };
6343
- }
6344
- }
6345
- }
6346
- }
6347
- }
6348
- return;
6349
- },
6350
- }))
6351
- .views((self) => ({
6352
- get highestRow() {
6353
- return Math.max(0, ...self.featureLayouts.map((layout) => Math.max(...layout.keys())));
6354
- },
6355
- }))
6356
- .actions((self) => ({
6357
- afterAttach() {
6358
- addDisposer(self, autorun(() => {
6359
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6360
- return;
6361
- }
6362
- for (const region of self.regions) {
6363
- const assembly = self.session.apolloDataStore.assemblies.get(region.assemblyName);
6364
- const ref = assembly?.getByRefName(region.refName);
6365
- const features = ref?.features;
6366
- if (!features) {
6367
- continue;
6368
- }
6369
- for (const [, feature] of features) {
6370
- if (doesIntersect2(region.start, region.end, feature.min, feature.max) &&
6371
- !self.seenFeatures.has(feature._id)) {
6372
- self.addSeenFeature(feature);
6373
- }
6374
- }
6375
- }
6376
- }, { name: 'LinearApolloDisplaySetSeenFeatures', delay: 1000 }));
6377
- },
6378
- }));
6379
- }
6380
-
6381
- function renderingModelIntermediateFactory(pluginManager, configSchema) {
6382
- const LinearApolloDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
6383
- return LinearApolloDisplayLayouts.named('LinearApolloDisplayRendering')
6384
- .props({
6385
- sequenceRowHeight: 15,
6386
- apolloRowHeight: 20,
6387
- detailsMinHeight: 200,
6388
- detailsHeight: 200,
6389
- lastRowTooltipBufferHeight: 40,
6390
- isShown: true,
6391
- })
6392
- .volatile(() => ({
6393
- canvas: null,
6394
- overlayCanvas: null,
6395
- collaboratorCanvas: null,
6396
- seqTrackCanvas: null,
6397
- seqTrackOverlayCanvas: null,
6398
- theme: undefined,
6399
- }))
6400
- .views((self) => ({
6401
- get featuresHeight() {
6402
- return ((self.highestRow + 1) * self.apolloRowHeight +
6403
- self.lastRowTooltipBufferHeight);
6404
- },
6405
- }))
6406
- .actions((self) => ({
6407
- toggleShown() {
6408
- self.isShown = !self.isShown;
6409
- },
6410
- setDetailsHeight(newHeight) {
6411
- self.detailsHeight = self.isShown
6412
- ? Math.max(Math.min(newHeight, self.height - 100), Math.min(self.height, self.detailsMinHeight))
6413
- : newHeight;
6414
- },
6415
- setCanvas(canvas) {
6416
- self.canvas = canvas;
6417
- },
6418
- setOverlayCanvas(canvas) {
6419
- self.overlayCanvas = canvas;
6420
- },
6421
- setCollaboratorCanvas(canvas) {
6422
- self.collaboratorCanvas = canvas;
6423
- },
6424
- setSeqTrackCanvas(canvas) {
6425
- self.seqTrackCanvas = canvas;
6426
- },
6427
- setSeqTrackOverlayCanvas(canvas) {
6428
- self.seqTrackOverlayCanvas = canvas;
6429
- },
6430
- setTheme(theme) {
6431
- self.theme = theme;
6432
- },
6433
- afterAttach() {
6434
- addDisposer(self, autorun(() => {
6435
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6436
- return;
6437
- }
6438
- const ctx = self.collaboratorCanvas?.getContext('2d');
6439
- if (!ctx) {
6440
- return;
6441
- }
6442
- ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
6443
- for (const collaborator of self.session.collaborators) {
6444
- const { locations } = collaborator;
6445
- if (locations.length === 0) {
6446
- continue;
6447
- }
6448
- let idx = 0;
6449
- for (const displayedRegion of self.lgv.displayedRegions) {
6450
- for (const location of locations) {
6451
- if (location.refSeq !== displayedRegion.refName) {
6452
- continue;
6453
- }
6454
- const { end, refSeq, start } = location;
6455
- const locationStartPxInfo = self.lgv.bpToPx({
6456
- refName: refSeq,
6457
- coord: start,
6458
- regionNumber: idx,
6459
- });
6460
- if (!locationStartPxInfo) {
6461
- continue;
6462
- }
6463
- const locationStartPx = locationStartPxInfo.offsetPx - self.lgv.offsetPx;
6464
- const locationWidthPx = (end - start) / self.lgv.bpPerPx;
6465
- ctx.fillStyle = 'rgba(0,255,0,.2)';
6466
- ctx.fillRect(locationStartPx, 1, locationWidthPx, 100);
6467
- ctx.fillStyle = 'black';
6468
- ctx.fillText(collaborator.name, locationStartPx + 1, 11, locationWidthPx - 2);
6469
- }
6470
- idx++;
6471
- }
6472
- }
6473
- }, { name: 'LinearApolloDisplayRenderCollaborators' }));
6474
- },
6475
- }));
6476
- }
6477
- function colorCode(letter, theme) {
6478
- return (theme?.palette.bases[letter.toUpperCase()].main.toString() ?? 'lightgray');
6479
- }
6480
- function codonColorCode(letter) {
6481
- const colorMap = {
6482
- M: '#33ee33',
6483
- '*': '#f44336',
6484
- };
6485
- return colorMap[letter.toUpperCase()];
6486
- }
6487
- function reverseCodonSeq(seq) {
6488
- return [...seq]
6489
- .map((c) => revcom(c))
6490
- .reverse()
6491
- .join('');
6492
- }
6493
- function drawLetter(seqTrackctx, startPx, widthPx, letter, textY) {
6494
- const fontSize = Math.min(widthPx, 10);
6495
- seqTrackctx.fillStyle = '#000';
6496
- seqTrackctx.font = `${fontSize}px`;
6497
- const textWidth = seqTrackctx.measureText(letter).width;
6498
- const textX = startPx + (widthPx - textWidth) / 2;
6499
- seqTrackctx.fillText(letter, textX, textY + 10);
6447
+ },
6448
+ arrow: {
6449
+ display: 'inline-block',
6450
+ width: '1.6em',
6451
+ textAlign: 'center',
6452
+ cursor: 'pointer',
6453
+ },
6454
+ arrowExpanded: {
6455
+ transform: 'rotate(90deg)',
6456
+ },
6457
+ hoveredFeature: {
6458
+ backgroundColor: theme.palette.action.hover,
6459
+ },
6460
+ typeInputElement: {
6461
+ border: 'none',
6462
+ background: 'none',
6463
+ },
6464
+ typeErrorMessage: {
6465
+ color: 'red',
6466
+ },
6467
+ }));
6468
+ function makeContextMenuItems(display, feature) {
6469
+ const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
6470
+ return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
6500
6471
  }
6501
- function drawTranslation(seqTrackctx, bpPerPx, trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight, seq, i, reverse) {
6502
- let codonSeq = seq.slice(i, i + 3).toUpperCase();
6503
- if (reverse) {
6504
- codonSeq = reverseCodonSeq(codonSeq);
6505
- }
6506
- const codonLetter = defaultCodonTable[codonSeq];
6507
- if (!codonLetter) {
6508
- return;
6509
- }
6510
- const fillColor = codonColorCode(codonLetter);
6511
- if (fillColor) {
6512
- seqTrackctx.fillStyle = fillColor;
6513
- seqTrackctx.fillRect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
6514
- }
6515
- if (bpPerPx <= 0.1) {
6516
- seqTrackctx.rect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
6517
- seqTrackctx.stroke();
6518
- drawLetter(seqTrackctx, trnslStartPx, trnslWidthPx, codonLetter, trnslY);
6472
+ function getTopLevelFeature(feature) {
6473
+ let cur = feature;
6474
+ while (cur.parent) {
6475
+ cur = cur.parent;
6519
6476
  }
6477
+ return cur;
6520
6478
  }
6521
- function sequenceRenderingModelFactory(pluginManager, configSchema) {
6522
- const LinearApolloDisplayRendering = renderingModelIntermediateFactory(pluginManager, configSchema);
6523
- return LinearApolloDisplayRendering.actions((self) => ({
6524
- afterAttach() {
6525
- addDisposer(self, autorun(async () => {
6526
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6527
- return;
6528
- }
6529
- if (self.lgv.bpPerPx > 3) {
6530
- return;
6531
- }
6532
- const seqTrackctx = self.seqTrackCanvas?.getContext('2d');
6533
- if (!seqTrackctx) {
6534
- return;
6535
- }
6536
- seqTrackctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
6537
- const frames = self.lgv.bpPerPx <= 1
6538
- ? [3, 2, 1, 0, 0, -1, -2, -3]
6539
- : [3, 2, 1, -1, -2, -3];
6540
- let height = 0;
6541
- for (const frame of frames) {
6542
- const frameColor = self.theme?.palette.framesCDS.at(frame)?.main;
6543
- if (frameColor) {
6544
- seqTrackctx.fillStyle = frameColor;
6545
- seqTrackctx.fillRect(0, height, self.lgv.dynamicBlocks.totalWidthPx, self.sequenceRowHeight);
6546
- }
6547
- height += self.sequenceRowHeight;
6548
- }
6549
- for (const [idx, region] of self.regions.entries()) {
6550
- const driver = self.session.apolloDataStore.getBackendDriver(region.assemblyName);
6551
- if (!driver) {
6552
- throw new Error('Failed to get the backend driver');
6553
- }
6554
- const { seq } = await driver.getSequence(region);
6555
- if (!seq) {
6556
- return;
6557
- }
6558
- for (const [i, letter] of [...seq].entries()) {
6559
- const trnslXOffset = (self.lgv.bpToPx({
6560
- refName: region.refName,
6561
- coord: region.start + i,
6562
- regionNumber: idx,
6563
- })?.offsetPx ?? 0) - self.lgv.offsetPx;
6564
- const trnslWidthPx = 3 / self.lgv.bpPerPx;
6565
- const trnslStartPx = self.lgv.displayedRegions[idx].reversed
6566
- ? trnslXOffset - trnslWidthPx
6567
- : trnslXOffset;
6568
- // Draw translation forward
6569
- for (let j = 2; j >= 0; j--) {
6570
- if ((region.start + i) % 3 === j) {
6571
- drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (2 - j), trnslWidthPx, self.sequenceRowHeight, seq, i, false);
6572
- }
6573
- }
6574
- if (self.lgv.bpPerPx <= 1) {
6575
- const xOffset = (self.lgv.bpToPx({
6576
- refName: region.refName,
6577
- coord: region.start + i,
6578
- regionNumber: idx,
6579
- })?.offsetPx ?? 0) - self.lgv.offsetPx;
6580
- const widthPx = 1 / self.lgv.bpPerPx;
6581
- const startPx = self.lgv.displayedRegions[idx].reversed
6582
- ? xOffset - widthPx
6583
- : xOffset;
6584
- // Draw forward
6585
- seqTrackctx.beginPath();
6586
- seqTrackctx.fillStyle = colorCode(letter, self.theme);
6587
- seqTrackctx.rect(startPx, self.sequenceRowHeight * 3, widthPx, self.sequenceRowHeight);
6588
- seqTrackctx.fill();
6589
- if (self.lgv.bpPerPx <= 0.1) {
6590
- seqTrackctx.stroke();
6591
- drawLetter(seqTrackctx, startPx, widthPx, letter, self.sequenceRowHeight * 3);
6592
- }
6593
- // Draw reverse
6594
- const revLetter = revcom(letter);
6595
- seqTrackctx.beginPath();
6596
- seqTrackctx.fillStyle = colorCode(revLetter, self.theme);
6597
- seqTrackctx.rect(startPx, self.sequenceRowHeight * 4, widthPx, self.sequenceRowHeight);
6598
- seqTrackctx.fill();
6599
- if (self.lgv.bpPerPx <= 0.1) {
6600
- seqTrackctx.stroke();
6601
- drawLetter(seqTrackctx, startPx, widthPx, revLetter, self.sequenceRowHeight * 4);
6602
- }
6603
- }
6604
- // Draw translation reverse
6605
- for (let k = 0; k <= 2; k++) {
6606
- const rowOffset = self.lgv.bpPerPx <= 1 ? 5 : 3;
6607
- if ((region.start + i) % 3 === k) {
6608
- drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (rowOffset + k), trnslWidthPx, self.sequenceRowHeight, seq, i, true);
6609
- }
6610
- }
6611
- }
6612
- }
6613
- }, { name: 'LinearApolloDisplayRenderSequence' }));
6614
- },
6615
- }));
6616
- }
6617
- function renderingModelFactory(pluginManager, configSchema) {
6618
- const LinearApolloDisplayRendering = sequenceRenderingModelFactory(pluginManager, configSchema);
6619
- return LinearApolloDisplayRendering.actions((self) => ({
6620
- afterAttach() {
6621
- addDisposer(self, autorun(() => {
6622
- const { canvas, featureLayouts, featuresHeight, lgv } = self;
6623
- if (!lgv.initialized || self.regionCannotBeRendered()) {
6624
- return;
6625
- }
6626
- const { displayedRegions, dynamicBlocks } = lgv;
6627
- const ctx = canvas?.getContext('2d');
6628
- if (!ctx) {
6629
- return;
6630
- }
6631
- ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight);
6632
- for (const [idx, featureLayout] of featureLayouts.entries()) {
6633
- const displayedRegion = displayedRegions[idx];
6634
- for (const [row, featureLayoutRow] of featureLayout.entries()) {
6635
- for (const [featureRow, feature] of featureLayoutRow) {
6636
- if (featureRow > 0) {
6637
- continue;
6638
- }
6639
- if (!doesIntersect2(displayedRegion.start, displayedRegion.end, feature.min, feature.max)) {
6640
- continue;
6641
- }
6642
- getGlyph(feature).draw(ctx, feature, row, self, idx);
6643
- }
6644
- }
6479
+ const Feature = observer(function Feature({ depth, feature, isHovered, isSelected, model: displayState, selectedFeatureClass, setContextMenu, }) {
6480
+ const { classes } = useStyles$4();
6481
+ const { apolloHover, changeManager, selectedFeature, session, tabularEditor: tabularEditorState, } = displayState;
6482
+ const { featureCollapsed, filterText } = tabularEditorState;
6483
+ const { _id, children, max, min, strand, type } = feature;
6484
+ const expanded = !featureCollapsed.get(_id);
6485
+ const toggleExpanded = (e) => {
6486
+ e.stopPropagation();
6487
+ tabularEditorState.setFeatureCollapsed(_id, expanded);
6488
+ };
6489
+ // pop up a snackbar in the session notifying user of an error
6490
+ const notifyError = (e) => {
6491
+ session.notify(e.message, 'error');
6492
+ };
6493
+ return (React__default.createElement(React__default.Fragment, null,
6494
+ React__default.createElement("tr", { onMouseEnter: (_e) => {
6495
+ displayState.setApolloHover({
6496
+ feature,
6497
+ topLevelFeature: getTopLevelFeature(feature),
6498
+ glyph: displayState.getGlyph(getTopLevelFeature(feature)),
6499
+ });
6500
+ }, className: classes.feature +
6501
+ (isSelected
6502
+ ? ` ${selectedFeatureClass}`
6503
+ : isHovered
6504
+ ? ` ${classes.hoveredFeature}`
6505
+ : ''), onClick: (e) => {
6506
+ e.stopPropagation();
6507
+ displayState.setSelectedFeature(feature);
6508
+ }, onContextMenu: (e) => {
6509
+ e.preventDefault();
6510
+ setContextMenu({
6511
+ position: { left: e.clientX + 2, top: e.clientY - 6 },
6512
+ items: makeContextMenuItems(displayState, feature),
6513
+ });
6514
+ return false;
6515
+ } },
6516
+ React__default.createElement("td", { style: {
6517
+ whiteSpace: 'nowrap',
6518
+ borderLeft: `${depth * 2}em solid transparent`,
6519
+ } },
6520
+ children?.size ? (
6521
+ // TODO: a11y
6522
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
6523
+ React__default.createElement("div", { onClick: toggleExpanded, className: classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '') }, "\u276F")) : null,
6524
+ React__default.createElement("div", { className: classes.typeContent },
6525
+ React__default.createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: 170 }, value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTypeTerms.bind(null, feature), renderInput: (params) => {
6526
+ return (React__default.createElement("div", { ref: params.InputProps.ref },
6527
+ React__default.createElement("input", { type: "text", ...params.inputProps, className: classes.typeInputElement, style: { width: 170 } }),
6528
+ params.error ? (React__default.createElement("div", { className: classes.typeErrorMessage }, params.errorMessage ?? 'unknown error')) : null));
6529
+ }, onChange: (oldValue, newValue) => {
6530
+ if (newValue) {
6531
+ handleFeatureTypeChange(changeManager, feature, oldValue, newValue).catch(notifyError);
6532
+ }
6533
+ } }))),
6534
+ React__default.createElement("td", null,
6535
+ React__default.createElement(NumberCell, { initialValue: min + 1, notifyError: notifyError, onChangeCommitted: (newStart) => handleFeatureStartChange(changeManager, feature, min, newStart - 1) })),
6536
+ React__default.createElement("td", null,
6537
+ React__default.createElement(NumberCell, { initialValue: max, notifyError: notifyError, onChangeCommitted: (newEnd) => handleFeatureEndChange(changeManager, feature, max, newEnd) })),
6538
+ React__default.createElement("td", null, strand === 1 ? '+' : strand === -1 ? '-' : undefined),
6539
+ React__default.createElement("td", null,
6540
+ React__default.createElement(FeatureAttributes, { filterText: filterText, feature: feature }))),
6541
+ expanded && children
6542
+ ? [...children.entries()]
6543
+ .filter((entry) => {
6544
+ if (!filterText) {
6545
+ return true;
6645
6546
  }
6646
- }, { name: 'LinearApolloDisplayRenderFeatures' }));
6647
- },
6648
- }));
6649
- }
6650
-
6651
- function isMousePositionWithFeatureAndGlyph(mousePosition) {
6652
- return 'featureAndGlyphUnderMouse' in mousePosition;
6653
- }
6654
- function getMousePosition(event, lgv) {
6655
- const canvas = event.currentTarget;
6656
- const { clientX, clientY } = event;
6657
- const { left, top } = canvas.getBoundingClientRect();
6658
- const x = clientX - left;
6659
- const y = clientY - top;
6660
- const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x);
6661
- return { x, y, refName, bp, regionNumber };
6662
- }
6663
- function getTranslationRow(frame, bpPerPx) {
6664
- const offset = bpPerPx <= 1 ? 2 : 0;
6665
- switch (frame) {
6666
- case 3: {
6667
- return 0;
6668
- }
6669
- case 2: {
6670
- return 1;
6671
- }
6672
- case 1: {
6673
- return 2;
6674
- }
6675
- case -1: {
6676
- return 3 + offset;
6677
- }
6678
- case -2: {
6679
- return 4 + offset;
6680
- }
6681
- case -3: {
6682
- return 5 + offset;
6547
+ const [, childFeature] = entry;
6548
+ // search feature and its subfeatures for the text
6549
+ const text = JSON.stringify(childFeature);
6550
+ return text.includes(filterText);
6551
+ })
6552
+ .map(([featureId, childFeature]) => {
6553
+ const childHovered = apolloHover?.feature._id === childFeature._id;
6554
+ const childSelected = selectedFeature?._id === childFeature._id;
6555
+ return (React__default.createElement(Feature, { isHovered: childHovered, isSelected: childSelected, selectedFeatureClass: selectedFeatureClass, key: featureId, depth: (depth || 0) + 1, feature: childFeature, model: displayState, setContextMenu: setContextMenu }));
6556
+ })
6557
+ : null));
6558
+ });
6559
+ async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
6560
+ const { parent: parentFeature } = feature;
6561
+ if (parentFeature) {
6562
+ // if this is a child of an existing feature, restrict the autocomplete choices to valid
6563
+ // parts of that feature
6564
+ const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(parentFeature.type, { includeSubclasses: false });
6565
+ // eslint-disable-next-line unicorn/no-array-callback-reference
6566
+ const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass);
6567
+ if (parentTypeClassTerms.length > 0) {
6568
+ const subpartTerms = await ontologyStore.getClassesThat('part_of', parentTypeClassTerms);
6569
+ return subpartTerms;
6683
6570
  }
6684
6571
  }
6572
+ return;
6685
6573
  }
6686
- function getSeqRow(strand, bpPerPx) {
6687
- if (bpPerPx > 1 || strand === undefined) {
6688
- return;
6689
- }
6690
- return strand === 1 ? 3 : 4;
6574
+
6575
+ const useStyles$3 = makeStyles()((theme) => ({
6576
+ scrollableTable: {
6577
+ width: '100%',
6578
+ height: '100%',
6579
+ th: {
6580
+ position: 'sticky',
6581
+ top: 0,
6582
+ zIndex: 2,
6583
+ textAlign: 'left',
6584
+ background: theme.palette.background.paper,
6585
+ paddingTop: '3.2em',
6586
+ },
6587
+ td: { whiteSpace: 'normal' },
6588
+ },
6589
+ selectedFeature: {
6590
+ backgroundColor: theme.palette.action.selected,
6591
+ },
6592
+ }));
6593
+ const HybridGrid = observer(function HybridGrid({ model, }) {
6594
+ const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model;
6595
+ const theme = useTheme();
6596
+ const { classes } = useStyles$3();
6597
+ const scrollContainerRef = useRef(null);
6598
+ const [contextMenu, setContextMenu] = useState(null);
6599
+ const { filterText } = tabularEditor;
6600
+ // scrolls to selected feature if one is selected and it's not already visible
6601
+ useEffect(() => {
6602
+ const scrollContainer = scrollContainerRef.current;
6603
+ if (scrollContainer && selectedFeature) {
6604
+ const selectedRow = scrollContainer.querySelector(`.${classes.selectedFeature}`);
6605
+ if (selectedRow) {
6606
+ const currScroll = scrollContainer.scrollTop;
6607
+ const newScrollTop = selectedRow.offsetTop - 25;
6608
+ const isVisible = newScrollTop > currScroll &&
6609
+ newScrollTop < currScroll + scrollContainer.offsetHeight;
6610
+ if (!isVisible) {
6611
+ scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' });
6612
+ }
6613
+ }
6614
+ }
6615
+ }, [selectedFeature, seenFeatures, classes.selectedFeature]);
6616
+ return (React__default.createElement("div", { ref: scrollContainerRef, style: { width: '100%', overflowY: 'auto', height: '100%' } },
6617
+ React__default.createElement("table", { className: classes.scrollableTable },
6618
+ React__default.createElement("thead", null,
6619
+ React__default.createElement("tr", null,
6620
+ React__default.createElement("th", null, "Type"),
6621
+ React__default.createElement("th", null, "Start"),
6622
+ React__default.createElement("th", null, "End"),
6623
+ React__default.createElement("th", null, "Strand"),
6624
+ React__default.createElement("th", null, "Attributes"))),
6625
+ React__default.createElement("tbody", null, [...seenFeatures.entries()]
6626
+ .filter((entry) => {
6627
+ if (!filterText) {
6628
+ return true;
6629
+ }
6630
+ const [, feature] = entry;
6631
+ // search feature and its subfeatures for the text
6632
+ const text = JSON.stringify(feature);
6633
+ return text.includes(filterText);
6634
+ })
6635
+ .sort((a, b) => {
6636
+ return a[1].min - b[1].min;
6637
+ })
6638
+ .map(([featureId, feature]) => {
6639
+ const isSelected = selectedFeature?._id === featureId;
6640
+ const isHovered = apolloHover?.feature._id === featureId;
6641
+ return (React__default.createElement(Feature, { key: featureId, isSelected: isSelected, isHovered: isHovered, selectedFeatureClass: classes.selectedFeature, feature: feature, model: model, depth: 0, setContextMenu: setContextMenu }));
6642
+ }))),
6643
+ React__default.createElement(Menu$1, { open: Boolean(contextMenu), onMenuItemClick: (_, callback) => {
6644
+ callback();
6645
+ setContextMenu(null);
6646
+ }, onClose: () => {
6647
+ setContextMenu(null);
6648
+ }, TransitionProps: {
6649
+ onExit: () => {
6650
+ setContextMenu(null);
6651
+ },
6652
+ }, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenu?.items ?? [], anchorReference: "anchorPosition", anchorPosition: contextMenu?.position })));
6653
+ });
6654
+
6655
+ /* eslint-disable @typescript-eslint/unbound-method */
6656
+ const useStyles$2 = makeStyles()({
6657
+ toolbar: {
6658
+ width: '100%',
6659
+ display: 'flex',
6660
+ paddingRight: '2em',
6661
+ flexDirection: 'row',
6662
+ justifyContent: 'space-between',
6663
+ position: 'absolute',
6664
+ zIndex: 4,
6665
+ },
6666
+ filterText: {},
6667
+ });
6668
+ const ToolBar = observer(function ToolBar({ model: displayState, }) {
6669
+ const model = displayState.tabularEditor;
6670
+ const { classes } = useStyles$2();
6671
+ return (React__default.createElement("div", { className: classes.toolbar },
6672
+ React__default.createElement(Tooltip, { title: "Collapse all" },
6673
+ React__default.createElement(IconButton, { "aria-label": "collapse", sx: { marginTop: 0 }, onClick: model.collapseAllFeatures },
6674
+ React__default.createElement(UnfoldLessIcon, null))),
6675
+ React__default.createElement(TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
6676
+ model.setFilterText(event.target.value);
6677
+ }, InputProps: {
6678
+ endAdornment: (React__default.createElement(InputAdornment$1, { position: "end" },
6679
+ React__default.createElement(IconButton, { onClick: () => {
6680
+ model.clearFilterText();
6681
+ } },
6682
+ React__default.createElement(ClearIcon, null)))),
6683
+ } })));
6684
+ });
6685
+
6686
+ function stopPropagation(e) {
6687
+ e.stopPropagation();
6691
6688
  }
6692
- function highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx) {
6693
- if (row !== undefined) {
6694
- seqTrackOverlayctx.fillStyle =
6695
- theme?.palette.action.focus ?? 'rgba(0,0,0,0.04)';
6696
- seqTrackOverlayctx.fillRect(startPx, sequenceRowHeight * row, widthPx, sequenceRowHeight);
6689
+ const TabularEditorPane = observer(function TabularEditorPane({ model: displayState, }) {
6690
+ const model = displayState.tabularEditor;
6691
+ if (!model.isShown) {
6692
+ return null;
6697
6693
  }
6698
- }
6699
- function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
6700
- const LinearApolloDisplayRendering = renderingModelFactory(pluginManager, configSchema);
6701
- return LinearApolloDisplayRendering.named('LinearApolloDisplayMouseEvents')
6694
+ return (
6695
+ // TODO: a11y
6696
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
6697
+ React__default.createElement("div", { onMouseDown: stopPropagation, onClick: stopPropagation, style: { width: '100%', height: '100%', position: 'relative' } },
6698
+ React__default.createElement(ToolBar, { model: displayState }),
6699
+ React__default.createElement(HybridGrid, { model: displayState })));
6700
+ });
6701
+
6702
+ const TabularEditorStateModelType = types
6703
+ .model('TabularEditor', {
6704
+ isShown: true,
6705
+ featureCollapsed: types.map(types.boolean),
6706
+ filterText: '',
6707
+ })
6708
+ .actions((self) => ({
6709
+ setFeatureCollapsed(id, state) {
6710
+ self.featureCollapsed.set(id, state);
6711
+ },
6712
+ setFilterText(text) {
6713
+ self.filterText = text;
6714
+ },
6715
+ clearFilterText() {
6716
+ self.filterText = '';
6717
+ },
6718
+ collapseAllFeatures() {
6719
+ // iterate over all seen features and set them to collapsed
6720
+ const display = getParent(self);
6721
+ for (const [featureId] of display.seenFeatures.entries()) {
6722
+ self.featureCollapsed.set(featureId, true);
6723
+ }
6724
+ },
6725
+ togglePane() {
6726
+ self.isShown = !self.isShown;
6727
+ },
6728
+ hidePane() {
6729
+ self.isShown = false;
6730
+ },
6731
+ showPane() {
6732
+ self.isShown = true;
6733
+ },
6734
+ // onPatch(patch: any) {
6735
+ // console.log(patch)
6736
+ // },
6737
+ }));
6738
+
6739
+ const FilterFeatures = observer(function FilterFeatures({ featureTypes, handleClose, onUpdate, session, }) {
6740
+ const [type, setType] = useState('');
6741
+ const [selectedFeatureTypes, setSelectedFeatureTypes] = useState(featureTypes);
6742
+ const handleChange = (value) => {
6743
+ setType(value);
6744
+ };
6745
+ const handleAddFeatureType = () => {
6746
+ if (type) {
6747
+ if (selectedFeatureTypes.includes(type)) {
6748
+ return;
6749
+ }
6750
+ onUpdate([...selectedFeatureTypes, type]);
6751
+ setSelectedFeatureTypes([...selectedFeatureTypes, type]);
6752
+ }
6753
+ };
6754
+ const handleFeatureTypeDelete = (value) => {
6755
+ const newTypes = selectedFeatureTypes.filter((type) => type !== value);
6756
+ onUpdate(newTypes);
6757
+ setSelectedFeatureTypes(newTypes);
6758
+ };
6759
+ return (React__default.createElement(Dialog, { open: true, maxWidth: false, "data-testid": "filter-features-dialog", title: "Filter features by type", handleClose: handleClose },
6760
+ React__default.createElement(DialogContent, null,
6761
+ React__default.createElement(DialogContentText, null, "Select the feature types you want to display in the apollo track"),
6762
+ React__default.createElement(Grid2, { container: true, spacing: 2 },
6763
+ React__default.createElement(Grid2, { size: 8 },
6764
+ React__default.createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: '100%' }, value: type, filterTerms: isOntologyClass, renderInput: (params) => (React__default.createElement(TextField, { ...params, label: "Feature type", variant: "outlined", fullWidth: true })), onChange: (oldValue, newValue) => {
6765
+ if (newValue) {
6766
+ handleChange(newValue);
6767
+ }
6768
+ } })),
6769
+ React__default.createElement(Grid2, { size: 4 },
6770
+ React__default.createElement(Button, { variant: "contained", onClick: handleAddFeatureType, disabled: !type, style: { marginTop: 9 }, size: "medium" }, "Add"))),
6771
+ selectedFeatureTypes.length > 0 && (React__default.createElement("div", null,
6772
+ React__default.createElement("hr", null),
6773
+ React__default.createElement("div", { style: { width: 300 } },
6774
+ React__default.createElement(DialogContentText, null, "Selected feature types:"),
6775
+ React__default.createElement(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 0.5 } }, selectedFeatureTypes.map((value) => (React__default.createElement(Chip, { key: value, label: value, onDelete: () => {
6776
+ handleFeatureTypeDelete(value);
6777
+ } }))))))))));
6778
+ });
6779
+
6780
+ const minDisplayHeight = 20;
6781
+ function baseModelFactory(_pluginManager, configSchema) {
6782
+ return BaseDisplay.named('BaseLinearApolloDisplay')
6783
+ .props({
6784
+ type: types.literal('LinearApolloDisplay'),
6785
+ configuration: ConfigurationReference(configSchema),
6786
+ graphical: true,
6787
+ table: false,
6788
+ heightPreConfig: types.maybe(types.refinement('displayHeight', types.number, (n) => n >= minDisplayHeight)),
6789
+ filteredFeatureTypes: types.array(types.string),
6790
+ })
6791
+ .views((self) => {
6792
+ const { configuration, renderProps: superRenderProps } = self;
6793
+ return {
6794
+ renderProps() {
6795
+ return {
6796
+ ...superRenderProps(),
6797
+ ...getParentRenderProps(self),
6798
+ config: configuration.renderer,
6799
+ };
6800
+ },
6801
+ };
6802
+ })
6702
6803
  .volatile(() => ({
6703
- apolloDragging: null,
6704
- cursor: undefined,
6705
- apolloHover: undefined,
6804
+ scrollTop: 0,
6706
6805
  }))
6707
6806
  .views((self) => ({
6708
- getMousePosition(event) {
6709
- const mousePosition = getMousePosition(event, self.lgv);
6710
- const { bp, regionNumber, y } = mousePosition;
6711
- const row = Math.floor(y / self.apolloRowHeight);
6712
- const featureLayout = self.featureLayouts[regionNumber];
6713
- const layoutRow = featureLayout.get(row);
6714
- if (!layoutRow) {
6715
- return mousePosition;
6807
+ get lgv() {
6808
+ return getContainingView(self);
6809
+ },
6810
+ get height() {
6811
+ if (self.heightPreConfig) {
6812
+ return self.heightPreConfig;
6716
6813
  }
6717
- const foundFeature = layoutRow.find((f) => bp >= f[1].min && bp <= f[1].max);
6718
- if (!foundFeature) {
6719
- return mousePosition;
6814
+ if (self.graphical && self.table) {
6815
+ return 500;
6720
6816
  }
6721
- const [featureRow, topLevelFeature] = foundFeature;
6722
- const glyph = getGlyph(topLevelFeature);
6723
- const feature = glyph.getFeatureFromLayout(topLevelFeature, bp, featureRow);
6724
- if (!feature) {
6725
- return mousePosition;
6817
+ if (self.graphical) {
6818
+ return 200;
6726
6819
  }
6727
- return {
6728
- ...mousePosition,
6729
- featureAndGlyphUnderMouse: { feature, topLevelFeature, glyph },
6730
- };
6820
+ return 300;
6731
6821
  },
6732
6822
  }))
6733
- .actions((self) => ({
6734
- continueDrag(mousePosition, event) {
6735
- if (!self.apolloDragging) {
6736
- throw new Error('continueDrag() called with no current drag in progress');
6737
- }
6738
- event.stopPropagation();
6739
- self.apolloDragging = { ...self.apolloDragging, current: mousePosition };
6823
+ .views((self) => ({
6824
+ get rendererTypeName() {
6825
+ return self.configuration.renderer.type;
6740
6826
  },
6741
- setDragging(dragInfo) {
6742
- self.apolloDragging = dragInfo ?? null;
6827
+ get session() {
6828
+ return getSession(self);
6743
6829
  },
6744
- }))
6745
- .actions((self) => ({
6746
- setApolloHover(n) {
6747
- self.apolloHover = n;
6830
+ get regions() {
6831
+ const regions = self.lgv.dynamicBlocks.contentBlocks.map(({ assemblyName, end, refName, start }) => ({
6832
+ assemblyName,
6833
+ refName,
6834
+ start: Math.round(start),
6835
+ end: Math.round(end),
6836
+ }));
6837
+ return regions;
6748
6838
  },
6749
- setCursor(cursor) {
6750
- if (self.cursor !== cursor) {
6751
- self.cursor = cursor;
6839
+ regionCannotBeRendered( /* region */) {
6840
+ if (self.lgv && self.lgv.bpPerPx >= 200) {
6841
+ return 'Zoom in to see annotations';
6752
6842
  }
6843
+ return;
6753
6844
  },
6754
6845
  }))
6755
- .actions(() => ({
6756
- // onClick(event: CanvasMouseEvent) {
6757
- onClick() {
6758
- // TODO: set the selected feature
6846
+ .views((self) => ({
6847
+ get apolloInternetAccount() {
6848
+ const [region] = self.regions;
6849
+ const { internetAccounts } = getRoot(self);
6850
+ const { assemblyName } = region;
6851
+ const { assemblyManager } = self.session;
6852
+ const assembly = assemblyManager.get(assemblyName);
6853
+ if (!assembly) {
6854
+ throw new Error(`No assembly found with name ${assemblyName}`);
6855
+ }
6856
+ const { internetAccountConfigId } = getConf(assembly, [
6857
+ 'sequence',
6858
+ 'metadata',
6859
+ ]);
6860
+ return internetAccounts.find((ia) => getConf(ia, 'internetAccountId') === internetAccountConfigId);
6759
6861
  },
6760
- }));
6761
- }
6762
- function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
6763
- const LinearApolloDisplayRendering = mouseEventsModelIntermediateFactory(pluginManager, configSchema);
6764
- return LinearApolloDisplayRendering.actions((self) => ({
6765
- afterAttach() {
6766
- addDisposer(self, autorun(() => {
6767
- // This type is wrong in @jbrowse/core
6768
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6769
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6770
- return;
6771
- }
6772
- const seqTrackOverlayctx = self.seqTrackOverlayCanvas?.getContext('2d');
6773
- if (!seqTrackOverlayctx) {
6774
- return;
6775
- }
6776
- seqTrackOverlayctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
6777
- const { apolloHover, lgv, regions, sequenceRowHeight, theme } = self;
6778
- if (!apolloHover) {
6779
- return;
6780
- }
6781
- const { feature } = apolloHover;
6782
- for (const [idx, region] of regions.entries()) {
6783
- if (feature.type === 'CDS') {
6784
- const parentFeature = feature.parent;
6785
- if (!parentFeature) {
6786
- continue;
6787
- }
6788
- const cdsLocs = parentFeature.cdsLocations.find((loc) => feature.min === loc.at(0)?.min &&
6789
- feature.max === loc.at(-1)?.max);
6790
- if (!cdsLocs) {
6791
- continue;
6792
- }
6793
- for (const dl of cdsLocs) {
6794
- const frame = getFrame(dl.min, dl.max, feature.strand ?? 1, dl.phase);
6795
- const row = getTranslationRow(frame, lgv.bpPerPx);
6796
- const offset = (lgv.bpToPx({
6797
- refName: region.refName,
6798
- coord: dl.min,
6799
- regionNumber: idx,
6800
- })?.offsetPx ?? 0) - lgv.offsetPx;
6801
- const widthPx = (dl.max - dl.min) / lgv.bpPerPx;
6802
- const startPx = lgv.displayedRegions[idx].reversed
6803
- ? offset - widthPx
6804
- : offset;
6805
- highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
6806
- }
6807
- }
6808
- else {
6809
- const row = getSeqRow(feature.strand, lgv.bpPerPx);
6810
- const offset = (lgv.bpToPx({
6811
- refName: region.refName,
6812
- coord: feature.min,
6813
- regionNumber: idx,
6814
- })?.offsetPx ?? 0) - lgv.offsetPx;
6815
- const widthPx = feature.length / lgv.bpPerPx;
6816
- const startPx = lgv.displayedRegions[idx].reversed
6817
- ? offset - widthPx
6818
- : offset;
6819
- highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
6820
- }
6821
- }
6822
- }, { name: 'LinearApolloDisplayRenderSeqHighlight' }));
6862
+ get changeManager() {
6863
+ return self.session.apolloDataStore
6864
+ .changeManager;
6823
6865
  },
6824
- }));
6825
- }
6826
- function mouseEventsModelFactory(pluginManager, configSchema) {
6827
- const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
6828
- return LinearApolloDisplayMouseEvents.views((self) => ({
6829
- contextMenuItems(contextCoord) {
6830
- const { apolloHover } = self;
6831
- if (!(apolloHover && contextCoord)) {
6832
- return [];
6866
+ getAssemblyId(assemblyName) {
6867
+ const { assemblyManager } = self.session;
6868
+ const assembly = assemblyManager.get(assemblyName);
6869
+ if (!assembly) {
6870
+ throw new Error(`Could not find assembly named ${assemblyName}`);
6833
6871
  }
6834
- const { topLevelFeature } = apolloHover;
6835
- const glyph = getGlyph(topLevelFeature);
6836
- return glyph.getContextMenuItems(self);
6872
+ return assembly.name;
6873
+ },
6874
+ get selectedFeature() {
6875
+ return self.session
6876
+ .apolloSelectedFeature;
6837
6877
  },
6838
6878
  }))
6839
6879
  .actions((self) => ({
6840
- // explicitly pass in a feature in case it's not the same as the one in
6841
- // mousePosition (e.g. if features are drawn overlapping).
6842
- startDrag(mousePosition, feature, edge) {
6843
- self.apolloDragging = {
6844
- start: mousePosition,
6845
- current: mousePosition,
6846
- feature,
6847
- edge,
6848
- };
6849
- },
6850
- endDrag() {
6851
- if (!self.apolloDragging) {
6852
- throw new Error('endDrag() called with no current drag in progress');
6853
- }
6854
- const { current, edge, feature, start } = self.apolloDragging;
6855
- // don't do anything if it was only dragged a tiny bit
6856
- if (Math.abs(current.x - start.x) <= 4) {
6857
- self.setDragging();
6858
- self.setCursor();
6859
- return;
6860
- }
6861
- const { displayedRegions } = self.lgv;
6862
- const region = displayedRegions[start.regionNumber];
6863
- const assembly = self.getAssemblyId(region.assemblyName);
6864
- let change;
6865
- if (edge === 'max') {
6866
- const featureId = feature._id;
6867
- const oldEnd = feature.max;
6868
- const newEnd = current.bp;
6869
- change = new LocationEndChange({
6870
- typeName: 'LocationEndChange',
6871
- changedIds: [featureId],
6872
- featureId,
6873
- oldEnd,
6874
- newEnd,
6875
- assembly,
6876
- });
6877
- }
6878
- else {
6879
- const featureId = feature._id;
6880
- const oldStart = feature.min;
6881
- const newStart = current.bp;
6882
- change = new LocationStartChange({
6883
- typeName: 'LocationStartChange',
6884
- changedIds: [featureId],
6885
- featureId,
6886
- oldStart,
6887
- newStart,
6888
- assembly,
6889
- });
6890
- }
6891
- void self.changeManager.submit(change);
6892
- self.setDragging();
6893
- self.setCursor();
6880
+ setScrollTop(scrollTop) {
6881
+ self.scrollTop = scrollTop;
6894
6882
  },
6895
- }))
6896
- .actions((self) => ({
6897
- onMouseDown(event) {
6898
- const mousePosition = self.getMousePosition(event);
6899
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6900
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseDown(self, mousePosition, event);
6901
- }
6883
+ setHeight(displayHeight) {
6884
+ self.heightPreConfig = Math.max(displayHeight, minDisplayHeight);
6885
+ return self.height;
6902
6886
  },
6903
- onMouseMove(event) {
6904
- const mousePosition = self.getMousePosition(event);
6905
- if (self.apolloDragging) {
6906
- self.setCursor('col-resize');
6907
- self.continueDrag(mousePosition, event);
6908
- return;
6909
- }
6910
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6911
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseMove(self, mousePosition, event);
6912
- }
6913
- else {
6914
- self.setApolloHover();
6915
- self.setCursor();
6916
- }
6887
+ resizeHeight(distance) {
6888
+ const oldHeight = self.height;
6889
+ const newHeight = this.setHeight(self.height + distance);
6890
+ return newHeight - oldHeight;
6917
6891
  },
6918
- onMouseLeave(event) {
6919
- self.setDragging();
6920
- self.setApolloHover();
6921
- const mousePosition = self.getMousePosition(event);
6922
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6923
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseLeave(self, mousePosition, event);
6924
- }
6892
+ showGraphicalOnly() {
6893
+ self.graphical = true;
6894
+ self.table = false;
6925
6895
  },
6926
- onMouseUp(event) {
6927
- const mousePosition = self.getMousePosition(event);
6928
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
6929
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
6930
- }
6931
- if (self.apolloDragging) {
6932
- self.endDrag();
6933
- }
6896
+ showTableOnly() {
6897
+ self.graphical = false;
6898
+ self.table = true;
6899
+ },
6900
+ showGraphicalAndTable() {
6901
+ self.graphical = true;
6902
+ self.table = true;
6903
+ },
6904
+ updateFilteredFeatureTypes(types) {
6905
+ self.filteredFeatureTypes = cast(types);
6934
6906
  },
6935
6907
  }))
6908
+ .views((self) => {
6909
+ const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self;
6910
+ return {
6911
+ trackMenuItems() {
6912
+ const { graphical, table } = self;
6913
+ return [
6914
+ ...superTrackMenuItems(),
6915
+ {
6916
+ type: 'subMenu',
6917
+ label: 'Appearance',
6918
+ subMenu: [
6919
+ {
6920
+ label: 'Show graphical display',
6921
+ type: 'radio',
6922
+ checked: graphical && !table,
6923
+ onClick: () => {
6924
+ self.showGraphicalOnly();
6925
+ },
6926
+ },
6927
+ {
6928
+ label: 'Show table display',
6929
+ type: 'radio',
6930
+ checked: table && !graphical,
6931
+ onClick: () => {
6932
+ self.showTableOnly();
6933
+ },
6934
+ },
6935
+ {
6936
+ label: 'Show both graphical and table display',
6937
+ type: 'radio',
6938
+ checked: table && graphical,
6939
+ onClick: () => {
6940
+ self.showGraphicalAndTable();
6941
+ },
6942
+ },
6943
+ ],
6944
+ },
6945
+ {
6946
+ label: 'Filter features by type',
6947
+ onClick: () => {
6948
+ const session = self.session;
6949
+ self.session.queueDialog((doneCallback) => [
6950
+ FilterFeatures,
6951
+ {
6952
+ session,
6953
+ handleClose: () => {
6954
+ doneCallback();
6955
+ },
6956
+ featureTypes: getSnapshot(filteredFeatureTypes),
6957
+ onUpdate: (types) => {
6958
+ self.updateFilteredFeatureTypes(types);
6959
+ },
6960
+ },
6961
+ ]);
6962
+ },
6963
+ },
6964
+ ];
6965
+ },
6966
+ };
6967
+ })
6936
6968
  .actions((self) => ({
6969
+ setSelectedFeature(feature) {
6970
+ self.session.apolloSetSelectedFeature(feature);
6971
+ },
6937
6972
  afterAttach() {
6938
6973
  addDisposer(self, autorun(() => {
6939
- // This type is wrong in @jbrowse/core
6940
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6941
6974
  if (!self.lgv.initialized || self.regionCannotBeRendered()) {
6942
6975
  return;
6943
6976
  }
6944
- const ctx = self.overlayCanvas?.getContext('2d');
6945
- if (!ctx) {
6946
- return;
6947
- }
6948
- ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
6949
- const { apolloDragging, apolloHover } = self;
6950
- if (!apolloHover) {
6951
- return;
6952
- }
6953
- const { glyph } = apolloHover;
6954
- // draw mouseover hovers
6955
- glyph.drawHover(self, ctx);
6956
- // draw tooltip on hover
6957
- glyph.drawTooltip(self, ctx);
6958
- // dragging previews
6959
- if (apolloDragging) {
6960
- // NOTE: the glyph where the drag started is responsible for drawing the preview.
6961
- // it can call methods in other glyphs to help with this though.
6962
- const glyph = getGlyph(apolloDragging.feature.topLevelFeature);
6963
- glyph.drawDragPreview(self, ctx);
6977
+ void self.session.apolloDataStore.loadFeatures(self.regions);
6978
+ if (self.lgv.bpPerPx <= 3) {
6979
+ void self.session.apolloDataStore.loadRefSeq(self.regions);
6964
6980
  }
6965
- }, { name: 'LinearApolloDisplayRenderMouseoverAndDrag' }));
6981
+ }, { name: 'LinearApolloDisplayLoadFeatures', delay: 1000 }));
6966
6982
  },
6967
6983
  }));
6968
6984
  }
@@ -7231,7 +7247,12 @@ function getContextMenuItems$2(display) {
7231
7247
  session.showWidget(apolloFeatureWidget);
7232
7248
  },
7233
7249
  });
7234
- if (sourceFeature.type === 'mRNA' && isSessionModelWithWidgets(session)) {
7250
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
7251
+ if (!featureTypeOntology) {
7252
+ throw new Error('featureTypeOntology is undefined');
7253
+ }
7254
+ if (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
7255
+ isSessionModelWithWidgets(session)) {
7235
7256
  menuItems.push({
7236
7257
  label: 'Edit transcript details',
7237
7258
  onClick: () => {
@@ -7389,6 +7410,11 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7389
7410
  return;
7390
7411
  }
7391
7412
  const { apolloSelectedFeature } = session;
7413
+ const { apolloDataStore } = session;
7414
+ const { featureTypeOntology } = apolloDataStore.ontologyManager;
7415
+ if (!featureTypeOntology) {
7416
+ throw new Error('featureTypeOntology is undefined');
7417
+ }
7392
7418
  // Draw background for gene
7393
7419
  const topLevelFeatureMinX = (lgv.bpToPx({
7394
7420
  refName,
@@ -7400,22 +7426,23 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7400
7426
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
7401
7427
  : topLevelFeatureMinX;
7402
7428
  const topLevelFeatureTop = row * rowHeight;
7403
- const topLevelFeatureHeight = getRowCount$1(feature) * rowHeight;
7429
+ const topLevelFeatureHeight = getRowCount$1(feature, featureTypeOntology) * rowHeight;
7404
7430
  ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6);
7405
7431
  ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
7406
- // Draw lines on different rows for each mRNA
7432
+ // Draw lines on different rows for each transcript
7407
7433
  let currentRow = 0;
7408
- for (const [, mrna] of children) {
7409
- if (mrna.type !== 'mRNA') {
7434
+ for (const [, transcript] of children) {
7435
+ const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript');
7436
+ if (!isTranscript) {
7410
7437
  currentRow += 1;
7411
7438
  continue;
7412
7439
  }
7413
- const { children: childrenOfmRNA, min } = mrna;
7414
- if (!childrenOfmRNA) {
7440
+ const { children: childrenOfTranscript, min } = transcript;
7441
+ if (!childrenOfTranscript) {
7415
7442
  continue;
7416
7443
  }
7417
- for (const [, cds] of childrenOfmRNA) {
7418
- if (cds.type !== 'CDS') {
7444
+ for (const [, cds] of childrenOfTranscript) {
7445
+ if (!featureTypeOntology.isTypeOf(cds.type, 'CDS')) {
7419
7446
  continue;
7420
7447
  }
7421
7448
  const minX = (lgv.bpToPx({
@@ -7423,7 +7450,7 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7423
7450
  coord: min,
7424
7451
  regionNumber: displayedRegionIndex,
7425
7452
  })?.offsetPx ?? 0) - offsetPx;
7426
- const widthPx = mrna.length / bpPerPx;
7453
+ const widthPx = transcript.length / bpPerPx;
7427
7454
  const startPx = reversed ? minX - widthPx : minX;
7428
7455
  const height = Math.round((currentRow + 1 / 2) * rowHeight) + row * rowHeight;
7429
7456
  ctx.strokeStyle = theme?.palette.text.primary ?? 'black';
@@ -7436,21 +7463,21 @@ function draw$1(ctx, feature, row, stateModel, displayedRegionIndex) {
7436
7463
  }
7437
7464
  const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
7438
7465
  const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
7439
- // Draw exon and CDS for each mRNA
7466
+ // Draw exon and CDS for each transcript
7440
7467
  currentRow = 0;
7441
7468
  for (const [, child] of children) {
7442
- if (child.type !== 'mRNA') {
7469
+ if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
7443
7470
  boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex);
7444
7471
  currentRow += 1;
7445
7472
  continue;
7446
7473
  }
7447
7474
  for (const cdsRow of child.cdsLocations) {
7448
- const { _id, children: childrenOfmRNA } = child;
7449
- if (!childrenOfmRNA) {
7475
+ const { _id, children: childrenOfTranscript } = child;
7476
+ if (!childrenOfTranscript) {
7450
7477
  continue;
7451
7478
  }
7452
- for (const [, exon] of childrenOfmRNA) {
7453
- if (exon.type !== 'exon') {
7479
+ for (const [, exon] of childrenOfTranscript) {
7480
+ if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
7454
7481
  continue;
7455
7482
  }
7456
7483
  const minX = (lgv.bpToPx({
@@ -7546,7 +7573,8 @@ function drawDragPreview$1(stateModel, overlayCtx) {
7546
7573
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight);
7547
7574
  }
7548
7575
  function drawHover$1(stateModel, ctx) {
7549
- const { apolloHover, apolloRowHeight, lgv, theme } = stateModel;
7576
+ const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel;
7577
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
7550
7578
  if (!apolloHover) {
7551
7579
  return;
7552
7580
  }
@@ -7569,10 +7597,13 @@ function drawHover$1(stateModel, ctx) {
7569
7597
  const top = row * apolloRowHeight;
7570
7598
  const widthPx = length / bpPerPx;
7571
7599
  ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)';
7572
- ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature));
7600
+ if (!featureTypeOntology) {
7601
+ throw new Error('featureTypeOntology is undefined');
7602
+ }
7603
+ ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature, featureTypeOntology));
7573
7604
  }
7574
- function getFeatureFromLayout$1(feature, bp, row) {
7575
- const featureInThisRow = featuresForRow$1(feature)[row] || [];
7605
+ function getFeatureFromLayout$1(feature, bp, row, featureTypeOntology) {
7606
+ const featureInThisRow = featuresForRow$1(feature, featureTypeOntology)[row] || [];
7576
7607
  for (const f of featureInThisRow) {
7577
7608
  let featureObj;
7578
7609
  if (bp >= f.min && bp <= f.max && f.parent) {
@@ -7581,9 +7612,9 @@ function getFeatureFromLayout$1(feature, bp, row) {
7581
7612
  if (!featureObj) {
7582
7613
  continue;
7583
7614
  }
7584
- if (featureObj.type === 'CDS' &&
7615
+ if (featureTypeOntology.isTypeOf(featureObj.type, 'CDS') &&
7585
7616
  featureObj.parent &&
7586
- featureObj.parent.type === 'mRNA') {
7617
+ featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript')) {
7587
7618
  const { cdsLocations } = featureObj.parent;
7588
7619
  for (const cdsLoc of cdsLocations) {
7589
7620
  for (const loc of cdsLoc) {
@@ -7592,7 +7623,7 @@ function getFeatureFromLayout$1(feature, bp, row) {
7592
7623
  }
7593
7624
  }
7594
7625
  }
7595
- // If mouse position is in the intron region, return the mRNA
7626
+ // If mouse position is in the intron region, return the transcript
7596
7627
  return featureObj.parent;
7597
7628
  }
7598
7629
  // If mouse position is in a feature that is not a CDS, return the feature
@@ -7600,33 +7631,36 @@ function getFeatureFromLayout$1(feature, bp, row) {
7600
7631
  }
7601
7632
  return feature;
7602
7633
  }
7603
- function getRowCount$1(feature, _bpPerPx) {
7634
+ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
7604
7635
  const { children, type } = feature;
7605
7636
  if (!children) {
7606
7637
  return 1;
7607
7638
  }
7639
+ const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript');
7608
7640
  let rowCount = 0;
7609
- if (type === 'mRNA') {
7641
+ if (isTranscript) {
7610
7642
  for (const [, child] of children) {
7611
- if (child.type === 'CDS') {
7643
+ const isCds = featureTypeOntology.isTypeOf(child.type, 'CDS');
7644
+ if (isCds) {
7612
7645
  rowCount += 1;
7613
7646
  }
7614
7647
  }
7615
7648
  return rowCount;
7616
7649
  }
7617
7650
  for (const [, child] of children) {
7618
- rowCount += getRowCount$1(child);
7651
+ rowCount += getRowCount$1(child, featureTypeOntology);
7619
7652
  }
7620
7653
  return rowCount;
7621
7654
  }
7622
7655
  /**
7623
7656
  * A list of all the subfeatures for each row for a given feature, as well as
7624
7657
  * the feature itself.
7625
- * If the row contains an mRNA, the order is CDS -\> exon -\> mRNA -\> gene
7626
- * If the row does not contain an mRNA, the order is subfeature -\> gene
7658
+ * If the row contains a transcript, the order is CDS -\> exon -\> transcript -\> gene
7659
+ * If the row does not contain an transcript, the order is subfeature -\> gene
7627
7660
  */
7628
- function featuresForRow$1(feature) {
7629
- if (feature.type !== 'gene') {
7661
+ function featuresForRow$1(feature, featureTypeOntology) {
7662
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
7663
+ if (!isGene) {
7630
7664
  throw new Error('Top level feature for GeneGlyph must have type "gene"');
7631
7665
  }
7632
7666
  const { children } = feature;
@@ -7635,7 +7669,7 @@ function featuresForRow$1(feature) {
7635
7669
  }
7636
7670
  const features = [];
7637
7671
  for (const [, child] of children) {
7638
- if (child.type !== 'mRNA') {
7672
+ if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
7639
7673
  features.push([child, feature]);
7640
7674
  continue;
7641
7675
  }
@@ -7645,10 +7679,10 @@ function featuresForRow$1(feature) {
7645
7679
  const cdss = [];
7646
7680
  const exons = [];
7647
7681
  for (const [, grandchild] of child.children) {
7648
- if (grandchild.type === 'CDS') {
7682
+ if (featureTypeOntology.isTypeOf(grandchild.type, 'CDS')) {
7649
7683
  cdss.push(grandchild);
7650
7684
  }
7651
- else if (grandchild.type === 'exon') {
7685
+ else if (featureTypeOntology.isTypeOf(grandchild.type, 'exon')) {
7652
7686
  exons.push(grandchild);
7653
7687
  }
7654
7688
  }
@@ -7658,8 +7692,8 @@ function featuresForRow$1(feature) {
7658
7692
  }
7659
7693
  return features;
7660
7694
  }
7661
- function getRowForFeature$1(feature, childFeature) {
7662
- const rows = featuresForRow$1(feature);
7695
+ function getRowForFeature$1(feature, childFeature, featureTypeOntology) {
7696
+ const rows = featuresForRow$1(feature, featureTypeOntology);
7663
7697
  for (const [idx, row] of rows.entries()) {
7664
7698
  if (row.some((feature) => feature._id === childFeature._id)) {
7665
7699
  return idx;
@@ -7701,7 +7735,16 @@ function onMouseUp$1(stateModel, mousePosition) {
7701
7735
  }
7702
7736
  }
7703
7737
  function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7704
- if (feature.type === 'gene' || feature.type === 'mRNA') {
7738
+ const { session } = stateModel;
7739
+ const { apolloDataStore } = session;
7740
+ const { featureTypeOntology } = apolloDataStore.ontologyManager;
7741
+ if (!featureTypeOntology) {
7742
+ throw new Error('featureTypeOntology is undefined');
7743
+ }
7744
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
7745
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript');
7746
+ const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS');
7747
+ if (isGene || isTranscript) {
7705
7748
  return;
7706
7749
  }
7707
7750
  const { bp, refName, regionNumber, x } = mousePosition;
@@ -7712,517 +7755,1019 @@ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
7712
7755
  if (minPxInfo === undefined || maxPxInfo === undefined) {
7713
7756
  return;
7714
7757
  }
7715
- const minPx = minPxInfo.offsetPx - offsetPx;
7716
- const maxPx = maxPxInfo.offsetPx - offsetPx;
7717
- if (Math.abs(maxPx - minPx) < 8) {
7758
+ const minPx = minPxInfo.offsetPx - offsetPx;
7759
+ const maxPx = maxPxInfo.offsetPx - offsetPx;
7760
+ if (Math.abs(maxPx - minPx) < 8) {
7761
+ return;
7762
+ }
7763
+ if (Math.abs(minPx - x) < 4) {
7764
+ return { feature, edge: 'min' };
7765
+ }
7766
+ if (Math.abs(maxPx - x) < 4) {
7767
+ return { feature, edge: 'max' };
7768
+ }
7769
+ if (isCds) {
7770
+ const transcript = feature.parent;
7771
+ if (!transcript?.children) {
7772
+ return;
7773
+ }
7774
+ const exonChildren = [];
7775
+ for (const child of transcript.children.values()) {
7776
+ const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon');
7777
+ if (childIsExon) {
7778
+ exonChildren.push(child);
7779
+ }
7780
+ }
7781
+ const overlappingExon = exonChildren.find((child) => {
7782
+ const [start, end] = intersection2(bp, bp + 1, child.min, child.max);
7783
+ return start !== undefined && end !== undefined;
7784
+ });
7785
+ if (!overlappingExon) {
7786
+ return;
7787
+ }
7788
+ const minPxInfo = lgv.bpToPx({
7789
+ refName,
7790
+ coord: overlappingExon.min,
7791
+ regionNumber,
7792
+ });
7793
+ const maxPxInfo = lgv.bpToPx({
7794
+ refName,
7795
+ coord: overlappingExon.max,
7796
+ regionNumber,
7797
+ });
7798
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
7799
+ return;
7800
+ }
7801
+ const minPx = minPxInfo.offsetPx - offsetPx;
7802
+ const maxPx = maxPxInfo.offsetPx - offsetPx;
7803
+ if (Math.abs(maxPx - minPx) < 8) {
7804
+ return;
7805
+ }
7806
+ if (Math.abs(minPx - x) < 4) {
7807
+ return { feature: overlappingExon, edge: 'min' };
7808
+ }
7809
+ if (Math.abs(maxPx - x) < 4) {
7810
+ return { feature: overlappingExon, edge: 'max' };
7811
+ }
7812
+ }
7813
+ return;
7814
+ }
7815
+ // False positive here, none of these functions use "this"
7816
+ /* eslint-disable @typescript-eslint/unbound-method */
7817
+ const { drawTooltip: drawTooltip$1, getContextMenuItems: getContextMenuItems$1, onMouseLeave: onMouseLeave$1 } = boxGlyph;
7818
+ /* eslint-enable @typescript-eslint/unbound-method */
7819
+ const geneGlyph = {
7820
+ draw: draw$1,
7821
+ drawDragPreview: drawDragPreview$1,
7822
+ drawHover: drawHover$1,
7823
+ drawTooltip: drawTooltip$1,
7824
+ getContextMenuItems: getContextMenuItems$1,
7825
+ getFeatureFromLayout: getFeatureFromLayout$1,
7826
+ getRowCount: getRowCount$1,
7827
+ getRowForFeature: getRowForFeature$1,
7828
+ onMouseDown: onMouseDown$1,
7829
+ onMouseLeave: onMouseLeave$1,
7830
+ onMouseMove: onMouseMove$1,
7831
+ onMouseUp: onMouseUp$1,
7832
+ };
7833
+
7834
+ function featuresForRow(feature) {
7835
+ const features = [[feature]];
7836
+ if (feature.children) {
7837
+ for (const [, child] of feature.children) {
7838
+ features.push(...featuresForRow(child));
7839
+ }
7840
+ }
7841
+ return features;
7842
+ }
7843
+ function getRowCount(feature) {
7844
+ return featuresForRow(feature).length;
7845
+ }
7846
+ function draw(ctx, feature, row, stateModel, displayedRegionIndex) {
7847
+ for (let i = 0; i < getRowCount(feature); i++) {
7848
+ drawRow(ctx, feature, row + i, row, stateModel, displayedRegionIndex);
7849
+ }
7850
+ }
7851
+ function drawRow(ctx, topLevelFeature, row, topRow, stateModel, displayedRegionIndex) {
7852
+ const features = featuresForRow(topLevelFeature)[row - topRow];
7853
+ for (const feature of features) {
7854
+ drawFeature(ctx, feature, row, stateModel, displayedRegionIndex);
7855
+ }
7856
+ }
7857
+ function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
7858
+ const { apolloRowHeight: heightPx, lgv, session } = stateModel;
7859
+ const { bpPerPx, displayedRegions, offsetPx } = lgv;
7860
+ const displayedRegion = displayedRegions[displayedRegionIndex];
7861
+ const minX = (lgv.bpToPx({
7862
+ refName: displayedRegion.refName,
7863
+ coord: feature.min,
7864
+ regionNumber: displayedRegionIndex,
7865
+ })?.offsetPx ?? 0) - offsetPx;
7866
+ const { reversed } = displayedRegion;
7867
+ const { apolloSelectedFeature } = session;
7868
+ const widthPx = feature.length / bpPerPx;
7869
+ const startPx = reversed ? minX - widthPx : minX;
7870
+ const top = row * heightPx;
7871
+ const rowCount = getRowCount(feature);
7872
+ const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
7873
+ const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
7874
+ if (rowCount > 1) {
7875
+ // draw background that encapsulates all child features
7876
+ const featureHeight = rowCount * heightPx;
7877
+ drawBox(ctx, startPx, top, widthPx, featureHeight, groupingColor);
7878
+ }
7879
+ boxGlyph.draw(ctx, feature, row, stateModel, displayedRegionIndex);
7880
+ }
7881
+ function drawHover(stateModel, ctx) {
7882
+ const { apolloHover, apolloRowHeight, lgv } = stateModel;
7883
+ if (!apolloHover) {
7884
+ return;
7885
+ }
7886
+ const { feature } = apolloHover;
7887
+ const position = stateModel.getFeatureLayoutPosition(feature);
7888
+ if (!position) {
7718
7889
  return;
7719
7890
  }
7720
- if (Math.abs(minPx - x) < 4) {
7721
- return { feature, edge: 'min' };
7722
- }
7723
- if (Math.abs(maxPx - x) < 4) {
7724
- return { feature, edge: 'max' };
7725
- }
7726
- if (feature.type === 'CDS') {
7727
- const mRNA = feature.parent;
7728
- if (!mRNA?.children) {
7729
- return;
7730
- }
7731
- const exonChildren = [...mRNA.children.values()].filter((child) => child.type === 'exon');
7732
- const overlappingExon = exonChildren.find((child) => {
7733
- const [start, end] = intersection2(bp, bp + 1, child.min, child.max);
7734
- return start !== undefined && end !== undefined;
7735
- });
7736
- if (!overlappingExon) {
7737
- return;
7738
- }
7739
- const minPxInfo = lgv.bpToPx({
7740
- refName,
7741
- coord: overlappingExon.min,
7742
- regionNumber,
7743
- });
7744
- const maxPxInfo = lgv.bpToPx({
7745
- refName,
7746
- coord: overlappingExon.max,
7747
- regionNumber,
7748
- });
7749
- if (minPxInfo === undefined || maxPxInfo === undefined) {
7750
- return;
7751
- }
7752
- const minPx = minPxInfo.offsetPx - offsetPx;
7753
- const maxPx = maxPxInfo.offsetPx - offsetPx;
7754
- if (Math.abs(maxPx - minPx) < 8) {
7755
- return;
7756
- }
7757
- if (Math.abs(minPx - x) < 4) {
7758
- return { feature: overlappingExon, edge: 'min' };
7759
- }
7760
- if (Math.abs(maxPx - x) < 4) {
7761
- return { feature: overlappingExon, edge: 'max' };
7891
+ const { featureRow, layoutIndex, layoutRow } = position;
7892
+ const { bpPerPx, displayedRegions, offsetPx } = lgv;
7893
+ const displayedRegion = displayedRegions[layoutIndex];
7894
+ const { refName, reversed } = displayedRegion;
7895
+ const { length, max, min } = feature;
7896
+ const startPx = (lgv.bpToPx({
7897
+ refName,
7898
+ coord: reversed ? max : min,
7899
+ regionNumber: layoutIndex,
7900
+ })?.offsetPx ?? 0) - offsetPx;
7901
+ const top = (layoutRow + featureRow) * apolloRowHeight;
7902
+ const widthPx = length / bpPerPx;
7903
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
7904
+ ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature));
7905
+ }
7906
+ function getFeatureFromLayout(feature, bp, row) {
7907
+ const layoutRow = featuresForRow(feature)[row];
7908
+ return layoutRow.find((f) => bp >= f.min && bp <= f.max);
7909
+ }
7910
+ function getRowForFeature(feature, childFeature) {
7911
+ const rows = featuresForRow(feature);
7912
+ for (const [idx, row] of rows.entries()) {
7913
+ if (row.some((feature) => feature._id === childFeature._id)) {
7914
+ return idx;
7762
7915
  }
7763
7916
  }
7764
7917
  return;
7765
7918
  }
7766
- // False positive here, none of these functions use "this"
7767
- /* eslint-disable @typescript-eslint/unbound-method */
7768
- const { drawTooltip: drawTooltip$1, getContextMenuItems: getContextMenuItems$1, onMouseLeave: onMouseLeave$1 } = boxGlyph;
7769
- /* eslint-enable @typescript-eslint/unbound-method */
7770
- const geneGlyph = {
7771
- draw: draw$1,
7772
- drawDragPreview: drawDragPreview$1,
7773
- drawHover: drawHover$1,
7774
- drawTooltip: drawTooltip$1,
7775
- getContextMenuItems: getContextMenuItems$1,
7776
- getFeatureFromLayout: getFeatureFromLayout$1,
7777
- getRowCount: getRowCount$1,
7778
- getRowForFeature: getRowForFeature$1,
7779
- onMouseDown: onMouseDown$1,
7780
- onMouseLeave: onMouseLeave$1,
7781
- onMouseMove: onMouseMove$1,
7782
- onMouseUp: onMouseUp$1,
7783
- };
7919
+ // False positive here, none of these functions use "this"
7920
+ /* eslint-disable @typescript-eslint/unbound-method */
7921
+ const { drawDragPreview, drawTooltip, getContextMenuItems, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, } = boxGlyph;
7922
+ /* eslint-enable @typescript-eslint/unbound-method */
7923
+ const genericChildGlyph = {
7924
+ draw,
7925
+ drawDragPreview,
7926
+ drawHover,
7927
+ drawTooltip,
7928
+ getContextMenuItems,
7929
+ getFeatureFromLayout,
7930
+ getRowCount,
7931
+ getRowForFeature,
7932
+ onMouseDown,
7933
+ onMouseLeave,
7934
+ onMouseMove,
7935
+ onMouseUp,
7936
+ };
7937
+
7938
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
7939
+ function layoutsModelFactory(pluginManager, configSchema) {
7940
+ const BaseLinearApolloDisplay = baseModelFactory(pluginManager, configSchema);
7941
+ return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
7942
+ .props({
7943
+ featuresMinMaxLimit: 500_000,
7944
+ })
7945
+ .volatile(() => ({
7946
+ seenFeatures: observable.map(),
7947
+ }))
7948
+ .views((self) => ({
7949
+ get featuresMinMax() {
7950
+ const { assemblyManager } = self.session;
7951
+ return self.lgv.displayedRegions.map((region) => {
7952
+ const assembly = assemblyManager.get(region.assemblyName);
7953
+ let min;
7954
+ let max;
7955
+ const { end, refName, start } = region;
7956
+ for (const [, feature] of self.seenFeatures) {
7957
+ if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
7958
+ !doesIntersect2(start, end, feature.min, feature.max) ||
7959
+ feature.length > self.featuresMinMaxLimit ||
7960
+ (self.filteredFeatureTypes.length > 0 &&
7961
+ !self.filteredFeatureTypes.includes(feature.type))) {
7962
+ continue;
7963
+ }
7964
+ if (min === undefined) {
7965
+ ({ min } = feature);
7966
+ }
7967
+ if (max === undefined) {
7968
+ ({ max } = feature);
7969
+ }
7970
+ if (feature.minWithChildren < min) {
7971
+ ({ min } = feature);
7972
+ }
7973
+ if (feature.maxWithChildren > max) {
7974
+ ({ max } = feature);
7975
+ }
7976
+ }
7977
+ if (min !== undefined && max !== undefined) {
7978
+ return [min, max];
7979
+ }
7980
+ return;
7981
+ });
7982
+ },
7983
+ getGlyph(feature) {
7984
+ if (this.looksLikeGene(feature)) {
7985
+ return geneGlyph;
7986
+ }
7987
+ if (feature.children?.size) {
7988
+ return genericChildGlyph;
7989
+ }
7990
+ return boxGlyph;
7991
+ },
7992
+ looksLikeGene(feature) {
7993
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
7994
+ if (!featureTypeOntology) {
7995
+ return false;
7996
+ }
7997
+ const { children } = feature;
7998
+ if (!children?.size) {
7999
+ return false;
8000
+ }
8001
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
8002
+ if (!isGene) {
8003
+ return false;
8004
+ }
8005
+ for (const [, child] of children) {
8006
+ if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
8007
+ const { children: grandChildren } = child;
8008
+ if (!grandChildren?.size) {
8009
+ return false;
8010
+ }
8011
+ const hasCDS = [...grandChildren.values()].some((grandchild) => featureTypeOntology.isTypeOf(grandchild.type, 'CDS'));
8012
+ const hasExon = [...grandChildren.values()].some((grandchild) => featureTypeOntology.isTypeOf(grandchild.type, 'exon'));
8013
+ if (hasCDS && hasExon) {
8014
+ return true;
8015
+ }
8016
+ }
8017
+ }
8018
+ return false;
8019
+ },
8020
+ }))
8021
+ .actions((self) => ({
8022
+ addSeenFeature(feature) {
8023
+ self.seenFeatures.set(feature._id, feature);
8024
+ },
8025
+ deleteSeenFeature(featureId) {
8026
+ self.seenFeatures.delete(featureId);
8027
+ },
8028
+ }))
8029
+ .views((self) => ({
8030
+ get featureLayouts() {
8031
+ const { assemblyManager } = self.session;
8032
+ return self.lgv.displayedRegions.map((region, idx) => {
8033
+ const assembly = assemblyManager.get(region.assemblyName);
8034
+ const featureLayout = new Map();
8035
+ const minMax = self.featuresMinMax[idx];
8036
+ if (!minMax) {
8037
+ return featureLayout;
8038
+ }
8039
+ const [min, max] = minMax;
8040
+ const rows = [];
8041
+ const { end, refName, start } = region;
8042
+ for (const [id, feature] of self.seenFeatures.entries()) {
8043
+ if (!isAlive(feature)) {
8044
+ self.deleteSeenFeature(id);
8045
+ continue;
8046
+ }
8047
+ if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
8048
+ !doesIntersect2(start, end, feature.min, feature.max) ||
8049
+ (self.filteredFeatureTypes.length > 0 &&
8050
+ !self.filteredFeatureTypes.includes(feature.type))) {
8051
+ continue;
8052
+ }
8053
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8054
+ if (!featureTypeOntology) {
8055
+ throw new Error('featureTypeOntology is undefined');
8056
+ }
8057
+ const rowCount = self
8058
+ .getGlyph(feature)
8059
+ .getRowCount(feature, featureTypeOntology, self.lgv.bpPerPx);
8060
+ let startingRow = 0;
8061
+ let placed = false;
8062
+ while (!placed) {
8063
+ let rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
8064
+ if (rowsForFeature.length < rowCount) {
8065
+ for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
8066
+ const newRowNumber = rows.length;
8067
+ rows[newRowNumber] = Array.from({ length: max - min });
8068
+ featureLayout.set(newRowNumber, []);
8069
+ }
8070
+ rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
8071
+ }
8072
+ if (rowsForFeature
8073
+ .map((rowForFeature) => {
8074
+ // zero-length features are allowed in the spec
8075
+ const featureMax = feature.max - feature.min === 0
8076
+ ? feature.min + 1
8077
+ : feature.max;
8078
+ let start = feature.min - min, end = featureMax - min;
8079
+ if (feature.min - min < 0) {
8080
+ start = 0;
8081
+ end = featureMax - feature.min;
8082
+ }
8083
+ return rowForFeature.slice(start, end).some(Boolean);
8084
+ })
8085
+ .some(Boolean)) {
8086
+ startingRow += 1;
8087
+ continue;
8088
+ }
8089
+ for (let rowNum = startingRow; rowNum < startingRow + rowCount; rowNum++) {
8090
+ const row = rows[rowNum];
8091
+ let start = feature.min - min, end = feature.max - min;
8092
+ if (feature.min - min < 0) {
8093
+ start = 0;
8094
+ end = feature.max - feature.min;
8095
+ }
8096
+ row.fill(true, start, end);
8097
+ const layoutRow = featureLayout.get(rowNum);
8098
+ layoutRow?.push([rowNum - startingRow, feature]);
8099
+ }
8100
+ placed = true;
8101
+ }
8102
+ }
8103
+ return featureLayout;
8104
+ });
8105
+ },
8106
+ getFeatureLayoutPosition(feature) {
8107
+ const { featureLayouts } = this;
8108
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8109
+ for (const [idx, layout] of featureLayouts.entries()) {
8110
+ for (const [layoutRowNum, layoutRow] of layout) {
8111
+ for (const [featureRowNum, layoutFeature] of layoutRow) {
8112
+ if (featureRowNum !== 0) {
8113
+ // Same top-level feature in all feature rows, so only need to
8114
+ // check the first one
8115
+ continue;
8116
+ }
8117
+ if (feature._id === layoutFeature._id) {
8118
+ return {
8119
+ layoutIndex: idx,
8120
+ layoutRow: layoutRowNum,
8121
+ featureRow: featureRowNum,
8122
+ };
8123
+ }
8124
+ if (layoutFeature.hasDescendant(feature._id)) {
8125
+ if (!featureTypeOntology) {
8126
+ throw new Error('featureTypeOntology is undefined');
8127
+ }
8128
+ const row = self
8129
+ .getGlyph(layoutFeature)
8130
+ .getRowForFeature(layoutFeature, feature, featureTypeOntology);
8131
+ if (row !== undefined) {
8132
+ return {
8133
+ layoutIndex: idx,
8134
+ layoutRow: layoutRowNum,
8135
+ featureRow: row,
8136
+ };
8137
+ }
8138
+ }
8139
+ }
8140
+ }
8141
+ }
8142
+ return;
8143
+ },
8144
+ }))
8145
+ .views((self) => ({
8146
+ get highestRow() {
8147
+ return Math.max(0, ...self.featureLayouts.map((layout) => Math.max(...layout.keys())));
8148
+ },
8149
+ }))
8150
+ .actions((self) => ({
8151
+ afterAttach() {
8152
+ addDisposer(self, autorun(() => {
8153
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8154
+ return;
8155
+ }
8156
+ for (const region of self.regions) {
8157
+ const assembly = self.session.apolloDataStore.assemblies.get(region.assemblyName);
8158
+ const ref = assembly?.getByRefName(region.refName);
8159
+ const features = ref?.features;
8160
+ if (!features) {
8161
+ continue;
8162
+ }
8163
+ for (const [, feature] of features) {
8164
+ if (doesIntersect2(region.start, region.end, feature.min, feature.max) &&
8165
+ !self.seenFeatures.has(feature._id)) {
8166
+ self.addSeenFeature(feature);
8167
+ }
8168
+ }
8169
+ }
8170
+ }, { name: 'LinearApolloDisplaySetSeenFeatures', delay: 1000 }));
8171
+ },
8172
+ }));
8173
+ }
7784
8174
 
7785
- function featuresForRow(feature) {
7786
- const features = [[feature]];
7787
- if (feature.children) {
7788
- for (const [, child] of feature.children) {
7789
- features.push(...featuresForRow(child));
7790
- }
7791
- }
7792
- return features;
7793
- }
7794
- function getRowCount(feature) {
7795
- return featuresForRow(feature).length;
7796
- }
7797
- function draw(ctx, feature, row, stateModel, displayedRegionIndex) {
7798
- for (let i = 0; i < getRowCount(feature); i++) {
7799
- drawRow(ctx, feature, row + i, row, stateModel, displayedRegionIndex);
7800
- }
7801
- }
7802
- function drawRow(ctx, topLevelFeature, row, topRow, stateModel, displayedRegionIndex) {
7803
- const features = featuresForRow(topLevelFeature)[row - topRow];
7804
- for (const feature of features) {
7805
- drawFeature(ctx, feature, row, stateModel, displayedRegionIndex);
7806
- }
7807
- }
7808
- function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
7809
- const { apolloRowHeight: heightPx, lgv, session } = stateModel;
7810
- const { bpPerPx, displayedRegions, offsetPx } = lgv;
7811
- const displayedRegion = displayedRegions[displayedRegionIndex];
7812
- const minX = (lgv.bpToPx({
7813
- refName: displayedRegion.refName,
7814
- coord: feature.min,
7815
- regionNumber: displayedRegionIndex,
7816
- })?.offsetPx ?? 0) - offsetPx;
7817
- const { reversed } = displayedRegion;
7818
- const { apolloSelectedFeature } = session;
7819
- const widthPx = feature.length / bpPerPx;
7820
- const startPx = reversed ? minX - widthPx : minX;
7821
- const top = row * heightPx;
7822
- const rowCount = getRowCount(feature);
7823
- const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
7824
- const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
7825
- if (rowCount > 1) {
7826
- // draw background that encapsulates all child features
7827
- const featureHeight = rowCount * heightPx;
7828
- drawBox(ctx, startPx, top, widthPx, featureHeight, groupingColor);
7829
- }
7830
- boxGlyph.draw(ctx, feature, row, stateModel, displayedRegionIndex);
8175
+ function renderingModelIntermediateFactory(pluginManager, configSchema) {
8176
+ const LinearApolloDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
8177
+ return LinearApolloDisplayLayouts.named('LinearApolloDisplayRendering')
8178
+ .props({
8179
+ sequenceRowHeight: 15,
8180
+ apolloRowHeight: 20,
8181
+ detailsMinHeight: 200,
8182
+ detailsHeight: 200,
8183
+ lastRowTooltipBufferHeight: 40,
8184
+ isShown: true,
8185
+ })
8186
+ .volatile(() => ({
8187
+ canvas: null,
8188
+ overlayCanvas: null,
8189
+ collaboratorCanvas: null,
8190
+ seqTrackCanvas: null,
8191
+ seqTrackOverlayCanvas: null,
8192
+ theme: undefined,
8193
+ }))
8194
+ .views((self) => ({
8195
+ get featuresHeight() {
8196
+ return ((self.highestRow + 1) * self.apolloRowHeight +
8197
+ self.lastRowTooltipBufferHeight);
8198
+ },
8199
+ }))
8200
+ .actions((self) => ({
8201
+ toggleShown() {
8202
+ self.isShown = !self.isShown;
8203
+ },
8204
+ setDetailsHeight(newHeight) {
8205
+ self.detailsHeight = self.isShown
8206
+ ? Math.max(Math.min(newHeight, self.height - 100), Math.min(self.height, self.detailsMinHeight))
8207
+ : newHeight;
8208
+ },
8209
+ setCanvas(canvas) {
8210
+ self.canvas = canvas;
8211
+ },
8212
+ setOverlayCanvas(canvas) {
8213
+ self.overlayCanvas = canvas;
8214
+ },
8215
+ setCollaboratorCanvas(canvas) {
8216
+ self.collaboratorCanvas = canvas;
8217
+ },
8218
+ setSeqTrackCanvas(canvas) {
8219
+ self.seqTrackCanvas = canvas;
8220
+ },
8221
+ setSeqTrackOverlayCanvas(canvas) {
8222
+ self.seqTrackOverlayCanvas = canvas;
8223
+ },
8224
+ setTheme(theme) {
8225
+ self.theme = theme;
8226
+ },
8227
+ afterAttach() {
8228
+ addDisposer(self, autorun(() => {
8229
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8230
+ return;
8231
+ }
8232
+ const ctx = self.collaboratorCanvas?.getContext('2d');
8233
+ if (!ctx) {
8234
+ return;
8235
+ }
8236
+ ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
8237
+ for (const collaborator of self.session.collaborators) {
8238
+ const { locations } = collaborator;
8239
+ if (locations.length === 0) {
8240
+ continue;
8241
+ }
8242
+ let idx = 0;
8243
+ for (const displayedRegion of self.lgv.displayedRegions) {
8244
+ for (const location of locations) {
8245
+ if (location.refSeq !== displayedRegion.refName) {
8246
+ continue;
8247
+ }
8248
+ const { end, refSeq, start } = location;
8249
+ const locationStartPxInfo = self.lgv.bpToPx({
8250
+ refName: refSeq,
8251
+ coord: start,
8252
+ regionNumber: idx,
8253
+ });
8254
+ if (!locationStartPxInfo) {
8255
+ continue;
8256
+ }
8257
+ const locationStartPx = locationStartPxInfo.offsetPx - self.lgv.offsetPx;
8258
+ const locationWidthPx = (end - start) / self.lgv.bpPerPx;
8259
+ ctx.fillStyle = 'rgba(0,255,0,.2)';
8260
+ ctx.fillRect(locationStartPx, 1, locationWidthPx, 100);
8261
+ ctx.fillStyle = 'black';
8262
+ ctx.fillText(collaborator.name, locationStartPx + 1, 11, locationWidthPx - 2);
8263
+ }
8264
+ idx++;
8265
+ }
8266
+ }
8267
+ }, { name: 'LinearApolloDisplayRenderCollaborators' }));
8268
+ },
8269
+ }));
7831
8270
  }
7832
- function drawHover(stateModel, ctx) {
7833
- const { apolloHover, apolloRowHeight, lgv } = stateModel;
7834
- if (!apolloHover) {
7835
- return;
7836
- }
7837
- const { feature } = apolloHover;
7838
- const position = stateModel.getFeatureLayoutPosition(feature);
7839
- if (!position) {
7840
- return;
7841
- }
7842
- const { featureRow, layoutIndex, layoutRow } = position;
7843
- const { bpPerPx, displayedRegions, offsetPx } = lgv;
7844
- const displayedRegion = displayedRegions[layoutIndex];
7845
- const { refName, reversed } = displayedRegion;
7846
- const { length, max, min } = feature;
7847
- const startPx = (lgv.bpToPx({
7848
- refName,
7849
- coord: reversed ? max : min,
7850
- regionNumber: layoutIndex,
7851
- })?.offsetPx ?? 0) - offsetPx;
7852
- const top = (layoutRow + featureRow) * apolloRowHeight;
7853
- const widthPx = length / bpPerPx;
7854
- ctx.fillStyle = 'rgba(0,0,0,0.2)';
7855
- ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature));
8271
+ function colorCode(letter, theme) {
8272
+ return (theme?.palette.bases[letter.toUpperCase()].main.toString() ?? 'lightgray');
7856
8273
  }
7857
- function getFeatureFromLayout(feature, bp, row) {
7858
- const layoutRow = featuresForRow(feature)[row];
7859
- return layoutRow.find((f) => bp >= f.min && bp <= f.max);
8274
+ function codonColorCode(letter) {
8275
+ const colorMap = {
8276
+ M: '#33ee33',
8277
+ '*': '#f44336',
8278
+ };
8279
+ return colorMap[letter.toUpperCase()];
7860
8280
  }
7861
- function getRowForFeature(feature, childFeature) {
7862
- const rows = featuresForRow(feature);
7863
- for (const [idx, row] of rows.entries()) {
7864
- if (row.some((feature) => feature._id === childFeature._id)) {
7865
- return idx;
7866
- }
7867
- }
7868
- return;
8281
+ function reverseCodonSeq(seq) {
8282
+ return [...seq]
8283
+ .map((c) => revcom(c))
8284
+ .reverse()
8285
+ .join('');
7869
8286
  }
7870
- // False positive here, none of these functions use "this"
7871
- /* eslint-disable @typescript-eslint/unbound-method */
7872
- const { drawDragPreview, drawTooltip, getContextMenuItems, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, } = boxGlyph;
7873
- /* eslint-enable @typescript-eslint/unbound-method */
7874
- const genericChildGlyph = {
7875
- draw,
7876
- drawDragPreview,
7877
- drawHover,
7878
- drawTooltip,
7879
- getContextMenuItems,
7880
- getFeatureFromLayout,
7881
- getRowCount,
7882
- getRowForFeature,
7883
- onMouseDown,
7884
- onMouseLeave,
7885
- onMouseMove,
7886
- onMouseUp,
7887
- };
7888
-
7889
- /** get the appropriate glyph for the given top-level feature */
7890
- function getGlyph(feature) {
7891
- if (looksLikeGene(feature)) {
7892
- return geneGlyph;
8287
+ function drawLetter(seqTrackctx, startPx, widthPx, letter, textY) {
8288
+ const fontSize = Math.min(widthPx, 10);
8289
+ seqTrackctx.fillStyle = '#000';
8290
+ seqTrackctx.font = `${fontSize}px`;
8291
+ const textWidth = seqTrackctx.measureText(letter).width;
8292
+ const textX = startPx + (widthPx - textWidth) / 2;
8293
+ seqTrackctx.fillText(letter, textX, textY + 10);
8294
+ }
8295
+ function drawTranslation(seqTrackctx, bpPerPx, trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight, seq, i, reverse) {
8296
+ let codonSeq = seq.slice(i, i + 3).toUpperCase();
8297
+ if (reverse) {
8298
+ codonSeq = reverseCodonSeq(codonSeq);
7893
8299
  }
7894
- if (feature.children?.size) {
7895
- return genericChildGlyph;
8300
+ const codonLetter = defaultCodonTable[codonSeq];
8301
+ if (!codonLetter) {
8302
+ return;
7896
8303
  }
7897
- return boxGlyph;
7898
- }
7899
- function looksLikeGene(feature) {
7900
- const { children } = feature;
7901
- if (!children?.size) {
7902
- return false;
8304
+ const fillColor = codonColorCode(codonLetter);
8305
+ if (fillColor) {
8306
+ seqTrackctx.fillStyle = fillColor;
8307
+ seqTrackctx.fillRect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
7903
8308
  }
7904
- for (const [, child] of children) {
7905
- if (child.type === 'mRNA') {
7906
- const { children: grandChildren } = child;
7907
- if (!grandChildren?.size) {
7908
- return false;
7909
- }
7910
- const hasCDS = [...grandChildren.values()].some((grandchild) => grandchild.type === 'CDS');
7911
- const hasExon = [...grandChildren.values()].some((grandchild) => grandchild.type === 'exon');
7912
- if (hasCDS && hasExon) {
7913
- return true;
7914
- }
7915
- }
8309
+ if (bpPerPx <= 0.1) {
8310
+ seqTrackctx.rect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
8311
+ seqTrackctx.stroke();
8312
+ drawLetter(seqTrackctx, trnslStartPx, trnslWidthPx, codonLetter, trnslY);
7916
8313
  }
7917
- return false;
7918
8314
  }
7919
-
7920
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
7921
- const useStyles$4 = makeStyles()((theme) => ({
7922
- typeContent: {
7923
- display: 'inline-block',
7924
- width: '174px',
7925
- height: '100%',
7926
- cursor: 'text',
7927
- },
7928
- feature: {
7929
- td: {
7930
- position: 'relative',
7931
- verticalAlign: 'top',
7932
- paddingLeft: '0.5em',
8315
+ function sequenceRenderingModelFactory(pluginManager, configSchema) {
8316
+ const LinearApolloDisplayRendering = renderingModelIntermediateFactory(pluginManager, configSchema);
8317
+ return LinearApolloDisplayRendering.actions((self) => ({
8318
+ afterAttach() {
8319
+ addDisposer(self, autorun(async () => {
8320
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8321
+ return;
8322
+ }
8323
+ if (self.lgv.bpPerPx > 3) {
8324
+ return;
8325
+ }
8326
+ const seqTrackctx = self.seqTrackCanvas?.getContext('2d');
8327
+ if (!seqTrackctx) {
8328
+ return;
8329
+ }
8330
+ seqTrackctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
8331
+ const frames = self.lgv.bpPerPx <= 1
8332
+ ? [3, 2, 1, 0, 0, -1, -2, -3]
8333
+ : [3, 2, 1, -1, -2, -3];
8334
+ let height = 0;
8335
+ for (const frame of frames) {
8336
+ const frameColor = self.theme?.palette.framesCDS.at(frame)?.main;
8337
+ if (frameColor) {
8338
+ seqTrackctx.fillStyle = frameColor;
8339
+ seqTrackctx.fillRect(0, height, self.lgv.dynamicBlocks.totalWidthPx, self.sequenceRowHeight);
8340
+ }
8341
+ height += self.sequenceRowHeight;
8342
+ }
8343
+ for (const [idx, region] of self.regions.entries()) {
8344
+ const driver = self.session.apolloDataStore.getBackendDriver(region.assemblyName);
8345
+ if (!driver) {
8346
+ throw new Error('Failed to get the backend driver');
8347
+ }
8348
+ const { seq } = await driver.getSequence(region);
8349
+ if (!seq) {
8350
+ return;
8351
+ }
8352
+ for (const [i, letter] of [...seq].entries()) {
8353
+ const trnslXOffset = (self.lgv.bpToPx({
8354
+ refName: region.refName,
8355
+ coord: region.start + i,
8356
+ regionNumber: idx,
8357
+ })?.offsetPx ?? 0) - self.lgv.offsetPx;
8358
+ const trnslWidthPx = 3 / self.lgv.bpPerPx;
8359
+ const trnslStartPx = self.lgv.displayedRegions[idx].reversed
8360
+ ? trnslXOffset - trnslWidthPx
8361
+ : trnslXOffset;
8362
+ // Draw translation forward
8363
+ for (let j = 2; j >= 0; j--) {
8364
+ if ((region.start + i) % 3 === j) {
8365
+ drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (2 - j), trnslWidthPx, self.sequenceRowHeight, seq, i, false);
8366
+ }
8367
+ }
8368
+ if (self.lgv.bpPerPx <= 1) {
8369
+ const xOffset = (self.lgv.bpToPx({
8370
+ refName: region.refName,
8371
+ coord: region.start + i,
8372
+ regionNumber: idx,
8373
+ })?.offsetPx ?? 0) - self.lgv.offsetPx;
8374
+ const widthPx = 1 / self.lgv.bpPerPx;
8375
+ const startPx = self.lgv.displayedRegions[idx].reversed
8376
+ ? xOffset - widthPx
8377
+ : xOffset;
8378
+ // Draw forward
8379
+ seqTrackctx.beginPath();
8380
+ seqTrackctx.fillStyle = colorCode(letter, self.theme);
8381
+ seqTrackctx.rect(startPx, self.sequenceRowHeight * 3, widthPx, self.sequenceRowHeight);
8382
+ seqTrackctx.fill();
8383
+ if (self.lgv.bpPerPx <= 0.1) {
8384
+ seqTrackctx.stroke();
8385
+ drawLetter(seqTrackctx, startPx, widthPx, letter, self.sequenceRowHeight * 3);
8386
+ }
8387
+ // Draw reverse
8388
+ const revLetter = revcom(letter);
8389
+ seqTrackctx.beginPath();
8390
+ seqTrackctx.fillStyle = colorCode(revLetter, self.theme);
8391
+ seqTrackctx.rect(startPx, self.sequenceRowHeight * 4, widthPx, self.sequenceRowHeight);
8392
+ seqTrackctx.fill();
8393
+ if (self.lgv.bpPerPx <= 0.1) {
8394
+ seqTrackctx.stroke();
8395
+ drawLetter(seqTrackctx, startPx, widthPx, revLetter, self.sequenceRowHeight * 4);
8396
+ }
8397
+ }
8398
+ // Draw translation reverse
8399
+ for (let k = 0; k <= 2; k++) {
8400
+ const rowOffset = self.lgv.bpPerPx <= 1 ? 5 : 3;
8401
+ if ((region.start + i) % 3 === k) {
8402
+ drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (rowOffset + k), trnslWidthPx, self.sequenceRowHeight, seq, i, true);
8403
+ }
8404
+ }
8405
+ }
8406
+ }
8407
+ }, { name: 'LinearApolloDisplayRenderSequence' }));
7933
8408
  },
7934
- },
7935
- arrow: {
7936
- display: 'inline-block',
7937
- width: '1.6em',
7938
- textAlign: 'center',
7939
- cursor: 'pointer',
7940
- },
7941
- arrowExpanded: {
7942
- transform: 'rotate(90deg)',
7943
- },
7944
- hoveredFeature: {
7945
- backgroundColor: theme.palette.action.hover,
7946
- },
7947
- typeInputElement: {
7948
- border: 'none',
7949
- background: 'none',
7950
- },
7951
- typeErrorMessage: {
7952
- color: 'red',
7953
- },
7954
- }));
7955
- function makeContextMenuItems(display, feature) {
7956
- const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
7957
- return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
7958
- }
7959
- function getTopLevelFeature(feature) {
7960
- let cur = feature;
7961
- while (cur.parent) {
7962
- cur = cur.parent;
7963
- }
7964
- return cur;
8409
+ }));
7965
8410
  }
7966
- const Feature = observer(function Feature({ depth, feature, isHovered, isSelected, model: displayState, selectedFeatureClass, setContextMenu, }) {
7967
- const { classes } = useStyles$4();
7968
- const { apolloHover, changeManager, selectedFeature, session, tabularEditor: tabularEditorState, } = displayState;
7969
- const { featureCollapsed, filterText } = tabularEditorState;
7970
- const { _id, children, max, min, strand, type } = feature;
7971
- const expanded = !featureCollapsed.get(_id);
7972
- const toggleExpanded = (e) => {
7973
- e.stopPropagation();
7974
- tabularEditorState.setFeatureCollapsed(_id, expanded);
7975
- };
7976
- // pop up a snackbar in the session notifying user of an error
7977
- const notifyError = (e) => {
7978
- session.notify(e.message, 'error');
7979
- };
7980
- return (React__default.createElement(React__default.Fragment, null,
7981
- React__default.createElement("tr", { onMouseEnter: (_e) => {
7982
- displayState.setApolloHover({
7983
- feature,
7984
- topLevelFeature: getTopLevelFeature(feature),
7985
- glyph: getGlyph(getTopLevelFeature(feature)),
7986
- });
7987
- }, className: classes.feature +
7988
- (isSelected
7989
- ? ` ${selectedFeatureClass}`
7990
- : isHovered
7991
- ? ` ${classes.hoveredFeature}`
7992
- : ''), onClick: (e) => {
7993
- e.stopPropagation();
7994
- displayState.setSelectedFeature(feature);
7995
- }, onContextMenu: (e) => {
7996
- e.preventDefault();
7997
- setContextMenu({
7998
- position: { left: e.clientX + 2, top: e.clientY - 6 },
7999
- items: makeContextMenuItems(displayState, feature),
8000
- });
8001
- return false;
8002
- } },
8003
- React__default.createElement("td", { style: {
8004
- whiteSpace: 'nowrap',
8005
- borderLeft: `${depth * 2}em solid transparent`,
8006
- } },
8007
- children?.size ? (
8008
- // TODO: a11y
8009
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8010
- React__default.createElement("div", { onClick: toggleExpanded, className: classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '') }, "\u276F")) : null,
8011
- React__default.createElement("div", { className: classes.typeContent },
8012
- React__default.createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: 170 }, value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTypeTerms.bind(null, feature), renderInput: (params) => {
8013
- return (React__default.createElement("div", { ref: params.InputProps.ref },
8014
- React__default.createElement("input", { type: "text", ...params.inputProps, className: classes.typeInputElement, style: { width: 170 } }),
8015
- params.error ? (React__default.createElement("div", { className: classes.typeErrorMessage }, params.errorMessage ?? 'unknown error')) : null));
8016
- }, onChange: (oldValue, newValue) => {
8017
- if (newValue) {
8018
- handleFeatureTypeChange(changeManager, feature, oldValue, newValue).catch(notifyError);
8411
+ function renderingModelFactory(pluginManager, configSchema) {
8412
+ const LinearApolloDisplayRendering = sequenceRenderingModelFactory(pluginManager, configSchema);
8413
+ return LinearApolloDisplayRendering.actions((self) => ({
8414
+ afterAttach() {
8415
+ addDisposer(self, autorun(() => {
8416
+ const { canvas, featureLayouts, featuresHeight, lgv } = self;
8417
+ if (!lgv.initialized || self.regionCannotBeRendered()) {
8418
+ return;
8419
+ }
8420
+ const { displayedRegions, dynamicBlocks } = lgv;
8421
+ const ctx = canvas?.getContext('2d');
8422
+ if (!ctx) {
8423
+ return;
8424
+ }
8425
+ ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight);
8426
+ for (const [idx, featureLayout] of featureLayouts.entries()) {
8427
+ const displayedRegion = displayedRegions[idx];
8428
+ for (const [row, featureLayoutRow] of featureLayout.entries()) {
8429
+ for (const [featureRow, feature] of featureLayoutRow) {
8430
+ if (featureRow > 0) {
8431
+ continue;
8019
8432
  }
8020
- } }))),
8021
- React__default.createElement("td", null,
8022
- React__default.createElement(NumberCell, { initialValue: min + 1, notifyError: notifyError, onChangeCommitted: (newStart) => handleFeatureStartChange(changeManager, feature, min, newStart - 1) })),
8023
- React__default.createElement("td", null,
8024
- React__default.createElement(NumberCell, { initialValue: max, notifyError: notifyError, onChangeCommitted: (newEnd) => handleFeatureEndChange(changeManager, feature, max, newEnd) })),
8025
- React__default.createElement("td", null, strand === 1 ? '+' : strand === -1 ? '-' : undefined),
8026
- React__default.createElement("td", null,
8027
- React__default.createElement(FeatureAttributes, { filterText: filterText, feature: feature }))),
8028
- expanded && children
8029
- ? [...children.entries()]
8030
- .filter((entry) => {
8031
- if (!filterText) {
8032
- return true;
8433
+ if (!doesIntersect2(displayedRegion.start, displayedRegion.end, feature.min, feature.max)) {
8434
+ continue;
8435
+ }
8436
+ self.getGlyph(feature).draw(ctx, feature, row, self, idx);
8437
+ }
8438
+ }
8033
8439
  }
8034
- const [, childFeature] = entry;
8035
- // search feature and its subfeatures for the text
8036
- const text = JSON.stringify(childFeature);
8037
- return text.includes(filterText);
8038
- })
8039
- .map(([featureId, childFeature]) => {
8040
- const childHovered = apolloHover?.feature._id === childFeature._id;
8041
- const childSelected = selectedFeature?._id === childFeature._id;
8042
- return (React__default.createElement(Feature, { isHovered: childHovered, isSelected: childSelected, selectedFeatureClass: selectedFeatureClass, key: featureId, depth: (depth || 0) + 1, feature: childFeature, model: displayState, setContextMenu: setContextMenu }));
8043
- })
8044
- : null));
8045
- });
8046
- async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
8047
- const { parent: parentFeature } = feature;
8048
- if (parentFeature) {
8049
- // if this is a child of an existing feature, restrict the autocomplete choices to valid
8050
- // parts of that feature
8051
- const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(parentFeature.type, { includeSubclasses: false });
8052
- // eslint-disable-next-line unicorn/no-array-callback-reference
8053
- const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass);
8054
- if (parentTypeClassTerms.length > 0) {
8055
- const subpartTerms = await ontologyStore.getClassesThat('part_of', parentTypeClassTerms);
8056
- return subpartTerms;
8440
+ }, { name: 'LinearApolloDisplayRenderFeatures' }));
8441
+ },
8442
+ }));
8443
+ }
8444
+
8445
+ function isMousePositionWithFeatureAndGlyph(mousePosition) {
8446
+ return 'featureAndGlyphUnderMouse' in mousePosition;
8447
+ }
8448
+ function getMousePosition(event, lgv) {
8449
+ const canvas = event.currentTarget;
8450
+ const { clientX, clientY } = event;
8451
+ const { left, top } = canvas.getBoundingClientRect();
8452
+ const x = clientX - left;
8453
+ const y = clientY - top;
8454
+ const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x);
8455
+ return { x, y, refName, bp, regionNumber };
8456
+ }
8457
+ function getTranslationRow(frame, bpPerPx) {
8458
+ const offset = bpPerPx <= 1 ? 2 : 0;
8459
+ switch (frame) {
8460
+ case 3: {
8461
+ return 0;
8462
+ }
8463
+ case 2: {
8464
+ return 1;
8465
+ }
8466
+ case 1: {
8467
+ return 2;
8468
+ }
8469
+ case -1: {
8470
+ return 3 + offset;
8471
+ }
8472
+ case -2: {
8473
+ return 4 + offset;
8474
+ }
8475
+ case -3: {
8476
+ return 5 + offset;
8057
8477
  }
8058
8478
  }
8059
- return;
8060
8479
  }
8061
-
8062
- /* eslint-disable @typescript-eslint/no-unsafe-call */
8063
- const useStyles$3 = makeStyles()((theme) => ({
8064
- scrollableTable: {
8065
- width: '100%',
8066
- height: '100%',
8067
- th: {
8068
- position: 'sticky',
8069
- top: 0,
8070
- zIndex: 2,
8071
- textAlign: 'left',
8072
- background: theme.palette.background.paper,
8073
- paddingTop: '3.2em',
8480
+ function getSeqRow(strand, bpPerPx) {
8481
+ if (bpPerPx > 1 || strand === undefined) {
8482
+ return;
8483
+ }
8484
+ return strand === 1 ? 3 : 4;
8485
+ }
8486
+ function highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx) {
8487
+ if (row !== undefined) {
8488
+ seqTrackOverlayctx.fillStyle =
8489
+ theme?.palette.action.focus ?? 'rgba(0,0,0,0.04)';
8490
+ seqTrackOverlayctx.fillRect(startPx, sequenceRowHeight * row, widthPx, sequenceRowHeight);
8491
+ }
8492
+ }
8493
+ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
8494
+ const LinearApolloDisplayRendering = renderingModelFactory(pluginManager, configSchema);
8495
+ return LinearApolloDisplayRendering.named('LinearApolloDisplayMouseEvents')
8496
+ .volatile(() => ({
8497
+ apolloDragging: null,
8498
+ cursor: undefined,
8499
+ apolloHover: undefined,
8500
+ }))
8501
+ .views((self) => ({
8502
+ getMousePosition(event) {
8503
+ const mousePosition = getMousePosition(event, self.lgv);
8504
+ const { bp, regionNumber, y } = mousePosition;
8505
+ const row = Math.floor(y / self.apolloRowHeight);
8506
+ const featureLayout = self.featureLayouts[regionNumber];
8507
+ const layoutRow = featureLayout.get(row);
8508
+ if (!layoutRow) {
8509
+ return mousePosition;
8510
+ }
8511
+ const foundFeature = layoutRow.find((f) => bp >= f[1].min && bp <= f[1].max);
8512
+ if (!foundFeature) {
8513
+ return mousePosition;
8514
+ }
8515
+ const [featureRow, topLevelFeature] = foundFeature;
8516
+ const glyph = self.getGlyph(topLevelFeature);
8517
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
8518
+ if (!featureTypeOntology) {
8519
+ throw new Error('featureTypeOntology is undefined');
8520
+ }
8521
+ const feature = glyph.getFeatureFromLayout(topLevelFeature, bp, featureRow, featureTypeOntology);
8522
+ if (!feature) {
8523
+ return mousePosition;
8524
+ }
8525
+ return {
8526
+ ...mousePosition,
8527
+ featureAndGlyphUnderMouse: { feature, topLevelFeature, glyph },
8528
+ };
8529
+ },
8530
+ }))
8531
+ .actions((self) => ({
8532
+ continueDrag(mousePosition, event) {
8533
+ if (!self.apolloDragging) {
8534
+ throw new Error('continueDrag() called with no current drag in progress');
8535
+ }
8536
+ event.stopPropagation();
8537
+ self.apolloDragging = { ...self.apolloDragging, current: mousePosition };
8538
+ },
8539
+ setDragging(dragInfo) {
8540
+ self.apolloDragging = dragInfo ?? null;
8541
+ },
8542
+ }))
8543
+ .actions((self) => ({
8544
+ setApolloHover(n) {
8545
+ self.apolloHover = n;
8546
+ },
8547
+ setCursor(cursor) {
8548
+ if (self.cursor !== cursor) {
8549
+ self.cursor = cursor;
8550
+ }
8551
+ },
8552
+ }))
8553
+ .actions(() => ({
8554
+ // onClick(event: CanvasMouseEvent) {
8555
+ onClick() {
8556
+ // TODO: set the selected feature
8557
+ },
8558
+ }));
8559
+ }
8560
+ function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
8561
+ const LinearApolloDisplayRendering = mouseEventsModelIntermediateFactory(pluginManager, configSchema);
8562
+ return LinearApolloDisplayRendering.actions((self) => ({
8563
+ afterAttach() {
8564
+ addDisposer(self, autorun(() => {
8565
+ // This type is wrong in @jbrowse/core
8566
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
8567
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8568
+ return;
8569
+ }
8570
+ const seqTrackOverlayctx = self.seqTrackOverlayCanvas?.getContext('2d');
8571
+ if (!seqTrackOverlayctx) {
8572
+ return;
8573
+ }
8574
+ seqTrackOverlayctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
8575
+ const { apolloHover, lgv, regions, sequenceRowHeight, session, theme, } = self;
8576
+ if (!apolloHover) {
8577
+ return;
8578
+ }
8579
+ const { feature } = apolloHover;
8580
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
8581
+ if (!featureTypeOntology) {
8582
+ throw new Error('featureTypeOntology is undefined');
8583
+ }
8584
+ for (const [idx, region] of regions.entries()) {
8585
+ if (featureTypeOntology.isTypeOf(feature.type, 'CDS')) {
8586
+ const parentFeature = feature.parent;
8587
+ if (!parentFeature) {
8588
+ continue;
8589
+ }
8590
+ const cdsLocs = parentFeature.cdsLocations.find((loc) => feature.min === loc.at(0)?.min &&
8591
+ feature.max === loc.at(-1)?.max);
8592
+ if (!cdsLocs) {
8593
+ continue;
8594
+ }
8595
+ for (const dl of cdsLocs) {
8596
+ const frame = getFrame(dl.min, dl.max, feature.strand ?? 1, dl.phase);
8597
+ const row = getTranslationRow(frame, lgv.bpPerPx);
8598
+ const offset = (lgv.bpToPx({
8599
+ refName: region.refName,
8600
+ coord: dl.min,
8601
+ regionNumber: idx,
8602
+ })?.offsetPx ?? 0) - lgv.offsetPx;
8603
+ const widthPx = (dl.max - dl.min) / lgv.bpPerPx;
8604
+ const startPx = lgv.displayedRegions[idx].reversed
8605
+ ? offset - widthPx
8606
+ : offset;
8607
+ highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
8608
+ }
8609
+ }
8610
+ else {
8611
+ const row = getSeqRow(feature.strand, lgv.bpPerPx);
8612
+ const offset = (lgv.bpToPx({
8613
+ refName: region.refName,
8614
+ coord: feature.min,
8615
+ regionNumber: idx,
8616
+ })?.offsetPx ?? 0) - lgv.offsetPx;
8617
+ const widthPx = feature.length / lgv.bpPerPx;
8618
+ const startPx = lgv.displayedRegions[idx].reversed
8619
+ ? offset - widthPx
8620
+ : offset;
8621
+ highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
8622
+ }
8623
+ }
8624
+ }, { name: 'LinearApolloDisplayRenderSeqHighlight' }));
8625
+ },
8626
+ }));
8627
+ }
8628
+ function mouseEventsModelFactory(pluginManager, configSchema) {
8629
+ const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
8630
+ return LinearApolloDisplayMouseEvents.views((self) => ({
8631
+ contextMenuItems(contextCoord) {
8632
+ const { apolloHover } = self;
8633
+ if (!(apolloHover && contextCoord)) {
8634
+ return [];
8635
+ }
8636
+ const { topLevelFeature } = apolloHover;
8637
+ const glyph = self.getGlyph(topLevelFeature);
8638
+ return glyph.getContextMenuItems(self);
8639
+ },
8640
+ }))
8641
+ .actions((self) => ({
8642
+ // explicitly pass in a feature in case it's not the same as the one in
8643
+ // mousePosition (e.g. if features are drawn overlapping).
8644
+ startDrag(mousePosition, feature, edge) {
8645
+ self.apolloDragging = {
8646
+ start: mousePosition,
8647
+ current: mousePosition,
8648
+ feature,
8649
+ edge,
8650
+ };
8651
+ },
8652
+ endDrag() {
8653
+ if (!self.apolloDragging) {
8654
+ throw new Error('endDrag() called with no current drag in progress');
8655
+ }
8656
+ const { current, edge, feature, start } = self.apolloDragging;
8657
+ // don't do anything if it was only dragged a tiny bit
8658
+ if (Math.abs(current.x - start.x) <= 4) {
8659
+ self.setDragging();
8660
+ self.setCursor();
8661
+ return;
8662
+ }
8663
+ const { displayedRegions } = self.lgv;
8664
+ const region = displayedRegions[start.regionNumber];
8665
+ const assembly = self.getAssemblyId(region.assemblyName);
8666
+ let change;
8667
+ if (edge === 'max') {
8668
+ const featureId = feature._id;
8669
+ const oldEnd = feature.max;
8670
+ const newEnd = current.bp;
8671
+ change = new LocationEndChange({
8672
+ typeName: 'LocationEndChange',
8673
+ changedIds: [featureId],
8674
+ featureId,
8675
+ oldEnd,
8676
+ newEnd,
8677
+ assembly,
8678
+ });
8679
+ }
8680
+ else {
8681
+ const featureId = feature._id;
8682
+ const oldStart = feature.min;
8683
+ const newStart = current.bp;
8684
+ change = new LocationStartChange({
8685
+ typeName: 'LocationStartChange',
8686
+ changedIds: [featureId],
8687
+ featureId,
8688
+ oldStart,
8689
+ newStart,
8690
+ assembly,
8691
+ });
8692
+ }
8693
+ void self.changeManager.submit(change);
8694
+ self.setDragging();
8695
+ self.setCursor();
8074
8696
  },
8075
- td: { whiteSpace: 'normal' },
8076
- },
8077
- selectedFeature: {
8078
- backgroundColor: theme.palette.action.selected,
8079
- },
8080
- }));
8081
- const HybridGrid = observer(function HybridGrid({ model, }) {
8082
- const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model;
8083
- const theme = useTheme();
8084
- const { classes } = useStyles$3();
8085
- const scrollContainerRef = useRef(null);
8086
- const [contextMenu, setContextMenu] = useState(null);
8087
- const { filterText } = tabularEditor;
8088
- // scrolls to selected feature if one is selected and it's not already visible
8089
- useEffect(() => {
8090
- const scrollContainer = scrollContainerRef.current;
8091
- if (scrollContainer && selectedFeature) {
8092
- const selectedRow = scrollContainer.querySelector(`.${classes.selectedFeature}`);
8093
- if (selectedRow) {
8094
- const currScroll = scrollContainer.scrollTop;
8095
- const newScrollTop = selectedRow.offsetTop - 25;
8096
- const isVisible = newScrollTop > currScroll &&
8097
- newScrollTop < currScroll + scrollContainer.offsetHeight;
8098
- if (!isVisible) {
8099
- scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' });
8100
- }
8697
+ }))
8698
+ .actions((self) => ({
8699
+ onMouseDown(event) {
8700
+ const mousePosition = self.getMousePosition(event);
8701
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8702
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseDown(self, mousePosition, event);
8101
8703
  }
8102
- }
8103
- }, [selectedFeature, seenFeatures, classes.selectedFeature]);
8104
- return (React__default.createElement("div", { ref: scrollContainerRef, style: { width: '100%', overflowY: 'auto', height: '100%' } },
8105
- React__default.createElement("table", { className: classes.scrollableTable },
8106
- React__default.createElement("thead", null,
8107
- React__default.createElement("tr", null,
8108
- React__default.createElement("th", null, "Type"),
8109
- React__default.createElement("th", null, "Start"),
8110
- React__default.createElement("th", null, "End"),
8111
- React__default.createElement("th", null, "Strand"),
8112
- React__default.createElement("th", null, "Attributes"))),
8113
- React__default.createElement("tbody", null, [...seenFeatures.entries()]
8114
- .filter((entry) => {
8115
- if (!filterText) {
8116
- return true;
8704
+ },
8705
+ onMouseMove(event) {
8706
+ const mousePosition = self.getMousePosition(event);
8707
+ if (self.apolloDragging) {
8708
+ self.setCursor('col-resize');
8709
+ self.continueDrag(mousePosition, event);
8710
+ return;
8711
+ }
8712
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8713
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseMove(self, mousePosition, event);
8714
+ }
8715
+ else {
8716
+ self.setApolloHover();
8717
+ self.setCursor();
8718
+ }
8719
+ },
8720
+ onMouseLeave(event) {
8721
+ self.setDragging();
8722
+ self.setApolloHover();
8723
+ const mousePosition = self.getMousePosition(event);
8724
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8725
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseLeave(self, mousePosition, event);
8726
+ }
8727
+ },
8728
+ onMouseUp(event) {
8729
+ const mousePosition = self.getMousePosition(event);
8730
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
8731
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
8732
+ }
8733
+ if (self.apolloDragging) {
8734
+ self.endDrag();
8735
+ }
8736
+ },
8737
+ }))
8738
+ .actions((self) => ({
8739
+ afterAttach() {
8740
+ addDisposer(self, autorun(() => {
8741
+ // This type is wrong in @jbrowse/core
8742
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
8743
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
8744
+ return;
8117
8745
  }
8118
- const [, feature] = entry;
8119
- // search feature and its subfeatures for the text
8120
- const text = JSON.stringify(feature);
8121
- return text.includes(filterText);
8122
- })
8123
- .sort((a, b) => {
8124
- return a[1].min - b[1].min;
8125
- })
8126
- .map(([featureId, feature]) => {
8127
- const isSelected = selectedFeature?._id === featureId;
8128
- const isHovered = apolloHover?.feature._id === featureId;
8129
- return (React__default.createElement(Feature, { key: featureId, isSelected: isSelected, isHovered: isHovered, selectedFeatureClass: classes.selectedFeature, feature: feature, model: model, depth: 0, setContextMenu: setContextMenu }));
8130
- }))),
8131
- React__default.createElement(Menu$1, { open: Boolean(contextMenu), onMenuItemClick: (_, callback) => {
8132
- callback();
8133
- setContextMenu(null);
8134
- }, onClose: () => {
8135
- setContextMenu(null);
8136
- }, TransitionProps: {
8137
- onExit: () => {
8138
- setContextMenu(null);
8139
- },
8140
- }, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenu?.items ?? [], anchorReference: "anchorPosition", anchorPosition: contextMenu?.position })));
8141
- });
8142
-
8143
- /* eslint-disable @typescript-eslint/unbound-method */
8144
- const useStyles$2 = makeStyles()({
8145
- toolbar: {
8146
- width: '100%',
8147
- display: 'flex',
8148
- paddingRight: '2em',
8149
- flexDirection: 'row',
8150
- justifyContent: 'space-between',
8151
- position: 'absolute',
8152
- zIndex: 4,
8153
- },
8154
- filterText: {},
8155
- });
8156
- const ToolBar = observer(function ToolBar({ model: displayState, }) {
8157
- const model = displayState.tabularEditor;
8158
- const { classes } = useStyles$2();
8159
- return (React__default.createElement("div", { className: classes.toolbar },
8160
- React__default.createElement(Tooltip, { title: "Collapse all" },
8161
- React__default.createElement(IconButton, { "aria-label": "collapse", sx: { marginTop: 0 }, onClick: model.collapseAllFeatures },
8162
- React__default.createElement(UnfoldLessIcon, null))),
8163
- React__default.createElement(TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
8164
- model.setFilterText(event.target.value);
8165
- }, InputProps: {
8166
- endAdornment: (React__default.createElement(InputAdornment$1, { position: "end" },
8167
- React__default.createElement(IconButton, { onClick: () => {
8168
- model.clearFilterText();
8169
- } },
8170
- React__default.createElement(ClearIcon, null)))),
8171
- } })));
8172
- });
8173
-
8174
- function stopPropagation(e) {
8175
- e.stopPropagation();
8746
+ const ctx = self.overlayCanvas?.getContext('2d');
8747
+ if (!ctx) {
8748
+ return;
8749
+ }
8750
+ ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
8751
+ const { apolloDragging, apolloHover } = self;
8752
+ if (!apolloHover) {
8753
+ return;
8754
+ }
8755
+ const { glyph } = apolloHover;
8756
+ // draw mouseover hovers
8757
+ glyph.drawHover(self, ctx);
8758
+ // draw tooltip on hover
8759
+ glyph.drawTooltip(self, ctx);
8760
+ // dragging previews
8761
+ if (apolloDragging) {
8762
+ // NOTE: the glyph where the drag started is responsible for drawing the preview.
8763
+ // it can call methods in other glyphs to help with this though.
8764
+ const glyph = self.getGlyph(apolloDragging.feature.topLevelFeature);
8765
+ glyph.drawDragPreview(self, ctx);
8766
+ }
8767
+ }, { name: 'LinearApolloDisplayRenderMouseoverAndDrag' }));
8768
+ },
8769
+ }));
8176
8770
  }
8177
- const TabularEditorPane = observer(function TabularEditorPane({ model: displayState, }) {
8178
- const model = displayState.tabularEditor;
8179
- if (!model.isShown) {
8180
- return null;
8181
- }
8182
- return (
8183
- // TODO: a11y
8184
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
8185
- React__default.createElement("div", { onMouseDown: stopPropagation, onClick: stopPropagation, style: { width: '100%', height: '100%', position: 'relative' } },
8186
- React__default.createElement(ToolBar, { model: displayState }),
8187
- React__default.createElement(HybridGrid, { model: displayState })));
8188
- });
8189
-
8190
- const TabularEditorStateModelType = types
8191
- .model('TabularEditor', {
8192
- isShown: true,
8193
- featureCollapsed: types.map(types.boolean),
8194
- filterText: '',
8195
- })
8196
- .actions((self) => ({
8197
- setFeatureCollapsed(id, state) {
8198
- self.featureCollapsed.set(id, state);
8199
- },
8200
- setFilterText(text) {
8201
- self.filterText = text;
8202
- },
8203
- clearFilterText() {
8204
- self.filterText = '';
8205
- },
8206
- collapseAllFeatures() {
8207
- // iterate over all seen features and set them to collapsed
8208
- const display = getParent(self);
8209
- for (const [featureId] of display.seenFeatures.entries()) {
8210
- self.featureCollapsed.set(featureId, true);
8211
- }
8212
- },
8213
- togglePane() {
8214
- self.isShown = !self.isShown;
8215
- },
8216
- hidePane() {
8217
- self.isShown = false;
8218
- },
8219
- showPane() {
8220
- self.isShown = true;
8221
- },
8222
- // onPatch(patch: any) {
8223
- // console.log(patch)
8224
- // },
8225
- }));
8226
8771
 
8227
8772
  function stateModelFactory$1(pluginManager, configSchema) {
8228
8773
  // TODO: this needs to be refactored so that the final composition of the
@@ -8232,7 +8777,7 @@ function stateModelFactory$1(pluginManager, configSchema) {
8232
8777
  .named('LinearApolloDisplay');
8233
8778
  }
8234
8779
 
8235
- /* eslint-disable @typescript-eslint/no-unsafe-call */
8780
+ /* eslint-disable @typescript-eslint/unbound-method */
8236
8781
  const useStyles$1 = makeStyles()((theme) => ({
8237
8782
  canvasContainer: {
8238
8783
  position: 'relative',
@@ -9268,8 +9813,34 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
9268
9813
  readConfObject(ont, 'textIndexFields'),
9269
9814
  ];
9270
9815
  if (!ontologyManager.findOntology(name)) {
9816
+ const session = getSession(self);
9817
+ const { jobsManager } = session;
9818
+ const controller = new AbortController();
9819
+ const jobName = `Loading ontology "${name}"`;
9820
+ const job = {
9821
+ name: jobName,
9822
+ statusMessage: `Loading ontology "${name}", version "${version}", this may take a while`,
9823
+ progressPct: 0,
9824
+ cancelCallback: () => {
9825
+ controller.abort();
9826
+ jobsManager.abortJob(job.name);
9827
+ },
9828
+ };
9829
+ const update = (message, progress) => {
9830
+ if (progress === 0) {
9831
+ jobsManager.runJob(job);
9832
+ return;
9833
+ }
9834
+ if (progress === 100) {
9835
+ jobsManager.done(job);
9836
+ return;
9837
+ }
9838
+ jobsManager.update(jobName, message, progress);
9839
+ return;
9840
+ };
9271
9841
  ontologyManager.addOntology(name, version, source, {
9272
9842
  textIndexing: { indexFields },
9843
+ update,
9273
9844
  });
9274
9845
  }
9275
9846
  }
@@ -9964,6 +10535,10 @@ function stateModelFactory(pluginManager, configSchema) {
9964
10535
  return codonLayout;
9965
10536
  },
9966
10537
  get featureLayout() {
10538
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
10539
+ if (!featureTypeOntology) {
10540
+ throw new Error('featureTypeOntology is undefined');
10541
+ }
9967
10542
  const featureLayout = new Map();
9968
10543
  for (const [refSeq, featuresForRefSeq] of this.features || []) {
9969
10544
  if (!featuresForRefSeq) {
@@ -9987,11 +10562,11 @@ function stateModelFactory(pluginManager, configSchema) {
9987
10562
  return start1 - start2 || end1 - end2;
9988
10563
  })) {
9989
10564
  for (const [, childFeature] of feature.children ?? new Map()) {
9990
- if (childFeature.type === 'mRNA') {
10565
+ if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript')) {
9991
10566
  for (const [, grandChildFeature] of childFeature.children ||
9992
10567
  new Map()) {
9993
10568
  let startingRow;
9994
- if (grandChildFeature.type === 'CDS') {
10569
+ if (featureTypeOntology.isTypeOf(grandChildFeature.type, 'CDS')) {
9995
10570
  let discontinuousLocations;
9996
10571
  if (grandChildFeature.discontinuousLocations.length > 0) {
9997
10572
  ({ discontinuousLocations } = grandChildFeature);
@@ -10180,7 +10755,7 @@ function installApolloRefNameAliasAdapter(pluginManager) {
10180
10755
  }));
10181
10756
  }
10182
10757
 
10183
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
10758
+ /* eslint-disable @typescript-eslint/unbound-method */
10184
10759
  function isApolloMessageData(data) {
10185
10760
  return (typeof data === 'object' &&
10186
10761
  data !== null &&
@@ -10314,6 +10889,7 @@ class ApolloPlugin extends Plugin {
10314
10889
  return pluggableElement;
10315
10890
  });
10316
10891
  pluginManager.addToExtensionPoint('Core-extendPluggableElement', annotationFromPileup);
10892
+ pluginManager.addToExtensionPoint('Core-extendPluggableElement', annotationFromJBrowseFeature);
10317
10893
  if (!inWebWorker) {
10318
10894
  pluginManager.addToExtensionPoint('Core-extendWorker', (handle) => {
10319
10895
  if (!('on' in handle && handle.on)) {