Skip to content

Commit f9f4052

Browse files
committed
Updates
1 parent f14b1c0 commit f9f4052

18 files changed

Lines changed: 256 additions & 62 deletions

src/LaunchProteinView/components/UserProvidedStructure.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ const UserProvidedStructure = observer(function UserProvidedStructure({
233233
</div>
234234
</DialogContent>
235235
<DialogActions>
236-
{protein?.seq.replaceAll('*', '') !== structureSequence ? (
236+
{protein?.seq && structureSequence && protein.seq.replaceAll('*', '') !== structureSequence ? (
237237
<Typography
238238
variant="body2"
239239
sx={{ mr: 2, display: 'flex', alignItems: 'center' }}

src/LaunchProteinView/services/lookupMethods.ts

Lines changed: 40 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,35 @@ async function searchUniProt(
8787
return data.results.map(mapApiResultToEntry)
8888
}
8989

90+
async function searchByXref(id: string) {
91+
const query = buildXrefQuery(id)
92+
if (query) {
93+
try {
94+
return await searchUniProt(query)
95+
} catch (e) {
96+
console.error(`xref search failed for ${id}:`, e)
97+
}
98+
}
99+
return []
100+
}
101+
102+
function deduplicateEntries(entries: UniProtEntry[]) {
103+
const seen = new Set<string>()
104+
const result: UniProtEntry[] = []
105+
for (const entry of entries) {
106+
if (!seen.has(entry.accession)) {
107+
seen.add(entry.accession)
108+
result.push(entry)
109+
}
110+
}
111+
return result
112+
}
113+
90114
/**
91115
* Search UniProt for entries matching a gene, returning multiple results.
92116
* Tries multiple strategies in order of specificity:
93117
* 1. Recognized database IDs (Ensembl, RefSeq, CCDS, HGNC) via xref search
94-
* 2. Gene name search
118+
* 2. Gene name search (fallback if no reviewed entries found)
95119
*/
96120
export async function searchUniProtEntries({
97121
recognizedIds = [],
@@ -103,72 +127,29 @@ export async function searchUniProtEntries({
103127
geneId?: string
104128
geneName?: string
105129
organismId?: number
106-
}): Promise<UniProtEntry[]> {
107-
const entries: UniProtEntry[] = []
108-
const seenAccessions = new Set<string>()
109-
110-
const addEntries = (newEntries: UniProtEntry[]) => {
111-
for (const entry of newEntries) {
112-
if (!seenAccessions.has(entry.accession)) {
113-
seenAccessions.add(entry.accession)
114-
entries.push(entry)
115-
}
116-
}
117-
}
118-
119-
// Strategy 1: Search by recognized database IDs (most specific)
120-
for (const id of recognizedIds) {
121-
const query = buildXrefQuery(id)
122-
if (query) {
123-
try {
124-
const results = await searchUniProt(query)
125-
addEntries(results)
126-
if (results.some(e => e.isReviewed)) {
127-
break
128-
}
129-
} catch {
130-
// xref search failed, continue to next ID
131-
}
132-
}
133-
}
134-
135-
// Strategy 2: Try legacy gene_id if it looks like a recognized database ID
130+
}) {
131+
// Collect all IDs to search, including legacy geneId if applicable
132+
const idsToSearch = new Set(recognizedIds)
136133
const strippedGeneId = geneId ? stripTrailingVersion(geneId) : undefined
137-
if (
138-
strippedGeneId &&
139-
isRecognizedDatabaseId(strippedGeneId) &&
140-
!recognizedIds.includes(strippedGeneId)
141-
) {
142-
const query = buildXrefQuery(strippedGeneId)
143-
if (query) {
144-
try {
145-
addEntries(await searchUniProt(query))
146-
} catch {
147-
// xref search failed
148-
}
149-
}
134+
if (strippedGeneId && isRecognizedDatabaseId(strippedGeneId)) {
135+
idsToSearch.add(strippedGeneId)
150136
}
151137

152-
// Strategy 3: If no reviewed entries found, try gene name search
138+
// Search all xrefs in parallel
139+
const xrefResults = await Promise.all([...idsToSearch].map(searchByXref))
140+
let entries = deduplicateEntries(xrefResults.flat())
141+
142+
// Fallback: if no reviewed entries found, try gene name search
153143
if (!entries.some(e => e.isReviewed) && geneName) {
154144
try {
155145
const query = `gene:${geneName}+AND+organism_id:${organismId}+AND+reviewed:true`
156-
addEntries(await searchUniProt(query, 5))
157-
} catch {
158-
// gene name search failed
146+
const geneNameResults = await searchUniProt(query, 5)
147+
entries = deduplicateEntries([...entries, ...geneNameResults])
148+
} catch (e) {
149+
console.error(`gene name search failed for ${geneName}:`, e)
159150
}
160151
}
161152

162153
// Sort reviewed entries first
163-
entries.sort((a, b) => {
164-
if (a.isReviewed && !b.isReviewed) {
165-
return -1
166-
}
167-
if (!a.isReviewed && b.isReviewed) {
168-
return 1
169-
}
170-
return 0
171-
})
172-
173-
return entries
154+
return entries.toSorted((a, b) => Number(b.isReviewed) - Number(a.isReviewed))
174155
}

src/ProteinView/components/ProteinFeatureTrack.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ const FeatureBar = observer(function FeatureBar({
4747
}
4848
}
4949

50+
const highlightAlignmentRange = () => {
51+
const { structurePositionToAlignmentMap } = model
52+
if (!structurePositionToAlignmentMap) {
53+
return
54+
}
55+
const startAlignmentPos = structurePositionToAlignmentMap[feature.start - 1]
56+
const endAlignmentPos = structurePositionToAlignmentMap[feature.end - 1]
57+
if (startAlignmentPos !== undefined && endAlignmentPos !== undefined) {
58+
model.setAlignmentHoverRange({
59+
start: startAlignmentPos,
60+
end: endAlignmentPos,
61+
})
62+
}
63+
}
64+
5065
const handleMouseEnter = () => {
5166
setIsHovered(true)
5267
const structure = model.molstarStructure
@@ -59,12 +74,14 @@ const FeatureBar = observer(function FeatureBar({
5974
})
6075
}
6176
highlightGenomeRange()
77+
highlightAlignmentRange()
6278
}
6379

6480
const handleMouseLeave = () => {
6581
setIsHovered(false)
6682
molstarPluginContext?.managers.interactivity.lociHighlights.clearHighlights()
6783
model.clearHoverGenomeHighlights()
84+
model.clearAlignmentHoverRange()
6885
}
6986

7087
const handleClick = () => {

src/ProteinView/components/SplitString.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,40 @@ const HoverHighlight = observer(function HoverHighlight({
7979
)
8080
})
8181

82+
const RangeHoverHighlight = observer(function RangeHoverHighlight({
83+
model,
84+
strLength,
85+
}: {
86+
model: JBrowsePluginProteinStructureModel
87+
strLength: number
88+
}) {
89+
const { alignmentHoverRange } = model
90+
if (!alignmentHoverRange) {
91+
return null
92+
}
93+
const { start, end } = alignmentHoverRange
94+
const clampedStart = Math.max(0, start)
95+
const clampedEnd = Math.min(strLength - 1, end)
96+
if (clampedStart > clampedEnd) {
97+
return null
98+
}
99+
const width = (clampedEnd - clampedStart + 1) * CHAR_WIDTH
100+
101+
return (
102+
<span
103+
style={{
104+
position: 'absolute',
105+
left: clampedStart * CHAR_WIDTH,
106+
top: 0,
107+
width,
108+
height: '100%',
109+
background: 'rgba(255, 165, 0, 0.4)',
110+
pointerEvents: 'none',
111+
}}
112+
/>
113+
)
114+
})
115+
82116
const SplitString = observer(function SplitString({
83117
model,
84118
str,
@@ -135,6 +169,7 @@ const SplitString = observer(function SplitString({
135169
>
136170
<MatchOverlays model={model} />
137171
<CharacterSpans str={str} />
172+
<RangeHoverHighlight model={model} strLength={str.length} />
138173
<HoverHighlight model={model} strLength={str.length} />
139174
</span>
140175
)

src/ProteinView/structureModel.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ const Structure = types
143143
* Tracks whether this structure has been loaded into Molstar
144144
*/
145145
loadedToMolstar: false,
146+
/**
147+
* #volatile
148+
* Range of alignment positions to highlight (e.g., when hovering a protein feature)
149+
*/
150+
alignmentHoverRange: undefined as
151+
| { start: number; end: number }
152+
| undefined,
146153
}))
147154
.actions(self => ({
148155
setSequences(str?: string[]) {
@@ -199,6 +206,18 @@ const Structure = types
199206
clearHoverGenomeHighlights() {
200207
self.hoverGenomeHighlights = []
201208
},
209+
/**
210+
* #action
211+
*/
212+
setAlignmentHoverRange(range?: { start: number; end: number }) {
213+
self.alignmentHoverRange = range
214+
},
215+
/**
216+
* #action
217+
*/
218+
clearAlignmentHoverRange() {
219+
self.alignmentHoverRange = undefined
220+
},
202221
/**
203222
* #action
204223
*/

src/ProteinView/util.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect, test } from 'vitest'
2+
3+
import { invertMap } from './util'
4+
5+
test('invertMap - simple case', () => {
6+
const map = { 0: 10, 1: 20, 2: 30 }
7+
const inverted = invertMap(map)
8+
expect(inverted[10]).toBe(0)
9+
expect(inverted[20]).toBe(1)
10+
expect(inverted[30]).toBe(2)
11+
})
12+
13+
test('invertMap - empty map', () => {
14+
const map = {}
15+
const inverted = invertMap(map)
16+
expect(Object.keys(inverted)).toHaveLength(0)
17+
})
18+
19+
test('invertMap - non-sequential values', () => {
20+
const map = { 5: 100, 10: 200, 15: 300 }
21+
const inverted = invertMap(map)
22+
expect(inverted[100]).toBe(5)
23+
expect(inverted[200]).toBe(10)
24+
expect(inverted[300]).toBe(15)
25+
})
26+
27+
test('invertMap - alignment position mapping inversion', () => {
28+
// Simulates inverting structurePositionToAlignmentMap
29+
// structure pos -> alignment pos
30+
const structureToAlignment = { 0: 2, 1: 3, 2: 4, 3: 5 }
31+
// alignment pos -> structure pos
32+
const alignmentToStructure = invertMap(structureToAlignment)
33+
34+
expect(alignmentToStructure[2]).toBe(0)
35+
expect(alignmentToStructure[3]).toBe(1)
36+
expect(alignmentToStructure[4]).toBe(2)
37+
expect(alignmentToStructure[5]).toBe(3)
38+
// positions 0,1 in alignment have no structure mapping
39+
expect(alignmentToStructure[0]).toBeUndefined()
40+
expect(alignmentToStructure[1]).toBeUndefined()
41+
})

src/__snapshots__/mappings.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ exports[`mapping 1`] = `
899899
}
900900
`;
901901

902-
exports[`test 1`] = `
902+
exports[`structureSeqVsTranscriptSeqMap snapshot 1`] = `
903903
{
904904
"structureSeqToTranscriptSeqPosition": {
905905
"0": 392,

0 commit comments

Comments
 (0)