ソースを参照

handle conformations in selections, marking, labels

Alexander Rose 5 年 前
コミット
9aa375a45f

+ 1 - 2
src/mol-data/int/_spec/ordered-set.spec.ts

@@ -1,12 +1,11 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import OrderedSet from '../ordered-set'
 import Interval from '../interval'
-//import SortedArray from '../sorted-array';
 
 describe('ordered set', () => {
     function ordSetToArray(set: OrderedSet) {

+ 34 - 5
src/mol-model/structure/structure/element/loci.ts

@@ -200,13 +200,30 @@ export namespace Loci {
         return false;
     }
 
-    export function extendToWholeResidues(loci: Loci): Loci {
+    /** Check if second loci is a subset of the first */
+    export function isSubset(xs: Loci, ys: Loci): boolean {
+        if (Loci.isEmpty(xs)) return Loci.isEmpty(ys);
+
+        const map = new Map<number, OrderedSet<UnitIndex>>();
+
+        for (const e of xs.elements) map.set(e.unit.id, e.indices);
+        for (const e of ys.elements) {
+            if (!map.has(e.unit.id)) continue;
+            if (!OrderedSet.isSubset(map.get(e.unit.id)!, e.indices)) return false;
+        }
+
+        return true;
+    }
+
+    export function extendToWholeResidues(loci: Loci, restrictToConformation?: boolean): Loci {
         const elements: Loci['elements'][0][] = [];
+        const residueAltIds = new Set<string>()
 
         for (const lociElement of loci.elements) {
             if (lociElement.unit.kind === Unit.Kind.Atomic) {
                 const unitElements = lociElement.unit.elements;
                 const h = lociElement.unit.model.atomicHierarchy;
+                const { label_alt_id } = lociElement.unit.model.atomicHierarchy.atoms;
 
                 const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
 
@@ -214,15 +231,26 @@ export namespace Loci {
                 const indices = lociElement.indices, len = OrderedSet.size(indices);
                 let i = 0;
                 while (i < len) {
-                    const rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
+                    residueAltIds.clear()
+                    const eI = unitElements[OrderedSet.getAt(indices, i)]
+                    const rI = residueIndex[eI];
+                    residueAltIds.add(label_alt_id.value(eI))
                     i++;
-                    while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
+                    while (i < len) {
+                        const eI = unitElements[OrderedSet.getAt(indices, i)]
+                        if (residueIndex[eI] !== rI) break;
+                        residueAltIds.add(label_alt_id.value(eI))
                         i++;
                     }
-
+                    const hasSharedAltId = residueAltIds.has('')
                     for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
                         const idx = OrderedSet.indexOf(unitElements, j);
-                        if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex;
+                        if (idx >= 0) {
+                            const altId = label_alt_id.value(j)
+                            if (!restrictToConformation || hasSharedAltId || !altId || residueAltIds.has(altId)) {
+                                newIndices[newIndices.length] = idx as UnitIndex;
+                            }
+                        }
                     }
                 }
 
@@ -244,6 +272,7 @@ export namespace Loci {
         }
     }
 
+    // take chainGroupId into account
     export function extendToWholeChains(loci: Loci): Loci {
         const elements: Loci['elements'][0][] = [];
 

+ 51 - 3
src/mol-model/structure/structure/element/stats.ts

@@ -9,13 +9,14 @@ import Unit from '../unit';
 import { Loci } from './loci';
 import { Location } from './location';
 
-
 export interface Stats {
     elementCount: number
+    conformationCount: number
     residueCount: number
     unitCount: number
 
     firstElementLoc: Location
+    firstConformationLoc: Location
     firstResidueLoc: Location
     firstUnitLoc: Location
 }
@@ -24,10 +25,12 @@ export namespace Stats {
     export function create(): Stats {
         return {
             elementCount: 0,
+            conformationCount: 0,
             residueCount: 0,
             unitCount: 0,
 
             firstElementLoc: Location.create(),
+            firstConformationLoc: Location.create(),
             firstResidueLoc: Location.create(),
             firstUnitLoc: Location.create(),
         }
@@ -38,6 +41,13 @@ export namespace Stats {
         const { elements } = unit
         const size = OrderedSet.size(indices)
 
+        const lociResidueAltIdCounts = new Map<string, number>()
+        const residueAltIdCounts = new Map<string, number>()
+        const addCount = (map: Map<string, number>, altId: string) => {
+            const count = map.get(altId) || 0
+            map.set(altId, count + 1)
+        }
+
         if (size > 0) {
             Location.set(stats.firstElementLoc, unit, elements[OrderedSet.start(indices)])
         }
@@ -55,12 +65,20 @@ export namespace Stats {
         } else {
             if (Unit.isAtomic(unit)) {
                 const { index, offsets } = unit.model.atomicHierarchy.residueAtomSegments
+                const { label_alt_id } = unit.model.atomicHierarchy.atoms;
                 let i = 0
                 while (i < size) {
+                    lociResidueAltIdCounts.clear()
                     let j = 0
                     const eI = elements[OrderedSet.getAt(indices, i)]
                     const rI = index[eI]
-                    while (i < size && index[elements[OrderedSet.getAt(indices, i)]] === rI) {
+                    addCount(lociResidueAltIdCounts, label_alt_id.value(eI))
+                    ++i
+                    ++j
+                    while (i < size) {
+                        const eI = elements[OrderedSet.getAt(indices, i)]
+                        if (index[eI] !== rI) break
+                        addCount(lociResidueAltIdCounts, label_alt_id.value(eI))
                         ++i
                         ++j
                     }
@@ -69,10 +87,32 @@ export namespace Stats {
                         // full residue
                         stats.residueCount += 1
                         if (stats.residueCount === 1) {
-                            Location.set(stats.firstResidueLoc, unit, elements[OrderedSet.start(indices)])
+                            Location.set(stats.firstResidueLoc, unit, elements[offsets[rI]])
                         }
                     } else {
                         // partial residue
+                        residueAltIdCounts.clear()
+                        for (let l = offsets[rI], _l = offsets[rI + 1]; l < _l; ++l) {
+                            addCount(residueAltIdCounts, label_alt_id.value(l))
+                        }
+                        // check if shared atom count match
+                        if (residueAltIdCounts.get('') === lociResidueAltIdCounts.get('')) {
+                            lociResidueAltIdCounts.forEach((v, k) => {
+                                if (residueAltIdCounts.get(k) !== v) return
+                                if (k !== '') {
+                                    stats.conformationCount += 1
+                                    if (stats.conformationCount === 1) {
+                                        for (let l = offsets[rI], _l = offsets[rI + 1]; l < _l; ++l) {
+                                            if (k === label_alt_id.value(l)) {
+                                                Location.set(stats.firstConformationLoc, unit, l)
+                                                break
+                                            }
+                                        }
+                                    }
+                                }
+                                j -= v
+                            })
+                        }
                         stats.elementCount += j
                     }
                 }
@@ -93,6 +133,7 @@ export namespace Stats {
         return stats
     }
 
+    /** Adds counts of two Stats objects together, assumes they describe different structures */
     export function add(out: Stats, a: Stats, b: Stats) {
         if (a.elementCount === 1 && b.elementCount === 0) {
             Location.copy(out.firstElementLoc, a.firstElementLoc)
@@ -100,6 +141,12 @@ export namespace Stats {
             Location.copy(out.firstElementLoc, b.firstElementLoc)
         }
 
+        if (a.conformationCount === 1 && b.conformationCount === 0) {
+            Location.copy(out.firstConformationLoc, a.firstConformationLoc)
+        } else if (a.conformationCount === 0 && b.conformationCount === 1) {
+            Location.copy(out.firstConformationLoc, b.firstConformationLoc)
+        }
+
         if (a.residueCount === 1 && b.residueCount === 0) {
             Location.copy(out.firstResidueLoc, a.firstResidueLoc)
         } else if (a.residueCount === 0 && b.residueCount === 1) {
@@ -113,6 +160,7 @@ export namespace Stats {
         }
 
         out.elementCount = a.elementCount + b.elementCount
+        out.conformationCount = a.conformationCount + b.conformationCount
         out.residueCount = a.residueCount + b.residueCount
         out.unitCount = a.unitCount + b.unitCount
         return out

+ 1 - 2
src/mol-plugin/ui/sequence.tsx

@@ -21,7 +21,6 @@ import { State, StateSelection } from '../../mol-state';
 import { ChainSequenceWrapper } from './sequence/chain';
 import { ElementSequenceWrapper } from './sequence/element';
 import { elementLabel } from '../../mol-theme/label';
-import { stripTags } from '../../mol-util/string';
 
 const MaxDisplaySequenceLength = 5000
 
@@ -125,7 +124,7 @@ function getUnitOptions(structure: Structure, modelEntityId: string) {
         // TODO handle special cases
         // - more than one chain in a unit
         // - chain spread over multiple units
-        let label = stripTags(elementLabel(l, 'chain', true))
+        let label = elementLabel(l, { granularity: 'chain', hidePrefix: true, htmlStyling: false })
         if (SP.entity.type(l) === 'water') {
             const count = water.get(label) || 1
             water.set(label, count + 1)

+ 2 - 1
src/mol-plugin/ui/structure/selection.tsx

@@ -12,6 +12,7 @@ import { PluginCommands } from '../../command';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Interactivity } from '../../util/interactivity';
 import { ParameterControls } from '../controls/parameters';
+import { stripTags } from '../../../mol-util/string';
 
 const StructureSelectionParams = {
     granularity: Interactivity.Params.granularity,
@@ -43,7 +44,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         if (stats.structureCount === 0 || stats.elementCount === 0) {
             return 'Selected nothing'
         } else {
-            return `Selected ${stats.label}`
+            return `Selected ${stripTags(stats.label)}`
         }
     }
 

+ 14 - 2
src/mol-plugin/util/interactivity.ts

@@ -54,7 +54,7 @@ namespace Interactivity {
 
     const Granularity = {
         'element': (loci: ModelLoci) => loci,
-        'residue': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeResidues(loci) : loci,
+        'residue': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeResidues(loci, true) : loci,
         'chain': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeChains(loci) : loci,
         'structure': (loci: ModelLoci) => SE.Loci.is(loci) ? Structure.Loci(loci.structure) : loci
     }
@@ -164,7 +164,7 @@ namespace Interactivity {
             if (StructureElement.Loci.is(normalized.loci)) {
                 this.toggleSel(normalized);
             } else {
-                this.mark(normalized, MarkerAction.Toggle);
+                super.mark(normalized, MarkerAction.Toggle);
             }
         }
 
@@ -210,6 +210,18 @@ namespace Interactivity {
             if (isEmptyLoci(current.loci)) this.deselectAll()
         }
 
+        protected mark(current: Loci<ModelLoci>, action: MarkerAction.Select | MarkerAction.Deselect) {
+            const { loci } = current
+            if (StructureElement.Loci.is(loci)) {
+                // do a full deselect/select so visuals that are marked with
+                // granularity unequal to 'element' are handled properly
+                super.mark({ loci: EveryLoci }, MarkerAction.Deselect)
+                super.mark({ loci: this.sel.get(loci.structure) }, MarkerAction.Select)
+            } else {
+                super.mark(current, action)
+            }
+        }
+
         private toggleSel(current: Loci<ModelLoci>) {
             if (this.sel.has(current.loci)) {
                 this.sel.remove(current.loci);

+ 1 - 1
src/mol-plugin/util/structure-element-selection.ts

@@ -148,7 +148,7 @@ class StructureElementSelectionManager {
         if (StructureElement.Loci.is(loci)) {
             const entry = this.getEntry(loci.structure);
             if (entry) {
-                return StructureElement.Loci.areIntersecting(loci, entry.selection);
+                return StructureElement.Loci.isSubset(entry.selection, loci);
             }
         }
         return false;

+ 38 - 30
src/mol-theme/label.ts

@@ -8,18 +8,9 @@
 import { Unit, StructureElement, StructureProperties as Props, Link } from '../mol-model/structure';
 import { Loci } from '../mol-model/loci';
 import { OrderedSet } from '../mol-data/int';
-import { capitalize } from '../mol-util/string';
+import { capitalize, stripTags } from '../mol-util/string';
 import { Column } from '../mol-data/db';
 
-// for `labelFirst`, don't create right away to avoid problems with circular dependencies/imports
-let elementLocA: StructureElement.Location
-let elementLocB: StructureElement.Location
-
-function setElementLocation(loc: StructureElement.Location, unit: Unit, index: StructureElement.UnitIndex) {
-    loc.unit = unit
-    loc.element = unit.elements[index]
-}
-
 export function lociLabel(loci: Loci): string {
     switch (loci.kind) {
         case 'structure-loci':
@@ -47,8 +38,8 @@ function countLabel(count: number, label: string) {
     return count === 1 ? `1 ${label}` : `${count} ${label}s`
 }
 
-function otherLabel(count: number, location: StructureElement.Location, granularity: LabelGranularity, hidePrefix: boolean, altCount?: string) {
-    return `${elementLabel(location, granularity, hidePrefix)} <small>[+ ${countLabel(count - 1, `other ${altCount || capitalize(granularity)}`)}]</small>`
+function otherLabel(count: number, location: StructureElement.Location, granularity: LabelGranularity, hidePrefix: boolean) {
+    return `${elementLabel(location, { granularity, hidePrefix })} <small>[+ ${countLabel(count - 1, `other ${capitalize(granularity)}`)}]</small>`
 }
 
 /** Gets residue count of the model chain segments the unit is a subset of */
@@ -61,47 +52,50 @@ function getResidueCount(unit: Unit.Atomic) {
 }
 
 export function structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false): string {
-    const { unitCount, residueCount, elementCount } = stats
+    const { unitCount, residueCount, conformationCount, elementCount } = stats
 
     if (!countsOnly && elementCount === 1 && residueCount === 0 && unitCount === 0) {
-        return elementLabel(stats.firstElementLoc, 'element')
+        return elementLabel(stats.firstElementLoc, { granularity: 'element' })
     } else if (!countsOnly && elementCount === 0 && residueCount === 1 && unitCount === 0) {
-        return elementLabel(stats.firstResidueLoc, 'residue')
+        return elementLabel(stats.firstResidueLoc, { granularity: '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)
+        return elementLabel(stats.firstUnitLoc, { granularity })
     } else if (!countsOnly) {
         const label: string[] = []
         let hidePrefix = false;
         if (unitCount > 0) {
-            label.push(unitCount === 1 ? elementLabel(stats.firstUnitLoc, 'chain') : otherLabel(unitCount, stats.firstElementLoc, 'chain', false, 'Instance'))
+            label.push(unitCount === 1 ? elementLabel(stats.firstUnitLoc, { granularity: 'chain' }) : otherLabel(unitCount, stats.firstElementLoc, 'chain', false))
             hidePrefix = true;
         }
         if (residueCount > 0) {
-            label.push(residueCount === 1 ? elementLabel(stats.firstResidueLoc, 'residue', hidePrefix) : otherLabel(residueCount, stats.firstElementLoc, 'residue', hidePrefix))
+            label.push(residueCount === 1 ? elementLabel(stats.firstResidueLoc, { granularity: 'residue', hidePrefix }) : otherLabel(residueCount, stats.firstElementLoc, 'residue', hidePrefix))
+            hidePrefix = true;
+        }
+        if (conformationCount > 0) {
+            label.push(conformationCount === 1 ? elementLabel(stats.firstConformationLoc, { granularity: 'conformation', hidePrefix }) : otherLabel(conformationCount, stats.firstElementLoc, 'conformation', hidePrefix))
             hidePrefix = true;
         }
         if (elementCount > 0) {
-            label.push(elementCount === 1 ? elementLabel(stats.firstElementLoc, 'element', hidePrefix) : otherLabel(elementCount, stats.firstElementLoc, 'element', hidePrefix))
+            label.push(elementCount === 1 ? elementLabel(stats.firstElementLoc, { granularity: 'element', hidePrefix }) : otherLabel(elementCount, stats.firstElementLoc, 'element', hidePrefix))
         }
         return label.join('<small> + </small>')
     } else {
         const label: string[] = []
-        if (unitCount > 0) label.push(countLabel(unitCount, 'Instance'))
+        if (unitCount > 0) label.push(countLabel(unitCount, 'Chain'))
         if (residueCount > 0) label.push(countLabel(residueCount, 'Residue'))
+        if (conformationCount > 0) label.push(countLabel(conformationCount, 'Conformation'))
         if (elementCount > 0) label.push(countLabel(elementCount, 'Element'))
         return label.join('<small> + </small>')
     }
 }
 
 export function linkLabel(link: Link.Location): string {
-    if (!elementLocA) elementLocA = StructureElement.Location.create()
-    if (!elementLocB) elementLocB = StructureElement.Location.create()
-    setElementLocation(elementLocA, link.aUnit, link.aIndex)
-    setElementLocation(elementLocB, link.bUnit, link.bIndex)
-    const labelA = _elementLabel(elementLocA)
-    const labelB = _elementLabel(elementLocB)
+    const locA = StructureElement.Location.create(link.aUnit, link.aUnit.elements[link.aIndex])
+    const locB = StructureElement.Location.create(link.bUnit, link.bUnit.elements[link.bIndex])
+    const labelA = _elementLabel(locA)
+    const labelB = _elementLabel(locB)
     let offset = 0
     for (let i = 0, il = Math.min(labelA.length, labelB.length); i < il; ++i) {
         if (labelA[i] === labelB[i]) offset += 1
@@ -110,10 +104,19 @@ export function linkLabel(link: Link.Location): string {
     return `${labelA.join(' | ')} \u2014 ${labelB.slice(offset).join(' | ')}`
 }
 
-export type LabelGranularity = 'element' | 'residue' | 'chain' | 'structure'
+export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure'
+
+export const DefaultLabelOptions = {
+    granularity: 'element' as LabelGranularity,
+    hidePrefix: false,
+    htmlStyling: true,
+}
+export type LabelOptions = typeof DefaultLabelOptions
 
-export function elementLabel(location: StructureElement.Location, granularity: LabelGranularity = 'element', hidePrefix = false): string {
-    return _elementLabel(location, granularity, hidePrefix).join(' | ')
+export function elementLabel(location: StructureElement.Location, options: Partial<LabelOptions>): string {
+    const o = { ...DefaultLabelOptions, ...options }
+    const label = _elementLabel(location, o.granularity, o.hidePrefix).join(' | ')
+    return o.htmlStyling ? label : stripTags(label)
 }
 
 function _elementLabel(location: StructureElement.Location, granularity: LabelGranularity = 'element', hidePrefix = false): string[] {
@@ -158,8 +161,12 @@ function _atomicElementLabel(location: StructureElement.Location<Unit.Atomic>, g
     switch (granularity) {
         case 'element':
             label.push(`<b>${atom_id}</b>${alt_id ? `%${alt_id}` : ''}`)
+        case 'conformation':
+            if (granularity === 'conformation' && alt_id) {
+                label.push(`<small>Conformation</small> <b>${alt_id}</b>`)
+            }
         case 'residue':
-            label.push(`<b>${compId} ${has_label_seq_id ? label_seq_id : ''}</b>${label_seq_id !== auth_seq_id ? ` <small> [auth</small> <b>${auth_seq_id}</b><small>]</small>` : ''}<b>${ins_code ? ins_code : ''}</b>`)
+            label.push(`<b>${compId}${has_label_seq_id ? ` ${label_seq_id}` : ''}</b>${label_seq_id !== auth_seq_id ? ` <small>[auth</small> <b>${auth_seq_id}</b><small>]</small>` : ''}<b>${ins_code ? ins_code : ''}</b>`)
         case 'chain':
             if (label_asym_id === auth_asym_id) {
                 label.push(`<b>${label_asym_id}</b>`)
@@ -180,6 +187,7 @@ function _coarseElementLabel(location: StructureElement.Location<Unit.Spheres |
 
     switch (granularity) {
         case 'element':
+        case 'conformation':
         case 'residue':
             if (seq_id_begin === seq_id_end) {
                 const entityIndex = Props.coarse.entityKey(location)