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,22 +1,86 @@
1
+ import { lazy } from 'react'
2
+
1
3
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes'
2
4
  import { getSession } from '@jbrowse/core/util'
5
+ import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree'
3
6
  import { genomeToTranscriptSeqMapping } from 'g2p_mapper'
4
7
  import { autorun } from 'mobx'
5
- import { addDisposer, cast, types } from 'mobx-state-tree'
6
8
  import { MSAModelF } from 'react-msaview'
7
9
 
8
10
  import { doLaunchBlast } from './doLaunchBlast'
9
11
  import { genomeToMSA } from './genomeToMSA'
10
12
  import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
13
+ import {
14
+ cleanupOldData,
15
+ generateDataStoreId,
16
+ retrieveMsaData,
17
+ storeMsaData,
18
+ } from './msaDataStore'
19
+ import { buildAlignmentMaps, runPairwiseAlignment } from './pairwiseAlignment'
20
+ import {
21
+ gappedToUngappedPosition,
22
+ mapToRecord,
23
+ ungappedToGappedPosition,
24
+ } from './structureConnection'
25
+ import { getUniprotIdFromAlphaFoldUrl } from './util'
11
26
 
27
+ import type { StructureConnection } from './structureConnection'
28
+ import type { MafRegion, MsaViewInitState } from './types'
12
29
  import type { Feature } from '@jbrowse/core/util'
30
+ import type { Instance } from '@jbrowse/mobx-state-tree'
13
31
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
14
- import type { Instance } from 'mobx-state-tree'
32
+
33
+ const ConnectStructureDialog = lazy(
34
+ () => import('./components/ConnectStructureDialog'),
35
+ )
15
36
 
16
37
  type LGV = LinearGenomeViewModel
17
38
 
18
39
  type MaybeLGV = LGV | undefined
19
40
 
41
+ /**
42
+ * Highlights residues in connected protein structures based on current MSA hover position
43
+ */
44
+ function highlightConnectedStructures(self: JBrowsePluginMsaViewModel) {
45
+ const { mouseCol, connectedProteinViews } = self
46
+ if (connectedProteinViews.length === 0) {
47
+ return
48
+ }
49
+
50
+ for (const conn of connectedProteinViews) {
51
+ const structure = conn.proteinView?.structures?.[conn.structureIdx]
52
+ if (!structure) {
53
+ continue
54
+ }
55
+
56
+ // Clear highlight if mouse left MSA
57
+ if (mouseCol === undefined) {
58
+ structure.clearHighlightFromExternal?.()
59
+ continue
60
+ }
61
+
62
+ const seq = self.getSequenceByRowName(conn.msaRowName)
63
+ if (!seq) {
64
+ continue
65
+ }
66
+
67
+ // Convert gapped MSA column to ungapped position
68
+ const msaUngapped = gappedToUngappedPosition(seq, mouseCol)
69
+ if (msaUngapped === undefined) {
70
+ structure.clearHighlightFromExternal?.()
71
+ continue
72
+ }
73
+
74
+ // Map to structure position and highlight
75
+ const structurePos = conn.msaToStructure[msaUngapped]
76
+ if (structurePos === undefined) {
77
+ structure.clearHighlightFromExternal?.()
78
+ } else {
79
+ structure.highlightFromExternal?.(structurePos)
80
+ }
81
+ }
82
+ }
83
+
20
84
  export interface IRegion {
21
85
  refName: string
22
86
  start: number
@@ -28,7 +92,7 @@ export interface BlastParams {
28
92
  blastDatabase: string
29
93
  msaAlgorithm: string
30
94
  blastProgram: string
31
- selectedTranscript: Feature
95
+ selectedTranscript?: Feature
32
96
  proteinSequence: string
33
97
  }
34
98
 
@@ -70,10 +134,48 @@ export default function stateModelFactory() {
70
134
  */
71
135
  querySeqName: 'QUERY',
72
136
 
137
+ /**
138
+ * #property
139
+ * UniProt ID extracted from AlphaFold MSA URL
140
+ */
141
+ uniprotId: types.maybe(types.string),
142
+
73
143
  /**
74
144
  * #property
75
145
  */
76
146
  zoomToBaseLevel: false,
147
+
148
+ /**
149
+ * #property
150
+ * used for loading the MSA view via session snapshots, e.g.
151
+ * {
152
+ * "type": "MsaView",
153
+ * "init": {
154
+ * "msaUrl": "https://example.com/alignment.fa",
155
+ * "treeUrl": "https://example.com/tree.nh",
156
+ * "querySeqName": "ENST00000123_hg38"
157
+ * }
158
+ * }
159
+ */
160
+ init: types.frozen<MsaViewInitState | undefined>(),
161
+
162
+ /**
163
+ * #property
164
+ * connections to protein 3D structure views for synchronized highlighting
165
+ */
166
+ connectedStructures: types.array(types.frozen<StructureConnection>()),
167
+
168
+ /**
169
+ * #property
170
+ * Reference ID for MSA data stored in IndexedDB (for large datasets)
171
+ */
172
+ dataStoreId: types.maybe(types.string),
173
+
174
+ /**
175
+ * #property
176
+ * MAF region for coordinate mapping (used when launched from MAF viewer)
177
+ */
178
+ mafRegion: types.frozen<MafRegion | undefined>(),
77
179
  }),
