jbrowse-plugin-msaview 2.2.3 → 2.2.5

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 +719 -226
  70. package/dist/MsaViewPanel/model.js +467 -39
  71. package/dist/MsaViewPanel/model.js.map +1 -1
  72. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +7 -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 +590 -43
  144. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +256 -0
  145. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +43 -29
  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 -55381
  162. package/src/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.ts +0 -25
@@ -0,0 +1,256 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
4
+
5
+ describe('msaCoordToGenomeCoord', () => {
6
+ test('returns undefined when neither transcriptToMsaMap nor mafRegion is defined', () => {
7
+ const model = {
8
+ querySeqName: 'QUERY',
9
+ transcriptToMsaMap: undefined,
10
+ mafRegion: undefined,
11
+ rows: [['QUERY', 'MKAA']],
12
+ }
13
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
14
+ expect(result).toBeUndefined()
15
+ })
16
+
17
+ test('returns undefined when query row is not found', () => {
18
+ const model = {
19
+ querySeqName: 'QUERY',
20
+ transcriptToMsaMap: {
21
+ refName: 'chr1',
22
+ p2g: { 0: 100, 1: 103 },
23
+ },
24
+ rows: [['OTHER', 'MKAA']],
25
+ }
26
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
27
+ expect(result).toBeUndefined()
28
+ })
29
+
30
+ test('returns undefined when coord is a gap', () => {
31
+ const model = {
32
+ querySeqName: 'QUERY',
33
+ transcriptToMsaMap: {
34
+ refName: 'chr1',
35
+ p2g: { 0: 100, 1: 103 },
36
+ },
37
+ rows: [['QUERY', 'M-KA']],
38
+ }
39
+ // Position 1 is a gap
40
+ const result = msaCoordToGenomeCoord({ model, coord: 1 })
41
+ expect(result).toBeUndefined()
42
+ })
43
+
44
+ test('returns genome region for valid non-gap position', () => {
45
+ const model = {
46
+ querySeqName: 'QUERY',
47
+ transcriptToMsaMap: {
48
+ refName: 'chr1',
49
+ p2g: { 0: 100, 1: 103, 2: 106, 3: 109 },
50
+ },
51
+ rows: [['QUERY', 'MKAA']],
52
+ }
53
+ // Position 0 (M) should map to ungapped 0, genome 100-103
54
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
55
+ expect(result).toEqual({
56
+ refName: 'chr1',
57
+ start: 100,
58
+ end: 103,
59
+ })
60
+ })
61
+
62
+ test('handles gapped sequence correctly', () => {
63
+ const model = {
64
+ querySeqName: 'QUERY',
65
+ transcriptToMsaMap: {
66
+ refName: 'chr1',
67
+ p2g: { 0: 100, 1: 103, 2: 106, 3: 109 },
68
+ },
69
+ rows: [['QUERY', 'M-K-AA']],
70
+ // 012345 gapped positions
71
+ // 0 1 23 ungapped positions
72
+ }
73
+ // Gapped position 2 (K) = ungapped 1
74
+ const result = msaCoordToGenomeCoord({ model, coord: 2 })
75
+ expect(result).toEqual({
76
+ refName: 'chr1',
77
+ start: 103,
78
+ end: 106,
79
+ })
80
+
81
+ // Gapped position 4 (first A) = ungapped 2
82
+ const result2 = msaCoordToGenomeCoord({ model, coord: 4 })
83
+ expect(result2).toEqual({
84
+ refName: 'chr1',
85
+ start: 106,
86
+ end: 109,
87
+ })
88
+ })
89
+
90
+ test('returns undefined when p2g mapping is incomplete', () => {
91
+ const model = {
92
+ querySeqName: 'QUERY',
93
+ transcriptToMsaMap: {
94
+ refName: 'chr1',
95
+ p2g: { 0: 100 }, // Missing entry for position 1
96
+ },
97
+ rows: [['QUERY', 'MKAA']],
98
+ }
99
+ // Position 0 needs p2g[0] and p2g[1], but p2g[1] is missing
100
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
101
+ expect(result).toBeUndefined()
102
+ })
103
+
104
+ test('handles reverse strand (start > end in p2g)', () => {
105
+ const model = {
106
+ querySeqName: 'QUERY',
107
+ transcriptToMsaMap: {
108
+ refName: 'chr1',
109
+ p2g: { 0: 109, 1: 106, 2: 103, 3: 100 }, // Reverse strand
110
+ },
111
+ rows: [['QUERY', 'MKAA']],
112
+ }
113
+ // Should return min/max correctly
114
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
115
+ expect(result).toEqual({
116
+ refName: 'chr1',
117
+ start: 106, // min(109, 106)
118
+ end: 109, // max(109, 106)
119
+ })
120
+ })
121
+
122
+ test('returns undefined for out of bounds coord', () => {
123
+ const model = {
124
+ querySeqName: 'QUERY',
125
+ transcriptToMsaMap: {
126
+ refName: 'chr1',
127
+ p2g: { 0: 100, 1: 103 },
128
+ },
129
+ rows: [['QUERY', 'MK']],
130
+ }
131
+ // Position 10 is out of bounds
132
+ const result = msaCoordToGenomeCoord({ model, coord: 10 })
133
+ expect(result).toBeUndefined()
134
+ })
135
+
136
+ test('works with multiple rows, uses querySeqName', () => {
137
+ const model = {
138
+ querySeqName: 'SEQ2',
139
+ transcriptToMsaMap: {
140
+ refName: 'chr1',
141
+ p2g: { 0: 200, 1: 203 },
142
+ },
143
+ rows: [
144
+ ['SEQ1', 'AAAA'],
145
+ ['SEQ2', 'MKAA'],
146
+ ['SEQ3', 'LLLL'],
147
+ ],
148
+ }
149
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
150
+ expect(result).toEqual({
151
+ refName: 'chr1',
152
+ start: 200,
153
+ end: 203,
154
+ })
155
+ })
156
+
157
+ // MAF region tests
158
+ describe('mafRegion', () => {
159
+ test('returns genome position for mafRegion mapping', () => {
160
+ const model = {
161
+ querySeqName: 'hg38.chr1',
162
+ transcriptToMsaMap: undefined,
163
+ mafRegion: {
164
+ refName: 'chr1',
165
+ start: 1000,
166
+ end: 1010,
167
+ assemblyName: 'hg38',
168
+ },
169
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
170
+ }
171
+ // Position 0 should map to genome 1000
172
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
173
+ expect(result).toEqual({
174
+ refName: 'chr1',
175
+ start: 1000,
176
+ end: 1001,
177
+ })
178
+
179
+ // Position 5 should map to genome 1005
180
+ const result2 = msaCoordToGenomeCoord({ model, coord: 5 })
181
+ expect(result2).toEqual({
182
+ refName: 'chr1',
183
+ start: 1005,
184
+ end: 1006,
185
+ })
186
+ })
187
+
188
+ test('handles gaps in mafRegion sequence', () => {
189
+ const model = {
190
+ querySeqName: 'hg38.chr1',
191
+ transcriptToMsaMap: undefined,
192
+ mafRegion: {
193
+ refName: 'chr1',
194
+ start: 1000,
195
+ end: 1008,
196
+ assemblyName: 'hg38',
197
+ },
198
+ rows: [['hg38.chr1', 'AC--GTAC']],
199
+ // Gapped positions: 0 1 2 3 4 5 6 7
200
+ // Ungapped: 0 1 2 3 4 5
201
+ }
202
+ // Position 2 is a gap, should return undefined
203
+ const result = msaCoordToGenomeCoord({ model, coord: 2 })
204
+ expect(result).toBeUndefined()
205
+
206
+ // Position 4 (G) = ungapped 2 = genome 1002
207
+ const result2 = msaCoordToGenomeCoord({ model, coord: 4 })
208
+ expect(result2).toEqual({
209
+ refName: 'chr1',
210
+ start: 1002,
211
+ end: 1003,
212
+ })
213
+ })
214
+
215
+ test('returns undefined when position exceeds mafRegion end', () => {
216
+ const model = {
217
+ querySeqName: 'hg38.chr1',
218
+ transcriptToMsaMap: undefined,
219
+ mafRegion: {
220
+ refName: 'chr1',
221
+ start: 1000,
222
+ end: 1005,
223
+ assemblyName: 'hg38',
224
+ },
225
+ rows: [['hg38.chr1', 'ACGTACGTAC']], // 10 chars but region is only 5bp
226
+ }
227
+ // Position 8 would be ungapped 8 = genome 1008, but region ends at 1005
228
+ const result = msaCoordToGenomeCoord({ model, coord: 8 })
229
+ expect(result).toBeUndefined()
230
+ })
231
+
232
+ test('mafRegion takes precedence over transcriptToMsaMap', () => {
233
+ const model = {
234
+ querySeqName: 'hg38.chr1',
235
+ transcriptToMsaMap: {
236
+ refName: 'chr2',
237
+ p2g: { 0: 5000, 1: 5003 },
238
+ },
239
+ mafRegion: {
240
+ refName: 'chr1',
241
+ start: 1000,
242
+ end: 1010,
243
+ assemblyName: 'hg38',
244
+ },
245
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
246
+ }
247
+ // Should use mafRegion, not transcriptToMsaMap
248
+ const result = msaCoordToGenomeCoord({ model, coord: 0 })
249
+ expect(result).toEqual({
250
+ refName: 'chr1',
251
+ start: 1000,
252
+ end: 1001,
253
+ })
254
+ })
255
+ })
256
+ })
@@ -1,44 +1,56 @@
1
- /**
2
- * Convert gapped MSA column coordinate to ungapped sequence coordinate
3
- * This is the inverse of ungappedCoordMap
4
- */
5
- function gappedToUngappedCoord(seq: string, gappedPos: number): number {
6
- let ungappedPos = 0
7
- for (let i = 0; i < gappedPos && i < seq.length; i++) {
8
- if (seq[i] !== '-') {
9
- ungappedPos++
10
- }
11
- }
12
- return ungappedPos
13
- }
1
+ import { gappedToUngappedPosition } from './structureConnection'
2
+
3
+ import type { MafRegion } from './types'
14
4
 
