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