diff --git a/src/mol-data/int/_spec/ordered-set.spec.ts b/src/mol-data/int/_spec/ordered-set.spec.ts index 04ca406f4aa38c26ab4acf1d9522f15cdedc77cd..4bd7fbdc0f13167d17c8b3880e2b72823b6e1f2d 100644 --- a/src/mol-data/int/_spec/ordered-set.spec.ts +++ b/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) { diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts index 8f6d7f8eb03a35ad45a89b6f6480daf7ba19c510..74c0f1345e6d3ec13d628092d96564e2c27032e4 100644 --- a/src/mol-model/structure/structure/element/loci.ts +++ b/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][] = []; diff --git a/src/mol-model/structure/structure/element/stats.ts b/src/mol-model/structure/structure/element/stats.ts index 805df216c1cb7eaa7aada59254b7f340e0a4f742..447cd7364d2a837a650d09bbb999d33162af2e52 100644 --- a/src/mol-model/structure/structure/element/stats.ts +++ b/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 diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx index 7fd4d3e210f69df38a43e7e60cbc6e43aa3a44c8..5ce88829d905c1bdae9746e23ea4b596daff8006 100644 --- a/src/mol-plugin/ui/sequence.tsx +++ b/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) diff --git a/src/mol-plugin/ui/structure/selection.tsx b/src/mol-plugin/ui/structure/selection.tsx index 9d9d3b63a3acb8eef5cb9b05f19fad52c4be8fe1..5a7e591f1a69d17716c0dfc992770bd1ac4ef1b0 100644 --- a/src/mol-plugin/ui/structure/selection.tsx +++ b/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)}` } } diff --git a/src/mol-plugin/util/interactivity.ts b/src/mol-plugin/util/interactivity.ts index 11f147a49785697ec7281fec5c906753bdd91c28..b1832b9a80fd3074e5aad0fc92d54c0790b4b97d 100644 --- a/src/mol-plugin/util/interactivity.ts +++ b/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); diff --git a/src/mol-plugin/util/structure-element-selection.ts b/src/mol-plugin/util/structure-element-selection.ts index 051929e0b6b2d8123fe0b952065e0d0609fbf883..e69d1f4d4850ed7c2f8a5e643ee864d0b60f7ed4 100644 --- a/src/mol-plugin/util/structure-element-selection.ts +++ b/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; diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts index 4f8418f0e1e3541f6a9dd25e35caf80dd8d292ec..0863284304a9bf9b224cd63b3dabd57c9f461d4f 100644 --- a/src/mol-theme/label.ts +++ b/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)