diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 8ac7af0c113873bfc35d6fae413435400d17e115..2630573e46291f6f11c7ddc05807f52c2afdff48 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -139,6 +139,8 @@ namespace StructureElement { } export function remap(loci: Loci, structure: Structure): Loci { + if (structure === loci.structure) return loci + return Loci(structure, loci.elements.map(e => ({ unit: structure.unitMap.get(e.unit.id)!, indices: e.indices diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx index caab759f3f073d6d9fe30b9ff0077228ca5a299a..82cbdd76bee45b604aba4ba7e1903a80ff289975 100644 --- a/src/mol-plugin/ui/sequence.tsx +++ b/src/mol-plugin/ui/sequence.tsx @@ -11,12 +11,13 @@ import { StateTreeSpine } from '../../mol-state/tree/spine'; import { PluginStateObject as SO } from '../state/objects'; import { Sequence } from './sequence/sequence'; import { Structure, StructureElement, StructureProperties as SP } from '../../mol-model/structure'; -import { SequenceWrapper } from './sequence/util'; +import { SequenceWrapper } from './sequence/wrapper'; import { PolymerSequenceWrapper } from './sequence/polymer'; import { StructureElementSelectionManager } from '../util/structure-element-selection'; import { MarkerAction } from '../../mol-util/marker-action'; import { ParameterControls } from './controls/parameters'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { HeteroSequenceWrapper } from './sequence/hetero'; function opKey(l: StructureElement) { const ids = SP.unit.pdbx_struct_oper_list_ids(l) @@ -27,19 +28,16 @@ function opKey(l: StructureElement) { } function getSequenceWrapper(state: SequenceViewState, structureSelection: StructureElementSelectionManager): SequenceWrapper.Any | undefined { - const { structure, entity, chain, operator } = state + const { structure, entityId, invariantUnitId, operatorKey } = state const l = StructureElement.create() - for (let i = 0, il = structure.units.length; i < il; ++i) { - const unit = structure.units[i] - if (unit.polymerElements.length === 0) continue - + for (const unit of structure.units) { StructureElement.set(l, unit, unit.elements[0]) - if (SP.entity.id(l) !== entity) continue - if (SP.chain.label_asym_id(l) !== chain) continue - if (opKey(l) !== operator) continue + if (SP.entity.id(l) !== entityId) continue + if (unit.invariantId !== invariantUnitId) continue + if (opKey(l) !== operatorKey) continue - // console.log('new PolymerSequenceWrapper', structureSelection.get(structure)) - const sw = new PolymerSequenceWrapper({ structure, unit }) + const Wrapper = unit.polymerElements.length ? PolymerSequenceWrapper : HeteroSequenceWrapper + const sw = new Wrapper({ structure, unit }) sw.markResidue(structureSelection.get(structure), MarkerAction.Select) return sw } @@ -50,74 +48,76 @@ function getEntityOptions(structure: Structure) { const l = StructureElement.create() const seen = new Set<string>() - structure.units.forEach(unit => { - if (unit.polymerElements.length === 0) return - + for (const unit of structure.units) { StructureElement.set(l, unit, unit.elements[0]) const id = SP.entity.id(l) - if (seen.has(id)) return + if (seen.has(id)) continue const label = `${id}: ${SP.entity.pdbx_description(l).join(', ')}` options.push([ id, label ]) seen.add(id) - }) + } if (options.length === 0) options.push(['', 'No entities']) return options } function getChainOptions(structure: Structure, entityId: string) { - const options: [string, string][] = [] + const options: [number, string][] = [] const l = StructureElement.create() - const seen = new Set<string>() - - structure.units.forEach(unit => { - if (unit.polymerElements.length === 0) return + const seen = new Set<number>() + const water = new Map<string, number>() + for (const unit of structure.units) { StructureElement.set(l, unit, unit.elements[0]) - if (SP.entity.id(l) !== entityId) return + if (SP.entity.id(l) !== entityId) continue + + const id = unit.invariantId + if (seen.has(id)) continue - const id = SP.chain.label_asym_id(l) - if (seen.has(id)) return + let label = `${SP.chain.label_asym_id(l)}: ${SP.chain.auth_asym_id(l)}` + if (SP.entity.type(l) === 'water') { + const count = water.get(label) || 1 + water.set(label, count + 1) + label += ` #${count}` + } - const label = `${id}: ${SP.chain.auth_asym_id(l)}` options.push([ id, label ]) seen.add(id) - }) + } - if (options.length === 0) options.push(['', 'No chains']) + if (options.length === 0) options.push([-1, 'No chains']) return options } -function getOperatorOptions(structure: Structure, entityId: string, label_asym_id: string) { +function getOperatorOptions(structure: Structure, entityId: string, invariantUnitId: number) { const options: [string, string][] = [] const l = StructureElement.create() const seen = new Set<string>() - structure.units.forEach(unit => { - if (unit.polymerElements.length === 0) return + for (const unit of structure.units) { StructureElement.set(l, unit, unit.elements[0]) - if (SP.entity.id(l) !== entityId) return - if (SP.chain.label_asym_id(l) !== label_asym_id) return + if (SP.entity.id(l) !== entityId) continue + if (unit.invariantId !== invariantUnitId) continue const id = opKey(l) - if (seen.has(id)) return + if (seen.has(id)) continue const label = unit.conformation.operator.name options.push([ id, label ]) seen.add(id) - }) + } if (options.length === 0) options.push(['', 'No operators']) return options } -type SequenceViewState = { structure: Structure, entity: string, chain: string, operator: string } +type SequenceViewState = { structure: Structure, entityId: string, invariantUnitId: number, operatorKey: string } export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> { private spine: StateTreeSpine.Impl - state = { structure: Structure.Empty, entity: '', chain: '', operator: '' } + state = { structure: Structure.Empty, entityId: '', invariantUnitId: -1, operatorKey: '' } constructor(props: {}, context?: any) { super(props, context); @@ -151,17 +151,17 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> { private getInitialState(): SequenceViewState { const structure = this.getStructure() - const entity = getEntityOptions(structure)[0][0] - const chain = getChainOptions(structure, entity)[0][0] - const operator = getOperatorOptions(structure, entity, chain)[0][0] - return { structure, entity, chain, operator } + const entityId = getEntityOptions(structure)[0][0] + const invariantUnitId = getChainOptions(structure, entityId)[0][0] + const operatorKey = getOperatorOptions(structure, entityId, invariantUnitId)[0][0] + return { structure, entityId, invariantUnitId, operatorKey } } private get params() { - const { structure, entity, chain } = this.state + const { structure, entityId, invariantUnitId } = this.state const entityOptions = getEntityOptions(structure) - const chainOptions = getChainOptions(structure, entity) - const operatorOptions = getOperatorOptions(structure, entity, chain) + const chainOptions = getChainOptions(structure, entityId) + const operatorOptions = getOperatorOptions(structure, entityId, invariantUnitId) return { entity: PD.Select(entityOptions[0][0], entityOptions), chain: PD.Select(chainOptions[0][0], chainOptions), @@ -169,20 +169,29 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> { } } + private get values(): PD.Values<SequenceView['params']> { + return { + entity: this.state.entityId, + chain: this.state.invariantUnitId, + operator: this.state.operatorKey + } + } + + // TODO try to use selected option from previous state private setParamProps = (p: { param: PD.Base<any>, name: string, value: any }) => { const state = { ...this.state } switch (p.name) { case 'entity': - state.entity = p.value - state.chain = getChainOptions(state.structure, state.entity)[0][0] - state.operator = getOperatorOptions(state.structure, state.entity, state.chain)[0][0] + state.entityId = p.value + state.invariantUnitId = getChainOptions(state.structure, state.entityId)[0][0] + state.operatorKey = getOperatorOptions(state.structure, state.entityId, state.invariantUnitId)[0][0] break case 'chain': - state.chain = p.value - state.operator = getOperatorOptions(state.structure, state.entity, state.chain)[0][0] + state.invariantUnitId = p.value + state.operatorKey = getOperatorOptions(state.structure, state.entityId, state.invariantUnitId)[0][0] break case 'operator': - state.operator = p.value + state.operatorKey = p.value break } this.setState(state) @@ -196,7 +205,7 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> { const sequenceWrapper = this.getSequenceWrapper() return <div className='msp-sequence'> <div className='msp-sequence-select'> - <ParameterControls params={this.params} values={this.state} onChange={this.setParamProps} /> + <ParameterControls params={this.params} values={this.values} onChange={this.setParamProps} /> </div> {sequenceWrapper !== undefined ? <Sequence sequenceWrapper={sequenceWrapper} /> diff --git a/src/mol-plugin/ui/sequence/hetero.ts b/src/mol-plugin/ui/sequence/hetero.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0e7d4d726e30cc3fe7c37ab00c3b38aab21cb62 --- /dev/null +++ b/src/mol-plugin/ui/sequence/hetero.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Structure, StructureElement, ResidueIndex } from '../../../mol-model/structure'; +import { SequenceWrapper, StructureUnit } from './wrapper'; +import { OrderedSet, Segmentation, Interval, SortedArray } from '../../../mol-data/int'; +import { Loci } from '../../../mol-model/loci'; +import { ColorNames } from '../../../mol-util/color/tables'; + +export class HeteroSequenceWrapper extends SequenceWrapper<StructureUnit> { + private readonly sequence: string[] + private readonly sequenceIndices: Map<ResidueIndex, number> + private readonly residueIndices: Map<number, ResidueIndex> + + residueLabel(seqIdx: number) { + return this.sequence[seqIdx] + } + residueColor(seqIdx: number) { + return ColorNames.black + } + + eachResidue(loci: Loci, apply: (set: OrderedSet) => boolean) { + let changed = false + const { structure, unit } = this.data + if (StructureElement.isLoci(loci)) { + if (!Structure.areParentsEqual(loci.structure, structure)) return false + + for (const e of loci.elements) { + if (e.unit.id === unit.id) { + const { index: residueIndex } = e.unit.model.atomicHierarchy.residueAtomSegments + OrderedSet.forEach(e.indices, v => { + const seqIdx = this.sequenceIndices.get(residueIndex[unit.elements[v]]) + if (seqIdx !== undefined && apply(Interval.ofSingleton(seqIdx))) changed = true + }) + } + } + } else if (Structure.isLoci(loci)) { + if (!Structure.areParentsEqual(loci.structure, structure)) return false + + if (apply(Interval.ofBounds(0, this.length))) changed = true + } + return changed + } + + getLoci(seqIdx: number) { + const elements: StructureElement.Loci['elements'][0][] = [] + const rI = this.residueIndices.get(seqIdx) + if (rI !== undefined) { + const { unit } = this.data + const { offsets } = unit.model.atomicHierarchy.residueAtomSegments + const start = SortedArray.findPredecessorIndex(unit.elements, offsets[rI]) + const end = SortedArray.findPredecessorIndex(unit.elements, offsets[rI + 1]) + elements.push({ unit, indices: Interval.ofBounds(start, end) }) + } + return StructureElement.Loci(this.data.structure, elements) + } + + constructor(data: StructureUnit) { + const sequence: string[] = [] + const sequenceIndices = new Map<ResidueIndex, number>() + const residueIndices = new Map<number, ResidueIndex>() + + const residueIt = Segmentation.transientSegments(data.unit.model.atomicHierarchy.residueAtomSegments, data.unit.elements) + while (residueIt.hasNext) { + const { index } = residueIt.move() + sequenceIndices.set(index, sequence.length) + residueIndices.set(sequence.length, index) + sequence.push(data.unit.model.atomicHierarchy.residues.label_comp_id.value(index)) + } + + const length = sequence.length + const markerArray = new Uint8Array(length) + + super(data, markerArray, length) + + this.sequence = sequence + this.sequenceIndices = sequenceIndices + this.residueIndices = residueIndices + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/sequence/polymer.ts b/src/mol-plugin/ui/sequence/polymer.ts index 6a63b0bdf154306212e56a539a678df26a337b93..b86f783defd3b14cbc4d22314d297a667cd93db5 100644 --- a/src/mol-plugin/ui/sequence/polymer.ts +++ b/src/mol-plugin/ui/sequence/polymer.ts @@ -5,15 +5,13 @@ */ import { StructureSelection, StructureQuery, Structure, Queries, StructureProperties as SP, StructureElement, Unit, ElementIndex } from '../../../mol-model/structure'; -import { SequenceWrapper } from './util'; +import { SequenceWrapper, StructureUnit } from './wrapper'; import { OrderedSet, Interval, SortedArray } from '../../../mol-data/int'; import { Loci } from '../../../mol-model/loci'; import { Sequence } from '../../../mol-model/sequence'; import { MissingResidues } from '../../../mol-model/structure/model/properties/common'; import { ColorNames } from '../../../mol-util/color/tables'; -export type StructureUnit = { structure: Structure, unit: Unit } - export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { private readonly sequence: Sequence private readonly missing: MissingResidues @@ -25,6 +23,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { seqId(seqIdx: number) { return this.sequence.offset + seqIdx + 1 } + residueLabel(seqIdx: number) { return this.sequence.sequence[seqIdx] } @@ -55,8 +54,8 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { return changed } - getLoci(seqId: number) { - const query = createResidueQuery(this.data.unit.id, seqId); + getLoci(seqIdx: number) { + const query = createResidueQuery(this.data.unit.id, this.seqId(seqIdx)); return StructureSelection.toLoci2(StructureQuery.run(query, this.data.structure)); } @@ -66,7 +65,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { const length = sequence.sequence.length const markerArray = new Uint8Array(length) - super(data, markerArray, sequence.sequence.length) + super(data, markerArray, length) this.sequence = sequence this.missing = data.unit.model.properties.missingResidues diff --git a/src/mol-plugin/ui/sequence/residue.tsx b/src/mol-plugin/ui/sequence/residue.tsx index 1d543db7742ee63acff131146ba66aa4b134d9ba..6e9d19a262f3032c3aae454a61dcbc510c55b48f 100644 --- a/src/mol-plugin/ui/sequence/residue.tsx +++ b/src/mol-plugin/ui/sequence/residue.tsx @@ -11,11 +11,11 @@ import { getButtons, getModifiers } from '../../../mol-util/input/input-observer import { Sequence } from './sequence'; import { Color } from '../../../mol-util/color'; -export class Residue extends PurePluginUIComponent<{ seqId: number, label: string, parent: Sequence<any>, marker: number, color: Color }> { +export class Residue extends PurePluginUIComponent<{ seqIdx: number, label: string, parent: Sequence<any>, marker: number, color: Color }> { mouseEnter = (e: React.MouseEvent) => { const modifiers = getModifiers(e.nativeEvent) - this.props.parent.highlight(this.props.seqId, modifiers); + this.props.parent.highlight(this.props.seqIdx, modifiers); } mouseLeave = () => { @@ -25,11 +25,11 @@ export class Residue extends PurePluginUIComponent<{ seqId: number, label: strin mouseDown = (e: React.MouseEvent) => { const buttons = getButtons(e.nativeEvent) const modifiers = getModifiers(e.nativeEvent) - this.props.parent.click(this.props.seqId, buttons, modifiers); + this.props.parent.click(this.props.seqIdx, buttons, modifiers); e.stopPropagation() // so that `parent.mouseDown` is not called } - getBackgroundColor() { + get backgroundColor() { // TODO make marker color configurable if (this.props.marker === 0) return '' if (this.props.marker % 2 === 0) return 'rgb(51, 255, 25)' // selected @@ -37,6 +37,12 @@ export class Residue extends PurePluginUIComponent<{ seqId: number, label: strin return 'rgb(255, 102, 153)' // highlighted } + get margin() { + return this.props.label.length > 1 && this.props.seqIdx + ? `0px 0px 0px 4px` + : undefined + } + render() { return <span onMouseEnter={this.mouseEnter} @@ -44,7 +50,8 @@ export class Residue extends PurePluginUIComponent<{ seqId: number, label: strin onMouseDown={this.mouseDown} style={{ color: Color.toStyle(this.props.color), - backgroundColor: this.getBackgroundColor() + backgroundColor: this.backgroundColor, + margin: this.margin }}> {this.props.label} </span>; diff --git a/src/mol-plugin/ui/sequence/sequence.tsx b/src/mol-plugin/ui/sequence/sequence.tsx index 913cbd9e2c2596a22d88be481c7740339604c468..2d1528bc11a77d88016310b099a5f5f19a229b55 100644 --- a/src/mol-plugin/ui/sequence/sequence.tsx +++ b/src/mol-plugin/ui/sequence/sequence.tsx @@ -12,7 +12,7 @@ import { MarkerAction } from '../../../mol-util/marker-action'; import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer'; import { ValueBox } from '../../../mol-util'; import { Residue } from './residue'; -import { SequenceWrapper } from './util'; +import { SequenceWrapper } from './wrapper'; type SequenceProps = { sequenceWrapper: SequenceWrapper.Any } type SequenceState = { markerData: ValueBox<Uint8Array> } @@ -93,7 +93,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, Sequ const elems: JSX.Element[] = []; for (let i = 0, il = sw.length; i < il; ++i) { elems[elems.length] = <Residue - seqId={sw.seqId(i)} + seqIdx={i} label={sw.residueLabel(i)} parent={this} marker={markerData.value[i]} diff --git a/src/mol-plugin/ui/sequence/util.ts b/src/mol-plugin/ui/sequence/wrapper.ts similarity index 83% rename from src/mol-plugin/ui/sequence/util.ts rename to src/mol-plugin/ui/sequence/wrapper.ts index 158b4729468f2ea7c51c843e391a348c141a154f..275b315a143ff5153e3ceb85597d867b85eebc0e 100644 --- a/src/mol-plugin/ui/sequence/util.ts +++ b/src/mol-plugin/ui/sequence/wrapper.ts @@ -7,18 +7,19 @@ import { OrderedSet } from '../../../mol-data/int'; import { Loci } from '../../../mol-model/loci'; import { MarkerAction, applyMarkerAction } from '../../../mol-util/marker-action'; -import { StructureElement } from '../../../mol-model/structure'; +import { StructureElement, Structure, Unit } from '../../../mol-model/structure'; import { Color } from '../../../mol-util/color'; +export type StructureUnit = { structure: Structure, unit: Unit } + export { SequenceWrapper } abstract class SequenceWrapper<D> { - abstract seqId(seqIdx: number): number abstract residueLabel(seqIdx: number): string abstract residueColor(seqIdx: number): Color abstract eachResidue(loci: Loci, apply: (set: OrderedSet) => boolean): boolean - abstract getLoci(seqId: number): StructureElement.Loci + abstract getLoci(seqIdx: number): StructureElement.Loci markResidue(loci: Loci, action: MarkerAction) { return this.eachResidue(loci, (set: OrderedSet) => { diff --git a/src/mol-plugin/util/interactivity.ts b/src/mol-plugin/util/interactivity.ts index dac8ca4bcb38543830e3795a29e2c304a303798a..87031ce0e1c0f76fe4f3189267579a5fd662ba1e 100644 --- a/src/mol-plugin/util/interactivity.ts +++ b/src/mol-plugin/util/interactivity.ts @@ -98,9 +98,11 @@ namespace Interactivity { } loci = Granularity[this.props.granularity](loci) if (Structure.isLoci(loci)) { - loci = Structure.toStructureElementLoci(loci) + // convert to StructureElement.Loci of root structure + loci = Structure.toStructureElementLoci(Structure.Loci(loci.structure.parent || loci.structure)) } if (StructureElement.isLoci(loci) && loci.structure.parent) { + // ensure the root structure is used loci = StructureElement.Loci.remap(loci, loci.structure.parent) } return { loci, repr }