diff --git a/src/mol-canvas3d/helper/postprocessing.ts b/src/mol-canvas3d/helper/postprocessing.ts index e47c803b8e3e1167bf5f8d6da1370ba3d0aeeeaa..3b476365a228b3ac3c2e0c3d446d1e6546671c37 100644 --- a/src/mol-canvas3d/helper/postprocessing.ts +++ b/src/mol-canvas3d/helper/postprocessing.ts @@ -33,7 +33,7 @@ const PostprocessingSchema = { export const PostprocessingParams = { occlusionEnable: PD.Boolean(false), - occlusionKernelSize: PD.Numeric(4, { min: 1, max: 100, step: 1 }), + occlusionKernelSize: PD.Numeric(4, { min: 1, max: 32, step: 1 }), occlusionBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }), occlusionRadius: PD.Numeric(64, { min: 0, max: 256, step: 1 }), diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts index 335e7b462eab27cad098c3ce20ce381e8cbdea98..ed77c0c6ca5f65cff27ac3771107ecf831f8563f 100644 --- a/src/mol-math/linear-algebra/3d/mat4.ts +++ b/src/mol-math/linear-algebra/3d/mat4.ts @@ -1021,8 +1021,8 @@ namespace Mat4 { } const xAxis = Vec3.create(1, 0, 0) - const yAxis = Vec3.create(1, 0, 0) - const zAxis = Vec3.create(1, 0, 0) + const yAxis = Vec3.create(0, 1, 0) + const zAxis = Vec3.create(0, 0, 1) /** Rotation matrix for 90deg around x-axis */ export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), xAxis) diff --git a/src/mol-model-formats/structure/mmcif/ihm.ts b/src/mol-model-formats/structure/mmcif/ihm.ts index 405fec8cf0fab7b019f477e41ac0dea1d61a9716..e6b86f11b6d6f8982c0495f95f4ef02a7811e560 100644 --- a/src/mol-model-formats/structure/mmcif/ihm.ts +++ b/src/mol-model-formats/structure/mmcif/ihm.ts @@ -19,6 +19,7 @@ import { FormatData } from './parser'; export interface IHMData { model_id: number, model_name: string, + model_group_name: string, entities: Entities, atom_site: mmCIF['atom_site'], atom_site_sourceIndex: Column<number>, diff --git a/src/mol-model-formats/structure/mmcif/parser.ts b/src/mol-model-formats/structure/mmcif/parser.ts index b7e9d1ef07f3af0c82a1d63dcb7e870c88d60fa1..eee752fd2ab941c65d818a9a7548012121ba0b37 100644 --- a/src/mol-model-formats/structure/mmcif/parser.ts +++ b/src/mol-model-formats/structure/mmcif/parser.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -190,6 +190,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIn return { id: UUID.create22(), label, + entry: label, sourceData: format, modelNum: atom_site.pdbx_PDB_model_num.value(0), entities, @@ -212,10 +213,15 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIn function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatData): Model { const atomic = getAtomicHierarchyAndConformation(data.atom_site, data.atom_site_sourceIndex, data.entities, formatData); const coarse = getIHMCoarse(data, formatData); + const entry = format.data.entry.id.valueKind(0) === Column.ValueKind.Present + ? format.data.entry.id.value(0) + : format.data._name; + const label = data.model_group_name ? `${data.model_name}: ${data.model_group_name}` : data.model_name return { id: UUID.create22(), - label: data.model_name, + label, + entry, sourceData: format, modelNum: data.model_id, entities: data.entities, @@ -324,14 +330,13 @@ function splitTable<T extends Table<any>>(table: T, col: Column<number>) { } async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: FormatData) { - if (format.data.atom_site._rowCount && !format.data.atom_site.ihm_model_id.isDefined) { - throw new Error('expected _atom_site.ihm_model_id to be defined') - } + // when `atom_site.ihm_model_id` is undefined fall back to `atom_site.pdbx_PDB_model_num` + const atom_sites_modelColumn = format.data.atom_site.ihm_model_id.isDefined ? format.data.atom_site.ihm_model_id : format.data.atom_site.pdbx_PDB_model_num const { ihm_model_list } = format.data; const entities = getEntities(format) - const atom_sites = splitTable(format.data.atom_site, format.data.atom_site.ihm_model_id); + const atom_sites = splitTable(format.data.atom_site, atom_sites_modelColumn); // TODO: will coarse IHM records require sorting or will we trust it? // ==> Probably implement a sort as as well and store the sourceIndex same as with atomSite // If the sorting is implemented, updated mol-model/structure/properties: atom.sourceIndex @@ -340,14 +345,15 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo const models: Model[] = []; - const { model_id, model_name } = ihm_model_list; + const { model_id, model_name, model_group_name } = ihm_model_list; for (let i = 0; i < ihm_model_list._rowCount; i++) { const id = model_id.value(i); let atom_site, atom_site_sourceIndex; if (atom_sites.has(id)) { const e = atom_sites.get(id)!; - const { atom_site: sorted, sourceIndex } = await sortAtomSite(ctx, e.table, e.start, e.end); + // need to sort `format.data.atom_site` as `e.start` and `e.end` are indices into that + const { atom_site: sorted, sourceIndex } = await sortAtomSite(ctx, format.data.atom_site, e.start, e.end); atom_site = sorted; atom_site_sourceIndex = sourceIndex; } else { @@ -358,6 +364,7 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo const data: IHMData = { model_id: id, model_name: model_name.value(i), + model_group_name: model_group_name.value(i), entities: entities, atom_site, atom_site_sourceIndex, diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts index 303048fdde6740ec47502a9c3baee05a0d927dc7..cad6d07e895cae6c8b3a3ebcfa1e2e0433441ff9 100644 --- a/src/mol-model/structure/model/model.ts +++ b/src/mol-model/structure/model/model.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import UUID from 'mol-util/uuid'; @@ -25,7 +26,10 @@ export interface Model extends Readonly<{ id: UUID, label: string, - // for IHM, corresponds to ihm_model_list.model_id + /** the name of the entry/file/collection the model is part of */ + entry: string, + + /** for IHM, corresponds to ihm_model_list.model_id */ modelNum: number, sourceData: ModelFormat, diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts index 75bec96914943b8ef01d6627bc188d49ad823d37..64cb0bac27a6606abc1fb1b836444bbf2b4c117f 100644 --- a/src/mol-model/structure/model/types.ts +++ b/src/mol-model/structure/model/types.ts @@ -576,4 +576,38 @@ export namespace LinkType { export function isCovalent(flags: LinkType.Flag) { return (flags & LinkType.Flag.Covalent) !== 0; } -} \ No newline at end of file +} + +/** + * "Experimentally determined hydrophobicity scale for proteins at membrane interfaces" + * by Wimely and White (doi:10.1038/nsb1096-842) + * http://blanco.biomol.uci.edu/Whole_residue_HFscales.txt + * https://www.nature.com/articles/nsb1096-842 + */ +export const ResidueHydrophobicity = { + // AA DGwif DGwoct Oct-IF + 'ALA': [ 0.17, 0.50, 0.33 ], + 'ARG': [ 0.81, 1.81, 1.00 ], + 'ASN': [ 0.42, 0.85, 0.43 ], + 'ASP': [ 1.23, 3.64, 2.41 ], + 'ASH': [ -0.07, 0.43, 0.50 ], + 'CYS': [ -0.24, -0.02, 0.22 ], + 'GLN': [ 0.58, 0.77, 0.19 ], + 'GLU': [ 2.02, 3.63, 1.61 ], + 'GLH': [ -0.01, 0.11, 0.12 ], + 'GLY': [ 0.01, 1.15, 1.14 ], + // "His+": [ 0.96, 2.33, 1.37 ], + 'HIS': [ 0.17, 0.11, -0.06 ], + 'ILE': [ -0.31, -1.12, -0.81 ], + 'LEU': [ -0.56, -1.25, -0.69 ], + 'LYS': [ 0.99, 2.80, 1.81 ], + 'MET': [ -0.23, -0.67, -0.44 ], + 'PHE': [ -1.13, -1.71, -0.58 ], + 'PRO': [ 0.45, 0.14, -0.31 ], + 'SER': [ 0.13, 0.46, 0.33 ], + 'THR': [ 0.14, 0.25, 0.11 ], + 'TRP': [ -1.85, -2.09, -0.24 ], + 'TYR': [ -0.94, -0.71, 0.23 ], + 'VAL': [ 0.07, -0.46, -0.53 ] + } + export const DefaultResidueHydrophobicity = [ 0.00, 0.00, 0.00 ] \ No newline at end of file diff --git a/src/mol-model/structure/structure/carbohydrates/compute.ts b/src/mol-model/structure/structure/carbohydrates/compute.ts index 8fc4688a561036e71c7ed334667dd531c8b3996f..342175438faa61d36f8595fa42f7945a66317f66 100644 --- a/src/mol-model/structure/structure/carbohydrates/compute.ts +++ b/src/mol-model/structure/structure/carbohydrates/compute.ts @@ -421,16 +421,22 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[ return `${unit.id}|${residueIndex}` } - const anomericCarbonMap = new Map<string, ElementIndex>() + const anomericCarbonMap = new Map<string, ElementIndex[]>() for (let i = 0, il = elements.length; i < il; ++i) { const { unit, anomericCarbon } = elements[i] const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[anomericCarbon] - anomericCarbonMap.set(anomericCarbonKey(unit, residueIndex), anomericCarbon) + const k = anomericCarbonKey(unit, residueIndex) + if (anomericCarbonMap.has(k)) { + anomericCarbonMap.get(k)!.push(anomericCarbon) + } else { + anomericCarbonMap.set(k, [anomericCarbon]) + } } - function getAnomericCarbon(unit: Unit, residueIndex: ResidueIndex) { - return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex)) + const EmptyArray: ReadonlyArray<any> = [] + function getAnomericCarbons(unit: Unit, residueIndex: ResidueIndex) { + return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex)) || EmptyArray } - return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbon } + return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbons } } \ No newline at end of file diff --git a/src/mol-model/structure/structure/carbohydrates/data.ts b/src/mol-model/structure/structure/carbohydrates/data.ts index 1eaae487ca98d877dd19d5fa592a2a76e5365151..a753378370aae16424a947f0714d07d7a8a32622 100644 --- a/src/mol-model/structure/structure/carbohydrates/data.ts +++ b/src/mol-model/structure/structure/carbohydrates/data.ts @@ -49,7 +49,7 @@ export interface Carbohydrates { getLinkIndices: (unit: Unit, anomericCarbon: ElementIndex) => ReadonlyArray<number> getTerminalLinkIndex: (unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) => number | undefined getTerminalLinkIndices: (unit: Unit, element: ElementIndex) => ReadonlyArray<number> - getAnomericCarbon: (unit: Unit, residueIndex: ResidueIndex) => ElementIndex | undefined + getAnomericCarbons: (unit: Unit, residueIndex: ResidueIndex) => ReadonlyArray<ElementIndex> } const EmptyArray: ReadonlyArray<any> = [] @@ -63,5 +63,5 @@ export const EmptyCarbohydrates: Carbohydrates = { getLinkIndices: () => EmptyArray, getTerminalLinkIndex: () => undefined, getTerminalLinkIndices: () => EmptyArray, - getAnomericCarbon: () => undefined, + getAnomericCarbons: () => [], } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index e7b357d9b7e3987edd704fcdd98559c4eed7bd73..6dbc53ea78ca7a90e2e4dd9afc17c94f0ccb0e0d 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts @@ -99,7 +99,7 @@ export namespace VolumeStreaming { channels: Channels = {} private async queryData(box?: Box3D) { - let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId}`); + let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`); if (box) { const { min: a, max: b } = box; diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts index aa98aeff147ac90f77973a1dab5f16b2bbb7d56a..8dd2be858575fc55f058fc4b57c166d963e22468 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts @@ -115,7 +115,7 @@ const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({ const dataId = params.dataId; const emDefaultContourLevel = params.source.name === 'em' ? params.source.params.isoValue : VolumeIsoValue.relative(1); await taskCtx.update('Getting server header...'); - const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId}`), type: 'json' }).runInContext(taskCtx); + const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId.toLocaleLowerCase()}`), type: 'json' }).runInContext(taskCtx); const data: VolumeServerInfo.Data = { serverUrl: params.serverUrl, dataId, diff --git a/src/mol-plugin/state/actions/data-format.ts b/src/mol-plugin/state/actions/data-format.ts index 3cb8a05da5a2fae2722df766e895f691050b2d53..9a0ffd829f3fb6808703f6ee63f8028c72b6f3a3 100644 --- a/src/mol-plugin/state/actions/data-format.ts +++ b/src/mol-plugin/state/actions/data-format.ts @@ -150,6 +150,8 @@ type cifVariants = 'dscif' | -1 export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants { if (info.ext === 'bcif') { try { + // TODO find a way to run msgpackDecode only once + // now it is run twice, here and during file parsing if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif' } catch { } } else if (info.ext === 'cif') { diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts index e8f8bfb09532de95672039efcd5215046e474232..d6ba75abdf75e930a23e905b800cfb4ca63126c5 100644 --- a/src/mol-plugin/state/actions/structure.ts +++ b/src/mol-plugin/state/actions/structure.ts @@ -91,6 +91,10 @@ const DownloadStructure = StateAction.build({ id: PD.Text('1tqn', { label: 'Id' }), options: DownloadStructurePdbIdSourceOptions }, { isFlat: true }), + 'pdb-dev': PD.Group({ + id: PD.Text('PDBDEV_00000001', { label: 'Id' }), + options: DownloadStructurePdbIdSourceOptions + }, { isFlat: true }), 'bcif-static': PD.Group({ id: PD.Text('1tqn', { label: 'Id' }), options: DownloadStructurePdbIdSourceOptions @@ -107,6 +111,7 @@ const DownloadStructure = StateAction.build({ options: [ ['pdbe-updated', 'PDBe Updated'], ['rcsb', 'RCSB'], + ['pdb-dev', 'PDBDEV'], ['bcif-static', 'BinaryCIF (static PDBe Updated)'], ['url', 'URL'] ] @@ -133,6 +138,18 @@ const DownloadStructure = StateAction.build({ supportProps = !!src.params.options.supportProps; asTrajectory = !!src.params.options.asTrajectory; break; + case 'pdb-dev': + downloadParams = getDownloadParams(src.params.id, + id => { + const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}` + return `https://pdb-dev.wwpdb.org/static/cif/${nId.toUpperCase()}.cif` + }, + id => id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`, + false + ); + supportProps = !!src.params.options.supportProps; + asTrajectory = !!src.params.options.asTrajectory; + break; case 'bcif-static': downloadParams = getDownloadParams(src.params.id, id => `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${id.toLowerCase()}`, id => `BinaryCIF: ${id}`, true); supportProps = !!src.params.options.supportProps; @@ -155,7 +172,7 @@ const DownloadStructure = StateAction.build({ }); function getDownloadParams(src: string, url: (id: string) => string, label: (id: string) => string, isBinary: boolean): StateTransformer.Params<Download>[] { - const ids = src.split(',').map(id => id.trim()).filter(id => !!id && id.length >= 4); + const ids = src.split(',').map(id => id.trim()).filter(id => !!id && (id.length >= 4 || /^[1-9][0-9]*$/.test(id))); const ret: StateTransformer.Params<Download>[] = []; for (const id of ids) { ret.push({ url: url(id), isBinary, label: label(id) }) @@ -228,7 +245,7 @@ export function complexRepresentation( if (!params || !params.hideCoarse) { root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill', {}, 'polymer-id')); } } diff --git a/src/mol-plugin/state/transforms/helpers.ts b/src/mol-plugin/state/transforms/helpers.ts index 730f140e5e979cf3c4aff7c65504379c46ec73f9..2a72f7a52fdb7e631e41519fa8fca721dc389c09 100644 --- a/src/mol-plugin/state/transforms/helpers.ts +++ b/src/mol-plugin/state/transforms/helpers.ts @@ -42,7 +42,7 @@ export function getStructureTransparency(structure: Structure, script: Script, v * Attaches ComputedSecondaryStructure property when unavailable in sourceData */ export async function ensureSecondaryStructure(s: Structure) { - if (s.model.sourceData.kind === 'mmCIF') { + if (s.model && s.model.sourceData.kind === 'mmCIF') { if (!s.model.sourceData.data.struct_conf.id.isDefined && !s.model.sourceData.data.struct_sheet_range.id.isDefined) { await ComputedSecondaryStructure.attachFromCifOrCompute(s) } diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index a8fb3fe99d0ff52d1bf0e7579a59b5acefae08f9..a5edcfbfc80ed6a4ba604f956bf741b92e3f0246 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -153,10 +153,9 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({ apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); const model = a.data[params.modelIndex]; - const props = a.data.length === 1 - ? { label: `${model.label}` } - : { label: `${model.label}:${model.modelNum}`, description: `Model ${params.modelIndex + 1} of ${a.data.length}` }; - return new SO.Molecule.Model(model, props); + const label = a.data.length === 1 ? model.entry : `${model.entry}: ${model.modelNum}` + const description = a.data.length === 1 ? undefined : `Model ${params.modelIndex + 1} of ${a.data.length}` + return new SO.Molecule.Model(model, { label, description }); } }); diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index f0873a69e5a3589938802a013ea1faaf89d58cba..127a50758decff275db4f14da3d144103e3d0016 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -108,13 +108,14 @@ namespace StructureRepresentation3DHelpers { }) } - export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> { + export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>, colorName?: BuiltInColorThemeName): StateTransformer.Params<StructureRepresentation3D> { const type = ctx.structureRepresentation.registry.get(name); - const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues; + const color = colorName || type.defaultColorTheme; + const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(color).defaultValues; const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues return ({ type: { name, params: structureParams ? { ...type.defaultValues, ...structureParams } : type.defaultValues }, - colorTheme: { name: type.defaultColorTheme, params: colorParams }, + colorTheme: { name: color, params: colorParams }, sizeTheme: { name: type.defaultSizeTheme, params: sizeParams } }) } diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts index c1c2b046f1d908e78c76dd2a27e102733a026ecd..10a0042d8b2c39b363ee647e52d1b8d767c141d4 100644 --- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts +++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts @@ -25,7 +25,7 @@ import { OrderedSet, Interval } from 'mol-data/int'; import { EmptyLoci, Loci } from 'mol-model/loci'; import { VisualContext } from 'mol-repr/visual'; import { Theme } from 'mol-theme/theme'; -import { getResidueLoci } from './util/common'; +import { getAltResidueLoci } from './util/common'; const t = Mat4.identity() const sVec = Vec3.zero() @@ -82,7 +82,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, Mat4.scaleUniformly(t, t, side) MeshBuilder.addPrimitive(builderState, t, perforatedBox) Mat4.mul(t, t, Mat4.rotZ90X180) - builderState.currentGroup = i * 2 + 1 + builderState.currentGroup += 1 MeshBuilder.addPrimitive(builderState, t, perforatedBox) break; case SaccharideShapes.FilledCone: @@ -93,7 +93,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, Mat4.scaleUniformly(t, t, side * 1.2) MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid) Mat4.mul(t, t, Mat4.rotZ90) - builderState.currentGroup = i * 2 + 1 + builderState.currentGroup += 1 MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid) break case SaccharideShapes.FlatBox: @@ -116,7 +116,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4)) MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron) Mat4.mul(t, t, Mat4.rotY90) - builderState.currentGroup = i * 2 + 1 + builderState.currentGroup += 1 MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron) break case SaccharideShapes.FlatDiamond: @@ -186,28 +186,28 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num const { objectId, groupId } = pickingId if (id === objectId) { const carb = structure.carbohydrates.elements[Math.floor(groupId / 2)] - return getResidueLoci(structure, carb.unit, carb.anomericCarbon) + return getAltResidueLoci(structure, carb.unit, carb.anomericCarbon) } return EmptyLoci } /** For each carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */ function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) { - const { getElementIndex, getAnomericCarbon } = structure.carbohydrates + const { getElementIndex, getAnomericCarbons } = structure.carbohydrates let changed = false if (!StructureElement.isLoci(loci)) return false if (!Structure.areEquivalent(loci.structure, structure)) return false for (const e of loci.elements) { + // TODO make more efficient by handling/grouping `e.indices` by residue index + // TODO only call apply when the full alt-residue of the unit is part of `e` OrderedSet.forEach(e.indices, v => { const { model, elements } = e.unit - const { index, offsets } = model.atomicHierarchy.residueAtomSegments + const { index } = model.atomicHierarchy.residueAtomSegments const rI = index[elements[v]] - const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI]) - const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1) - const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax) - if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return - const eI = getAnomericCarbon(e.unit, rI) - if (eI !== undefined) { + const eIndices = getAnomericCarbons(e.unit, rI) + for (let i = 0, il = eIndices.length; i < il; ++i) { + const eI = eIndices[i] + if (!OrderedSet.has(e.indices, OrderedSet.indexOf(elements, eI))) continue const idx = getElementIndex(e.unit, eI) if (idx !== undefined) { if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true diff --git a/src/mol-repr/structure/visual/util/common.ts b/src/mol-repr/structure/visual/util/common.ts index f2f8faa6da167c06383549f84056fc1af8ee163a..7aaa308dafc2f6dc70d7c765279d3e684caedb77 100644 --- a/src/mol-repr/structure/visual/util/common.ts +++ b/src/mol-repr/structure/visual/util/common.ts @@ -28,6 +28,33 @@ export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementI return EmptyLoci } +/** + * Return a Loci for the elements of a whole residue the elementIndex belongs to but + * restrict to elements that have the same label_alt_id or none + */ +export function getAltResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci { + const { elements, model } = unit + const { label_alt_id } = model.atomicHierarchy.atoms + const elementAltId = label_alt_id.value(elementIndex) + if (OrderedSet.indexOf(elements, elementIndex) !== -1) { + const { index, offsets } = model.atomicHierarchy.residueAtomSegments + const rI = index[elementIndex] + const _indices: number[] = [] + for (let i = offsets[rI], il = offsets[rI + 1]; i < il; ++i) { + const unitIndex = OrderedSet.indexOf(elements, i) + if (unitIndex !== -1) { + const altId = label_alt_id.value(i) + if (elementAltId === altId || altId === '') { + _indices.push(unitIndex) + } + } + } + const indices = OrderedSet.ofSortedArray<StructureElement.UnitIndex>(SortedArray.ofSortedArray(_indices)) + return StructureElement.Loci(structure, [{ unit, indices }]) + } + return EmptyLoci +} + // export function createUnitsTransform({ units }: Unit.SymmetryGroup, transformData?: TransformData) { diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts index 4ddbdb8b1945619c723c187001858a952a489ad8..ca269c8b679b911795fe653ba7706b5b493655a9 100644 --- a/src/mol-script/runtime/query/table.ts +++ b/src/mol-script/runtime/query/table.ts @@ -187,7 +187,25 @@ const symbols = [ // C(MolScript.structureQuery.slot.elementSetReduce, (ctx, _) => ctx.element), // ============= FILTERS ================ + D(MolScript.structureQuery.filter.pick, (ctx, xs) => Queries.filters.pick(xs[0] as any, xs['test'])(ctx)), D(MolScript.structureQuery.filter.first, (ctx, xs) => Queries.filters.first(xs[0] as any)(ctx)), + D(MolScript.structureQuery.filter.withSameAtomProperties, (ctx, xs) => Queries.filters.withSameAtomProperties(xs[0] as any, xs['source'] as any, xs['property'] as any)(ctx)), + D(MolScript.structureQuery.filter.intersectedBy, (ctx, xs) => Queries.filters.areIntersectedBy(xs[0] as any, xs['by'] as any)(ctx)), + D(MolScript.structureQuery.filter.within, (ctx, xs) => Queries.filters.within({ + query: xs[0] as any, + target: xs['target'] as any, + minRadius: xs['min-radius'] as any, + maxRadius: xs['max-radius'] as any, + elementRadius: xs['atom-radius'] as any, + invert: xs['invert'] as any + })(ctx)), + D(MolScript.structureQuery.filter.isConnectedTo, (ctx, xs) => Queries.filters.isConnectedTo({ + query: xs[0] as any, + target: xs['target'] as any, + disjunct: xs['disjunct'] as any, + invert: xs['invert'] as any, + bondTest: xs['bond-test'] + })(ctx)), // ============= GENERATORS ================ D(MolScript.structureQuery.generator.atomGroups, (ctx, xs) => Queries.generators.atoms({ diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts index 5d49660e57af6b6b08dd31a2b7253b7f31e7edb7..b82996be6a92aa90ee0bd5101e55c1ca462fb3a2 100644 --- a/src/mol-theme/color.ts +++ b/src/mol-theme/color.ts @@ -28,6 +28,8 @@ import { ScaleLegend } from 'mol-util/color/scale'; import { TableLegend } from 'mol-util/color/tables'; import { UncertaintyColorThemeProvider } from './color/uncertainty'; import { GeneColorThemeProvider } from './color/gene'; +import { IllustrativeColorThemeProvider } from './color/illustrative'; +import { HydrophobicityColorThemeProvider } from './color/hydrophobicity'; export type LocationColor = (location: Location, isSecondary: boolean) => Color @@ -75,6 +77,8 @@ export const BuiltInColorThemes = { 'element-index': ElementIndexColorThemeProvider, 'element-symbol': ElementSymbolColorThemeProvider, 'gene': GeneColorThemeProvider, + 'hydrophobicity': HydrophobicityColorThemeProvider, + 'illustrative': IllustrativeColorThemeProvider, 'molecule-type': MoleculeTypeColorThemeProvider, 'polymer-id': PolymerIdColorThemeProvider, 'polymer-index': PolymerIndexColorThemeProvider, diff --git a/src/mol-theme/color/carbohydrate-symbol.ts b/src/mol-theme/color/carbohydrate-symbol.ts index a92d183e0f4fa61d5aa0837c14793b8c2455e32a..f76bd90ed1ee75561453df525509489effc11fb7 100644 --- a/src/mol-theme/color/carbohydrate-symbol.ts +++ b/src/mol-theme/color/carbohydrate-symbol.ts @@ -27,13 +27,13 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va let color: LocationColor if (ctx.structure) { - const { elements, getElementIndex, getAnomericCarbon } = ctx.structure.carbohydrates + const { elements, getElementIndex, getAnomericCarbons } = ctx.structure.carbohydrates const getColor = (unit: Unit, index: ElementIndex) => { const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[index] - const anomericCarbon = getAnomericCarbon(unit, residueIndex) - if (anomericCarbon !== undefined) { - const idx = getElementIndex(unit, anomericCarbon) + const anomericCarbons = getAnomericCarbons(unit, residueIndex) + if (anomericCarbons.length > 0) { + const idx = getElementIndex(unit, anomericCarbons[0]) if (idx !== undefined) return elements[idx].component.color } return DefaultColor diff --git a/src/mol-theme/color/hydrophobicity.ts b/src/mol-theme/color/hydrophobicity.ts new file mode 100644 index 0000000000000000000000000000000000000000..60be994f3888c64b8518d901bb95901de6cc353c --- /dev/null +++ b/src/mol-theme/color/hydrophobicity.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Color, ColorScale } from 'mol-util/color'; +import { StructureElement, Unit, Link, ElementIndex } from 'mol-model/structure'; +import { Location } from 'mol-model/location'; +import { ColorTheme } from '../color'; +import { ParamDefinition as PD } from 'mol-util/param-definition' +import { ThemeDataContext } from '../theme'; +import { ResidueHydrophobicity } from 'mol-model/structure/model/types'; +import { ColorListName, ColorListOptions } from 'mol-util/color/scale'; + +const Description = 'Assigns a color to every amino acid according to the "Experimentally determined hydrophobicity scale for proteins at membrane interfaces" by Wimely and White (doi:10.1038/nsb1096-842).' + +export const HydrophobicityColorThemeParams = { + list: PD.ColorScale<ColorListName>('RedYellowGreen', ColorListOptions), + scale: PD.Select('DGwif', [['DGwif', 'DG water-membrane'], ['DGwoct', 'DG water-octanol'], ['Oct-IF', 'DG difference']]) +} +export type HydrophobicityColorThemeParams = typeof HydrophobicityColorThemeParams +export function getHydrophobicityColorThemeParams(ctx: ThemeDataContext) { + return HydrophobicityColorThemeParams // TODO return copy +} + +const scaleIndexMap = { 'DGwif': 0, 'DGwoct': 1, 'Oct-IF': 2 } + +export function hydrophobicity(compId: string, scaleIndex: number): number { + const c = (ResidueHydrophobicity as { [k: string]: number[] })[compId]; + return c === undefined ? 0 : c[scaleIndex] +} + +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 +} + +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] + let compId = seq.compId.value(seqIdBegin - 1) // 1-indexed + const parentId = modifiedResidues.parentId.get(compId) + return parentId === undefined ? compId : parentId + } +} + +export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values<HydrophobicityColorThemeParams>): ColorTheme<HydrophobicityColorThemeParams> { + const scaleIndex = scaleIndexMap[props.scale] + + // get domain + let min = Infinity + let max = -Infinity + for (const name in ResidueHydrophobicity) { + const val = (ResidueHydrophobicity as { [k: string]: number[] })[name][scaleIndex] + min = Math.min(min, val) + max = Math.max(max, val) + } + + const scale = ColorScale.create({ + listOrName: props.list, + domain: [ max, min ], + minLabel: 'Hydrophobic', + maxLabel: 'Hydrophilic' + }) + + function color(location: Location): Color { + let compId: string | undefined + if (StructureElement.isLocation(location)) { + if (Unit.isAtomic(location.unit)) { + compId = getAtomicCompId(location.unit, location.element) + } else { + compId = getCoarseCompId(location.unit, location.element) + } + } else if (Link.isLocation(location)) { + if (Unit.isAtomic(location.aUnit)) { + compId = getAtomicCompId(location.aUnit, location.aUnit.elements[location.aIndex]) + } else { + compId = getCoarseCompId(location.aUnit, location.aUnit.elements[location.aIndex]) + } + } + return scale.color(compId ? hydrophobicity(compId, scaleIndex) : 0) + } + + return { + factory: HydrophobicityColorTheme, + granularity: 'group', + color, + props, + description: Description, + legend: scale ? scale.legend : undefined + } +} + +export const HydrophobicityColorThemeProvider: ColorTheme.Provider<HydrophobicityColorThemeParams> = { + label: 'Hydrophobicity', + factory: HydrophobicityColorTheme, + getParams: getHydrophobicityColorThemeParams, + defaultValues: PD.getDefaultValues(HydrophobicityColorThemeParams), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure +} \ No newline at end of file diff --git a/src/mol-theme/color/illustrative.ts b/src/mol-theme/color/illustrative.ts new file mode 100644 index 0000000000000000000000000000000000000000..b25da5db4b549023d8b638a1239d5b92e749bdea --- /dev/null +++ b/src/mol-theme/color/illustrative.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ElementSymbol, isNucleic, isProtein, MoleculeType } from 'mol-model/structure/model/types'; +import { Color } from 'mol-util/color'; +import { StructureElement, Unit, Link } from 'mol-model/structure'; +import { Location } from 'mol-model/location'; +import { ColorTheme } from '../color'; +import { ParamDefinition as PD } from 'mol-util/param-definition' +import { ThemeDataContext } from '../theme'; +import { elementSymbolColor } from './element-symbol'; + +const DefaultIllustrativeColor = Color(0xFFFFFF) +const Description = `Assigns an illustrative color similar to David Goodsell's Molecule of the Month style.` + +export const IllustrativeColorThemeParams = {} +export type IllustrativeColorThemeParams = typeof IllustrativeColorThemeParams +export function getIllustrativeColorThemeParams(ctx: ThemeDataContext) { + return IllustrativeColorThemeParams // TODO return copy +} + +function illustrativeColor(typeSymbol: ElementSymbol, moleculeType: MoleculeType) { + if (isNucleic(moleculeType)) { + if (typeSymbol === 'O') { + return Color(0xFF8C8C) + } else if (typeSymbol === 'P') { + return Color(0xFF7D7D) + } else { + return Color(0xFFA6A6) + } + } else if (isProtein(moleculeType)) { + if (typeSymbol === 'C') { + return Color(0x7FB2FF) + } else { + return Color(0x6699FF) + } + } else { + return elementSymbolColor(typeSymbol) + } +} + +export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<IllustrativeColorThemeParams>): ColorTheme<IllustrativeColorThemeParams> { + function color(location: Location): Color { + if (StructureElement.isLocation(location)) { + if (Unit.isAtomic(location.unit)) { + const moleculeType = location.unit.model.atomicHierarchy.derived.residue.moleculeType[location.unit.residueIndex[location.element]] + const typeSymbol = location.unit.model.atomicHierarchy.atoms.type_symbol.value(location.element) + return illustrativeColor(typeSymbol, moleculeType) + } + } else if (Link.isLocation(location)) { + if (Unit.isAtomic(location.aUnit)) { + const elementIndex = location.aUnit.elements[location.aIndex] + const moleculeType = location.aUnit.model.atomicHierarchy.derived.residue.moleculeType[location.aUnit.residueIndex[elementIndex]] + const typeSymbol = location.aUnit.model.atomicHierarchy.atoms.type_symbol.value(elementIndex) + return illustrativeColor(typeSymbol, moleculeType) + } + } + return DefaultIllustrativeColor + } + + return { + factory: IllustrativeColorTheme, + granularity: 'group', + color, + props, + description: Description, + // TODO add legend + } +} + +export const IllustrativeColorThemeProvider: ColorTheme.Provider<IllustrativeColorThemeParams> = { + label: 'Illustrative', + factory: IllustrativeColorTheme, + getParams: getIllustrativeColorThemeParams, + defaultValues: PD.getDefaultValues(IllustrativeColorThemeParams), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure +} \ No newline at end of file diff --git a/src/mol-util/color/scale.ts b/src/mol-util/color/scale.ts index 39f1b716c2511002c81578c45ac8fdb7742c3281..73de47668c1a9093493c9d5386cc418f8a1caa06 100644 --- a/src/mol-util/color/scale.ts +++ b/src/mol-util/color/scale.ts @@ -55,7 +55,7 @@ export interface ColorScale { } export const DefaultColorScaleProps = { - domain: [0, 1], + domain: [0, 1] as [number, number], reverse: false, listOrName: ColorBrewer.RedYellowBlue as Color[] | ColorListName, minLabel: '' as string | undefined, diff --git a/src/mol-util/string.ts b/src/mol-util/string.ts index 62ebf10cf0c16383d93237f2e008d1d1fb48bf5a..dfc8f03bb1e7925389e341edc9b44521bfa6484d 100644 --- a/src/mol-util/string.ts +++ b/src/mol-util/string.ts @@ -52,4 +52,25 @@ export function interpolate(str: string, params: { [k: string]: any }) { const names = Object.keys(params); const values = Object.values(params); return new Function(...names, `return \`${str}\`;`)(...values); +} + +export function trimChar(str: string, char: string) { + let start = 0; + let end = str.length; + while (start < end && str[start] === char) ++start; + while (end > start && str[end - 1] === char) --end; + return (start > 0 || end < str.length) ? str.substring(start, end) : str; +} + +export function trimCharStart(str: string, char: string) { + let start = 0; + const end = str.length; + while (start < end && str[start] === char) ++start; + return (start > 0) ? str.substring(start, end) : str; +} + +export function trimCharEnd(str: string, char: string) { + let end = str.length; + while (end > 0 && str[end - 1] === char) --end; + return (end < str.length) ? str.substring(0, end) : str; } \ No newline at end of file