浏览代码

added StructureElement.Stats and improved loci-labels

Alexander Rose 5 年之前
父节点
当前提交
b09549439f

+ 124 - 0
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

+ 2 - 2
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) { }

+ 7 - 3
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 }) => {

+ 3 - 2
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) }))
     }
 }

+ 6 - 13
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 {

+ 83 - 36
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}`
+    }
 }

+ 2 - 2
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)