jbrowse-plugin-msaview 2.2.2 → 2.2.4

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 (162) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +229 -0
  3. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +23 -18
  4. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js.map +1 -1
  5. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +23 -13
  6. package/dist/AddHighlightModel/MsaToGenomeHighlight.js.map +1 -1
  7. package/dist/AddHighlightModel/index.js +8 -1
  8. package/dist/AddHighlightModel/index.js.map +1 -1
  9. package/dist/AddHighlightModel/util.d.ts +2 -2
  10. package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +2 -2
  11. package/dist/LaunchMsaView/components/EnsemblGeneTree/EnsemblGeneTree.js +5 -11
  12. package/dist/LaunchMsaView/components/EnsemblGeneTree/EnsemblGeneTree.js.map +1 -1
  13. package/dist/LaunchMsaView/components/EnsemblGeneTree/useGeneTree.js +5 -1
  14. package/dist/LaunchMsaView/components/EnsemblGeneTree/useGeneTree.js.map +1 -1
  15. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +16 -16
  16. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js.map +1 -1
  17. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +38 -46
  18. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js.map +1 -1
  19. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +4 -3
  20. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +4 -3
  21. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js.map +1 -1
  22. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.d.ts +9 -0
  23. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +76 -0
  24. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js.map +1 -0
  25. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.js +35 -13
  26. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.js.map +1 -1
  27. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastManualPanel.js +6 -12
  28. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastManualPanel.js.map +1 -1
  29. package/dist/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.d.ts +6 -0
  30. package/dist/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.js +15 -0
  31. package/dist/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.js.map +1 -1
  32. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +12 -34
  33. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js.map +1 -1
  34. package/dist/LaunchMsaView/components/PreLoadedMSA/consts.d.ts +1 -0
  35. package/dist/LaunchMsaView/components/PreLoadedMSA/consts.js +1 -0
  36. package/dist/LaunchMsaView/components/PreLoadedMSA/consts.js.map +1 -1
  37. package/dist/LaunchMsaView/components/TabPanel.d.ts +2 -2
  38. package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +2 -2
  39. package/dist/LaunchMsaView/components/TranscriptSelector.js +3 -6
  40. package/dist/LaunchMsaView/components/TranscriptSelector.js.map +1 -1
  41. package/dist/LaunchMsaView/components/useSWRFeatureSequence.js +6 -4
  42. package/dist/LaunchMsaView/components/useSWRFeatureSequence.js.map +1 -1
  43. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +16 -0
  44. package/dist/LaunchMsaView/components/useTranscriptSelection.js +31 -0
  45. package/dist/LaunchMsaView/components/useTranscriptSelection.js.map +1 -0
  46. package/dist/LaunchMsaView/components/util.d.ts +3 -1
  47. package/dist/LaunchMsaView/components/util.js +12 -2
  48. package/dist/LaunchMsaView/components/util.js.map +1 -1
  49. package/dist/LaunchMsaView/util.d.ts +2 -0
  50. package/dist/LaunchMsaView/util.js +16 -4
  51. package/dist/LaunchMsaView/util.js.map +1 -1
  52. package/dist/LaunchMsaViewExtensionPoint/index.d.ts +2 -0
  53. package/dist/LaunchMsaViewExtensionPoint/index.js +31 -0
  54. package/dist/LaunchMsaViewExtensionPoint/index.js.map +1 -0
  55. package/dist/MsaViewPanel/components/ConnectStructureDialog.d.ts +7 -0
  56. package/dist/MsaViewPanel/components/ConnectStructureDialog.js +56 -0
  57. package/dist/MsaViewPanel/components/ConnectStructureDialog.js.map +1 -0
  58. package/dist/MsaViewPanel/components/MsaViewPanel.js +4 -2
  59. package/dist/MsaViewPanel/components/MsaViewPanel.js.map +1 -1
  60. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -0
  61. package/dist/MsaViewPanel/doLaunchBlast.js +65 -19
  62. package/dist/MsaViewPanel/doLaunchBlast.js.map +1 -1
  63. package/dist/MsaViewPanel/genomeToMSA.d.ts +6 -0
  64. package/dist/MsaViewPanel/genomeToMSA.js +38 -8
  65. package/dist/MsaViewPanel/genomeToMSA.js.map +1 -1
  66. package/dist/MsaViewPanel/genomeToMSA.test.d.ts +1 -0
  67. package/dist/MsaViewPanel/genomeToMSA.test.js +244 -0
  68. package/dist/MsaViewPanel/genomeToMSA.test.js.map +1 -0
  69. package/dist/MsaViewPanel/model.d.ts +727 -226
  70. package/dist/MsaViewPanel/model.js +496 -52
  71. package/dist/MsaViewPanel/model.js.map +1 -1
  72. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +10 -2
  73. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +26 -27
  74. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js.map +1 -1
  75. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.d.ts +1 -0
  76. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +240 -0
  77. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js.map +1 -0
  78. package/dist/MsaViewPanel/msaDataStore.d.ts +14 -0
  79. package/dist/MsaViewPanel/msaDataStore.js +197 -0
  80. package/dist/MsaViewPanel/msaDataStore.js.map +1 -0
  81. package/dist/MsaViewPanel/pairwiseAlignment.d.ts +27 -0
  82. package/dist/MsaViewPanel/pairwiseAlignment.js +776 -0
  83. package/dist/MsaViewPanel/pairwiseAlignment.js.map +1 -0
  84. package/dist/MsaViewPanel/pairwiseAlignment.test.d.ts +1 -0
  85. package/dist/MsaViewPanel/pairwiseAlignment.test.js +112 -0
  86. package/dist/MsaViewPanel/pairwiseAlignment.test.js.map +1 -0
  87. package/dist/MsaViewPanel/structureConnection.d.ts +27 -0
  88. package/dist/MsaViewPanel/structureConnection.js +46 -0
  89. package/dist/MsaViewPanel/structureConnection.js.map +1 -0
  90. package/dist/MsaViewPanel/structureConnection.test.d.ts +1 -0
  91. package/dist/MsaViewPanel/structureConnection.test.js +122 -0
  92. package/dist/MsaViewPanel/structureConnection.test.js.map +1 -0
  93. package/dist/MsaViewPanel/types.d.ts +13 -0
  94. package/dist/MsaViewPanel/types.js +2 -0
  95. package/dist/MsaViewPanel/types.js.map +1 -0
  96. package/dist/MsaViewPanel/util.d.ts +7 -0
  97. package/dist/MsaViewPanel/util.js +10 -0
  98. package/dist/MsaViewPanel/util.js.map +1 -1
  99. package/dist/index.d.ts +5 -5
  100. package/dist/index.js +3 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/jbrowse-plugin-msaview.umd.production.min.js +39 -90
  103. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  104. package/dist/utils/blastCache.d.ts +34 -0
  105. package/dist/utils/blastCache.js +58 -0
  106. package/dist/utils/blastCache.js.map +1 -0
  107. package/dist/utils/fetch.d.ts +1 -1
  108. package/dist/utils/fetch.js +1 -1
  109. package/dist/utils/fetch.js.map +1 -1
  110. package/dist/utils/ncbiBlast.d.ts +1 -5
  111. package/dist/utils/taxonomyNames.d.ts +5 -0
  112. package/dist/utils/taxonomyNames.js +79 -0
  113. package/dist/utils/taxonomyNames.js.map +1 -0
  114. package/dist/utils/types.d.ts +8 -5
  115. package/package.json +50 -54
  116. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +37 -21
  117. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +38 -17
  118. package/src/AddHighlightModel/index.tsx +9 -4
  119. package/src/LaunchMsaView/components/EnsemblGeneTree/EnsemblGeneTree.tsx +13 -13
  120. package/src/LaunchMsaView/components/EnsemblGeneTree/useGeneTree.ts +6 -0
  121. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +30 -23
  122. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +64 -51
  123. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +9 -6
  124. package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +146 -0
  125. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx +53 -22
  126. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastManualPanel.tsx +8 -13
  127. package/src/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.ts +25 -0
  128. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +27 -47
  129. package/src/LaunchMsaView/components/PreLoadedMSA/consts.ts +1 -0
  130. package/src/LaunchMsaView/components/TabPanel.tsx +2 -2
  131. package/src/LaunchMsaView/components/TranscriptSelector.tsx +13 -20
  132. package/src/LaunchMsaView/components/useSWRFeatureSequence.ts +5 -5
  133. package/src/LaunchMsaView/components/useTranscriptSelection.ts +48 -0
  134. package/src/LaunchMsaView/components/util.ts +17 -2
  135. package/src/LaunchMsaView/index.ts +1 -1
  136. package/src/LaunchMsaView/util.ts +25 -6
  137. package/src/LaunchMsaViewExtensionPoint/index.ts +74 -0
  138. package/src/MsaViewPanel/components/ConnectStructureDialog.tsx +156 -0
  139. package/src/MsaViewPanel/components/MsaViewPanel.tsx +6 -1
  140. package/src/MsaViewPanel/doLaunchBlast.ts +83 -23
  141. package/src/MsaViewPanel/genomeToMSA.test.ts +281 -0
  142. package/src/MsaViewPanel/genomeToMSA.ts +43 -10
  143. package/src/MsaViewPanel/model.ts +617 -58
  144. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +256 -0
  145. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +42 -30
  146. package/src/MsaViewPanel/msaDataStore.ts +236 -0
  147. package/src/MsaViewPanel/pairwiseAlignment.test.ts +140 -0
  148. package/src/MsaViewPanel/pairwiseAlignment.ts +818 -0
  149. package/src/MsaViewPanel/structureConnection.test.ts +143 -0
  150. package/src/MsaViewPanel/structureConnection.ts +72 -0
  151. package/src/MsaViewPanel/types.ts +14 -0
  152. package/src/MsaViewPanel/util.ts +11 -0
  153. package/src/index.ts +3 -1
  154. package/src/utils/blastCache.ts +114 -0
  155. package/src/utils/fetch.ts +1 -1
  156. package/src/utils/taxonomyNames.ts +111 -0
  157. package/src/utils/types.ts +9 -1
  158. package/dist/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.d.ts +0 -5
  159. package/dist/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.js +0 -16
  160. package/dist/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.js.map +0 -1
  161. package/dist/out.js +0 -55367
  162. package/src/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.ts +0 -25