78
180
  )
79
181
 
@@ -90,28 +192,31 @@ export default function stateModelFactory() {
90
192
  * #volatile
91
193
  */
92
194
  error: undefined as unknown,
195
+ /**
196
+ * #volatile
197
+ * True when loading MSA data from IndexedDB
198
+ */
199
+ loadingStoredData: false,
93
200
  }))
94
201
 
95
202
  .views(self => ({
96
203
  /**
97
204
  * #method
205
+ * Get a row by name, returns [name, sequence] or undefined
98
206
  */
99
- ungappedCoordMap(rowName: string, position: number) {
100
- const row = self.rows.find(f => f[0] === rowName)
101
- const seq = row?.[1]
102
- if (seq && position < seq.length) {
103
- let i = 0
104
- let j = 0
105
- for (; j < position; j++, i++) {
106
- while (seq[i] === '-') {
107
- i++
108
- }
109
- }
110
- return i
111
- }
112
- return undefined
207
+ getRowByName(rowName: string) {
208
+ return self.rows.find(r => r[0] === rowName)
209
+ },
210
+
211
+ /**
212
+ * #method
213
+ * Get the sequence for a row by name
214
+ */
215
+ getSequenceByRowName(rowName: string) {
216
+ return self.rows.find(r => r[0] === rowName)?.[1]
113
217
  },
114
218
  }))
219
+
115
220
  .views(self => ({
116
221
  /**
117
222
  * #getter
@@ -121,38 +226,91 @@ export default function stateModelFactory() {
121
226
  ? genomeToTranscriptSeqMapping(self.connectedFeature)
122
227
  : undefined
123
228
  },
124
- }))
125
- .views(self => ({
229
+
126
230
  /**
127
231
  * #getter
128
232
  */
129
- get mouseCol2(): number | undefined {
130
- return genomeToMSA({ model: self as JBrowsePluginMsaViewModel })
233
+ get processing() {
234
+ return !!self.progress
131
235
  },
236
+
132
237
  /**
133
238
  * #getter
134
239
  */
135
- get clickCol2() {
136
- return undefined
240
+ get connectedView() {
241
+ const { views } = getSession(self)
242
+ return views.find(f => f.id === self.connectedViewId) as MaybeLGV
243
+ },
244
+
245
+ /**
246
+ * #getter
247
+ * Get connected protein views with their full model references
248
+ */
249
+ get connectedProteinViews() {
250
+ const { views } = getSession(self)
251
+ return self.connectedStructures
252
+ .map(conn => {
253
+ const proteinView = views.find(
254
+ (v: any) => v.id === conn.proteinViewId,
255
+ ) as any
256
+ return proteinView ? { ...conn, proteinView } : undefined
257
+ })
258
+ .filter((c): c is StructureConnection & { proteinView: any } => !!c)
137
259
  },
138
260
  }))
139
261
 
140
262
  .views(self => ({
141
263
  /**
142
264
  * #getter
265
+ * Get the MSA column that corresponds to the currently hovered structure position
266
+ * Returns the first match from any connected structure
143
267
  */
144
- get processing() {
145
- return !!self.progress
268
+ get structureHoverCol(): number | undefined {
269
+ for (const conn of self.connectedProteinViews) {
270
+ const structure = conn.proteinView?.structures?.[conn.structureIdx]
271
+ const structurePos = structure?.hoverPosition?.structureSeqPos
272
+ if (structurePos !== undefined) {
273
+ const msaUngapped = conn.structureToMsa[structurePos]
274
+ if (msaUngapped !== undefined) {
275
+ const seq = self.getSequenceByRowName(conn.msaRowName)
276
+ if (seq) {
277
+ // Convert ungapped position to global column, then to visible column
278
+ const globalCol = ungappedToGappedPosition(seq, msaUngapped)
279
+ if (globalCol !== undefined) {
280
+ return self.globalColToVisibleCol(globalCol)
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
286
+ return undefined
146
287
  },
288
+ }))
147
289
 
290
+ .views(self => ({
148
291
  /**
149
292
  * #getter
293
+ * Returns a secondary highlight column from either:
294
+ * 1. Structure hover (from connected protein 3D view)
295
+ * 2. Genome hover (from connected linear genome view)
150
296
  */
151
- get connectedView() {
152
- const { views } = getSession(self)
153
- return views.find(f => f.id === self.connectedViewId) as MaybeLGV
297
+ get mouseCol2(): number | undefined {
298
+ // Check structure hover first
299
+ const structureCol = self.structureHoverCol
300
+ if (structureCol !== undefined) {
301
+ return structureCol
302
+ }
303
+ // Fall back to genome hover
304
+ return genomeToMSA({ model: self as JBrowsePluginMsaViewModel })
305
+ },
306
+ /**
307
+ * #getter
308
+ */
309
+ get clickCol2() {
310
+ return undefined
154
311
  },
155
312
  }))
313
+
156
314
  .actions(self => ({
157
315
  /**
158
316
  * #action
@@ -202,7 +360,158 @@ export default function stateModelFactory() {
202
360
  setBlastParams(args?: BlastParams) {
203
361
  self.blastParams = args
204
362
  },
363
+ /**
364
+ * #action
365
+ */
366
+ setInit(arg?: MsaViewInitState) {
367
+ self.init = arg
368
+ },
369
+ /**
370
+ * #action
371
+ */
372
+ setQuerySeqName(arg: string) {
373
+ self.querySeqName = arg
374
+ },
375
+ /**
376
+ * #action
377
+ */
378
+ setUniprotId(arg?: string) {
379
+ self.uniprotId = arg
380
+ },
381
+ /**
382
+ * #action
383
+ */
384
+ setDataStoreId(arg?: string) {
385
+ self.dataStoreId = arg
386
+ },
387
+ /**
388
+ * #action
389
+ */
390
+ setMafRegion(arg?: MafRegion) {
391
+ self.mafRegion = arg
392
+ },
393
+ /**
394
+ * #action
395
+ */
396
+ setLoadingStoredData(arg: boolean) {
397
+ self.loadingStoredData = arg
398
+ },
399
+ /**
400
+ * #action
401
+ */
402
+ handleMsaClick(coord: number) {
403
+ const { connectedView, zoomToBaseLevel } = self
404
+ const { assemblyManager } = getSession(self)
405
+ const r2 = msaCoordToGenomeCoord({ model: self, coord })
406
+
407
+ if (!r2 || !connectedView) {
408
+ return
409
+ }
410
+
411
+ if (zoomToBaseLevel) {
412
+ connectedView.navTo(r2)
413
+ } else {
414
+ const r =
415
+ assemblyManager
416
+ .get(connectedView.assemblyNames[0]!)
417
+ ?.getCanonicalRefName(r2.refName) ?? r2.refName
418
+ connectedView.centerAt(r2.start, r)
419
+ }
420
+ },
421
+
422
+ /**
423
+ * #action
424
+ * Connect to a protein structure for synchronized highlighting
425
+ */
426
+ connectToStructure(
427
+ proteinViewId: string,
428
+ structureIdx: number,
429
+ msaRowName?: string,
430
+ ) {
431
+ const rowName = msaRowName ?? self.querySeqName
432
+ const msaSequence = self.getSequenceByRowName(rowName)
433
+ if (!msaSequence) {
434
+ throw new Error(`MSA row "${rowName}" not found`)
435
+ }
436
+
437
+ const ungappedMsaSequence = msaSequence.replaceAll('-', '')
438
+
439
+ const { views } = getSession(self)
440
+
441
+ const proteinView = views.find(
442
+ (v: any) => v.id === proteinViewId,
443
+ ) as any
444
+ if (!proteinView) {
445
+ throw new Error(`ProteinView "${proteinViewId}" not found`)
446
+ }
447
+
448
+ const structure = proteinView.structures?.[structureIdx]
449
+ if (!structure) {
450
+ throw new Error(`Structure at index ${structureIdx} not found`)
451
+ }
452
+
453
+ const structureSequence = structure.structureSequences?.[0]
454
+ if (!structureSequence) {
455
+ throw new Error('Structure sequence not available')
456
+ }
457
+
458
+ const alignment = runPairwiseAlignment(
459
+ ungappedMsaSequence,
460
+ structureSequence,
461
+ )
462
+ const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment)
463
+
464
+ const connection: StructureConnection = {
465
+ proteinViewId,
466
+ structureIdx,
467
+ msaRowName: rowName,
468
+ msaToStructure: mapToRecord(seq1ToSeq2),
469
+ structureToMsa: mapToRecord(seq2ToSeq1),
470
+ }
471
+
472
+ self.connectedStructures.push(connection)
473
+ },
474
+
475
+ /**
476
+ * #action
477
+ * Disconnect from a protein structure
478
+ */
479
+ disconnectFromStructure(proteinViewId: string, structureIdx: number) {
480
+ const idx = self.connectedStructures.findIndex(
481
+ c =>
482
+ c.proteinViewId === proteinViewId &&
483
+ c.structureIdx === structureIdx,
484
+ )
485
+ if (idx !== -1) {
486
+ self.connectedStructures.splice(idx, 1)
487
+ }
488
+ },
489
+
490
+ /**
491
+ * #action
492
+ * Disconnect from all protein structures
493
+ */
494
+ disconnectAllStructures() {
495
+ self.connectedStructures.clear()
496
+ },
205
497
  }))
498
+ .actions(self => {
499
+ // store reference to the original action from react-msaview
500
+ const superSetMouseClickPos = self.setMouseClickPos.bind(self)
501
+
502
+ return {
503
+ /**
504
+ * #action
505
+ * overrides base setMouseClickPos to trigger navigation
506
+ */
507
+ setMouseClickPos(col?: number, row?: number) {
508
+ superSetMouseClickPos(col, row)
509
+ if (col !== undefined) {
510
+ self.handleMsaClick(col)
511
+ }
512
+ },
513
+ }
514
+ })
206
515
 
207
516
  .views(self => ({
208
517
  /**
@@ -219,26 +528,106 @@ export default function stateModelFactory() {
219
528
  self.setZoomToBaseLevel(!self.zoomToBaseLevel)
220
529
  },
221
530
  },
531
+ {
532
+ label: 'Connect to protein structure...',
533
+ onClick: () => {
534
+ getSession(self).queueDialog(handleClose => [
535
+ ConnectStructureDialog,
536
+ {
537
+ model: self,
538
+ handleClose,
539
+ },
540
+ ])
541
+ },
542
+ },
543
+ ...(self.connectedStructures.length > 0
544
+ ? [
545
+ {
546
+ label: 'Disconnect from protein structures',
547
+ onClick: () => {
548
+ self.disconnectAllStructures()
549
+ },
550
+ },
551
+ ]
552
+ : []),
222
553
  ]
223
554
  },
224
- /**
225
- * #getter
226
- */
227
- get processing() {
228
- return !!self.progress
229
- },
230
-
231
- /**
232
- * #getter
233
- */
234
- get connectedView() {
235
- const { views } = getSession(self)
236
- return views.find(f => f.id === self.connectedViewId) as MaybeLGV
237
- },
238
555
  }))
239
556
 
240
557
  .actions(self => ({
241
558
  afterCreate() {
559
+ // Clean up old IndexedDB entries on startup
560
+ cleanupOldData().catch((e: unknown) => {
561
+ console.error('Failed to cleanup old MSA data:', e)
562
+ })
563
+
564
+ // Load MSA data from IndexedDB if dataStoreId exists and no data loaded
565
+ addDisposer(
566
+ self,
567
+ autorun(async () => {
568
+ const { dataStoreId, rows } = self
569
+ if (dataStoreId && rows.length === 0) {
570
+ try {
571
+ self.setLoadingStoredData(true)
572
+ const storedData = await retrieveMsaData(dataStoreId)
573
+ if (storedData) {
574
+ if (storedData.msa) {
575
+ self.setMSA(storedData.msa)
576
+ }
577
+ if (storedData.tree) {
578
+ self.setTree(storedData.tree)
579
+ }
580
+ }
581
+ } catch (e) {
582
+ console.error('Failed to load MSA data from IndexedDB:', e)
583
+ } finally {
584
+ self.setLoadingStoredData(false)
585
+ }
586
+ }
587
+ }),
588
+ )
589
+
590
+ // Store MSA data to IndexedDB when loaded from inline data (no filehandle)
591
+ // This ensures data persists across page refreshes even when
592
+ // react-msaview's postProcessSnapshot would strip it
593
+ addDisposer(
594
+ self,
595
+ autorun(async () => {
596
+ const { rows, dataStoreId } = self
597
+ // Only store if we have data and don't already have a dataStoreId
598
+ if (rows.length > 0 && !dataStoreId) {
599
+ // Only store if there's no filehandle (filehandles can reload from source)
600
+ const hasFilehandle = !!(
601
+ self.msaFilehandle ?? self.treeFilehandle
602
+ )
603
+ if (hasFilehandle) {
604
+ return
605
+ }
606
+
607
+ const msaData = self.data.msa
608
+ const treeData = self.data.tree
609
+
610
+ // Only store if we have actual data
611
+ if (msaData || treeData) {
612
+ try {
613
+ const newId = generateDataStoreId()
614
+ const success = await storeMsaData(newId, {
615
+ msa: msaData,
616
+ tree: treeData,
617
+ treeMetadata: self.data.treeMetadata,
618
+ })
619
+ // Only set dataStoreId if storage was successful
620
+ if (success) {
621
+ self.setDataStoreId(newId)
622
+ }
623
+ } catch (e) {
624
+ console.error('Failed to store MSA data to IndexedDB:', e)
625
+ }
626
+ }
627
+ }
628
+ }),
629
+ )
630
+
242
631
  addDisposer(
243
632
  self,
244
633
  autorun(async () => {
@@ -261,6 +650,63 @@ export default function stateModelFactory() {
261
650
  }),
262
651
  )
263
652
 
653
+ // process init parameter for loading MSA from session snapshots
654
+ addDisposer(
655
+ self,
656
+ autorun(async () => {
657
+ const { init } = self
658
+ if (init) {
659
+ try {
660
+ self.setError(undefined)
661
+ const { msaData, msaUrl, treeData, treeUrl, querySeqName } =
662
+ init
663
+
664
+ // Extract uniprotId from AlphaFold MSA URL and set querySeqName
665
+ if (msaUrl) {
666
+ const id = getUniprotIdFromAlphaFoldUrl(msaUrl)
667
+ if (id) {
668
+ self.setUniprotId(id)
669
+ // AlphaFold MSA files use 'query' as the row name
670
+ self.setQuerySeqName('query')
671
+ }
672
+ }
673
+
674
+ // User-provided querySeqName takes precedence
675
+ if (querySeqName) {
676
+ self.setQuerySeqName(querySeqName)
677
+ }
678
+
679
+ if (msaData) {
680
+ self.setMSA(msaData)
681
+ } else if (msaUrl) {
682
+ const response = await fetch(msaUrl)
683
+ if (!response.ok) {
684
+ throw new Error(`Failed to fetch MSA: ${response.status}`)
685
+ }
686
+ const data = await response.text()
687
+ self.setMSA(data)
688
+ }
689
+
690
+ if (treeData) {
691
+ self.setTree(treeData)
692
+ } else if (treeUrl) {
693
+ const response = await fetch(treeUrl)
694
+ if (!response.ok) {
695
+ throw new Error(`Failed to fetch tree: ${response.status}`)
696
+ }
697
+ const data = await response.text()
698
+ self.setTree(data)
699
+ }
700
+
701
+ self.setInit(undefined)
702
+ } catch (e) {
703
+ self.setError(e)
704
+ console.error(e)
705
+ }
706
+ }
707
+ }),
708
+ )
709
+
264
710
  // this adds highlights to the genome view when mouse-ing over the MSA
265
711
  addDisposer(
266
712
  self,
@@ -274,36 +720,147 @@ export default function stateModelFactory() {
274
720
  mouseClickCol === undefined
275
721
  ? undefined
276
722
  : msaCoordToGenomeCoord({ model: self, coord: mouseClickCol })
723
+
277
724
  self.setConnectedHighlights([r1, r2].filter(f => !!f))
278
725
  }),
279
726
  )
280
727
 
281
- // nav to genome position after click
728
+ // this highlights residues in connected protein structures when mousing over the MSA
282
729
  addDisposer(
283
730
  self,
284
731
  autorun(() => {
285
- const { connectedView, zoomToBaseLevel, mouseClickCol } = self
286
- const { assemblyManager } = getSession(self)
287
- const r2 =
288
- mouseClickCol === undefined
289
- ? undefined
290
- : msaCoordToGenomeCoord({ model: self, coord: mouseClickCol })
732
+ highlightConnectedStructures(self)
733
+ }),
734
+ )
735
+
736
+ // auto-connect to compatible ProteinViews
737
+ addDisposer(
738
+ self,
739
+ autorun(() => {
740
+ const { views } = getSession(self)
741
+ const { connectedViewId, uniprotId, rows, connectedStructures } =
742
+ self
291
743
 
292
- if (!r2 || !connectedView) {
744
+ // Need MSA loaded and a uniprotId to auto-connect
745
+ if (!uniprotId || rows.length === 0) {
293
746
  return
294
747
  }
295
748
 
296
- if (zoomToBaseLevel) {
297
- connectedView.navTo(r2)
298
- } else {
299
- const r =
300
- assemblyManager
301
- .get(connectedView.assemblyNames[0]!)
302
- ?.getCanonicalRefName(r2.refName) ?? r2.refName
303
- connectedView.centerAt(r2.start, r)
749
+ // Find ProteinViews that share the same connectedViewId
750
+ for (const view of views) {
751
+ const v = view as any
752
+ if (v.type !== 'ProteinView' || !v.structures) {
753
+ continue
754
+ }
755
+
756
+ for (
757
+ let structureIdx = 0;
758
+ structureIdx < v.structures.length;
759
+ structureIdx++
760
+ ) {
761
+ const structure = v.structures[structureIdx]
762
+
763
+ // Check if structure shares the same genome view connection
764
+ if (structure.connectedViewId !== connectedViewId) {
765
+ continue
766
+ }
767
+
768
+ // Check if structure has matching uniprotId
769
+ if (structure.uniprotId !== uniprotId) {
770
+ continue
771
+ }
772
+
773
+ // Check if already connected
774
+ const alreadyConnected = connectedStructures.some(
775
+ c =>
776
+ c.proteinViewId === v.id && c.structureIdx === structureIdx,
777
+ )
778
+ if (alreadyConnected) {
779
+ continue
780
+ }
781
+
782
+ // Check if structure sequence is available
783
+ if (!structure.structureSequences?.[0]) {
784
+ continue
785
+ }
786
+
787
+ // Auto-connect
788
+ try {
789
+ self.connectToStructure(v.id, structureIdx)
790
+ } catch (e) {
791
+ console.error('Failed to auto-connect to ProteinView:', e)
792
+ }
793
+ }
304
794
  }
305
795
  }),
306
796
  )
797
+
798
+ // Observe protein3d genome highlights and update MSA highlighted columns
799
+ // This enables communication via the linear genome view coordinates
800
+ addDisposer(
801
+ self,
802
+ autorun(() => {
803
+ const { views } = getSession(self)
804
+ const { connectedViewId, transcriptToMsaMap, querySeqName } = self
805
+
806
+ if (!connectedViewId || !transcriptToMsaMap) {
807
+ return
808
+ }
809
+
810
+ const columns: number[] = []
811
+
812
+ // Find ProteinViews that share the same connected genome view
813
+ for (const view of views) {
814
+ const v = view as any
815
+ if (v.type !== 'ProteinView' || !v.structures) {
816
+ continue
817
+ }
818
+
819
+ for (const structure of v.structures) {
820
+ // Check if structure is connected to same genome view
821
+ if (structure.connectedViewId !== connectedViewId) {
822
+ continue
823
+ }
824
+
825
+ // Check if structure has hover genome highlights
826
+ const highlights = structure.hoverGenomeHighlights
827
+ if (!highlights || highlights.length === 0) {
828
+ continue
829
+ }
830
+
831
+ // Map genome coordinates to MSA columns
832
+ const { g2p } = transcriptToMsaMap
833
+ for (const highlight of highlights) {
834
+ for (
835
+ let coord = highlight.start;
836
+ coord < highlight.end;
837
+ coord++
838
+ ) {
839
+ const proteinPos = g2p[coord]
840
+ if (proteinPos !== undefined) {
841
+ const col = self.seqPosToGlobalCol(
842
+ querySeqName,
843
+ proteinPos,
844
+ )
845
+ if (!columns.includes(col)) {
846
+ columns.push(col)
847
+ }
848
+ }
849
+ }
850
+ }
851
+ }
852
+ }
853
+
854
+ // Convert global column indices to visible column indices
855
+ const visibleColumns = columns
856
+ .map(col => self.globalColToVisibleCol(col))
857
+ .filter((col): col is number => col !== undefined)
858
+
859
+ self.setHighlightedColumns(
860
+ visibleColumns.length > 0 ? visibleColumns : undefined,
861
+ )
862
+ }),
863
+ )
307
864
  },
308
865
  }))
309
866
  }
@@ -312,3 +869,5 @@ export type JBrowsePluginMsaViewStateModel = ReturnType<
312
869
  typeof stateModelFactory
313
870
  >
314
871
  export type JBrowsePluginMsaViewModel = Instance<JBrowsePluginMsaViewStateModel>
872
+
873
+ export { type MafRegion, type MsaViewInitState } from './types'