jbrowse-plugin-protein3d 0.4.11 → 0.4.13
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/LaunchSettingsDialog.d.ts +5 -0
- package/dist/LaunchProteinView/components/LaunchSettingsDialog.js +23 -0
- package/dist/LaunchProteinView/components/ProteinViewActions.js +13 -2
- package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +12 -3
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +2 -1
- package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.js +2 -1
- package/dist/LaunchProteinView/hooks/useFoldseekSearch.js +47 -12
- package/dist/LaunchProteinView/hooks/useStructureFileSequence.d.ts +1 -0
- package/dist/LaunchProteinView/hooks/useStructureFileSequence.js +5 -2
- package/dist/LaunchProteinView/services/foldseekApi.d.ts +23 -5
- package/dist/LaunchProteinView/services/foldseekApi.js +21 -13
- package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +2 -1
- package/dist/LaunchProteinView/utils/launchViewUtils.js +7 -2
- package/dist/LaunchProteinView/utils/sideBySide.d.ts +11 -0
- package/dist/LaunchProteinView/utils/sideBySide.js +33 -0
- package/dist/LaunchProteinViewExtensionPoint/index.js +9 -2
- package/dist/ProteinView/applyColorTheme.d.ts +1 -1
- package/dist/ProteinView/loadStructureData.d.ts +18 -0
- package/dist/ProteinView/loadStructureData.js +22 -0
- package/dist/ProteinView/model.d.ts +2 -2
- package/dist/ProteinView/model.js +6 -36
- package/dist/ProteinView/structureLoader.d.ts +30 -0
- package/dist/ProteinView/structureLoader.js +58 -0
- package/dist/config.json +1 -1
- package/dist/fetchUtils.d.ts +1 -1
- package/dist/fetchUtils.js +18 -2
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/molstar-chunk.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
- package/src/LaunchProteinView/components/LaunchSettingsDialog.tsx +63 -0
- package/src/LaunchProteinView/components/ProteinViewActions.tsx +21 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +13 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +3 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldSequenceSearch.ts +12 -10
- package/src/LaunchProteinView/hooks/useFoldseekSearch.ts +49 -12
- package/src/LaunchProteinView/hooks/useStructureFileSequence.ts +5 -2
- package/src/LaunchProteinView/services/foldseekApi.ts +57 -23
- package/src/LaunchProteinView/utils/launchViewUtils.ts +10 -1
- package/src/LaunchProteinView/utils/sideBySide.ts +55 -0
- package/src/LaunchProteinViewExtensionPoint/index.ts +17 -1
- package/src/ProteinView/loadStructureData.ts +36 -0
- package/src/ProteinView/model.ts +6 -47
- package/src/ProteinView/structureLoader.test.ts +102 -0
- package/src/ProteinView/structureLoader.ts +74 -0
- package/src/fetchUtils.test.ts +27 -0
- package/src/fetchUtils.ts +22 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, FormGroup, Typography, } from '@mui/material';
|
|
3
|
+
import { getLaunchSideBySide, setLaunchSideBySide } from '../utils/sideBySide';
|
|
4
|
+
// Small, self-contained launch settings (NOT the global preferences dialog):
|
|
5
|
+
// just the options that affect how this protein view opens.
|
|
6
|
+
export default function LaunchSettingsDialog({ open, onClose, }) {
|
|
7
|
+
const [sideBySide, setSideBySide] = useState(() => getLaunchSideBySide());
|
|
8
|
+
return (React.createElement(Dialog, { open: open, onClose: () => {
|
|
9
|
+
onClose();
|
|
10
|
+
} },
|
|
11
|
+
React.createElement(DialogTitle, null, "Launch settings"),
|
|
12
|
+
React.createElement(DialogContent, null,
|
|
13
|
+
React.createElement(FormGroup, null,
|
|
14
|
+
React.createElement(FormControlLabel, { control: React.createElement(Checkbox, { checked: sideBySide }), label: "Open protein view side-by-side with the genome view", onChange: (_, checked) => {
|
|
15
|
+
setSideBySide(checked);
|
|
16
|
+
setLaunchSideBySide(checked);
|
|
17
|
+
} })),
|
|
18
|
+
React.createElement(Typography, { variant: "body2", color: "text.secondary" }, "When enabled, launching a protein view places it to the right of the connected genome view in a split layout instead of stacking it below.")),
|
|
19
|
+
React.createElement(DialogActions, null,
|
|
20
|
+
React.createElement(Button, { variant: "contained", onClick: () => {
|
|
21
|
+
onClose();
|
|
22
|
+
} }, "Close"))));
|
|
23
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { ErrorMessage } from '@jbrowse/core/ui';
|
|
3
3
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
4
|
-
import
|
|
4
|
+
import SettingsIcon from '@mui/icons-material/Settings';
|
|
5
|
+
import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/material';
|
|
5
6
|
import LaunchOptionsDialog from './LaunchOptionsDialog';
|
|
7
|
+
import LaunchSettingsDialog from './LaunchSettingsDialog';
|
|
6
8
|
import SequenceMismatchNotice from './SequenceMismatchNotice';
|
|
7
9
|
import { getLaunchMissingReasons, safeLaunch } from '../utils/launchHelpers';
|
|
8
10
|
import { hasMsaViewPlugin, launch1DProteinView, launch3DProteinView, launch3DProteinViewWithMsa, launchMsaView, } from '../utils/launchViewUtils';
|
|
9
11
|
export default function ProteinViewActions({ handleClose, uniprotId, userSelectedProteinSequence, selectedTranscript, url, confidenceUrl, feature, view, session, alignmentAlgorithm, onAlignmentAlgorithmChange, sequencesMatch, isLoading, error, }) {
|
|
10
12
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
13
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
11
14
|
const [launchError, setLaunchError] = useState();
|
|
12
15
|
// Disable launch while loading — SWR's keepPreviousData would otherwise let
|
|
13
16
|
// a user click Launch on stale results (wrong UniProt ID) during a refetch.
|
|
@@ -87,6 +90,11 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
|
|
|
87
90
|
return (React.createElement(React.Fragment, null,
|
|
88
91
|
launchError ? React.createElement(ErrorMessage, { error: launchError }) : null,
|
|
89
92
|
sequencesMatch === false ? (React.createElement(SequenceMismatchNotice, { alignmentAlgorithm: alignmentAlgorithm, onAlignmentAlgorithmChange: onAlignmentAlgorithmChange })) : null,
|
|
93
|
+
React.createElement(Tooltip, { title: "Launch settings" },
|
|
94
|
+
React.createElement(IconButton, { size: "small", "aria-label": "Launch settings", onClick: () => {
|
|
95
|
+
setSettingsOpen(true);
|
|
96
|
+
} },
|
|
97
|
+
React.createElement(SettingsIcon, { fontSize: "small" }))),
|
|
90
98
|
React.createElement(Button, { variant: "contained", color: "secondary", size: "small", onClick: () => {
|
|
91
99
|
handleClose();
|
|
92
100
|
} }, "Cancel"),
|
|
@@ -97,5 +105,8 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
|
|
|
97
105
|
setDialogOpen(true);
|
|
98
106
|
}, "aria-label": "More launch options" },
|
|
99
107
|
React.createElement(ArrowDropDownIcon, null))),
|
|
100
|
-
React.createElement(LaunchOptionsDialog, { open: dialogOpen, onClose: closeMenu, options: launchOptions })
|
|
108
|
+
React.createElement(LaunchOptionsDialog, { open: dialogOpen, onClose: closeMenu, options: launchOptions }),
|
|
109
|
+
React.createElement(LaunchSettingsDialog, { open: settingsOpen, onClose: () => {
|
|
110
|
+
setSettingsOpen(false);
|
|
111
|
+
} })));
|
|
101
112
|
}
|
|
@@ -35,7 +35,7 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
|
|
|
35
35
|
: effectiveLookupMode === 'manual'
|
|
36
36
|
? debouncedManualUniprotId
|
|
37
37
|
: undefined;
|
|
38
|
-
const { isLoading: isAlphaFoldLoading, error: alphaFoldError, url: alphaFoldUrl, confidenceUrl: alphaFoldConfidenceUrl, structureSequence: alphaFoldStructureSequence, } = useAlphaFoldData({
|
|
38
|
+
const { isLoading: isAlphaFoldLoading, isValidating: isAlphaFoldValidating, error: alphaFoldError, url: alphaFoldUrl, confidenceUrl: alphaFoldConfidenceUrl, structureSequence: alphaFoldStructureSequence, } = useAlphaFoldData({
|
|
39
39
|
uniprotId: isSequenceMode ? undefined : uniprotId,
|
|
40
40
|
});
|
|
41
41
|
const { transcripts: transcriptOptions, isoformSequences, isLoading: isIsoformLoading, error: isoformError, selectedTranscriptId: effectiveTranscriptId, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: userSelectedProteinSequence, } = useTranscriptIsoformSelection({
|
|
@@ -44,7 +44,7 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
|
|
|
44
44
|
structureSequence: alphaFoldStructureSequence,
|
|
45
45
|
resetKey: uniprotId,
|
|
46
46
|
});
|
|
47
|
-
const { uniprotId: seqSearchUniprotId, cifUrl: seqSearchUrl, plddtDocUrl: seqSearchConfidenceUrl, structureSequence: seqSearchStructureSequence, isLoading: isSequenceSearchLoading, error: sequenceSearchError, } = useAlphaFoldSequenceSearch({
|
|
47
|
+
const { uniprotId: seqSearchUniprotId, cifUrl: seqSearchUrl, plddtDocUrl: seqSearchConfidenceUrl, structureSequence: seqSearchStructureSequence, isLoading: isSequenceSearchLoading, isValidating: isSequenceSearchValidating, error: sequenceSearchError, } = useAlphaFoldSequenceSearch({
|
|
48
48
|
sequence: userSelectedProteinSequence?.seq,
|
|
49
49
|
searchType: sequenceSearchType,
|
|
50
50
|
enabled: isSequenceMode,
|
|
@@ -58,6 +58,13 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
|
|
|
58
58
|
? seqSearchStructureSequence
|
|
59
59
|
: alphaFoldStructureSequence;
|
|
60
60
|
const finalUniprotId = isSequenceMode ? seqSearchUniprotId : uniprotId;
|
|
61
|
+
// While a structure fetch is in flight, finalStructureSequence may still be
|
|
62
|
+
// the previous selection's sequence (keepPreviousData). Comparing that stale
|
|
63
|
+
// sequence to the freshly-selected transcript would give a wrong match, so
|
|
64
|
+
// the match is treated as unknown until the fetch settles.
|
|
65
|
+
const isStructureValidating = isSequenceMode
|
|
66
|
+
? isSequenceSearchValidating
|
|
67
|
+
: isAlphaFoldValidating;
|
|
61
68
|
const loadingStatuses = [
|
|
62
69
|
isLookupLoading && 'Looking up UniProt ID',
|
|
63
70
|
isIsoformLoading && 'Loading protein sequences from transcript isoforms',
|
|
@@ -104,7 +111,9 @@ export default function useAlphaFoldDBSearch({ feature, view, }) {
|
|
|
104
111
|
showStructureSelectors: !!isoformSequences &&
|
|
105
112
|
!!selectedTranscript &&
|
|
106
113
|
(isSequenceMode || !!(finalStructureSequence && finalUniprotId)),
|
|
107
|
-
sequencesMatch:
|
|
114
|
+
sequencesMatch: !isStructureValidating &&
|
|
115
|
+
userSelectedProteinSequence?.seq &&
|
|
116
|
+
finalStructureSequence
|
|
108
117
|
? stripStopCodon(userSelectedProteinSequence.seq) ===
|
|
109
118
|
finalStructureSequence
|
|
110
119
|
: undefined,
|
|
@@ -5,9 +5,10 @@ export default function useAlphaFoldData({ uniprotId, }) {
|
|
|
5
5
|
const confidenceUrl = uniprotId
|
|
6
6
|
? getAlphaFoldConfidenceUrl(uniprotId)
|
|
7
7
|
: undefined;
|
|
8
|
-
const { sequences, isLoading, error } = useStructureFileSequence({ url });
|
|
8
|
+
const { sequences, isLoading, isValidating, error } = useStructureFileSequence({ url });
|
|
9
9
|
return {
|
|
10
10
|
isLoading,
|
|
11
|
+
isValidating,
|
|
11
12
|
error,
|
|
12
13
|
url,
|
|
13
14
|
confidenceUrl,
|
|
@@ -19,6 +19,7 @@ export default function useAlphaFoldSequenceSearch({ sequence, searchType, enabl
|
|
|
19
19
|
enabled?: boolean;
|
|
20
20
|
}): {
|
|
21
21
|
isLoading: boolean;
|
|
22
|
+
isValidating: boolean;
|
|
22
23
|
result: SequenceSummaryResponse | undefined;
|
|
23
24
|
uniprotId: string | undefined;
|
|
24
25
|
cifUrl: string | undefined;
|
|
@@ -12,7 +12,7 @@ export default function useAlphaFoldSequenceSearch({ sequence, searchType, enabl
|
|
|
12
12
|
const cleanSeq = stripStopCodon(sequence.toUpperCase());
|
|
13
13
|
return searchType === 'md5' ? md5(cleanSeq) : cleanSeq;
|
|
14
14
|
}, [sequence, searchType]);
|
|
15
|
-
const { data, error, isLoading } = useSWR(enabled && searchValue
|
|
15
|
+
const { data, error, isLoading, isValidating } = useSWR(enabled && searchValue
|
|
16
16
|
? `https://alphafold.ebi.ac.uk/api/sequence/summary?id=${encodeURIComponent(searchValue)}&type=${searchType}`
|
|
17
17
|
: null, jsonfetch, {
|
|
18
18
|
...STATIC_SWR_OPTIONS,
|
|
@@ -20,6 +20,7 @@ export default function useAlphaFoldSequenceSearch({ sequence, searchType, enabl
|
|
|
20
20
|
});
|
|
21
21
|
return {
|
|
22
22
|
isLoading,
|
|
23
|
+
isValidating,
|
|
23
24
|
result: data,
|
|
24
25
|
uniprotId: data?.uniprotAccession,
|
|
25
26
|
cifUrl: data?.cifUrl,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { DEFAULT_DATABASES, predict3Di, submitFoldseekSearch, waitForFoldseekResults, } from '../services/foldseekApi';
|
|
3
3
|
export default function useFoldseekSearch() {
|
|
4
4
|
const [results, setResults] = useState();
|
|
@@ -7,46 +7,81 @@ export default function useFoldseekSearch() {
|
|
|
7
7
|
const [isPredicting, setIsPredicting] = useState(false);
|
|
8
8
|
const [error, setError] = useState();
|
|
9
9
|
const [statusMessage, setStatusMessage] = useState('');
|
|
10
|
+
// Aborts the in-flight request (3Di prediction or the up-to-3-minute Foldseek
|
|
11
|
+
// poll) when the dialog closes/unmounts, so it stops hitting the external API
|
|
12
|
+
// and stops updating dead state.
|
|
13
|
+
const abortRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
return () => {
|
|
16
|
+
abortRef.current?.abort();
|
|
17
|
+
};
|
|
18
|
+
}, []);
|
|
19
|
+
const startOperation = () => {
|
|
20
|
+
abortRef.current?.abort();
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
abortRef.current = controller;
|
|
23
|
+
return controller.signal;
|
|
24
|
+
};
|
|
10
25
|
const predictStructure = async (aaSequence) => {
|
|
26
|
+
const signal = startOperation();
|
|
11
27
|
setIsPredicting(true);
|
|
12
28
|
setError(undefined);
|
|
13
29
|
setStatusMessage('Predicting 3Di structure...');
|
|
14
30
|
try {
|
|
15
|
-
const result = await predict3Di(aaSequence);
|
|
31
|
+
const result = await predict3Di({ aaSequence, signal });
|
|
16
32
|
setPredictData(result);
|
|
17
33
|
return result;
|
|
18
34
|
}
|
|
19
35
|
catch (e) {
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
if (!signal.aborted) {
|
|
37
|
+
console.error(e);
|
|
38
|
+
setError(e);
|
|
39
|
+
}
|
|
22
40
|
return undefined;
|
|
23
41
|
}
|
|
24
42
|
finally {
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
if (!signal.aborted) {
|
|
44
|
+
setIsPredicting(false);
|
|
45
|
+
setStatusMessage('');
|
|
46
|
+
}
|
|
27
47
|
}
|
|
28
48
|
};
|
|
29
49
|
const search = async (aaSeq, di3Seq, databases = DEFAULT_DATABASES) => {
|
|
50
|
+
const signal = startOperation();
|
|
30
51
|
setIsLoading(true);
|
|
31
52
|
setError(undefined);
|
|
32
53
|
setStatusMessage('Submitting search...');
|
|
33
54
|
try {
|
|
34
|
-
const ticket = await submitFoldseekSearch(
|
|
35
|
-
|
|
55
|
+
const ticket = await submitFoldseekSearch({
|
|
56
|
+
aaSequence: aaSeq,
|
|
57
|
+
di3Sequence: di3Seq,
|
|
58
|
+
databases,
|
|
59
|
+
signal,
|
|
60
|
+
});
|
|
61
|
+
const result = await waitForFoldseekResults({
|
|
62
|
+
ticketId: ticket.id,
|
|
63
|
+
onStatusChange: setStatusMessage,
|
|
64
|
+
signal,
|
|
65
|
+
});
|
|
36
66
|
setResults(result);
|
|
37
67
|
return result;
|
|
38
68
|
}
|
|
39
69
|
catch (e) {
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
if (!signal.aborted) {
|
|
71
|
+
console.error(e);
|
|
72
|
+
setError(e);
|
|
73
|
+
}
|
|
42
74
|
return undefined;
|
|
43
75
|
}
|
|
44
76
|
finally {
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
if (!signal.aborted) {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
setStatusMessage('');
|
|
80
|
+
}
|
|
47
81
|
}
|
|
48
82
|
};
|
|
49
83
|
const reset = () => {
|
|
84
|
+
abortRef.current?.abort();
|
|
50
85
|
setResults(undefined);
|
|
51
86
|
setPredictData(undefined);
|
|
52
87
|
setError(undefined);
|
|
@@ -33,7 +33,7 @@ export default function useStructureFileSequence({ file, url, }) {
|
|
|
33
33
|
: url
|
|
34
34
|
? ['structure-url', url]
|
|
35
35
|
: null;
|
|
36
|
-
const { data, error, isLoading } = useSWR(key, async () => {
|
|
36
|
+
const { data, error, isLoading, isValidating } = useSWR(key, async () => {
|
|
37
37
|
const seq = await fetchSequences({ file, url });
|
|
38
38
|
if (!seq) {
|
|
39
39
|
throw new Error('no sequences detected in file');
|
|
@@ -43,5 +43,8 @@ export default function useStructureFileSequence({ file, url, }) {
|
|
|
43
43
|
...STATIC_SWR_OPTIONS,
|
|
44
44
|
keepPreviousData: true,
|
|
45
45
|
});
|
|
46
|
-
|
|
46
|
+
// isValidating distinguishes "fetching for the current key" from the stale
|
|
47
|
+
// data keepPreviousData keeps around during a key change. Consumers comparing
|
|
48
|
+
// this sequence against another need it to avoid matching against stale data.
|
|
49
|
+
return { error, isLoading, isValidating, sequences: data };
|
|
47
50
|
}
|
|
@@ -64,12 +64,23 @@ export interface FoldseekResult {
|
|
|
64
64
|
};
|
|
65
65
|
results: FoldseekDatabaseResult[];
|
|
66
66
|
}
|
|
67
|
-
export declare function predict3Di(aaSequence
|
|
67
|
+
export declare function predict3Di({ aaSequence, signal, }: {
|
|
68
|
+
aaSequence: string;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}): Promise<{
|
|
68
71
|
aaSequence: string;
|
|
69
72
|
di3Sequence: string;
|
|
70
73
|
}>;
|
|
71
|
-
export declare function submitFoldseekSearch(aaSequence
|
|
72
|
-
|
|
74
|
+
export declare function submitFoldseekSearch({ aaSequence, di3Sequence, databases, signal, }: {
|
|
75
|
+
aaSequence: string;
|
|
76
|
+
di3Sequence: string;
|
|
77
|
+
databases: FoldseekDatabaseId[];
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<FoldseekTicketResponse>;
|
|
80
|
+
export declare function pollFoldseekStatus({ ticketId, signal, }: {
|
|
81
|
+
ticketId: string;
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
}): Promise<FoldseekTicketResponse>;
|
|
73
84
|
interface FoldseekApiResponse {
|
|
74
85
|
mode: string;
|
|
75
86
|
queries: {
|
|
@@ -82,6 +93,13 @@ interface FoldseekApiResponse {
|
|
|
82
93
|
taxonomyreports: unknown[];
|
|
83
94
|
}[];
|
|
84
95
|
}
|
|
85
|
-
export declare function getFoldseekResults(ticketId
|
|
86
|
-
|
|
96
|
+
export declare function getFoldseekResults({ ticketId, signal, }: {
|
|
97
|
+
ticketId: string;
|
|
98
|
+
signal?: AbortSignal;
|
|
99
|
+
}): Promise<FoldseekApiResponse>;
|
|
100
|
+
export declare function waitForFoldseekResults({ ticketId, onStatusChange, signal, }: {
|
|
101
|
+
ticketId: string;
|
|
102
|
+
onStatusChange?: (status: string) => void;
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<FoldseekResult>;
|
|
87
105
|
export {};
|
|
@@ -13,7 +13,7 @@ export const DEFAULT_DATABASES = [
|
|
|
13
13
|
'pdb100',
|
|
14
14
|
'afdb-swissprot',
|
|
15
15
|
];
|
|
16
|
-
export async function predict3Di(aaSequence) {
|
|
16
|
+
export async function predict3Di({ aaSequence, signal, }) {
|
|
17
17
|
// Clean the sequence - remove FASTA header, whitespace, stop codons, and non-AA chars
|
|
18
18
|
const cleanSequence = aaSequence
|
|
19
19
|
.split('\n')
|
|
@@ -23,7 +23,7 @@ export async function predict3Di(aaSequence) {
|
|
|
23
23
|
.replace(/\*/g, '') // Remove stop codons before querying 3Di
|
|
24
24
|
.toUpperCase()
|
|
25
25
|
.replace(/[^ACDEFGHIKLMNPQRSTVWY]/g, ''); // Keep only valid amino acids
|
|
26
|
-
const response = await fetch(`https://3di.foldseek.com/predict/${encodeURIComponent(cleanSequence)}
|
|
26
|
+
const response = await fetch(`https://3di.foldseek.com/predict/${encodeURIComponent(cleanSequence)}`, { signal });
|
|
27
27
|
if (!response.ok) {
|
|
28
28
|
throw new Error(`3Di prediction failed: ${response.status} ${await response.text()}`);
|
|
29
29
|
}
|
|
@@ -35,7 +35,7 @@ export async function predict3Di(aaSequence) {
|
|
|
35
35
|
.trim();
|
|
36
36
|
return { aaSequence: cleanSequence, di3Sequence: cleanDi3 };
|
|
37
37
|
}
|
|
38
|
-
export async function submitFoldseekSearch(aaSequence, di3Sequence, databases) {
|
|
38
|
+
export async function submitFoldseekSearch({ aaSequence, di3Sequence, databases, signal, }) {
|
|
39
39
|
// Submit both AA and 3Di sequences (with trailing newline like working example)
|
|
40
40
|
const fastaContent = `>query\n${aaSequence}\n>3DI\n${di3Sequence}\n`;
|
|
41
41
|
const params = new URLSearchParams();
|
|
@@ -51,14 +51,18 @@ export async function submitFoldseekSearch(aaSequence, di3Sequence, databases) {
|
|
|
51
51
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
52
52
|
},
|
|
53
53
|
body: params,
|
|
54
|
+
signal,
|
|
54
55
|
});
|
|
55
|
-
|
|
56
|
+
// Read the body as text first so a non-JSON error page (e.g. a gateway/500
|
|
57
|
+
// HTML response) surfaces the real status instead of an opaque JSON
|
|
58
|
+
// SyntaxError that hides it.
|
|
59
|
+
const text = await response.text();
|
|
56
60
|
if (!response.ok) {
|
|
57
|
-
throw new Error(`Foldseek submission failed: ${response.status} ${
|
|
61
|
+
throw new Error(`Foldseek submission failed: ${response.status} ${text}`);
|
|
58
62
|
}
|
|
59
|
-
return
|
|
63
|
+
return JSON.parse(text);
|
|
60
64
|
}
|
|
61
|
-
export async function pollFoldseekStatus(ticketId) {
|
|
65
|
+
export async function pollFoldseekStatus({ ticketId, signal, }) {
|
|
62
66
|
// Use the /tickets endpoint (plural) with POST
|
|
63
67
|
const params = new URLSearchParams();
|
|
64
68
|
params.append('tickets[]', ticketId);
|
|
@@ -68,6 +72,7 @@ export async function pollFoldseekStatus(ticketId) {
|
|
|
68
72
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
69
73
|
},
|
|
70
74
|
body: params,
|
|
75
|
+
signal,
|
|
71
76
|
});
|
|
72
77
|
if (!response.ok) {
|
|
73
78
|
throw new Error(`Failed to poll ticket status: ${response.status}`);
|
|
@@ -80,21 +85,24 @@ export async function pollFoldseekStatus(ticketId) {
|
|
|
80
85
|
}
|
|
81
86
|
return result;
|
|
82
87
|
}
|
|
83
|
-
export async function getFoldseekResults(ticketId) {
|
|
84
|
-
return jsonfetch(`https://search.foldseek.com/api/result/${ticketId}/0
|
|
88
|
+
export async function getFoldseekResults({ ticketId, signal, }) {
|
|
89
|
+
return jsonfetch(`https://search.foldseek.com/api/result/${ticketId}/0`, { signal });
|
|
85
90
|
}
|
|
86
|
-
export async function waitForFoldseekResults(ticketId, onStatusChange) {
|
|
91
|
+
export async function waitForFoldseekResults({ ticketId, onStatusChange, signal, }) {
|
|
87
92
|
const maxAttempts = 180;
|
|
88
93
|
let attempts = 0;
|
|
89
94
|
while (attempts < maxAttempts) {
|
|
90
|
-
|
|
95
|
+
if (signal?.aborted) {
|
|
96
|
+
throw signal.reason;
|
|
97
|
+
}
|
|
98
|
+
const status = await pollFoldseekStatus({ ticketId, signal });
|
|
91
99
|
if (status.status === 'ERROR') {
|
|
92
100
|
console.error('[Foldseek] Search error:', status);
|
|
93
101
|
throw new Error(`Foldseek search failed: ${status.error ?? 'Unknown error'}`);
|
|
94
102
|
}
|
|
95
103
|
if (status.status === 'COMPLETE') {
|
|
96
104
|
onStatusChange?.('Fetching results...');
|
|
97
|
-
const apiResponse = await getFoldseekResults(ticketId);
|
|
105
|
+
const apiResponse = await getFoldseekResults({ ticketId, signal });
|
|
98
106
|
// Transform API response to our format
|
|
99
107
|
const results = {
|
|
100
108
|
query: apiResponse.queries[0] ?? { header: '', sequence: '' },
|
|
@@ -106,7 +114,7 @@ export async function waitForFoldseekResults(ticketId, onStatusChange) {
|
|
|
106
114
|
return results;
|
|
107
115
|
}
|
|
108
116
|
onStatusChange?.(`Search ${status.status.toLowerCase()}... (${attempts + 1}s)`);
|
|
109
|
-
await timeout(1000);
|
|
117
|
+
await timeout(1000, signal);
|
|
110
118
|
attempts++;
|
|
111
119
|
}
|
|
112
120
|
throw new Error('Foldseek search timed out');
|
|
@@ -21,13 +21,14 @@ interface LaunchViewParams {
|
|
|
21
21
|
uniprotId?: string;
|
|
22
22
|
}
|
|
23
23
|
export declare function formatViewName(prefix: string, feature: Feature, selectedTranscript?: Feature, uniprotId?: string): string;
|
|
24
|
-
export declare function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, }: LaunchViewParams & {
|
|
24
|
+
export declare function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }: LaunchViewParams & {
|
|
25
25
|
url?: string;
|
|
26
26
|
data?: string;
|
|
27
27
|
userProvidedTranscriptSequence?: string;
|
|
28
28
|
alignmentAlgorithm?: string;
|
|
29
29
|
displayName?: string;
|
|
30
30
|
connectedMsaViewId?: string;
|
|
31
|
+
sideBySide?: boolean;
|
|
31
32
|
}): import("@jbrowse/core/util").AbstractViewModel;
|
|
32
33
|
export declare function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }: LaunchViewParams & {
|
|
33
34
|
confidenceUrl?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isSessionWithAddTracks } from '@jbrowse/core/util';
|
|
2
|
+
import { getLaunchSideBySide, launchViewSideBySide } from './sideBySide';
|
|
2
3
|
import { getGeneDisplayName, getTranscriptDisplayName } from './util';
|
|
3
4
|
import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView';
|
|
4
5
|
export const ALPHAFOLD_VERSION = 'v6';
|
|
@@ -58,7 +59,7 @@ export function formatViewName(prefix, feature, selectedTranscript, uniprotId) {
|
|
|
58
59
|
.filter(s => !!s)
|
|
59
60
|
.join(' - ');
|
|
60
61
|
}
|
|
61
|
-
export function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, }) {
|
|
62
|
+
export function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }) {
|
|
62
63
|
const snap = {
|
|
63
64
|
type: 'ProteinView',
|
|
64
65
|
alignmentAlgorithm,
|
|
@@ -75,7 +76,11 @@ export function launch3DProteinView({ session, view, feature, selectedTranscript
|
|
|
75
76
|
displayName: displayName ??
|
|
76
77
|
formatViewName('Protein view', feature, selectedTranscript, uniprotId),
|
|
77
78
|
};
|
|
78
|
-
|
|
79
|
+
const proteinView = session.addView('ProteinView', snap);
|
|
80
|
+
if (sideBySide ?? getLaunchSideBySide()) {
|
|
81
|
+
launchViewSideBySide(session, proteinView.id);
|
|
82
|
+
}
|
|
83
|
+
return proteinView;
|
|
79
84
|
}
|
|
80
85
|
export async function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }) {
|
|
81
86
|
if (!uniprotId || !isSessionWithAddTracks(session)) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AbstractSessionModel } from '@jbrowse/core/util';
|
|
2
|
+
export declare function getLaunchSideBySide(): boolean;
|
|
3
|
+
export declare function setLaunchSideBySide(value: boolean): void;
|
|
4
|
+
/**
|
|
5
|
+
* Place a freshly-added view to the right of the others in a workspaces (tiled)
|
|
6
|
+
* layout. Mirrors the "Move to split view" view-menu action: queue a splitRight
|
|
7
|
+
* pending move for this view, then enable workspaces so TiledViewsContainer
|
|
8
|
+
* consumes the move on mount (other views land in the left panel, this one in a
|
|
9
|
+
* new right panel). No-op on sessions without workspaces support.
|
|
10
|
+
*/
|
|
11
|
+
export declare function launchViewSideBySide(session: AbstractSessionModel, viewId: string): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Self-contained launch preference (NOT the global/core preferences system):
|
|
2
|
+
// whether a protein view launched from a genome feature opens side-by-side with
|
|
3
|
+
// its connected genome view (left genome | right protein) instead of stacked.
|
|
4
|
+
const SIDE_BY_SIDE_KEY = 'proteinView-launchSideBySide';
|
|
5
|
+
// Default to side-by-side: a connected genome+protein pair reads best as a
|
|
6
|
+
// left/right split. Users can turn it off in the launch dialog's settings.
|
|
7
|
+
const DEFAULT_SIDE_BY_SIDE = true;
|
|
8
|
+
export function getLaunchSideBySide() {
|
|
9
|
+
const stored = localStorage.getItem(SIDE_BY_SIDE_KEY);
|
|
10
|
+
return stored === null ? DEFAULT_SIDE_BY_SIDE : stored === 'true';
|
|
11
|
+
}
|
|
12
|
+
export function setLaunchSideBySide(value) {
|
|
13
|
+
localStorage.setItem(SIDE_BY_SIDE_KEY, value ? 'true' : 'false');
|
|
14
|
+
}
|
|
15
|
+
function isSessionWithWorkspaces(session) {
|
|
16
|
+
return ('setUseWorkspaces' in session &&
|
|
17
|
+
typeof session.setUseWorkspaces === 'function' &&
|
|
18
|
+
'setPendingMove' in session &&
|
|
19
|
+
typeof session.setPendingMove === 'function');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Place a freshly-added view to the right of the others in a workspaces (tiled)
|
|
23
|
+
* layout. Mirrors the "Move to split view" view-menu action: queue a splitRight
|
|
24
|
+
* pending move for this view, then enable workspaces so TiledViewsContainer
|
|
25
|
+
* consumes the move on mount (other views land in the left panel, this one in a
|
|
26
|
+
* new right panel). No-op on sessions without workspaces support.
|
|
27
|
+
*/
|
|
28
|
+
export function launchViewSideBySide(session, viewId) {
|
|
29
|
+
if (isSessionWithWorkspaces(session)) {
|
|
30
|
+
session.setPendingMove({ type: 'splitRight', viewId });
|
|
31
|
+
session.setUseWorkspaces(true);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveShortLaunch } from './resolveShortLaunch';
|
|
2
|
+
import { getLaunchSideBySide, launchViewSideBySide, } from '../LaunchProteinView/utils/sideBySide';
|
|
2
3
|
export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
3
4
|
pluginManager.addToExtensionPoint('LaunchView-ProteinView',
|
|
4
5
|
// LaunchView extension points are typed as transformers `(extendee, props)
|
|
@@ -6,7 +7,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
6
7
|
// handlers and ignores the return value. Casting away the signature
|
|
7
8
|
// mismatch rather than fabricating a fake return.
|
|
8
9
|
// @ts-expect-error
|
|
9
|
-
async ({ session, url, uniprotId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, displayName, height, showControls, showHighlight, zoomToBaseLevel, }) => {
|
|
10
|
+
async ({ session, url, uniprotId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, displayName, height, showControls, showHighlight, zoomToBaseLevel, sideBySide, }) => {
|
|
10
11
|
// Short-URL form: `uniprotId` + `transcriptId` + `connectedView` (no
|
|
11
12
|
// explicit `url`/`feature`/sequence). Derive the structure URL, the
|
|
12
13
|
// transcript feature, and the translated sequence from the connected
|
|
@@ -37,6 +38,9 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
37
38
|
// `connectedView` is supplied we create the LinearGenomeView here and wire
|
|
38
39
|
// its id, letting a single spec entry produce a connected genome+protein
|
|
39
40
|
// pair (e.g. hover a variant to highlight the residue).
|
|
41
|
+
// a connected view this launch created itself can be split beside the
|
|
42
|
+
// protein view; a pre-existing connectedViewId is left in place
|
|
43
|
+
const ownsConnectedView = !connectedViewId && !!connectedView;
|
|
40
44
|
const resolvedConnectedViewId = connectedViewId ??
|
|
41
45
|
(connectedView
|
|
42
46
|
? session.addView('LinearGenomeView', {
|
|
@@ -44,7 +48,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
44
48
|
init: connectedView,
|
|
45
49
|
}).id
|
|
46
50
|
: undefined);
|
|
47
|
-
session.addView('ProteinView', {
|
|
51
|
+
const proteinView = session.addView('ProteinView', {
|
|
48
52
|
type: 'ProteinView',
|
|
49
53
|
alignmentAlgorithm,
|
|
50
54
|
displayName,
|
|
@@ -63,5 +67,8 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
63
67
|
},
|
|
64
68
|
],
|
|
65
69
|
});
|
|
70
|
+
if (ownsConnectedView && (sideBySide ?? getLaunchSideBySide())) {
|
|
71
|
+
launchViewSideBySide(session, proteinView.id);
|
|
72
|
+
}
|
|
66
73
|
});
|
|
67
74
|
}
|
|
@@ -31,7 +31,7 @@ export declare const COLOR_SCHEMES: readonly [{
|
|
|
31
31
|
readonly label: "Molecule type";
|
|
32
32
|
}];
|
|
33
33
|
export type ProteinColorScheme = (typeof COLOR_SCHEMES)[number]['value'];
|
|
34
|
-
export declare const COLOR_SCHEME_VALUES: ("
|
|
34
|
+
export declare const COLOR_SCHEME_VALUES: ("default" | "plddt-confidence" | "chain-id" | "secondary-structure" | "hydrophobicity" | "residue-name" | "uncertainty" | "molecule-type")[];
|
|
35
35
|
export declare function applyColorTheme({ plugin, colorScheme, }: {
|
|
36
36
|
plugin: PluginContext;
|
|
37
37
|
colorScheme: ProteinColorScheme;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PluginContext } from 'molstar/lib/mol-plugin/context';
|
|
2
|
+
export interface StructureData {
|
|
3
|
+
sequences?: string[];
|
|
4
|
+
confidence?: number[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Loads a structure (from inline data or a URL) into the given Molstar plugin
|
|
8
|
+
* and pulls out its per-chain sequences and per-residue confidence. Pure with
|
|
9
|
+
* respect to the model — it only touches the plugin and returns plain data, so
|
|
10
|
+
* callers own the decision of whether/where to store the result.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadStructureData({ structure, plugin, }: {
|
|
13
|
+
structure: {
|
|
14
|
+
data?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
};
|
|
17
|
+
plugin: PluginContext;
|
|
18
|
+
}): Promise<StructureData>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { addStructureFromData } from './addStructureFromData';
|
|
2
|
+
import { addStructureFromURL } from './addStructureFromURL';
|
|
3
|
+
import { extractPerResidueConfidence } from './extractPerResidueConfidence';
|
|
4
|
+
import { extractStructureSequences } from './extractStructureSequences';
|
|
5
|
+
/**
|
|
6
|
+
* Loads a structure (from inline data or a URL) into the given Molstar plugin
|
|
7
|
+
* and pulls out its per-chain sequences and per-residue confidence. Pure with
|
|
8
|
+
* respect to the model — it only touches the plugin and returns plain data, so
|
|
9
|
+
* callers own the decision of whether/where to store the result.
|
|
10
|
+
*/
|
|
11
|
+
export async function loadStructureData({ structure, plugin, }) {
|
|
12
|
+
const { model } = structure.data
|
|
13
|
+
? await addStructureFromData({ data: structure.data, plugin })
|
|
14
|
+
: structure.url
|
|
15
|
+
? await addStructureFromURL({ url: structure.url, plugin })
|
|
16
|
+
: { model: undefined };
|
|
17
|
+
const sequences = model ? extractStructureSequences(model) : undefined;
|
|
18
|
+
const confidence = model
|
|
19
|
+
? extractPerResidueConfidence(model, sequences?.[0]?.length)
|
|
20
|
+
: undefined;
|
|
21
|
+
return { sequences, confidence };
|
|
22
|
+
}
|
|
@@ -19,7 +19,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
19
19
|
id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
|
|
20
20
|
displayName: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
|
|
21
21
|
minimized: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
22
|
-
}, "colorScheme" | "structures" | "
|
|
22
|
+
}, "colorScheme" | "structures" | "id" | "type" | "init" | "zoomToBaseLevel" | "alignmentAlgorithm" | "connectedMsaViewId" | "showHighlight" | "showAlignment" | "showProteinTracks" | "autoScrollAlignment" | "compactTracks" | "showControls" | "height"> & {
|
|
23
23
|
id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
|
|
24
24
|
type: import("@jbrowse/mobx-state-tree").ISimpleType<"ProteinView">;
|
|
25
25
|
structures: import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IModelType<{
|
|
@@ -641,7 +641,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
641
641
|
showHighlight: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
642
642
|
zoomToBaseLevel: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
643
643
|
autoScrollAlignment: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
644
|
-
colorScheme: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<"
|
|
644
|
+
colorScheme: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<"default" | "plddt-confidence" | "chain-id" | "secondary-structure" | "hydrophobicity" | "residue-name" | "uncertainty" | "molecule-type">, [undefined]>;
|
|
645
645
|
showAlignment: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
646
646
|
showProteinTracks: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
647
647
|
compactTracks: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|