From f740ba95b06ae28a444e6930ffe6e4078a1303a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mal=C3=BD?= <michal.maly@ibt.cas.cz> Date: Mon, 21 Mar 2022 19:21:56 +0100 Subject: [PATCH] ReDNATCO plugin stage 5 --- src/apps/rednatco/commands.ts | 25 ++++ src/apps/rednatco/index.html | 2 +- src/apps/rednatco/index.tsx | 275 +++++++++++++++++++++------------- src/apps/rednatco/step.ts | 109 ++++++++++++++ src/apps/rednatco/traverse.ts | 92 ++++++++++++ 5 files changed, 394 insertions(+), 109 deletions(-) create mode 100644 src/apps/rednatco/commands.ts create mode 100644 src/apps/rednatco/traverse.ts diff --git a/src/apps/rednatco/commands.ts b/src/apps/rednatco/commands.ts new file mode 100644 index 000000000..869955365 --- /dev/null +++ b/src/apps/rednatco/commands.ts @@ -0,0 +1,25 @@ +export namespace Commands { + export type Type = 'select-step'|'switch-model'; + + export type SelectStep = { + type: 'select-step'; + stepName: string; + referenceNtC: string; + references: ('sel'|'prev'|'next')[]; + } + export function SelectStep(stepName: string, referenceNtC = '', references = ['sel', 'prev', 'next']): SelectStep { + return { + type: 'select-step', + stepName, + referenceNtC, + references: references as ('sel'|'prev'|'next')[], + }; + } + + export type SwitchModel = { type: 'switch-model', model: number }; + export function SwitchModel(model: number): SwitchModel { return { type: 'switch-model', model }; } + + export type Cmd = SelectStep|SwitchModel; +} + + diff --git a/src/apps/rednatco/index.html b/src/apps/rednatco/index.html index a2633701a..889b15a28 100644 --- a/src/apps/rednatco/index.html +++ b/src/apps/rednatco/index.html @@ -9,7 +9,7 @@ <script type="text/javascript" src="./molstar.js"></script> <script> async function loadStructure() { - const resp = await fetch('./3vok_v32C35A23.cif'); + const resp = await fetch('./1bna_v41C35A23.cif'); const data = await resp.text(); molstar.ReDNATCOMspApi.loadStructure(data); diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx index d8f6b2c0f..172952e91 100644 --- a/src/apps/rednatco/index.tsx +++ b/src/apps/rednatco/index.tsx @@ -2,18 +2,20 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { NtCColors } from './colors'; import { ColorPicker } from './color-picker'; +import { Commands } from './commands'; import { PushButton, ToggleButton } from './controls'; import * as IDs from './idents'; import * as RefCfmr from './reference-conformers'; import { ReferenceConformersPdbs } from './reference-conformers-pdbs'; import { Step } from './step'; import { Superpose } from './superpose'; +import { Traverse } from './traverse'; import { DnatcoConfalPyramids } from '../../extensions/dnatco'; import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation'; import { OrderedSet } from '../../mol-data/int/ordered-set'; import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper'; import { Loci } from '../../mol-model/loci'; -import { Model, Structure, StructureElement, StructureProperties, Trajectory } from '../../mol-model/structure'; +import { Model, Structure, StructureElement, StructureProperties, StructureSelection, Trajectory } from '../../mol-model/structure'; import { Location } from '../../mol-model/structure/structure/element/location'; import { MmcifFormat } from '../../mol-model-formats/structure/mmcif'; import { PluginBehavior, PluginBehaviors } from '../../mol-plugin/behavior'; @@ -30,7 +32,6 @@ import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec'; import { Representation } from '../../mol-repr/representation'; import { StateObjectCell, StateObject, StateSelection } from '../../mol-state'; import { StateTreeSpine } from '../../mol-state/tree/spine'; -import { Script } from '../../mol-script/script'; import { lociLabel } from '../../mol-theme/label'; import { Color } from '../../mol-util/color'; import { arrayMax } from '../../mol-util/array'; @@ -54,7 +55,6 @@ const NtCSupPrev = 'ntc-sup-prev'; const NtCSupSel = 'ntc-sup-sel'; const NtCSupNext = 'ntc-sup-next'; -const SelectAllScript = Script('(sel.atom.atoms true)', 'mol-script'); const SphereBoundaryHelper = new BoundaryHelper('98'); type StepInfo = { @@ -171,8 +171,9 @@ const ConformersByClass = { }; type ConformersByClass = typeof ConformersByClass; +type VisualRepresentations = 'ball-and-stick'|'cartoon'; const Display = { - representation: 'cartoon', + representation: 'cartoon' as VisualRepresentations, showNucleic: true, showProtein: false, @@ -350,6 +351,13 @@ class ReDNATCOMspViewer { interactionContext.self = this; } + private currentModelNumber() { + const model = this.plugin.state.data.cells.get(IDs.ID('model', '', BaseRef))?.obj; + if (!model) + return -1; + return (model as StateObject<Model>).data.modelNum; + } + private getBuilder(id: IDs.ID, sub: IDs.Substructure|'' = '', ref = BaseRef) { return this.plugin.state.data.build().to(IDs.ID(id, sub, ref)); } @@ -401,7 +409,8 @@ class ReDNATCOMspViewer { continue; const parent = this.getStructureParent(cell); if (parent) { - const s = Loci.getBoundingSphere(Script.toLoci(SelectAllScript, parent.data)); + const loci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(parent.data, parent.data)); + const s = Loci.getBoundingSphere(loci); if (s) spheres.push(s); } @@ -424,6 +433,27 @@ class ReDNATCOMspViewer { PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: AnimationDurationMsec }); } + private substructureVisuals(representation: 'ball-and-stick'|'cartoon') { + switch (representation) { + case 'cartoon': + return { + type: { + name: 'cartoon', + params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false }, + }, + colorTheme: { name: 'chain-id', params: { asymId: 'auth' } }, + }; + case 'ball-and-stick': + return { + type: { + name: 'ball-and-stick', + params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false }, + }, + colorTheme: { name: 'element-symbol', params: { carbonColor: 'chain-id' } }, + }; + } + } + private superpose(reference: StructureElement.Loci, stru: StructureElement.Loci) { const refElems = dinucleotideBackbone(reference); const struElems = dinucleotideBackbone(stru); @@ -525,9 +555,8 @@ class ReDNATCOMspViewer { ); await b.commit(); } - } else { + } else await PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: IDs.ID('pyramids', 'nucleic', BaseRef) }); - } } async changeRepresentation(display: Partial<Display>) { @@ -541,7 +570,7 @@ class ReDNATCOMspViewer { StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, - type: { ...old.type, name: repr } + ...this.substructureVisuals(repr), }) ); } @@ -689,9 +718,7 @@ class ReDNATCOMspViewer { bb.to(IDs.ID('structure', 'nucleic', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - { - type: { name: display.representation ?? 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } }, - }, + this.substructureVisuals('cartoon'), { ref: IDs.ID('visual', 'nucleic', BaseRef) } ); if (display.showPyramids) { @@ -707,9 +734,7 @@ class ReDNATCOMspViewer { bb.to(IDs.ID('structure', 'protein', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - { - type: { name: display.representation ?? 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } }, - }, + this.substructureVisuals('cartoon'), { ref: IDs.ID('visual', 'protein', BaseRef) } ); } @@ -717,9 +742,7 @@ class ReDNATCOMspViewer { bb.to(IDs.ID('structure', 'water', BaseRef)) .apply( StateTransforms.Representation.StructureRepresentation3D, - { - type: { name: display.representation ?? 'ball-and-stick', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35 } }, - }, + this.substructureVisuals('ball-and-stick'), { ref: IDs.ID('visual', 'water', BaseRef) } ); } @@ -770,35 +793,81 @@ class ReDNATCOMspViewer { onLociSelected(selected: Representation.Loci) { const loci = Loci.normalize(selected.loci, 'two-residues'); - if (loci.kind === 'element-loci') - this.superposeReferences(loci); + if (loci.kind === 'element-loci') { + // TODO: This cannot call superposeReferences directly + // Instead, we must make a callback via the API + // and have the listener decide what to do with this event + const stepDesc = Step.describe(loci); + if (!stepDesc) + return; + const stepName = Step.name(stepDesc, this.haveMultipleModels); + this.superposeReferences(stepName, '', []); + } } async switchModel(display: Partial<Display>) { - const b = this.getBuilder('model', '', BaseRef); - b.update( - StateTransforms.Model.ModelFromTrajectory, - old => ({ - ...old, - modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 - }) - ); + if (display.modelNumber && display.modelNumber === this.currentModelNumber()) + return; + + const b = this.plugin.state.data.build() + .delete(IDs.ID('superposition', '', NtCSupSel)) + .delete(IDs.ID('superposition', '', NtCSupPrev)) + .delete(IDs.ID('superposition', '', NtCSupNext)) + .to(IDs.ID('model', '', BaseRef)) + .update( + StateTransforms.Model.ModelFromTrajectory, + old => ({ + ...old, + modelIndex: display.modelNumber ? display.modelNumber - 1 : 0 + }) + ); await b.commit(); } - superposeReferences(selected: StructureElement.Loci) { - const step = Step.describe(selected); - if (!step) - return; + async superposeReferences(stepName: string, referenceNtc: string, references: ('sel'|'prev'|'next')[]) { + const ReferenceVisuals = (color: number) => { + return { + type: { name: 'ball-and-stick', params: { sizeFactor: 0.15, aromaticBonds: false } }, + colorTheme: { name: 'uniform', params: { value: Color(color) } }, + }; + }; - const stepName = Step.name(step, this.haveMultipleModels); + const stepDesc = Step.fromName(stepName); + if (!stepDesc) + return; const stepId = this.stepNames.get(stepName); if (stepId === undefined) { console.error(`Unknown step name ${stepName}`); return; } + if (stepDesc.model !== this.currentModelNumber()) { + const b = this.getBuilder('model') + .update( + StateTransforms.Model.ModelFromTrajectory, + old => ({ + ...old, + modelIndex: stepDesc.model - 1, + }) + ); + await b.commit(); + } + + const entireStru = this.plugin.state.data.cells.get(IDs.ID('structure', 'nucleic', BaseRef))!.obj!; + const loci = Traverse.findResidue( + stepDesc.chain, + stepDesc.resNo1, + stepDesc.altId1, + StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(entireStru.data, entireStru.data)), + 'auth' + ); + if (loci.kind !== 'element-loci') + return; + const selLoci = Loci.normalize(loci, 'two-residues'); + if (selLoci.kind !== 'element-loci') + return; + const stepIdPrev = stepId === 0 ? void 0 : stepId - 1; const stepIdNext = stepId === this.steps.length - 1 ? void 0 : stepId + 1; @@ -807,7 +876,7 @@ class ReDNATCOMspViewer { const ntcRefNext = this.ntcRef(stepIdNext, 'next'); if (!ntcRefSel) { - console.error(`Seemingly invalid stepId ${stepId}`); + console.error(`stepId ${stepId} does not map to a known step`); return; } @@ -816,74 +885,39 @@ class ReDNATCOMspViewer { .delete(IDs.ID('superposition', '', NtCSupPrev)) .delete(IDs.ID('superposition', '', NtCSupNext)); - { - const stru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRefSel))!.obj!; - const loci = Script.toLoci(SelectAllScript, stru.data); + const addReference = (ntcRef: string, superposRef: string, loci: Loci, color: number) => { + const refStru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRef))!.obj!; + const refLoci = StructureSelection.toLociWithSourceUnits(StructureSelection.Singletons(refStru.data, refStru.data)); - const { bTransform } = this.superpose(loci, selected); - if (isNaN(bTransform[0])) { - console.error(`Cannot superpose reference conformer ${ntcRefSel} onto selection`); - return; - } - b.to(IDs.ID('structure', '', ntcRefSel)) - .apply( - StateTransforms.Model.TransformStructureConformation, - { transform: { name: 'matrix', params: { data: bTransform, transpose: false } } }, - { ref: IDs.ID('superposition', '', NtCSupSel) } - ).apply( - StateTransforms.Representation.StructureRepresentation3D, - { type: { name: 'ball-and-stick', params: { sizeFactor: 0.15 } } }, - { ref: IDs.ID('visual', '', NtCSupSel) } - ); - } - - // TODO: These cannot be applied onto selection!!! - if (ntcRefPrev) { - const stru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRefPrev))!.obj!; - const loci = Script.toLoci(SelectAllScript, stru.data); - - const { bTransform } = this.superpose(loci, selected); - if (isNaN(bTransform[0])) { - console.error(`Cannot superpose reference conformer ${ntcRefPrev} onto selection`); - return; - } - b.to(IDs.ID('structure', '', ntcRefPrev)) - .apply( - StateTransforms.Model.TransformStructureConformation, - { transform: { name: 'matrix', params: { data: bTransform, transpose: false } } }, - { ref: IDs.ID('superposition', '', NtCSupPrev) } - ).apply( - StateTransforms.Representation.StructureRepresentation3D, - { type: { name: 'ball-and-stick', params: { sizeFactor: 0.15 } } }, - { ref: IDs.ID('visual', '', NtCSupPrev) } - ); - } - - // TODO: These cannot be applied onto selection!!! - if (ntcRefNext) { - const stru = this.plugin.state.data.cells.get(IDs.ID('structure', '', ntcRefNext))!.obj!; - const loci = Script.toLoci(SelectAllScript, stru.data); - - const { bTransform } = this.superpose(loci, selected); - if (isNaN(bTransform[0])) { - console.error(`Cannot superpose reference conformer ${ntcRefNext} onto selection`); - return; + if (loci.kind === 'element-loci' && Step.is(loci)) { + const { bTransform, rmsd } = this.superpose(refLoci, loci); + if (isNaN(bTransform[0])) { + console.error(`Cannot superpose reference conformer ${ntcRef} onto selection`); + return; + } + b.to(IDs.ID('structure', '', ntcRef)) + .apply( + StateTransforms.Model.TransformStructureConformation, + { transform: { name: 'matrix', params: { data: bTransform, transpose: false } } }, + { ref: IDs.ID('superposition', '', superposRef) } + ).apply( + StateTransforms.Representation.StructureRepresentation3D, + ReferenceVisuals(color), + { ref: IDs.ID('visual', '', superposRef) } + ); + return rmsd; } - b.to(IDs.ID('structure', '', ntcRefNext)) - .apply( - StateTransforms.Model.TransformStructureConformation, - { transform: { name: 'matrix', params: { data: bTransform, transpose: false } } }, - { ref: IDs.ID('superposition', '', NtCSupNext) } - ).apply( - StateTransforms.Representation.StructureRepresentation3D, - { type: { name: 'ball-and-stick', params: { sizeFactor: 0.15 } } }, - { ref: IDs.ID('visual', '', NtCSupNext) } - ); - } + }; - // TODO: Superpose previous and next step too!!! + const rmsd = addReference(ntcRefSel, NtCSupSel, selLoci, 0x008000); + if (ntcRefPrev) + addReference(ntcRefPrev, NtCSupPrev, Loci.normalize(Traverse.residue(-1, stepDesc.altId1, selLoci), 'two-residues'), 0x0000FF); + if (ntcRefNext) + addReference(ntcRefNext, NtCSupNext, Loci.normalize(Traverse.residue(1, stepDesc.altId2, selLoci), 'two-residues'), 0x00FFFF); b.commit(); + + return rmsd; } async toggleSubstructure(sub: IDs.Substructure, display: Partial<Display>) { @@ -893,12 +927,11 @@ class ReDNATCOMspViewer { if (show) { const b = this.getBuilder('structure', sub); + const visuals = this.substructureVisuals(sub === 'water' ? 'ball-and-stick' : repr); if (b) { b.apply( StateTransforms.Representation.StructureRepresentation3D, - { - type: { name: repr, params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } }, // TODO: Use different params for water - }, + visuals, { ref: IDs.ID('visual', sub, BaseRef) } ); await b.commit(); @@ -949,6 +982,26 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { this.setState({ ...this.state, display }); } + command(cmd: Commands.Cmd) { + if (!this.viewer) + return; + + if (cmd.type === 'select-step') { + this.viewer.superposeReferences(cmd.stepName, cmd.referenceNtC, cmd.references); + } else if (cmd.type === 'switch-model') { + if (cmd.model < 1 || cmd.model > this.viewer.getModelCount()) + return; + + const display: Display = { + ...this.state.display, + modelNumber: cmd.model, + }; + + this.viewer.switchModel(display); + this.setState({ ...this.state, display }); + } + } + constructor(props: ReDNATCOMsp.Props) { super(props); @@ -972,7 +1025,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { ReDNATCOMspViewer.create(elem!).then(viewer => { this.viewer = viewer; this.viewer.loadReferenceConformers().then(() => { - ReDNATCOMspApi.bind(this); + ReDNATCOMspApi._bind(this); if (this.props.onInited) this.props.onInited(); @@ -1006,7 +1059,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { text={capitalize(this.state.display.representation)} enabled={ready} onClick={() => { - const display = { + const display: Display = { ...this.state.display, representation: this.state.display.representation === 'cartoon' ? 'ball-and-stick' : 'cartoon', }; @@ -1025,7 +1078,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { enabled={hasNucleic} switchedOn={this.state.display.showNucleic} onClick={() => { - const display = { + const display: Display = { ...this.state.display, showNucleic: !this.state.display.showNucleic, }; @@ -1040,7 +1093,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { enabled={hasProtein} switchedOn={this.state.display.showProtein} onClick={() => { - const display = { + const display: Display = { ...this.state.display, showProtein: !this.state.display.showProtein, }; @@ -1055,7 +1108,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { enabled={hasWater} switchedOn={this.state.display.showWater} onClick={() => { - const display = { + const display: Display = { ...this.state.display, showWater: !this.state.display.showWater, }; @@ -1075,7 +1128,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { enabled={ready} switchedOn={this.state.display.showPyramids} onClick={() => { - const display = { + const display: Display = { ...this.state.display, showPyramids: !this.state.display.showPyramids, }; @@ -1089,7 +1142,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { text={this.state.display.pyramidsTransparent ? 'Transparent' : 'Solid'} enabled={this.state.display.showPyramids} onClick={() => { - const display = { + const display: Display = { ...this.state.display, pyramidsTransparent: !this.state.display.pyramidsTransparent, }; @@ -1113,7 +1166,7 @@ class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props, State> { text={this.state.display.ballsTransparent ? 'Transparent' : 'Solid'} enabled={this.state.display.showBalls} onClick={() => { - const display = { + const display: Display = { ...this.state.display, ballsTransparent: !this.state.display.ballsTransparent, }; @@ -1226,12 +1279,18 @@ class _ReDNATCOMspApi { throw new Error('ReDNATCOMsp object not bound'); } - bind(target: ReDNATCOMsp) { + _bind(target: ReDNATCOMsp) { this.target = target; } + command(cmd: Commands.Cmd) { + this.check(); + this.target!.command(cmd); + } + init(elemId: string, onInited?: () => void) { ReDNATCOMsp.init(elemId, onInited); + return this; } loadStructure(data: string) { diff --git a/src/apps/rednatco/step.ts b/src/apps/rednatco/step.ts index c35bdc271..a988acfad 100644 --- a/src/apps/rednatco/step.ts +++ b/src/apps/rednatco/step.ts @@ -21,6 +21,25 @@ export namespace Step { return `${compId}${altId ? `.${altId}` : ''}_${seqId}${insCode ? `.${insCode}` : '' }`; } + function residueDescription(a: string, b: string): { comp: string, altId?: string, resNo: number, insCode?: string }|undefined { + const toksA = a.split('.'); + const toksB = b.split('.'); + + if (toksA.length > 2 || toksB.length > 2) + return void 0; + + const resNo = parseInt(toksB[0]); + if (isNaN(resNo)) + return void 0; + + return { + comp: toksA[0], + altId: toksA.length === 2 ? toksA[1] : void 0, + resNo, + insCode: toksB.length === 2 ? toksB[1] : void 0, + }; + } + export function describe(loci: StructureElement.Loci) { const es = loci.elements[0]; // Ignore multiple selections @@ -62,6 +81,96 @@ export namespace Step { return description; } + export function fromName(name: string) { + const description: Description = { + model: -1, + entryId: '', + chain: '', + resNo1: -1, + comp1: '', + altId1: void 0, + insCode1: void 0, + resNo2: -1, + comp2: '', + altId2: void 0, + insCode2: void 0, + }; + + const toks = name.split('_'); + if (toks.length !== 6) { + console.error(`String ${name} is not valid step name`); + return void 0; + } + + const entryTok = toks[0]; + const chain = toks[1]; + const res1TokA = toks[2]; + const res1TokB = toks[3]; + const res2TokA = toks[4]; + const res2TokB = toks[5]; + + const ets = entryTok.split('-'); + if (ets.length === 1) { + description.entryId = ets[0]; + description.model = 1; + } else if (ets.length === 2) { + description.entryId = ets[0]; + const m = parseInt(ets[1].slice(1)); + if (isNaN(m)) { + console.error(`String ${name} is not valid step name`); + return void 0; + } + description.model = m; + } else { + console.error(`String ${name} is not valid step name`); + return void 0; + } + + if (chain.length !== 1) { + console.error(`String ${name} is not valid step name`); + return void 0; + } else + description.chain = chain; + + const res1 = residueDescription(res1TokA, res1TokB); + const res2 = residueDescription(res2TokA, res2TokB); + if (!res1 || !res2) { + console.error(`String ${name} is not valid step name`); + return void 0; + } + + description.resNo1 = res1.resNo; + description.comp1 = res1.comp; + description.altId1 = res1.altId; + description.insCode1 = res1.insCode; + description.resNo2 = res2.resNo; + description.comp2 = res2.comp; + description.altId2 = res2.altId; + description.insCode2 = res2.insCode; + + return description; + } + + export function is(loci: StructureElement.Loci) { + const e = loci.elements[0]; + const loc = Location.create(loci.structure, e.unit, e.unit.elements[OrderedSet.getAt(e.indices, 0)]); + + const resNo1 = StructureProperties.residue.label_seq_id(loc); + const asymId = StructureProperties.chain.label_asym_id(loc); + for (let idx = 1; idx < OrderedSet.size(e.indices); idx++) { + loc.element = e.unit.elements[OrderedSet.getAt(e.indices, idx)]; + + const resNo = StructureProperties.residue.label_seq_id(loc); + if (resNo !== resNo1 + 1) + continue; + const _asymId = StructureProperties.chain.label_asym_id(loc); + if (_asymId === asymId) + return true; + } + + return false; + } + export function name(description: Description, multipleModels: boolean) { const res1 = nameResidue(description.resNo1, description.comp1, description.altId1, description.insCode1); const res2 = nameResidue(description.resNo2, description.comp2, description.altId2, description.insCode2); diff --git a/src/apps/rednatco/traverse.ts b/src/apps/rednatco/traverse.ts new file mode 100644 index 000000000..316e4c3e9 --- /dev/null +++ b/src/apps/rednatco/traverse.ts @@ -0,0 +1,92 @@ +import { Segmentation } from '../../mol-data/int'; +import { OrderedSet } from '../../mol-data/int/ordered-set'; +import { EmptyLoci, Loci } from '../../mol-model/loci'; +import { ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure'; +import { Location } from '../../mol-model/structure/structure/element/location'; + +export namespace Traverse { + type Residue = Segmentation.Segment<ResidueIndex>; + + export function residueAltId(structure: Structure, unit: Unit, residue: Residue) { + const loc = Location.create(structure, unit); + for (let rI = residue.start; rI < residue.end; rI++) { + loc.element = OrderedSet.getAt(unit.elements, rI); + const altId = StructureProperties.atom.label_alt_id(loc); + if (altId !== '') + return altId; + } + + return void 0; + } + + export function findResidue(asymId: string, seqId: number, altId: string|undefined, loci: StructureElement.Loci, source: 'label'|'auth') { + for (const e of loci.elements) { + const loc = Location.create(loci.structure, e.unit); + + const getAsymId = source === 'label' ? StructureProperties.chain.label_asym_id : StructureProperties.chain.auth_asym_id; + const getSeqId = source === 'label' ? StructureProperties.residue.label_seq_id : StructureProperties.residue.auth_seq_id; + + // Walk the entire unit and look for the requested residue + const chainIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.chainAtomSegments, e.unit.elements); + const residueIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.residueAtomSegments, e.unit.elements); + + const elemIndex = (idx: number) => OrderedSet.getAt(e.unit.elements, idx); + while (chainIt.hasNext) { + const chain = chainIt.move(); + loc.element = elemIndex(chain.start); + const _asymId = getAsymId(loc); + if (_asymId !== asymId) + continue; // Wrong chain, skip it + + residueIt.setSegment(chain); + while (residueIt.hasNext) { + const residue = residueIt.move(); + loc.element = elemIndex(residue.start); + + const _seqId = getSeqId(loc); + if (_seqId === seqId) { + if (altId) { + const _altId = residueAltId(loci.structure, e.unit, residue); + if (_altId && _altId !== altId) + continue; + } + + const start = residue.start as StructureElement.UnitIndex; + const end = residue.end as StructureElement.UnitIndex; + return StructureElement.Loci( + loci.structure, + [{ unit: e.unit, indices: OrderedSet.ofBounds(start, end) }] + ); + } + } + } + } + + return EmptyLoci; + } + + export function residue(shift: number, altId: string|undefined, cursor: StructureElement.Loci) { + for (const e of cursor.elements) { + const entireUnit = cursor.structure.units[e.unit.id]; + const loc = Location.create(cursor.structure, e.unit); + + loc.element = e.unit.elements[OrderedSet.getAt(e.indices, 0)]; + const asymId = StructureProperties.chain.label_asym_id(loc); + const seqId = StructureProperties.residue.label_seq_id(loc); + + const from = 0 as StructureElement.UnitIndex; + const to = entireUnit.elements.length as StructureElement.UnitIndex; + + const loci = findResidue( + asymId, + seqId + shift, + altId, + StructureElement.Loci(cursor.structure, [{ unit: entireUnit, indices: OrderedSet.ofBounds(from, to) }]), + 'label' + ); + if (!Loci.isEmpty(loci)) + return loci; + } + return EmptyLoci; + } +} -- GitLab