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