@@ -1,12 +1,56 @@
1
+ import { lazy } from 'react';
1
2
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes';
2
3
  import { getSession } from '@jbrowse/core/util';
4
+ import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree';
3
5
  import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
4
6
  import { autorun } from 'mobx';
5
- import { addDisposer, cast, types } from 'mobx-state-tree';
6
7
  import { MSAModelF } from 'react-msaview';
7
8
  import { doLaunchBlast } from './doLaunchBlast';
8
9
  import { genomeToMSA } from './genomeToMSA';
9
10
  import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord';
11
+ import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
12
+ import { buildAlignmentMaps, runPairwiseAlignment } from './pairwiseAlignment';
13
+ import { gappedToUngappedPosition, mapToRecord, ungappedToGappedPosition, } from './structureConnection';
14
+ import { getUniprotIdFromAlphaFoldUrl } from './util';
15
+ const ConnectStructureDialog = lazy(() => import('./components/ConnectStructureDialog'));
16
+ /**
17
+ * Highlights residues in connected protein structures based on current MSA hover position
18
+ */
19
+ function highlightConnectedStructures(self) {
20
+ const { mouseCol, connectedProteinViews } = self;
21
+ if (connectedProteinViews.length === 0) {
22
+ return;
23
+ }
24
+ for (const conn of connectedProteinViews) {
25
+ const structure = conn.proteinView?.structures?.[conn.structureIdx];
26
+ if (!structure) {
27
+ continue;
28
+ }
29
+ // Clear highlight if mouse left MSA
30
+ if (mouseCol === undefined) {
31
+ structure.clearHighlightFromExternal?.();
32
+ continue;
33
+ }
34
+ const seq = self.getSequenceByRowName(conn.msaRowName);
35
+ if (!seq) {
36
+ continue;
37
+ }
38
+ // Convert gapped MSA column to ungapped position
39
+ const msaUngapped = gappedToUngappedPosition(seq, mouseCol);
40
+ if (msaUngapped === undefined) {
41
+ structure.clearHighlightFromExternal?.();
42
+ continue;
43
+ }
44
+ // Map to structure position and highlight
45
+ const structurePos = conn.msaToStructure[msaUngapped];
46
+ if (structurePos === undefined) {
47
+ structure.clearHighlightFromExternal?.();
48
+ }
49
+ else {
50
+ structure.highlightFromExternal?.(structurePos);
51
+ }
52
+ }
53
+ }
10
54
  /**
11
55
  * #stateModel MsaViewPlugin
12
56
  * extends
@@ -39,10 +83,43 @@ export default function stateModelFactory() {
39
83
  * #property
40
84
  */
