diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts index 01b89bc62ce9a2f824304a0ef169c19ee86129b0..913465a6fc681385021d318ae1ed16edda112fc8 100644 --- a/src/examples/proteopedia-wrapper/helpers.ts +++ b/src/examples/proteopedia-wrapper/helpers.ts @@ -4,13 +4,83 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { ResidueIndex } from 'mol-model/structure'; +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 { PluginContext } from 'mol-plugin/context'; -export interface StructureInfo { - ligands: { name: string, indices: ResidueIndex[] }[], - assemblies: { id: string, description: string, isPreferred: boolean }[] +export interface ModelInfo { + hetResidues: { name: string, indices: ResidueIndex[] }[], + assemblies: { id: string, details: string, isPreferred: boolean }[], + preferredAssemblyId: string | undefined +} + +export namespace ModelInfo { + async function getPreferredAssembly(ctx: PluginContext, model: Model) { + if (model.label.length <= 3) return void 0; + try { + const id = model.label.toLowerCase(); + const src = await ctx.runTask(ctx.fetch(`https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/${id}`)) as string; + const json = JSON.parse(src); + const data = json && json[id]; + + console.log(data); + + const assemblies = data[0] && data[0].assemblies; + if (!assemblies || !assemblies.length) return void 0; + + for (const asm of assemblies) { + if (asm.preferred) { + return asm.assembly_id; + } + } + return void 0; + } catch (e) { + console.warn('getPreferredAssembly', e); + } + } + + export async function get(ctx: PluginContext, model: Model, checkPreferred: boolean): Promise<ModelInfo> { + const { _rowCount: residueCount } = model.atomicHierarchy.residues; + const { offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments; + const chainIndex = model.atomicHierarchy.chainAtomSegments.index; + // const resn = SP.residue.label_comp_id, entType = SP.entity.type; + + const pref = checkPreferred + ? getPreferredAssembly(ctx, model) + : void 0; + + const hetResidues: ModelInfo['hetResidues'] = []; + 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; + + const cI = chainIndex[residueOffsets[rI]]; + const eI = model.atomicHierarchy.index.getEntityFromChain(cI); + if (model.entities.data.type.value(eI) === 'water') continue; + + let lig = hetMap.get(comp_id); + if (!lig) { + lig = { name: comp_id, indices: [] }; + hetResidues.push(lig); + hetMap.set(comp_id, lig); + } + lig.indices.push(rI); + } + + const preferredAssemblyId = await pref; + + return { + hetResidues: hetResidues, + assemblies: model.symmetry.assemblies.map(a => ({ id: a.id, details: a.details, isPreferred: a.id === preferredAssemblyId })), + preferredAssemblyId + }; + } } export type SupportedFormats = 'cif' | 'pdb' diff --git a/src/examples/proteopedia-wrapper/index.html b/src/examples/proteopedia-wrapper/index.html index 8e2219d8eba2f84474ef99187c02d084d68a321f..59b5a762d6d7dc89cb42d34f862df3c5bc70f86f 100644 --- a/src/examples/proteopedia-wrapper/index.html +++ b/src/examples/proteopedia-wrapper/index.html @@ -61,7 +61,7 @@ function $(id) { return document.getElementById(id); } - var pdbId = '1grm', assemblyId= '1'; + var pdbId = '3usb', assemblyId= 'preferred'; var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif'; var format = 'cif'; @@ -81,6 +81,10 @@ MolStarProteopediaWrapper.load({ url: url, format: format, assemblyId: assemblyId }); MolStarProteopediaWrapper.toggleSpin(); + MolStarProteopediaWrapper.events.modelInfo.subscribe(function (info) { + console.log('Model Info', info); + }); + addControl('Load Asym Unit', () => MolStarProteopediaWrapper.load({ url: url, format: format })); addControl('Load Assembly', () => MolStarProteopediaWrapper.load({ url: url, format: format, assemblyId: assemblyId })); diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts index 5370a843a1b1717048a5b5db903b99b071cd75e4..3868ddf08a495c14e553e41fe969087b7f7ff298 100644 --- a/src/examples/proteopedia-wrapper/index.ts +++ b/src/examples/proteopedia-wrapper/index.ts @@ -13,14 +13,21 @@ import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/re import { Color } from 'mol-util/color'; import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/objects'; import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; -import { StateBuilder } from 'mol-state'; +import { StateBuilder, StateObject } from 'mol-state'; import { StripedResidues } from './annotation'; -import { LoadParams, SupportedFormats, RepresentationStyle } from './helpers'; +import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo } from './helpers'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; require('mol-plugin/skin/light.scss') class MolStarProteopediaWrapper { static VERSION_MAJOR = 1; + private _ev = RxEventHelper.create(); + + readonly events = { + modelInfo: this._ev<ModelInfo>() + }; + plugin: PluginContext; init(target: string | HTMLElement) { @@ -45,24 +52,28 @@ class MolStarProteopediaWrapper { return b.apply(StateTransforms.Data.Download, { url, isBinary: false }) } - private parse(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { + private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { const parsed = format === 'cif' ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) : b.apply(StateTransforms.Model.TrajectoryFromPDB); return parsed - .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' }); + } + + private structure(assemblyId: string) { + const model = this.state.build().to('model'); + + return model .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } }) .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); } private visual(ref: string, style?: RepresentationStyle) { - const state = this.state; - const cell = state.select(ref)[0]; - if (!cell || !cell.obj) return void 0; - const structure = (cell.obj as PluginStateObject.Molecule.Structure).data; + const structure = this.getObj<PluginStateObject.Molecule.Structure>(ref); + if (!structure) return; - const root = state.build().to(ref); + const root = this.state.build().to(ref); root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' }) .apply(StateTransforms.Representation.StructureRepresentation3D, @@ -86,6 +97,26 @@ class MolStarProteopediaWrapper { return root; } + private getObj<T extends StateObject>(ref: string) { + const state = this.state; + const cell = state.select(ref)[0]; + if (!cell || !cell.obj) return void 0; + return (cell.obj as T).data; + } + + private async doInfo(checkPreferredAssembly: boolean) { + const s = this.getObj<PluginStateObject.Molecule.Model>('model'); + if (!s) return; + + const info = await ModelInfo.get(this.plugin, s, checkPreferredAssembly) + this.events.modelInfo.next(info); + return info; + } + + private applyState(tree: StateBuilder) { + return PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); + } + private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' }; async load({ url, format = 'cif', assemblyId = '', representationStyle }: LoadParams) { let loadType: 'full' | 'update' = 'full'; @@ -98,17 +129,19 @@ class MolStarProteopediaWrapper { if (state.select('asm').length > 0) loadType = 'update'; } - let tree: StateBuilder.Root; if (loadType === 'full') { await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref }); - tree = state.build(); - this.parse(this.download(tree.toRoot(), url), format, assemblyId); + const modelTree = this.model(this.download(state.build().toRoot(), url), format, assemblyId); + await this.applyState(modelTree); + const info = await this.doInfo(true); + const structureTree = this.structure((assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId); + await this.applyState(structureTree); } else { - tree = state.build(); + const tree = state.build(); tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + await this.applyState(tree); } - await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); await this.updateStyle(representationStyle); this.loadedParams = { url, format, assemblyId };