From 613ebaf5f34eaf72fa083be68a80513d814cdf5c Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 22 Aug 2024 13:17:58 -0400 Subject: [PATCH] Wow --- eslint.config.mjs | 1 + .../GenomeMouseoverHighlight.tsx | 2 +- .../ProteinToGenomeClickHighlight.tsx | 24 ++++---- .../ProteinToGenomeHoverHighlight.tsx | 28 ++++----- .../components/AlphaFoldDBSearch.tsx | 20 ++++--- .../components/TranscriptSelector.tsx | 12 ++-- .../components/UserProvidedStructure.tsx | 7 ++- .../components/calculateProteinSequence.ts | 8 ++- .../useLocalStructureFileSequence.ts | 37 +++++++++--- .../useRemoteStructureFileSequence.ts | 31 +++++++--- src/ProteinView/components/Header.tsx | 50 ++++++++++------ .../components/ProteinAlignment.tsx | 18 +++--- .../components/ProteinAlignmentHelpButton.tsx | 4 +- src/ProteinView/components/ProteinView.tsx | 5 ++ src/ProteinView/components/SplitString.tsx | 8 +-- src/ProteinView/model.ts | 60 ++++++++++++++++++- src/ProteinView/proteinToGenomeMapping.ts | 5 +- 17 files changed, 221 insertions(+), 99 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 8e619d2..2e6f926 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -78,6 +78,7 @@ export default tseslint.config( 'unicorn/no-null': 'off', 'unicorn/prefer-spread': 'off', + 'unicorn/no-nested-ternary': 'off', 'unicorn/no-useless-undefined': 'off', 'unicorn/catch-error-name': 'off', 'unicorn/filename-case': 'off', diff --git a/src/AddHighlightModel/GenomeMouseoverHighlight.tsx b/src/AddHighlightModel/GenomeMouseoverHighlight.tsx index bfed472..4ea8b9c 100644 --- a/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +++ b/src/AddHighlightModel/GenomeMouseoverHighlight.tsx @@ -35,7 +35,7 @@ const HoverHighlight = observer(function ({ start={coord - 1} end={coord} refName={refName} - assemblyName={assemblyNames[0]} + assemblyName={assemblyNames[0]!} /> ) } diff --git a/src/AddHighlightModel/ProteinToGenomeClickHighlight.tsx b/src/AddHighlightModel/ProteinToGenomeClickHighlight.tsx index fd3a3b9..c829061 100644 --- a/src/AddHighlightModel/ProteinToGenomeClickHighlight.tsx +++ b/src/AddHighlightModel/ProteinToGenomeClickHighlight.tsx @@ -16,23 +16,25 @@ const ProteinToGenomeClickHighlight = observer(function ({ }) { const { assemblyManager, views } = getSession(model) const { assemblyNames } = model - const p = views.find(f => f.type === 'ProteinView') as + const proteinView = views.find(f => f.type === 'ProteinView') as | JBrowsePluginProteinViewModel | undefined const assemblyName = assemblyNames[0]! const assembly = assemblyManager.get(assemblyName) return assembly ? ( <> - {p?.clickGenomeHighlights.map((r, idx) => ( - - ))} + {proteinView?.structures.map(structure => + structure.clickGenomeHighlights.map((r, idx) => ( + + )), + )} ) : null }) diff --git a/src/AddHighlightModel/ProteinToGenomeHoverHighlight.tsx b/src/AddHighlightModel/ProteinToGenomeHoverHighlight.tsx index f98b61d..048e454 100644 --- a/src/AddHighlightModel/ProteinToGenomeHoverHighlight.tsx +++ b/src/AddHighlightModel/ProteinToGenomeHoverHighlight.tsx @@ -7,32 +7,32 @@ import { getSession } from '@jbrowse/core/util' import { JBrowsePluginProteinViewModel } from '../ProteinView/model' import Highlight from './Highlight' -type LGV = LinearGenomeViewModel - const ProteinToGenomeHoverHighlight = observer(function ({ model, }: { - model: LGV + model: LinearGenomeViewModel }) { const { assemblyManager, views } = getSession(model) const { assemblyNames } = model - const p = views.find(f => f.type === 'ProteinView') as + const proteinView = views.find(f => f.type === 'ProteinView') as | JBrowsePluginProteinViewModel | undefined const assemblyName = assemblyNames[0]! const assembly = assemblyManager.get(assemblyName) return assembly ? ( <> - {p?.hoverGenomeHighlights.map((r, idx) => ( - - ))} + {proteinView?.structures.map(structure => + structure.hoverGenomeHighlights.map((r, idx) => ( + + )), + )} ) : null }) diff --git a/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx b/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx index a7b95aa..19ef03f 100644 --- a/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx +++ b/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx @@ -78,13 +78,14 @@ const AlphaFoldDBSearch = observer(function ({ ? `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-model_v4.cif` : undefined const { - seq: structureSequence, + sequences: structureSequences, isLoading: isRemoteStructureSequenceLoading, error: remoteStructureSequenceError, } = useRemoteStructureFileSequence({ url }) const e = myGeneError || isoformProteinSequencesError || remoteStructureSequenceError + const structureSequence = structureSequences?.[0]! useEffect(() => { if (isoformSequences !== undefined) { const ret = @@ -116,7 +117,7 @@ const AlphaFoldDBSearch = observer(function ({ variant="h6" message="Looking up UniProt ID from mygene.info" /> - ) : (uniprotId ? null : ( + ) : uniprotId ? null : (
UniProt ID not found. Search sequence on AlphaFoldDB{' '} - ))} + )} {isIsoformProteinSequencesLoading ? ( { session.addView('ProteinView', { type: 'ProteinView', - url, - seq2: userSelectedProteinSequence?.seq, - feature: selectedTranscript?.toJSON(), - connectedViewId: view.id, + structures: [ + { + url, + userProvidedTranscriptSequence: + userSelectedProteinSequence?.seq, + feature: selectedTranscript?.toJSON(), + connectedViewId: view.id, + }, + ], displayName: `Protein view ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`, }) handleClose() diff --git a/src/LaunchProteinView/components/TranscriptSelector.tsx b/src/LaunchProteinView/components/TranscriptSelector.tsx index 5cb27b2..698238a 100644 --- a/src/LaunchProteinView/components/TranscriptSelector.tsx +++ b/src/LaunchProteinView/components/TranscriptSelector.tsx @@ -41,13 +41,13 @@ export default function TranscriptSelector({ .filter(f => !!isoformSequences[f.id()]) .filter( f => - isoformSequences[f.id()].seq.replaceAll('*', '') === + isoformSequences[f.id()]!.seq.replaceAll('*', '') === structureSequence, ) .map(f => ( {getGeneDisplayName(feature)} - {getTranscriptDisplayName(f)} ( - {isoformSequences[f.id()].seq.length}aa) (matches structure + {isoformSequences[f.id()]!.seq.length}aa) (matches structure residues) ))} @@ -55,18 +55,18 @@ export default function TranscriptSelector({ .filter(f => !!isoformSequences[f.id()]) .filter( f => - isoformSequences[f.id()].seq.replaceAll('*', '') !== + isoformSequences[f.id()]!.seq.replaceAll('*', '') !== structureSequence, ) .sort( (a, b) => - isoformSequences[b.id()].seq.length - - isoformSequences[a.id()].seq.length, + isoformSequences[b.id()]!.seq.length - + isoformSequences[a.id()]!.seq.length, ) .map(f => ( {getGeneDisplayName(feature)} - {getTranscriptDisplayName(f)} ( - {isoformSequences[f.id()].seq.length}aa) + {isoformSequences[f.id()]!.seq.length}aa) ))} {isoforms diff --git a/src/LaunchProteinView/components/UserProvidedStructure.tsx b/src/LaunchProteinView/components/UserProvidedStructure.tsx index 05d661f..da7b5f6 100644 --- a/src/LaunchProteinView/components/UserProvidedStructure.tsx +++ b/src/LaunchProteinView/components/UserProvidedStructure.tsx @@ -96,14 +96,15 @@ const UserProvidedStructure = observer(function ({ view, }) const protein = isoformSequences?.[userSelection ?? ''] - const { seq: structureSequence1, error: error3 } = + const { sequences: structureSequences1, error: error3 } = useLocalStructureFileSequence({ file }) - const { seq: structureSequence2, error: error4 } = + const { sequences: structureSequences2, error: error4 } = useRemoteStructureFileSequence({ url: structureURL }) const structureName = file?.name ?? structureURL.slice(structureURL.lastIndexOf('/') + 1) - const structureSequence = structureSequence1 ?? structureSequence2 + const structureSequences = structureSequences1 ?? structureSequences2 + const structureSequence = structureSequences?.[0] useEffect(() => { if (isoformSequences !== undefined) { diff --git a/src/LaunchProteinView/components/calculateProteinSequence.ts b/src/LaunchProteinView/components/calculateProteinSequence.ts index 48791bf..97808aa 100644 --- a/src/LaunchProteinView/components/calculateProteinSequence.ts +++ b/src/LaunchProteinView/components/calculateProteinSequence.ts @@ -53,7 +53,7 @@ function getItemId(feat: Feat) { // filters if successive elements share same start/end export function dedupe(list: Feat[]) { return list.filter( - (item, pos, ary) => !pos || getItemId(item) !== getItemId(ary[pos - 1]), + (item, pos, ary) => !pos || getItemId(item) !== getItemId(ary[pos - 1]!), ) } @@ -102,8 +102,10 @@ export async function fetchProteinSeq({ const refName = feature.get('refName') const session = getSession(view) const { assemblyManager, rpcManager } = session - const [assemblyName] = view?.assemblyNames ?? [] - const assembly = await assemblyManager.waitForAssembly(assemblyName) + const assemblyName = view?.assemblyNames?.[0] + const assembly = assemblyName + ? await assemblyManager.waitForAssembly(assemblyName) + : undefined if (!assembly) { throw new Error('assembly not found') } diff --git a/src/LaunchProteinView/components/useLocalStructureFileSequence.ts b/src/LaunchProteinView/components/useLocalStructureFileSequence.ts index f145a68..597ce2e 100644 --- a/src/LaunchProteinView/components/useLocalStructureFileSequence.ts +++ b/src/LaunchProteinView/components/useLocalStructureFileSequence.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { createPluginUI } from 'molstar/lib/mol-plugin-ui' import { renderReact18 } from 'molstar/lib/mol-plugin-ui/react18' -import { loadStructureFromData } from '../../ProteinView/loadStructureFromData' +import { addStructureFromData } from '../../ProteinView/addStructureFromData' async function structureFileSequenceFetcher( file: File, @@ -12,11 +12,26 @@ async function structureFileSequenceFetcher( target: ret, render: renderReact18, }) - const data = await file.text() - const { seq } = await loadStructureFromData({ data, plugin: p, format }) - p.unmount() - ret.remove() - return seq + + try { + const { model } = await addStructureFromData({ + data: await file.text(), + plugin: p, + format, + }) + return model.obj?.data.sequence.sequences.map(s => { + let seq = '' + const arr = s.sequence.label.toArray() + // eslint-disable-next-line unicorn/no-for-loop,@typescript-eslint/prefer-for-of + for (let i = 0; i < arr.length; i++) { + seq += arr[i]! + } + return seq + }) + } finally { + p.unmount() + ret.remove() + } } export default function useLocalStructureFileSequence({ @@ -26,7 +41,7 @@ export default function useLocalStructureFileSequence({ }) { const [error, setError] = useState() const [isLoading, setLoading] = useState(false) - const [seq, setSeq] = useState() + const [sequences, setSequences] = useState() useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises ;(async () => { @@ -39,7 +54,11 @@ export default function useLocalStructureFileSequence({ file, (ext === 'cif' ? 'mmcif' : ext) as 'pdb' | 'mmcif', ) - setSeq(seq) + if (seq) { + setSequences(seq) + } else { + throw new Error('no sequences detected in file') + } } } catch (e) { console.error(e) @@ -49,5 +68,5 @@ export default function useLocalStructureFileSequence({ } })() }, [file]) - return { error, isLoading, seq } + return { error, isLoading, sequences } } diff --git a/src/LaunchProteinView/components/useRemoteStructureFileSequence.ts b/src/LaunchProteinView/components/useRemoteStructureFileSequence.ts index dcb3c9e..d4ae3ae 100644 --- a/src/LaunchProteinView/components/useRemoteStructureFileSequence.ts +++ b/src/LaunchProteinView/components/useRemoteStructureFileSequence.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { createPluginUI } from 'molstar/lib/mol-plugin-ui' import { renderReact18 } from 'molstar/lib/mol-plugin-ui/react18' -import { loadStructureFromURL } from '../../ProteinView/loadStructureFromURL' +import { addStructureFromURL } from '../../ProteinView/addStructureFromURL' async function structureFileSequenceFetcher(url: string) { const ret = document.createElement('div') @@ -9,10 +9,21 @@ async function structureFileSequenceFetcher(url: string) { target: ret, render: renderReact18, }) - const { seq } = await loadStructureFromURL({ url, plugin: p }) - p.unmount() - ret.remove() - return seq + try { + const { model } = await addStructureFromURL({ url, plugin: p }) + return model.obj?.data.sequence.sequences.map(s => { + let seq = '' + const arr = s.sequence.label.toArray() + // eslint-disable-next-line unicorn/no-for-loop,@typescript-eslint/prefer-for-of + for (let i = 0; i < arr.length; i++) { + seq += arr[i]! + } + return seq + }) + } finally { + p.unmount() + ret.remove() + } } export default function useRemoteStructureFileSequence({ @@ -22,7 +33,7 @@ export default function useRemoteStructureFileSequence({ }) { const [error, setError] = useState() const [isLoading, setLoading] = useState(false) - const [seq, setSeq] = useState() + const [sequences, setSequences] = useState() useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises ;(async () => { @@ -30,7 +41,11 @@ export default function useRemoteStructureFileSequence({ if (url) { setLoading(true) const seq = await structureFileSequenceFetcher(url) - setSeq(seq) + if (seq) { + setSequences(seq) + } else { + throw new Error('no sequences detected in file') + } } } catch (e) { console.error(e) @@ -40,5 +55,5 @@ export default function useRemoteStructureFileSequence({ } })() }, [url]) - return { error, isLoading, seq } + return { error, isLoading, sequences } } diff --git a/src/ProteinView/components/Header.tsx b/src/ProteinView/components/Header.tsx index da914b1..0581670 100644 --- a/src/ProteinView/components/Header.tsx +++ b/src/ProteinView/components/Header.tsx @@ -16,41 +16,53 @@ const ProteinViewHeader = observer(function ({ }: { model: JBrowsePluginProteinViewModel }) { - const { alignment, showAlignment } = model + const { showAlignment } = model return (
- {showAlignment ? ( - alignment ? ( - - ) : ( - - ) - ) : null} + {showAlignment + ? model.structures.map(s => { + const { alignment } = s + + return alignment ? ( + + ) : ( + + ) + }) + : null}
) }) -const InformativeHeaderArea = observer(function ({ +const StructureInfoHeaderArea = observer(function ({ model, }: { model: JBrowsePluginProteinViewModel }) { - const { - showAlignment, - clickString, - hoverString, - showHighlight, - zoomToBaseLevel, - } = model - return ( -
- + return model.structures.map((s, id) => { + const { clickString, hoverString } = s + + return ( + {[ clickString ? `Click: ${clickString}` : '', hoverString ? `Hover: ${hoverString}` : '', ].join(' ')} + ) + }) +}) + +const InformativeHeaderArea = observer(function ({ + model, +}: { + model: JBrowsePluginProteinViewModel +}) { + const { showAlignment, showHighlight, zoomToBaseLevel } = model + return ( +
+ { - console.error(e) - }) + if (alignmentToStructurePosition) { + const structureSeqPos = alignmentToStructurePosition[alignmentPos]! + clickProteinToGenome({ model, structureSeqPos }).catch((e: unknown) => { + console.error(e) + }) + } } return (
diff --git a/src/ProteinView/components/ProteinAlignmentHelpButton.tsx b/src/ProteinView/components/ProteinAlignmentHelpButton.tsx index 48aeee0..1f8256e 100644 --- a/src/ProteinView/components/ProteinAlignmentHelpButton.tsx +++ b/src/ProteinView/components/ProteinAlignmentHelpButton.tsx @@ -3,7 +3,7 @@ import { IconButton } from '@mui/material' import { getSession } from '@jbrowse/core/util' // locals -import { JBrowsePluginProteinViewModel } from '../model' +import { JBrowsePluginProteinStructureModel } from '../model' // icons import Help from '@mui/icons-material/Help' @@ -15,7 +15,7 @@ const ProteinAlignmentHelpDialog = lazy( export default function ProteinAlignmentHelpButton({ model, }: { - model: JBrowsePluginProteinViewModel + model: JBrowsePluginProteinStructureModel }) { return ( { + model.setMolstarPluginContext(plugin) + }, [plugin, model]) + return error ? ( ) : ( diff --git a/src/ProteinView/components/SplitString.tsx b/src/ProteinView/components/SplitString.tsx index bad7b3d..0b4ca59 100644 --- a/src/ProteinView/components/SplitString.tsx +++ b/src/ProteinView/components/SplitString.tsx @@ -4,16 +4,16 @@ export default function SplitString({ str, col, set, + showHighlight, onMouseOver, onClick, - showHighlight, }: { str: string col?: number set?: Set + showHighlight: boolean onMouseOver?: (arg: number) => void onClick?: (arg: number) => void - showHighlight: boolean }) { return str.split('').map((d, i) => ( {d === ' ' ? <>  : d} diff --git a/src/ProteinView/model.ts b/src/ProteinView/model.ts index 1dac78a..b703691 100644 --- a/src/ProteinView/model.ts +++ b/src/ProteinView/model.ts @@ -2,7 +2,13 @@ import { autorun } from 'mobx' import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes' import { ElementId } from '@jbrowse/core/util/types/mst' import { Region as IRegion } from '@jbrowse/core/util/types' -import { Instance, addDisposer, getParent, types } from 'mobx-state-tree' +import { + Instance, + addDisposer, + getParent, + getSnapshot, + types, +} from 'mobx-state-tree' import { SimpleFeature, SimpleFeatureSerialized, @@ -21,6 +27,8 @@ import { PairwiseAlignment, } from '../mappings' import { PluginContext } from 'molstar/lib/mol-plugin/context' +import { addStructureFromData } from './addStructureFromData' +import { addStructureFromURL } from './addStructureFromURL' type LGV = LinearGenomeViewModel type MaybeLGV = LGV | undefined @@ -325,11 +333,14 @@ const Structure = types self.setAlignment(alignment.alignment) // showHighlight when we are + // @ts-expect-error getParent(self, 2).setShowHighlight(true) + // @ts-expect-error getParent(self, 2).setShowAlignment(true) } } catch (e) { console.error(e) + // @ts-expect-error getParent(self, 2).setError(e) } }), @@ -420,7 +431,14 @@ function stateModelFactory() { * #volatile */ progress: '', + /** + * #volatile + */ error: undefined as unknown, + /** + * #volatile + */ + molstarPluginContext: undefined as PluginContext | undefined, })) .actions(self => ({ @@ -457,6 +475,46 @@ function stateModelFactory() { setZoomToBaseLevel(arg: boolean) { self.zoomToBaseLevel = arg }, + /** + * #action + */ + setMolstarPluginContext(p?: PluginContext) { + self.molstarPluginContext = p + }, + })) + .actions(self => ({ + afterCreate() { + addDisposer( + self, + autorun(async () => { + const { structures, molstarPluginContext } = self + console.log({ + structures: getSnapshot(structures), + molstarPluginContext, + }) + if (molstarPluginContext) { + for (const structure of structures) { + try { + if (structure.data) { + await addStructureFromData({ + data: structure.data, + plugin: molstarPluginContext, + }) + } else if (structure.url) { + await addStructureFromURL({ + url: structure.url, + plugin: molstarPluginContext, + }) + } + } catch (e) { + self.setError(e) + console.error(e) + } + } + } + }), + ) + }, })) } diff --git a/src/ProteinView/proteinToGenomeMapping.ts b/src/ProteinView/proteinToGenomeMapping.ts index 76034fd..a18a810 100644 --- a/src/ProteinView/proteinToGenomeMapping.ts +++ b/src/ProteinView/proteinToGenomeMapping.ts @@ -2,10 +2,7 @@ import { getSession } from '@jbrowse/core/util' import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' // locals -import { - JBrowsePluginProteinStructureModel, - JBrowsePluginProteinViewModel, -} from './model' +import { JBrowsePluginProteinStructureModel } from './model' export function proteinToGenomeMapping({ model,