15
5
  export function msaCoordToGenomeCoord({
16
6
  model,
17
7
  coord: mouseCol,
18
8
  }: {
19
- model: { querySeqName: string; transcriptToMsaMap: any; rows: string[][] }
9
+ model: {
10
+ querySeqName: string
11
+ transcriptToMsaMap:
12
+ | {
13
+ refName: string
14
+ p2g: Record<number, number>
15
+ }
16
+ | undefined
17
+ mafRegion?: MafRegion
18
+ rows: string[][]
19
+ }
20
20
  coord: number
21
21
  }) {
22
- const { querySeqName, transcriptToMsaMap } = model
23
- if (transcriptToMsaMap === undefined) {
22
+ const { querySeqName, transcriptToMsaMap, mafRegion } = model
23
+
24
+ // Get the query sequence
25
+ const queryRow = model.rows.find(f => f[0] === querySeqName)
26
+ const querySeq = queryRow?.[1]
27
+ if (!querySeq) {
24
28
  return undefined
25
- } else {
26
- // Get the query sequence
27
- const queryRow = model.rows.find(f => f[0] === querySeqName)
28
- const querySeq = queryRow?.[1]
29
- if (!querySeq) {
30
- return undefined
31
- }
29
+ }
30
+
31
+ // Convert gapped MSA column to ungapped sequence coordinate
32
+ // Returns undefined if the position is a gap
33
+ const ungappedPos = gappedToUngappedPosition(querySeq, mouseCol)
34
+ if (ungappedPos === undefined) {
35
+ return undefined
36
+ }
32
37
 
33
- // Check if the position in the query sequence is a gap
34
- if (querySeq[mouseCol] === '-') {
38
+ // Handle MAF region mapping
39
+ if (mafRegion) {
40
+ const genomePos = mafRegion.start + ungappedPos
41
+ // Check if position is within the region
42
+ if (genomePos >= mafRegion.end) {
35
43
  return undefined
36
44
  }
45
+ return {
46
+ refName: mafRegion.refName,
47
+ start: genomePos,
48
+ end: genomePos + 1,
49
+ }
50
+ }
37
51
 
38
- // Convert gapped MSA column to ungapped sequence coordinate
39
- const ungappedPos = gappedToUngappedCoord(querySeq, mouseCol)
40
-
41
- // Use the ungapped position to look up in the p2g map
52
+ // Handle transcript mapping (original behavior)
53
+ if (transcriptToMsaMap) {
42
54
  const { refName, p2g } = transcriptToMsaMap
43
55
  const s = p2g[ungappedPos]
44
56
  const e = p2g[ungappedPos + 1]
@@ -50,4 +62,6 @@ export function msaCoordToGenomeCoord({
50
62
  }
51
63
  : undefined
52
64
  }
65
+
66
+ return undefined
53
67
  }
@@ -0,0 +1,236 @@
1
+ const DB_NAME = 'jbrowse-msaview-data'
2
+ const DB_VERSION = 1
3
+ const STORE_NAME = 'msa-data'
4
+
5
+ interface StoredMsaData {
6
+ id: string
7
+ msa?: string
8
+ tree?: string
9
+ treeMetadata?: string
10
+ timestamp: number
11
+ }
12
+
13
+ let dbPromise: Promise<IDBDatabase | undefined> | undefined
14
+ let indexedDBAvailable: boolean | undefined
15
+
16
+ function checkIndexedDBAvailable(): boolean {
17
+ if (indexedDBAvailable !== undefined) {
18
+ return indexedDBAvailable
19
+ }
20
+
21
+ try {
22
+ // Check if indexedDB exists and is accessible
23
+ if (typeof indexedDB === 'undefined') {
24
+ indexedDBAvailable = false
25
+ return false
26
+ }
27
+
28
+ // Try to open a test database to verify IndexedDB is working
29
+ // This can fail in private browsing mode in some browsers
30
+ indexedDBAvailable = true
31
+ return true
32
+ } catch {
33
+ indexedDBAvailable = false
34
+ return false
35
+ }
36
+ }
37
+
38
+ async function openDB(): Promise<IDBDatabase | undefined> {
39
+ if (!checkIndexedDBAvailable()) {
40
+ return undefined
41
+ }
42
+
43
+ if (dbPromise) {
44
+ return dbPromise
45
+ }
46
+
47
+ dbPromise = new Promise(resolve => {
48
+ try {
49
+ const request = indexedDB.open(DB_NAME, DB_VERSION)
50
+
51
+ request.addEventListener('error', () => {
52
+ // IndexedDB may be blocked in private browsing mode
53
+ console.warn(
54
+ 'IndexedDB unavailable - MSA data will not persist across page refreshes',
55
+ )
56
+ indexedDBAvailable = false
57
+ resolve(undefined)
58
+ })
59
+
60
+ request.onsuccess = () => {
61
+ resolve(request.result)
62
+ }
63
+
64
+ request.onupgradeneeded = event => {
65
+ const db = (event.target as IDBOpenDBRequest).result
66
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
67
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
68
+ store.createIndex('timestamp', 'timestamp', { unique: false })
69
+ }
70
+ }
71
+ } catch (e) {
72
+ console.warn('Failed to open IndexedDB:', e)
73
+ indexedDBAvailable = false
74
+ resolve(undefined)
75
+ }
76
+ })
77
+
78
+ return dbPromise
79
+ }
80
+
81
+ export function generateDataStoreId() {
82
+ return `msa-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
83
+ }
84
+
85
+ export async function storeMsaData(
86
+ id: string,
87
+ data: { msa?: string; tree?: string; treeMetadata?: string },
88
+ ): Promise<boolean> {
89
+ const db = await openDB()
90
+ if (!db) {
91
+ // IndexedDB not available, silently skip storage
92
+ return false
93
+ }
94
+
95
+ return new Promise<boolean>(resolve => {
96
+ try {
97
+ const transaction = db.transaction(STORE_NAME, 'readwrite')
98
+ const store = transaction.objectStore(STORE_NAME)
99
+
100
+ const storedData: StoredMsaData = {
101
+ id,
102
+ msa: data.msa,
103
+ tree: data.tree,
104
+ treeMetadata: data.treeMetadata,
105
+ timestamp: Date.now(),
106
+ }
107
+
108
+ const request = store.put(storedData)
109
+
110
+ request.addEventListener('error', () => {
111
+ // Log but don't fail - storage is best-effort
112
+ console.warn('Failed to store MSA data:', request.error)
113
+ resolve(false)
114
+ })
115
+
116
+ request.onsuccess = () => {
117
+ resolve(true)
118
+ }
119
+ } catch (e) {
120
+ console.warn('Failed to store MSA data:', e)
121
+ resolve(false)
122
+ }
123
+ })
124
+ }
125
+
126
+ export async function retrieveMsaData(
127
+ id: string,
128
+ ): Promise<{ msa?: string; tree?: string; treeMetadata?: string } | undefined> {
129
+ const db = await openDB()
130
+ if (!db) {
131
+ return undefined
132
+ }
133
+
134
+ return new Promise(resolve => {
135
+ try {
136
+ const transaction = db.transaction(STORE_NAME, 'readonly')
137
+ const store = transaction.objectStore(STORE_NAME)
138
+ const request = store.get(id)
139
+
140
+ request.addEventListener('error', () => {
141
+ console.warn('Failed to retrieve MSA data:', request.error)
142
+ resolve(undefined)
143
+ })
144
+
145
+ request.onsuccess = () => {
146
+ const result = request.result as StoredMsaData | undefined
147
+ if (result) {
148
+ resolve({
149
+ msa: result.msa,
150
+ tree: result.tree,
151
+ treeMetadata: result.treeMetadata,
152
+ })
153
+ } else {
154
+ resolve(undefined)
155
+ }
156
+ }
157
+ } catch (e) {
158
+ console.warn('Failed to retrieve MSA data:', e)
159
+ resolve(undefined)
160
+ }
161
+ })
162
+ }
163
+
164
+ export async function deleteMsaData(id: string) {
165
+ const db = await openDB()
166
+ if (!db) {
167
+ return
168
+ }
169
+
170
+ return new Promise<void>(resolve => {
171
+ try {
172
+ const transaction = db.transaction(STORE_NAME, 'readwrite')
173
+ const store = transaction.objectStore(STORE_NAME)
174
+ const request = store.delete(id)
175
+
176
+ request.addEventListener('error', () => {
177
+ console.warn('Failed to delete MSA data:', request.error)
178
+ resolve()
179
+ })
180
+
181
+ request.onsuccess = () => {
182
+ resolve()
183
+ }
184
+ } catch (e) {
185
+ console.warn('Failed to delete MSA data:', e)
186
+ resolve()
187
+ }
188
+ })
189
+ }
190
+
191
+ // Clean up entries older than the specified age (default 7 days)
192
+ export async function cleanupOldData(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
193
+ const db = await openDB()
194
+ if (!db) {
195
+ return 0
196
+ }
197
+
198
+ const cutoffTime = Date.now() - maxAgeMs
199
+
200
+ return new Promise<number>(resolve => {
201
+ try {
202
+ const transaction = db.transaction(STORE_NAME, 'readwrite')
203
+ const store = transaction.objectStore(STORE_NAME)
204
+ const index = store.index('timestamp')
205
+ const range = IDBKeyRange.upperBound(cutoffTime)
206
+ const request = index.openCursor(range)
207
+
208
+ let deletedCount = 0
209
+
210
+ request.addEventListener('error', () => {
211
+ console.warn('Failed to cleanup old MSA data:', request.error)
212
+ resolve(deletedCount)
213
+ })
214
+
215
+ request.onsuccess = event => {
216
+ const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>)
217
+ .result
218
+ if (cursor) {
219
+ cursor.delete()
220
+ deletedCount++
221
+ cursor.continue()
222
+ } else {
223
+ resolve(deletedCount)
224
+ }
225
+ }
226
+ } catch (e) {
227
+ console.warn('Failed to cleanup old MSA data:', e)
228
+ resolve(0)
229
+ }
230
+ })
231
+ }
232
+
233
+ // Check if IndexedDB storage is available
234
+ export function isIndexedDBAvailable() {
235
+ return checkIndexedDBAvailable()
236
+ }