-
Alexander Rose authoredAlexander Rose authored
sequence.tsx 10.44 KiB
/**
* 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 { Structure, StructureSequence, Queries, StructureSelection, StructureProperties as SP, StructureQuery, StructureElement, Unit } from '../../mol-model/structure';
import { PluginUIComponent } from './base';
import { StateTreeSpine } from '../../mol-state/tree/spine';
import { PluginStateObject as SO } from '../state/objects';
import { Interaction } from '../util/interaction';
import { OrderedSet, Interval } from '../../mol-data/int';
import { Loci } from '../../mol-model/loci';
import { applyMarkerAction, MarkerAction } from '../../mol-geo/geometry/marker-data';
import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../mol-util/input/input-observer';
function getStructureSeqKey(structureSeq: StructureSeq) {
const { structure, seq } = structureSeq
const strucHash = structure.parent ? structure.parent.hashCode : structure.hashCode
return `${strucHash}|${seq.entityId}`
}
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);
this.subscribe(this.plugin.state.behavior.currentObject, o => {
const current = this.plugin.state.dataState.cells.get(o.ref)!;
this.spine.current = current
this.forceUpdate();
});
this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
const current = this.spine.current;
if (!current || current.sourceRef !== ref || current.state !== state) return;
this.forceUpdate();
});
}
private getStructure() {
const so = this.spine && this.spine.getRootOfType(SO.Molecule.Structure)
return so && so.data
}
render() {
const structure = this.getStructure();
if (!structure) return <div className='msp-sequence'>
<div className='msp-sequence-entity'>No structure available</div>
</div>;
const seqs = structure.models[0].sequence.sequences;
return <div className='msp-sequence'>
{seqs.map((seq, i) => {
const structureSeq = { structure, seq }
const key = getStructureSeqKey(structureSeq)
let markerArray = this.markerArrays.get(key)
if (!markerArray) {
markerArray = new Uint8Array(seq.sequence.sequence.length)
this.markerArrays.set(key, markerArray)
}
return <EntitySequence key={i} structureSeq={structureSeq} markerArray={markerArray} />
})}
</div>;
}
}
function createQuery(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
)
}
}
});
}
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)
case Unit.Kind.Spheres:
return Interval.ofRange(
model.coarseHierarchy.spheres.seq_id_begin.value(element),
model.coarseHierarchy.spheres.seq_id_end.value(element)
)
case Unit.Kind.Gaussians:
return Interval.ofRange(
model.coarseHierarchy.gaussians.seq_id_begin.value(element),
model.coarseHierarchy.gaussians.seq_id_end.value(element)
)
}
}
type StructureSeq = { structure: Structure, seq: StructureSequence.Entity }
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
}
})
}
return changed
}
function markResidue(loci: Loci, structureSeq: StructureSeq, array: Uint8Array, action: MarkerAction) {
const { structure, seq } = structureSeq
return eachResidue(loci, { structure , seq }, (i: Interval) => {
let changed = false
OrderedSet.forEach(i, (v: number) => {
const start = Interval.start(i) - 1
const end = Interval.end(i) - 1
if (applyMarkerAction(array, start, end, action)) changed = true
})
return changed
})
}
type EntitySequenceProps = { structureSeq: StructureSeq, markerArray: Uint8Array }
type EntitySequenceState = { markerData: { array: Uint8Array } }
// TODO: this is really inefficient and should be done using a canvas.
class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequenceState> {
state = {
markerData: { array: new Uint8Array(this.props.markerArray) }
}
private lociHighlightProvider = (loci: Interaction.Loci, action: MarkerAction) => {
const { array } = this.state.markerData;
const { structureSeq } = this.props
const changed = markResidue(loci.loci, structureSeq, array, action)
if (changed) this.setState({ markerData: { array } })
}
private lociSelectionProvider = (loci: Interaction.Loci, action: MarkerAction) => {
const { array } = this.state.markerData;
const { structureSeq } = this.props
const changed = markResidue(loci.loci, structureSeq, array, action)
if (changed) this.setState({ markerData: { array } })
}
static getDerivedStateFromProps(nextProps: EntitySequenceProps, prevState: EntitySequenceState) {
if (prevState.markerData.array !== nextProps.markerArray) {
return { markerData: { array: nextProps.markerArray } }
}
return null
}
componentDidMount() {
this.plugin.lociHighlights.addProvider(this.lociHighlightProvider)
this.plugin.lociSelections.addProvider(this.lociSelectionProvider)
}
componentWillUnmount() {
this.plugin.lociHighlights.removeProvider(this.lociHighlightProvider)
this.plugin.lociSelections.removeProvider(this.lociSelectionProvider)
}
getLoci(seqId: number) {
const { structure, seq } = this.props.structureSeq
const query = createQuery(seq.entityId, seqId);
return StructureSelection.toLoci2(StructureQuery.run(query, structure));
}
highlight(seqId?: number, modifiers?: ModifiersKeys) {
const ev = { current: Interaction.Loci.Empty, modifiers }
if (seqId !== undefined) {
const loci = this.getLoci(seqId);
if (loci.elements.length > 0) ev.current = { loci };
}
this.plugin.behaviors.interaction.highlight.next(ev)
}
click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
const ev = { current: Interaction.Loci.Empty, buttons, modifiers }
if (seqId !== undefined) {
const loci = this.getLoci(seqId);
if (loci.elements.length > 0) ev.current = { loci };
}
this.plugin.behaviors.interaction.click.next(ev)
}
contextMenu = (e: React.MouseEvent) => {
e.preventDefault()
}
mouseDown = (e: React.MouseEvent) => {
const buttons = getButtons(e.nativeEvent)
const modifiers = getModifiers(e.nativeEvent)
this.click(undefined, buttons, modifiers);
}
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.array[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>;
}
}
class Residue extends PluginUIComponent<{ seqId: number, letter: string, parent: EntitySequence, marker: number }> {
mouseEnter = (e: React.MouseEvent) => {
const modifiers = getModifiers(e.nativeEvent)
this.props.parent.highlight(this.props.seqId, modifiers);
}
mouseLeave = () => {
this.props.parent.highlight();
}
mouseDown = (e: React.MouseEvent) => {
const buttons = getButtons(e.nativeEvent)
const modifiers = getModifiers(e.nativeEvent)
this.props.parent.click(this.props.seqId, buttons, modifiers);
e.stopPropagation() // so that `parent.mouseDown` is not called
}
getBackgroundColor() {
// TODO make marker color configurable
if (this.props.marker === 0) return ''
if (this.props.marker % 2 === 0) return 'rgb(51, 255, 25)' // selected
if (this.props.marker === undefined) console.error('unexpected marker value')
return 'rgb(255, 102, 153)' // highlighted
}
render() {
return <span
onMouseEnter={this.mouseEnter}
onMouseLeave={this.mouseLeave}
onMouseDown={this.mouseDown}
style={{ backgroundColor: this.getBackgroundColor() }}>
{this.props.letter}
</span>;
}
}