Skip to content
Snippets Groups Projects
Commit b0954943 authored by Alexander Rose's avatar Alexander Rose
Browse files

added StructureElement.Stats and improved loci-labels

parent e1b7a5b2
No related branches found
No related tags found
No related merge requests found
......@@ -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
......@@ -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) { }
......
......@@ -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 }) => {
......
/**
* 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
......@@ -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 {
......
/**
* 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,19 +82,48 @@ 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)
label.push(atomicElementLabel(location as StructureElement<Unit.Atomic>, granularity))
} else if (Unit.isCoarse(location.unit)) {
label.push(coarseElementLabel(location as StructureElement<Unit.Spheres | Unit.Gaussians>, granularity))
} else {
label.push('Unknown')
}
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)
label = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}${alt_id ? `%${alt_id}` : ''}`
} else if (Unit.isCoarse(location.unit)) {
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)
......@@ -79,13 +131,8 @@ export function elementLabel(location: StructureElement) {
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}`
return `${comp_id} ${seq_id_begin}:${asym_id}`
} else {
label = `${seq_id_begin}-${seq_id_end}:${asym_id}`
return `${seq_id_begin}-${seq_id_end}:${asym_id}`
}
} else {
label = 'unknown'
}
return `${model} ${instance} ${label}`
}
\ No newline at end of file
......@@ -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)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment