From e301eca9c20585cccf5af02482956ea22090a1ca Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Thu, 20 Jun 2019 15:45:47 -0700 Subject: [PATCH] wip, sequence view, select options --- .../skin/base/components/controls.scss | 54 +++--- .../skin/base/components/sequence.scss | 15 +- src/mol-plugin/skin/base/variables.scss | 3 +- src/mol-plugin/ui/sequence.tsx | 165 ++++++++++++++---- src/mol-plugin/ui/sequence/polymer.ts | 17 +- src/mol-plugin/ui/sequence/sequence.tsx | 2 +- 6 files changed, 176 insertions(+), 80 deletions(-) diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss index ef2dd86f5..2475e7b56 100644 --- a/src/mol-plugin/skin/base/components/controls.scss +++ b/src/mol-plugin/skin/base/components/controls.scss @@ -8,42 +8,42 @@ select, button, input[type=text] { @extend .msp-form-control; } - + button { @extend .msp-btn; @extend .msp-btn-block; } } -.msp-control-row { +.msp-control-row { position: relative; height: $row-height; background: $default-background; margin-top: 1px; - + > span { line-height: $row-height; display: block; width: $control-label-width + $control-spacing; text-align: right; - padding: 0 $control-spacing; + padding: 0 $control-spacing; color: color-lower-contrast($font-color, 15%); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - - @include non-selectable; + + @include non-selectable; } - + select, button, input[type=text] { @extend .msp-form-control; } - + button { @extend .msp-btn; @extend .msp-btn-block; } - + > div:nth-child(2) { background: $msp-form-control-background; position: absolute; @@ -58,21 +58,21 @@ position: relative; } -.msp-toggle-button { +.msp-toggle-button { .msp-icon { display: inline-block; margin-right: 6px; } - + > div > button:hover { border-color: color-increase-contrast($msp-form-control-background, 5%) !important; border: none; outline-offset: -1px !important; - outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important; + outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important; } } -.msp-slider { +.msp-slider { > div:first-child { position: absolute; top: 0; @@ -91,14 +91,14 @@ top: 0; bottom: 0; } - + input[type=text] { padding-right: 6px; padding-left: 4px; font-size: 80%; text-align: right; } - + // input[type=range] { // width: 100%; // } @@ -135,14 +135,14 @@ bottom: 0; font-size: 80%; } - + input[type=text] { padding-right: 4px; padding-left: 4px; font-size: 80%; text-align: center; } - + // input[type=range] { // width: 100%; // } @@ -154,24 +154,24 @@ margin: 0; text-align: center; padding-right: $control-spacing; - padding-left: $control-spacing; - + padding-left: $control-spacing; + &:hover { border-color: color-increase-contrast($msp-form-control-background, 5%) !important; border: none; outline-offset: -1px !important; - outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important; + outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important; } } - + .msp-color-picker { position: absolute; z-index: 100000; background: $default-background; border-top: 1px solid $default-background; padding-bottom: $control-spacing / 2; - width: 100%; - + width: 100%; + // input[type=text] { // background: $msp-form-control-background !important; // } @@ -217,7 +217,7 @@ height: 2 * $row-height / 3 !important; line-height: 2 * $row-height / 3 !important; font-size: 70% !important; - background: $default-background !important; + background: $default-background !important; color: color-lower-contrast($font-color, 15%) !important; } } @@ -231,13 +231,13 @@ .msp-control-subgroup { margin-top: 1px; - + .msp-control-row { margin-left: $control-spacing !important; > span { width: $control-label-width !important; } - + > div:nth-child(2) { left: $control-label-width !important; } @@ -254,7 +254,7 @@ width: $control-label-width + $control-spacing; text-align: left; background: transparent; - + .msp-icon { line-height: $row-height - 3; width: $row-height - 1; diff --git a/src/mol-plugin/skin/base/components/sequence.scss b/src/mol-plugin/skin/base/components/sequence.scss index c4e59e97d..31ae7767d 100644 --- a/src/mol-plugin/skin/base/components/sequence.scss +++ b/src/mol-plugin/skin/base/components/sequence.scss @@ -4,19 +4,26 @@ top: 0; left: 0; bottom: 0; - overflow-y: scroll; - overflow-x: hidden; font-size: 90%; background: $sequence-background; } -.msp-sequence-entity { +.msp-sequence-select { + float: left; + width: $sequence-select-width; +} + +.msp-sequence-wrapper { word-break: break-word; padding: $info-vertical-padding $control-spacing $info-vertical-padding $control-spacing; user-select: none; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + font-size: 90%; } -.msp-sequence-entity { +.msp-sequence-wrapper { span { cursor: pointer; } diff --git a/src/mol-plugin/skin/base/variables.scss b/src/mol-plugin/skin/base/variables.scss index ba76987f7..d030806a5 100644 --- a/src/mol-plugin/skin/base/variables.scss +++ b/src/mol-plugin/skin/base/variables.scss @@ -78,4 +78,5 @@ $entity-color-partialy-visible: color-lower-contrast($font-color, 33%); $entity-tag-color: color-lower-contrast($font-color, 20%); // sequence -$sequence-background: $default-background; \ No newline at end of file +$sequence-background: $default-background; +$sequence-select-width: 300px; \ No newline at end of file diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx index b7915b21b..b6855b9f5 100644 --- a/src/mol-plugin/ui/sequence.tsx +++ b/src/mol-plugin/ui/sequence.tsx @@ -18,92 +18,183 @@ import { MarkerAction } from '../../mol-util/marker-action'; import { ParameterControls } from './controls/parameters'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; -function getSequenceWrapperForStructure(index: number, structure: Structure, structureSelection: StructureElementSelectionManager): SequenceWrapper.Any | undefined { - let j = 0 +function opKey(ids: string[]) { + return ids.sort().join(',') +} + +function getSequenceWrapper(state: SequenceViewState, structureSelection: StructureElementSelectionManager): SequenceWrapper.Any | undefined { + const { structure, entity, chain, operator } = 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 - if (j === index) { - const sw = new PolymerSequenceWrapper({ structure, unit }) - sw.markResidue(structureSelection.get(structure), MarkerAction.Select) - return sw - } - j += 1 + + 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(SP.unit.pdbx_struct_oper_list_ids(l)) !== operator) continue + + // console.log('new PolymerSequenceWrapper', structureSelection.get(structure)) + const sw = new PolymerSequenceWrapper({ structure, unit }) + sw.markResidue(structureSelection.get(structure), MarkerAction.Select) + return sw } } -function getPolymerOptionsForStructure(structure: Structure) { - const options: [number, string][] = [] +function getEntityOptions(structure: Structure) { + const options: [string, string][] = [] + const l = StructureElement.create() + const seen = new Set<string>() + + structure.units.forEach(unit => { + if (unit.polymerElements.length === 0) return + + StructureElement.set(l, unit, unit.elements[0]) + const id = SP.entity.id(l) + if (seen.has(id)) return + + 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 l = StructureElement.create() + const seen = new Set<string>() - let i = 0 structure.units.forEach(unit => { if (unit.polymerElements.length === 0) return - const l = StructureElement.create(unit, unit.elements[0]) - const entityDescription = SP.entity.pdbx_description(l) - const label_asym_id = SP.chain.label_asym_id(l) - const label = `${label_asym_id}: ${entityDescription}` + StructureElement.set(l, unit, unit.elements[0]) + if (SP.entity.id(l) !== entityId) return + + const id = SP.chain.label_asym_id(l) + if (seen.has(id)) return - options.push([ i, label ]) - i += 1 + const label = `${id}: ${SP.chain.auth_asym_id(l)}` + options.push([ id, label ]) + seen.add(id) }) + if (options.length === 0) options.push(['', 'No chains']) return options } -export class SequenceView extends PluginUIComponent<{ }, { polymer: number }> { +function getOperatorOptions(structure: Structure, entityId: string, label_asym_id: string) { + const options: [string, string][] = [] + const l = StructureElement.create() + const seen = new Set<string>() + + structure.units.forEach(unit => { + if (unit.polymerElements.length === 0) return + 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 + + const id = opKey(SP.unit.pdbx_struct_oper_list_ids(l)) + if (seen.has(id)) return + + const label = `${SP.unit.pdbx_struct_oper_list_ids(l).join(', ')}` + 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 } + +export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> { private spine: StateTreeSpine.Impl - state = { polymer: 0 } + state = { structure: Structure.Empty, entity: '', chain: '', operator: '' } - componentDidMount() { + constructor(props: {}, context?: any) { + super(props, context); this.spine = new StateTreeSpine.Impl(this.plugin.state.dataState.cells); + } + componentDidMount() { 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.setState(this.getInitialState()) }); 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(); + this.setState(this.getInitialState()) }); } private getStructure() { - const so = this.spine && this.spine.getRootOfType(SO.Molecule.Structure) - return so && so.data + const so = this.spine.getRootOfType(SO.Molecule.Structure) + return (so && so.data) || Structure.Empty + } + + private getSequenceWrapper() { + return getSequenceWrapper(this.state, this.plugin.helpers.structureSelection) + } + + 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 } } - private getParams(structure: Structure) { + private get params() { + const { structure, entity, chain } = this.state + const entityOptions = getEntityOptions(structure) + const chainOptions = getChainOptions(structure, entity) + const operatorOptions = getOperatorOptions(structure, entity, chain) return { - polymer: PD.Select(0, getPolymerOptionsForStructure(structure)) + entity: PD.Select(entityOptions[0][0], entityOptions), + chain: PD.Select(chainOptions[0][0], chainOptions), + operator: PD.Select(operatorOptions[0][0], operatorOptions) } } private setParamProps = (p: { param: PD.Base<any>, name: string, value: any }) => { - if (p.name === 'polymer') { - this.setState({ polymer: p.value }) + 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] + break + case 'chain': + state.chain = p.value + state.operator = getOperatorOptions(state.structure, state.entity, state.chain)[0][0] + break + case 'operator': + state.operator = p.value + break } + this.setState(state) } render() { - const structure = this.getStructure(); - if (!structure) return <div className='msp-sequence'> - <div className='msp-sequence-entity'>No structure available</div> + if (this.state.structure === Structure.Empty) return <div className='msp-sequence'> + <div className='msp-sequence-wrapper'>No structure available</div> </div>; - const { structureSelection } = this.plugin.helpers - const params = this.getParams(structure) - const sequenceWrapper = getSequenceWrapperForStructure(this.state.polymer, structure, structureSelection) - + const sequenceWrapper = this.getSequenceWrapper() return <div className='msp-sequence'> - <ParameterControls params={params} values={this.state} onChange={this.setParamProps} /> + <div className='msp-sequence-select'> + <ParameterControls params={this.params} values={this.state} onChange={this.setParamProps} /> + </div> {sequenceWrapper !== undefined ? <Sequence sequenceWrapper={sequenceWrapper} /> - : <div className='msp-sequence-entity'>No sequence available</div>} + : <div className='msp-sequence-wrapper'>No sequence available</div>} </div>; } } \ 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 77516a239..077b21596 100644 --- a/src/mol-plugin/ui/sequence/polymer.ts +++ b/src/mol-plugin/ui/sequence/polymer.ts @@ -19,11 +19,11 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { eachResidue(loci: Loci, apply: (interval: Interval) => boolean) { let changed = false - const { structure } = this.data + const { structure, unit } = this.data if (!StructureElement.isLoci(loci)) return false if (!Structure.areParentsEquivalent(loci.structure, structure)) return false - const { location, entityId, label_asym_id } = this + const { location, label_asym_id } = this for (const e of loci.elements) { let rIprev = -1 location.unit = e.unit @@ -35,7 +35,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { 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.unit.id(location) !== unit.id) return if (SP.chain.label_asym_id(location) !== label_asym_id) return if (apply(getSeqIdInterval(location))) changed = true @@ -47,7 +47,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { } getLoci(seqId: number) { - const query = createResidueQuery(this.entityId, seqId, this.label_asym_id); + const query = createResidueQuery(this.data.unit.id, seqId); return StructureSelection.toLoci2(StructureQuery.run(query, this.data.structure)); } @@ -67,13 +67,10 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { } } -function createResidueQuery(entityId: string, label_seq_id: number, label_asym_id: string) { +function createResidueQuery(unitId: number, label_seq_id: number) { 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 + unitTest: ctx => { + return SP.unit.id(ctx.element) === unitId }, residueTest: ctx => { if (ctx.element.unit.kind === Unit.Kind.Atomic) { diff --git a/src/mol-plugin/ui/sequence/sequence.tsx b/src/mol-plugin/ui/sequence/sequence.tsx index 45a88b136..fcc6d1e7b 100644 --- a/src/mol-plugin/ui/sequence/sequence.tsx +++ b/src/mol-plugin/ui/sequence/sequence.tsx @@ -96,7 +96,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, Sequ } return <div - className='msp-sequence-entity' + className='msp-sequence-wrapper' onContextMenu={this.contextMenu} onMouseDown={this.mouseDown} > -- GitLab