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.
Files changed (71) hide show
  1. package/dist/LaunchProteinView/components/PdbSearch.js +18 -4
  2. package/dist/LaunchProteinView/hooks/useAlphaFoldData.js +5 -2
  3. package/dist/LaunchProteinView/hooks/useUniProtIdLookup.js +11 -15
  4. package/dist/LaunchProteinView/services/foldseekApi.js +33 -8
  5. package/dist/LaunchProteinView/utils/launchHelpers.js +3 -3
  6. package/dist/ProteinView/components/ChainSelect.js +6 -3
  7. package/dist/ProteinView/components/FeatureBar.js +1 -1
  8. package/dist/ProteinView/components/HeaderStructureRow.js +13 -4
  9. package/dist/ProteinView/components/ManualAlignmentDialog.js +14 -18
  10. package/dist/ProteinView/entityAlignedTo.js +1 -1
  11. package/dist/ProteinView/frameSelection.js +10 -4
  12. package/dist/ProteinView/model.js +14 -7
  13. package/dist/ProteinView/molstarExports.js +1 -0
  14. package/dist/ProteinView/storedSettings.js +17 -1
  15. package/dist/ProteinView/structureLoader.js +12 -2
  16. package/dist/ProteinView/structureModel.js +93 -56
  17. package/dist/ProteinView/structureSuperposer.js +23 -14
  18. package/dist/ProteinView/structureVisibility.js +17 -0
  19. package/dist/chunks/ProteinView-CFQG6IHO.js +9 -0
  20. package/dist/chunks/ProteinView-CFQG6IHO.js.map +7 -0
  21. package/dist/chunks/chunk-V2WE2V7Z.js +16 -0
  22. package/dist/chunks/chunk-V2WE2V7Z.js.map +7 -0
  23. package/dist/chunks/{molstarExports-HAGQ7LSM.js → molstarExports-YN6TKVEW.js} +36 -36
  24. package/dist/jbrowse-plugin-protein3d.esm.js +7 -7
  25. package/dist/jbrowse-plugin-protein3d.esm.js.map +4 -4
  26. package/dist/version.js +1 -1
  27. package/package.json +2 -15
  28. package/src/AddHighlightModel/index.test.ts +6 -5
  29. package/src/AddHighlightModel/proteinViewLookup.test.ts +3 -1
  30. package/src/LaunchProteinView/components/PdbSearch.tsx +21 -5
  31. package/src/LaunchProteinView/hooks/useAlphaFoldData.test.ts +44 -0
  32. package/src/LaunchProteinView/hooks/useAlphaFoldData.ts +6 -3
  33. package/src/LaunchProteinView/hooks/useUniProtIdLookup.ts +14 -18
  34. package/src/LaunchProteinView/services/foldseekApi.test.ts +43 -0
  35. package/src/LaunchProteinView/services/foldseekApi.ts +40 -12
  36. package/src/LaunchProteinView/utils/launchHelpers.ts +3 -3
  37. package/src/LaunchProteinView/utils/translateTranscripts.test.ts +9 -1
  38. package/src/ProteinView/components/ChainSelect.tsx +8 -3
  39. package/src/ProteinView/components/FeatureBar.tsx +1 -1
  40. package/src/ProteinView/components/HeaderStructureRow.tsx +30 -3
  41. package/src/ProteinView/components/ManualAlignmentDialog.tsx +19 -25
  42. package/src/ProteinView/connectedHover.test.ts +3 -1
  43. package/src/ProteinView/entityAlignedTo.ts +3 -1
  44. package/src/ProteinView/frameSelection.test.ts +46 -1
  45. package/src/ProteinView/frameSelection.ts +16 -5
  46. package/src/ProteinView/geneExplorerLinkage.test.ts +3 -2
  47. package/src/ProteinView/kyteDoolittleColorTheme.test.ts +4 -1
  48. package/src/ProteinView/mappedChainColorTheme.test.ts +4 -1
  49. package/src/ProteinView/model.test.ts +13 -6
  50. package/src/ProteinView/model.ts +20 -10
  51. package/src/ProteinView/molstarExports.ts +1 -0
  52. package/src/ProteinView/proteinViewSpec.ts +2 -0
  53. package/src/ProteinView/storedSettings.ts +18 -2
  54. package/src/ProteinView/structureLoader.test.ts +34 -1
  55. package/src/ProteinView/structureLoader.ts +14 -2
  56. package/src/ProteinView/structureModel.test.ts +112 -3
  57. package/src/ProteinView/structureModel.ts +106 -67
  58. package/src/ProteinView/structureSuperposer.test.ts +46 -0
  59. package/src/ProteinView/structureSuperposer.ts +26 -19
  60. package/src/ProteinView/structureVisibility.ts +33 -0
  61. package/src/ProteinView/viewInteractions.ts +0 -1
  62. package/src/ProteinView/withStoredSettings.test.ts +30 -5
  63. package/src/UniProtVariationAdapter/parseUniProtVariants.test.ts +4 -3
  64. package/src/version.ts +1 -1
  65. package/dist/ProteinView/proteinAbbreviationMapping.js +0 -22
  66. package/dist/chunks/ProteinView-5UGGQVH6.js +0 -9
  67. package/dist/chunks/ProteinView-5UGGQVH6.js.map +0 -7
  68. package/dist/chunks/chunk-3IKI3UVA.js +0 -16
  69. package/dist/chunks/chunk-3IKI3UVA.js.map +0 -7
  70. package/src/ProteinView/proteinAbbreviationMapping.ts +0 -24
  71. /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 loadingStatuses = [
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
- ].filter((s) => !!s);
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: isLoading, error: error }))));
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
- // with an error, data is the previous accession's (keepPreviousData)
11
- const model = useMemo(() => data && !error ? pickAlphaFoldModel(data, isoformSequences) : undefined, [data, error, isoformSequences]);
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 [lookupMode, setLookupMode] = useState('auto');
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 featureUniprotId = geneIds.uniprotId;
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 = effectiveLookupMode === 'feature'
73
+ const uniprotId = lookupMode === 'feature'
78
74
  ? featureUniprotId
