diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 5e4809a5e9fae1ef367f66ab6a6f8e3a52fd9265..8fcb7e512d4b1cfd6cc7dee866ad8e68c2b81a53 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -38,6 +38,12 @@ namespace StructureElement { return a; } + export function copy(out: StructureElement, a: StructureElement): StructureElement { + out.unit = a.unit + out.element = a.element + return out + } + // TODO: when nominal types are available, make this indexed by UnitIndex export type Set = SortedArray<ElementIndex> @@ -483,6 +489,8 @@ namespace StructureElement { } } + // + interface QueryElement { /** * Array (sorted by first element in sub-array) of @@ -675,6 +683,122 @@ namespace StructureElement { return true } } + + // + + export interface Stats { + elementCount: number + residueCount: number + unitCount: number + + firstElementLoc: StructureElement + firstResidueLoc: StructureElement + firstUnitLoc: StructureElement + } + + export namespace Stats { + export function create(): Stats { + return { + elementCount: 0, + residueCount: 0, + unitCount: 0, + + firstElementLoc: StructureElement.create(), + firstResidueLoc: StructureElement.create(), + firstUnitLoc: StructureElement.create(), + } + } + + function handleElement(stats: Stats, element: StructureElement.Loci['elements'][0]) { + const { indices, unit } = element + const { elements } = unit + const size = OrderedSet.size(indices) + if (size === 1) { + stats.elementCount += 1 + if (stats.elementCount === 1) { + StructureElement.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)]) + } + } else if (size === elements.length) { + stats.unitCount += 1 + if (stats.unitCount === 1) { + StructureElement.set(stats.firstUnitLoc, unit, elements[OrderedSet.start(indices)]) + } + } else { + if (Unit.isAtomic(unit)) { + const { index, offsets } = unit.model.atomicHierarchy.residueAtomSegments + let i = 0 + while (i < size) { + const eI = elements[OrderedSet.getAt(indices, i)] + const rI = index[eI] + if (offsets[rI] !== eI) { + // partial residue, start missing + ++i + stats.elementCount += 1 + while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) { + ++i + stats.elementCount += 1 + } + } else { + ++i + while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) { + ++i + } + + if (offsets[rI + 1] - 1 === elements[OrderedSet.getAt(indices, i - 1)]) { + // full residue + stats.residueCount += 1 + if (stats.residueCount === 1) { + StructureElement.set(stats.firstResidueLoc, unit, elements[OrderedSet.start(indices)]) + } + } else { + // partial residue, end missing + stats.elementCount += offsets[rI + 1] - 1 - elements[OrderedSet.getAt(indices, i - 1)] + } + } + } + } else { + // TODO + stats.elementCount += size + if (stats.elementCount === 1) { + StructureElement.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)]) + } + } + } + } + + export function ofLoci(loci: StructureElement.Loci) { + const stats = create() + if (loci.elements.length > 0) { + for (const e of loci.elements) handleElement(stats, e) + } + return stats + } + + export function add(out: Stats, a: Stats, b: Stats) { + if (a.elementCount === 1 && b.elementCount === 0) { + StructureElement.copy(out.firstElementLoc, a.firstElementLoc) + } else if (a.elementCount === 0 && b.elementCount === 1) { + StructureElement.copy(out.firstElementLoc, b.firstElementLoc) + } + + if (a.residueCount === 1 && b.residueCount === 0) { + StructureElement.copy(out.firstResidueLoc, a.firstResidueLoc) + } else if (a.residueCount === 0 && b.residueCount === 1) { + StructureElement.copy(out.firstResidueLoc, b.firstResidueLoc) + } + + if (a.unitCount === 1 && b.unitCount === 0) { + StructureElement.copy(out.firstUnitLoc, a.firstUnitLoc) + } else if (a.unitCount === 0 && b.unitCount === 1) { + StructureElement.copy(out.firstUnitLoc, b.firstUnitLoc) + } + + out.elementCount = a.elementCount + b.elementCount + out.residueCount = a.residueCount + b.residueCount + out.unitCount = a.unitCount + b.unitCount + return out + } + } } export default StructureElement \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index 09fab185318486ab21feee5601ae390708520b67..d1e11929ac2284a6a5f8ee23db21157293bd5698 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -8,7 +8,7 @@ import { MarkerAction } from '../../../mol-util/marker-action'; import { PluginContext } from '../../../mol-plugin/context'; import { PluginStateObject as SO } from '../../state/objects'; -import { labelFirst } from '../../../mol-theme/label'; +import { lociLabel } from '../../../mol-theme/label'; import { PluginBehavior } from '../behavior'; import { Interactivity } from '../../util/interactivity'; import { StateTreeSpine } from '../../../mol-state/tree/spine'; @@ -70,7 +70,7 @@ export const DefaultLociLabelProvider = PluginBehavior.create({ name: 'default-loci-label-provider', category: 'interaction', ctor: class implements PluginBehavior<undefined> { - private f = labelFirst; + private f = lociLabel; register() { this.ctx.lociLabels.addProvider(this.f); } unregister() { this.ctx.lociLabels.removeProvider(this.f); } constructor(protected ctx: PluginContext) { } diff --git a/src/mol-plugin/ui/structure/selection.tsx b/src/mol-plugin/ui/structure/selection.tsx index 36ac44600da2ef4f57bb16736c9f8a587c35bfea..ae3df5a7ca139041c1fa3aa9caa42b016b72a5c0 100644 --- a/src/mol-plugin/ui/structure/selection.tsx +++ b/src/mol-plugin/ui/structure/selection.tsx @@ -6,8 +6,7 @@ import * as React from 'react'; import { PluginUIComponent } from '../base'; -import { formatStructureSelectionStats } from '../../util/structure-element-selection'; -import { StructureSelectionQueries } from '../../util/structure-selection-helper'; +import { StructureSelectionQueries, SelectionModifier } from '../../util/structure-selection-helper'; import { ButtonSelect, Options } from '../controls/common'; import { PluginCommands } from '../../command'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; @@ -33,7 +32,12 @@ export class StructureSelectionControls extends PluginUIComponent<{}, {}> { } get stats() { - return formatStructureSelectionStats(this.plugin.helpers.structureSelectionManager.stats) + const stats = this.plugin.helpers.structureSelectionManager.stats + if (stats.structureCount === 0 || stats.elementCount === 0) { + return 'Selected nothing' + } else { + return `Selected ${stats.label}` + } } setProps = (p: { param: PD.Base<any>, name: string, value: any }) => { diff --git a/src/mol-plugin/util/loci-label-manager.ts b/src/mol-plugin/util/loci-label-manager.ts index d6d6f53993749676adafc8e1a715800eb30c6b9b..a0b9f85729ea371251d4aa1e9d1a22b0b25ab27b 100644 --- a/src/mol-plugin/util/loci-label-manager.ts +++ b/src/mol-plugin/util/loci-label-manager.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { PluginContext } from '../../mol-plugin/context'; @@ -35,6 +36,6 @@ export class LociLabelManager { } constructor(public ctx: PluginContext) { - ctx.behaviors.interaction.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) })); + ctx.interactivity.lociHighlights.addProvider((loci) => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(loci) })) } } \ No newline at end of file diff --git a/src/mol-plugin/util/structure-element-selection.ts b/src/mol-plugin/util/structure-element-selection.ts index 8c60270ea2a54c8ec92819e17d670d0aede03c01..441b7c51c6fcb3b80819c4d8c818f0dda4d7b3a7 100644 --- a/src/mol-plugin/util/structure-element-selection.ts +++ b/src/mol-plugin/util/structure-element-selection.ts @@ -11,18 +11,7 @@ import { Structure, StructureElement } from '../../mol-model/structure'; import { StateObject } from '../../mol-state'; import { PluginContext } from '../context'; import { PluginStateObject } from '../state/objects'; - -export type StructureSelectionStats = { structureCount: number, elementCount: number } - -export function formatStructureSelectionStats(stats: StructureSelectionStats) { - if (stats.structureCount === 0 || stats.elementCount === 0) { - return 'Selected nothing' - } else if (stats.structureCount === 1) { - return `Selected ${stats.elementCount} elements` - } - return `Selected ${stats.elementCount} elements in ${stats.structureCount} structures` -} - +import { structureElementStatsLabel } from '../../mol-theme/label'; export { StructureElementSelectionManager }; class StructureElementSelectionManager { @@ -44,6 +33,7 @@ class StructureElementSelectionManager { get stats() { let structureCount = 0 let elementCount = 0 + const stats = StructureElement.Stats.create() this.entries.forEach(v => { const { elements } = v.selection @@ -52,10 +42,13 @@ class StructureElementSelectionManager { for (let i = 0, il = elements.length; i < il; ++i) { elementCount += OrderedSet.size(elements[i].indices) } + StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(v.selection)) } }) - return { structureCount, elementCount } + const label = structureElementStatsLabel(stats, true) + + return { structureCount, elementCount, label } } add(loci: Loci): Loci { diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts index 791bffec731ed57539863ada9df944bedca80937..3724e4106ee360c0a13956bbdb1b6a1c68d0a0e4 100644 --- a/src/mol-theme/label.ts +++ b/src/mol-theme/label.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * 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> @@ -18,18 +18,12 @@ function setElementLocation(loc: StructureElement, unit: Unit, index: StructureE loc.element = unit.elements[index] } -export function labelFirst(loci: Loci): string { +export function lociLabel(loci: Loci): string { switch (loci.kind) { case 'structure-loci': - return loci.structure.models.map(m => m.label).join(', ') + return loci.structure.models.map(m => m.entry).join(', ') case 'element-loci': - const e = loci.elements[0] - if (e) { - const el = e.unit.elements[OrderedSet.getAt(e.indices, 0)]; - return elementLabel(StructureElement.create(e.unit, el)) - } else { - return 'Unknown' - } + return structureElementStatsLabel(StructureElement.Stats.ofLoci(loci)) case 'link-loci': const link = loci.links[0] return link ? linkLabel(link) : 'Unknown' @@ -37,11 +31,7 @@ export function labelFirst(loci: Loci): string { return loci.shape.name case 'group-loci': const g = loci.groups[0] - if (g) { - return loci.shape.getLabel(OrderedSet.getAt(g.ids, 0), loci.instance) - } else { - return 'Unknown' - } + return g ? loci.shape.getLabel(OrderedSet.start(g.ids), loci.instance) : 'Unknown' case 'every-loci': return 'Everything' case 'empty-loci': @@ -51,6 +41,39 @@ export function labelFirst(loci: Loci): string { } } +function countLabel(count: number, label: string) { + return count === 1 ? `1 ${label}` : `${count} ${label}s` +} + +/** Gets residue count of the model chain segments the unit is a subset of */ +function getResidueCount(unit: Unit.Atomic) { + const { elements, model } = unit + const { chainAtomSegments, residueAtomSegments } = model.atomicHierarchy + const elementStart = chainAtomSegments.offsets[chainAtomSegments.index[elements[0]]] + const elementEnd = chainAtomSegments.offsets[chainAtomSegments.index[elements[elements.length - 1]] + 1] + return residueAtomSegments.index[elementEnd] - residueAtomSegments.index[elementStart] +} + +export function structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false) { + const { unitCount, residueCount, elementCount } = stats + + if (!countsOnly && elementCount === 1 && residueCount === 0 && unitCount === 0) { + return elementLabel(stats.firstElementLoc, 'element') + } else if (!countsOnly && elementCount === 0 && residueCount === 1 && unitCount === 0) { + return elementLabel(stats.firstResidueLoc, 'residue') + } else if (!countsOnly && elementCount === 0 && residueCount === 0 && unitCount === 1) { + const { unit } = stats.firstUnitLoc + const granularity = (Unit.isAtomic(unit) && getResidueCount(unit) === 1) ? 'residue' : 'chain' + return elementLabel(stats.firstUnitLoc, granularity) + } else { + const label: string[] = [] + if (unitCount > 0) label.push(countLabel(unitCount, 'Chain')) + if (residueCount > 0) label.push(countLabel(residueCount, 'Residue')) + if (elementCount > 0) label.push(countLabel(elementCount, 'Element')) + return label.join(', ') + } +} + export function linkLabel(link: Link.Location) { if (!elementLocA) elementLocA = StructureElement.create() if (!elementLocB) elementLocB = StructureElement.create() @@ -59,33 +82,57 @@ export function linkLabel(link: Link.Location) { return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}` } -export function elementLabel(location: StructureElement) { +export type LabelGranularity = 'element' | 'residue' | 'chain' | 'structure' + +export function elementLabel(location: StructureElement, granularity: LabelGranularity = 'element') { const model = location.unit.model.entry const instance = location.unit.conformation.operator.name - let label = '' + const label = [model, instance] if (Unit.isAtomic(location.unit)) { - const asym_id = Props.chain.auth_asym_id(location) - const seq_id = location.unit.model.atomicHierarchy.residues.auth_seq_id.isDefined ? Props.residue.auth_seq_id(location) : Props.residue.label_seq_id(location) - const comp_id = Props.residue.label_comp_id(location) - const atom_id = Props.atom.label_atom_id(location) - const alt_id = Props.atom.label_alt_id(location) - label = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}${alt_id ? `%${alt_id}` : ''}` + label.push(atomicElementLabel(location as StructureElement<Unit.Atomic>, granularity)) } else if (Unit.isCoarse(location.unit)) { - const asym_id = Props.coarse.asym_id(location) - const seq_id_begin = Props.coarse.seq_id_begin(location) - const seq_id_end = Props.coarse.seq_id_end(location) - if (seq_id_begin === seq_id_end) { - const entityIndex = Props.coarse.entityKey(location) - const seq = location.unit.model.sequence.byEntityKey[entityIndex] - const comp_id = seq.compId.value(seq_id_begin - 1) // 1-indexed - label = `[${comp_id}]${seq_id_begin}:${asym_id}` - } else { - label = `${seq_id_begin}-${seq_id_end}:${asym_id}` - } + label.push(coarseElementLabel(location as StructureElement<Unit.Spheres | Unit.Gaussians>, granularity)) } else { - label = 'unknown' + label.push('Unknown') } - return `${model} ${instance} ${label}` + return label.join(' | ') +} + +export function atomicElementLabel(location: StructureElement<Unit.Atomic>, granularity: LabelGranularity) { + const label_asym_id = Props.chain.label_asym_id(location) + const auth_asym_id = Props.chain.auth_asym_id(location) + const seq_id = location.unit.model.atomicHierarchy.residues.auth_seq_id.isDefined ? Props.residue.auth_seq_id(location) : Props.residue.label_seq_id(location) + const comp_id = Props.residue.label_comp_id(location) + const atom_id = Props.atom.label_atom_id(location) + const alt_id = Props.atom.label_alt_id(location) + + const label: string[] = [] + + switch (granularity) { + case 'element': + label.push(`${atom_id}${alt_id ? `%${alt_id}` : ''}`) + case 'residue': + label.push(`${comp_id} ${seq_id}`) + case 'chain': + label.push(`Chain ${label_asym_id}:${auth_asym_id}`) + } + + return label.reverse().join(' | ') +} + +export function coarseElementLabel(location: StructureElement<Unit.Spheres | Unit.Gaussians>, granularity: LabelGranularity) { + // TODO handle granularity + const asym_id = Props.coarse.asym_id(location) + const seq_id_begin = Props.coarse.seq_id_begin(location) + const seq_id_end = Props.coarse.seq_id_end(location) + if (seq_id_begin === seq_id_end) { + const entityIndex = Props.coarse.entityKey(location) + const seq = location.unit.model.sequence.byEntityKey[entityIndex] + const comp_id = seq.compId.value(seq_id_begin - 1) // 1-indexed + return `${comp_id} ${seq_id_begin}:${asym_id}` + } else { + return `${seq_id_begin}-${seq_id_end}:${asym_id}` + } } \ No newline at end of file diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index d06d257ce4dd1e1dd4a434015d84617df3948d20..ac2153ce5d55f3069be39ccb5cb08700833dc2d0 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -8,7 +8,7 @@ import './index.html' import { resizeCanvas } from '../../mol-canvas3d/util'; import { Representation } from '../../mol-repr/representation'; import { Canvas3D } from '../../mol-canvas3d/canvas3d'; -import { labelFirst } from '../../mol-theme/label'; +import { lociLabel } from '../../mol-theme/label'; import { MarkerAction } from '../../mol-util/marker-action'; import { EveryLoci } from '../../mol-model/loci'; import { RuntimeContext, Progress } from '../../mol-task'; @@ -45,7 +45,7 @@ canvas3d.input.move.subscribe(({x, y}) => { let label = '' if (pickingId) { const reprLoci = canvas3d.getLoci(pickingId) - label = labelFirst(reprLoci.loci) + label = lociLabel(reprLoci.loci) if (!Representation.Loci.areEqual(prevReprLoci, reprLoci)) { canvas3d.mark(prevReprLoci, MarkerAction.RemoveHighlight) canvas3d.mark(reprLoci, MarkerAction.Highlight)