41
85
  querySeqName: 'QUERY',
86
+ /**
87
+ * #property
88
+ * UniProt ID extracted from AlphaFold MSA URL
89
+ */
90
+ uniprotId: types.maybe(types.string),
42
91
  /**
43
92
  * #property
44
93
  */
45
94
  zoomToBaseLevel: false,
95
+ /**
96
+ * #property
97
+ * used for loading the MSA view via session snapshots, e.g.
98
+ * {
99
+ * "type": "MsaView",
100
+ * "init": {
101
+ * "msaUrl": "https://example.com/alignment.fa",
102
+ * "treeUrl": "https://example.com/tree.nh",
103
+ * "querySeqName": "ENST00000123_hg38"
104
+ * }
105
+ * }
106
+ */
107
+ init: types.frozen(),
108
+ /**
109
+ * #property
110
+ * connections to protein 3D structure views for synchronized highlighting
111
+ */
112
+ connectedStructures: types.array(types.frozen()),
113
+ /**
114
+ * #property
115
+ * Reference ID for MSA data stored in IndexedDB (for large datasets)
116
+ */
117
+ dataStoreId: types.maybe(types.string),
118
+ /**
119
+ * #property
120
+ * MAF region for coordinate mapping (used when launched from MAF viewer)
121
+ */
122
+ mafRegion: types.frozen(),
46
123
  }))