79
75
  : isAutoMode
80
76
  ? (pickedUniprotId ?? autoUniprotId)
81
77
  : debouncedManualUniprotId;
82
78
  return {
83
- lookupMode: effectiveLookupMode,
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
- // Read the body as text rather than response.json() so a non-JSON error page
73
- // (a gateway 500's HTML) surfaces as itself instead of an opaque
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
- return JSON.parse(text);
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. The
26
- * AlphaFold tab keeps the requirement for free — its structure url is derived
27
- * from the accession, so no accession means no structure either.
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 || !userProvidedTranscriptSequence) {
12
+ if (!entities || entities.length < 2) {
12
13
  return null;
13
14
  }
14
- return (React.createElement(Tooltip, { title: "Mapped chain: the one the transcript maps to", placement: "left" },
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: {
@@ -47,7 +47,7 @@ const FeatureBar = observer(function FeatureBar({ layout, top, selected, model,
47
47
  structureSeqEndPos: layout.structureEnd,
48
48
  }).catch((e) => {
49
49
  console.error(e);
50
- model.setError(e);
50
+ model.setViewError(e);
51
51
  });
52
52
  }
53
53
  }, onMouseEnter: () => {
@@ -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 switchable = model.showAlignment && model.structures.length > 1;
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 ? React.createElement(ChainSelect, { model: structure }) : null,
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 { pairwiseAlignmentProblem, pairwiseAlignmentSequenceProblem, stripStopCodon, } from 'p2s_mapper';
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, primaryStructure } = model;
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
- // Rejected here rather than committed: every coordinate map is built
20
- // from these two rows by the `coordinateMapper` getter, which throws on
21
- // a bad pair during render — outside this catch, taking the whole view
22
- // down instead of reporting a bad paste. Same predicate the map builder
23
- // asserts on, so what the dialog accepts is exactly what it can use.
24
- // The rows also have to spell the transcript and the mapped chain:
25
- // the maps count residues along each row, so an alignment made
26
- // against another isoform or chain is well-formed and wrong.
27
- const problem = pairwiseAlignmentProblem(parsed) ??
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
- primaryStructure.importAlignment(parsed);
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, superposed: the
5
- * reset that ends a superposition would undo any framing done before it.
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
- (structures.length < 2 || superposedCount === structures.length));
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 && s.molstarStructure && s.clickedLabelSeqIds.length
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 { superposeStructures } from './superposeStructures';
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
- if (self.molstarPluginContext) {
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
- return readStoredJson(SETTINGS_KEY);
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(() => loadStructureData({ structure, plugin }));
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 && !loadingStructures.has(structure)) {
131
+ if (!structure.loadedToMolstar &&
132
+ !loadingStructures.has(structure) &&
133
+ failedIn.get(structure) !== molstarPluginContext) {
124
134
  loadInto(structure, molstarPluginContext);
125
135
  }
126
136
  }