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