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.
- package/CHANGELOG.md +1 -1
- package/README.md +229 -0
- package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +23 -18
- package/dist/AddHighlightModel/GenomeMouseoverHighlight.js.map +1 -1
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +23 -13
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js.map +1 -1
- package/dist/AddHighlightModel/index.js +8 -1
- package/dist/AddHighlightModel/index.js.map +1 -1
- package/dist/AddHighlightModel/util.d.ts +2 -2
- package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +2 -2
- package/dist/LaunchMsaView/components/EnsemblGeneTree/EnsemblGeneTree.js +5 -11
- package/dist/LaunchMsaView/components/EnsemblGeneTree/EnsemblGeneTree.js.map +1 -1
- package/dist/LaunchMsaView/components/EnsemblGeneTree/useGeneTree.js +5 -1
- package/dist/LaunchMsaView/components/EnsemblGeneTree/useGeneTree.js.map +1 -1
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +16 -16
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js.map +1 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +38 -46
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js.map +1 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +4 -3
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +4 -3
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js.map +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.d.ts +9 -0
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +76 -0
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js.map +1 -0
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.js +35 -13
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.js.map +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastManualPanel.js +6 -12
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastManualPanel.js.map +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.d.ts +6 -0
- package/dist/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.js +15 -0
- package/dist/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.js.map +1 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +12 -34
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js.map +1 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/consts.d.ts +1 -0
- package/dist/LaunchMsaView/components/PreLoadedMSA/consts.js +1 -0
- package/dist/LaunchMsaView/components/PreLoadedMSA/consts.js.map +1 -1
- package/dist/LaunchMsaView/components/TabPanel.d.ts +2 -2
- package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +2 -2
- package/dist/LaunchMsaView/components/TranscriptSelector.js +3 -6
- package/dist/LaunchMsaView/components/TranscriptSelector.js.map +1 -1
- package/dist/LaunchMsaView/components/useSWRFeatureSequence.js +6 -4
- package/dist/LaunchMsaView/components/useSWRFeatureSequence.js.map +1 -1
- package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +16 -0
- package/dist/LaunchMsaView/components/useTranscriptSelection.js +31 -0
- package/dist/LaunchMsaView/components/useTranscriptSelection.js.map +1 -0
- package/dist/LaunchMsaView/components/util.d.ts +3 -1
- package/dist/LaunchMsaView/components/util.js +12 -2
- package/dist/LaunchMsaView/components/util.js.map +1 -1
- package/dist/LaunchMsaView/util.d.ts +2 -0
- package/dist/LaunchMsaView/util.js +16 -4
- package/dist/LaunchMsaView/util.js.map +1 -1
- package/dist/LaunchMsaViewExtensionPoint/index.d.ts +2 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +31 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js.map +1 -0
- package/dist/MsaViewPanel/components/ConnectStructureDialog.d.ts +7 -0
- package/dist/MsaViewPanel/components/ConnectStructureDialog.js +56 -0
- package/dist/MsaViewPanel/components/ConnectStructureDialog.js.map +1 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.js +4 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.js.map +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +65 -19
- package/dist/MsaViewPanel/doLaunchBlast.js.map +1 -1
- package/dist/MsaViewPanel/genomeToMSA.d.ts +6 -0
- package/dist/MsaViewPanel/genomeToMSA.js +38 -8
- package/dist/MsaViewPanel/genomeToMSA.js.map +1 -1
- package/dist/MsaViewPanel/genomeToMSA.test.d.ts +1 -0
- package/dist/MsaViewPanel/genomeToMSA.test.js +244 -0
- package/dist/MsaViewPanel/genomeToMSA.test.js.map +1 -0
- package/dist/MsaViewPanel/model.d.ts +719 -226
- package/dist/MsaViewPanel/model.js +467 -39
- package/dist/MsaViewPanel/model.js.map +1 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +7 -2
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +26 -27
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js.map +1 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +240 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js.map +1 -0
- package/dist/MsaViewPanel/msaDataStore.d.ts +14 -0
- package/dist/MsaViewPanel/msaDataStore.js +197 -0
- package/dist/MsaViewPanel/msaDataStore.js.map +1 -0
- package/dist/MsaViewPanel/pairwiseAlignment.d.ts +27 -0
- package/dist/MsaViewPanel/pairwiseAlignment.js +776 -0
- package/dist/MsaViewPanel/pairwiseAlignment.js.map +1 -0
- package/dist/MsaViewPanel/pairwiseAlignment.test.d.ts +1 -0
- package/dist/MsaViewPanel/pairwiseAlignment.test.js +112 -0
- package/dist/MsaViewPanel/pairwiseAlignment.test.js.map +1 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +27 -0
- package/dist/MsaViewPanel/structureConnection.js +46 -0
- package/dist/MsaViewPanel/structureConnection.js.map +1 -0
- package/dist/MsaViewPanel/structureConnection.test.d.ts +1 -0
- package/dist/MsaViewPanel/structureConnection.test.js +122 -0
- package/dist/MsaViewPanel/structureConnection.test.js.map +1 -0
- package/dist/MsaViewPanel/types.d.ts +13 -0
- package/dist/MsaViewPanel/types.js +2 -0
- package/dist/MsaViewPanel/types.js.map +1 -0
- package/dist/MsaViewPanel/util.d.ts +7 -0
- package/dist/MsaViewPanel/util.js +10 -0
- package/dist/MsaViewPanel/util.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +39 -90
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +34 -0
- package/dist/utils/blastCache.js +58 -0
- package/dist/utils/blastCache.js.map +1 -0
- package/dist/utils/fetch.d.ts +1 -1
- package/dist/utils/fetch.js +1 -1
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/ncbiBlast.d.ts +1 -5
- package/dist/utils/taxonomyNames.d.ts +5 -0
- package/dist/utils/taxonomyNames.js +79 -0
- package/dist/utils/taxonomyNames.js.map +1 -0
- package/dist/utils/types.d.ts +8 -5
- package/package.json +50 -54
- package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +37 -21
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +38 -17
- package/src/AddHighlightModel/index.tsx +9 -4
- package/src/LaunchMsaView/components/EnsemblGeneTree/EnsemblGeneTree.tsx +13 -13
- package/src/LaunchMsaView/components/EnsemblGeneTree/useGeneTree.ts +6 -0
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +30 -23
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +64 -51
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +9 -6
- package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +146 -0
- package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx +53 -22
- package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastManualPanel.tsx +8 -13
- package/src/LaunchMsaView/components/NCBIBlastQuery/blastLaunchView.ts +25 -0
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +27 -47
- package/src/LaunchMsaView/components/PreLoadedMSA/consts.ts +1 -0
- package/src/LaunchMsaView/components/TabPanel.tsx +2 -2
- package/src/LaunchMsaView/components/TranscriptSelector.tsx +13 -20
- package/src/LaunchMsaView/components/useSWRFeatureSequence.ts +5 -5
- package/src/LaunchMsaView/components/useTranscriptSelection.ts +48 -0
- package/src/LaunchMsaView/components/util.ts +17 -2
- package/src/LaunchMsaView/index.ts +1 -1
- package/src/LaunchMsaView/util.ts +25 -6
- package/src/LaunchMsaViewExtensionPoint/index.ts +74 -0
- package/src/MsaViewPanel/components/ConnectStructureDialog.tsx +156 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +6 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +83 -23
- package/src/MsaViewPanel/genomeToMSA.test.ts +281 -0
- package/src/MsaViewPanel/genomeToMSA.ts +43 -10
- package/src/MsaViewPanel/model.ts +590 -43
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +256 -0
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +43 -29
- package/src/MsaViewPanel/msaDataStore.ts +236 -0
- package/src/MsaViewPanel/pairwiseAlignment.test.ts +140 -0
- package/src/MsaViewPanel/pairwiseAlignment.ts +818 -0
- package/src/MsaViewPanel/structureConnection.test.ts +143 -0
- package/src/MsaViewPanel/structureConnection.ts +72 -0
- package/src/MsaViewPanel/types.ts +14 -0
- package/src/MsaViewPanel/util.ts +11 -0
- package/src/index.ts +3 -1
- package/src/utils/blastCache.ts +114 -0
- package/src/utils/fetch.ts +1 -1
- package/src/utils/taxonomyNames.ts +111 -0
- package/src/utils/types.ts +9 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.d.ts +0 -5
- package/dist/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.js +0 -16
- package/dist/LaunchMsaView/components/PreLoadedMSA/findValidTranscriptId.js.map +0 -1
- package/dist/out.js +0 -55381
- 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
|
-
|
|
3
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
+
}
|