47
124
  .volatile(() => ({
48
125
  /**
@@ -57,25 +134,26 @@ export default function stateModelFactory() {
57
134
  * #volatile
58
135
  */
59
136
  error: undefined,
137
+ /**
138
+ * #volatile
139
+ * True when loading MSA data from IndexedDB
140
+ */
141
+ loadingStoredData: false,
60
142
  }))
61
143
  .views(self => ({
62
144
  /**
63
145
  * #method
146
+ * Get a row by name, returns [name, sequence] or undefined
64
147
  */
65
- ungappedCoordMap(rowName, position) {
66
- const row = self.rows.find(f => f[0] === rowName);
67
- const seq = row?.[1];
68
- if (seq && position < seq.length) {
69
- let i = 0;
70
- let j = 0;
71
- for (; j < position; j++, i++) {
72
- while (seq[i] === '-') {
73
- i++;
74
- }
75
- }
76
- return i;
77
- }
78
- return undefined;
148
+ getRowByName(rowName) {
149
+ return self.rows.find(r => r[0] === rowName);
150
+ },
151
+ /**
152
+ * #method
153
+ * Get the sequence for a row by name
154
+ */
155
+ getSequenceByRowName(rowName) {
156
+ return self.rows.find(r => r[0] === rowName)?.[1];
79
157
  },
80
158
  }))
81
159
  .views(self => ({
@@ -87,34 +165,81 @@ export default function stateModelFactory() {
87
165
  ? genomeToTranscriptSeqMapping(self.connectedFeature)
88
166
  : undefined;
89
167
  },
90
- }))
91
- .views(self => ({
92
168
  /**
93
169
  * #getter
94
170
  */
95
- get mouseCol2() {
96
- return genomeToMSA({ model: self });
171
+ get processing() {
172
+ return !!self.progress;
97
173
  },
98
174
  /**
99
175
  * #getter
100
176
  */
101
- get clickCol2() {
177
+ get connectedView() {
178
+ const { views } = getSession(self);
179
+ return views.find(f => f.id === self.connectedViewId);
180
+ },
181
+ /**
182
+ * #getter
183
+ * Get connected protein views with their full model references
184
+ */
185
+ get connectedProteinViews() {
186
+ const { views } = getSession(self);
187
+ return self.connectedStructures
188
+ .map(conn => {
189
+ const proteinView = views.find((v) => v.id === conn.proteinViewId);
190
+ return proteinView ? { ...conn, proteinView } : undefined;
191
+ })
192
+ .filter((c) => !!c);
193
+ },
194
+ }))
195
+ .views(self => ({
196
+ /**
197
+ * #getter
198
+ * Get the MSA column that corresponds to the currently hovered structure position
199
+ * Returns the first match from any connected structure
200
+ */
201
+ get structureHoverCol() {
202
+ for (const conn of self.connectedProteinViews) {
203
+ const structure = conn.proteinView?.structures?.[conn.structureIdx];
204
+ const structurePos = structure?.hoverPosition?.structureSeqPos;
205
+ if (structurePos !== undefined) {
206
+ const msaUngapped = conn.structureToMsa[structurePos];
207
+ if (msaUngapped !== undefined) {
208
+ const seq = self.getSequenceByRowName(conn.msaRowName);
209
+ if (seq) {
210
+ // Convert ungapped position to global column, then to visible column
211
+ const globalCol = ungappedToGappedPosition(seq, msaUngapped);
212
+ if (globalCol !== undefined) {
213
+ return self.globalColToVisibleCol(globalCol);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
102
219
  return undefined;
103
220
  },
104
221
  }))
105
222
  .views(self => ({
106
223
  /**
107
224
  * #getter
225
+ * Returns a secondary highlight column from either:
226
+ * 1. Structure hover (from connected protein 3D view)
227
+ * 2. Genome hover (from connected linear genome view)
108
228
  */
109
- get processing() {
110
- return !!self.progress;
229
+ get mouseCol2() {
230
+ // Check structure hover first
231
+ const structureCol = self.structureHoverCol;
232
+ if (structureCol !== undefined) {
233
+ return structureCol;
234
+ }
235
+ // Fall back to genome hover
236
+ return genomeToMSA({ model: self });
111
237
  },
112
238
  /**
113
239
  * #getter
114
240
  */
115
- get connectedView() {
116
- const { views } = getSession(self);
117
- return views.find(f => f.id === self.connectedViewId);
241
+ get clickCol2() {
242
+ return undefined;
118
243
  },
119
244
  }))
120
245
  .actions(self => ({
@@ -166,7 +291,132 @@ export default function stateModelFactory() {
166
291
  setBlastParams(args) {
167
292
  self.blastParams = args;
168
293
  },
294
+ /**
295
+ * #action
296
+ */
297
+ setInit(arg) {
298
+ self.init = arg;
299
+ },
300
+ /**
301
+ * #action
302
+ */
303
+ setQuerySeqName(arg) {
304
+ self.querySeqName = arg;
305
+ },
306
+ /**
307
+ * #action
308
+ */
309
+ setUniprotId(arg) {
310
+ self.uniprotId = arg;
311
+ },
312
+ /**
313
+ * #action
314
+ */
315
+ setDataStoreId(arg) {
316
+ self.dataStoreId = arg;
317
+ },
318
+ /**
319
+ * #action
320
+ */
321
+ setMafRegion(arg) {
322
+ self.mafRegion = arg;
323
+ },
324
+ /**
325
+ * #action
326
+ */
327
+ setLoadingStoredData(arg) {
328
+ self.loadingStoredData = arg;
329
+ },
330
+ /**
331
+ * #action
332
+ */
333
+ handleMsaClick(coord) {
334
+ const { connectedView, zoomToBaseLevel } = self;
335
+ const { assemblyManager } = getSession(self);
336
+ const r2 = msaCoordToGenomeCoord({ model: self, coord });
337
+ if (!r2 || !connectedView) {
338
+ return;
339
+ }
340
+ if (zoomToBaseLevel) {
341
+ connectedView.navTo(r2);
342
+ }
343
+ else {
344
+ const r = assemblyManager
345
+ .get(connectedView.assemblyNames[0])
346
+ ?.getCanonicalRefName(r2.refName) ?? r2.refName;
347
+ connectedView.centerAt(r2.start, r);
348
+ }
349
+ },
350
+ /**
351
+ * #action
352
+ * Connect to a protein structure for synchronized highlighting
353
+ */
354
+ connectToStructure(proteinViewId, structureIdx, msaRowName) {
355
+ const rowName = msaRowName ?? self.querySeqName;
356
+ const msaSequence = self.getSequenceByRowName(rowName);
357
+ if (!msaSequence) {
358
+ throw new Error(`MSA row "${rowName}" not found`);
359
+ }
360
+ const ungappedMsaSequence = msaSequence.replaceAll('-', '');
361
+ const { views } = getSession(self);
362
+ const proteinView = views.find((v) => v.id === proteinViewId);
363
+ if (!proteinView) {
364
+ throw new Error(`ProteinView "${proteinViewId}" not found`);
365
+ }
366
+ const structure = proteinView.structures?.[structureIdx];
367
+ if (!structure) {
368
+ throw new Error(`Structure at index ${structureIdx} not found`);
369
+ }
370
+ const structureSequence = structure.structureSequences?.[0];
371
+ if (!structureSequence) {
372
+ throw new Error('Structure sequence not available');
373
+ }
374
+ const alignment = runPairwiseAlignment(ungappedMsaSequence, structureSequence);
375
+ const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment);
376
+ const connection = {
377
+ proteinViewId,
378
+ structureIdx,
379
+ msaRowName: rowName,
380
+ msaToStructure: mapToRecord(seq1ToSeq2),
381
+ structureToMsa: mapToRecord(seq2ToSeq1),
382
+ };
383
+ self.connectedStructures.push(connection);
384
+ },
385
+ /**
386
+ * #action
387
+ * Disconnect from a protein structure
388
+ */
389
+ disconnectFromStructure(proteinViewId, structureIdx) {
390
+ const idx = self.connectedStructures.findIndex(c => c.proteinViewId === proteinViewId &&
391
+ c.structureIdx === structureIdx);
392
+ if (idx !== -1) {
393
+ self.connectedStructures.splice(idx, 1);
394
+ }
395
+ },
396
+ /**
397
+ * #action
398
+ * Disconnect from all protein structures
399
+ */
400
+ disconnectAllStructures() {
401
+ self.connectedStructures.clear();
402
+ },
169
403
  }))
404
+ .actions(self => {
405
+ // store reference to the original action from react-msaview
406
+ const superSetMouseClickPos = self.setMouseClickPos.bind(self);
407
+ return {
408
+ /**
409
+ * #action
410
+ * overrides base setMouseClickPos to trigger navigation
411
+ */
412
+ setMouseClickPos(col, row) {
413
+ superSetMouseClickPos(col, row);
414
+ if (col !== undefined) {
415
+ self.handleMsaClick(col);
416
+ }
417
+ },
418
+ };
419
+ })
170
420
  .views(self => ({
171
421
  /**
172
422
  * #method
@@ -182,24 +432,95 @@ export default function stateModelFactory() {
182
432
  self.setZoomToBaseLevel(!self.zoomToBaseLevel);
183
433
  },
184
434
  },
435
+ {
436
+ label: 'Connect to protein structure...',
437
+ onClick: () => {
438
+ getSession(self).queueDialog(handleClose => [
439
+ ConnectStructureDialog,
440
+ {
441
+ model: self,
442
+ handleClose,
443
+ },
444
+ ]);
445
+ },
446
+ },
447
+ ...(self.connectedStructures.length > 0
448
+ ? [
449
+ {
450
+ label: 'Disconnect from protein structures',
451
+ onClick: () => {
452
+ self.disconnectAllStructures();
453
+ },
454
+ },
455
+ ]
456
+ : []),
185
457
  ];
186
458
  },
187
- /**
188
- * #getter
189
- */
190
- get processing() {
191
- return !!self.progress;
192
- },
193
- /**
194
- * #getter
195
- */
196
- get connectedView() {
197
- const { views } = getSession(self);
198
- return views.find(f => f.id === self.connectedViewId);
199
- },
200
459
  }))
201
460
  .actions(self => ({
202
461
  afterCreate() {
462
+ // Clean up old IndexedDB entries on startup
463
+ cleanupOldData().catch((e) => {
464
+ console.error('Failed to cleanup old MSA data:', e);
465
+ });
466
+ // Load MSA data from IndexedDB if dataStoreId exists and no data loaded
467
+ addDisposer(self, autorun(async () => {
468
+ const { dataStoreId, rows } = self;
469
+ if (dataStoreId && rows.length === 0) {
470
+ try {
471
+ self.setLoadingStoredData(true);
472
+ const storedData = await retrieveMsaData(dataStoreId);
473
+ if (storedData) {
474
+ if (storedData.msa) {
475
+ self.setMSA(storedData.msa);
476
+ }
477
+ if (storedData.tree) {
478
+ self.setTree(storedData.tree);
479
+ }
480
+ }
481
+ }
482
+ catch (e) {
483
+ console.error('Failed to load MSA data from IndexedDB:', e);
484
+ }
485
+ finally {
486
+ self.setLoadingStoredData(false);
487
+ }
488
+ }
489
+ }));
490
+ // Store MSA data to IndexedDB when loaded from inline data (no filehandle)
491
+ // This ensures data persists across page refreshes even when
492
+ // react-msaview's postProcessSnapshot would strip it
493
+ addDisposer(self, autorun(async () => {
494
+ const { rows, dataStoreId } = self;
495
+ // Only store if we have data and don't already have a dataStoreId
496
+ if (rows.length > 0 && !dataStoreId) {
497
+ // Only store if there's no filehandle (filehandles can reload from source)
498
+ const hasFilehandle = !!(self.msaFilehandle ?? self.treeFilehandle);
499
+ if (hasFilehandle) {
500
+ return;
501
+ }
502
+ const msaData = self.data.msa;
503
+ const treeData = self.data.tree;
504
+ // Only store if we have actual data
505
+ if (msaData || treeData) {
506
+ try {
507
+ const newId = generateDataStoreId();
508
+ const success = await storeMsaData(newId, {
509
+ msa: msaData,
510
+ tree: treeData,
511
+ treeMetadata: self.data.treeMetadata,
512
+ });
513
+ // Only set dataStoreId if storage was successful
514
+ if (success) {
515
+ self.setDataStoreId(newId);
516
+ }
517
+ }
518
+ catch (e) {
519
+ console.error('Failed to store MSA data to IndexedDB:', e);
520
+ }
521
+ }
522
+ }
523
+ }));
203
524
  addDisposer(self, autorun(async () => {
204
525
  if (self.blastParams) {
205
526
  try {
@@ -220,6 +541,56 @@ export default function stateModelFactory() {
220
541
  }
221
542
  }
222
543
  }));
544
+ // process init parameter for loading MSA from session snapshots
545
+ addDisposer(self, autorun(async () => {
546
+ const { init } = self;
547
+ if (init) {
548
+ try {
549
+ self.setError(undefined);
550
+ const { msaData, msaUrl, treeData, treeUrl, querySeqName } = init;
551
+ // Extract uniprotId from AlphaFold MSA URL and set querySeqName
552
+ if (msaUrl) {
553
+ const id = getUniprotIdFromAlphaFoldUrl(msaUrl);
554
+ if (id) {
555
+ self.setUniprotId(id);
556
+ // AlphaFold MSA files use 'query' as the row name
557
+ self.setQuerySeqName('query');
558
+ }
559
+ }
560
+ // User-provided querySeqName takes precedence
561
+ if (querySeqName) {
562
+ self.setQuerySeqName(querySeqName);
563
+ }
564
+ if (msaData) {
565
+ self.setMSA(msaData);
566
+ }
567
+ else if (msaUrl) {
568
+ const response = await fetch(msaUrl);
569
+ if (!response.ok) {
570
+ throw new Error(`Failed to fetch MSA: ${response.status}`);
571
+ }
572
+ const data = await response.text();
573
+ self.setMSA(data);
574
+ }
575
+ if (treeData) {
576
+ self.setTree(treeData);
577
+ }
578
+ else if (treeUrl) {
579
+ const response = await fetch(treeUrl);
580
+ if (!response.ok) {
581
+ throw new Error(`Failed to fetch tree: ${response.status}`);
582
+ }
583
+ const data = await response.text();
584
+ self.setTree(data);
585
+ }
586
+ self.setInit(undefined);
587
+ }
588
+ catch (e) {
589
+ self.setError(e);
590
+ console.error(e);
591
+ }
592
+ }
593
+ }));
223
594
  // this adds highlights to the genome view when mouse-ing over the MSA
224
595
  addDisposer(self, autorun(() => {
225
596
  const { mouseCol, mouseClickCol } = self;
@@ -231,25 +602,98 @@ export default function stateModelFactory() {
231
602
  : msaCoordToGenomeCoord({ model: self, coord: mouseClickCol });
232
603
  self.setConnectedHighlights([r1, r2].filter(f => !!f));
233
604
  }));
234
- // nav to genome position after click
605
+ // this highlights residues in connected protein structures when mousing over the MSA
235
606
  addDisposer(self, autorun(() => {
236
- const { connectedView, zoomToBaseLevel, mouseClickCol } = self;
237
- const { assemblyManager } = getSession(self);
238
- const r2 = mouseClickCol === undefined
239
- ? undefined
240
- : msaCoordToGenomeCoord({ model: self, coord: mouseClickCol });
241
- if (!r2 || !connectedView) {
607
+ highlightConnectedStructures(self);
608
+ }));
609
+ // auto-connect to compatible ProteinViews
610
+ addDisposer(self, autorun(() => {
611
+ const { views } = getSession(self);
612
+ const { connectedViewId, uniprotId, rows, connectedStructures } = self;
613
+ // Need MSA loaded and a uniprotId to auto-connect
614
+ if (!uniprotId || rows.length === 0) {
242
615
  return;
243
616
  }
244
- if (zoomToBaseLevel) {
245
- connectedView.navTo(r2);
617
+ // Find ProteinViews that share the same connectedViewId
618
+ for (const view of views) {
619
+ const v = view;
620
+ if (v.type !== 'ProteinView' || !v.structures) {
621
+ continue;
622
+ }
623
+ for (let structureIdx = 0; structureIdx < v.structures.length; structureIdx++) {
624
+ const structure = v.structures[structureIdx];
625
+ // Check if structure shares the same genome view connection
626
+ if (structure.connectedViewId !== connectedViewId) {
627
+ continue;
628
+ }
629
+ // Check if structure has matching uniprotId
630
+ if (structure.uniprotId !== uniprotId) {
631
+ continue;
632
+ }
633
+ // Check if already connected
634
+ const alreadyConnected = connectedStructures.some(c => c.proteinViewId === v.id && c.structureIdx === structureIdx);
635
+ if (alreadyConnected) {
636
+ continue;
637
+ }
638
+ // Check if structure sequence is available
639
+ if (!structure.structureSequences?.[0]) {
640
+ continue;
641
+ }
642
+ // Auto-connect
643
+ try {
644
+ self.connectToStructure(v.id, structureIdx);
645
+ }
646
+ catch (e) {
647
+ console.error('Failed to auto-connect to ProteinView:', e);
648
+ }
649
+ }
650
+ }
651
+ }));
652
+ // Observe protein3d genome highlights and update MSA highlighted columns
653
+ // This enables communication via the linear genome view coordinates
654
+ addDisposer(self, autorun(() => {
655
+ const { views } = getSession(self);
656
+ const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
657
+ if (!connectedViewId || !transcriptToMsaMap) {
658
+ return;
246
659
  }
247
- else {
248
- const r = assemblyManager
249
- .get(connectedView.assemblyNames[0])
250
- ?.getCanonicalRefName(r2.refName) ?? r2.refName;
251
- connectedView.centerAt(r2.start, r);
660
+ const columns = [];
661
+ // Find ProteinViews that share the same connected genome view
662
+ for (const view of views) {
663
+ const v = view;
664
+ if (v.type !== 'ProteinView' || !v.structures) {
665
+ continue;
666
+ }
667
+ for (const structure of v.structures) {
668
+ // Check if structure is connected to same genome view
669
+ if (structure.connectedViewId !== connectedViewId) {
670
+ continue;
671
+ }
672
+ // Check if structure has hover genome highlights
673
+ const highlights = structure.hoverGenomeHighlights;
674
+ if (!highlights || highlights.length === 0) {
675
+ continue;
676
+ }
677
+ // Map genome coordinates to MSA columns
678
+ const { g2p } = transcriptToMsaMap;
679
+ for (const highlight of highlights) {
680
+ for (let coord = highlight.start; coord < highlight.end; coord++) {
681
+ const proteinPos = g2p[coord];
682
+ if (proteinPos !== undefined) {
683
+ const col = self.seqPosToGlobalCol(querySeqName, proteinPos);
684
+ if (!columns.includes(col)) {
685
+ columns.push(col);
686
+ }
687
+ }
688
+ }
689
+ }
690
+ }
252
691
  }
692
+ // Convert global column indices to visible column indices
693
+ const visibleColumns = columns
694
+ .map(col => self.globalColToVisibleCol(col))
695
+ .filter((col) => col !== undefined);
696
+ self.setHighlightedColumns(visibleColumns.length > 0 ? visibleColumns : undefined);
253
697
  }));
254
698
  },
255
699
  }));