diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx index 40f1764118a066fd308c4f6d366387ca37acff93..520d828dc22fe54f8237ddec17542931d527439a 100644 --- a/src/mol-plugin/ui/sequence.tsx +++ b/src/mol-plugin/ui/sequence.tsx @@ -9,19 +9,29 @@ import * as React from 'react' import { PluginUIComponent } from './base'; import { StateTreeSpine } from '../../mol-state/tree/spine'; import { PluginStateObject as SO } from '../state/objects'; +import { Sequence } from './sequence/sequence'; +import { Structure } from '../../mol-model/structure'; +import { SequenceWrapper } from './sequence/util'; +import { PolymerSequenceWrapper } from './sequence/polymer'; +import { StructureElementSelectionManager } from '../util/structure-element-selection'; import { MarkerAction } from '../../mol-util/marker-action'; -import { PolymerSequence } from './sequence/polymer'; -import { StructureSeq, markResidue } from './sequence/util'; -function getStructureSeqKey(structureSeq: StructureSeq) { - const { structure, seq } = structureSeq - const strucHash = structure.parent ? structure.parent.hashCode : structure.hashCode - return `${strucHash}|${seq.entityId}` +function getSequenceWrappersForStructure(structure: Structure, structureSelection: StructureElementSelectionManager) { + const sequenceWrappers: SequenceWrapper.Any[] = [] + + structure.units.forEach(unit => { + if (unit.polymerElements.length === 0) return + + const sw = new PolymerSequenceWrapper({ structure, unit }) + sw.markResidue(structureSelection.get(structure), MarkerAction.Select) + sequenceWrappers.push(sw) + }) + + return sequenceWrappers } export class SequenceView extends PluginUIComponent<{ }, { }> { private spine: StateTreeSpine.Impl - private markerArrays = new Map<string, Uint8Array>() componentDidMount() { this.spine = new StateTreeSpine.Impl(this.plugin.state.dataState.cells); @@ -39,20 +49,6 @@ export class SequenceView extends PluginUIComponent<{ }, { }> { }); } - private getMarkerArray(structureSeq: StructureSeq): Uint8Array { - const { structure, seq } = structureSeq - const key = getStructureSeqKey(structureSeq) - let markerArray = this.markerArrays.get(key) - if (!markerArray) { - markerArray = new Uint8Array(seq.sequence.sequence.length) - this.markerArrays.set(key, markerArray) - } - const loci = this.plugin.helpers.structureSelection.get(structure) - markerArray.fill(0) - markResidue(loci, structureSeq, markerArray, MarkerAction.Select) - return markerArray - } - private getStructure() { const so = this.spine && this.spine.getRootOfType(SO.Molecule.Structure) return so && so.data @@ -64,12 +60,11 @@ export class SequenceView extends PluginUIComponent<{ }, { }> { <div className='msp-sequence-entity'>No structure available</div> </div>; - const seqs = structure.models[0].sequence.sequences; + const { structureSelection } = this.plugin.helpers + const sequenceWrappers = getSequenceWrappersForStructure(structure, structureSelection) return <div className='msp-sequence'> - {seqs.map((seq, i) => { - const structureSeq = { structure, seq } - const markerArray = this.getMarkerArray(structureSeq) - return <PolymerSequence key={i} structureSeq={structureSeq} markerArray={markerArray} /> + {sequenceWrappers.map((sequenceWrapper, i) => { + return <Sequence key={i} sequenceWrapper={sequenceWrapper} /> })} </div>; } diff --git a/src/mol-plugin/ui/sequence/polymer.ts b/src/mol-plugin/ui/sequence/polymer.ts new file mode 100644 index 0000000000000000000000000000000000000000..77516a23902d0ff4b762f10a8d62161eeb2e7da6 --- /dev/null +++ b/src/mol-plugin/ui/sequence/polymer.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { StructureSelection, StructureQuery, Structure, Queries, StructureProperties as SP, StructureElement, Unit } from '../../../mol-model/structure'; +import { SequenceWrapper } from './util'; +import { OrderedSet, Interval } from '../../../mol-data/int'; +import { Loci } from '../../../mol-model/loci'; + +export type StructureUnit = { structure: Structure, unit: Unit } + +export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { + private readonly location = StructureElement.create() + + private entityId: string + private label_asym_id: string + + eachResidue(loci: Loci, apply: (interval: Interval) => boolean) { + let changed = false + const { structure } = this.data + if (!StructureElement.isLoci(loci)) return false + if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + + const { location, entityId, label_asym_id } = this + for (const e of loci.elements) { + let rIprev = -1 + location.unit = e.unit + + const { index: residueIndex } = e.unit.model.atomicHierarchy.residueAtomSegments + + OrderedSet.forEach(e.indices, v => { + location.element = e.unit.elements[v] + const rI = residueIndex[location.element] + // avoid checking for the same residue multiple times + if (rI !== rIprev) { + if (SP.entity.id(location) !== entityId) return + if (SP.chain.label_asym_id(location) !== label_asym_id) return + + if (apply(getSeqIdInterval(location))) changed = true + rIprev = rI + } + }) + } + return changed + } + + getLoci(seqId: number) { + const query = createResidueQuery(this.entityId, seqId, this.label_asym_id); + return StructureSelection.toLoci2(StructureQuery.run(query, this.data.structure)); + } + + constructor(readonly data: StructureUnit) { + super() + + const l = this.location + l.unit = data.unit + l.element = data.unit.elements[0] + + this.entityId = SP.entity.id(l) + this.label_asym_id = SP.chain.label_asym_id(l) + + this.label = `${this.label_asym_id}|${this.entityId}` + this.sequence = data.unit.model.sequence.byEntityKey[SP.entity.key(l)].sequence + this.markerArray = new Uint8Array(this.sequence.sequence.length) + } +} + +function createResidueQuery(entityId: string, label_seq_id: number, label_asym_id: string) { + return Queries.generators.atoms({ + entityTest: ctx => { + return SP.entity.id(ctx.element) === entityId + }, + chainTest: ctx => { + return SP.chain.label_asym_id(ctx.element) === label_asym_id + }, + residueTest: ctx => { + if (ctx.element.unit.kind === Unit.Kind.Atomic) { + return SP.residue.label_seq_id(ctx.element) === label_seq_id + } else { + return ( + SP.coarse.seq_id_begin(ctx.element) <= label_seq_id && + SP.coarse.seq_id_end(ctx.element) >= label_seq_id + ) + } + } + }); +} + +/** Zero-indexed */ +function getSeqIdInterval(location: StructureElement): Interval { + const { unit, element } = location + const { model } = unit + switch (unit.kind) { + case Unit.Kind.Atomic: + const residueIndex = model.atomicHierarchy.residueAtomSegments.index[element] + const seqId = model.atomicHierarchy.residues.label_seq_id.value(residueIndex) + return Interval.ofSingleton(seqId - 1) + case Unit.Kind.Spheres: + return Interval.ofRange( + model.coarseHierarchy.spheres.seq_id_begin.value(element) - 1, + model.coarseHierarchy.spheres.seq_id_end.value(element) - 1 + ) + case Unit.Kind.Gaussians: + return Interval.ofRange( + model.coarseHierarchy.gaussians.seq_id_begin.value(element) - 1, + model.coarseHierarchy.gaussians.seq_id_end.value(element) - 1 + ) + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/sequence/polymer.tsx b/src/mol-plugin/ui/sequence/polymer.tsx deleted file mode 100644 index 2e1f5e5ce7f188017a8c181e77b61d7e01cbc27b..0000000000000000000000000000000000000000 --- a/src/mol-plugin/ui/sequence/polymer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import * as React from 'react' -import { StructureSelection, StructureQuery } from '../../../mol-model/structure'; -import { createResidueQuery } from './util'; -import { Residue } from './residue'; -import { BaseSequence } from './base'; - -export class PolymerSequence extends BaseSequence { - getLoci(seqId: number) { - const { structure, seq } = this.props.structureSeq - const query = createResidueQuery(seq.entityId, seqId); - return StructureSelection.toLoci2(StructureQuery.run(query, structure)); - } - - render() { - const { markerData } = this.state; - const { seq } = this.props.structureSeq; - const { offset, sequence } = seq.sequence; - - const elems: JSX.Element[] = []; - for (let i = 0, _i = sequence.length; i < _i; i++) { - elems[elems.length] = <Residue seqId={offset + i + 1} letter={sequence[i]} parent={this} marker={markerData.value[i]} key={i} />; - } - - return <div - className='msp-sequence-entity' - onContextMenu={this.contextMenu} - onMouseDown={this.mouseDown} - > - <span style={{ fontWeight: 'bold' }}>{seq.entityId}:{offset} </span> - {elems} - </div>; - } -} diff --git a/src/mol-plugin/ui/sequence/residue.tsx b/src/mol-plugin/ui/sequence/residue.tsx index d27f477687c68f7495121727df696c2c4c9ead4c..d5329a90be40d8a03f8b8dc026e0142431a1850b 100644 --- a/src/mol-plugin/ui/sequence/residue.tsx +++ b/src/mol-plugin/ui/sequence/residue.tsx @@ -8,9 +8,9 @@ import * as React from 'react' import { PurePluginUIComponent } from '../base'; import { getButtons, getModifiers } from '../../../mol-util/input/input-observer'; -import { BaseSequence } from './base'; +import { Sequence } from './sequence'; -export class Residue extends PurePluginUIComponent<{ seqId: number, letter: string, parent: BaseSequence, marker: number }> { +export class Residue extends PurePluginUIComponent<{ seqId: number, letter: string, parent: Sequence<any>, marker: number }> { mouseEnter = (e: React.MouseEvent) => { const modifiers = getModifiers(e.nativeEvent) diff --git a/src/mol-plugin/ui/sequence/base.tsx b/src/mol-plugin/ui/sequence/sequence.tsx similarity index 61% rename from src/mol-plugin/ui/sequence/base.tsx rename to src/mol-plugin/ui/sequence/sequence.tsx index b10a549f83ba5acf9cdbdc7a85b17cc8cd1df529..b392a335b35d6435927a4f0b0ef19b1dd21b0a4f 100644 --- a/src/mol-plugin/ui/sequence/base.tsx +++ b/src/mol-plugin/ui/sequence/sequence.tsx @@ -6,39 +6,44 @@ */ import * as React from 'react' -import { StructureSelection, StructureQuery } from '../../../mol-model/structure'; import { PluginUIComponent } from '../base'; import { Interactivity } from '../../util/interactivity'; import { MarkerAction } from '../../../mol-util/marker-action'; import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer'; import { ValueBox } from '../../../mol-util'; -import { createResidueQuery, markResidue, StructureSeq } from './util'; import { Residue } from './residue'; +import { SequenceWrapper } from './util'; -type BaseSequenceProps = { structureSeq: StructureSeq, markerArray: Uint8Array } -type BaseSequenceState = { markerData: ValueBox<Uint8Array> } +type SequenceProps = { sequenceWrapper: SequenceWrapper.Any } +type SequenceState = { markerData: ValueBox<Uint8Array> } + +function getState(markerData: ValueBox<Uint8Array>) { + return { markerData: ValueBox.withValue(markerData, markerData.value) } +} // TODO: this is really inefficient and should be done using a canvas. -export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSequenceState> { +export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, SequenceState> { state = { - markerData: ValueBox.create(new Uint8Array(this.props.markerArray)) + markerData: ValueBox.create(this.props.sequenceWrapper.markerArray) + } + + private setMarkerData(markerData: ValueBox<Uint8Array>) { + this.setState(getState(markerData)) } private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => { - const { markerData } = this.state; - const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action) - if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) }) + const changed = this.props.sequenceWrapper.markResidue(loci.loci, action) + if (changed) this.setMarkerData(this.state.markerData) } private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => { - const { markerData } = this.state; - const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action) - if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) }) + const changed = this.props.sequenceWrapper.markResidue(loci.loci, action) + if (changed) this.setMarkerData(this.state.markerData) } - static getDerivedStateFromProps(nextProps: BaseSequenceProps, prevState: BaseSequenceState): BaseSequenceState | null { - if (prevState.markerData.value !== nextProps.markerArray) { - return { markerData: ValueBox.create(nextProps.markerArray) } + static getDerivedStateFromProps(nextProps: SequenceProps, prevState: SequenceState): SequenceState | null { + if (prevState.markerData.value !== nextProps.sequenceWrapper.markerArray) { + return getState(ValueBox.create(nextProps.sequenceWrapper.markerArray)) } return null } @@ -53,16 +58,10 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque this.plugin.interactivity.lociSelections.removeProvider(this.lociSelectionProvider) } - getLoci(seqId: number) { - const { structure, seq } = this.props.structureSeq - const query = createResidueQuery(seq.entityId, seqId); - return StructureSelection.toLoci2(StructureQuery.run(query, structure)); - } - highlight(seqId?: number, modifiers?: ModifiersKeys) { const ev = { current: Interactivity.Loci.Empty, modifiers } if (seqId !== undefined) { - const loci = this.getLoci(seqId); + const loci = this.props.sequenceWrapper.getLoci(seqId); if (loci.elements.length > 0) ev.current = { loci }; } this.plugin.behaviors.interaction.highlight.next(ev) @@ -71,7 +70,7 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) { const ev = { current: Interactivity.Loci.Empty, buttons, modifiers } if (seqId !== undefined) { - const loci = this.getLoci(seqId); + const loci = this.props.sequenceWrapper.getLoci(seqId); if (loci.elements.length > 0) ev.current = { loci }; } this.plugin.behaviors.interaction.click.next(ev) @@ -89,8 +88,8 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque render() { const { markerData } = this.state; - const { seq } = this.props.structureSeq; - const { offset, sequence } = seq.sequence; + const { label } = this.props.sequenceWrapper + const { offset, sequence } = this.props.sequenceWrapper.sequence; const elems: JSX.Element[] = []; for (let i = 0, _i = sequence.length; i < _i; i++) { @@ -102,7 +101,7 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque onContextMenu={this.contextMenu} onMouseDown={this.mouseDown} > - <span style={{ fontWeight: 'bold' }}>{seq.entityId}:{offset} </span> + <span style={{ fontWeight: 'bold' }}>{label}:{offset} </span> {elems} </div>; } diff --git a/src/mol-plugin/ui/sequence/util.ts b/src/mol-plugin/ui/sequence/util.ts index 59ceb975157eeb060611b36ebfb30618a48ea653..68b4e8d567a4777bffd4e9f75c91c52538696783 100644 --- a/src/mol-plugin/ui/sequence/util.ts +++ b/src/mol-plugin/ui/sequence/util.ts @@ -1,79 +1,32 @@ /** - * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> - * @author David Sehnal <david.sehnal@gmail.com> */ -import { Structure, StructureSequence, Queries, StructureProperties as SP, StructureElement, Unit } from '../../../mol-model/structure'; -import { OrderedSet, Interval } from '../../../mol-data/int'; +import { Interval } from '../../../mol-data/int'; import { Loci } from '../../../mol-model/loci'; -import { applyMarkerAction, MarkerAction } from '../../../mol-util/marker-action'; +import { MarkerAction, applyMarkerAction } from '../../../mol-util/marker-action'; +import { StructureElement } from '../../../mol-model/structure'; +import { Sequence } from '../../../mol-model/sequence'; -export function createResidueQuery(entityId: string, label_seq_id: number) { - return Queries.generators.atoms({ - entityTest: ctx => { - return SP.entity.id(ctx.element) === entityId - }, - residueTest: ctx => { - if (ctx.element.unit.kind === Unit.Kind.Atomic) { - return SP.residue.label_seq_id(ctx.element) === label_seq_id - } else { - return ( - SP.coarse.seq_id_begin(ctx.element) <= label_seq_id && - SP.coarse.seq_id_end(ctx.element) >= label_seq_id - ) - } - } - }); -} - -/** Zero-indexed */ -export function getSeqIdInterval(location: StructureElement): Interval { - const { unit, element } = location - const { model } = unit - switch (unit.kind) { - case Unit.Kind.Atomic: - const residueIndex = model.atomicHierarchy.residueAtomSegments.index[element] - const seqId = model.atomicHierarchy.residues.label_seq_id.value(residueIndex) - return Interval.ofSingleton(seqId - 1) - case Unit.Kind.Spheres: - return Interval.ofRange( - model.coarseHierarchy.spheres.seq_id_begin.value(element) - 1, - model.coarseHierarchy.spheres.seq_id_end.value(element) - 1 - ) - case Unit.Kind.Gaussians: - return Interval.ofRange( - model.coarseHierarchy.gaussians.seq_id_begin.value(element) - 1, - model.coarseHierarchy.gaussians.seq_id_end.value(element) - 1 - ) - } -} +export { SequenceWrapper } -export type StructureSeq = { structure: Structure, seq: StructureSequence.Entity } +abstract class SequenceWrapper<D> { + label: string + data: D + markerArray: Uint8Array + sequence: Sequence + abstract eachResidue(loci: Loci, apply: (interval: Interval) => boolean): boolean + abstract getLoci(seqId: number): StructureElement.Loci -export function eachResidue(loci: Loci, structureSeq: StructureSeq, apply: (interval: Interval) => boolean) { - let changed = false - const { structure, seq } = structureSeq - if (!StructureElement.isLoci(loci)) return false - if (!Structure.areParentsEquivalent(loci.structure, structure)) return false - const l = StructureElement.create() - for (const e of loci.elements) { - l.unit = e.unit - OrderedSet.forEach(e.indices, v => { - l.element = e.unit.elements[v] - const entityId = SP.entity.id(l) - if (entityId === seq.entityId) { - if (apply(getSeqIdInterval(l))) changed = true - } + markResidue(loci: Loci, action: MarkerAction) { + return this.eachResidue(loci, (i: Interval) => { + return applyMarkerAction(this.markerArray, i, action) }) } - return changed } -export function markResidue(loci: Loci, structureSeq: StructureSeq, array: Uint8Array, action: MarkerAction) { - const { structure, seq } = structureSeq - return eachResidue(loci, { structure , seq }, (i: Interval) => { - return applyMarkerAction(array, i, action) - }) +namespace SequenceWrapper { + export type Any = SequenceWrapper<any> } \ No newline at end of file