diff --git a/package-lock.json b/package-lock.json index 5cd5ce9e9f5b8b0c6b8e3a51e9cbd0b25dd54a4d..24f29c7ffd17e68037eca304abde828732480165 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index a376d66ec660ecd0a6d414d448da20af92c989c0..6b9d3e8cdacb2caa9d0de392f8eba63daebb36b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "molstar", - "version": "0.5.1", + "version": "0.5.5", "description": "A comprehensive macromolecular library.", "homepage": "https://github.com/molstar/molstar#readme", "repository": { @@ -72,9 +72,8 @@ "@graphql-codegen/typescript-graphql-request": "^1.12.2", "@graphql-codegen/typescript-operations": "^1.12.2", "@types/cors": "^2.8.6", - "@typescript-eslint/eslint-plugin": "^2.20.0", - "@typescript-eslint/eslint-plugin-tslint": "^2.20.0", - "@typescript-eslint/parser": "^2.20.0", + "@typescript-eslint/eslint-plugin": "^2.21.0", + "@typescript-eslint/parser": "^2.21.0", "benchmark": "^2.1.4", "circular-dependency-plugin": "^5.2.0", "concurrently": "^5.1.0", @@ -96,7 +95,7 @@ "simple-git": "^1.131.0", "style-loader": "^1.1.3", "ts-jest": "^25.2.1", - "typescript": "^3.8.2", + "typescript": "^3.8.3", "webpack": "^4.41.6", "webpack-cli": "^3.3.11" }, @@ -106,9 +105,9 @@ "@types/compression": "1.7.0", "@types/express": "^4.17.2", "@types/jest": "^25.1.3", - "@types/node": "^13.7.4", - "@types/node-fetch": "^2.5.4", - "@types/react": "^16.9.22", + "@types/node": "^13.7.7", + "@types/node-fetch": "^2.5.5", + "@types/react": "^16.9.23", "@types/react-dom": "^16.9.5", "@types/swagger-ui-dist": "3.0.5", "argparse": "^1.0.10", @@ -117,10 +116,11 @@ "cors": "^2.8.5", "express": "^4.17.1", "graphql": "^14.6.0", + "immer": "^5.3.6", "immutable": "^3.8.2", "node-fetch": "^2.6.0", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.13.0", + "react-dom": "^16.13.0", "rxjs": "^6.5.4", "swagger-ui-dist": "^3.25.0", "util.promisify": "^1.0.1", diff --git a/src/apps/basic-wrapper/helpers.ts b/src/apps/basic-wrapper/helpers.ts index aefcb26e4fd6b4f2fdd7fe211407f549ce82e208..c26cb3eb4b274478620222f333a88f7eea3c05a2 100644 --- a/src/apps/basic-wrapper/helpers.ts +++ b/src/apps/basic-wrapper/helpers.ts @@ -67,7 +67,13 @@ export namespace StateHelper { } export function assemble(b: StateBuilder.To<PSO.Molecule.Model>, id?: string) { - return b.apply(StateTransforms.Model.StructureAssemblyFromModel, { id: id || 'deposited' }, { tags: 'asm' }) + const props = { + type: { + name: 'assembly' as const, + params: { id: id || 'deposited' } + } + } + return b.apply(StateTransforms.Model.StructureFromModel, props, { tags: 'asm' }) } export function visual(ctx: PluginContext, visualRoot: StateBuilder.To<PSO.Molecule.Structure>) { diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts index 79c65460b2a7fb765db156c3f5e0804b2feeb805..1d77c47c9186bf19616a3a6a9cd4c33d317ad62f 100644 --- a/src/apps/basic-wrapper/index.ts +++ b/src/apps/basic-wrapper/index.ts @@ -61,10 +61,16 @@ class BasicWrapper { ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) : b.apply(StateTransforms.Model.TrajectoryFromPDB); + const props = { + type: { + name: 'assembly' as const, + params: { id: assemblyId || 'deposited' } + } + } return parsed .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) .apply(StateTransforms.Model.CustomModelProperties, { autoAttach: [StripedResidues.propertyProvider.descriptor.name], properties: {} }, { ref: 'props', state: { isGhost: false } }) - .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); + .apply(StateTransforms.Model.StructureFromModel, props, { ref: 'asm' }); } private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) { @@ -101,8 +107,15 @@ class BasicWrapper { tree = state.build(); this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId)); } else { + const props = { + type: { + name: 'assembly' as const, + params: { id: assemblyId || 'deposited' } + } + } + tree = state.build(); - tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + tree.to('asm').update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props })); } await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); diff --git a/src/apps/demos/lighting/index.ts b/src/apps/demos/lighting/index.ts index f54e0eb8eea59ecec5bb3ff27ee87c3a711194e3..dab86f4637c955aee04d9b6ace4e3f36486aaeb0 100644 --- a/src/apps/demos/lighting/index.ts +++ b/src/apps/demos/lighting/index.ts @@ -120,9 +120,15 @@ class LightingDemo { ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) : b.apply(StateTransforms.Model.TrajectoryFromPDB); + const props = { + type: { + name: 'assembly' as const, + params: { id: assemblyId || 'deposited' } + } + } return parsed .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) - .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); + .apply(StateTransforms.Model.StructureFromModel, props, { ref: 'asm' }); } private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) { @@ -153,8 +159,14 @@ class LightingDemo { tree = state.build(); this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId)); } else { + const props = { + type: { + name: 'assembly' as const, + params: { id: assemblyId || 'deposited' } + } + } tree = state.build(); - tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + tree.to('asm').update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props })); } await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); diff --git a/src/apps/state-docs/pd-to-md.ts b/src/apps/state-docs/pd-to-md.ts index f0df30519f9e4e18dfd3cd60509d6b45baf244f0..f725105b4028af9d934d5361e1a20198ea15c1ca 100644 --- a/src/apps/state-docs/pd-to-md.ts +++ b/src/apps/state-docs/pd-to-md.ts @@ -39,7 +39,7 @@ function paramInfo(param: PD.Any, offset: number): string { } } -function oToS(options: readonly (readonly [string, string])[]) { +function oToS(options: readonly (readonly [string, string] | readonly [string, string, string])[]) { return options.map(o => `'${o[0]}'`).join(', '); } diff --git a/src/apps/structure-info/model.ts b/src/apps/structure-info/model.ts index 507ef7a19c151e9e7dfbb02c55540c4fbcfd25b5..d0498beff3601b1444116c301229b81fed5da37d 100644 --- a/src/apps/structure-info/model.ts +++ b/src/apps/structure-info/model.ts @@ -123,18 +123,6 @@ export function printSequence(model: Model) { console.log(); } -export function printModRes(model: Model) { - console.log('\nModified Residues\n============='); - const map = model.properties.modifiedResidues.parentId; - const { label_comp_id, _rowCount } = model.atomicHierarchy.residues; - for (let i = 0; i < _rowCount; i++) { - const comp_id = label_comp_id.value(i); - if (!map.has(comp_id)) continue; - console.log(`[${i}] ${map.get(comp_id)} -> ${comp_id}`); - } - console.log(); -} - export function printRings(structure: Structure) { console.log('\nRings\n============='); for (const unit of structure.units) { @@ -221,7 +209,6 @@ async function run(frame: CifFrame, args: Args) { if (args.rings) printRings(structure); if (args.intraBonds) printBonds(structure, true, false); if (args.interBonds) printBonds(structure, false, true); - if (args.mod) printModRes(models[0]); if (args.sec) printSecStructure(models[0]); } diff --git a/src/examples/proteopedia-wrapper/coloring.ts b/src/examples/proteopedia-wrapper/coloring.ts index 0f7bc4b3a5f423f5ccabcdb4f5cb38ca521508fb..3b25496365d11cc143d1c182c1e8a273286a171b 100644 --- a/src/examples/proteopedia-wrapper/coloring.ts +++ b/src/examples/proteopedia-wrapper/coloring.ts @@ -96,6 +96,7 @@ export function createProteopediaCustomTheme(colors: number[]) { const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = { label: 'Proteopedia Custom', + category: 'Custom', factory: ProteopediaCustomColorTheme, getParams: getChainIdColorThemeParams, defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams), diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts index 8611b4ba02d43c5e12770e9935159e4a7520e08f..4efdd23612fc34cc2acb51da55f333be23516716 100644 --- a/src/examples/proteopedia-wrapper/helpers.ts +++ b/src/examples/proteopedia-wrapper/helpers.ts @@ -7,7 +7,7 @@ import { ResidueIndex, Model } from '../../mol-model/structure'; import { BuiltInStructureRepresentationsName } from '../../mol-repr/structure/registry'; import { BuiltInColorThemeName } from '../../mol-theme/color'; -import { AminoAcidNames } from '../../mol-model/structure/model/types'; +import { PolymerType } from '../../mol-model/structure/model/types'; import { PluginContext } from '../../mol-plugin/context'; import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry'; @@ -54,15 +54,14 @@ export namespace ModelInfo { const hetMap = new Map<string, ModelInfo['hetResidues'][0]>(); for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) { - const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI); - if (AminoAcidNames.has(comp_id)) continue; - const mod_parent = model.properties.modifiedResidues.parentId.get(comp_id); - if (mod_parent && AminoAcidNames.has(mod_parent)) continue; + if (model.atomicHierarchy.derived.residue.polymerType[rI] !== PolymerType.NA) continue; const cI = chainIndex[residueOffsets[rI]]; const eI = model.atomicHierarchy.index.getEntityFromChain(cI); if (model.entities.data.type.value(eI) === 'water') continue; + const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI); + let lig = hetMap.get(comp_id); if (!lig) { lig = { name: comp_id, indices: [] }; diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts index 7254ea8b8727546c2c030bd517a0879cec262d2d..2ecd40ac6a54e4d3f43cb5d3ff2785735b06b1bc 100644 --- a/src/examples/proteopedia-wrapper/index.ts +++ b/src/examples/proteopedia-wrapper/index.ts @@ -92,10 +92,16 @@ class MolStarProteopediaWrapper { private structure(assemblyId: string) { const model = this.state.build().to(StateElements.Model); + const props = { + type: { + name: 'assembly' as const, + params: { id: assemblyId || 'deposited' } + } + } const s = model .apply(StateTransforms.Model.CustomModelProperties, { autoAttach: [EvolutionaryConservation.propertyProvider.descriptor.name], properties: {} }, { ref: StateElements.ModelProps, state: { isGhost: false } }) - .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly }); + .apply(StateTransforms.Model.StructureFromModel, props, { ref: StateElements.Assembly }); s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence }); s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het }); @@ -213,7 +219,13 @@ class MolStarProteopediaWrapper { const tree = state.build(); const info = await this.doInfo(true); const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId; - tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: asmId })); + const props = { + type: { + name: 'assembly' as const, + params: { id: asmId || 'deposited' } + } + } + tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props })); await this.applyState(tree); } diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index a00217f4b7b7aed0e509e252a7fd5ffcd6483c66..5d91550849344da4735be84e2ef6ff116d597e65 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -77,7 +77,7 @@ interface Canvas3D { handleResize(): void /** Focuses camera on scene's bounding sphere, centered and zoomed. */ - requestCameraReset(durationMs?: number): void + requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void readonly camera: Camera readonly boundingSphere: Readonly<Sphere3D> downloadScreenshot(): void @@ -191,6 +191,7 @@ namespace Canvas3D { let drawPending = false let cameraResetRequested = false let nextCameraResetDuration: number | undefined = void 0 + let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0 function getLoci(pickingId: PickingId) { let loci: Loci = EmptyLoci @@ -290,9 +291,15 @@ namespace Canvas3D { function resolveCameraReset() { if (!cameraResetRequested) return; const { center, radius } = scene.boundingSphere; - const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration - camera.focus(center, radius, radius, duration); + if (radius > 0) { + const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration + const focus = camera.getFocus(center, radius, radius); + const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus; + camera.setState(snapshot, duration); + } + nextCameraResetDuration = void 0; + nextCameraResetSnapshot = void 0; cameraResetRequested = false; } @@ -388,8 +395,9 @@ namespace Canvas3D { getLoci, handleResize, - requestCameraReset: (durationMs) => { - nextCameraResetDuration = durationMs; + requestCameraReset: options => { + nextCameraResetDuration = options?.durationMs; + nextCameraResetSnapshot = options?.snapshot; cameraResetRequested = true; }, camera, diff --git a/src/mol-geo/geometry/base.ts b/src/mol-geo/geometry/base.ts index 5cce347e24baa25b2e09a91c197d8728f68d4834..f726d3447da2dc8c9d01cc144c0e3c452ce1f863 100644 --- a/src/mol-geo/geometry/base.ts +++ b/src/mol-geo/geometry/base.ts @@ -28,8 +28,8 @@ export const VisualQualityInfo = { 'lowest': {}, } export type VisualQuality = keyof typeof VisualQualityInfo -export const VisualQualityNames = Object.keys(VisualQualityInfo) -export const VisualQualityOptions = VisualQualityNames.map(n => [n, n] as [VisualQuality, string]) +export const VisualQualityNames = Object.keys(VisualQualityInfo) as VisualQuality[] +export const VisualQualityOptions = PD.arrayToOptions(VisualQualityNames) // diff --git a/src/mol-math/geometry/_spec/spacegroup.spec.ts b/src/mol-math/geometry/_spec/spacegroup.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7783f936c638b226e08433d1a85a8fad57a91696 --- /dev/null +++ b/src/mol-math/geometry/_spec/spacegroup.spec.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Spacegroup, SpacegroupCell } from '../spacegroup/construction'; +import { Vec3 } from '../../linear-algebra'; + +function getSpacegroup(name: string) { + const size = Vec3.create(1, 1, 1) + const anglesInRadians = Vec3.create(Math.PI / 2, Math.PI / 2, Math.PI / 2) + const cell = SpacegroupCell.create(name, size, anglesInRadians) + return Spacegroup.create(cell) +} + +function checkOperatorsXyz(name: string, expected: string[]) { + const spacegroup = getSpacegroup(name) + for (let i = 0, il = spacegroup.operators.length; i < il; ++i) { + const op = spacegroup.operators[i] + const actual = Spacegroup.getOperatorXyz(op) + expect(actual).toBe(expected[i]) + } +} + +describe('Spacegroup', () => { + it('operators xyz', () => { + checkOperatorsXyz('P 1', ['X,Y,Z']) + checkOperatorsXyz('P -1', ['X,Y,Z', '-X,-Y,-Z']) + checkOperatorsXyz('P 1 21 1', ['X,Y,Z', '-X,1/2+Y,-Z']) + checkOperatorsXyz('P 1 21/m 1', ['X,Y,Z', '-X,1/2+Y,-Z', '-X,-Y,-Z', 'X,1/2-Y,Z']) + checkOperatorsXyz('P 41', ['X,Y,Z', '-X,-Y,1/2+Z', '-Y,X,1/4+Z', 'Y,-X,3/4+Z']) + checkOperatorsXyz('P 41 21 2', ['X,Y,Z', '-X,-Y,1/2+Z', '1/2-Y,1/2+X,1/4+Z', '1/2+Y,1/2-X,3/4+Z', '1/2-X,1/2+Y,1/4-Z', '1/2+X,1/2-Y,3/4-Z', 'Y,X,-Z', '-Y,-X,1/2-Z']) + checkOperatorsXyz('P 3', ['X,Y,Z', '-Y,X-Y,Z', 'Y-X,-X,Z']) + }); +}) diff --git a/src/mol-math/geometry/spacegroup/construction.ts b/src/mol-math/geometry/spacegroup/construction.ts index 9bc4d9d6cee4cf8879c43ae978b66cbfa45a9028..944c12cf3352af1d42f0c59472d15bade372cd87 100644 --- a/src/mol-math/geometry/spacegroup/construction.ts +++ b/src/mol-math/geometry/spacegroup/construction.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2020 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> @@ -145,6 +145,54 @@ namespace Spacegroup { const r3 = TransformData[ids[2]]; return Mat4.ofRows([r1, r2, r3, [0, 0, 0, 1]]); } + + export function getOperatorXyz(op: Mat4) { + return [ + formatElement(getRotation(op[0], op[4], op[8]), getShift(op[12])), + formatElement(getRotation(op[1], op[5], op[9]), getShift(op[13])), + formatElement(getRotation(op[2], op[6], op[10]), getShift(op[14])) + ].join(',') + } + + function getRotation(x: number, y: number, z: number) { + let r: string[] = [] + if (x > 0) r.push('+X') + else if (x < 0) r.push('-X') + if (y > 0) r.push('+Y') + else if (y < 0) r.push('-Y') + if (z > 0) r.push('+Z') + else if (z < 0) r.push('-Z') + + if (r.length === 1) { + return r[0].charAt(0) === '+' ? r[0].substr(1) : r[0] + } + if (r.length === 2) { + const s0 = r[0].charAt(0) + const s1 = r[1].charAt(0) + if (s0 === '+') return `${r[0].substr(1)}${r[1]}` + if (s1 === '+') return `${r[1].substr(1)}${r[0]}` + } + throw new Error(`unknown rotation '${r}', ${x} ${y} ${z}`) + } + + function getShift(s: number) { + switch (s) { + case 1/2: return '1/2' + case 1/4: return '1/4' + case 3/4: return '3/4' + case 1/3: return '1/3' + case 2/3: return '2/3' + case 1/6: return '1/6' + case 5/6: return '5/6' + } + return '' + } + + function formatElement(rotation: string, shift: string) { + if (shift === '') return rotation + if (rotation.length > 2) return `${rotation}+${shift}` + return rotation.charAt(0) === '-' ? `${shift}${rotation}` : `${shift}+${rotation}` + } } export { Spacegroup, SpacegroupCell } \ No newline at end of file diff --git a/src/mol-model-formats/structure/basic/parser.ts b/src/mol-model-formats/structure/basic/parser.ts index cf41cd1866c7c82aef5f6b339cdf6d5f28a8e402..554c6eea02fa352e9ec5962db71e8ab0b42b7d3c 100644 --- a/src/mol-model-formats/structure/basic/parser.ts +++ b/src/mol-model-formats/structure/basic/parser.ts @@ -45,7 +45,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex: } const coarse = EmptyCoarse; - const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy, properties.modifiedResidues.parentId) + const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy) const atomicRanges = getAtomicRanges(atomic.hierarchy, entities, atomic.conformation, sequence) const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present @@ -80,7 +80,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex: function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Model['properties'], format: ModelFormat): Model { const atomic = getAtomicHierarchyAndConformation(ihm.atom_site, ihm.atom_site_sourceIndex, ihm.entities, properties.chemicalComponentMap); const coarse = getCoarse(ihm, properties); - const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy, properties.modifiedResidues.parentId) + const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy) const atomicRanges = getAtomicRanges(atomic.hierarchy, ihm.entities, atomic.conformation, sequence) const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present diff --git a/src/mol-model-formats/structure/basic/properties.ts b/src/mol-model-formats/structure/basic/properties.ts index 45ecd6e5d17ee6f0d4e13803c91981b6cf0369b0..743c57c7f71b2565896b6a587cfecc9840afcbb0 100644 --- a/src/mol-model-formats/structure/basic/properties.ts +++ b/src/mol-model-formats/structure/basic/properties.ts @@ -6,30 +6,13 @@ */ import { Model } from '../../../mol-model/structure/model/model'; -import { ChemicalComponent, MissingResidue } from '../../../mol-model/structure/model/properties/common'; +import { ChemicalComponent, MissingResidue, StructAsym } from '../../../mol-model/structure/model/properties/common'; import { getMoleculeType, MoleculeType, getDefaultChemicalComponent } from '../../../mol-model/structure/model/types'; import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants'; import { memoize1 } from '../../../mol-util/memoize'; import { BasicData } from './schema'; import { Table } from '../../../mol-data/db'; -function getModifiedResidueNameMap(data: BasicData): Model['properties']['modifiedResidues'] { - const parentId = new Map<string, string>(); - const details = new Map<string, string>(); - - const c = data.pdbx_struct_mod_residue; - const comp_id = c.label_comp_id.isDefined ? c.label_comp_id : c.auth_comp_id; - const parent_id = c.parent_comp_id, details_data = c.details; - - for (let i = 0; i < c._rowCount; i++) { - const id = comp_id.value(i); - parentId.set(id, parent_id.value(i)); - details.set(id, details_data.value(i)); - } - - return { parentId, details }; -} - function getMissingResidues(data: BasicData): Model['properties']['missingResidues'] { const map = new Map<string, MissingResidue>(); const getKey = (model_num: number, asym_id: string, seq_id: number) => { @@ -124,11 +107,43 @@ const getUniqueComponentNames = memoize1((data: BasicData) => { return uniqueNames }) + +function getStructAsymMap(data: BasicData): Model['properties']['structAsymMap'] { + const map = new Map<string, StructAsym>(); + + const { label_asym_id, auth_asym_id, label_entity_id } = data.atom_site + for (let i = 0, il = label_asym_id.rowCount; i < il; ++i) { + const id = label_asym_id.value(i) + if (!map.has(id)) { + map.set(id, { + id, + auth_id: auth_asym_id.value(i), + entity_id: label_entity_id.value(i) + }) + } + } + + if (data.struct_asym._rowCount > 0) { + const { id, entity_id } = data.struct_asym + for (let i = 0, il = id.rowCount; i < il; ++i) { + const _id = id.value(i) + if (!map.has(_id)) { + map.set(_id, { + id: _id, + auth_id: '', + entity_id: entity_id.value(i) + }) + } + } + } + return map +} + export function getProperties(data: BasicData): Model['properties'] { return { - modifiedResidues: getModifiedResidueNameMap(data), missingResidues: getMissingResidues(data), chemicalComponentMap: getChemicalComponentMap(data), - saccharideComponentMap: getSaccharideComponentMap(data) + saccharideComponentMap: getSaccharideComponentMap(data), + structAsymMap: getStructAsymMap(data) } } \ No newline at end of file diff --git a/src/mol-model-formats/structure/basic/schema.ts b/src/mol-model-formats/structure/basic/schema.ts index dea7e328e8a9db387542f49ee8c057e7fb9f73aa..670432bc674935ecfb1cb8a1acdf473f4c049417 100644 --- a/src/mol-model-formats/structure/basic/schema.ts +++ b/src/mol-model-formats/structure/basic/schema.ts @@ -8,7 +8,6 @@ import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif'; import { Table } from '../../../mol-data/db'; // TODO split into conformation and hierarchy parts -// TODO extract `pdbx_struct_mod_residue` as property? export type Entry = Table<mmCIF_Schema['entry']> export type Struct = Table<mmCIF_Schema['struct']> @@ -22,7 +21,6 @@ export type EntityPolySeq = Table<mmCIF_Schema['entity_poly_seq']> export type EntityBranch = Table<mmCIF_Schema['pdbx_entity_branch']> export type ChemComp = Table<mmCIF_Schema['chem_comp']> export type ChemCompIdentifier = Table<mmCIF_Schema['pdbx_chem_comp_identifier']> -export type StructModResidue = Table<mmCIF_Schema['pdbx_struct_mod_residue']> export type AtomSite = Table<mmCIF_Schema['atom_site']> export type IhmSphereObjSite = Table<mmCIF_Schema['ihm_sphere_obj_site']> export type IhmGaussianObjSite =Table<mmCIF_Schema['ihm_gaussian_obj_site']> @@ -41,7 +39,6 @@ export const BasicSchema = { pdbx_entity_branch: mmCIF_Schema.pdbx_entity_branch, chem_comp: mmCIF_Schema.chem_comp, pdbx_chem_comp_identifier: mmCIF_Schema.pdbx_chem_comp_identifier, - pdbx_struct_mod_residue: mmCIF_Schema.pdbx_struct_mod_residue, atom_site: mmCIF_Schema.atom_site, ihm_sphere_obj_site: mmCIF_Schema.ihm_sphere_obj_site, ihm_gaussian_obj_site: mmCIF_Schema.ihm_gaussian_obj_site, @@ -61,7 +58,6 @@ export interface BasicData { pdbx_entity_branch: EntityBranch chem_comp: ChemComp pdbx_chem_comp_identifier: ChemCompIdentifier - pdbx_struct_mod_residue: StructModResidue atom_site: AtomSite ihm_sphere_obj_site: IhmSphereObjSite ihm_gaussian_obj_site: IhmGaussianObjSite diff --git a/src/mol-model-formats/structure/basic/sequence.ts b/src/mol-model-formats/structure/basic/sequence.ts index d73a6f84a50bf48ff02504a0002813012f49d417..8a3328d52ddfdb7e2d55876c72aa52c6fb84ffd8 100644 --- a/src/mol-model-formats/structure/basic/sequence.ts +++ b/src/mol-model-formats/structure/basic/sequence.ts @@ -13,9 +13,9 @@ import { Sequence } from '../../../mol-model/sequence'; import { CoarseHierarchy } from '../../../mol-model/structure/model/properties/coarse'; import { BasicData } from './schema'; -export function getSequence(data: BasicData, entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy, modResMap: ReadonlyMap<string, string>): StructureSequence { +export function getSequence(data: BasicData, entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy): StructureSequence { if (!data.entity_poly_seq || !data.entity_poly_seq._rowCount) { - return StructureSequence.fromHierarchy(entities, atomicHierarchy, coarseHierarchy, modResMap); + return StructureSequence.fromHierarchy(entities, atomicHierarchy, coarseHierarchy); } const { entity_id, num, mon_id } = data.entity_poly_seq; @@ -37,7 +37,7 @@ export function getSequence(data: BasicData, entities: Entities, atomicHierarchy byEntityKey[entityKey] = { entityId: id, - sequence: Sequence.ofResidueNames(compId, seqId, modResMap) + sequence: Sequence.ofResidueNames(compId, seqId) }; sequences.push(byEntityKey[entityKey]); diff --git a/src/mol-model-formats/structure/common/component.ts b/src/mol-model-formats/structure/common/component.ts index 483ac6e95369b33506deee7132a663104a8a7b91..7dbbfa176c7bcc7eb0907b04015d539ee67c13ff 100644 --- a/src/mol-model-formats/structure/common/component.ts +++ b/src/mol-model-formats/structure/common/component.ts @@ -6,7 +6,7 @@ import { Table, Column } from '../../../mol-data/db'; import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif'; -import { WaterNames } from '../../../mol-model/structure/model/types'; +import { WaterNames, PolymerNames } from '../../../mol-model/structure/model/types'; import { SetUtils } from '../../../mol-util/set'; import { BasicSchema } from '../basic/schema'; @@ -77,12 +77,14 @@ export class ComponentBuilder { private ids: string[] = [] private names: string[] = [] private types: mmCIF_Schema['chem_comp']['type']['T'][] = [] + private mon_nstd_flags: mmCIF_Schema['chem_comp']['mon_nstd_flag']['T'][] = [] private set(c: Component) { this.comps.set(c.id, c) this.ids.push(c.id) this.names.push(c.name) this.types.push(c.type) + this.mon_nstd_flags.push(PolymerNames.has(c.id) ? 'y' : 'n') } private getAtomIds(index: number) { @@ -141,6 +143,7 @@ export class ComponentBuilder { id: Column.ofStringArray(this.ids), name: Column.ofStringArray(this.names), type: Column.ofStringAliasArray(this.types), + mon_nstd_flag: Column.ofStringAliasArray(this.mon_nstd_flags), }, this.ids.length) } diff --git a/src/mol-model-formats/structure/common/property.ts b/src/mol-model-formats/structure/common/property.ts index 44bae5f2f3e0e1642a01a9fc10b3a302dde39f69..873e8bd5848a0bf1edd36a69f9001d4555c293fb 100644 --- a/src/mol-model-formats/structure/common/property.ts +++ b/src/mol-model-formats/structure/common/property.ts @@ -14,6 +14,10 @@ class FormatRegistry<T> { this.map.set(kind, obtain) } + remove(kind: ModelFormat['kind']) { + this.map.delete(kind) + } + get(kind: ModelFormat['kind']) { return this.map.get(kind) } diff --git a/src/mol-model-formats/structure/mmcif.ts b/src/mol-model-formats/structure/mmcif.ts index d52f7898ce97b0431e80ea59a6e726efa58e3660..ae7c3deb8147f42d7a2f660493581d921cc68b31 100644 --- a/src/mol-model-formats/structure/mmcif.ts +++ b/src/mol-model-formats/structure/mmcif.ts @@ -17,7 +17,6 @@ import { Table } from '../../mol-data/db'; import { AtomSiteAnisotrop } from './property/anisotropic'; import { ComponentBond } from './property/bonds/comp'; import { StructConn } from './property/bonds/struct_conn'; -import { ModelCrossLinkRestraint } from './property/pair-restraints/cross-links'; function modelSymmetryFromMmcif(model: Model) { if (!MmcifFormat.is(model.sourceData)) return; @@ -65,14 +64,6 @@ function structConnFromMmcif(model: Model) { } StructConn.Provider.formatRegistry.add('mmCIF', structConnFromMmcif) -function crossLinkRestraintFromMmcif(model: Model) { - if (!MmcifFormat.is(model.sourceData)) return; - const { ihm_cross_link_restraint } = model.sourceData.data.db; - if (ihm_cross_link_restraint._rowCount === 0) return; - return ModelCrossLinkRestraint.fromTable(ihm_cross_link_restraint, model) -} -ModelCrossLinkRestraint.Provider.formatRegistry.add('mmCIF', crossLinkRestraintFromMmcif) - // export { MmcifFormat } diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts index 05e0f8592689d7a48a0d44b3cac74e06f83284d5..708ff61e5a57349b16f54777780ad08075cf8f03 100644 --- a/src/mol-model-formats/structure/pdb/to-cif.ts +++ b/src/mol-model-formats/structure/pdb/to-cif.ts @@ -162,8 +162,8 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> { } const categories = { - entity: entityBuilder.getEntityTable(), - chem_comp: componentBuilder.getChemCompTable(), + entity: CifCategory.ofTable('entity', entityBuilder.getEntityTable()), + chem_comp: CifCategory.ofTable('chem_comp', componentBuilder.getChemCompTable()), atom_site: CifCategory.ofFields('atom_site', getAtomSite(atomSite)), atom_site_anisotrop: CifCategory.ofFields('atom_site_anisotrop', getAnisotropic(anisotropic)) } as any; diff --git a/src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts b/src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts deleted file mode 100644 index d736eabdf2c073838585736ba735813d76143d75..0000000000000000000000000000000000000000 --- a/src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -// TODO -// ihm_predicted_contact_restraint: { -// id: int, -// entity_id_1: str, -// entity_id_2: str, -// asym_id_1: str, -// asym_id_2: str, -// comp_id_1: str, -// comp_id_2: str, -// seq_id_1: int, -// seq_id_2: int, -// atom_id_1: str, -// atom_id_2: str, -// distance_upper_limit: float, -// probability: float, -// restraint_type: Aliased<'lower bound' | 'upper bound' | 'lower and upper bound'>(str), -// model_granularity: Aliased<'by-residue' | 'by-feature' | 'by-atom'>(str), -// dataset_list_id: int, -// software_id: int, -// }, diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts index 3dea414a939e75449f7438195624b5387c5b300f..4684a20e428f90b48a8ec7f6b404faf13f19e911 100644 --- a/src/mol-model-props/common/custom-element-property.ts +++ b/src/mol-model-props/common/custom-element-property.ts @@ -99,6 +99,7 @@ namespace CustomElementProperty { return { label: modelProperty.label, + category: 'Custom', factory: Coloring, getParams: () => ({}), defaultValues: {}, diff --git a/src/mol-model-props/computed/accessible-surface-area.ts b/src/mol-model-props/computed/accessible-surface-area.ts index 292ab54c6aef5fb52eed0c9259c4bec15fc9df91..b3867ca743589cb6f1fd8d90be17510dce2e1715 100644 --- a/src/mol-model-props/computed/accessible-surface-area.ts +++ b/src/mol-model-props/computed/accessible-surface-area.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -7,9 +7,12 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition' import { ShrakeRupleyComputationParams, AccessibleSurfaceArea } from './accessible-surface-area/shrake-rupley'; -import { Structure, CustomPropertyDescriptor } from '../../mol-model/structure'; +import { Structure, CustomPropertyDescriptor, Unit } from '../../mol-model/structure'; import { CustomStructureProperty } from '../common/custom-structure-property'; import { CustomProperty } from '../common/custom-property'; +import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler'; +import { CustomPropSymbol } from '../../mol-script/language/symbol'; +import Type from '../../mol-script/language/type'; export const AccessibleSurfaceAreaParams = { ...ShrakeRupleyComputationParams @@ -17,13 +20,33 @@ export const AccessibleSurfaceAreaParams = { export type AccessibleSurfaceAreaParams = typeof AccessibleSurfaceAreaParams export type AccessibleSurfaceAreaProps = PD.Values<AccessibleSurfaceAreaParams> +export const AccessibleSurfaceAreaSymbols = { + isBuried: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'accessible-surface-area.is-buried', Type.Bool), + ctx => { + if (!Unit.isAtomic(ctx.element.unit)) return false + const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(ctx.element.structure).value + if (!accessibleSurfaceArea) return false + return AccessibleSurfaceArea.getFlag(ctx.element, accessibleSurfaceArea) === AccessibleSurfaceArea.Flag.Buried + } + ), + isAccessible: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'accessible-surface-area.is-accessible', Type.Bool), + ctx => { + if (!Unit.isAtomic(ctx.element.unit)) return false + const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(ctx.element.structure).value + if (!accessibleSurfaceArea) return false + return AccessibleSurfaceArea.getFlag(ctx.element, accessibleSurfaceArea) === AccessibleSurfaceArea.Flag.Accessible + } + ), +} + export type AccessibleSurfaceAreaValue = AccessibleSurfaceArea export const AccessibleSurfaceAreaProvider: CustomStructureProperty.Provider<AccessibleSurfaceAreaParams, AccessibleSurfaceAreaValue> = CustomStructureProperty.createProvider({ label: 'Accessible Surface Area', descriptor: CustomPropertyDescriptor({ name: 'molstar_accessible_surface_area', - // TODO `cifExport` and `symbol` + symbols: AccessibleSurfaceAreaSymbols, + // TODO `cifExport` }), type: 'root', defaultParams: AccessibleSurfaceAreaParams, diff --git a/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts b/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts index 6212d89f131d9826f87acb25af0a7b741c788223..b7b116061901f1357ffc613bcae58cb7070ac922 100644 --- a/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts +++ b/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts @@ -9,7 +9,7 @@ import { Task, RuntimeContext } from '../../../mol-task'; // import { BitFlags } from '../../../mol-util'; import { ParamDefinition as PD } from '../../../mol-util/param-definition' import { Vec3 } from '../../../mol-math/linear-algebra'; -import { Structure } from '../../../mol-model/structure'; +import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure'; import { assignRadiusForHeavyAtoms } from './shrake-rupley/radii'; import { ShrakeRupleyContext, VdWLookup, MaxAsa, DefaultMaxAsa } from './shrake-rupley/common'; import { computeArea } from './shrake-rupley/area'; @@ -86,19 +86,35 @@ namespace AccessibleSurfaceArea { return points; } - // export namespace SolventAccessibility { - // export const is: (t: number, f: Flag) => boolean = BitFlags.has - // export const create: (f: Flag) => number = BitFlags.create - // export const enum Flag { - // _ = 0x0, - // BURIED = 0x1, - // ACCESSIBLE = 0x2 - // } - // } + export const enum Flag { + NA = 0x0, + Buried = 0x1, + Accessible = 0x2 + } /** Get relative area for a given component id */ export function normalize(compId: string, asa: number) { const maxAsa = MaxAsa[compId] || DefaultMaxAsa; return asa / maxAsa } + + export function getValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) { + const { getSerialIndex } = location.structure.root.serialMapping + const { area, serialResidueIndex } = accessibleSurfaceArea + const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)] + if (rSI === -1) return -1 + return area[rSI] + } + + export function getNormalizedValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) { + const value = getValue(location, accessibleSurfaceArea) + return value === -1 ? -1 : normalize(StructureProperties.residue.label_comp_id(location), value) + } + + export function getFlag(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) { + const value = getNormalizedValue(location, accessibleSurfaceArea) + return value === -1 ? Flag.NA : + value < 0.16 ? Flag.Buried : + Flag.Accessible + } } \ No newline at end of file diff --git a/src/mol-model-props/computed/interactions/interactions.ts b/src/mol-model-props/computed/interactions/interactions.ts index 881163733e3549460322b933d2297b734621be20..85411d94db757a29b5eaedb7a75b81f5eccb07f3 100644 --- a/src/mol-model-props/computed/interactions/interactions.ts +++ b/src/mol-model-props/computed/interactions/interactions.ts @@ -38,8 +38,9 @@ interface Interactions { } namespace Interactions { + type StructureInteractions = { readonly structure: Structure, readonly interactions: Interactions } + export interface Element { - structure: Structure, unitA: Unit /** Index into features of unitA */ indexA: Features.FeatureIndex @@ -47,11 +48,12 @@ namespace Interactions { /** Index into features of unitB */ indexB: Features.FeatureIndex } - export interface Location extends DataLocation<Interactions, Element> {} + + export interface Location extends DataLocation<StructureInteractions, Element> {} export function Location(interactions: Interactions, structure: Structure, unitA?: Unit, indexA?: Features.FeatureIndex, unitB?: Unit, indexB?: Features.FeatureIndex): Location { - return DataLocation('interactions', interactions, - { structure: structure as any, unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any }); + return DataLocation('interactions', { structure, interactions }, + { unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any }); } export function isLocation(x: any): x is Location { @@ -60,7 +62,8 @@ namespace Interactions { export function areLocationsEqual(locA: Location, locB: Location) { return ( - locA.data === locB.data && + locA.data.structure === locB.data.structure && + locA.data.interactions === locB.data.interactions && locA.element.indexA === locB.element.indexA && locA.element.indexB === locB.element.indexB && locA.element.unitA === locB.element.unitA && @@ -82,10 +85,9 @@ namespace Interactions { } export function locationLabel(location: Location): string { - return _label(location.data, location.element) + return _label(location.data.interactions, location.element) } - type StructureInteractions = { readonly structure: Structure, readonly interactions: Interactions } export interface Loci extends DataLoci<StructureInteractions, Element> { } export function Loci(structure: Structure, interactions: Interactions, elements: ReadonlyArray<Element>): Loci { diff --git a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts index a13a95720688fd97ff2f042fa9ad144fb688971e..1895d5e5536bcc212138f0ec3fda1c01c2ae6d68 100644 --- a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts +++ b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts @@ -101,8 +101,8 @@ function getInteractionLoci(pickingId: PickingId, structure: Structure, id: numb const interactions = InteractionsProvider.get(structure).value! const c = interactions.contacts.edges[groupId] return Interactions.Loci(structure, interactions, [ - { structure, unitA: c.unitA, indexA: c.indexA, unitB: c.unitB, indexB: c.indexB }, - { structure, unitA: c.unitB, indexA: c.indexB, unitB: c.unitA, indexB: c.indexA }, + { unitA: c.unitA, indexA: c.indexA, unitB: c.unitB, indexB: c.indexB }, + { unitA: c.unitB, indexA: c.indexB, unitB: c.unitA, indexB: c.indexA }, ]) } return EmptyLoci diff --git a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts index 66a03f6ac7ee24d281c2251051e40bbf4e9a22ba..3e0b864a7bfb1718587e070f2b68718fbda39d95 100644 --- a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts +++ b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts @@ -97,8 +97,8 @@ function getInteractionLoci(pickingId: PickingId, structureGroup: StructureGroup const interactions = InteractionsProvider.get(structure).value! const { a, b } = interactions.unitsContacts.get(unit.id) return Interactions.Loci(structure, interactions, [ - { structure, unitA: unit, indexA: a[groupId], unitB: unit, indexB: b[groupId] }, - { structure, unitA: unit, indexA: b[groupId], unitB: unit, indexB: a[groupId] }, + { unitA: unit, indexA: a[groupId], unitB: unit, indexB: b[groupId] }, + { unitA: unit, indexA: b[groupId], unitB: unit, indexB: a[groupId] }, ]) } return EmptyLoci diff --git a/src/mol-model-props/computed/secondary-structure.ts b/src/mol-model-props/computed/secondary-structure.ts index d80c58b383b9a8725daed90eb9632203986e2f17..5a373f6d0334ee5065675fa655cb6809c7e1ecd6 100644 --- a/src/mol-model-props/computed/secondary-structure.ts +++ b/src/mol-model-props/computed/secondary-structure.ts @@ -1,10 +1,10 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure'; +import { Structure } from '../../mol-model/structure'; import { DSSPComputationParams, DSSPComputationProps, computeUnitDSSP } from './secondary-structure/dssp'; import { SecondaryStructure } from '../../mol-model/structure/model/properties/seconday-structure'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; @@ -13,9 +13,10 @@ import { CustomStructureProperty } from '../common/custom-structure-property'; import { CustomProperty } from '../common/custom-property'; import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure'; import { MmcifFormat } from '../../mol-model-formats/structure/mmcif'; +import { CustomPropertyDescriptor } from '../../mol-model/structure/common/custom-property'; function getSecondaryStructureParams(data?: Structure) { - let defaultType = 'mmcif' as 'mmcif' | 'dssp' + let defaultType = 'model' as 'model' | 'dssp' if (data) { defaultType = 'dssp' for (let i = 0, il = data.models.length; i < il; ++i) { @@ -27,7 +28,7 @@ function getSecondaryStructureParams(data?: Structure) { ) { // if there is any secondary structure definition given or if there is // an archival model, don't calculate dssp by default - defaultType = 'mmcif' + defaultType = 'model' break } } @@ -35,9 +36,9 @@ function getSecondaryStructureParams(data?: Structure) { } return { type: PD.MappedStatic(defaultType, { - 'mmcif': PD.EmptyGroup({ label: 'mmCIF' }), + 'model': PD.EmptyGroup({ label: 'Model' }), 'dssp': PD.Group(DSSPComputationParams, { label: 'DSSP', isFlat: true }) - }, { options: [['mmcif', 'mmCIF'], ['dssp', 'DSSP']] }) + }, { options: [['model', 'Model'], ['dssp', 'DSSP']] }) } } @@ -45,6 +46,7 @@ export const SecondaryStructureParams = getSecondaryStructureParams() export type SecondaryStructureParams = typeof SecondaryStructureParams export type SecondaryStructureProps = PD.Values<SecondaryStructureParams> +/** Maps `unit.id` to `SecondaryStructure` */ export type SecondaryStructureValue = Map<number, SecondaryStructure> export const SecondaryStructureProvider: CustomStructureProperty.Provider<SecondaryStructureParams, SecondaryStructureValue> = CustomStructureProperty.createProvider({ @@ -61,7 +63,7 @@ export const SecondaryStructureProvider: CustomStructureProperty.Provider<Second const p = { ...PD.getDefaultValues(SecondaryStructureParams), ...props } switch (p.type.name) { case 'dssp': return await computeDssp(data, p.type.params) - case 'mmcif': return await computeMmcif(data) + case 'model': return await computeModel(data) } } }) @@ -80,7 +82,7 @@ async function computeDssp(structure: Structure, props: DSSPComputationProps): P return map } -async function computeMmcif(structure: Structure): Promise<SecondaryStructureValue> { +async function computeModel(structure: Structure): Promise<SecondaryStructureValue> { const map = new Map<number, SecondaryStructure>() for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) { const u = structure.unitSymmetryGroups[i].units[0] diff --git a/src/mol-model-props/computed/themes/accessible-surface-area.ts b/src/mol-model-props/computed/themes/accessible-surface-area.ts index fe2a60134c831deb06d546d4bec2c49f7ec8252c..5de7df9837f50baec785434be678d0ebf43c8cec 100644 --- a/src/mol-model-props/computed/themes/accessible-surface-area.ts +++ b/src/mol-model-props/computed/themes/accessible-surface-area.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -10,7 +10,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition' import { Color, ColorScale } from '../../../mol-util/color' import { ThemeDataContext } from '../../../mol-theme/theme' import { ColorTheme, LocationColor } from '../../../mol-theme/color' -import { StructureProperties, StructureElement, Unit } from '../../../mol-model/structure' +import { StructureElement, Unit } from '../../../mol-model/structure' import { AccessibleSurfaceAreaProvider } from '../accessible-surface-area' import { AccessibleSurfaceArea } from '../accessible-surface-area/shrake-rupley' import { CustomProperty } from '../../common/custom-property' @@ -36,18 +36,16 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD domain: [0.0, 1.0] }) - const { label_comp_id } = StructureProperties.residue const accessibleSurfaceArea = ctx.structure && AccessibleSurfaceAreaProvider.get(ctx.structure) const contextHash = accessibleSurfaceArea?.version if (accessibleSurfaceArea?.value && ctx.structure) { - const { getSerialIndex } = ctx.structure.root.serialMapping - const { area, serialResidueIndex } = accessibleSurfaceArea.value + const asa = accessibleSurfaceArea.value color = (location: Location): Color => { if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) { - const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)] - return rSI === -1 ? DefaultColor : scale.color(AccessibleSurfaceArea.normalize(label_comp_id(location), area[rSI])) + const value = AccessibleSurfaceArea.getNormalizedValue(location, asa) + return value === -1 ? DefaultColor : scale.color(value) } return DefaultColor } @@ -68,6 +66,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<AccessibleSurfaceAreaColorThemeParams> = { label: 'Accessible Surface Area', + category: ColorTheme.Category.Residue, factory: AccessibleSurfaceAreaColorTheme, getParams: getAccessibleSurfaceAreaColorThemeParams, defaultValues: PD.getDefaultValues(AccessibleSurfaceAreaColorThemeParams), diff --git a/src/mol-model-props/computed/themes/interaction-type.ts b/src/mol-model-props/computed/themes/interaction-type.ts index 32e0a9174c3494f48f07bf930b5cdb824d6d4329..70c88fd04979d9616f22bfd086f191b9a65c56a4 100644 --- a/src/mol-model-props/computed/themes/interaction-type.ts +++ b/src/mol-model-props/computed/themes/interaction-type.ts @@ -78,7 +78,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value if (interactions && interactions.value) { color = (location: Location) => { if (Interactions.isLocation(location)) { - const { unitsContacts, contacts } = location.data + const { unitsContacts, contacts } = location.data.interactions const { unitA, unitB, indexA, indexB } = location.element if (unitA === unitB) { const links = unitsContacts.get(unitA.id) @@ -108,6 +108,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionTypeColorThemeParams> = { label: 'Interaction Type', + category: ColorTheme.Category.Misc, factory: InteractionTypeColorTheme, getParams: getInteractionTypeColorThemeParams, defaultValues: PD.getDefaultValues(InteractionTypeColorThemeParams), diff --git a/src/mol-theme/color/cross-link.ts b/src/mol-model-props/integrative/cross-link-restraint/color.ts similarity index 51% rename from src/mol-theme/color/cross-link.ts rename to src/mol-model-props/integrative/cross-link-restraint/color.ts index d744e44b4cc9f22126a9c018173b25d1a09f8fbd..0ff69cd16f2a016ca9907c76aa0ab662570255e3 100644 --- a/src/mol-theme/color/cross-link.ts +++ b/src/mol-model-props/integrative/cross-link-restraint/color.ts @@ -1,23 +1,23 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Bond } from '../../mol-model/structure'; -import { Color, ColorScale } from '../../mol-util/color'; -import { Location } from '../../mol-model/location'; -import { ColorTheme, LocationColor } from '../color'; -import { Vec3 } from '../../mol-math/linear-algebra'; -import { ParamDefinition as PD } from '../../mol-util/param-definition' -import { ThemeDataContext } from '../../mol-theme/theme'; -import { ColorListName, ColorListOptionsScale } from '../../mol-util/color/lists'; +import { Color, ColorScale } from '../../../mol-util/color'; +import { Location } from '../../../mol-model/location'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition' +import { ThemeDataContext } from '../../../mol-theme/theme'; +import { ColorListName, ColorListOptionsScale } from '../../../mol-util/color/lists'; +import { ColorTheme, LocationColor } from '../../../mol-theme/color'; +import { CustomProperty } from '../../common/custom-property'; +import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property'; const DefaultColor = Color(0xCCCCCC) -const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. `ihm_cross_link_restraint.distance_threshold`).' +const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. modeled / `ihm_cross_link_restraint.distance_threshold`).' export const CrossLinkColorThemeParams = { - domain: PD.Interval([-10, 10]), + domain: PD.Interval([0.5, 1.5], { step: 0.01 }), list: PD.ColorList<ColorListName>('red-grey', ColorListOptionsScale), } export type CrossLinkColorThemeParams = typeof CrossLinkColorThemeParams @@ -25,19 +25,13 @@ export function getCrossLinkColorThemeParams(ctx: ThemeDataContext) { return CrossLinkColorThemeParams // TODO return copy } -const distVecA = Vec3.zero(), distVecB = Vec3.zero() -function linkDistance(link: Bond.Location) { - link.aUnit.conformation.position(link.aUnit.elements[link.aIndex], distVecA) - link.bUnit.conformation.position(link.bUnit.elements[link.bIndex], distVecB) - return Vec3.distance(distVecA, distVecB) -} - export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<CrossLinkColorThemeParams>): ColorTheme<CrossLinkColorThemeParams> { let color: LocationColor let scale: ColorScale | undefined = undefined - if (ctx.structure) { - const crosslinks = ctx.structure.crossLinkRestraints + const crossLinkRestraints = ctx.structure && CrossLinkRestraintProvider.get(ctx.structure).value + + if (crossLinkRestraints) { scale = ColorScale.create({ domain: props.domain, listOrName: props.list @@ -45,10 +39,10 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros const scaleColor = scale.color color = (location: Location): Color => { - if (Bond.isLocation(location)) { - const pairs = crosslinks.getPairs(location.aIndex, location.aUnit, location.bIndex, location.bUnit) - if (pairs) { - return scaleColor(linkDistance(location) - pairs[0].distanceThreshold) + if (CrossLinkRestraint.isLocation(location)) { + const pair = crossLinkRestraints.pairs[location.element] + if (pair) { + return scaleColor(CrossLinkRestraint.distance(pair) / pair.distanceThreshold) } } return DefaultColor @@ -69,8 +63,12 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams> = { label: 'Cross Link', + category: ColorTheme.Category.Misc, factory: CrossLinkColorTheme, getParams: getCrossLinkColorThemeParams, defaultValues: PD.getDefaultValues(CrossLinkColorThemeParams), - isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.crossLinkRestraints.count > 0 + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && CrossLinkRestraint.isApplicable(ctx.structure), + ensureCustomProperties: (ctx: CustomProperty.Context, data: ThemeDataContext) => { + return data.structure ? CrossLinkRestraintProvider.attach(ctx, data.structure) : Promise.resolve() + } } \ No newline at end of file diff --git a/src/mol-model-formats/structure/property/pair-restraints/cross-links.ts b/src/mol-model-props/integrative/cross-link-restraint/format.ts similarity index 89% rename from src/mol-model-formats/structure/property/pair-restraints/cross-links.ts rename to src/mol-model-props/integrative/cross-link-restraint/format.ts index 484645a3a921c46bc2aa3f63aa26766043b3569d..a4e9c0c738a9aaaace9628f3fcb4497e54892b43 100644 --- a/src/mol-model-formats/structure/property/pair-restraints/cross-links.ts +++ b/src/mol-model-props/integrative/cross-link-restraint/format.ts @@ -4,12 +4,12 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Model } from '../../../../mol-model/structure/model/model' -import { Table } from '../../../../mol-data/db' -import { mmCIF_Schema } from '../../../../mol-io/reader/cif/schema/mmcif'; -import { Unit, CustomPropertyDescriptor } from '../../../../mol-model/structure'; -import { ElementIndex } from '../../../../mol-model/structure/model/indexing'; -import { FormatPropertyProvider } from '../../common/property'; +import { Model } from '../../../mol-model/structure/model/model' +import { Table } from '../../../mol-data/db' +import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif'; +import { Unit, CustomPropertyDescriptor } from '../../../mol-model/structure'; +import { ElementIndex } from '../../../mol-model/structure/model/indexing'; +import { FormatPropertyProvider } from '../../../mol-model-formats/structure/common/property'; export { ModelCrossLinkRestraint } diff --git a/src/mol-model-props/integrative/cross-link-restraint/property.ts b/src/mol-model-props/integrative/cross-link-restraint/property.ts new file mode 100644 index 0000000000000000000000000000000000000000..5deb93896a949af60ea84b5774a24e7ccb183bb4 --- /dev/null +++ b/src/mol-model-props/integrative/cross-link-restraint/property.ts @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ModelCrossLinkRestraint } from './format'; +import { Unit, StructureElement, Structure, CustomPropertyDescriptor, Bond} from '../../../mol-model/structure'; +import { PairRestraints, PairRestraint } from '../pair-restraints'; +import { CustomStructureProperty } from '../../common/custom-structure-property'; +import { CustomProperty } from '../../common/custom-property'; +import { DataLocation } from '../../../mol-model/location'; +import { DataLoci } from '../../../mol-model/loci'; +import { Sphere3D } from '../../../mol-math/geometry'; +import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper'; +import { bondLabel } from '../../../mol-theme/label'; +import { Vec3 } from '../../../mol-math/linear-algebra'; + +export type CrossLinkRestraintValue = PairRestraints<CrossLinkRestraint> + +export const CrossLinkRestraintProvider: CustomStructureProperty.Provider<{}, CrossLinkRestraintValue> = CustomStructureProperty.createProvider({ + label: 'Cross Link Restraint', + descriptor: CustomPropertyDescriptor({ + name: 'integrative-cross-link-restraint', + // TODO `cifExport` and `symbol` + }), + type: 'local', + defaultParams: {}, + getParams: (data: Structure) => ({}), + isApplicable: (data: Structure) => data.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m)), + obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<{}>) => { + return extractCrossLinkRestraints(data) + } +}) + +export { CrossLinkRestraint } + +interface CrossLinkRestraint extends PairRestraint { + readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound' + readonly distanceThreshold: number + readonly psi: number + readonly sigma1: number + readonly sigma2: number +} + +namespace CrossLinkRestraint { + export enum Tag { + CrossLinkRestraint = 'cross-link-restraint' + } + + export function isApplicable(structure: Structure) { + return structure.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m)) + } + + const distVecA = Vec3(), distVecB = Vec3() + export function distance(pair: CrossLinkRestraint) { + pair.unitA.conformation.position(pair.unitA.elements[pair.indexA], distVecA) + pair.unitB.conformation.position(pair.unitB.elements[pair.indexB], distVecB) + return Vec3.distance(distVecA, distVecB) + } + + type StructureCrossLinkRestraints = { readonly structure: Structure, readonly crossLinkRestraints: CrossLinkRestraintValue } + + export type Element = number + export interface Location extends DataLocation<StructureCrossLinkRestraints, Element> {} + + export function Location(crossLinkRestraints: CrossLinkRestraintValue, structure: Structure, index?: number): Location { + return DataLocation('cross-link-restraints', { structure, crossLinkRestraints }, index as any); + } + + export function isLocation(x: any): x is Location { + return !!x && x.kind === 'data-location' && x.tag === 'cross-link-restraints'; + } + + export function areLocationsEqual(locA: Location, locB: Location) { + return ( + locA.data.structure === locB.data.structure && + locA.data.crossLinkRestraints === locB.data.crossLinkRestraints && + locA.element === locB.element + ) + } + + function _label(crossLinkRestraints: CrossLinkRestraintValue, element: Element): string { + const p = crossLinkRestraints.pairs[element] + return `Cross Link Restraint | Type: ${p.restraintType} | Threshold: ${p.distanceThreshold} \u212B | Psi: ${p.psi} | Sigma 1: ${p.sigma1} | Sigma 2: ${p.sigma2} | Distance: ${distance(p).toFixed(2)} \u212B` + } + + export function locationLabel(location: Location): string { + return _label(location.data.crossLinkRestraints, location.element) + } + + export interface Loci extends DataLoci<StructureCrossLinkRestraints, Element> { } + + export function Loci(structure: Structure, crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>): Loci { + return DataLoci('cross-link-restraints', { structure, crossLinkRestraints }, elements, + (boundingSphere) => getBoundingSphere(crossLinkRestraints, elements, boundingSphere), + () => getLabel(structure, crossLinkRestraints, elements)); + } + + export function isLoci(x: any): x is Loci { + return !!x && x.kind === 'data-loci' && x.tag === 'interactions'; + } + + export function getBoundingSphere(crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>, boundingSphere: Sphere3D) { + return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => { + const p = crossLinkRestraints.pairs[elements[i]] + p.unitA.conformation.position(p.unitA.elements[p.indexA], pA) + p.unitB.conformation.position(p.unitB.elements[p.indexB], pB) + }, boundingSphere) + } + + export function getLabel(structure: Structure, crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>) { + const element = elements[0] + if (element === undefined) return '' + const p = crossLinkRestraints.pairs[element] + return [ + _label(crossLinkRestraints, element), + bondLabel(Bond.Location(structure, p.unitA, p.indexA, structure, p.unitB, p.indexB)) + ].join('</br>') + } +} + +// + +function _addRestraints(map: Map<number, number>, unit: Unit, restraints: ModelCrossLinkRestraint) { + const { elements } = unit; + const elementCount = elements.length; + const kind = unit.kind + + for (let i = 0; i < elementCount; i++) { + const e = elements[i]; + restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i)) + } +} + +function extractInter(pairs: CrossLinkRestraint[], unitA: Unit, unitB: Unit) { + if (unitA.model !== unitB.model) return + if (unitA.model.sourceData.kind !== 'mmCIF') return + + const restraints = ModelCrossLinkRestraint.Provider.get(unitA.model) + if (!restraints) return + + const rA = new Map<number, StructureElement.UnitIndex>(); + const rB = new Map<number, StructureElement.UnitIndex>(); + _addRestraints(rA, unitA, restraints) + _addRestraints(rB, unitB, restraints) + + rA.forEach((indexA, ri) => { + const indexB = rB.get(ri) + if (indexB !== undefined) { + pairs.push( + createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri), + createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri) + ) + } + }) +} + +function extractIntra(pairs: CrossLinkRestraint[], unit: Unit) { + if (unit.model.sourceData.kind !== 'mmCIF') return + + const restraints = ModelCrossLinkRestraint.Provider.get(unit.model) + if (!restraints) return + + const { elements } = unit; + const elementCount = elements.length; + const kind = unit.kind + + const r = new Map<number, StructureElement.UnitIndex[]>(); + + for (let i = 0; i < elementCount; i++) { + const e = elements[i]; + restraints.getIndicesByElement(e, kind).forEach(ri => { + const il = r.get(ri) + if (il) il.push(i as StructureElement.UnitIndex) + else r.set(ri, [i as StructureElement.UnitIndex]) + }) + } + + r.forEach((il, ri) => { + if (il.length < 2) return + const [ indexA, indexB ] = il + pairs.push( + createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri), + createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri) + ) + }) +} + +function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: ModelCrossLinkRestraint, row: number): CrossLinkRestraint { + return { + unitA, indexA, unitB, indexB, + + restraintType: restraints.data.restraint_type.value(row), + distanceThreshold: restraints.data.distance_threshold.value(row), + psi: restraints.data.psi.value(row), + sigma1: restraints.data.sigma_1.value(row), + sigma2: restraints.data.sigma_2.value(row), + } +} + +function extractCrossLinkRestraints(structure: Structure): PairRestraints<CrossLinkRestraint> { + const pairs: CrossLinkRestraint[] = [] + if (!structure.models.some(m => ModelCrossLinkRestraint.Provider.get(m))) { + return new PairRestraints(pairs) + } + + const n = structure.units.length + for (let i = 0; i < n; ++i) { + const unitA = structure.units[i] + extractIntra(pairs, unitA) + for (let j = i + 1; j < n; ++j) { + const unitB = structure.units[j] + if (unitA.model === unitB.model) { + extractInter(pairs, unitA, unitB) + } + } + } + + return new PairRestraints(pairs) +} \ No newline at end of file diff --git a/src/mol-model-props/integrative/cross-link-restraint/representation.ts b/src/mol-model-props/integrative/cross-link-restraint/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..31718c637cfc91321088ad32d386856011d639d1 --- /dev/null +++ b/src/mol-model-props/integrative/cross-link-restraint/representation.ts @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation'; +import { ThemeRegistryContext } from '../../../mol-theme/theme'; +import { Theme } from '../../../mol-theme/theme'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { Vec3 } from '../../../mol-math/linear-algebra'; +import { LocationIterator } from '../../../mol-geo/util/location-iterator'; +import { PickingId } from '../../../mol-geo/geometry/picking'; +import { EmptyLoci, Loci } from '../../../mol-model/loci'; +import { Interval } from '../../../mol-data/int'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { Structure, StructureElement } from '../../../mol-model/structure'; +import { VisualContext } from '../../../mol-repr/visual'; +import { createLinkCylinderMesh, LinkCylinderParams } from '../../../mol-repr/structure/visual/util/link'; +import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual'; +import { VisualUpdateState } from '../../../mol-repr/util'; +import { ComplexRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider } from '../../../mol-repr/structure/representation'; +import { UnitKind, UnitKindOptions } from '../../../mol-repr/structure/visual/util/common'; +import { CustomProperty } from '../../common/custom-property'; +import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property'; + +function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintCylinderParams>, mesh?: Mesh) { + + const crossLinks = CrossLinkRestraintProvider.get(structure).value! + if (!crossLinks.count) return Mesh.createEmpty(mesh) + const { sizeFactor } = props + + const location = StructureElement.Location.create(structure) + + const builderProps = { + linkCount: crossLinks.count, + position: (posA: Vec3, posB: Vec3, edgeIndex: number) => { + const b = crossLinks.pairs[edgeIndex] + const uA = b.unitA, uB = b.unitB + uA.conformation.position(uA.elements[b.indexA], posA) + uB.conformation.position(uB.elements[b.indexB], posB) + }, + radius: (edgeIndex: number) => { + const b = crossLinks.pairs[edgeIndex] + location.unit = b.unitA + location.element = b.unitA.elements[b.indexA] + return theme.size.size(location) * sizeFactor + }, + } + + return createLinkCylinderMesh(ctx, builderProps, props, mesh) +} + +export const CrossLinkRestraintCylinderParams = { + ...ComplexMeshParams, + ...LinkCylinderParams, + sizeFactor: PD.Numeric(0.5, { min: 0, max: 10, step: 0.1 }), +} +export type CrossLinkRestraintCylinderParams = typeof CrossLinkRestraintCylinderParams + +export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintCylinderParams> { + return ComplexMeshVisual<CrossLinkRestraintCylinderParams>({ + defaultProps: PD.getDefaultValues(CrossLinkRestraintCylinderParams), + createGeometry: createCrossLinkRestraintCylinderMesh, + createLocationIterator: createCrossLinkRestraintIterator, + getLoci: getLinkLoci, + eachLocation: eachCrossLink, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintCylinderParams>, currentProps: PD.Values<CrossLinkRestraintCylinderParams>) => { + state.createGeometry = ( + newProps.sizeFactor !== currentProps.sizeFactor || + newProps.radialSegments !== currentProps.radialSegments || + newProps.linkCap !== currentProps.linkCap + ) + } + }, materialId) +} + +function createCrossLinkRestraintIterator(structure: Structure): LocationIterator { + const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value! + const { pairs } = crossLinkRestraints + const groupCount = pairs.length + const instanceCount = 1 + const location = CrossLinkRestraint.Location(crossLinkRestraints, structure) + const getLocation = (groupIndex: number) => { + location.element = groupIndex + return location + } + return LocationIterator(groupCount, instanceCount, getLocation, true) +} + +function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { + const { objectId, groupId } = pickingId + if (id === objectId) { + const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value! + const pair = crossLinkRestraints.pairs[groupId] + if (pair) { + return CrossLinkRestraint.Loci(structure, crossLinkRestraints, [groupId]) + } + } + return EmptyLoci +} + +function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) { + let changed = false + if (CrossLinkRestraint.isLoci(loci)) { + if (!Structure.areEquivalent(loci.data.structure, structure)) return false + const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value! + if (loci.data.crossLinkRestraints !== crossLinkRestraints) return false + + for (const e of loci.elements) { + if (apply(Interval.ofSingleton(e))) changed = true + } + } + return changed +} + +// + +const CrossLinkRestraintVisuals = { + 'cross-link-restraint': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintCylinderParams>) => ComplexRepresentation('Cross-link restraint', ctx, getParams, CrossLinkRestraintVisual), +} + +export const CrossLinkRestraintParams = { + ...CrossLinkRestraintCylinderParams, + unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions), +} +export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams +export function getCrossLinkRestraintParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(CrossLinkRestraintParams) +} + +export type CrossLinkRestraintRepresentation = StructureRepresentation<CrossLinkRestraintParams> +export function CrossLinkRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintParams>): CrossLinkRestraintRepresentation { + return Representation.createMulti('CrossLinkRestraint', ctx, getParams, StructureRepresentationStateBuilder, CrossLinkRestraintVisuals as unknown as Representation.Def<Structure, CrossLinkRestraintParams>) +} + +export const CrossLinkRestraintRepresentationProvider: StructureRepresentationProvider<CrossLinkRestraintParams> = { + label: 'Cross Link Restraint', + description: 'Displays cross-link restraints.', + factory: CrossLinkRestraintRepresentation, + getParams: getCrossLinkRestraintParams, + defaultValues: PD.getDefaultValues(CrossLinkRestraintParams), + defaultColorTheme: { name: CrossLinkRestraint.Tag.CrossLinkRestraint }, + defaultSizeTheme: { name: 'uniform' }, + isApplicable: (structure: Structure) => CrossLinkRestraint.isApplicable(structure), + ensureCustomProperties: (ctx: CustomProperty.Context, structure: Structure) => { + return CrossLinkRestraintProvider.attach(ctx, structure) + } +} \ No newline at end of file diff --git a/src/mol-model-props/integrative/pair-restraints.ts b/src/mol-model-props/integrative/pair-restraints.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a74a4412957f165d44893263ec4a01082894c01 --- /dev/null +++ b/src/mol-model-props/integrative/pair-restraints.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { StructureElement, Unit } from '../../mol-model/structure'; + +const emptyArray: number[] = [] + +export interface PairRestraint { + readonly unitA: Unit, + readonly unitB: Unit, + readonly indexA: StructureElement.UnitIndex, + readonly indexB: StructureElement.UnitIndex, +} + +function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) { + return `${indexA}|${unitA.id}|${indexB}|${unitB.id}` +} + +export class PairRestraints<T extends PairRestraint> { + readonly count: number + private readonly pairKeyIndices: Map<string, number[]> + + /** Indices into this.pairs */ + getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> { + const key = getPairKey(indexA, unitA, indexB, unitB) + return this.pairKeyIndices.get(key) || emptyArray + } + + getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): T[] { + const indices = this.getPairIndices(indexA, unitA, indexB, unitB) + return indices.map(idx => this.pairs[idx]) + } + + constructor(public pairs: ReadonlyArray<T>) { + const pairKeyIndices = new Map<string, number[]>() + this.pairs.forEach((p, i) => { + const key = getPairKey(p.indexA, p.unitA, p.indexB, p.unitB) + const indices = pairKeyIndices.get(key) + if (indices) indices.push(i) + else pairKeyIndices.set(key, [i]) + }) + + this.count = pairs.length + this.pairKeyIndices = pairKeyIndices + } +} \ No newline at end of file diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index e153de014ba3749c37f0700edb55b9f0a01698b3..1950f925299c9d2e8f684e5831dbefe1f36e870c 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/structure-quality-report.ts @@ -119,7 +119,7 @@ export type StructureQualityReportParams = typeof StructureQualityReportParams export type StructureQualityReportProps = PD.Values<StructureQualityReportParams> export const StructureQualityReportProvider: CustomModelProperty.Provider<StructureQualityReportParams, StructureQualityReport> = CustomModelProperty.createProvider({ - label: 'PDBe Structure Quality Report', + label: 'Structure Quality Report', descriptor: CustomPropertyDescriptor<ReportExportContext, any>({ name: 'pdbe_structure_quality_report', cifExport: { diff --git a/src/mol-model-props/pdbe/themes/structure-quality-report.ts b/src/mol-model-props/pdbe/themes/structure-quality-report.ts index e3296bab946d7dc478f50b5b20c3cfe05a533ef4..5f624e0f26a1944fb890826b3b7a72a8cee56ec0 100644 --- a/src/mol-model-props/pdbe/themes/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/themes/structure-quality-report.ts @@ -72,13 +72,14 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P granularity: 'group', color: color, props: props, - description: 'Assigns residue colors according to the number of issues or a specific issue in the PDBe Validation Report.', + description: 'Assigns residue colors according to the number of quality issues or a specific quality issue. Data from wwPDB Validation Report, obtained via PDBe.', legend: TableLegend(ValidationColorTable) } } export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params> = { - label: 'PDBe Structure Quality Report', + label: 'Structure Quality Report', + category: ColorTheme.Category.Validation, factory: StructureQualityReportColorTheme, getParams: ctx => { const issueTypes = StructureQualityReport.getIssueTypes(ctx.structure); diff --git a/src/mol-model-props/rcsb/assembly-symmetry.ts b/src/mol-model-props/rcsb/assembly-symmetry.ts index 6dce3e871e3ebd414256ecc7fb2af5825fe524f6..09a370b2ecd52b8a91fafcb91b73a5e9654190bf 100644 --- a/src/mol-model-props/rcsb/assembly-symmetry.ts +++ b/src/mol-model-props/rcsb/assembly-symmetry.ts @@ -15,6 +15,7 @@ import { CustomProperty } from '../common/custom-property'; import { NonNullableArray } from '../../mol-util/type-helpers'; import { CustomStructureProperty } from '../common/custom-structure-property'; import { MmcifFormat } from '../../mol-model-formats/structure/mmcif'; +import { ReadonlyVec3 } from '../../mol-math/linear-algebra/3d/vec3'; const BiologicalAssemblyNames = new Set([ 'author_and_software_defined_assembly', @@ -26,6 +27,11 @@ const BiologicalAssemblyNames = new Set([ ]) export namespace AssemblySymmetry { + export enum Tag { + Cluster = 'rcsb-assembly-symmetry-cluster', + Representation = 'rcsb-assembly-symmetry-3d' + } + export const DefaultServerUrl = 'https://data-beta.rcsb.org/graphql' export function isApplicable(structure?: Structure): boolean { @@ -37,6 +43,7 @@ export namespace AssemblySymmetry { const mmcif = structure.models[0].sourceData.data.db if (!mmcif.pdbx_struct_assembly.details.isDefined) return false const id = structure.units[0].conformation.operator.assembly.id + if (id === '' || id === 'deposited') return true const indices = Column.indicesOf(mmcif.pdbx_struct_assembly.id, e => e === id) if (indices.length !== 1) return false const details = mmcif.pdbx_struct_assembly.details.value(indices[0]) @@ -48,27 +55,35 @@ export namespace AssemblySymmetry { const client = new GraphQLClient(props.serverUrl, ctx.fetch) const variables: AssemblySymmetryQueryVariables = { - assembly_id: structure.units[0].conformation.operator.assembly.id, + assembly_id: structure.units[0].conformation.operator.assembly.id || 'deposited', entry_id: structure.units[0].model.entryId } const result = await client.request<AssemblySymmetryQuery>(ctx.runtime, query, variables) if (!result.assembly?.rcsb_struct_symmetry) { - throw new Error('missing fields') + console.error('expected `rcsb_struct_symmetry` field') + return [] } return result.assembly.rcsb_struct_symmetry as AssemblySymmetryValue } + + export type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }> + export function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes { + return !!x && x.length > 0 + } } export function getSymmetrySelectParam(structure?: Structure) { - const param = PD.Select<number>(0, [[0, 'No Symmetries']]) + const param = PD.Select<number>(-1, [[-1, 'No Symmetries']]) if (structure) { const assemblySymmetry = AssemblySymmetryProvider.get(structure).value if (assemblySymmetry) { const options: [number, string][] = [] for (let i = 0, il = assemblySymmetry.length; i < il; ++i) { const { symbol, kind } = assemblySymmetry[i] - options.push([ i, `${i + 1}: ${symbol} ${kind}` ]) + if (symbol !== 'C1') { + options.push([ i, `${i + 1}: ${symbol} ${kind}` ]) + } } if (options.length) { param.options = options diff --git a/src/mol-model-props/rcsb/representations/assembly-symmetry.ts b/src/mol-model-props/rcsb/representations/assembly-symmetry.ts index b2c2d994f281bc48e7d52175fdc73f0e2e427969..b18fc0b27b51ac96c22d0337d13bb6f3e56e7e29 100644 --- a/src/mol-model-props/rcsb/representations/assembly-symmetry.ts +++ b/src/mol-model-props/rcsb/representations/assembly-symmetry.ts @@ -5,7 +5,7 @@ */ import { ParamDefinition as PD } from '../../../mol-util/param-definition'; -import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider } from '../assembly-symmetry'; +import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; import { Vec3, Mat4 } from '../../../mol-math/linear-algebra'; import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder'; @@ -29,7 +29,6 @@ import { TetrahedronCage } from '../../../mol-geo/primitive/tetrahedron'; import { IcosahedronCage } from '../../../mol-geo/primitive/icosahedron'; import { degToRad, radToDeg } from '../../../mol-math/misc'; import { Mutable } from '../../../mol-util/type-helpers'; -import { ReadonlyVec3 } from '../../../mol-math/linear-algebra/3d/vec3'; import { equalEps } from '../../../mol-math/linear-algebra/3d/common'; import { Structure } from '../../../mol-model/structure'; import { isInteger } from '../../../mol-util/number'; @@ -87,11 +86,6 @@ export type AssemblySymmetryProps = PD.Values<AssemblySymmetryParams> // -type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }> -function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes { - return !!x && x.length > 0 -} - function getAssemblyName(s: Structure) { const { id } = s.units[0].conformation.operator.assembly return isInteger(id) ? `Assembly ${id}` : id @@ -122,7 +116,7 @@ function getAxesMesh(data: AssemblySymmetryValue, props: PD.Values<AxesParams>, const { symmetryIndex, scale } = props const { rotation_axes } = data[symmetryIndex] - if (!isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh) + if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh) const { start, end } = rotation_axes[0] const radius = (Vec3.distance(start, end) / 500) * scale @@ -227,11 +221,11 @@ function getSymbolScale(symbol: string) { return 1 } -function setSymbolTransform(t: Mat4, symbol: string, axes: RotationAxes, size: number, structure: Structure) { +function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.RotationAxes, size: number, structure: Structure) { const eye = Vec3() const target = Vec3() const up = Vec3() - let pair: Mutable<RotationAxes> | undefined = undefined + let pair: Mutable<AssemblySymmetry.RotationAxes> | undefined = undefined if (symbol.startsWith('C')) { pair = [axes[0]] @@ -288,7 +282,7 @@ function getCageMesh(data: Structure, props: PD.Values<CageParams>, mesh?: Mesh) const { symmetryIndex, scale } = props const { rotation_axes, symbol } = assemblySymmetry[symmetryIndex] - if (!isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh) + if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh) const cage = getSymbolCage(symbol) if (!cage) return Mesh.createEmpty(mesh) @@ -329,5 +323,5 @@ function getCageShape(ctx: RuntimeContext, data: Structure, props: AssemblySymme export type AssemblySymmetryRepresentation = Representation<Structure, AssemblySymmetryParams> export function AssemblySymmetryRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, AssemblySymmetryParams>): AssemblySymmetryRepresentation { - return Representation.createMulti('Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>) + return Representation.createMulti('Assembly Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>) } \ No newline at end of file diff --git a/src/mol-model-props/rcsb/representations/validation-report-clashes.ts b/src/mol-model-props/rcsb/representations/validation-report-clashes.ts index be69dbcda1645c51fda6b31ec0768f2cc15460c4..7f05f21e29f49b49b3192d30de8fa4003c9d6b82 100644 --- a/src/mol-model-props/rcsb/representations/validation-report-clashes.ts +++ b/src/mol-model-props/rcsb/representations/validation-report-clashes.ts @@ -279,8 +279,8 @@ export function ClashesRepresentation(ctx: RepresentationContext, getParams: Rep } export const ClashesRepresentationProvider: StructureRepresentationProvider<ClashesParams> = { - label: 'RCSB Clashes', - description: 'Displays clashes between atoms as disks.', + label: 'Validation Clashes', + description: 'Displays clashes between atoms as disks. Data from wwPDB Validation Report, obtained via RCSB PDB.', factory: ClashesRepresentation, getParams: getClashesParams, defaultValues: PD.getDefaultValues(ClashesParams), diff --git a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts index e32a62f46d426ad45635add7173fcc7039fd5b8e..5ef15770e2c16dd1b724849768f0cbb4e46d8494 100644 --- a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts +++ b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts @@ -59,11 +59,13 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props: for (let j = 0, jl = members.length; j < jl; ++j) { const asymId = members[j]!.asym_id const operList = [...members[j]!.pdbx_struct_oper_list_ids || []] as string[] - if (operList.length === 0) operList.push('1') // TODO hack assuming '1' is the id of the identity operator clusterByMember.set(clusterMemberKey(asymId, operList), i) + if (operList.length === 0) { + operList.push('1') // TODO hack assuming '1' is the id of the identity operator + clusterByMember.set(clusterMemberKey(asymId, operList), i) + } } } - const palette = getPalette(clusters.length, props) legend = palette.legend @@ -84,13 +86,14 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props: color, props, contextHash, - description: 'Assigns chain colors according to assembly symmetry cluster membership.', + description: 'Assigns chain colors according to assembly symmetry cluster membership calculated with BioJava and obtained via RCSB PDB.', legend } } export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = { - label: 'RCSB Assembly Symmetry Cluster', + label: 'Assembly Symmetry Cluster', + category: ColorTheme.Category.Symmetry, factory: AssemblySymmetryClusterColorTheme, getParams: getAssemblySymmetryClusterColorThemeParams, defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams), diff --git a/src/mol-model-props/rcsb/themes/density-fit.ts b/src/mol-model-props/rcsb/themes/density-fit.ts index 9bc3d3d0fe4f8756e9483ad1b205e2966629a677..0661d73d228daf94cf144292aa26404113533b1c 100644 --- a/src/mol-model-props/rcsb/themes/density-fit.ts +++ b/src/mol-model-props/rcsb/themes/density-fit.ts @@ -55,13 +55,14 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe color, props, contextHash, - description: 'Assigns residue colors according to the density fit using normalized Real Space R (RSRZ) for polymer residues and real space correlation coefficient (RSCC) for ligands. Colors range from poor (RSRZ = 2 or RSCC = 0.678) - to better (RSRZ = 0 or RSCC = 1.0).', + description: 'Assigns residue colors according to the density fit using normalized Real Space R (RSRZ) for polymer residues and real space correlation coefficient (RSCC) for ligands. Colors range from poor (RSRZ = 2 or RSCC = 0.678) - to better (RSRZ = 0 or RSCC = 1.0). Data from wwPDB Validation Report, obtained via RCSB PDB.', legend: scaleRsrz.legend } } export const DensityFitColorThemeProvider: ColorTheme.Provider<{}> = { - label: 'RCSB Density Fit', + label: 'Density Fit', + category: ColorTheme.Category.Validation, factory: DensityFitColorTheme, getParams: () => ({}), defaultValues: PD.getDefaultValues({}), diff --git a/src/mol-model-props/rcsb/themes/geometry-quality.ts b/src/mol-model-props/rcsb/themes/geometry-quality.ts index 1f39b8de4b19e47b1765c0c85ae93cbe7ab9c64f..834ccefab83e3a9807c063e41f1ab8749eac0fa6 100644 --- a/src/mol-model-props/rcsb/themes/geometry-quality.ts +++ b/src/mol-model-props/rcsb/themes/geometry-quality.ts @@ -95,13 +95,14 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value color, props, contextHash, - description: 'Assigns residue colors according to the number of (filtered) geometry issues.', + description: 'Assigns residue colors according to the number of (filtered) geometry issues. Data from wwPDB Validation Report, obtained via RCSB PDB.', legend: ColorLegend } } export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQualityColorThemeParams> = { - label: 'RCSB Geometry Quality', + label: 'Geometry Quality', + category: ColorTheme.Category.Validation, factory: GeometryQualityColorTheme, getParams: getGeometricQualityColorThemeParams, defaultValues: PD.getDefaultValues(getGeometricQualityColorThemeParams({})), diff --git a/src/mol-model-props/rcsb/themes/random-coil-index.ts b/src/mol-model-props/rcsb/themes/random-coil-index.ts index 596aa205587e6fbbade46e08d205404414e226a5..1dedd686f457c9380f8b857046956a3200238fd1 100644 --- a/src/mol-model-props/rcsb/themes/random-coil-index.ts +++ b/src/mol-model-props/rcsb/themes/random-coil-index.ts @@ -46,13 +46,14 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col color, props, contextHash, - description: 'Assigns residue colors according to the Random Coil Index value.', + description: 'Assigns residue colors according to the Random Coil Index value. Data from wwPDB Validation Report, obtained via RCSB PDB.', legend: scale.legend } } export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}> = { - label: 'RCSB Random Coil Index', + label: 'Random Coil Index', + category: ColorTheme.Category.Validation, factory: RandomCoilIndexColorTheme, getParams: () => ({}), defaultValues: PD.getDefaultValues({}), diff --git a/src/mol-model-props/rcsb/validation-report.ts b/src/mol-model-props/rcsb/validation-report.ts index ada844656b5fd832507d19efae31e330cbe3d98c..307c05dc428375e0ecfe5da2a6a0e7ee95578170 100644 --- a/src/mol-model-props/rcsb/validation-report.ts +++ b/src/mol-model-props/rcsb/validation-report.ts @@ -19,6 +19,9 @@ import { arrayMax } from '../../mol-util/array'; import { equalEps } from '../../mol-math/linear-algebra/3d/common'; import { Vec3 } from '../../mol-math/linear-algebra'; import { MmcifFormat } from '../../mol-model-formats/structure/mmcif'; +import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler'; +import { CustomPropSymbol } from '../../mol-script/language/symbol'; +import Type from '../../mol-script/language/type'; export { ValidationReport } @@ -118,6 +121,25 @@ namespace ValidationReport { case 'server': return fetch(ctx, model, props.source.params) } } + + export const symbols = { + hasClash: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.has-clash', Type.Bool), + ctx => { + const { unit, element } = ctx.element + if (!Unit.isAtomic(unit)) return 0 + const validationReport = ValidationReportProvider.get(unit.model).value + return validationReport && validationReport.clashes.getVertexEdgeCount(element) > 0 + } + ), + issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.issue-count', Type.Num), + ctx => { + const { unit, element } = ctx.element + if (!Unit.isAtomic(unit)) return 0 + const validationReport = ValidationReportProvider.get(unit.model).value + return validationReport?.geometryIssues.get(unit.residueIndex[element])?.size || 0 + } + ), + } } const FileSourceParams = { @@ -140,10 +162,10 @@ export type ValidationReportParams = typeof ValidationReportParams export type ValidationReportProps = PD.Values<ValidationReportParams> export const ValidationReportProvider: CustomModelProperty.Provider<ValidationReportParams, ValidationReport> = CustomModelProperty.createProvider({ - label: 'RCSB Validation Report', + label: 'Validation Report', descriptor: CustomPropertyDescriptor({ name: 'rcsb_validation_report', - // TODO `cifExport` and `symbol` + symbols: ValidationReport.symbols }), type: 'dynamic', defaultParams: ValidationReportParams, diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index f80c1468a9566c1f8dc0bad747490c5545ee30a0..d8151534a663129aa1ba109219ad7d3f35d1ebca 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -42,7 +42,8 @@ export function isDataLoci(x?: Loci): x is DataLoci { return !!x && x.kind === 'data-loci'; } export function areDataLociEqual(a: DataLoci, b: DataLoci) { - if (a.data !== b.data || a.tag !== b.tag) return false + // use shallowEqual to allow simple data objects that are contructed on-the-fly + if (!shallowEqual(a.data, b.data) || a.tag !== b.tag) return false if (a.elements.length !== b.elements.length) return false for (let i = 0, il = a.elements.length; i < il; ++i) { if (!shallowEqual(a.elements[i], b.elements[i])) return false diff --git a/src/mol-model/sequence/sequence.ts b/src/mol-model/sequence/sequence.ts index 946e953b38d0873a8687647bbf1bed559805d86c..5e381e198a1da2eefc611c1754a53514b799bcbb 100644 --- a/src/mol-model/sequence/sequence.ts +++ b/src/mol-model/sequence/sequence.ts @@ -80,11 +80,11 @@ namespace Sequence { return code } - export function ofResidueNames(compId: Column<string>, seqId: Column<number>, modifiedMap?: ReadonlyMap<string, string>): Sequence { + export function ofResidueNames(compId: Column<string>, seqId: Column<number>): Sequence { if (seqId.rowCount === 0) throw new Error('cannot be empty'); const kind = determineKind(compId); - return new ResidueNamesImpl(kind, compId, seqId, modifiedMap) as Sequence; + return new ResidueNamesImpl(kind, compId, seqId) as Sequence; } class ResidueNamesImpl<K extends Kind, Alphabet extends string> implements Base<K, Alphabet> { @@ -154,11 +154,7 @@ namespace Sequence { const code = this.codeFromName(name); // in case of MICROHETEROGENEITY `sequenceArray[idx]` may already be set if (!sequenceArray[idx] || sequenceArray[idx] === '-') { - if (code === 'X' && this.modifiedMap && this.modifiedMap.has(name)) { - sequenceArray[idx] = this.modifiedMap.get(name)! - } else { - sequenceArray[idx] = code; - } + sequenceArray[idx] = code; } labels[idx].push(code === 'X' ? name : code); compIds[seqId].push(name); @@ -183,7 +179,7 @@ namespace Sequence { this._length = count } - constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>, private modifiedMap?: ReadonlyMap<string, string>) { + constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>) { this.codeFromName = codeProvider(kind) } diff --git a/src/mol-model/structure/export/categories/modified-residues.ts b/src/mol-model/structure/export/categories/modified-residues.ts deleted file mode 100644 index 51726e8c6b2a1df02b78335e1b9087f7c01b3a33..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/export/categories/modified-residues.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2017-2018 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 { Segmentation } from '../../../../mol-data/int'; -import { CifWriter } from '../../../../mol-io/writer/cif'; -import { StructureElement, StructureProperties as P, Unit } from '../../../structure'; -import { CifExportContext } from '../mmcif'; - -import CifField = CifWriter.Field -import CifCategory = CifWriter.Category - -const pdbx_struct_mod_residue_fields: CifField<number, StructureElement.Location[]>[] = [ - CifField.index('id'), - CifField.str(`label_comp_id`, (i, xs) => P.residue.label_comp_id(xs[i])), - CifField.int(`label_seq_id`, (i, xs) => P.residue.label_seq_id(xs[i])), - CifField.str(`pdbx_PDB_ins_code`, (i, xs) => P.residue.pdbx_PDB_ins_code(xs[i])), - CifField.str(`label_asym_id`, (i, xs) => P.chain.label_asym_id(xs[i])), - CifField.str(`label_entity_id`, (i, xs) => P.chain.label_entity_id(xs[i])), - CifField.str(`auth_comp_id`, (i, xs) => P.residue.auth_comp_id(xs[i])), - CifField.int(`auth_seq_id`, (i, xs) => P.residue.auth_seq_id(xs[i])), - CifField.str(`auth_asym_id`, (i, xs) => P.chain.auth_asym_id(xs[i])), - CifField.str<number, StructureElement.Location[]>('parent_comp_id', (i, xs) => xs[i].unit.model.properties.modifiedResidues.parentId.get(P.residue.label_comp_id(xs[i]))!), - CifField.str('details', (i, xs) => xs[i].unit.model.properties.modifiedResidues.details.get(P.residue.label_comp_id(xs[i]))!) -]; - -function getModifiedResidues({ structures }: CifExportContext): StructureElement.Location[] { - // TODO: can different models (in the same mmCIF file) have different modified residues? - const structure = structures[0], model = structure.model; - const map = model.properties.modifiedResidues.parentId; - if (!map.size) return []; - - const ret = []; - const prop = P.residue.label_comp_id; - const loc = StructureElement.Location.create(structure); - for (const unit of structure.units) { - if (!Unit.isAtomic(unit) || !unit.conformation.operator.isIdentity) continue; - const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements); - loc.unit = unit; - while (residues.hasNext) { - const seg = residues.move(); - loc.element = unit.elements[seg.start]; - const name = prop(loc); - if (map.has(name)) { - ret[ret.length] = StructureElement.Location.clone(loc); - } - } - } - return ret; -} - -export const _pdbx_struct_mod_residue: CifCategory<CifExportContext> = { - name: 'pdbx_struct_mod_residue', - instance(ctx) { - const residues = getModifiedResidues(ctx); - return { - fields: pdbx_struct_mod_residue_fields, - source: [{ data: residues, rowCount: residues.length }] - }; - } -} \ No newline at end of file diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts index 45071c82cddc16fb1654c76e1e69595d1bef0622..c90223bb94e5df514d24d19cb64df12c4b04cb4d 100644 --- a/src/mol-model/structure/export/mmcif.ts +++ b/src/mol-model/structure/export/mmcif.ts @@ -11,7 +11,6 @@ import { Structure } from '../structure' import { _atom_site } from './categories/atom_site'; import CifCategory = CifWriter.Category import { _struct_conf, _struct_sheet_range } from './categories/secondary-structure'; -import { _pdbx_struct_mod_residue } from './categories/modified-residues'; import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc'; import { Model } from '../model'; import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils'; @@ -83,7 +82,6 @@ const Categories = [ copy_mmCif_category('atom_sites'), _pdbx_nonpoly_scheme, - _pdbx_struct_mod_residue, // Atoms _atom_site diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts index 04dd2a0dd482c28713a5bb4c68686bf244752153..220fa825faa923cf9cdcfd042940213d4248b7e6 100644 --- a/src/mol-model/structure/model/model.ts +++ b/src/mol-model/structure/model/model.ts @@ -9,7 +9,7 @@ import UUID from '../../../mol-util/uuid'; import StructureSequence from './properties/sequence'; import { AtomicHierarchy, AtomicConformation, AtomicRanges } from './properties/atomic'; import { CoarseHierarchy, CoarseConformation } from './properties/coarse'; -import { Entities, ChemicalComponentMap, MissingResidues } from './properties/common'; +import { Entities, ChemicalComponentMap, MissingResidues, StructAsymMap } from './properties/common'; import { CustomProperties } from '../common/custom-property'; import { SaccharideComponentMap } from '../structure/carbohydrates/constants'; import { ModelFormat } from '../../../mol-model-formats/structure/format'; @@ -53,17 +53,14 @@ export interface Model extends Readonly<{ atomicRanges: AtomicRanges, properties: { - /** maps modified residue name to its parent */ - readonly modifiedResidues: Readonly<{ - parentId: ReadonlyMap<string, string>, - details: ReadonlyMap<string, string> - }>, /** map that holds details about unobserved or zero occurrence residues */ readonly missingResidues: MissingResidues, /** maps residue name to `ChemicalComponent` data */ readonly chemicalComponentMap: ChemicalComponentMap /** maps residue name to `SaccharideComponent` data */ readonly saccharideComponentMap: SaccharideComponentMap + /** maps label_asym_id name to `StructAsym` data */ + readonly structAsymMap: StructAsymMap }, customProperties: CustomProperties, diff --git a/src/mol-model/structure/model/properties/common.ts b/src/mol-model/structure/model/properties/common.ts index 84809978a4f22079e51edfbaca0f7b1433aff234..f8ae77e6b3869499d0eae32c3db939d6c92a8111 100644 --- a/src/mol-model/structure/model/properties/common.ts +++ b/src/mol-model/structure/model/properties/common.ts @@ -29,4 +29,7 @@ export interface MissingResidues { has(model_num: number, asym_id: string, seq_id: number): boolean get(model_num: number, asym_id: string, seq_id: number): MissingResidue | undefined readonly size: number -} \ No newline at end of file +} + +export type StructAsym = Table.Row<Pick<mmCIF_Schema['struct_asym'], 'id' | 'entity_id'> & { auth_id: Column.Schema.Str }> +export type StructAsymMap = ReadonlyMap<string, StructAsym> \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/sequence.ts b/src/mol-model/structure/model/properties/sequence.ts index 1e8b787b0bd4c7f2d50a185066df1022fcb36938..f05dd175091aad6396e32c882c774333924597e6 100644 --- a/src/mol-model/structure/model/properties/sequence.ts +++ b/src/mol-model/structure/model/properties/sequence.ts @@ -37,13 +37,13 @@ namespace StructureSequence { return { sequences, byEntityKey } } - export function fromHierarchy(entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy, modResMap?: ReadonlyMap<string, string>): StructureSequence { - const atomic = fromAtomicHierarchy(entities, atomicHierarchy, modResMap) + export function fromHierarchy(entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy): StructureSequence { + const atomic = fromAtomicHierarchy(entities, atomicHierarchy) const coarse = coarseHierarchy.isDefined ? fromCoarseHierarchy(entities, coarseHierarchy) : Empty return merge(atomic, coarse) } - export function fromAtomicHierarchy(entities: Entities, hierarchy: AtomicHierarchy, modResMap?: ReadonlyMap<string, string>): StructureSequence { + export function fromAtomicHierarchy(entities: Entities, hierarchy: AtomicHierarchy): StructureSequence { const { label_comp_id, label_seq_id } = hierarchy.residues const { chainAtomSegments, residueAtomSegments } = hierarchy const { count, offsets } = chainAtomSegments @@ -75,7 +75,7 @@ namespace StructureSequence { const num = Column.window(label_seq_id, rStart, rEnd); byEntityKey[entityKey] = { entityId: entities.data.id.value(entityKey), - sequence: Sequence.ofResidueNames(compId, num, modResMap) + sequence: Sequence.ofResidueNames(compId, num) }; sequences.push(byEntityKey[entityKey]); diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts index 95a1e8e41d6b9688c79b8316186748eb08501001..fd37efe1d499a5c0193eac441e13ad09cdbc80e3 100644 --- a/src/mol-model/structure/model/types.ts +++ b/src/mol-model/structure/model/types.ts @@ -137,7 +137,7 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set export const ProteinBackboneAtoms = new Set([ 'CA', 'C', 'N', 'O', 'O1', 'O2', 'OC1', 'OC2', 'OX1', 'OXT', - 'H', 'H1', 'H2', 'H3', 'HA', 'HN', + 'H', 'H1', 'H2', 'H3', 'HA', 'HN', 'HXT', 'BB' ]) @@ -257,7 +257,7 @@ export const DnaBaseNames = new Set([ 'DN' // unknown DNA base from CCD ]) export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ]) -export const PurineBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ]) +export const PurineBaseNames = new Set([ 'A', 'G', 'I', 'DA', 'DG', 'DI', 'APN', 'GPN' ]) export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ]) export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames) @@ -342,7 +342,7 @@ export function getDefaultChemicalComponent(compId: string): ChemicalComponent { formula_weight: 0, id: compId, name: compId, - mon_nstd_flag: 'n', + mon_nstd_flag: PolymerNames.has(compId) ? 'y' : 'n', pdbx_synonyms: [], type: getComponentType(compId) }; diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts index 76746ddb8b49af4dcda61173166e9c1c6d6df908..ffe5a13d5afa36d7c701cdfa54d8915fee2b555c 100644 --- a/src/mol-model/structure/structure/element/loci.ts +++ b/src/mol-model/structure/structure/element/loci.ts @@ -98,6 +98,24 @@ export namespace Loci { return Location.create(loci.structure, unit, element); } + export function firstElement(loci: Loci): Loci { + if (isEmpty(loci)) return loci; + return Loci(loci.structure, [{ + unit: loci.elements[0].unit, + indices: OrderedSet.ofSingleton(OrderedSet.start(loci.elements[0].indices)) + }]) + } + + export function firstResidue(loci: Loci): Loci { + if (isEmpty(loci)) return loci; + return extendToWholeResidues(firstElement(loci)) + } + + export function firstChain(loci: Loci): Loci { + if (isEmpty(loci)) return loci; + return extendToWholeChains(firstElement(loci)) + } + export function toStructure(loci: Loci): Structure { const units: Unit[] = [] for (const e of loci.elements) { diff --git a/src/mol-model/structure/structure/properties.ts b/src/mol-model/structure/structure/properties.ts index 48ab835a516d3c50613ad4e5004389db0f6d53a8..6890b2a742c7e14f48d490ecbaebfa0ef1e3e017 100644 --- a/src/mol-model/structure/structure/properties.ts +++ b/src/mol-model/structure/structure/properties.ts @@ -1,14 +1,15 @@ /** - * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2020 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 StructureElement from './element' import Unit from './unit' import { VdwRadius } from '../model/properties/atomic'; -import { ModelSecondaryStructure } from '../../../mol-model-formats/structure/property/secondary-structure'; import { SecondaryStructureType } from '../model/types'; +import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure'; function p<T>(p: StructureElement.Property<T>) { return p; } @@ -96,28 +97,18 @@ const residue = { pdbx_PDB_ins_code: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.pdbx_PDB_ins_code.value(l.unit.residueIndex[l.element])), // Properties - isModified: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.modifiedResidues.parentId.has(compId(l))), - modifiedParentName: p(l => { - if (!Unit.isAtomic(l.unit)) notAtomic() - const id = compId(l) - return l.unit.model.properties.modifiedResidues.parentId.get(id) || id - }), isNonStandard: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.mon_nstd_flag[0] !== 'y'), hasMicroheterogeneity: p(hasMicroheterogeneity), microheterogeneityCompIds: p(microheterogeneityCompIds), - // TODO implement as symbol in SecondaryStructureProvider (not ModelSecondaryStructure.Provider) secondary_structure_type: p(l => { if (!Unit.isAtomic(l.unit)) notAtomic() - const secondaryStructure = ModelSecondaryStructure.Provider.get(l.unit.model) - if (secondaryStructure) return secondaryStructure.type[l.unit.residueIndex[l.element]] - else return SecondaryStructureType.Flag.NA + const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id) + return secStruc?.type[l.unit.residueIndex[l.element]] ?? SecondaryStructureType.Flag.NA }), - // TODO implement as symbol in SecondaryStructureProvider (not ModelSecondaryStructure.Provider) secondary_structure_key: p(l => { if (!Unit.isAtomic(l.unit)) notAtomic() - const secondaryStructure = ModelSecondaryStructure.Provider.get(l.unit.model) - if (secondaryStructure) return secondaryStructure.key[l.unit.residueIndex[l.element]] - else return -1 + const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id) + return secStruc?.key[l.unit.residueIndex[l.element]] ?? -1 }), chem_comp_type: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.type), } diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index c03a2a97b212e350f5bcf609fc421b98bb2036d1..aac52cad54c5f86a8c3b55734057d100fd6178f0 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2020 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> @@ -16,7 +16,6 @@ import { StructureLookup3D } from './util/lookup3d'; import { CoarseElements } from '../model/properties/coarse'; import { StructureSubsetBuilder } from './util/subset-builder'; import { InterUnitBonds, computeInterUnitBonds, Bond } from './unit/bonds'; -import { PairRestraints, CrossLinkRestraint, extractCrossLinkRestraints } from './unit/pair-restraints'; import StructureSymmetry from './symmetry'; import StructureProperties from './properties'; import { ResidueIndex, ChainIndex, EntityIndex } from '../model/indexing'; @@ -41,7 +40,6 @@ class Structure { parent?: Structure, lookup3d?: StructureLookup3D, interUnitBonds?: InterUnitBonds, - crossLinkRestraints?: PairRestraints<CrossLinkRestraint>, unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>, unitSymmetryGroupsIndexMap?: IntMap<number>, carbohydrates?: Carbohydrates, @@ -228,12 +226,6 @@ class Structure { return this._props.interUnitBonds; } - get crossLinkRestraints() { - if (this._props.crossLinkRestraints) return this._props.crossLinkRestraints; - this._props.crossLinkRestraints = extractCrossLinkRestraints(this); - return this._props.crossLinkRestraints; - } - get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> { if (this._props.unitSymmetryGroups) return this._props.unitSymmetryGroups; this._props.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this); diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts index e6c5bae4673f2379bfaeb1b2296f905b74613b70..40df81d6ed48db7c2190141b34a06a9a26476a46 100644 --- a/src/mol-model/structure/structure/symmetry.ts +++ b/src/mol-model/structure/structure/symmetry.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2020 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> @@ -11,10 +11,11 @@ import { Spacegroup, SpacegroupCell, SymmetryOperator } from '../../../mol-math/ import { Vec3, Mat4 } from '../../../mol-math/linear-algebra'; import { RuntimeContext, Task } from '../../../mol-task'; import { Symmetry, Model } from '../model'; -import { QueryContext, StructureSelection } from '../query'; +import { QueryContext, StructureSelection, Queries as Q } from '../query'; import Structure from './structure'; import Unit from './unit'; import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry'; +import StructureProperties from './properties'; namespace StructureSymmetry { export function buildAssembly(structure: Structure, asmName: string) { @@ -48,6 +49,40 @@ namespace StructureSymmetry { }); } + export type Generators = { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[] + + export function buildSymmetryAssembly(structure: Structure, generators: Generators, symmetry: Symmetry) { + return Task.create('Build Symmetry Assembly', async ctx => { + const models = structure.models; + if (models.length !== 1) throw new Error('Can only build symmetry assemblies from structures based on 1 model.'); + + const modelCenter = Vec3() + const assembler = Structure.Builder({ label: structure.label }); + + const queryCtx = new QueryContext(structure); + + for (const g of generators) { + const selector = getSelector(g.asymIds); + const selection = selector(queryCtx); + if (StructureSelection.structureCount(selection) === 0) { + continue; + } + const { units } = StructureSelection.unionStructure(selection); + + for (const { index, shift: [i, j, k] } of g.operators) { + const operators = getOperatorsForIndex(symmetry, index, i, j, k, modelCenter) + for (const unit of units) { + for (const op of operators) { + assembler.addWithOperator(unit, op); + } + } + } + } + + return assembler.getStructure(); + }); + } + export function builderSymmetryMates(structure: Structure, radius: number) { return Task.create('Find Symmetry Mates', ctx => findMatesRadius(ctx, structure, radius)); } @@ -96,7 +131,35 @@ namespace StructureSymmetry { } } -function getOperators(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) { +function getSelector(asymIds: string[]) { + return Q.generators.atoms({ chainTest: Q.pred.and( + Q.pred.eq(ctx => StructureProperties.unit.operator_name(ctx.element), SymmetryOperator.DefaultName), + Q.pred.inSet(ctx => StructureProperties.chain.label_asym_id(ctx.element), asymIds) + )}); +} + +function getOperatorsForIndex(symmetry: Symmetry, index: number, i: number, j: number, k: number, modelCenter: Vec3) { + const { spacegroup, ncsOperators } = symmetry; + const operators: SymmetryOperator[] = [] + + const { toFractional } = spacegroup.cell + const ref = Vec3.transformMat4(Vec3(), modelCenter, toFractional) + + const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, index, i, j, k, ref) + if (ncsOperators && ncsOperators.length) { + for (let u = 0, ul = ncsOperators.length; u < ul; ++u) { + const ncsOp = ncsOperators![u] + const matrix = Mat4.mul(Mat4(), symOp.matrix, ncsOp.matrix) + const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp); + operators.push(operator) + } + } else { + operators.push(symOp) + } + return operators +} + +function getOperatorsForRange(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) { const { spacegroup, ncsOperators } = symmetry; const ncsCount = (ncsOperators && ncsOperators.length) || 0 const operators: SymmetryOperator[] = []; @@ -117,18 +180,7 @@ function getOperators(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCente for (let k = ijkMin[2]; k <= ijkMax[2]; k++) { // check if we have added identity as the 1st operator. if (!ncsCount && op === 0 && i === 0 && j === 0 && k === 0) continue; - - const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, op, i, j, k, ref) - if (ncsCount) { - for (let u = 0; u < ncsCount; ++u) { - const ncsOp = ncsOperators![u] - const matrix = Mat4.mul(Mat4.zero(), symOp.matrix, ncsOp.matrix) - const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp); - operators[operators.length] = operator; - } - } else { - operators[operators.length] = symOp; - } + operators.push(...getOperatorsForIndex(symmetry, op, i, j, k, ref)) } } } @@ -142,7 +194,7 @@ function getOperatorsCached333(symmetry: Symmetry, ref: Vec3) { } symmetry._operators_333 = { ref: Vec3.clone(ref), - operators: getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref) + operators: getOperatorsForRange(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref) }; return symmetry._operators_333.operators; } @@ -181,7 +233,7 @@ async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkM if (SpacegroupCell.isZero(spacegroup.cell)) return structure; const modelCenter = Model.getCenter(models[0]) - const operators = getOperators(symmetry, ijkMin, ijkMax, modelCenter); + const operators = getOperatorsForRange(symmetry, ijkMin, ijkMax, modelCenter); return assembleOperators(structure, operators); } diff --git a/src/mol-model/structure/structure/unit/pair-restraints.ts b/src/mol-model/structure/structure/unit/pair-restraints.ts deleted file mode 100644 index 890fb6b03666a40e5157d80430f0b098d2db98ad..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/structure/unit/pair-restraints.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -export * from './pair-restraints/data' -export * from './pair-restraints/extract-cross-links' -// export * from './pair-restraints/extract-predicted-contacts' -// export * from './pair-restraints/extract-distance-restraints' diff --git a/src/mol-model/structure/structure/unit/pair-restraints/data.ts b/src/mol-model/structure/structure/unit/pair-restraints/data.ts deleted file mode 100644 index f402aab8e991b6fbc7a03e4bdc4fb38fbb9ef622..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/structure/unit/pair-restraints/data.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import Unit from '../../unit'; -import { StructureElement } from '../../../structure'; - -const emptyArray: number[] = [] - -interface PairRestraint { - readonly unitA: Unit, - readonly unitB: Unit, - readonly indexA: StructureElement.UnitIndex, - readonly indexB: StructureElement.UnitIndex, -} - -function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) { - return `${indexA}|${unitA.id}|${indexB}|${unitB.id}` -} - -export class PairRestraints<T extends PairRestraint> { - readonly count: number - private readonly pairKeyIndices: Map<string, number[]> - - /** Indices into this.pairs */ - getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> { - const key = getPairKey(indexA, unitA, indexB, unitB) - const indices = this.pairKeyIndices.get(key) - return indices !== undefined ? indices : emptyArray - } - - getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): T[] | undefined { - const indices = this.getPairIndices(indexA, unitA, indexB, unitB) - return indices.length ? indices.map(idx => this.pairs[idx]) : undefined - } - - constructor(public pairs: ReadonlyArray<T>) { - const pairKeyIndices = new Map<string, number[]>() - this.pairs.forEach((p, i) => { - const key = getPairKey(p.indexA, p.unitA, p.indexB, p.unitB) - const indices = pairKeyIndices.get(key) - if (indices) indices.push(i) - else pairKeyIndices.set(key, [i]) - }) - - this.count = pairs.length - this.pairKeyIndices = pairKeyIndices - } -} - -export interface CrossLinkRestraint extends PairRestraint { - readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound' - readonly distanceThreshold: number - readonly psi: number - readonly sigma1: number - readonly sigma2: number -} - -export interface PredictedContactRestraint extends PairRestraint { - readonly distance_lower_limit: number - readonly distance_upper_limit: number - readonly probability: number - readonly restraint_type: 'lower bound' | 'upper bound' | 'lower and upper bound' - readonly model_granularity: 'by-residue' | 'by-feature' | 'by-atom' -} - -export interface DistanceRestraint extends PairRestraint { - readonly upper_limit: number - readonly upper_limit_esd: number - readonly lower_limit: number - readonly lower_limit_esd: number - readonly probability: number - readonly restraint_type: 'lower bound' | 'upper bound' | 'lower and upper bound' - readonly granularity: 'by-residue' | 'by-atom' -} \ No newline at end of file diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts deleted file mode 100644 index 0b98c288cd7e44f5409655d1919e232a726fdb0b..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import Unit from '../../unit'; -import Structure from '../../structure'; -import { PairRestraints, CrossLinkRestraint } from './data'; -import { StructureElement } from '../../../structure'; -import { ModelCrossLinkRestraint } from '../../../../../mol-model-formats/structure/property/pair-restraints/cross-links'; - -function _addRestraints(map: Map<number, number>, unit: Unit, restraints: ModelCrossLinkRestraint) { - const { elements } = unit; - const elementCount = elements.length; - const kind = unit.kind - - for (let i = 0; i < elementCount; i++) { - const e = elements[i]; - restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i)) - } -} - -function extractInter(pairs: CrossLinkRestraint[], unitA: Unit, unitB: Unit) { - if (unitA.model !== unitB.model) return - if (unitA.model.sourceData.kind !== 'mmCIF') return - - const restraints = ModelCrossLinkRestraint.Provider.get(unitA.model) - if (!restraints) return - - const rA = new Map<number, StructureElement.UnitIndex>(); - const rB = new Map<number, StructureElement.UnitIndex>(); - _addRestraints(rA, unitA, restraints) - _addRestraints(rB, unitB, restraints) - - rA.forEach((indexA, ri) => { - const indexB = rB.get(ri) - if (indexB !== undefined) { - pairs.push( - createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri), - createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri) - ) - } - }) -} - -function extractIntra(pairs: CrossLinkRestraint[], unit: Unit) { - if (unit.model.sourceData.kind !== 'mmCIF') return - - const restraints = ModelCrossLinkRestraint.Provider.get(unit.model) - if (!restraints) return - - const { elements } = unit; - const elementCount = elements.length; - const kind = unit.kind - - const r = new Map<number, StructureElement.UnitIndex[]>(); - - for (let i = 0; i < elementCount; i++) { - const e = elements[i]; - restraints.getIndicesByElement(e, kind).forEach(ri => { - const il = r.get(ri) - if (il) il.push(i as StructureElement.UnitIndex) - else r.set(ri, [i as StructureElement.UnitIndex]) - }) - } - - r.forEach((il, ri) => { - if (il.length < 2) return - const [ indexA, indexB ] = il - pairs.push( - createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri), - createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri) - ) - }) -} - -function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: ModelCrossLinkRestraint, row: number): CrossLinkRestraint { - return { - unitA, indexA, unitB, indexB, - - restraintType: restraints.data.restraint_type.value(row), - distanceThreshold: restraints.data.distance_threshold.value(row), - psi: restraints.data.psi.value(row), - sigma1: restraints.data.sigma_1.value(row), - sigma2: restraints.data.sigma_2.value(row), - } -} - -function extractCrossLinkRestraints(structure: Structure): PairRestraints<CrossLinkRestraint> { - const pairs: CrossLinkRestraint[] = [] - if (!structure.models.some(m => ModelCrossLinkRestraint.Provider.get(m))) { - return new PairRestraints(pairs) - } - - const n = structure.units.length - for (let i = 0; i < n; ++i) { - const unitA = structure.units[i] - extractIntra(pairs, unitA) - for (let j = i + 1; j < n; ++j) { - const unitB = structure.units[j] - if (unitA.model === unitB.model) { - extractInter(pairs, unitA, unitB) - } - } - } - - return new PairRestraints(pairs) -} - -export { extractCrossLinkRestraints }; diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts deleted file mode 100644 index f19797cb6a8dc5eed5dacaaf0201d24d458fba44..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -// TODO extract from `_ma_distance_restraints` \ No newline at end of file diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts deleted file mode 100644 index ad6d43f51f4d524171b5d66a2c19f4d0348c45cb..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -// TODO extract from `ihm_predicted_contact_restraint` \ No newline at end of file diff --git a/src/mol-plugin-ui/base.tsx b/src/mol-plugin-ui/base.tsx index d377417addc71ca14c8c4eec0c0cbccd37cdcc07..38d3977f1b6eecf3d819ab8e038ef5ea36cf048c 100644 --- a/src/mol-plugin-ui/base.tsx +++ b/src/mol-plugin-ui/base.tsx @@ -25,6 +25,7 @@ export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.C componentWillUnmount() { if (!this.subs) return; for (const s of this.subs) s.unsubscribe(); + this.subs = []; } protected init?(): void; diff --git a/src/mol-plugin-ui/controls/action-menu.tsx b/src/mol-plugin-ui/controls/action-menu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..016a4db71cf127f89be927c5dd5ec53885aa457f --- /dev/null +++ b/src/mol-plugin-ui/controls/action-menu.tsx @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react' +import { Icon } from './common'; +import { ParamDefinition } from '../../mol-util/param-definition'; + +export class ActionMenu extends React.PureComponent<ActionMenu.Props> { + hide = () => this.props.onSelect(void 0) + + render() { + const cmd = this.props; + + return <div className='msp-action-menu-options' style={{ marginTop: '1px' }}> + {cmd.header && <div className='msp-control-group-header' style={{ position: 'relative' }}> + <button className='msp-btn msp-btn-block' onClick={this.hide}> + <Icon name='off' style={{ position: 'absolute', right: '2px', top: 0 }} /> + <b>{cmd.header}</b> + </button> + </div>} + <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} /> + </div> + } +} + +export namespace ActionMenu { + export type Props = { items: Items, onSelect: OnSelect, header?: string, current?: Item | undefined } + + export type OnSelect = (item: Item | undefined) => void + + export type Items = string | Item | [Items] + export type Item = { label: string, icon?: string, value: unknown } + + export function Item(label: string, value: unknown): Item + export function Item(label: string, icon: string, value: unknown): Item + export function Item(label: string, iconOrValue: any, value?: unknown): Item { + if (value) return { label, icon: iconOrValue, value }; + return { label, value: iconOrValue }; + } + + export function createItems<T>(xs: ArrayLike<T>, options?: { label?: (t: T) => string, value?: (t: T) => any, category?: (t: T) => string | undefined }) { + const { label, value, category } = options || { }; + let cats: Map<string, (ActionMenu.Item | string)[]> | undefined = void 0; + const items: (ActionMenu.Item | (ActionMenu.Item | string)[] | string)[] = []; + for (let i = 0; i < xs.length; i++) { + const x = xs[i]; + + const catName = category?.(x); + const l = label ? label(x) : '' + x; + const v = value ? value(x) : x; + + if (!!catName) { + if (!cats) cats = new Map<string, (ActionMenu.Item | string)[]>(); + + let cat = cats.get(catName); + if (!cat) { + cat = [catName]; + cats.set(catName, cat); + items.push(cat); + } + cat.push(ActionMenu.Item(l, v)); + } else { + items.push(ActionMenu.Item(l, v)); + } + } + return items as ActionMenu.Items; + } + + type Opt = ParamDefinition.Select<any>['options'][0]; + const _selectOptions = { value: (o: Opt) => o[0], label: (o: Opt) => o[1], category: (o: Opt) => o[2] }; + + export function createItemsFromSelectParam(param: ParamDefinition.Select<any>) { + return createItems(param.options, _selectOptions); + } + + export function findItem(items: Items, value: any): Item | undefined { + if (typeof items === 'string') return; + if (isItem(items)) return items.value === value ? items : void 0; + for (const s of items) { + const found = findItem(s, value); + if (found) return found; + } + } + + export function getFirstItem(items: Items): Item | undefined { + if (typeof items === 'string') return; + if (isItem(items)) return items; + for (const s of items) { + const found = getFirstItem(s); + if (found) return found; + } + } +} + +type SectionProps = { header?: string, items: ActionMenu.Items, onSelect: ActionMenu.OnSelect, current: ActionMenu.Item | undefined } +type SectionState = { items: ActionMenu.Items, current: ActionMenu.Item | undefined, isExpanded: boolean } + +class Section extends React.PureComponent<SectionProps, SectionState> { + state = { + items: this.props.items, + current: this.props.current, + isExpanded: !!this.props.current && !!ActionMenu.findItem(this.props.items, this.props.current.value) + } + + toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => { + this.setState({ isExpanded: !this.state.isExpanded }); + e.currentTarget.blur(); + } + + static getDerivedStateFromProps(props: SectionProps, state: SectionState) { + if (props.items === state.items && props.current === state.current) return null; + return { items: props.items, current: props.current, isExpanded: props.current && !!ActionMenu.findItem(props.items, props.current.value) } + } + + render() { + const { header, items, onSelect, current } = this.props; + + if (typeof items === 'string') return null; + if (isItem(items)) return <Action item={items} onSelect={onSelect} current={current} /> + + const hasCurrent = header && current && !!ActionMenu.findItem(items, current.value) + + return <div> + {header && <div className='msp-control-group-header' style={{ marginTop: '1px' }}> + <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}> + <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} /> + {hasCurrent ? <b>{header}</b> : header} + </button> + </div>} + <div className='msp-control-offset'> + {(!header || this.state.isExpanded) && items.map((x, i) => { + if (typeof x === 'string') return null; + if (isItem(x)) return <Action key={i} item={x} onSelect={onSelect} current={current} /> + return <Section key={i} header={typeof x[0] === 'string' ? x[0] : void 0} items={x} onSelect={onSelect} current={current} /> + })} + </div> + </div>; + } +} + +const Action: React.FC<{ item: ActionMenu.Item, onSelect: ActionMenu.OnSelect, current: ActionMenu.Item | undefined }> = ({ item, onSelect, current }) => { + const isCurrent = current === item; + return <div className='msp-control-row'> + <button onClick={() => onSelect(item)}> + {item.icon && <Icon name={item.icon} />} + {isCurrent ? <b>{item.label}</b> : item.label} + </button> + </div>; +} + +function isItem(x: any): x is ActionMenu.Item { + const v = x as ActionMenu.Item; + return v && !!v.label && typeof v.value !== 'undefined'; +} \ No newline at end of file diff --git a/src/mol-plugin-ui/controls/common.tsx b/src/mol-plugin-ui/controls/common.tsx index c345eb5d86c0a1c0e90f8e6c7f8590700a6d5576..8faadb834ce81d0f303727627e77c69cf6a68513 100644 --- a/src/mol-plugin-ui/controls/common.tsx +++ b/src/mol-plugin-ui/controls/common.tsx @@ -308,16 +308,28 @@ export function SectionHeader(props: { icon?: string, title: string | JSX.Elemen </div> } -// export const ToggleButton = (props: { -// onChange: (v: boolean) => void, -// value: boolean, -// label: string, -// title?: string -// }) => <div className='lm-control-row lm-toggle-button' title={props.title}> -// <span>{props.label}</span> -// <div> -// <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}> -// <span className={ `lm-icon lm-icon-${props.value ? 'ok' : 'off'}` }></span> {props.value ? 'On' : 'Off'} -// </button> -// </div> -// </div> \ No newline at end of file +export type ToggleButtonProps = { + style?: React.CSSProperties, + className?: string, + disabled?: boolean, + label: string | JSX.Element, + title?: string, + isSelected?: boolean, + toggle: () => void +} + +export class ToggleButton extends React.PureComponent<ToggleButtonProps> { + onClick = (e: React.MouseEvent<HTMLButtonElement>) => { + e.currentTarget.blur(); + this.props.toggle(); + } + + render() { + const props = this.props; + const label = props.label; + return <button onClick={this.onClick} title={this.props.title} + disabled={props.disabled} style={props.style} className={props.className}> + {this.props.isSelected ? <b>{label}</b> : label} + </button>; + } +} \ No newline at end of file diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index 49363d5cd68a1fa21a03855f4ffe426dd887a1a4..ddb081b50dd7dc3449f0fc282d76f4cd980aafed 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2020 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> @@ -8,27 +8,33 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra'; import { Color } from '../../mol-util/color'; import { ColorListName, getColorListFromName } from '../../mol-util/color/lists'; -import { memoize1 } from '../../mol-util/memoize'; +import { memoize1, memoizeLatest } from '../../mol-util/memoize'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { camelCaseToWords } from '../../mol-util/string'; import * as React from 'react'; import LineGraphComponent from './line-graph/line-graph-component'; import { Slider, Slider2 } from './slider'; -import { NumericInput, IconButton, ControlGroup } from './common'; -import { _Props, _State } from '../base'; +import { NumericInput, IconButton, ControlGroup, ToggleButton } from './common'; +import { _Props, _State, PluginUIComponent } from '../base'; import { legendFor } from './legend'; import { Legend as LegendData } from '../../mol-util/legend'; import { CombinedColorControl, ColorValueOption, ColorOptions } from './color'; +import { getPrecision } from '../../mol-util/number'; +import { ParamMapping } from '../../mol-util/param-mapping'; +import { PluginContext } from '../../mol-plugin/context'; +import { ActionMenu } from './action-menu'; export interface ParameterControlsProps<P extends PD.Params = PD.Params> { params: P, values: any, - onChange: ParamOnChange, + onChange: ParamsOnChange<PD.Values<P>>, isDisabled?: boolean, onEnter?: () => void } export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> { + onChange: ParamOnChange = (params) => this.props.onChange(params, this.props.values); + render() { const params = this.props.params; const values = this.props.values; @@ -40,12 +46,31 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent< if (param.isHidden) return null; const Control = controlFor(param); if (!Control) return null; - return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} /> + return <Control param={param} key={key} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} /> })} </>; } } +export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: ParamMapping<S, T, PluginContext> }> { + setSettings = (p: { param: PD.Base<any>, name: string, value: any }, old: any) => { + const values = { ...old, [p.name]: p.value }; + const t = this.props.mapping.update(values, this.plugin); + this.props.mapping.apply(t, this.plugin); + } + + componentDidMount() { + this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate()); + } + + render() { + const t = this.props.mapping.getTarget(this.plugin); + const values = this.props.mapping.getValues(t, this.plugin); + const params = this.props.mapping.params(this.plugin) as any as PD.Params; + return <ParameterControls params={params} values={values} onChange={this.setSettings} /> + } +} + function controlFor(param: PD.Any): ParamControl | undefined { switch (param.type) { case 'value': return void 0; @@ -90,6 +115,7 @@ export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legen } } +export type ParamsOnChange<P> = (params: { param: PD.Base<any>, name: string, value: any }, values: Readonly<P>) => void export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: string, @@ -101,51 +127,63 @@ export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { } export type ParamControl = React.ComponentClass<ParamProps<any>> -export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { isExpanded: boolean }> { - state = { isExpanded: false }; +function renderSimple(options: { props: ParamProps<any>, state: { showHelp: boolean }, control: JSX.Element, addOn: JSX.Element | null, toggleHelp: () => void }) { + const { props, state, control, toggleHelp, addOn } = options; + + const _className = ['msp-control-row']; + if (props.param.shortLabel) _className.push('msp-control-label-short') + if (props.param.twoColumns) _className.push('msp-control-col-2') + const className = _className.join(' '); + + const label = props.param.label || camelCaseToWords(props.name); + const help = props.param.help + ? props.param.help(props.value) + : { description: props.param.description, legend: props.param.legend } + const desc = props.param.description; + const hasHelp = help.description || help.legend + return <> + <div className={className}> + <span title={desc}> + {label} + {hasHelp && + <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={toggleHelp} + title={desc || `${state.showHelp ? 'Hide' : 'Show'} help`} + style={{ background: 'transparent', textAlign: 'left', padding: '0' }}> + <span className={`msp-icon msp-icon-help-circle-${state.showHelp ? 'collapse' : 'expand'}`} /> + </button> + } + </span> + <div> + {control} + </div> + </div> + {hasHelp && state.showHelp && <div className='msp-control-offset'> + <ParamHelp legend={help.legend} description={help.description} /> + </div>} + {addOn} + </>; +} + +export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { showHelp: boolean }> { + state = { showHelp: false }; protected update(value: P['defaultValue']) { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } abstract renderControl(): JSX.Element; + renderAddOn(): JSX.Element | null { return null; } - private get className() { - const className = ['msp-control-row']; - if (this.props.param.shortLabel) className.push('msp-control-label-short') - if (this.props.param.twoColumns) className.push('msp-control-col-2') - return className.join(' ') - } - - toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); + toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); render() { - const label = this.props.param.label || camelCaseToWords(this.props.name); - const help = this.props.param.help - ? this.props.param.help(this.props.value) - : { description: this.props.param.description, legend: this.props.param.legend } - const desc = this.props.param.description; - const hasHelp = help.description || help.legend - return <> - <div className={this.className}> - <span title={desc}> - {label} - {hasHelp && - <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={this.toggleExpanded} - title={desc || `${this.state.isExpanded ? 'Hide' : 'Show'} help`} - style={{ background: 'transparent', textAlign: 'left', padding: '0' }}> - <span className={`msp-icon msp-icon-help-circle-${this.state.isExpanded ? 'collapse' : 'expand'}`} /> - </button> - } - </span> - <div> - {this.renderControl()} - </div> - </div> - {hasHelp && this.state.isExpanded && <div className='msp-control-offset'> - <ParamHelp legend={help.legend} description={help.description} /> - </div>} - </>; + return renderSimple({ + props: this.props, + state: this.state, + control: this.renderControl(), + toggleHelp: this.toggleHelp, + addOn: this.renderAddOn() + }); } } @@ -214,17 +252,20 @@ export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeri state = { value: '0' }; update = (value: number) => { + const p = getPrecision(this.props.param.step || 0.01) + value = parseFloat(value.toFixed(p)) this.props.onChange({ param: this.props.param, name: this.props.name, value }); } render() { const placeholder = this.props.param.label || camelCaseToWords(this.props.name); const label = this.props.param.label || camelCaseToWords(this.props.name); + const p = getPrecision(this.props.param.step || 0.01) return <div className='msp-control-row'> <span title={this.props.param.description}>{label}</span> <div> <NumericInput - value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder} + value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder} isDisabled={this.props.isDisabled} onChange={this.update} /> </div> </div>; @@ -289,27 +330,97 @@ export class PureSelectControl extends React.PureComponent<ParamProps<PD.Select } } -export class SelectControl extends SimpleParam<PD.Select<string | number>> { - onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { - if (typeof this.props.param.defaultValue === 'number') { - this.update(parseInt(e.target.value, 10)); +export class SelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>>, { showHelp: boolean, showOptions: boolean }> { + state = { showHelp: false, showOptions: false }; + + onSelect: ActionMenu.OnSelect = item => { + if (!item || item.value === this.props.value) { + this.setState({ showOptions: false }); } else { - this.update(e.target.value); + this.setState({ showOptions: false }, () => { + this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value }); + }); } } + + toggle = () => this.setState({ showOptions: !this.state.showOptions }); + + items = memoizeLatest((param: PD.Select<any>) => ActionMenu.createItemsFromSelectParam(param)); + renderControl() { - const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value); - return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}> - {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>} - {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)} - </select>; + const items = this.items(this.props.param); + const current = this.props.value !== undefined ? ActionMenu.findItem(items, this.props.value) : void 0; + const label = current + ? current.label + : typeof this.props.value === 'undefined' + ? `${ActionMenu.getFirstItem(items)?.label || ''} [Default]` + : `[Invalid] ${this.props.value}`; + + return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }} + label={label} title={label as string} toggle={this.toggle} isSelected={this.state.showOptions} />; + } + + renderAddOn() { + if (!this.state.showOptions) return null; + + const items = this.items(this.props.param); + const current = ActionMenu.findItem(items, this.props.value); + + return <ActionMenu items={items} current={current} onSelect={this.onSelect} />; + } + + toggleHelp = () => this.setState({ showHelp: !this.state.showHelp }); + + render() { + return renderSimple({ + props: this.props, + state: this.state, + control: this.renderControl(), + toggleHelp: this.toggleHelp, + addOn: this.renderAddOn() + }); } } -export class IntervalControl extends SimpleParam<PD.Interval> { - onChange = (v: [number, number]) => { this.update(v); } - renderControl() { - return <span>interval TODO</span>; +export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> { + state = { isExpanded: false } + + components = { + 0: PD.Numeric(0, { step: this.props.param.step }, { label: 'Min' }), + 1: PD.Numeric(0, { step: this.props.param.step }, { label: 'Max' }) + } + + change(value: PD.MultiSelect<any>['defaultValue']) { + this.props.onChange({ name: this.props.name, param: this.props.param, value }); + } + + componentChange: ParamOnChange = ({ name, value }) => { + const v = [...this.props.value]; + v[+name] = value; + this.change(v); + } + + toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => { + this.setState({ isExpanded: !this.state.isExpanded }); + e.currentTarget.blur(); + } + + render() { + const v = this.props.value; + const label = this.props.param.label || camelCaseToWords(this.props.name); + const p = getPrecision(this.props.param.step || 0.01) + const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`; + return <> + <div className='msp-control-row'> + <span>{label}</span> + <div> + <button onClick={this.toggleExpanded}>{value}</button> + </div> + </div> + <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}> + <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} /> + </div> + </>; } } @@ -399,9 +510,9 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx state = { isExpanded: false } components = { - 0: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }), - 1: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }), - 2: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' }) + 0: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }), + 1: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }), + 2: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' }) } change(value: PD.MultiSelect<any>['defaultValue']) { @@ -422,7 +533,8 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx render() { const v = this.props.value; const label = this.props.param.label || camelCaseToWords(this.props.name); - const value = `[${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)}]`; + const p = getPrecision(this.props.param.step || 0.01) + const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`; return <> <div className='msp-control-row'> <span>{label}</span> @@ -529,7 +641,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS } } -export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> { +export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>> & { inMapped?: boolean }, { isExpanded: boolean }> { state = { isExpanded: !!this.props.param.isExpanded } change(value: any) { @@ -552,6 +664,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />; + if (this.props.inMapped) { + return <div className='msp-control-offset'>{controls}</div>; + } + if (this.props.param.isFlat) { return controls; } @@ -570,7 +686,9 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, } } -export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> { +export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>, { isExpanded: boolean }> { + state = { isExpanded: false } + private valuesCache: { [name: string]: PD.Values<any> } = {} private setValues(name: string, values: PD.Values<any>) { this.valuesCache[name] = values @@ -596,6 +714,8 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any> this.change({ name: this.props.value.name, params: e.value }); } + toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); + render() { const value: PD.Mapped<any>['defaultValue'] = this.props.value; const param = this.props.param.map(value.name); @@ -618,6 +738,14 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any> return Select; } + if (param.type === 'group' && !param.isFlat && Object.keys(param.params).length > 0) { + return <div className='msp-mapped-parameter-group'> + {Select} + <IconButton icon='log' onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} /> + {this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />} + </div> + } + return <> {Select} <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} /> diff --git a/src/mol-plugin-ui/skin/base/components/controls.scss b/src/mol-plugin-ui/skin/base/components/controls.scss index 8a0f44a4cd5bf72828f1d4e5ce42f92b582f6c6f..725039cf8f025c6aae4781b2134ec1b1a5a9686c 100644 --- a/src/mol-plugin-ui/skin/base/components/controls.scss +++ b/src/mol-plugin-ui/skin/base/components/controls.scss @@ -31,6 +31,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + position: relative; @include non-selectable; } diff --git a/src/mol-plugin-ui/skin/base/components/misc.scss b/src/mol-plugin-ui/skin/base/components/misc.scss index 5208ebcec37b932de09e6fc9d5dc5103b51b38cd..a37bb0d4367c29a9d3a6fe8a3ed2fc31b6ff0709 100644 --- a/src/mol-plugin-ui/skin/base/components/misc.scss +++ b/src/mol-plugin-ui/skin/base/components/misc.scss @@ -166,4 +166,21 @@ .msp-scrollable-container { left: $row-height + 1px; } +} + +.msp-mapped-parameter-group { + position: relative; + + > .msp-control-row:first-child { + > div:nth-child(2) { + right: 33px; + } + } + + > .msp-btn-icon { + position: absolute; + right: 0; + width: 32px; + top: 0; + } } \ No newline at end of file diff --git a/src/mol-plugin-ui/skin/base/components/temp.scss b/src/mol-plugin-ui/skin/base/components/temp.scss index 004a90f172d7f88a990b195e914aeb6ba1fda3e5..b614362ecbc4390fb85adbda052cbabca7ee8fc4 100644 --- a/src/mol-plugin-ui/skin/base/components/temp.scss +++ b/src/mol-plugin-ui/skin/base/components/temp.scss @@ -66,6 +66,7 @@ text-align-last: center; background: none; padding: 0 $control-spacing; + overflow: hidden; > option[value = _] { display: none; @@ -274,4 +275,34 @@ .msp-transform-wrapper:last-child { margin-bottom: 10px; } +} + +.msp-button-row { + display:flex; + flex-direction:row; + height: $row-height; + width: inherit; + + > button { + margin: 0; + flex: 1 1 auto; + margin-right: 1px; + height: $row-height; + + text-align-last: center; + background: none; + padding: 0 $control-spacing; + overflow: hidden; + } +} + +.msp-action-menu-options { + .msp-control-row, button, .msp-icon { + height: 24px; + line-height: 24px; + } + + button { + text-align: left; + } } \ No newline at end of file diff --git a/src/mol-plugin-ui/structure/representation.tsx b/src/mol-plugin-ui/structure/representation.tsx index 2dcc444e1c6fb6f544967c3355526cb32ba2e121..bef7495baf6691e21d9856a7994657fd18883185 100644 --- a/src/mol-plugin-ui/structure/representation.tsx +++ b/src/mol-plugin-ui/structure/representation.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -13,8 +13,6 @@ import { Color } from '../../mol-util/color'; import { ButtonSelect, Options } from '../controls/common' import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base'; -import { StructureRepresentationPresets as P } from '../../mol-plugin/util/structure-representation-helper'; -import { camelCaseToWords } from '../../mol-util/string'; import { CollapsableControls } from '../base'; import { StateSelection, StateObject } from '../../mol-state'; import { PluginStateObject } from '../../mol-plugin/state/objects'; @@ -133,13 +131,6 @@ export class StructureRepresentationControls extends CollapsableControls<Collaps this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v })) } - preset = async (value: string) => { - const presetFn = P[value as keyof typeof P] - if (presetFn) { - await presetFn(this.plugin.helpers.structureRepresentation) - } - } - onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => { if (p.name === 'options') { await this.plugin.helpers.structureRepresentation.setIgnoreHydrogens(!p.value.showHydrogens) @@ -178,15 +169,7 @@ export class StructureRepresentationControls extends CollapsableControls<Collaps } renderControls() { - const presets = PD.objectToOptions(P, camelCaseToWords); return <div> - <div className='msp-control-row'> - <div className='msp-select-row'> - <ButtonSelect label='Preset' onChange={this.preset}> - <optgroup label='Preset'>{Options(presets)}</optgroup> - </ButtonSelect> - </div> - </div> <EverythingStructureRepresentationControls /> <SelectionStructureRepresentationControls /> diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx index 8847ccc0b9e8154c62ce29936bed0e2e6c4bcae3..f7e380a84963e0cfea31e5e74c3aed2c9bc0bedd 100644 --- a/src/mol-plugin-ui/structure/selection.tsx +++ b/src/mol-plugin-ui/structure/selection.tsx @@ -1,28 +1,26 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 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> */ import * as React from 'react'; import { CollapsableControls, CollapsableState } from '../base'; -import { StructureSelectionQueries, SelectionModifier } from '../../mol-plugin/util/structure-selection-helper'; -import { ButtonSelect, Options } from '../controls/common'; +import { StructureSelectionQuery, SelectionModifier, StructureSelectionQueryList } from '../../mol-plugin/util/structure-selection-helper'; import { PluginCommands } from '../../mol-plugin/command'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { Interactivity } from '../../mol-plugin/util/interactivity'; import { ParameterControls } from '../controls/parameters'; import { stripTags } from '../../mol-util/string'; import { StructureElement } from '../../mol-model/structure'; +import { ActionMenu } from '../controls/action-menu'; +import { ToggleButton } from '../controls/common'; -const SSQ = StructureSelectionQueries -const DefaultQueries: (keyof typeof SSQ)[] = [ - 'all', 'polymer', 'trace', 'backbone', 'protein', 'nucleic', - 'helix', 'beta', - 'water', 'branched', 'ligand', 'nonStandardPolymer', - 'ring', 'aromaticRing', - 'surroundings', 'complement', 'bonded' -] +export const DefaultQueries = ActionMenu.createItems(StructureSelectionQueryList, { + label: q => q.label, + category: q => q.category +}); const StructureSelectionParams = { granularity: Interactivity.Params.granularity, @@ -33,7 +31,9 @@ interface StructureSelectionControlsState extends CollapsableState { extraRadius: number, durationMs: number, - isDisabled: boolean + isDisabled: boolean, + + queryAction?: SelectionModifier } export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> { @@ -46,7 +46,9 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS this.forceUpdate() }); - this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v })) + this.subscribe(this.plugin.state.dataState.events.isUpdating, v => { + this.setState({ isDisabled: v, queryAction: void 0 }) + }) } get stats() { @@ -115,38 +117,41 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS } } - set = (modifier: SelectionModifier, value: string) => { - const query = SSQ[value as keyof typeof SSQ] - this.plugin.helpers.structureSelection.set(modifier, query.query, false) - } - - add = (value: string) => this.set('add', value) - remove = (value: string) => this.set('remove', value) - only = (value: string) => this.set('only', value) - - queries = Options(Object.keys(StructureSelectionQueries) - .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string]) - .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ))); - - controls = <div className='msp-control-row'> - <div className='msp-select-row'> - <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}> - <optgroup label='Select'> - {this.queries} - </optgroup> - </ButtonSelect> - <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}> - <optgroup label='Deselect'> - {this.queries} - </optgroup> - </ButtonSelect> - <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}> - <optgroup label='Only'> - {this.queries} - </optgroup> - </ButtonSelect> + set = (modifier: SelectionModifier, selectionQuery: StructureSelectionQuery) => { + this.plugin.helpers.structureSelection.set(modifier, selectionQuery, false) + } + + selectQuery: ActionMenu.OnSelect = item => { + if (!item || !this.state.queryAction) { + this.setState({ queryAction: void 0 }); + return; + } + const q = this.state.queryAction!; + this.setState({ queryAction: void 0 }, () => { + this.set(q, item.value as StructureSelectionQuery); + }) + } + + queries = DefaultQueries + + private showQueries(q: SelectionModifier) { + return () => this.setState({ queryAction: this.state.queryAction === q ? void 0 : q }); + } + + toggleAdd = this.showQueries('add') + toggleRemove = this.showQueries('remove') + toggleOnly = this.showQueries('only') + + get controls() { + return <div> + <div className='msp-control-row msp-button-row'> + <ToggleButton label='Select' toggle={this.toggleAdd} isSelected={this.state.queryAction === 'add'} disabled={this.state.isDisabled} /> + <ToggleButton label='Deselect' toggle={this.toggleRemove} isSelected={this.state.queryAction === 'remove'} disabled={this.state.isDisabled} /> + <ToggleButton label='Only' toggle={this.toggleOnly} isSelected={this.state.queryAction === 'only'} disabled={this.state.isDisabled} /> + </div> + {this.state.queryAction && <ActionMenu items={this.queries} onSelect={this.selectQuery} />} </div> - </div> + } defaultState() { return { @@ -157,6 +162,8 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS extraRadius: 4, durationMs: 250, + queryAction: void 0, + isDisabled: false } as S } diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx index 387e0154319e1b650725ccdc27eeaecbc10c2596..11c6b703f18c3bf991e0acbf09e81bd15f0c34b0 100644 --- a/src/mol-plugin-ui/viewport/simple-settings.tsx +++ b/src/mol-plugin-ui/viewport/simple-settings.tsx @@ -9,10 +9,23 @@ import * as React from 'react'; import { Canvas3DParams } from '../../mol-canvas3d/canvas3d'; import { PluginCommands } from '../../mol-plugin/command'; import { ColorNames } from '../../mol-util/color/names'; -import { ParameterControls } from '../controls/parameters'; +import { ParameterMappingControl } from '../controls/parameters'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { PluginUIComponent } from '../base'; import { Color } from '../../mol-util/color'; +import { ParamMapping } from '../../mol-util/param-mapping'; +import { PluginContext } from '../../mol-plugin/context'; + +export class SimpleSettingsControl extends PluginUIComponent { + componentDidMount() { + this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate()); + } + + render() { + if (!this.plugin.canvas3d) return null; + return <ParameterMappingControl mapping={SimpleSettingsMapping} /> + } +} const SimpleSettingsParams = { spin: Canvas3DParams.trackball.params.spin, @@ -21,80 +34,21 @@ const SimpleSettingsParams = { 'transparent': PD.EmptyGroup(), 'opaque': PD.Group({ color: PD.Color(Color(0xFCFBF9), { description: 'Custom background color' }) }, { isFlat: true }) }, { description: 'Background of the 3D canvas' }), - renderStyle: PD.Select('glossy', [['flat', 'Flat'], ['matte', 'Matte'], ['glossy', 'Glossy'], ['metallic', 'Metallic']], { description: 'Style in which the 3D scene is rendered' }), + renderStyle: PD.Select('glossy', PD.arrayToOptions(['flat', 'matte', 'glossy', 'metallic']), { description: 'Style in which the 3D scene is rendered' }), occlusion: PD.Boolean(false, { description: 'Darken occluded crevices with the ambient occlusion effect' }), outline: PD.Boolean(false, { description: 'Draw outline around 3D objects' }), fog: PD.Boolean(false, { description: 'Show fog in the distance' }), clipFar: PD.Boolean(true, { description: 'Clip scene in the distance' }), }; -export class SimpleSettingsControl extends PluginUIComponent { - setSettings = (p: { param: PD.Base<any>, name: keyof typeof SimpleSettingsParams | string, value: any }) => { - if (p.name === 'spin') { - if (!this.plugin.canvas3d) return; - const trackball = this.plugin.canvas3d.props.trackball; - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: p.value } } }); - } else if (p.name === 'camera') { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { cameraMode: p.value }}); - } else if (p.name === 'background') { - if (!this.plugin.canvas3d) return; - const renderer = this.plugin.canvas3d.props.renderer; - const color: typeof SimpleSettingsParams['background']['defaultValue'] = p.value; - if (color.name === 'transparent') { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: ColorNames.white }, transparentBackground: true } }); - } else { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: color.params.color }, transparentBackground: false } }); - } - } else if (p.name === 'renderStyle') { - if (!this.plugin.canvas3d) return; - - const renderer = this.plugin.canvas3d.props.renderer; - if (p.value === 'flat') { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - renderer: { ...renderer, lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 } - } }); - } else if (p.value === 'matte') { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 } - } }); - } else if (p.value === 'glossy') { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 } - } }); - } else if (p.value === 'metallic') { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 } - } }); - } - } else if (p.name === 'occlusion') { - if (!this.plugin.canvas3d) return; - const postprocessing = this.plugin.canvas3d.props.postprocessing; - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - postprocessing: { ...postprocessing, occlusionEnable: p.value, occlusionBias: 0.5, occlusionRadius: 64 }, - } }); - } else if (p.name === 'outline') { - if (!this.plugin.canvas3d) return; - const postprocessing = this.plugin.canvas3d.props.postprocessing; - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - postprocessing: { ...postprocessing, outlineEnable: p.value }, - } }); - } else if (p.name === 'fog') {; - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - cameraFog: p.value ? 50 : 0, - } }); - } else if (p.name === 'clipFar') {; - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { - cameraClipFar: p.value, - } }); - } - } - - get values () { - const renderer = this.plugin.canvas3d?.props.renderer; - - let renderStyle = 'custom' - let background: typeof SimpleSettingsParams['background']['defaultValue'] = { name: 'transparent', params: { } } +type SimpleSettingsParams = typeof SimpleSettingsParams +const SimpleSettingsMapping = ParamMapping({ + params: SimpleSettingsParams, + target(ctx: PluginContext) { return ctx.canvas3d?.props!; } })({ + values(t, ctx) { + const renderer = t.renderer; + let renderStyle: SimpleSettingsParams['renderStyle']['defaultValue'] = 'custom' as any; if (renderer) { if (renderer.lightIntensity === 0 && renderer.ambientIntensity === 1 && renderer.roughness === 0.4 && renderer.metalness === 0) { renderStyle = 'flat' @@ -107,31 +61,42 @@ export class SimpleSettingsControl extends PluginUIComponent { renderStyle = 'metallic' } } - - if (renderer.backgroundColor === ColorNames.white && this.plugin.canvas3d?.props.transparentBackground) { - background = { name: 'transparent', params: { } } - } else { - background = { name: 'opaque', params: { color: renderer.backgroundColor } } - } } return { - spin: !!this.plugin.canvas3d?.props.trackball.spin, - camera: this.plugin.canvas3d?.props.cameraMode, - background, + spin: !!t.trackball.spin, + camera: t.cameraMode, + background: (renderer.backgroundColor === ColorNames.white && t.transparentBackground) + ? { name: 'transparent', params: { } } + : { name: 'opaque', params: { color: renderer.backgroundColor } }, renderStyle, - occlusion: this.plugin.canvas3d?.props.postprocessing.occlusionEnable, - outline: this.plugin.canvas3d?.props.postprocessing.outlineEnable, - fog: this.plugin.canvas3d ? this.plugin.canvas3d.props.cameraFog > 1 : false, - clipFar: this.plugin.canvas3d?.props.cameraClipFar + occlusion: t.postprocessing.occlusionEnable, + outline: t.postprocessing.outlineEnable, + fog: ctx.canvas3d ? t.cameraFog > 1 : false, + clipFar: t.cameraClipFar + }; + }, + update(s, t) { + t.trackball.spin = s.spin; + t.cameraMode = s.camera; + t.transparentBackground = s.background.name === 'transparent'; + t.renderer.backgroundColor = s.background.name === 'transparent' ? ColorNames.white : s.background.params.color; + switch (s.renderStyle) { + case 'flat': Object.assign(t.renderer, { lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }); break; + case 'matte': Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }); break; + case 'glossy': Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }); break; + case 'metallic': Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }); break; } + t.postprocessing.occlusionEnable = s.occlusion; + if (s.occlusion) { + t.postprocessing.occlusionBias = 0.5; + t.postprocessing.occlusionRadius = 64; + } + t.postprocessing.outlineEnable = s.outline; + t.cameraFog = s.fog ? 50 : 0; + t.cameraClipFar = s.clipFar; + }, + apply(settings, ctx) { + return PluginCommands.Canvas3D.SetSettings.dispatch(ctx, { settings }); } - - componentDidMount() { - this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate()); - } - - render() { - return <ParameterControls params={SimpleSettingsParams} values={this.values} onChange={this.setSettings} /> - } -} \ No newline at end of file +}) \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props.ts b/src/mol-plugin/behavior/dynamic/custom-props.ts index 6b5428cacbec1b4744308c6d5f88bdef7fb5b426..91586c5cfe9ac4294e0b5896f769604f5cee8e2e 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props.ts @@ -10,6 +10,8 @@ export { Interactions } from './custom-props/computed/interactions' export { SecondaryStructure } from './custom-props/computed/secondary-structure' export { ValenceModel } from './custom-props/computed/valence-model' +export { CrossLinkRestraint } from './custom-props/integrative/cross-link-restraint' + export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report' export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry' export { RCSBValidationReport } from './custom-props/rcsb/validation-report' \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts b/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts index aa95e36eb6e73a018fe22ebfbf13857eb78b0669..b5013fced3479ffdcc2b73838a4755ad3d472a5e 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts @@ -11,6 +11,7 @@ import { Loci } from '../../../../../mol-model/loci'; import { AccessibleSurfaceAreaColorThemeProvider } from '../../../../../mol-model-props/computed/themes/accessible-surface-area'; import { OrderedSet } from '../../../../../mol-data/int'; import { arraySum } from '../../../../../mol-util/array'; +import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler'; export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({ name: 'computed-accessible-surface-area-prop', @@ -36,12 +37,17 @@ export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean } register(): void { + DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor); + this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach); this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('accessible-surface-area', AccessibleSurfaceAreaColorThemeProvider) this.ctx.lociLabels.addProvider(this.label); } unregister() { + // TODO + // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor); + this.ctx.customStructureProperties.unregister(this.provider.descriptor.name); this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('accessible-surface-area') this.ctx.lociLabels.removeProvider(this.label); diff --git a/src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts b/src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1a105666a5813a1cf7583e3f6247a886b7f5ec4 --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginBehavior } from '../../../behavior'; +import { ModelCrossLinkRestraint } from '../../../../../mol-model-props/integrative/cross-link-restraint/format'; +import { Model } from '../../../../../mol-model/structure'; +import { MmcifFormat } from '../../../../../mol-model-formats/structure/mmcif'; +import { CrossLinkRestraintRepresentationProvider } from '../../../../../mol-model-props/integrative/cross-link-restraint/representation'; +import { CrossLinkColorThemeProvider } from '../../../../../mol-model-props/integrative/cross-link-restraint/color'; +import { CrossLinkRestraint as _CrossLinkRestraint } from '../../../../../mol-model-props/integrative/cross-link-restraint/property'; + +const Tag = _CrossLinkRestraint.Tag + +export const CrossLinkRestraint = PluginBehavior.create<{ }>({ + name: 'integrative-cross-link-restraint', + category: 'custom-props', + display: { name: 'Cross Link Restraint' }, + ctor: class extends PluginBehavior.Handler<{ }> { + private provider = ModelCrossLinkRestraint.Provider + + register(): void { + this.provider.formatRegistry.add('mmCIF', crossLinkRestraintFromMmcif) + + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.CrossLinkRestraint, CrossLinkColorThemeProvider) + this.ctx.structureRepresentation.registry.add(Tag.CrossLinkRestraint, CrossLinkRestraintRepresentationProvider) + } + + unregister() { + this.provider.formatRegistry.remove('mmCIF') + + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.CrossLinkRestraint) + this.ctx.structureRepresentation.registry.remove(Tag.CrossLinkRestraint) + } + } +}); + +function crossLinkRestraintFromMmcif(model: Model) { + if (!MmcifFormat.is(model.sourceData)) return; + const { ihm_cross_link_restraint } = model.sourceData.data.db; + if (ihm_cross_link_restraint._rowCount === 0) return; + return ModelCrossLinkRestraint.fromTable(ihm_cross_link_restraint, model) +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts index 69742b2992896625fd383526de1a7e8183c90520..ce22b7c29bed4f7e5215c66bec5bbfdbdc47476e 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts @@ -15,7 +15,10 @@ import { PluginBehavior } from '../../../behavior'; export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({ name: 'pdbe-structure-quality-report-prop', category: 'custom-props', - display: { name: 'PDBe Structure Quality Report' }, + display: { + name: 'Structure Quality Report', + description: 'Data from wwPDB Validation Report, obtained via PDBe.' + }, ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> { private provider = StructureQualityReportProvider @@ -32,8 +35,8 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo const se = StructureElement.Location.create(loci.structure, u, u.elements[OrderedSet.getAt(e.indices, 0)]); const issues = StructureQualityReport.getIssues(se); - if (issues.length === 0) return 'PDBe Validation: No Issues'; - return `PDBe Validation: ${issues.join(', ')}`; + if (issues.length === 0) return 'Validation: No Issues'; + return `Validation: ${issues.join(', ')}`; default: return void 0; } diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts index 803f2b45daeed063f6e8d43ead4fde991f1f5df0..32aeba1bd6b85eec5bbd8d2580a64fc0686b1900 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts @@ -14,17 +14,22 @@ import { Task } from '../../../../../mol-task'; import { PluginContext } from '../../../../context'; import { StateTransformer, StateAction, StateObject } from '../../../../../mol-state'; +const Tag = AssemblySymmetry.Tag + export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({ name: 'rcsb-assembly-symmetry-prop', category: 'custom-props', - display: { name: 'RCSB Assembly Symmetry' }, + display: { + name: 'Assembly Symmetry', + description: 'Assembly Symmetry data calculated with BioJava, obtained via RCSB PDB.' + }, ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { private provider = AssemblySymmetryProvider register(): void { this.ctx.state.dataState.actions.add(InitAssemblySymmetry3D) this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach); - this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('rcsb-assembly-symmetry-cluster', AssemblySymmetryClusterColorThemeProvider) + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.Cluster, AssemblySymmetryClusterColorThemeProvider) } update(p: { autoAttach: boolean }) { @@ -37,7 +42,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean unregister() { this.ctx.state.dataState.actions.remove(InitAssemblySymmetry3D) this.ctx.customStructureProperties.unregister(this.provider.descriptor.name); - this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('rcsb-assembly-symmetry-cluster') + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.Cluster) } }, params: () => ({ @@ -47,24 +52,32 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }); const InitAssemblySymmetry3D = StateAction.build({ - display: { name: 'RCSB Assembly Symmetry' }, + display: { + name: 'Assembly Symmetry', + description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.' + }, from: PluginStateObject.Molecule.Structure, isApplicable: (a) => AssemblySymmetry.isApplicable(a.data) -})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init RCSB Assembly Symmetry', async ctx => { +})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => { try { await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data) } catch(e) { - plugin.log.error(`RCSB Assembly Symmetry: ${e}`) + plugin.log.error(`Assembly Symmetry: ${e}`) return } const tree = state.build().to(ref).apply(AssemblySymmetry3D); await state.updateTree(tree).runInContext(ctx); })); +export { AssemblySymmetry3D } + type AssemblySymmetry3D = typeof AssemblySymmetry3D const AssemblySymmetry3D = PluginStateTransform.BuiltIn({ - name: 'rcsb-assembly-symmetry-3d', - display: 'RCSB Assembly Symmetry', + name: Tag.Representation, + display: { + name: 'Assembly Symmetry', + description: 'Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.' + }, from: PluginStateObject.Molecule.Structure, to: PluginStateObject.Shape.Representation3D, params: (a) => { @@ -78,7 +91,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({ return true; }, apply({ a, params }, plugin: PluginContext) { - return Task.create('RCSB Assembly Symmetry', async ctx => { + return Task.create('Assembly Symmetry', async ctx => { await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data) const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value if (!assemblySymmetry || assemblySymmetry.length === 0) { @@ -91,7 +104,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({ }); }, update({ a, b, newParams }, plugin: PluginContext) { - return Task.create('RCSB Assembly Symmetry', async ctx => { + return Task.create('Assembly Symmetry', async ctx => { await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data) const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value if (!assemblySymmetry || assemblySymmetry.length === 0) { diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts index e8ec2fad939d038830e26bdad8da2d1c762361f7..94b90517cc7f6da20f045dcce6f745acb00e1afd 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts @@ -14,13 +14,17 @@ import { OrderedSet } from '../../../../../mol-data/int'; import { ClashesRepresentationProvider } from '../../../../../mol-model-props/rcsb/representations/validation-report-clashes'; import { DensityFitColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/density-fit'; import { cantorPairing } from '../../../../../mol-data/util'; +import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler'; const Tag = ValidationReport.Tag export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({ name: 'rcsb-validation-report-prop', category: 'custom-props', - display: { name: 'RCSB Validation Report' }, + display: { + name: 'Validation Report', + description: 'Data from wwPDB Validation Report, obtained via RCSB PDB.' + }, ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> { private provider = ValidationReportProvider @@ -34,6 +38,8 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean, } register(): void { + DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor); + this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); this.ctx.lociLabels.addProvider(this.label); @@ -54,6 +60,9 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean, } unregister() { + // TODO + // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor); + this.ctx.customStructureProperties.unregister(this.provider.descriptor.name); this.ctx.lociLabels.removeProvider(this.label); @@ -93,7 +102,7 @@ function geometryQualityLabel(loci: Loci): string | undefined { if (angles) angles.forEach(a => issues.add(angleOutliers.data[a].tag)) if (issues.size === 0) { - return `RCSB Geometry Quality <small>(1 Atom)</small>: no issues`; + return `Geometry Quality <small>(1 Atom)</small>: no issues`; } const summary: string[] = [] diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index ed27bf3cadff50819fe399260972f5498a4b0b9d..85e873d15309e9d490c547ee8fbbff889497431e 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts @@ -93,8 +93,8 @@ export namespace VolumeStreaming { }, { description: 'Static box defined by cartesian coords.', isFlat: true }), 'selection-box': PD.Group({ radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }), - bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }), - topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }), + bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }), + topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }), }, { description: 'Box around last-interacted element.', isFlat: true }), 'cell': PD.Group({}), // 'auto': PD.Group({ }), // TODO based on camera distance/active selection/whatever, show whole structure or slice. diff --git a/src/mol-plugin/behavior/static/camera.ts b/src/mol-plugin/behavior/static/camera.ts index 32971d8edc6ae922e406d40e21293f20c74290a0..dbe81f4af08aa43ab72f2f00ed78f390744aa2c7 100644 --- a/src/mol-plugin/behavior/static/camera.ts +++ b/src/mol-plugin/behavior/static/camera.ts @@ -15,8 +15,8 @@ export function registerDefault(ctx: PluginContext) { } export function Reset(ctx: PluginContext) { - PluginCommands.Camera.Reset.subscribe(ctx, () => { - ctx.canvas3d?.requestCameraReset(); + PluginCommands.Camera.Reset.subscribe(ctx, options => { + ctx.canvas3d?.requestCameraReset(options); }) } diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index a861e3ba3f4b70527e3c06f5a7bda3268467bf2b..e4e8e8532dccb0eeb8e2085f230563d3830072df 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -59,7 +59,7 @@ export const PluginCommands = { Hide: PluginCommand<{ key: string }>() }, Camera: { - Reset: PluginCommand<{}>(), + Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(), SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(), Snapshots: { Add: PluginCommand<{ name?: string, description?: string }>(), diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 9c43953cd18827be185aeac38a5988635e214bc9..16fcadc04e4c3a240ebadd97ee9f6c7e2377d3ab 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -70,14 +70,17 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci), + PluginSpec.Behavior(StructureRepresentationInteraction), + PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea), PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions), PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure), PluginSpec.Behavior(PluginBehaviors.CustomProps.ValenceModel), + PluginSpec.Behavior(PluginBehaviors.CustomProps.CrossLinkRestraint), + PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }), PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry), PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBValidationReport), - PluginSpec.Behavior(StructureRepresentationInteraction) ], customParamEditors: [ [CreateVolumeStreamingBehavior, VolumeStreamingCustomControls] diff --git a/src/mol-plugin/state/representation/model.ts b/src/mol-plugin/state/representation/model.ts index 47abb3649f33ac19334f3edca6fddc52675692c9..fee6cbda0b3429fbb55548eb5b03127da3f9b3e0 100644 --- a/src/mol-plugin/state/representation/model.ts +++ b/src/mol-plugin/state/representation/model.ts @@ -1,12 +1,13 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 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 { Model, Structure, StructureSymmetry } from '../../../mol-model/structure'; import { stringToWords } from '../../../mol-util/string'; -import { SpacegroupCell } from '../../../mol-math/geometry'; +import { SpacegroupCell, Spacegroup } from '../../../mol-math/geometry'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { Vec3 } from '../../../mol-math/linear-algebra'; import { RuntimeContext } from '../../../mol-task'; @@ -16,12 +17,28 @@ import { PluginStateObject as SO } from '../objects'; import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry'; export namespace ModelStructureRepresentation { - export function getParams(model?: Model, defaultValue?: 'deposited' | 'assembly' | 'symmetry' | 'symmetry-mates') { + export function getParams(model?: Model, defaultValue?: 'deposited' | 'assembly' | 'symmetry' | 'symmetry-mates' | 'symmetry-assembly') { const symmetry = model && ModelSymmetry.Provider.get(model) const assemblyIds = symmetry ? symmetry.assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]) : []; const showSymm = !symmetry ? true : !SpacegroupCell.isZero(symmetry.spacegroup.cell); + const operatorOptions: [number, string][] = [] + if (symmetry) { + const { operators } = symmetry.spacegroup + for (let i = 0, il = operators.length; i < il; i++) { + operatorOptions.push([i, `${i + 1}: ${Spacegroup.getOperatorXyz(operators[i])}`]) + } + } + + const asymIdsOptions: [string, string][] = [] + if (model) { + model.properties.structAsymMap.forEach(v => { + const label = v.id === v.auth_id ? v.id : `${v.id} [auth ${v.auth_id}]` + asymIdsOptions.push([v.id, label]) + }) + } + const modes = { deposited: PD.EmptyGroup(), assembly: PD.Group({ @@ -33,8 +50,21 @@ export namespace ModelStructureRepresentation { radius: PD.Numeric(5) }, { isFlat: true }), 'symmetry': PD.Group({ - ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }), - ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }) + ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { step: 1 }, { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }), + ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { step: 1 }, { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }) + }, { isFlat: true }), + 'symmetry-assembly': PD.Group({ + generators: PD.ObjectList({ + operators: PD.ObjectList({ + index: PD.Select(0, operatorOptions), + shift: PD.Vec3(Vec3(), { step: 1 }, { label: 'IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }) + }, e => `${e.index + 1}_${e.shift.map(a => a + 5).join('')}`, { + defaultValue: [] as { index: number, shift: Vec3 }[] + }), + asymIds: PD.MultiSelect([] as string[], asymIdsOptions) + }, e => `${e.asymIds.length} asym ids, ${e.operators.length} operators`, { + defaultValue: [] as { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[] + }) }, { isFlat: true }) }; @@ -49,6 +79,7 @@ export namespace ModelStructureRepresentation { if (showSymm) { options.push(['symmetry-mates', 'Symmetry Mates']); options.push(['symmetry', 'Symmetry (indices)']); + options.push(['symmetry-assembly', 'Symmetry (assembly)']); } return { @@ -105,8 +136,16 @@ export namespace ModelStructureRepresentation { return new SO.Molecule.Structure(s, props); } + async function buildSymmetryAssembly(ctx: RuntimeContext, model: Model, generators: StructureSymmetry.Generators, symmetry: Symmetry) { + const base = Structure.ofModel(model); + const s = await StructureSymmetry.buildSymmetryAssembly(base, generators, symmetry).runInContext(ctx); + const props = { label: `Symmetry Assembly`, description: Structure.elementDescription(s) }; + return new SO.Molecule.Structure(s, props); + } + export async function create(plugin: PluginContext, ctx: RuntimeContext, model: Model, params?: Params): Promise<SO.Molecule.Structure> { - if (!params || params.name === 'deposited') { + const symmetry = ModelSymmetry.Provider.get(model) + if (!symmetry || !params || params.name === 'deposited') { const s = Structure.ofModel(model); return new SO.Molecule.Structure(s, { label: 'Deposited', description: Structure.elementDescription(s) }); } @@ -119,6 +158,9 @@ export namespace ModelStructureRepresentation { if (params.name === 'symmetry-mates') { return buildSymmetryMates(ctx, model, params.params.radius) } + if (params.name === 'symmetry-assembly') { + return buildSymmetryAssembly(ctx, model, params.params.generators, symmetry) + } throw new Error(`Unknown represetation type: ${(params as any).name}`); } diff --git a/src/mol-plugin/state/representation/structure/preset.ts b/src/mol-plugin/state/representation/structure/preset.ts index 418cbb6405616bf58e8c5bb0770f51922380e938..f9bebdd26fc8923bed8ce14d8a929d7f9ee4240b 100644 --- a/src/mol-plugin/state/representation/structure/preset.ts +++ b/src/mol-plugin/state/representation/structure/preset.ts @@ -52,7 +52,7 @@ const defaultPreset = StructureRepresentationProvider({ const ligandRepr = ligand.applyOrUpdateTagged(reprTags, StateTransforms.Representation.StructureRepresentation3D, StructureRepresentation3DHelpers.getDefaultParams(plugin, 'ball-and-stick', structure)); - applyComplex(root, 'modified') + applyComplex(root, 'non-standard') .applyOrUpdateTagged(reprTags, StateTransforms.Representation.StructureRepresentation3D, StructureRepresentation3DHelpers.getDefaultParamsWithTheme(plugin, 'ball-and-stick', 'polymer-id', structure, void 0)); diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index d593a97b01f8ae8f2aa79c5d5577ee468e0eee94..bb25ec0f9a602b8581483ce21003e4c117dbe993 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -16,7 +16,6 @@ import Expression from '../../../mol-script/language/expression'; import { StateObject, StateTransformer } from '../../../mol-state'; import { RuntimeContext, Task } from '../../../mol-task'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; -import { stringToWords } from '../../../mol-util/string'; import { PluginStateObject as SO, PluginStateTransform } from '../objects'; import { trajectoryFromGRO } from '../../../mol-model-formats/structure/gro'; import { parseGRO } from '../../../mol-io/reader/gro/parser'; @@ -32,7 +31,6 @@ import { parseDcd } from '../../../mol-io/reader/dcd/parser'; import { coordinatesFromDcd } from '../../../mol-model-formats/structure/dcd'; import { topologyFromPsf } from '../../../mol-model-formats/structure/psf'; import { deepEqual } from '../../../mol-util'; -import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry'; export { CoordinatesFromDcd }; export { TopologyFromPsf }; @@ -45,7 +43,6 @@ export { TrajectoryFrom3DG }; export { ModelFromTrajectory }; export { StructureFromTrajectory }; export { StructureFromModel }; -export { StructureAssemblyFromModel }; export { TransformStructureConformation }; export { TransformStructureConformationByMatrix }; export { StructureSelectionFromExpression }; @@ -290,32 +287,6 @@ const StructureFromModel = PluginStateTransform.BuiltIn({ } }); -// TODO: deprecate this in favor of StructureFromModel -type StructureAssemblyFromModel = typeof StructureAssemblyFromModel -const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ - name: 'structure-assembly-from-model', - display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' }, - from: SO.Molecule.Model, - to: SO.Molecule.Structure, - params(a) { - if (!a) { - return { id: PD.Optional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. Value \'deposited\' can be used to specify deposited asymmetric unit.' })) }; - } - const assemblies = ModelSymmetry.Provider.get(a.data)?.assemblies || [] - const ids = assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]); - ids.push(['deposited', 'Deposited']); - return { - id: PD.Optional(PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' })) - }; - } -})({ - apply({ a, params }, plugin: PluginContext) { - return Task.create('Build Assembly', async ctx => { - return ModelStructureRepresentation.create(plugin, ctx, a.data, { name: 'assembly', params }); - }) - } -}); - const _translation = Vec3(), _m = Mat4(), _n = Mat4(); type TransformStructureConformation = typeof TransformStructureConformation const TransformStructureConformation = PluginStateTransform.BuiltIn({ @@ -643,7 +614,7 @@ export const StructureComplexElementTypes = { 'branched': 'branched', // = carbs 'ligand': 'ligand', - 'modified': 'modified', + 'non-standard': 'non-standard', 'coarse': 'coarse', @@ -678,7 +649,7 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({ case 'branched': query = StructureSelectionQueries.branchedPlusConnected.query; label = 'Branched'; break; case 'ligand': query = StructureSelectionQueries.ligandPlusConnected.query; label = 'Ligand'; break; - case 'modified': query = StructureSelectionQueries.modified.query; label = 'Modified'; break; + case 'non-standard': query = StructureSelectionQueries.nonStandardPolymer.query; label = 'Non-standard'; break; case 'coarse': query = StructureSelectionQueries.coarse.query; label = 'Coarse'; break; @@ -719,7 +690,7 @@ async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: Runti const { autoAttach, properties } = params for (const name of Object.keys(properties)) { const property = ctx.customModelProperties.get(name) - const props = params[name as keyof typeof params] + const props = properties[name] if (autoAttach.includes(name)) { try { await property.attach(propertyCtx, model, props) @@ -754,7 +725,7 @@ async function attachStructureProps(structure: Structure, ctx: PluginContext, ta const { autoAttach, properties } = params for (const name of Object.keys(properties)) { const property = ctx.customStructureProperties.get(name) - const props = params[name as keyof typeof params] + const props = properties[name] if (autoAttach.includes(name)) { try { await property.attach(propertyCtx, structure, props) diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index 611d19c55790844b5bd6f68bb60c8121f857f134..263060eaa25003721fdcc19f05d5eb62270cc4a2 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -63,37 +63,40 @@ namespace StructureRepresentation3DHelpers { }) } + export type Props<R extends RepresentationProvider<Structure, any, any> = any, C extends ColorTheme.Provider<any> = any, S extends SizeTheme.Provider<any> = any> = { + repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => Partial<RepresentationProvider.ParamValues<R>>], + color?: C | [C, (c: C, ctx: ThemeRegistryContext) => Partial<ColorTheme.ParamValues<C>>], + size?: S | [S, (c: S, ctx: ThemeRegistryContext) => Partial<SizeTheme.ParamValues<S>>] + } + export function createParams<R extends RepresentationProvider<Structure, any, any>, C extends ColorTheme.Provider<any>, S extends SizeTheme.Provider<any>>( - ctx: PluginContext, structure: Structure, params: { - repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => Partial<RepresentationProvider.ParamValues<R>>], - color?: C | [C, (c: C, ctx: ThemeRegistryContext) => Partial<ColorTheme.ParamValues<C>>], - size?: S | [S, (c: S, ctx: ThemeRegistryContext) => Partial<SizeTheme.ParamValues<S>>] - }): StateTransformer.Params<StructureRepresentation3D> { + ctx: PluginContext, structure: Structure, props: Props<R, C, S> = {}): StateTransformer.Params<StructureRepresentation3D> { - const themeCtx = ctx.structureRepresentation.themeCtx + const { themeCtx } = ctx.structureRepresentation + const themeDataCtx = { structure } - const repr = params.repr - ? params.repr instanceof Array ? params.repr[0] : params.repr + const repr = props.repr + ? props.repr instanceof Array ? props.repr[0] : props.repr : ctx.structureRepresentation.registry.default.provider; const reprDefaultParams = PD.getDefaultValues(repr.getParams(themeCtx, structure)); - const reprParams = params.repr instanceof Array - ? { ...reprDefaultParams, ...params.repr[1](repr as R, themeCtx, structure) } + const reprParams = props.repr instanceof Array + ? { ...reprDefaultParams, ...props.repr[1](repr as R, themeCtx, structure) } : reprDefaultParams; - const color = params.color - ? params.color instanceof Array ? params.color[0] : params.color + const color = props.color + ? props.color instanceof Array ? props.color[0] : props.color : themeCtx.colorThemeRegistry.get(repr.defaultColorTheme.name); - const colorDefaultParams = { ...PD.getDefaultValues(color.getParams(themeCtx)), ...repr.defaultColorTheme.props } - const colorParams = params.color instanceof Array - ? { ...colorDefaultParams, ...params.color[1](color as C, themeCtx) } + const colorDefaultParams = { ...PD.getDefaultValues(color.getParams(themeDataCtx)), ...repr.defaultColorTheme.props } + const colorParams = props.color instanceof Array + ? { ...colorDefaultParams, ...props.color[1](color as C, themeCtx) } : colorDefaultParams; - const size = params.size - ? params.size instanceof Array ? params.size[0] : params.size + const size = props.size + ? props.size instanceof Array ? props.size[0] : props.size : themeCtx.sizeThemeRegistry.get(repr.defaultSizeTheme.name); - const sizeDefaultParams = { ...PD.getDefaultValues(size.getParams(themeCtx)), ...repr.defaultSizeTheme.props } - const sizeParams = params.size instanceof Array - ? { ...sizeDefaultParams, ...params.size[1](size as S, themeCtx) } + const sizeDefaultParams = { ...PD.getDefaultValues(size.getParams(themeDataCtx)), ...repr.defaultSizeTheme.props } + const sizeParams = props.size instanceof Array + ? { ...sizeDefaultParams, ...props.size[1](size as S, themeCtx) } : sizeDefaultParams; return ({ diff --git a/src/mol-plugin/util/structure-complex-helper.ts b/src/mol-plugin/util/structure-complex-helper.ts index 59a428cc8f037ba995e6dfad62ef1d1384c9558e..af783510d5b2d0fe6786adf52226b8e17497b97e 100644 --- a/src/mol-plugin/util/structure-complex-helper.ts +++ b/src/mol-plugin/util/structure-complex-helper.ts @@ -22,7 +22,7 @@ export function createDefaultStructureComplex( .apply(StateTransforms.Representation.StructureRepresentation3D, StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'modified' }, { tags: StructureComplexElementTypes.modified }) + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'non-standard' }, { tags: StructureComplexElementTypes['non-standard'] }) .apply(StateTransforms.Representation.StructureRepresentation3D, StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', void 0, 'polymer-id')); diff --git a/src/mol-plugin/util/structure-overpaint-helper.ts b/src/mol-plugin/util/structure-overpaint-helper.ts index 05d7ea690925ad9ce6b893b9be46daafeb38c0a7..5a2b2da67e59e51c4b2e3c93618aa93c0cbda5ad 100644 --- a/src/mol-plugin/util/structure-overpaint-helper.ts +++ b/src/mol-plugin/util/structure-overpaint-helper.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -7,10 +7,12 @@ import { PluginStateObject } from '../../mol-plugin/state/objects'; import { StateTransforms } from '../../mol-plugin/state/transforms'; import { StateSelection, StateObjectCell, StateTransform, StateBuilder } from '../../mol-state'; -import { Structure, StructureElement } from '../../mol-model/structure'; +import { Structure, StructureElement, StructureSelection, QueryContext } from '../../mol-model/structure'; import { PluginContext } from '../context'; import { Color } from '../../mol-util/color'; import { Overpaint } from '../../mol-theme/overpaint'; +import Expression from '../../mol-script/language/expression'; +import { compile } from '../../mol-script/runtime/query/compiler'; type OverpaintEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, overpaint?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle>>) => void const OverpaintManagerTag = 'overpaint-controls' @@ -29,7 +31,7 @@ export class StructureOverpaintHelper { await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })); } - async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[]) { + async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[], alpha = 1) { await this.eachRepr((update, repr, overpaintCell) => { if (types && !types.includes(repr.params!.values.type.name)) return @@ -48,15 +50,23 @@ export class StructureOverpaintHelper { if (overpaintCell) { const bundleLayers = [ ...overpaintCell.params!.values.layers, layer ] const filtered = getFilteredBundle(bundleLayers, structure) - update.to(overpaintCell).update(Overpaint.toBundle(filtered, 1)) + update.to(overpaintCell).update(Overpaint.toBundle(filtered, alpha)) } else { const filtered = getFilteredBundle([ layer ], structure) update.to(repr.transform.ref) - .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, 1), { tags: OverpaintManagerTag }); + .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, alpha), { tags: OverpaintManagerTag }); } }) } + async setFromExpression(color: Color | -1, expression: Expression, types?: string[], alpha = 1) { + return this.set(color, (structure) => { + const compiled = compile<StructureSelection>(expression) + const result = compiled(new QueryContext(structure)) + return StructureSelection.toLociWithSourceUnits(result) + }, types, alpha) + } + constructor(private plugin: PluginContext) { } diff --git a/src/mol-plugin/util/structure-representation-helper.ts b/src/mol-plugin/util/structure-representation-helper.ts index 6e58d3cae071cc81f2b5f1670cbd63048d9a2380..241d632bd9c4b8437f77e894de6b57c6f3c7770e 100644 --- a/src/mol-plugin/util/structure-representation-helper.ts +++ b/src/mol-plugin/util/structure-representation-helper.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -12,8 +12,6 @@ import { PluginContext } from '../context'; import { StructureRepresentation3DHelpers } from '../state/transforms/representation'; import Expression from '../../mol-script/language/expression'; import { compile } from '../../mol-script/runtime/query/compiler'; -import { StructureSelectionQueries as Q } from '../util/structure-selection-helper'; -import { MolScriptBuilder as MS } from '../../mol-script/language/builder'; import { VisualQuality } from '../../mol-geo/geometry/base'; type StructureTransform = StateObjectCell<PSO.Molecule.Structure, StateTransform<StateTransformer<any, PSO.Molecule.Structure, any>>> @@ -34,6 +32,12 @@ function getCombinedLoci(mode: SelectionModifier, loci: StructureElement.Loci, c type SelectionModifier = 'add' | 'remove' | 'only' +type ReprProps = { + repr?: {}, + color?: string | [string, {}], + size?: string | [string, {}], +} + export class StructureRepresentationHelper { getRepresentationStructure(rootRef: string, type: string) { const state = this.plugin.state.dataState @@ -49,12 +53,52 @@ export class StructureRepresentationHelper { return selections.length > 0 ? selections[0] : undefined } - private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform, props = {}) { + private getRepresentationParams(structure: Structure, type: string, repr: RepresentationTransform | undefined, props: ReprProps = {}) { + const reprProps = { + ...(repr?.params && repr.params.values.type.params), + ignoreHydrogens: this._ignoreHydrogens, + quality: this._quality, + ...props.repr + } + const { themeCtx } = this.plugin.structureRepresentation + + const p: StructureRepresentation3DHelpers.Props = { + repr: [ + this.plugin.structureRepresentation.registry.get(type), + () => reprProps + ] + } + if (props.color) { + const colorType = props.color instanceof Array ? props.color[0] : props.color + const colorTheme = themeCtx.colorThemeRegistry.get(colorType) + const colorProps = { + ...(repr?.params && repr.params.values.colorTheme.name === colorType && repr.params.values.colorTheme.params), + ...(props.color instanceof Array ? props.color[1] : {}) + } + p.color = [colorTheme, () => colorProps] + } + if (props.size) { + const sizeType = props.size instanceof Array ? props.size[0] : props.size + const sizeTheme = themeCtx.sizeThemeRegistry.get(sizeType) + const sizeProps = { + ...(repr?.params && repr.params.values.sizeTheme.name === sizeType && repr.params.values.sizeTheme.params), + ...(props.size instanceof Array ? props.size[1] : {}) + } + p.size = [sizeTheme, () => sizeProps] + } + if (props.size) p.size = props.size + + return StructureRepresentation3DHelpers.createParams(this.plugin, structure, p) + } + + private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform, props: ReprProps = {}) { const state = this.plugin.state.dataState const update = state.build() const s = structure.obj!.data + const repr = this.getRepresentation(structure.transform.ref, type) const reprStructure = this.getRepresentationStructure(structure.transform.ref, type) + const reprParams = this.getRepresentationParams(s.root, type, repr, props) if (reprStructure) { const currentLoci = StructureElement.Bundle.toLoci(reprStructure.params!.values.bundle, s) @@ -64,14 +108,9 @@ export class StructureRepresentationHelper { ...reprStructure.params!.values, bundle: StructureElement.Bundle.fromLoci(combinedLoci) }) + if (repr) update.to(repr).update(reprParams) } else { const combinedLoci = getCombinedLoci(modifier, loci, StructureElement.Loci.none(s)) - const params = StructureRepresentation3DHelpers.getDefaultParams(this.plugin, type as any, s) - - const p = params.type.params - Object.assign(p, props) - if (p.ignoreHydrogens !== undefined) p.ignoreHydrogens = this._ignoreHydrogens - if (p.quality !== undefined) p.quality = this._quality update.to(structure.transform.ref) .apply( @@ -79,13 +118,13 @@ export class StructureRepresentationHelper { { bundle: StructureElement.Bundle.fromLoci(combinedLoci), label: type }, { tags: [ RepresentationManagerTag, getRepresentationManagerTag(type) ] } ) - .apply( StateTransforms.Representation.StructureRepresentation3D, params) + .apply(StateTransforms.Representation.StructureRepresentation3D, reprParams) } await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })) } - async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci, props = {}) { + async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci, props: ReprProps = {}) { const state = this.plugin.state.dataState; const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)) @@ -96,7 +135,7 @@ export class StructureRepresentationHelper { } } - async setFromExpression(modifier: SelectionModifier, type: string, expression: Expression, props = {}) { + async setFromExpression(modifier: SelectionModifier, type: string, expression: Expression, props: ReprProps = {}) { return this.set(modifier, type, (structure) => { const compiled = compile<StructureSelection>(expression) const result = compiled(new QueryContext(structure)) @@ -104,44 +143,76 @@ export class StructureRepresentationHelper { }, props) } - async clear() { + async eachStructure(callback: (structure: StructureTransform, type: string, update: StateBuilder.Root) => void) { const { registry } = this.plugin.structureRepresentation const state = this.plugin.state.dataState; const update = state.build() const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)) - const bundle = StructureElement.Bundle.Empty for (const structure of structures) { for (let i = 0, il = registry.types.length; i < il; ++i) { const type = registry.types[i][0] const reprStructure = this.getRepresentationStructure(structure.transform.ref, type) - if (reprStructure) { - update.to(reprStructure).update({ ...reprStructure.params!.values, bundle }) - } + if (reprStructure) callback(reprStructure, type, update) } } await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })) } - async eachRepresentation(callback: (repr: RepresentationTransform, update: StateBuilder.Root) => void) { + async clear() { + const bundle = StructureElement.Bundle.Empty + await this.eachStructure((structure, type, update) => { + update.to(structure).update({ ...structure.params!.values, bundle }) + }) + } + + async clearExcept(exceptTypes: string[]) { + const bundle = StructureElement.Bundle.Empty + await this.eachStructure((structure, type, update) => { + if (!exceptTypes.includes(type)) { + update.to(structure).update({ ...structure.params!.values, bundle }) + } + }) + } + + async eachRepresentation(callback: (repr: RepresentationTransform, type: string, update: StateBuilder.Root) => void) { const { registry } = this.plugin.structureRepresentation const state = this.plugin.state.dataState; const update = state.build() const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)) for (const structure of structures) { for (let i = 0, il = registry.types.length; i < il; ++i) { - const repr = this.getRepresentation(structure.transform.ref, registry.types[i][0]) - if (repr) callback(repr, update) + const type = registry.types[i][0] + const repr = this.getRepresentation(structure.transform.ref, type) + if (repr) callback(repr, type, update) } } await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })) } + setRepresentationParams(repr: RepresentationTransform, type: string, update: StateBuilder.Root, props: ReprProps) { + const state = this.plugin.state.dataState; + const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)) + + for (const structure of structures) { + const s = structure.obj!.data + const reprParams = this.getRepresentationParams(s.root, type, repr, props) + update.to(repr).update(reprParams) + } + } + + async updateRepresentation(repr: RepresentationTransform, type: string, props: ReprProps) { + const state = this.plugin.state.dataState; + const update = state.build() + this.setRepresentationParams(repr, type, update, props) + await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })) + } + private _ignoreHydrogens = false get ignoreHydrogens () { return this._ignoreHydrogens } async setIgnoreHydrogens(ignoreHydrogens: boolean) { if (ignoreHydrogens === this._ignoreHydrogens) return - await this.eachRepresentation((repr, update) => { + await this.eachRepresentation((repr, type, update) => { if (repr.params && repr.params.values.type.params.ignoreHydrogens !== undefined) { const { name, params } = repr.params.values.type update.to(repr.transform.ref).update( @@ -157,7 +228,7 @@ export class StructureRepresentationHelper { get quality () { return this._quality } async setQuality(quality: VisualQuality) { if (quality === this._quality) return - await this.eachRepresentation((repr, update) => { + await this.eachRepresentation((repr, type, update) => { if (repr.params && repr.params.values.type.params.quality !== undefined) { const { name, params } = repr.params.values.type update.to(repr.transform.ref).update( @@ -169,74 +240,7 @@ export class StructureRepresentationHelper { this._quality = quality } - async preset() { - // TODO option to limit to specific structure - const state = this.plugin.state.dataState; - const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)) - - if (structures.length === 0) return - const s = structures[0].obj!.data - - if (s.elementCount < 50000) { - await polymerAndLigand(this) - } else if (s.elementCount < 200000) { - await proteinAndNucleic(this) - } else { - if (s.unitSymmetryGroups[0].units.length > 10) { - await capsid(this) - } else { - await coarseCapsid(this) - } - } - } - constructor(private plugin: PluginContext) { } -} - -// - -async function polymerAndLigand(r: StructureRepresentationHelper) { - await r.clear() - await r.setFromExpression('add', 'cartoon', Q.polymer.expression) - await r.setFromExpression('add', 'carbohydrate', Q.branchedPlusConnected.expression) - await r.setFromExpression('add', 'ball-and-stick', MS.struct.modifier.union([ - MS.struct.combinator.merge([ - Q.ligandPlusConnected.expression, - Q.branchedConnectedOnly.expression, - Q.disulfideBridges.expression, - Q.nonStandardPolymer.expression, - Q.water.expression - ]) - ])) -} - -async function proteinAndNucleic(r: StructureRepresentationHelper) { - await r.clear() - await r.setFromExpression('add', 'cartoon', Q.protein.expression) - await r.setFromExpression('add', 'gaussian-surface', Q.nucleic.expression) -} - -async function capsid(r: StructureRepresentationHelper) { - await r.clear() - await r.setFromExpression('add', 'gaussian-surface', Q.polymer.expression, { - smoothness: 0.5, - }) -} - -async function coarseCapsid(r: StructureRepresentationHelper) { - await r.clear() - await r.setFromExpression('add', 'gaussian-surface', Q.trace.expression, { - radiusOffset: 1, - smoothness: 0.5, - visuals: ['structure-gaussian-surface-mesh'] - }) -} - -export const StructureRepresentationPresets = { - polymerAndLigand, - proteinAndNucleic, - capsid, - coarseCapsid } \ No newline at end of file diff --git a/src/mol-plugin/util/structure-selection-helper.ts b/src/mol-plugin/util/structure-selection-helper.ts index e8b3c455305d0d85a04cb3d7f407d91abae352f2..377c6507a249752adc9d183299bcbc951afff6e9 100644 --- a/src/mol-plugin/util/structure-selection-helper.ts +++ b/src/mol-plugin/util/structure-selection-helper.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2020 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> @@ -8,32 +8,76 @@ import { MolScriptBuilder as MS } from '../../mol-script/language/builder'; import { StateSelection, StateBuilder } from '../../mol-state'; import { PluginStateObject } from '../state/objects'; -import { QueryContext, StructureSelection, StructureQuery, StructureElement } from '../../mol-model/structure'; +import { QueryContext, StructureSelection, StructureQuery, StructureElement, Structure } from '../../mol-model/structure'; import { compile } from '../../mol-script/runtime/query/compiler'; import { Loci } from '../../mol-model/loci'; import { PluginContext } from '../context'; import Expression from '../../mol-script/language/expression'; import { BondType, ProteinBackboneAtoms, NucleicBackboneAtoms, SecondaryStructureType } from '../../mol-model/structure/model/types'; import { StateTransforms } from '../state/transforms'; +import { SetUtils } from '../../mol-util/set'; +import { ValidationReport, ValidationReportProvider } from '../../mol-model-props/rcsb/validation-report'; +import { CustomProperty } from '../../mol-model-props/common/custom-property'; +import { Task } from '../../mol-task'; +import { AccessibleSurfaceAreaSymbols, AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area'; +import { stringToWords } from '../../mol-util/string'; + +export enum StructureSelectionCategory { + Type = 'Type', + Structure = 'Structure Property', + Atom = 'Atom Property', + Bond = 'Bond Property', + Residue = 'Residue Property', + AminoAcid = 'Amino Acid', + NucleicBase = 'Nucleic Base', + Manipulate = 'Manipulate Selection', + Validation = 'Validation', + Misc = 'Miscellaneous', + Internal = 'Internal', +} + +export { StructureSelectionQuery } + +interface StructureSelectionQuery { + readonly label: string + readonly expression: Expression + readonly description: string + readonly category: string + readonly isHidden: boolean + readonly query: StructureQuery + readonly ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void> +} -export interface StructureSelectionQuery { - label: string - query: StructureQuery - expression: Expression - description: string +interface StructureSelectionQueryProps { + description?: string, + category?: string + isHidden?: boolean + ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void> } -export function StructureSelectionQuery(label: string, expression: Expression, description = ''): StructureSelectionQuery { - return { label, expression, query: compile<StructureSelection>(expression), description } +function StructureSelectionQuery(label: string, expression: Expression, props: StructureSelectionQueryProps = {}): StructureSelectionQuery { + let _query: StructureQuery + return { + label, + expression, + description: props.description || '', + category: props.category ?? StructureSelectionCategory.Misc, + isHidden: !!props.isHidden, + get query() { + if (!_query) _query = compile<StructureSelection>(expression) + return _query + }, + ensureCustomProperties: props.ensureCustomProperties + } } -const all = StructureSelectionQuery('All', MS.struct.generator.all()) +const all = StructureSelectionQuery('All', MS.struct.generator.all(), { category: '' }) const polymer = StructureSelectionQuery('Polymer', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ 'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']) }) -])) +]), { category: StructureSelectionCategory.Type }) const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([ MS.struct.combinator.merge([ @@ -53,7 +97,7 @@ const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([ }) ]) ]) -])) +]), { category: StructureSelectionCategory.Structure }) // TODO maybe pre-calculate atom properties like backbone/sidechain const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([ @@ -68,7 +112,7 @@ const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([ ]) ]), 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), - 'atom-test': MS.core.set.has([MS.set(...Array.from(ProteinBackboneAtoms.values())), MS.ammp('label_atom_id')]) + 'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(ProteinBackboneAtoms)), MS.ammp('label_atom_id')]) }) ]), MS.struct.modifier.union([ @@ -81,11 +125,11 @@ const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([ ]) ]), 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), - 'atom-test': MS.core.set.has([MS.set(...Array.from(NucleicBackboneAtoms.values())), MS.ammp('label_atom_id')]) + 'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(NucleicBackboneAtoms)), MS.ammp('label_atom_id')]) }) ]) ]) -])) +]), { category: StructureSelectionCategory.Structure }) const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -97,7 +141,7 @@ const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([ ]) ]) }) -])) +]), { category: StructureSelectionCategory.Type }) const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -109,7 +153,7 @@ const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([ ]) ]) }) -])) +]), { category: StructureSelectionCategory.Type }) const proteinOrNucleic = StructureSelectionQuery('Protein or Nucleic', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -121,7 +165,7 @@ const proteinOrNucleic = StructureSelectionQuery('Protein or Nucleic', MS.struct ]) ]) }) -])) +]), { category: StructureSelectionCategory.Type }) const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -137,7 +181,7 @@ const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([ MS.core.type.bitflags([SecondaryStructureType.Flag.Helix]) ]) }) -])) +]), { category: StructureSelectionCategory.Residue }) const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -153,13 +197,13 @@ const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.uni MS.core.type.bitflags([SecondaryStructureType.Flag.Beta]) ]) }) -])) +]), { category: StructureSelectionCategory.Residue }) const water = StructureSelectionQuery('Water', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ 'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'water']) }) -])) +]), { category: StructureSelectionCategory.Type }) const branched = StructureSelectionQuery('Carbohydrate', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -174,20 +218,20 @@ const branched = StructureSelectionQuery('Carbohydrate', MS.struct.modifier.unio ]) ]) }) -])) +]), { category: StructureSelectionCategory.Type }) const branchedPlusConnected = StructureSelectionQuery('Carbohydrate with Connected', MS.struct.modifier.union([ MS.struct.modifier.includeConnected({ 0: branched.expression, 'layer-count': 1, 'as-whole-residues': true }) -])) +]), { category: StructureSelectionCategory.Internal }) const branchedConnectedOnly = StructureSelectionQuery('Connected to Carbohydrate', MS.struct.modifier.union([ MS.struct.modifier.exceptBy({ 0: branchedPlusConnected.expression, by: branched.expression }) -])) +]), { category: StructureSelectionCategory.Internal }) const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([ MS.struct.combinator.merge([ @@ -221,7 +265,7 @@ const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([ }) ]) ]), -])) +]), { category: StructureSelectionCategory.Residue }) // don't include branched entities as they have their own link representation const ligandPlusConnected = StructureSelectionQuery('Ligand with Connected', MS.struct.modifier.union([ @@ -241,14 +285,14 @@ const ligandPlusConnected = StructureSelectionQuery('Ligand with Connected', MS. ]), by: branched.expression }) -])) +]), { category: StructureSelectionCategory.Internal }) const ligandConnectedOnly = StructureSelectionQuery('Connected to Ligand', MS.struct.modifier.union([ MS.struct.modifier.exceptBy({ 0: ligandPlusConnected.expression, by: ligand.expression }) -])) +]), { category: StructureSelectionCategory.Internal }) // residues connected to ligands or branched entities const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydrate', MS.struct.modifier.union([ @@ -256,7 +300,7 @@ const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydra branchedConnectedOnly.expression, ligandConnectedOnly.expression ]), -])) +]), { category: StructureSelectionCategory.Internal }) const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct.modifier.union([ MS.struct.modifier.wholeResidues([ @@ -269,14 +313,7 @@ const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct. }) ]) ]) -])) - -const modified = StructureSelectionQuery('Modified Residues', MS.struct.modifier.union([ - MS.struct.generator.atomGroups({ - 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), - 'residue-test': MS.ammp('isModified') - }) -])) +]), { category: StructureSelectionCategory.Bond }) const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Polymers', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -284,7 +321,7 @@ const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Pol 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), 'residue-test': MS.ammp('isNonStandard') }) -])) +]), { category: StructureSelectionCategory.Residue }) const coarse = StructureSelectionQuery('Coarse Elements', MS.struct.modifier.union([ MS.struct.generator.atomGroups({ @@ -292,15 +329,15 @@ const coarse = StructureSelectionQuery('Coarse Elements', MS.struct.modifier.uni MS.set('sphere', 'gaussian'), MS.ammp('objectPrimitive') ]) }) -])) +]), { category: StructureSelectionCategory.Residue }) const ring = StructureSelectionQuery('Rings in Residues', MS.struct.modifier.union([ MS.struct.generator.rings() -])) +]), { category: StructureSelectionCategory.Residue }) const aromaticRing = StructureSelectionQuery('Aromatic Rings in Residues', MS.struct.modifier.union([ MS.struct.generator.rings({ 'only-aromatic': true }) -])) +]), { category: StructureSelectionCategory.Residue }) const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Selection', MS.struct.modifier.union([ MS.struct.modifier.exceptBy({ @@ -311,20 +348,121 @@ const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of }), by: MS.internal.generator.current() }) -]), 'Select residues within 5 \u212B of the current selection.') +]), { + description: 'Select residues within 5 \u212B of the current selection.', + category: StructureSelectionCategory.Manipulate +}) const complement = StructureSelectionQuery('Inverse / Complement of Selection', MS.struct.modifier.union([ MS.struct.modifier.exceptBy({ 0: MS.struct.generator.all(), by: MS.internal.generator.current() }) -]), 'Select everything not in the current selection.') +]), { + description: 'Select everything not in the current selection.', + category: StructureSelectionCategory.Manipulate +}) const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct.modifier.union([ MS.struct.modifier.includeConnected({ 0: MS.internal.generator.current(), 'layer-count': 1, 'as-whole-residues': true }) -]), 'Select residues covalently bonded to current selection.') +]), { + description: 'Select residues covalently bonded to current selection.', + category: StructureSelectionCategory.Manipulate +}) + +const hasClash = StructureSelectionQuery('Residues with Clashes', MS.struct.modifier.union([ + MS.struct.modifier.wholeResidues([ + MS.struct.modifier.union([ + MS.struct.generator.atomGroups({ + 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), + 'atom-test': ValidationReport.symbols.hasClash.symbol(), + }) + ]) + ]) +]), { + description: 'Select residues with clashes in the wwPDB validation report.', + category: StructureSelectionCategory.Residue, + ensureCustomProperties: (ctx, structure) => { + return ValidationReportProvider.attach(ctx, structure.models[0]) + } +}) + +const isBuried = StructureSelectionQuery('Buried Protein Residues', MS.struct.modifier.union([ + MS.struct.modifier.wholeResidues([ + MS.struct.modifier.union([ + MS.struct.generator.atomGroups({ + 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), + 'residue-test': AccessibleSurfaceAreaSymbols.isBuried.symbol(), + }) + ]) + ]) +]), { + description: 'Select buried protein residues.', + category: StructureSelectionCategory.Residue, + ensureCustomProperties: (ctx, structure) => { + return AccessibleSurfaceAreaProvider.attach(ctx, structure) + } +}) + +const isAccessible = StructureSelectionQuery('Accessible Protein Residues', MS.struct.modifier.union([ + MS.struct.modifier.wholeResidues([ + MS.struct.modifier.union([ + MS.struct.generator.atomGroups({ + 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']), + 'residue-test': AccessibleSurfaceAreaSymbols.isAccessible.symbol(), + }) + ]) + ]) +]), { + description: 'Select accessible protein residues.', + category: StructureSelectionCategory.Residue, + ensureCustomProperties: (ctx, structure) => { + return AccessibleSurfaceAreaProvider.attach(ctx, structure) + } +}) + +const StandardAminoAcids = [ + [['HIS'], 'HISTIDINE'], + [['ARG'], 'ARGININE'], + [['LYS'], 'LYSINE'], + [['ILE'], 'ISOLEUCINE'], + [['PHE'], 'PHENYLALANINE'], + [['LEU'], 'LEUCINE'], + [['TRP'], 'TRYPTOPHAN'], + [['ALA'], 'ALANINE'], + [['MET'], 'METHIONINE'], + [['CYS'], 'CYSTEINE'], + [['ASN'], 'ASPARAGINE'], + [['VAL'], 'VALINE'], + [['GLY'], 'GLYCINE'], + [['SER'], 'SERINE'], + [['GLN'], 'GLUTAMINE'], + [['TYR'], 'TYROSINE'], + [['ASP'], 'ASPARTIC ACID'], + [['GLU'], 'GLUTAMIC ACID'], + [['THR'], 'THREONINE'], + [['SEC'], 'SELENOCYSTEINE'], + [['PYL'], 'PYRROLYSINE'], +].sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) as [string[], string][] + +const StandardNucleicBases = [ + [['A', 'DA'], 'ADENOSINE'], + [['C', 'DC'], 'CYTIDINE'], + [['T', 'DT'], 'THYMIDINE'], + [['G', 'DG'], 'GUANOSINE'], + [['I', 'DI'], 'INOSINE'], + [['U', 'DU'], 'URIDINE'], +].sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) as [string[], string][] + +function ResidueQuery([names, label]: [string[], string], category: string) { + return StructureSelectionQuery(`${stringToWords(label)} (${names.join(', ')})`, MS.struct.modifier.union([ + MS.struct.generator.atomGroups({ + 'residue-test': MS.core.set.has([MS.set(...names), MS.ammp('auth_comp_id')]) + }) + ]), { category }) +} export const StructureSelectionQueries = { all, @@ -345,7 +483,6 @@ export const StructureSelectionQueries = { ligandConnectedOnly, connectedOnly, disulfideBridges, - modified, nonStandardPolymer, coarse, ring, @@ -353,8 +490,18 @@ export const StructureSelectionQueries = { surroundings, complement, bonded, + + hasClash, + isBuried, + isAccessible } +export const StructureSelectionQueryList = [ + ...Object.values(StructureSelectionQueries), + ...StandardAminoAcids.map(v => ResidueQuery(v, StructureSelectionCategory.AminoAcid)), + ...StandardNucleicBases.map(v => ResidueQuery(v, StructureSelectionCategory.NucleicBase)), +] + export function applyBuiltInSelection(to: StateBuilder.To<PluginStateObject.Molecule.Structure>, query: keyof typeof StructureSelectionQueries, customTag?: string) { return to.apply(StateTransforms.Model.StructureSelectionFromExpression, { expression: StructureSelectionQueries[query].expression, label: StructureSelectionQueries[query].label }, @@ -384,17 +531,24 @@ export class StructureSelectionHelper { } } - set(modifier: SelectionModifier, query: StructureQuery, applyGranularity = true) { - for (const s of this.structures) { - const current = this.plugin.helpers.structureSelectionManager.get(s) - const currentSelection = Loci.isEmpty(current) - ? StructureSelection.Empty(s) - : StructureSelection.Singletons(s, StructureElement.Loci.toStructure(current)) - - const result = query(new QueryContext(s, { currentSelection })) - const loci = StructureSelection.toLociWithSourceUnits(result) - this._set(modifier, loci, applyGranularity) - } + async set(modifier: SelectionModifier, selectionQuery: StructureSelectionQuery, applyGranularity = true) { + this.plugin.runTask(Task.create('Structure Selection', async runtime => { + const ctx = { fetch: this.plugin.fetch, runtime } + for (const s of this.structures) { + const current = this.plugin.helpers.structureSelectionManager.get(s) + const currentSelection = Loci.isEmpty(current) + ? StructureSelection.Empty(s) + : StructureSelection.Singletons(s, StructureElement.Loci.toStructure(current)) + + if (selectionQuery.ensureCustomProperties) { + await selectionQuery.ensureCustomProperties(ctx, s) + } + + const result = selectionQuery.query(new QueryContext(s, { currentSelection })) + const loci = StructureSelection.toLociWithSourceUnits(result) + this._set(modifier, loci, applyGranularity) + } + })) } constructor(private plugin: PluginContext) { diff --git a/src/mol-repr/structure/registry.ts b/src/mol-repr/structure/registry.ts index 970c5e12bc299113d8865aac03aa6dbb16fc3835..9ec7a53d1263c59f042a1a0ee4686e106ffebb70 100644 --- a/src/mol-repr/structure/registry.ts +++ b/src/mol-repr/structure/registry.ts @@ -11,7 +11,6 @@ import { BallAndStickRepresentationProvider } from './representation/ball-and-st import { GaussianSurfaceRepresentationProvider } from './representation/gaussian-surface'; import { CarbohydrateRepresentationProvider } from './representation/carbohydrate'; import { SpacefillRepresentationProvider } from './representation/spacefill'; -import { DistanceRestraintRepresentationProvider } from './representation/distance-restraint'; import { PointRepresentationProvider } from './representation/point'; import { StructureRepresentationState } from './representation'; import { PuttyRepresentationProvider } from './representation/putty'; @@ -34,7 +33,6 @@ export const BuiltInStructureRepresentations = { 'cartoon': CartoonRepresentationProvider, 'ball-and-stick': BallAndStickRepresentationProvider, 'carbohydrate': CarbohydrateRepresentationProvider, - 'distance-restraint': DistanceRestraintRepresentationProvider, 'ellipsoid': EllipsoidRepresentationProvider, 'gaussian-surface': GaussianSurfaceRepresentationProvider, // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work diff --git a/src/mol-repr/structure/representation/distance-restraint.ts b/src/mol-repr/structure/representation/distance-restraint.ts deleted file mode 100644 index 752a9b46d0f1d0ffb50bcf4bc81b199fbe88dd20..0000000000000000000000000000000000000000 --- a/src/mol-repr/structure/representation/distance-restraint.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { CrossLinkRestraintVisual, CrossLinkRestraintParams } from '../visual/cross-link-restraint-cylinder'; -import { ParamDefinition as PD } from '../../../mol-util/param-definition'; -import { ComplexRepresentation } from '../complex-representation'; -import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation'; -import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation'; -import { ThemeRegistryContext } from '../../../mol-theme/theme'; -import { Structure } from '../../../mol-model/structure'; -import { UnitKind, UnitKindOptions } from '../visual/util/common'; - -const DistanceRestraintVisuals = { - 'distance-restraint': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintParams>) => ComplexRepresentation('Cross-link restraint', ctx, getParams, CrossLinkRestraintVisual), -} - -export const DistanceRestraintParams = { - ...CrossLinkRestraintParams, - unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions), -} -export type DistanceRestraintParams = typeof DistanceRestraintParams -export function getDistanceRestraintParams(ctx: ThemeRegistryContext, structure: Structure) { - return PD.clone(DistanceRestraintParams) -} - -export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintParams> -export function DistanceRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, DistanceRestraintParams>): DistanceRestraintRepresentation { - return Representation.createMulti('DistanceRestraint', ctx, getParams, StructureRepresentationStateBuilder, DistanceRestraintVisuals as unknown as Representation.Def<Structure, DistanceRestraintParams>) -} - -export const DistanceRestraintRepresentationProvider: StructureRepresentationProvider<DistanceRestraintParams> = { - label: 'Distance Restraint', - description: 'Displays cross-link distance restraints.', - factory: DistanceRestraintRepresentation, - getParams: getDistanceRestraintParams, - defaultValues: PD.getDefaultValues(DistanceRestraintParams), - defaultColorTheme: { name: 'cross-link' }, - defaultSizeTheme: { name: 'uniform' }, - isApplicable: (structure: Structure) => structure.crossLinkRestraints.count > 0 -} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts deleted file mode 100644 index db047214479a432ea8d0e84dc57c3f5d9b3fd0d1..0000000000000000000000000000000000000000 --- a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { ParamDefinition as PD } from '../../../mol-util/param-definition'; -import { VisualContext } from '../../visual'; -import { Structure, StructureElement, Bond } from '../../../mol-model/structure'; -import { Theme } from '../../../mol-theme/theme'; -import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; -import { Vec3 } from '../../../mol-math/linear-algebra'; -import { createLinkCylinderMesh, LinkCylinderParams } from './util/link'; -import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual'; -import { VisualUpdateState } from '../../util'; -import { LocationIterator } from '../../../mol-geo/util/location-iterator'; -import { PickingId } from '../../../mol-geo/geometry/picking'; -import { EmptyLoci, Loci } from '../../../mol-model/loci'; -import { Interval } from '../../../mol-data/int'; - -function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintParams>, mesh?: Mesh) { - - const crossLinks = structure.crossLinkRestraints - if (!crossLinks.count) return Mesh.createEmpty(mesh) - const { sizeFactor } = props - - const location = StructureElement.Location.create(structure) - - const builderProps = { - linkCount: crossLinks.count, - position: (posA: Vec3, posB: Vec3, edgeIndex: number) => { - const b = crossLinks.pairs[edgeIndex] - const uA = b.unitA, uB = b.unitB - uA.conformation.position(uA.elements[b.indexA], posA) - uB.conformation.position(uB.elements[b.indexB], posB) - }, - radius: (edgeIndex: number) => { - const b = crossLinks.pairs[edgeIndex] - location.unit = b.unitA - location.element = b.unitA.elements[b.indexA] - return theme.size.size(location) * sizeFactor - }, - } - - return createLinkCylinderMesh(ctx, builderProps, props, mesh) -} - -export const CrossLinkRestraintParams = { - ...ComplexMeshParams, - ...LinkCylinderParams, - sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }), -} -export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams - -export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintParams> { - return ComplexMeshVisual<CrossLinkRestraintParams>({ - defaultProps: PD.getDefaultValues(CrossLinkRestraintParams), - createGeometry: createCrossLinkRestraintCylinderMesh, - createLocationIterator: CrossLinkRestraintIterator, - getLoci: getLinkLoci, - eachLocation: eachCrossLink, - setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintParams>, currentProps: PD.Values<CrossLinkRestraintParams>) => { - state.createGeometry = ( - newProps.sizeFactor !== currentProps.sizeFactor || - newProps.radialSegments !== currentProps.radialSegments || - newProps.linkCap !== currentProps.linkCap - ) - } - }, materialId) -} - -function CrossLinkRestraintIterator(structure: Structure): LocationIterator { - const { pairs } = structure.crossLinkRestraints - const groupCount = pairs.length - const instanceCount = 1 - const location = Bond.Location() - const getLocation = (groupIndex: number) => { - const pair = pairs[groupIndex] - location.aStructure = structure - location.aUnit = pair.unitA - location.aIndex = pair.indexA - location.bStructure = structure - location.bUnit = pair.unitB - location.bIndex = pair.indexB - return location - } - return LocationIterator(groupCount, instanceCount, getLocation, true) -} - -function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { - const { objectId, groupId } = pickingId - if (id === objectId) { - const pair = structure.crossLinkRestraints.pairs[groupId] - if (pair) { - return Bond.Loci(structure, [ - Bond.Location(structure, pair.unitA, pair.indexA, structure, pair.unitB, pair.indexB), - Bond.Location(structure, pair.unitB, pair.indexB, structure, pair.unitA, pair.indexA) - ]) - } - } - return EmptyLoci -} - -function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) { - const crossLinks = structure.crossLinkRestraints - let changed = false - if (Bond.isLoci(loci)) { - if (!Structure.areEquivalent(loci.structure, structure)) return false - for (const b of loci.bonds) { - const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit) - if (indices) { - for (let i = 0, il = indices.length; i < il; ++i) { - if (apply(Interval.ofSingleton(indices[i]))) changed = true - } - } - } - } - return changed -} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts index 255001edbe5f4f32d766b2506c72e33d317e7fa5..95b701f2c222eef56b850df1ea6d09f48d8ebdf6 100644 --- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts @@ -53,7 +53,6 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh) const { elements, model } = unit - const { modifiedResidues } = model.properties const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue const { label_comp_id } = residues @@ -72,9 +71,7 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St const { index: residueIndex } = residueIt.move(); if (isNucleic(moleculeType[residueIndex])) { - let compId = label_comp_id.value(residueIndex) - const parentId = modifiedResidues.parentId.get(compId) - if (parentId !== undefined) compId = parentId + const compId = label_comp_id.value(residueIndex) let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1 let width = 4.5, height = 4.5, depth = 2.5 * sizeFactor diff --git a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts index c205c3f5af71e495c8a23a30ac560dbd134b5d73..2dd871368e68d2040ba15f7ce17d8215cd7f8e9d 100644 --- a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts @@ -72,7 +72,6 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh) const { elements, model } = unit - const { modifiedResidues } = model.properties const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue const { label_comp_id } = residues @@ -93,9 +92,7 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str const { index: residueIndex } = residueIt.move(); if (isNucleic(moleculeType[residueIndex])) { - let compId = label_comp_id.value(residueIndex) - const parentId = modifiedResidues.parentId.get(compId) - if (parentId !== undefined) compId = parentId + const compId = label_comp_id.value(residueIndex) let idxTrace: ElementIndex | -1 = -1, idxN1: ElementIndex | -1 = -1, idxC2: ElementIndex | -1 = -1, idxN3: ElementIndex | -1 = -1, idxC4: ElementIndex | -1 = -1, idxC5: ElementIndex | -1 = -1, idxC6: ElementIndex | -1 = -1, idxN7: ElementIndex | -1 = -1, idxC8: ElementIndex | -1 = -1, idxN9: ElementIndex | -1 = -1 diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts index 390e0c57a1051c2eff7c4a63268803e1a39b2df0..2046a6bb472b1066350284515affb13749057b21 100644 --- a/src/mol-script/runtime/query/table.ts +++ b/src/mol-script/runtime/query/table.ts @@ -325,8 +325,6 @@ const symbols = [ D(MolScript.structureQuery.atomProperty.macromolecular.entitySubtype, atomProp(StructureProperties.entity.subtype)), D(MolScript.structureQuery.atomProperty.macromolecular.objectPrimitive, atomProp(StructureProperties.unit.object_primitive)), - D(MolScript.structureQuery.atomProperty.macromolecular.isModified, atomProp(StructureProperties.residue.isModified)), - D(MolScript.structureQuery.atomProperty.macromolecular.modifiedParentName, atomProp(StructureProperties.residue.modifiedParentName)), D(MolScript.structureQuery.atomProperty.macromolecular.isNonStandard, atomProp(StructureProperties.residue.isNonStandard)), D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureKey, atomProp(StructureProperties.residue.secondary_structure_key)), D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureFlags, atomProp(StructureProperties.residue.secondary_structure_type)), diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts index 9288607d6d919b675f76a7ddd2b686f63c5bbb55..6829233f6e18fe8642b1216451c6ad8bd7f8c84f 100644 --- a/src/mol-theme/color.ts +++ b/src/mol-theme/color.ts @@ -13,7 +13,6 @@ import { deepEqual } from '../mol-util'; import { ParamDefinition as PD } from '../mol-util/param-definition'; import { ThemeDataContext, ThemeRegistry, ThemeProvider } from './theme'; import { ChainIdColorThemeProvider } from './color/chain-id'; -import { CrossLinkColorThemeProvider } from './color/cross-link'; import { ElementIndexColorThemeProvider } from './color/element-index'; import { ElementSymbolColorThemeProvider } from './color/element-symbol'; import { MoleculeTypeColorThemeProvider } from './color/molecule-type'; @@ -47,6 +46,15 @@ interface ColorTheme<P extends PD.Params> { readonly legend?: Readonly<ScaleLegend | TableLegend> } namespace ColorTheme { + export const enum Category { + Atom = 'Atom Property', + Chain = 'Chain Property', + Residue = 'Residue Property', + Symmetry = 'Symmetry', + Validation = 'Validation', + Misc = 'Miscellaneous', + } + export type Props = { [k: string]: any } export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P> export const EmptyFactory = () => Empty @@ -63,7 +71,7 @@ namespace ColorTheme { } export interface Provider<P extends PD.Params> extends ThemeProvider<ColorTheme<P>, P> { } - export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true } + export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true } export type Registry = ThemeRegistry<ColorTheme<any>> export function createRegistry() { @@ -76,7 +84,6 @@ namespace ColorTheme { export const BuiltInColorThemes = { 'carbohydrate-symbol': CarbohydrateSymbolColorThemeProvider, 'chain-id': ChainIdColorThemeProvider, - 'cross-link': CrossLinkColorThemeProvider, 'element-index': ElementIndexColorThemeProvider, 'element-symbol': ElementSymbolColorThemeProvider, 'entity-source': EntitySourceColorThemeProvider, diff --git a/src/mol-theme/color/carbohydrate-symbol.ts b/src/mol-theme/color/carbohydrate-symbol.ts index f29d10170f11dffba9626bc8ac4a212e68db3d94..39d41725491f48c9bf5ee3fcccdcc363d401d4c5 100644 --- a/src/mol-theme/color/carbohydrate-symbol.ts +++ b/src/mol-theme/color/carbohydrate-symbol.ts @@ -62,6 +62,7 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<CarbohydrateSymbolColorThemeParams> = { label: 'Carbohydrate Symbol', + category: ColorTheme.Category.Residue, factory: CarbohydrateSymbolColorTheme, getParams: getCarbohydrateSymbolColorThemeParams, defaultValues: PD.getDefaultValues(CarbohydrateSymbolColorThemeParams), diff --git a/src/mol-theme/color/chain-id.ts b/src/mol-theme/color/chain-id.ts index 16c072ff40491cae1f2c181df6d4eef3194fe1fa..46b587c708517f24ee6c052fe6f7c47376c21a87 100644 --- a/src/mol-theme/color/chain-id.ts +++ b/src/mol-theme/color/chain-id.ts @@ -119,6 +119,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI export const ChainIdColorThemeProvider: ColorTheme.Provider<ChainIdColorThemeParams> = { label: 'Chain Id', + category: ColorTheme.Category.Chain, factory: ChainIdColorTheme, getParams: getChainIdColorThemeParams, defaultValues: PD.getDefaultValues(ChainIdColorThemeParams), diff --git a/src/mol-theme/color/element-index.ts b/src/mol-theme/color/element-index.ts index be94f3376dd57a6609497ea829522055eff3e7d2..0e796512a00b6af51a604b3a92276aa49f1bf17f 100644 --- a/src/mol-theme/color/element-index.ts +++ b/src/mol-theme/color/element-index.ts @@ -73,6 +73,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<E export const ElementIndexColorThemeProvider: ColorTheme.Provider<ElementIndexColorThemeParams> = { label: 'Element Index', + category: ColorTheme.Category.Atom, factory: ElementIndexColorTheme, getParams: getElementIndexColorThemeParams, defaultValues: PD.getDefaultValues(ElementIndexColorThemeParams), diff --git a/src/mol-theme/color/element-symbol.ts b/src/mol-theme/color/element-symbol.ts index 6f2f8881f7d016719366e50f9c3730ad96ed996d..3582d1414f04afaac51849cf3cd262c4b8c6af71 100644 --- a/src/mol-theme/color/element-symbol.ts +++ b/src/mol-theme/color/element-symbol.ts @@ -69,6 +69,7 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values< export const ElementSymbolColorThemeProvider: ColorTheme.Provider<ElementSymbolColorThemeParams> = { label: 'Element Symbol', + category: ColorTheme.Category.Atom, factory: ElementSymbolColorTheme, getParams: getElementSymbolColorThemeParams, defaultValues: PD.getDefaultValues(ElementSymbolColorThemeParams), diff --git a/src/mol-theme/color/entity-source.ts b/src/mol-theme/color/entity-source.ts index a8ac312bfe1693d8be1637d780f1709702303b5d..641839576b59aba23a4e8ac23672aa9150928f64 100644 --- a/src/mol-theme/color/entity-source.ts +++ b/src/mol-theme/color/entity-source.ts @@ -175,6 +175,7 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E export const EntitySourceColorThemeProvider: ColorTheme.Provider<EntitySourceColorThemeParams> = { label: 'Entity Source', + category: ColorTheme.Category.Chain, factory: EntitySourceColorTheme, getParams: getEntitySourceColorThemeParams, defaultValues: PD.getDefaultValues(EntitySourceColorThemeParams), diff --git a/src/mol-theme/color/hydrophobicity.ts b/src/mol-theme/color/hydrophobicity.ts index b281987b2d1d828456a8c39f075f74d68f69c6ca..6ddea14a6e505d8270eca238da5ec61450a8fc6e 100644 --- a/src/mol-theme/color/hydrophobicity.ts +++ b/src/mol-theme/color/hydrophobicity.ts @@ -32,22 +32,16 @@ export function hydrophobicity(compId: string, scaleIndex: number): number { } function getAtomicCompId(unit: Unit.Atomic, element: ElementIndex) { - const { modifiedResidues } = unit.model.properties - const compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element]) - const parentId = modifiedResidues.parentId.get(compId) - return parentId === undefined ? compId : parentId + return unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element]) } function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) { const seqIdBegin = unit.coarseElements.seq_id_begin.value(element) const seqIdEnd = unit.coarseElements.seq_id_end.value(element) if (seqIdBegin === seqIdEnd) { - const { modifiedResidues } = unit.model.properties const entityKey = unit.coarseElements.entityKey[element] const seq = unit.model.sequence.byEntityKey[entityKey].sequence - let compId = seq.compId.value(seqIdBegin - 1) // 1-indexed - const parentId = modifiedResidues.parentId.get(compId) - return parentId === undefined ? compId : parentId + return seq.compId.value(seqIdBegin - 1) // 1-indexed } } @@ -100,6 +94,7 @@ export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values export const HydrophobicityColorThemeProvider: ColorTheme.Provider<HydrophobicityColorThemeParams> = { label: 'Hydrophobicity', + category: ColorTheme.Category.Residue, factory: HydrophobicityColorTheme, getParams: getHydrophobicityColorThemeParams, defaultValues: PD.getDefaultValues(HydrophobicityColorThemeParams), diff --git a/src/mol-theme/color/illustrative.ts b/src/mol-theme/color/illustrative.ts index 6025f1eed563a09a307540d332fc67f3f4583bd7..9b86d8b97a76e5ac85504745ffb30c46fa28b17a 100644 --- a/src/mol-theme/color/illustrative.ts +++ b/src/mol-theme/color/illustrative.ts @@ -76,6 +76,7 @@ export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<I export const IllustrativeColorThemeProvider: ColorTheme.Provider<IllustrativeColorThemeParams> = { label: 'Illustrative', + category: ColorTheme.Category.Misc, factory: IllustrativeColorTheme, getParams: getIllustrativeColorThemeParams, defaultValues: PD.getDefaultValues(IllustrativeColorThemeParams), diff --git a/src/mol-theme/color/model-index.ts b/src/mol-theme/color/model-index.ts index fd98c44f77c4797a3710b20f0313f8969e25b899..20df7e57212ef604707c73e3fd707f4f8f82109b 100644 --- a/src/mol-theme/color/model-index.ts +++ b/src/mol-theme/color/model-index.ts @@ -61,6 +61,7 @@ export function ModelIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Mod export const ModelIndexColorThemeProvider: ColorTheme.Provider<ModelIndexColorThemeParams> = { label: 'Model Index', + category: ColorTheme.Category.Chain, factory: ModelIndexColorTheme, getParams: getModelIndexColorThemeParams, defaultValues: PD.getDefaultValues(ModelIndexColorThemeParams), diff --git a/src/mol-theme/color/molecule-type.ts b/src/mol-theme/color/molecule-type.ts index 49f891381cf11888e0264233757fc4051bdc2707..1692e649728c5564deef6859b770c0bd6677e060 100644 --- a/src/mol-theme/color/molecule-type.ts +++ b/src/mol-theme/color/molecule-type.ts @@ -78,6 +78,7 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<M export const MoleculeTypeColorThemeProvider: ColorTheme.Provider<MoleculeTypeColorThemeParams> = { label: 'Molecule Type', + category: ColorTheme.Category.Residue, factory: MoleculeTypeColorTheme, getParams: getMoleculeTypeColorThemeParams, defaultValues: PD.getDefaultValues(MoleculeTypeColorThemeParams), diff --git a/src/mol-theme/color/occupancy.ts b/src/mol-theme/color/occupancy.ts index cf609f010a192f7adad85f8c1846d376b30e2627..47686503c292c9da785e2fd077b2617de628be14 100644 --- a/src/mol-theme/color/occupancy.ts +++ b/src/mol-theme/color/occupancy.ts @@ -60,6 +60,7 @@ export function OccupancyColorTheme(ctx: ThemeDataContext, props: PD.Values<Occu export const OccupancyColorThemeProvider: ColorTheme.Provider<OccupancyColorThemeParams> = { label: 'Occupancy', + category: ColorTheme.Category.Atom, factory: OccupancyColorTheme, getParams: getOccupancyColorThemeParams, defaultValues: PD.getDefaultValues(OccupancyColorThemeParams), diff --git a/src/mol-theme/color/operator-hkl.ts b/src/mol-theme/color/operator-hkl.ts index 2469356eaa12799c0496bf2f27cad96a04184311..b6adab5f2161bd87dd6eb2c6a8fe864295714184 100644 --- a/src/mol-theme/color/operator-hkl.ts +++ b/src/mol-theme/color/operator-hkl.ts @@ -119,6 +119,7 @@ export function OperatorHklColorTheme(ctx: ThemeDataContext, props: PD.Values<Op export const OperatorHklColorThemeProvider: ColorTheme.Provider<OperatorHklColorThemeParams> = { label: 'Operator HKL', + category: ColorTheme.Category.Symmetry, factory: OperatorHklColorTheme, getParams: getOperatorHklColorThemeParams, defaultValues: PD.getDefaultValues(OperatorHklColorThemeParams), diff --git a/src/mol-theme/color/operator-name.ts b/src/mol-theme/color/operator-name.ts index 16d48b147f4833878b6bd90452f70ac5dbb5a06d..3c4b60895f0dabda9af8c4bf6a817ed6f11d5830 100644 --- a/src/mol-theme/color/operator-name.ts +++ b/src/mol-theme/color/operator-name.ts @@ -85,6 +85,7 @@ export function OperatorNameColorTheme(ctx: ThemeDataContext, props: PD.Values<O export const OperatorNameColorThemeProvider: ColorTheme.Provider<OperatorNameColorThemeParams> = { label: 'Operator Name', + category: ColorTheme.Category.Symmetry, factory: OperatorNameColorTheme, getParams: getOperatorNameColorThemeParams, defaultValues: PD.getDefaultValues(OperatorNameColorThemeParams), diff --git a/src/mol-theme/color/polymer-id.ts b/src/mol-theme/color/polymer-id.ts index 46955e905c48bad6d951683869e8c9f3fb5f94ee..68649703483620535f88e05a03f19076ad7831d2 100644 --- a/src/mol-theme/color/polymer-id.ts +++ b/src/mol-theme/color/polymer-id.ts @@ -127,7 +127,8 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly } export const PolymerIdColorThemeProvider: ColorTheme.Provider<PolymerIdColorThemeParams> = { - label: 'Polymer Id', + label: 'Polymer Chain Id', + category: ColorTheme.Category.Chain, factory: PolymerIdColorTheme, getParams: getPolymerIdColorThemeParams, defaultValues: PD.getDefaultValues(PolymerIdColorThemeParams), diff --git a/src/mol-theme/color/polymer-index.ts b/src/mol-theme/color/polymer-index.ts index 02bfa5b44266b20e431980f35b4d78545a5b5dbd..6d6c86dc9ae258c16509e6478d0b1a63ce6d974f 100644 --- a/src/mol-theme/color/polymer-index.ts +++ b/src/mol-theme/color/polymer-index.ts @@ -16,7 +16,7 @@ import { ColorLists } from '../../mol-util/color/lists'; const DefaultList = 'dark-2' const DefaultColor = Color(0xCCCCCC) -const Description = 'Gives every polymer a unique color based on the position (index) of the polymer in the list of polymers in the structure.' +const Description = 'Gives every polymer chain instance a unique color based on the position (index) of the polymer in the list of polymers in the structure.' export const PolymerIndexColorThemeParams = { ...getPaletteParams({ type: 'set', setList: DefaultList }), @@ -87,7 +87,8 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<P } export const PolymerIndexColorThemeProvider: ColorTheme.Provider<PolymerIndexColorThemeParams> = { - label: 'Polymer Index', + label: 'Polymer Chain Instance', + category: ColorTheme.Category.Chain, factory: PolymerIndexColorTheme, getParams: getPolymerIndexColorThemeParams, defaultValues: PD.getDefaultValues(PolymerIndexColorThemeParams), diff --git a/src/mol-theme/color/residue-name.ts b/src/mol-theme/color/residue-name.ts index 1703761c3b9252c2535b70a3e0f6d8ee50454916..741601e1cd2e6f37dd068d3d59857ac24b4e45de 100644 --- a/src/mol-theme/color/residue-name.ts +++ b/src/mol-theme/color/residue-name.ts @@ -74,22 +74,16 @@ export function getResidueNameColorThemeParams(ctx: ThemeDataContext) { } function getAtomicCompId(unit: Unit.Atomic, element: ElementIndex) { - const { modifiedResidues } = unit.model.properties - const compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element]) - const parentId = modifiedResidues.parentId.get(compId) - return parentId === undefined ? compId : parentId + return unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element]) } function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) { const seqIdBegin = unit.coarseElements.seq_id_begin.value(element) const seqIdEnd = unit.coarseElements.seq_id_end.value(element) if (seqIdBegin === seqIdEnd) { - const { modifiedResidues } = unit.model.properties const entityKey = unit.coarseElements.entityKey[element] const seq = unit.model.sequence.byEntityKey[entityKey].sequence - let compId = seq.compId.value(seqIdBegin - 1) // 1-indexed - const parentId = modifiedResidues.parentId.get(compId) - return parentId === undefined ? compId : parentId + return seq.compId.value(seqIdBegin - 1) // 1-indexed } } @@ -136,6 +130,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<Re export const ResidueNameColorThemeProvider: ColorTheme.Provider<ResidueNameColorThemeParams> = { label: 'Residue Name', + category: ColorTheme.Category.Residue, factory: ResidueNameColorTheme, getParams: getResidueNameColorThemeParams, defaultValues: PD.getDefaultValues(ResidueNameColorThemeParams), diff --git a/src/mol-theme/color/secondary-structure.ts b/src/mol-theme/color/secondary-structure.ts index 9eef3bb1c704290a78bd9cb9112104af87a1125a..12eaf2eb8df454b20d2b7b1d9cf1d7d2c6521da4 100644 --- a/src/mol-theme/color/secondary-structure.ts +++ b/src/mol-theme/color/secondary-structure.ts @@ -113,6 +113,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Va export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<SecondaryStructureColorThemeParams> = { label: 'Secondary Structure', + category: ColorTheme.Category.Residue, factory: SecondaryStructureColorTheme, getParams: getSecondaryStructureColorThemeParams, defaultValues: PD.getDefaultValues(SecondaryStructureColorThemeParams), diff --git a/src/mol-theme/color/sequence-id.ts b/src/mol-theme/color/sequence-id.ts index 66457c0b576b579c692cae88ab9ccdc728aec05b..26881241a45d546ec50da3cba5c17b4cf6314487 100644 --- a/src/mol-theme/color/sequence-id.ts +++ b/src/mol-theme/color/sequence-id.ts @@ -101,6 +101,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Seq export const SequenceIdColorThemeProvider: ColorTheme.Provider<SequenceIdColorThemeParams> = { label: 'Sequence Id', + category: ColorTheme.Category.Residue, factory: SequenceIdColorTheme, getParams: getSequenceIdColorThemeParams, defaultValues: PD.getDefaultValues(SequenceIdColorThemeParams), diff --git a/src/mol-theme/color/shape-group.ts b/src/mol-theme/color/shape-group.ts index 1c3166fafdadbefae4f83b7b9cd12d708a46ddf3..8a8232657dec24eac00254c08ba2bf2a458b4dc7 100644 --- a/src/mol-theme/color/shape-group.ts +++ b/src/mol-theme/color/shape-group.ts @@ -37,6 +37,7 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<Sha export const ShapeGroupColorThemeProvider: ColorTheme.Provider<ShapeGroupColorThemeParams> = { label: 'Shape Group', + category: ColorTheme.Category.Misc, factory: ShapeGroupColorTheme, getParams: getShapeGroupColorThemeParams, defaultValues: PD.getDefaultValues(ShapeGroupColorThemeParams), diff --git a/src/mol-theme/color/uncertainty.ts b/src/mol-theme/color/uncertainty.ts index d7511cb0533f6554bf50d9d92797ef1b0df223f5..512dbea67781636037f14c4bbf34b9cf7202acf4 100644 --- a/src/mol-theme/color/uncertainty.ts +++ b/src/mol-theme/color/uncertainty.ts @@ -64,6 +64,7 @@ export function UncertaintyColorTheme(ctx: ThemeDataContext, props: PD.Values<Un export const UncertaintyColorThemeProvider: ColorTheme.Provider<UncertaintyColorThemeParams> = { label: 'Uncertainty/Disorder', + category: ColorTheme.Category.Atom, factory: UncertaintyColorTheme, getParams: getUncertaintyColorThemeParams, defaultValues: PD.getDefaultValues(UncertaintyColorThemeParams), diff --git a/src/mol-theme/color/uniform.ts b/src/mol-theme/color/uniform.ts index 6bc0e8bdc70be6be9be3c061eb5c7936ab8d270d..2fd01719375da61b3e1693a7ba213095e8895981 100644 --- a/src/mol-theme/color/uniform.ts +++ b/src/mol-theme/color/uniform.ts @@ -37,6 +37,7 @@ export function UniformColorTheme(ctx: ThemeDataContext, props: PD.Values<Unifor export const UniformColorThemeProvider: ColorTheme.Provider<UniformColorThemeParams> = { label: 'Uniform', + category: ColorTheme.Category.Misc, factory: UniformColorTheme, getParams: getUniformColorThemeParams, defaultValues: PD.getDefaultValues(UniformColorThemeParams), diff --git a/src/mol-theme/color/unit-index.ts b/src/mol-theme/color/unit-index.ts index bb6bb9c5bbc40dead1edf85da2ec110fb3011d13..3cc6cdcf26bbd93a71bda07611a6e1201cd149c7 100644 --- a/src/mol-theme/color/unit-index.ts +++ b/src/mol-theme/color/unit-index.ts @@ -16,7 +16,7 @@ import { ColorLists } from '../../mol-util/color/lists'; const DefaultList = 'dark-2' const DefaultColor = Color(0xCCCCCC) -const Description = 'Gives every unit (single chain or collection of single elements) a unique color based on the position (index) of the unit in the list of units in the structure.' +const Description = 'Gives every chain instance (single chain or collection of single elements) a unique color based on the position (index) of the chain in the list of chains in the structure.' export const UnitIndexColorThemeParams = { ...getPaletteParams({ type: 'set', setList: DefaultList }), @@ -72,7 +72,8 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Unit } export const UnitIndexColorThemeProvider: ColorTheme.Provider<UnitIndexColorThemeParams> = { - label: 'Unit Index', + label: 'Chain Instance', + category: ColorTheme.Category.Chain, factory: UnitIndexColorTheme, getParams: getUnitIndexColorThemeParams, defaultValues: PD.getDefaultValues(UnitIndexColorThemeParams), diff --git a/src/mol-theme/size.ts b/src/mol-theme/size.ts index 71c79b8d22d463ca3b3efa0779c41e5b9d77f227..2283474f79175887aa15b56780d5c71b6c01fffa 100644 --- a/src/mol-theme/size.ts +++ b/src/mol-theme/size.ts @@ -32,7 +32,7 @@ namespace SizeTheme { } export interface Provider<P extends PD.Params> extends ThemeProvider<SizeTheme<P>, P> { } - export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true } + export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true } export type Registry = ThemeRegistry<SizeTheme<any>> export function createRegistry() { diff --git a/src/mol-theme/size/physical.ts b/src/mol-theme/size/physical.ts index 4456ad2091c98280488f9c1dd34f1e42784efc6a..8622967d19f8e774b71b9446d580530e5f9ce01d 100644 --- a/src/mol-theme/size/physical.ts +++ b/src/mol-theme/size/physical.ts @@ -58,6 +58,7 @@ export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PD.Values<Physic export const PhysicalSizeThemeProvider: SizeTheme.Provider<PhysicalSizeThemeParams> = { label: 'Physical', + category: '', factory: PhysicalSizeTheme, getParams: getPhysicalSizeThemeParams, defaultValues: PD.getDefaultValues(PhysicalSizeThemeParams), diff --git a/src/mol-theme/size/shape-group.ts b/src/mol-theme/size/shape-group.ts index 2e2f867d55c5910a8ca844a4cb42215c77607412..b7eaa993fe5d1afcf5af100b6c4fb2ce215f2c26 100644 --- a/src/mol-theme/size/shape-group.ts +++ b/src/mol-theme/size/shape-group.ts @@ -36,6 +36,7 @@ export function ShapeGroupSizeTheme(ctx: ThemeDataContext, props: PD.Values<Shap export const ShapeGroupSizeThemeProvider: SizeTheme.Provider<ShapeGroupSizeThemeParams> = { label: 'Shape Group', + category: '', factory: ShapeGroupSizeTheme, getParams: getShapeGroupSizeThemeParams, defaultValues: PD.getDefaultValues(ShapeGroupSizeThemeParams), diff --git a/src/mol-theme/size/uncertainty.ts b/src/mol-theme/size/uncertainty.ts index 5f9da065b4b8331ac620db16e6e4b806446ac176..bbeec077745086a04cdb72d76cd9ef911cd0595f 100644 --- a/src/mol-theme/size/uncertainty.ts +++ b/src/mol-theme/size/uncertainty.ts @@ -54,6 +54,7 @@ export function UncertaintySizeTheme(ctx: ThemeDataContext, props: PD.Values<Unc export const UncertaintySizeThemeProvider: SizeTheme.Provider<UncertaintySizeThemeParams> = { label: 'Uncertainty/Disorder', + category: '', factory: UncertaintySizeTheme, getParams: getUncertaintySizeThemeParams, defaultValues: PD.getDefaultValues(UncertaintySizeThemeParams), diff --git a/src/mol-theme/size/uniform.ts b/src/mol-theme/size/uniform.ts index 4d82bef1048e7b01b74c711df38221f3fd3aafdd..32b2718744f145c5ec3540eefb8a7326f46f7d0a 100644 --- a/src/mol-theme/size/uniform.ts +++ b/src/mol-theme/size/uniform.ts @@ -32,6 +32,7 @@ export function UniformSizeTheme(ctx: ThemeDataContext, props: PD.Values<Uniform export const UniformSizeThemeProvider: SizeTheme.Provider<UniformSizeThemeParams> = { label: 'Uniform', + category: '', factory: UniformSizeTheme, getParams: getUniformSizeThemeParams, defaultValues: PD.getDefaultValues(UniformSizeThemeParams), diff --git a/src/mol-theme/theme.ts b/src/mol-theme/theme.ts index ce5350a5a696a2b2315415576755f1556667dea2..e4d038f175147906882d58b5df7e6c6f2e15f9da 100644 --- a/src/mol-theme/theme.ts +++ b/src/mol-theme/theme.ts @@ -61,6 +61,7 @@ namespace Theme { export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends PD.Params> { readonly label: string + readonly category: string readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => T readonly getParams: (ctx: ThemeDataContext) => P readonly defaultValues: PD.Values<P> @@ -69,7 +70,7 @@ export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends } function getTypes(list: { name: string, provider: ThemeProvider<any, any> }[]) { - return list.map(e => [e.name, e.provider.label] as [string, string]); + return list.map(e => [e.name, e.provider.label, e.provider.category] as [string, string, string]); } export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> { @@ -79,16 +80,26 @@ export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> { get default() { return this._list[0] } get list() { return this._list } - get types(): [string, string][] { return getTypes(this._list) } + get types(): [string, string, string][] { return getTypes(this._list) } constructor(builtInThemes: { [k: string]: ThemeProvider<T, any> }, private emptyProvider: ThemeProvider<T, any>) { Object.keys(builtInThemes).forEach(name => this.add(name, builtInThemes[name])) } + private sort() { + this._list.sort((a, b) => { + if (a.provider.category === b.provider.category) { + return a.provider.label < b.provider.label ? -1 : a.provider.label > b.provider.label ? 1 : 0; + } + return a.provider.category < b.provider.category ? -1 : 1; + }); + } + add<P extends PD.Params>(name: string, provider: ThemeProvider<T, P>) { this._list.push({ name, provider }) this._map.set(name, provider) this._name.set(provider, name) + this.sort(); } remove(name: string) { diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index edcf9300c4635fb1ac00869978d8f9457f58906c..c835570ab397c7db04c08804bed2277b79b6d756 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -65,9 +65,9 @@ export namespace ParamDefinition { export interface Select<T extends string | number> extends Base<T> { type: 'select' /** array of (value, label) tuples */ - options: readonly (readonly [T, string])[] + options: readonly (readonly [T, string] | readonly [T, string, string])[] } - export function Select<T extends string | number>(defaultValue: T, options: readonly (readonly [T, string])[], info?: Info): Select<T> { + export function Select<T extends string | number>(defaultValue: T, options: readonly (readonly [T, string] | readonly [T, string, string])[], info?: Info): Select<T> { return setInfo<Select<T>>({ type: 'select', defaultValue: checkDefaultKey(defaultValue, options), options }, info) } @@ -111,11 +111,11 @@ export namespace ParamDefinition { return setInfo<Color>({ type: 'color', defaultValue }, info) } - export interface Vec3 extends Base<Vec3Data> { + export interface Vec3 extends Base<Vec3Data>, Range { type: 'vec3' } - export function Vec3(defaultValue: Vec3Data, info?: Info): Vec3 { - return setInfo<Vec3>({ type: 'vec3', defaultValue }, info) + export function Vec3(defaultValue: Vec3Data, range?: { min?: number, max?: number, step?: number }, info?: Info): Vec3 { + return setInfo<Vec3>(setRange({ type: 'vec3', defaultValue }, range), info) } export interface FileParam extends Base<File> { @@ -149,7 +149,7 @@ export namespace ParamDefinition { */ step?: number } - function setRange<T extends Numeric | Interval>(p: T, range?: { min?: number, max?: number, step?: number }) { + function setRange<T extends Numeric | Interval | Vec3>(p: T, range?: { min?: number, max?: number, step?: number }) { if (!range) return p; if (typeof range.min !== 'undefined') p.min = range.min; if (typeof range.max !== 'undefined') p.max = range.max; @@ -201,7 +201,7 @@ export namespace ParamDefinition { select: Select<string>, map(name: string): Any } - export function Mapped<T>(defaultKey: string, names: [string, string][], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> { + export function Mapped<T>(defaultKey: string, names: ([string, string] | [string, string, string])[], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> { const name = checkDefaultKey(defaultKey, names); return setInfo<Mapped<NamedParams<T>>>({ type: 'mapped', @@ -406,7 +406,7 @@ export namespace ParamDefinition { return ret; } - function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string])[]) { + function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string] | readonly [T, string, string])[]) { for (const o of options) { if (o[0] === k) return k; } diff --git a/src/mol-util/param-mapping.ts b/src/mol-util/param-mapping.ts new file mode 100644 index 0000000000000000000000000000000000000000..e85731ffa8f14e9ab2f8b69dcf46c57264dd77c7 --- /dev/null +++ b/src/mol-util/param-mapping.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { ParamDefinition as PD } from './param-definition'; +import produce from 'immer' +import { Mutable } from './type-helpers'; + +export interface ParamMapping<S, T, Ctx> { + params(ctx: Ctx): PD.For<S>, + getTarget(ctx: Ctx): T, + getValues(t: T, ctx: Ctx): S, + update(s: S, ctx: Ctx): T, + apply(t: T, ctx: Ctx): void | Promise<void> +} + +export function ParamMapping<S, T, Ctx>(def: { + params: ((ctx: Ctx) => PD.For<S>) | PD.For<S>, + target(ctx: Ctx): T +}): (options: { + values(t: T, ctx: Ctx): S, + update(s: S, t: Mutable<T>, ctx: Ctx): void, + apply?(t: T, ctx: Ctx): void | Promise<void> +}) => ParamMapping<S, T, Ctx> { + return ({ values, update, apply }) => ({ + params: typeof def.params === 'function' ? def.params as any : ctx => def.params, + getTarget: def.target, + getValues: values, + update(s, ctx) { + const t = def.target(ctx); + return produce(t, t1 => update(s, t1 as any, ctx)); + }, + apply: apply ? apply : () => { } + }); +} \ No newline at end of file diff --git a/src/mol-util/set.ts b/src/mol-util/set.ts index af02d56a3cab789b73612da9c24a48d43c4c461f..9e1f71738511e4ef34547c9aae44f375f635859b 100644 --- a/src/mol-util/set.ts +++ b/src/mol-util/set.ts @@ -4,9 +4,15 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ +import { iterableToArray } from '../mol-data/util/array'; + // TODO use set@@iterator when targeting es6 export namespace SetUtils { + export function toArray<T>(set: ReadonlySet<T>) { + return iterableToArray(set.values()) + } + /** Test if set a contains all elements of set b. */ export function isSuperset<T>(setA: ReadonlySet<T>, setB: ReadonlySet<T>) { let flag = true diff --git a/webpack.config.js b/webpack.config.js index f33a385636c5aff1423777bb51873a7d60824ddb..9e9aceeaccfddf66f9691890c05f5a854fabdfb7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -46,7 +46,8 @@ const sharedConfig = { 'node_modules', path.resolve(__dirname, 'lib/') ], - } + }, + devtool: '' }