jbrowse-plugin-protein3d 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LaunchProteinView/components/PdbSearch.js +18 -4
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +5 -2
- package/dist/LaunchProteinView/hooks/useUniProtIdLookup.js +11 -15
- package/dist/LaunchProteinView/services/foldseekApi.js +33 -8
- package/dist/LaunchProteinView/utils/launchHelpers.js +3 -3
- package/dist/ProteinView/components/ChainSelect.js +6 -3
- package/dist/ProteinView/components/FeatureBar.js +1 -1
- package/dist/ProteinView/components/HeaderStructureRow.js +13 -4
- package/dist/ProteinView/components/ManualAlignmentDialog.js +14 -18
- package/dist/ProteinView/entityAlignedTo.js +1 -1
- package/dist/ProteinView/frameSelection.js +10 -4
- package/dist/ProteinView/model.js +14 -7
- package/dist/ProteinView/molstarExports.js +1 -0
- package/dist/ProteinView/storedSettings.js +17 -1
- package/dist/ProteinView/structureLoader.js +12 -2
- package/dist/ProteinView/structureModel.js +93 -56
- package/dist/ProteinView/structureSuperposer.js +23 -14
- package/dist/ProteinView/structureVisibility.js +17 -0
- package/dist/chunks/ProteinView-CFQG6IHO.js +9 -0
- package/dist/chunks/ProteinView-CFQG6IHO.js.map +7 -0
- package/dist/chunks/chunk-V2WE2V7Z.js +16 -0
- package/dist/chunks/chunk-V2WE2V7Z.js.map +7 -0
- package/dist/chunks/{molstarExports-HAGQ7LSM.js → molstarExports-YN6TKVEW.js} +36 -36
- package/dist/jbrowse-plugin-protein3d.esm.js +7 -7
- package/dist/jbrowse-plugin-protein3d.esm.js.map +4 -4
- package/dist/version.js +1 -1
- package/package.json +2 -15
- package/src/AddHighlightModel/index.test.ts +6 -5
- package/src/AddHighlightModel/proteinViewLookup.test.ts +3 -1
- package/src/LaunchProteinView/components/PdbSearch.tsx +21 -5
- package/src/LaunchProteinView/hooks/useAlphaFoldData.test.ts +44 -0
- package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +6 -3
- package/src/LaunchProteinView/hooks/useUniProtIdLookup.ts +14 -18
- package/src/LaunchProteinView/services/foldseekApi.test.ts +43 -0
- package/src/LaunchProteinView/services/foldseekApi.ts +40 -12
- package/src/LaunchProteinView/utils/launchHelpers.ts +3 -3
- package/src/LaunchProteinView/utils/translateTranscripts.test.ts +9 -1
- package/src/ProteinView/components/ChainSelect.tsx +8 -3
- package/src/ProteinView/components/FeatureBar.tsx +1 -1
- package/src/ProteinView/components/HeaderStructureRow.tsx +30 -3
- package/src/ProteinView/components/ManualAlignmentDialog.tsx +19 -25
- package/src/ProteinView/connectedHover.test.ts +3 -1
- package/src/ProteinView/entityAlignedTo.ts +3 -1
- package/src/ProteinView/frameSelection.test.ts +46 -1
- package/src/ProteinView/frameSelection.ts +16 -5
- package/src/ProteinView/geneExplorerLinkage.test.ts +3 -2
- package/src/ProteinView/kyteDoolittleColorTheme.test.ts +4 -1
- package/src/ProteinView/mappedChainColorTheme.test.ts +4 -1
- package/src/ProteinView/model.test.ts +13 -6
- package/src/ProteinView/model.ts +20 -10
- package/src/ProteinView/molstarExports.ts +1 -0
- package/src/ProteinView/proteinViewSpec.ts +2 -0
- package/src/ProteinView/storedSettings.ts +18 -2
- package/src/ProteinView/structureLoader.test.ts +34 -1
- package/src/ProteinView/structureLoader.ts +14 -2
- package/src/ProteinView/structureModel.test.ts +112 -3
- package/src/ProteinView/structureModel.ts +106 -67
- package/src/ProteinView/structureSuperposer.test.ts +46 -0
- package/src/ProteinView/structureSuperposer.ts +26 -19
- package/src/ProteinView/structureVisibility.ts +33 -0
- package/src/ProteinView/viewInteractions.ts +0 -1
- package/src/ProteinView/withStoredSettings.test.ts +30 -5
- package/src/UniProtVariationAdapter/parseUniProtVariants.test.ts +4 -3
- package/src/version.ts +1 -1
- package/dist/ProteinView/proteinAbbreviationMapping.js +0 -22
- package/dist/chunks/ProteinView-5UGGQVH6.js +0 -9
- package/dist/chunks/ProteinView-5UGGQVH6.js.map +0 -7
- package/dist/chunks/chunk-3IKI3UVA.js +0 -16
- package/dist/chunks/chunk-3IKI3UVA.js.map +0 -7
- package/src/ProteinView/proteinAbbreviationMapping.ts +0 -24
- /package/dist/chunks/{molstarExports-HAGQ7LSM.js.map → molstarExports-YN6TKVEW.js.map} +0 -0
|
@@ -70,16 +70,30 @@ const PdbSearch = observer(function PdbSearch({ feature, preferredTranscriptId,
|
|
|
70
70
|
preferredTranscriptId,
|
|
71
71
|
resetKey: uniprotId,
|
|
72
72
|
});
|
|
73
|
-
const
|
|
74
|
-
isLookupLoading && 'Looking up UniProt ID',
|
|
73
|
+
const isoformStatuses = [
|
|
75
74
|
isIsoformLoading && 'Loading protein sequences from transcript isoforms',
|
|
76
75
|
isRanking && 'Aligning isoforms to the structure',
|
|
76
|
+
];
|
|
77
|
+
const lookupStatuses = [
|
|
78
|
+
isLookupLoading && 'Looking up UniProt ID',
|
|
77
79
|
isPdbLoading && 'Listing PDB entries from PDBe',
|
|
78
|
-
]
|
|
80
|
+
];
|
|
81
|
+
const loadingStatuses = [...lookupStatuses, ...isoformStatuses].filter((s) => !!s);
|
|
79
82
|
const isLoading = loadingStatuses.length > 0;
|
|
80
83
|
const error = isLoading
|
|
81
84
|
? undefined
|
|
82
85
|
: (isoformError ?? lookup.lookupError ?? pdbError);
|
|
86
|
+
// A typed PDB ID exists to get around a lookup that is slow, failing or
|
|
87
|
+
// wrong, so only the isoforms it is ranked against hold its launch back
|
|
88
|
+
const typedIdOverrides = isPdbId(debouncedTypedPdbId);
|
|
89
|
+
const launchWaiting = typedIdOverrides
|
|
90
|
+
? isoformStatuses.some(Boolean)
|
|
91
|
+
: isLoading;
|
|
92
|
+
const launchError = typedIdOverrides
|
|
93
|
+
? launchWaiting
|
|
94
|
+
? undefined
|
|
95
|
+
: isoformError
|
|
96
|
+
: error;
|
|
83
97
|
return (React.createElement(React.Fragment, null,
|
|
84
98
|
React.createElement(DialogContent, { className: classes.dialogContent },
|
|
85
99
|
error ? React.createElement(ErrorMessage, { error: error }) : null,
|
|
@@ -113,6 +127,6 @@ const PdbSearch = observer(function PdbSearch({ feature, preferredTranscriptId,
|
|
|
113
127
|
". The AlphaFoldDB tab has a predicted one."))) : null,
|
|
114
128
|
ranking && selectedTranscript ? (React.createElement(TranscriptSelector, { val: selectedTranscriptId, setVal: setSelectedTranscriptId, structureSequence: structureSequence, feature: feature, isoforms: transcripts, ranking: ranking })) : null),
|
|
115
129
|
React.createElement(DialogActions, null,
|
|
116
|
-
React.createElement(ProteinViewActions, { handleClose: handleClose, uniprotId: uniprotId, userSelectedProteinSequence: selectedIsoform, selectedTranscript: selectedTranscript, url: structureUrl, pdbId: selectedPdbId, feature: feature, view: view, session: session, sideBySide: sideBySide, onSideBySideChange: onSideBySideChange, isLoading:
|
|
130
|
+
React.createElement(ProteinViewActions, { handleClose: handleClose, uniprotId: uniprotId, userSelectedProteinSequence: selectedIsoform, selectedTranscript: selectedTranscript, url: structureUrl, pdbId: selectedPdbId, feature: feature, view: view, session: session, sideBySide: sideBySide, onSideBySideChange: onSideBySideChange, isLoading: launchWaiting, error: launchError }))));
|
|
117
131
|
});
|
|
118
132
|
export default PdbSearch;
|
|
@@ -7,8 +7,11 @@ import useIsoformProteinSequences from './useIsoformProteinSequences';
|
|
|
7
7
|
export default function useAlphaFoldData({ uniprotId, feature, view, }) {
|
|
8
8
|
const { data, isLoading, isValidating, error } = useSWR(uniprotId ? ['alphafold-models', uniprotId] : null, ([, id]) => fetchAlphaFoldModels(id), { ...STATIC_SWR_OPTIONS, keepPreviousData: true });
|
|
9
9
|
const { isoformSequences } = useIsoformProteinSequences({ feature, view });
|
|
10
|
-
//
|
|
11
|
-
|
|
10
|
+
// keepPreviousData hands back the last accession's models after an error
|
|
11
|
+
// and after the accession is cleared, when the key is null
|
|
12
|
+
const model = useMemo(() => uniprotId && data && !error
|
|
13
|
+
? pickAlphaFoldModel(data, isoformSequences)
|
|
14
|
+
: undefined, [uniprotId, data, error, isoformSequences]);
|
|
12
15
|
return {
|
|
13
16
|
isLoading,
|
|
14
17
|
isValidating,
|
|
@@ -17,10 +17,16 @@ export function describeOrganism(taxonId, source) {
|
|
|
17
17
|
* to every tab, so all of them run one search and agree on what the gene is.
|
|
18
18
|
*/
|
|
19
19
|
export default function useUniProtIdLookup({ feature, view, }) {
|
|
20
|
-
const
|
|
20
|
+
const geneIds = extractFeatureIdentifiers(feature);
|
|
21
|
+
const featureUniprotId = geneIds.uniprotId;
|
|
22
|
+
const hasSearchableIdentifier = geneIds.recognizedIds.length > 0 || !!geneIds.geneName;
|
|
23
|
+
// Nothing to search and no accession on the feature: the auto mode has no
|
|
24
|
+
// query to run, so the dialog opens on the manual field instead of reporting
|
|
25
|
+
// an empty result for an empty query. Only the opening mode is chosen here;
|
|
26
|
+
// deciding it on every render snapped the user's pick back.
|
|
27
|
+
const [lookupMode, setLookupMode] = useState(() => featureUniprotId ? 'feature' : hasSearchableIdentifier ? 'auto' : 'manual');
|
|
21
28
|
const [manualUniprotId, setManualUniprotId] = useState('');
|
|
22
29
|
const [taxonIdInput, setTaxonIdInput] = useState('');
|
|
23
|
-
const geneIds = extractFeatureIdentifiers(feature);
|
|
24
30
|
// The gene-name UniProt search is ambiguous across species, so scope it to
|
|
25
31
|
// the assembly's organism where the assembly says what that is. jb2hubs
|
|
26
32
|
// assemblies carry the NCBI taxon in the reference-sequence track metadata
|
|
@@ -46,17 +52,7 @@ export default function useUniProtIdLookup({ feature, view, }) {
|
|
|
46
52
|
const taxonIdError = typedTaxon !== '' && !hasOverride;
|
|
47
53
|
const [selectedQueryId, setSelectedQueryId] = useState('auto');
|
|
48
54
|
const [selectedUniprotId, setSelectedUniprotId] = useState();
|
|
49
|
-
const
|
|
50
|
-
const hasSearchableIdentifier = geneIds.recognizedIds.length > 0 || !!geneIds.geneName;
|
|
51
|
-
// Nothing to search and no accession on the feature: the auto mode has no
|
|
52
|
-
// query to run, so the dialog opens on the manual field instead of reporting
|
|
53
|
-
// an empty result for an empty query.
|
|
54
|
-
const effectiveLookupMode = lookupMode === 'auto' && featureUniprotId
|
|
55
|
-
? 'feature'
|
|
56
|
-
: lookupMode === 'auto' && !hasSearchableIdentifier
|
|
57
|
-
? 'manual'
|
|
58
|
-
: lookupMode;
|
|
59
|
-
const isAutoMode = effectiveLookupMode === 'auto';
|
|
55
|
+
const isAutoMode = lookupMode === 'auto';
|
|
60
56
|
const { entries: uniprotEntries, isLoading: isLookupLoading, error: lookupError, partialFailure: lookupPartialFailure, } = useUniProtSearch({
|
|
61
57
|
recognizedIds: geneIds.recognizedIds,
|
|
62
58
|
geneId: geneIds.geneId,
|
|
@@ -74,13 +70,13 @@ export default function useUniProtIdLookup({ feature, view, }) {
|
|
|
74
70
|
? selectedUniprotId
|
|
75
71
|
: undefined;
|
|
76
72
|
const autoUniprotId = uniprotEntries[0]?.accession;
|
|
77
|
-
const uniprotId =
|
|
73
|
+
const uniprotId = lookupMode === 'feature'
|
|
78
74
|
? featureUniprotId
|
|
79
75
|
: isAutoMode
|
|
80
76
|
? (pickedUniprotId ?? autoUniprotId)
|
|
81
77
|
: debouncedManualUniprotId;
|
|
82
78
|
return {
|
|
83
|
-
lookupMode
|
|
79
|
+
lookupMode,
|
|
84
80
|
setLookupMode,
|
|
85
81
|
manualUniprotId,
|
|
86
82
|
setManualUniprotId,
|
|
@@ -69,11 +69,35 @@ export async function submitFoldseekSearch({ aaSequence, di3Sequence, databases,
|
|
|
69
69
|
if (!response.ok) {
|
|
70
70
|
throw await httpError(response, url);
|
|
71
71
|
}
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// SyntaxError.
|
|
72
|
+
// The server answers a refusal (RATELIMIT, MAINTENANCE) with a 200 and no
|
|
73
|
+
// id, which used to be polled as ticket "undefined" for three minutes
|
|
75
74
|
const text = await response.text();
|
|
76
|
-
|
|
75
|
+
const ticket = parseTicket(text);
|
|
76
|
+
if (!ticket) {
|
|
77
|
+
throw new Error(`Foldseek did not accept the search: ${text.slice(0, 200)}`);
|
|
78
|
+
}
|
|
79
|
+
return ticket;
|
|
80
|
+
}
|
|
81
|
+
function parseTicket(text) {
|
|
82
|
+
let body;
|
|
83
|
+
try {
|
|
84
|
+
body = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (typeof body !== 'object' || body === null) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const id = Reflect.get(body, 'id');
|
|
93
|
+
const status = Reflect.get(body, 'status');
|
|
94
|
+
return typeof id === 'string' &&
|
|
95
|
+
(status === 'PENDING' ||
|
|
96
|
+
status === 'RUNNING' ||
|
|
97
|
+
status === 'COMPLETE' ||
|
|
98
|
+
status === 'ERROR')
|
|
99
|
+
? { id, status }
|
|
100
|
+
: undefined;
|
|
77
101
|
}
|
|
78
102
|
async function pollFoldseekStatus({ ticketId, signal, }) {
|
|
79
103
|
// Use the /tickets endpoint (plural) with POST
|
|
@@ -114,10 +138,6 @@ export async function waitForFoldseekResults({ ticketId, onStatusChange, signal,
|
|
|
114
138
|
throw abortError(signal);
|
|
115
139
|
}
|
|
116
140
|
const status = await pollFoldseekStatus({ ticketId, signal });
|
|
117
|
-
if (status.status === 'ERROR') {
|
|
118
|
-
console.error('[Foldseek] Search error:', status);
|
|
119
|
-
throw new Error(`Foldseek search failed: ${status.error ?? 'Unknown error'}`);
|
|
120
|
-
}
|
|
121
141
|
if (status.status === 'COMPLETE') {
|
|
122
142
|
onStatusChange?.('Fetching results...');
|
|
123
143
|
const apiResponse = await getFoldseekResults({ ticketId, signal });
|
|
@@ -131,6 +151,11 @@ export async function waitForFoldseekResults({ ticketId, onStatusChange, signal,
|
|
|
131
151
|
};
|
|
132
152
|
return results;
|
|
133
153
|
}
|
|
154
|
+
// the server also answers RATELIMIT, MAINTENANCE and UNKNOWN, none of
|
|
155
|
+
// which a wait resolves
|
|
156
|
+
if (status.status !== 'PENDING' && status.status !== 'RUNNING') {
|
|
157
|
+
throw new Error(`Foldseek search failed: ${status.error ?? status.status}`);
|
|
158
|
+
}
|
|
134
159
|
onStatusChange?.(`Search ${status.status.toLowerCase()}... (${elapsedSeconds()}s)`);
|
|
135
160
|
await timeout(1000, signal);
|
|
136
161
|
}
|
|
@@ -22,9 +22,9 @@ export async function safeLaunch(fn, onSuccess, onError) {
|
|
|
22
22
|
* A launch that names its own structure needs no accession. Bypassing a lookup
|
|
23
23
|
* that failed or resolved the wrong gene is the whole point of typing a PDB id,
|
|
24
24
|
* and the view resolves SIFTS from the entry itself; the accession only feeds
|
|
25
|
-
* the feature tracks and the view's name, both of which do without it.
|
|
26
|
-
* AlphaFold tab
|
|
27
|
-
*
|
|
25
|
+
* the feature tracks and the view's name, both of which do without it. On the
|
|
26
|
+
* AlphaFold tab the url is derived from the accession, so no accession means
|
|
27
|
+
* no url either; `useAlphaFoldData` makes sure of it.
|
|
28
28
|
*/
|
|
29
29
|
export function getLaunchMissingReasons({ uniprotId, userSelectedProteinSequence, selectedTranscript, url, pdbId, }) {
|
|
30
30
|
const namesOwnStructure = !!url || !!pdbId;
|
|
@@ -5,13 +5,16 @@ import { entityLabel } from 'p2s_mapper';
|
|
|
5
5
|
// Which chain the transcript maps to. The structure picks the protein chain
|
|
6
6
|
// the transcript explains most of, which cannot separate paralogs in a complex
|
|
7
7
|
// or the halves of a chimeric construct, so the choice is exposed for the
|
|
8
|
-
// cases it gets wrong.
|
|
8
|
+
// cases it gets wrong. With no transcript it picks the chain hovers and the
|
|
9
|
+
// ruler read.
|
|
9
10
|
const ChainSelect = observer(function ChainSelect({ model, }) {
|
|
10
11
|
const { entities, mappedEntity, userProvidedTranscriptSequence } = model;
|
|
11
|
-
if (!entities || entities.length < 2
|
|
12
|
+
if (!entities || entities.length < 2) {
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
14
|
-
return (React.createElement(Tooltip, { title:
|
|
15
|
+
return (React.createElement(Tooltip, { title: userProvidedTranscriptSequence
|
|
16
|
+
? 'Mapped chain: the one the transcript maps to'
|
|
17
|
+
: 'Chain: the one hovers read', placement: "left" },
|
|
15
18
|
React.createElement(TextField, { select: true, size: "small", variant: "standard", "data-testid": "protein-mapped-chain", value: model.pendingEntityId ?? mappedEntity?.entityId ?? '', onChange: event => {
|
|
16
19
|
model.chooseEntity(event.target.value);
|
|
17
20
|
}, slotProps: {
|
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
3
3
|
import CloseIcon from '@mui/icons-material/Close';
|
|
4
4
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
5
|
+
import VisibilityIcon from '@mui/icons-material/Visibility';
|
|
6
|
+
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
5
7
|
import Chip from '@mui/material/Chip';
|
|
6
8
|
import IconButton from '@mui/material/IconButton';
|
|
7
9
|
import Link from '@mui/material/Link';
|
|
@@ -23,11 +25,13 @@ const UniProtLink = observer(function UniProtLink({ structure, }) {
|
|
|
23
25
|
uniprotId))) : null;
|
|
24
26
|
});
|
|
25
27
|
const StructureRow = observer(function StructureRow({ model, structure, readout, }) {
|
|
26
|
-
const { label, alignmentQuality: quality, statusMessage } = structure;
|
|
27
|
-
const
|
|
28
|
+
const { label, alignmentQuality: quality, statusMessage, hidden } = structure;
|
|
29
|
+
const several = model.structures.length > 1;
|
|
30
|
+
const switchable = model.showAlignment && several;
|
|
28
31
|
const open = model.alignmentStructure === structure;
|
|
29
32
|
const coveredRange = quality ? describeCoveredRange(quality) : undefined;
|
|
30
|
-
return (React.createElement("div", { "data-testid": "structure-row", "data-label": label, "data-open": switchable ? open : undefined, style: {
|
|
33
|
+
return (React.createElement("div", { "data-testid": "structure-row", "data-label": label, "data-open": switchable ? open : undefined, "data-hidden": hidden || undefined, style: {
|
|
34
|
+
opacity: hidden ? 0.6 : undefined,
|
|
31
35
|
display: 'flex',
|
|
32
36
|
alignItems: 'center',
|
|
33
37
|
gap: 8,
|
|
@@ -47,9 +51,14 @@ const StructureRow = observer(function StructureRow({ model, structure, readout,
|
|
|
47
51
|
React.createElement(Typography, { variant: "caption", color: "textSecondary", noWrap: true, "data-testid": "header-alignment-quality" }, describeCoverage(quality)))) : null,
|
|
48
52
|
React.createElement(UniProtLink, { structure: structure }),
|
|
49
53
|
React.createElement("div", { style: { flex: 1 } }),
|
|
50
|
-
open && model.showAlignment
|
|
54
|
+
(open && model.showAlignment) ||
|
|
55
|
+
!structure.userProvidedTranscriptSequence ? (React.createElement(ChainSelect, { model: structure })) : null,
|
|
51
56
|
quality && isLowSimilarity(quality) ? (React.createElement(Tooltip, { title: LOW_SIMILARITY_EXPLANATION },
|
|
52
57
|
React.createElement(Chip, { size: "small", color: "warning", variant: "outlined", label: "low similarity", "data-testid": "header-low-similarity" }))) : null,
|
|
58
|
+
several || hidden ? (React.createElement(Tooltip, { title: hidden ? `Show ${label}` : `Hide ${label}` },
|
|
59
|
+
React.createElement(IconButton, { size: "small", "aria-label": hidden ? `Show ${label}` : `Hide ${label}`, "aria-pressed": hidden, onClick: () => {
|
|
60
|
+
structure.setHidden(!hidden);
|
|
61
|
+
}, sx: { p: 0.25 } }, hidden ? (React.createElement(VisibilityOffIcon, { fontSize: "small" })) : (React.createElement(VisibilityIcon, { fontSize: "small" }))))) : null,
|
|
53
62
|
React.createElement(Tooltip, { title: `Remove ${label}` },
|
|
54
63
|
React.createElement(IconButton, { size: "small", "aria-label": `Remove ${label}`, onClick: () => {
|
|
55
64
|
model.removeStructure(structure);
|
|
@@ -2,11 +2,11 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography, } from '@mui/material';
|
|
3
3
|
import { parsePairwise } from 'clustal-js';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
|
-
import {
|
|
5
|
+
import { entityAlignedTo, withoutStopColumn } from '../entityAlignedTo';
|
|
6
6
|
const ManualAlignmentDialog = observer(function ManualAlignmentDialog({ model, }) {
|
|
7
7
|
const [alignment, setAlignment] = useState('');
|
|
8
8
|
const [parseError, setParseError] = useState();
|
|
9
|
-
const { showManualAlignmentDialog,
|
|
9
|
+
const { showManualAlignmentDialog, alignmentStructure } = model;
|
|
10
10
|
const handleClose = () => {
|
|
11
11
|
setAlignment('');
|
|
12
12
|
setParseError(undefined);
|
|
@@ -16,26 +16,22 @@ const ManualAlignmentDialog = observer(function ManualAlignmentDialog({ model, }
|
|
|
16
16
|
if (alignment.trim()) {
|
|
17
17
|
try {
|
|
18
18
|
const parsed = parsePairwise(alignment.trim());
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
// a bad pair during render
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
(primaryStructure
|
|
29
|
-
? pairwiseAlignmentSequenceProblem(parsed, stripStopCodon(primaryStructure.userProvidedTranscriptSequence), stripStopCodon(primaryStructure.mappedStructureSeq ?? ''))
|
|
30
|
-
: undefined);
|
|
31
|
-
if (!primaryStructure) {
|
|
19
|
+
const entities = alignmentStructure?.entities;
|
|
20
|
+
// Rejected here rather than committed: the `coordinateMapper` getter
|
|
21
|
+
// throws on a bad pair during render, outside this catch. The check is
|
|
22
|
+
// the one the model applies to an imported alignment, so a paste whose
|
|
23
|
+
// second row spells another chain switches to that chain.
|
|
24
|
+
const fit = alignmentStructure && entities
|
|
25
|
+
? entityAlignedTo(parsed, alignmentStructure.userProvidedTranscriptSequence, entities, alignmentStructure.mappedEntityId)
|
|
26
|
+
: undefined;
|
|
27
|
+
if (!alignmentStructure || !fit) {
|
|
32
28
|
setParseError('No structure loaded to apply alignment to');
|
|
33
29
|
}
|
|
34
|
-
else if (problem) {
|
|
35
|
-
setParseError(problem);
|
|
30
|
+
else if ('problem' in fit) {
|
|
31
|
+
setParseError(fit.problem);
|
|
36
32
|
}
|
|
37
33
|
else {
|
|
38
|
-
|
|
34
|
+
alignmentStructure.importAlignment(withoutStopColumn(parsed));
|
|
39
35
|
handleClose();
|
|
40
36
|
}
|
|
41
37
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pairwiseAlignmentProblem, pairwiseAlignmentSequenceProblem, stripStopCodon, structureAlignedSeq, transcriptAlignedSeq, } from 'p2s_mapper';
|
|
2
|
-
function withoutStopColumn(alignment) {
|
|
2
|
+
export function withoutStopColumn(alignment) {
|
|
3
3
|
const t = transcriptAlignedSeq(alignment);
|
|
4
4
|
const s = structureAlignedSeq(alignment);
|
|
5
5
|
const [a, b] = alignment.alns;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { residueLoci } from './applyLociInteractivity';
|
|
2
2
|
import loadMolstar from './loadMolstar';
|
|
3
3
|
/**
|
|
4
|
-
* Every structure loaded and aligned and, with several,
|
|
5
|
-
* reset that ends a superposition would undo any framing done
|
|
4
|
+
* Every structure loaded and aligned or failed and, with several loaded,
|
|
5
|
+
* superposed: the reset that ends a superposition would undo any framing done
|
|
6
|
+
* before it. The superposer counts only what reached Mol*, so a failed
|
|
7
|
+
* structure is left out of the comparison.
|
|
6
8
|
*/
|
|
7
9
|
export function structuresSettled(host) {
|
|
8
10
|
const { structures, superposedCount } = host;
|
|
11
|
+
const loadedCount = structures.filter(s => s.loadedToMolstar).length;
|
|
9
12
|
return (structures.length > 0 &&
|
|
10
13
|
structures.every(s => !s.loading) &&
|
|
11
|
-
(
|
|
14
|
+
(loadedCount < 2 || superposedCount === loadedCount));
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
17
|
* Moves the camera to the residues. A single residue is also focused, which
|
|
@@ -45,7 +48,10 @@ export function makeSelectionFramer(host) {
|
|
|
45
48
|
}
|
|
46
49
|
// a seed resolved by the same change that settles the structure may land
|
|
47
50
|
// after this run, so the plugin counts as framed only once it has targets
|
|
48
|
-
const targets = structures.flatMap(s => s.seedLit &&
|
|
51
|
+
const targets = structures.flatMap(s => s.seedLit &&
|
|
52
|
+
!s.hidden &&
|
|
53
|
+
s.molstarStructure &&
|
|
54
|
+
s.clickedLabelSeqIds.length
|
|
49
55
|
? [
|
|
50
56
|
{
|
|
51
57
|
structure: s.molstarStructure,
|
|
@@ -13,7 +13,7 @@ import { readStoredSettings, storeSetting, withStoredSettings, } from './storedS
|
|
|
13
13
|
import { makeStructureLoader } from './structureLoader';
|
|
14
14
|
import Structure from './structureModel';
|
|
15
15
|
import { makeStructureSuperposer } from './structureSuperposer';
|
|
16
|
-
import {
|
|
16
|
+
import { setStructuresHidden } from './structureVisibility';
|
|
17
17
|
import { attachViewInteractions } from './viewInteractions';
|
|
18
18
|
// What a click and a highlight do, as opposed to what the panel shows. Named
|
|
19
19
|
// here rather than in storedSettings because these are deliberately not
|
|
@@ -277,6 +277,18 @@ function stateModelFactory() {
|
|
|
277
277
|
});
|
|
278
278
|
}
|
|
279
279
|
}));
|
|
280
|
+
addDisposer(self, autorun(() => {
|
|
281
|
+
const plugin = self.molstarPluginContext;
|
|
282
|
+
for (const s of self.structures) {
|
|
283
|
+
const { hidden, molstarStructures: structures } = s;
|
|
284
|
+
if (plugin && structures.length > 0) {
|
|
285
|
+
setStructuresHidden({ plugin, structures, hidden }).catch((e) => {
|
|
286
|
+
console.error(e);
|
|
287
|
+
self.setError(e);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}));
|
|
280
292
|
// Load structures into Molstar as they appear or whenever the plugin
|
|
281
293
|
// context changes. See makeStructureLoader for why the autorun body is
|
|
282
294
|
// synchronous and how it guards against duplicate/stale loads.
|
|
@@ -432,12 +444,7 @@ function stateModelFactory() {
|
|
|
432
444
|
{
|
|
433
445
|
label: 'Re-align structures (TM-align)',
|
|
434
446
|
onClick: () => {
|
|
435
|
-
|
|
436
|
-
superposeStructures(self.molstarPluginContext, self.structures.map(s => s.molstarStructures)).catch((e) => {
|
|
437
|
-
console.error(e);
|
|
438
|
-
self.setError(e);
|
|
439
|
-
});
|
|
440
|
-
}
|
|
447
|
+
self.setSuperposedCount(0);
|
|
441
448
|
},
|
|
442
449
|
},
|
|
443
450
|
{
|
|
@@ -4,6 +4,7 @@ export { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
|
|
|
4
4
|
export { QueryContext, StructureElement, StructureProperties, StructureSelection, } from 'molstar/lib/mol-model/structure';
|
|
5
5
|
export { tmAlign } from 'molstar/lib/mol-model/structure/structure/util/tm-align';
|
|
6
6
|
export { MmcifFormat } from 'molstar/lib/mol-model-formats/structure/mmcif';
|
|
7
|
+
export { setSubtreeVisibility } from 'molstar/lib/mol-plugin/behavior/static/state';
|
|
7
8
|
export { PluginCommands } from 'molstar/lib/mol-plugin/commands';
|
|
8
9
|
export { PluginConfig } from 'molstar/lib/mol-plugin/config';
|
|
9
10
|
export { PluginContext } from 'molstar/lib/mol-plugin/context';
|
|
@@ -34,8 +34,24 @@ export function withStoredSettings(snapshot, stored) {
|
|
|
34
34
|
}
|
|
35
35
|
return filled;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The stored settings that are booleans. Anything else in storage (an older
|
|
39
|
+
* format, a hand edit) is dropped: copied into the snapshot it fails MST's
|
|
40
|
+
* type check and the view never opens.
|
|
41
|
+
*/
|
|
37
42
|
export function readStoredSettings() {
|
|
38
|
-
|
|
43
|
+
const stored = readStoredJson(SETTINGS_KEY);
|
|
44
|
+
if (typeof stored !== 'object' || stored === null) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const settings = {};
|
|
48
|
+
for (const key of PERSISTED_SETTINGS) {
|
|
49
|
+
const value = Reflect.get(stored, key);
|
|
50
|
+
if (typeof value === 'boolean') {
|
|
51
|
+
settings[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return settings;
|
|
39
55
|
}
|
|
40
56
|
/** Remembers one choice, leaving the other stored settings as they were. */
|
|
41
57
|
export function storeSetting(key, value) {
|
|
@@ -23,6 +23,7 @@ import { removeMolstarStructure } from './removeStructure';
|
|
|
23
23
|
*/
|
|
24
24
|
export function makeStructureLoader(host, fetchModels = fetchAlphaFoldModels) {
|
|
25
25
|
const loadingStructures = new Set();
|
|
26
|
+
const failedIn = new WeakMap();
|
|
26
27
|
/** The accession a structure still has to turn into a file, if any. */
|
|
27
28
|
function unresolvedAccession(structure) {
|
|
28
29
|
const { url, data, uniprotId } = structure;
|
|
@@ -70,9 +71,15 @@ export function makeStructureLoader(host, fetchModels = fetchAlphaFoldModels) {
|
|
|
70
71
|
const accession = unresolvedAccession(structure);
|
|
71
72
|
const loaded = accession === undefined
|
|
72
73
|
? loadStructureData({ structure, plugin })
|
|
73
|
-
: resolveAlphaFoldUrl(structure, accession).then(() =>
|
|
74
|
+
: resolveAlphaFoldUrl(structure, accession).then(() => isAlive(structure)
|
|
75
|
+
? loadStructureData({ structure, plugin })
|
|
76
|
+
: undefined);
|
|
74
77
|
loaded
|
|
75
78
|
.then(data => {
|
|
79
|
+
if (!data) {
|
|
80
|
+
loadingStructures.delete(structure);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
76
83
|
if (!isAlive(structure)) {
|
|
77
84
|
// Removed while it was loading. The load still put a trajectory in
|
|
78
85
|
// Mol*, and no model owns it any more, so it would stay on the
|
|
@@ -111,6 +118,7 @@ export function makeStructureLoader(host, fetchModels = fetchAlphaFoldModels) {
|
|
|
111
118
|
else {
|
|
112
119
|
// the structure carries its own failure: a view-wide "Failed to
|
|
113
120
|
// fetch" names neither which structure nor what it was fetching
|
|
121
|
+
failedIn.set(structure, plugin);
|
|
114
122
|
structure.setError(e);
|
|
115
123
|
console.error(e);
|
|
116
124
|
}
|
|
@@ -120,7 +128,9 @@ export function makeStructureLoader(host, fetchModels = fetchAlphaFoldModels) {
|
|
|
120
128
|
const { structures, molstarPluginContext } = host;
|
|
121
129
|
if (molstarPluginContext) {
|
|
122
130
|
for (const structure of structures) {
|
|
123
|
-
if (!structure.loadedToMolstar &&
|
|
131
|
+
if (!structure.loadedToMolstar &&
|
|
132
|
+
!loadingStructures.has(structure) &&
|
|
133
|
+
failedIn.get(structure) !== molstarPluginContext) {
|
|
124
134
|
loadInto(structure, molstarPluginContext);
|
|
125
135
|
}
|
|
126
136
|
}
|