소스 검색

structure superposition by chains or by atoms

- superposition by chains can be guided by sequence alignment
- TODO not working with already transformed structures in the general case
Alexander Rose 5 년 전
부모
커밋
78c70b3f5b

+ 2 - 2
src/examples/basic-wrapper/superposition.ts

@@ -6,7 +6,7 @@
 
 import { Mat4 } from '../../mol-math/linear-algebra';
 import { QueryContext, StructureSelection } from '../../mol-model/structure';
-import { superposeStructures } from '../../mol-model/structure/structure/util/superposition';
+import { superpose } from '../../mol-model/structure/structure/util/superposition';
 import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
 import { PluginContext } from '../../mol-plugin/context';
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
@@ -79,7 +79,7 @@ export function dynamicSuperpositionTest(plugin: PluginContext, src: string[], c
         const xs = plugin.managers.structure.hierarchy.current.structures;
         const selections = xs.map(s => StructureSelection.toLociWithCurrentUnits(query(new QueryContext(s.cell.obj!.data))));
 
-        const transforms = superposeStructures(selections);
+        const transforms = superpose(selections);
 
         await siteVisual(plugin, xs[0].cell, pivot, rest);
         for (let i = 1; i < selections.length; i++) {

+ 10 - 5
src/mol-model/sequence/alignment/_spec/alignment.spec.ts

@@ -9,13 +9,18 @@ import { align } from '../alignment';
 describe('Alignment', () => {
     it('basic', () => {
         // 3PQR: Rhodopsin
-        const seq1 = 'MNGTEGPNFYVPFSNKTGVVRSPFEAPQYYLAEPWQFSMLAAYMFLLIMLGFPINFLTLYVTVQHKKLRTPLNYILLNLAVADLFMVFGGFTTTLYTSLHGYFVFGPTGCNLEGFFATLGGEIALWSLVVLAIERYVVVCKPMSNFRFGENHAIMGVAFTWVMALACAAPPLVGWSRYIPEGMQCSCGIDYYTPHEETNNESFVIYMFVVHFIIPLIVIFFCYGQLVFTVKEAAAQQQESATTQKAEKEVTRMVIIMVIAFLICWLPYAGVAFYIFTHQGSDFGPIFMTIPAFFAKTSAVYNPVIYIMMNKQFRNCMVTTLCCGKNPLGDDEASTTVSKTETSQVAPA';
+        const seqA = 'MNGTEGPNFYVPFSNKTGVVRSPFEAPQYYLAEPWQFSMLAAYMFLLIMLGFPINFLTLYVTVQHKKLRTPLNYILLNLAVADLFMVFGGFTTTLYTSLHGYFVFGPTGCNLEGFFATLGGEIALWSLVVLAIERYVVVCKPMSNFRFGENHAIMGVAFTWVMALACAAPPLVGWSRYIPEGMQCSCGIDYYTPHEETNNESFVIYMFVVHFIIPLIVIFFCYGQLVFTVKEAAAQQQESATTQKAEKEVTRMVIIMVIAFLICWLPYAGVAFYIFTHQGSDFGPIFMTIPAFFAKTSAVYNPVIYIMMNKQFRNCMVTTLCCGKNPLGDDEASTTVSKTETSQVAPA';
         // 3SN6: Endolysin,Beta-2 adrenergic
-        const seq2 = 'DYKDDDDAENLYFQGNIFEMLRIDEGLRLKIYKDTEGYYTIGIGHLLTKSPSLNAAKSELDKAIGRNTNGVITKDEAEKLFNQDVDAAVRGILRNAKLKPVYDSLDAVRRAALINMVFQMGETGVAGFTNSLRMLQQKRWDEAAVNLAKSRWYNQTPNRAKRVITTFRTGTWDAYAADEVWVVGMGIVMSLIVLAIVFGNVLVITAIAKFERLQTVTNYFITSLACADLVMGLAVVPFGAAHILTKTWTFGNFWCEFWTSIDVLCVTASIETLCVIAVDRYFAITSPFKYQSLLTKNKARVIILMVWIVSGLTSFLPIQMHWYRATHQEAINCYAEETCCDFFTNQAYAIASSIVSFYVPLVIMVFVYSRVFQEAKRQLQKIDKSEGRFHVQNLSQVEQDGRTGHGLRRSSKFCLKEHKALKTLGIIMGTFTLCWLPFFIVNIVHVIQDNLIRKEVYILLNWIGYVNSGFNPLIYCRSPDFRIAFQELLCLRRSSLKAYGNGYSSNGNTGEQSG';
-        const { ali1, ali2, score } = align(seq1, seq2);
+        const seqB = 'DYKDDDDAENLYFQGNIFEMLRIDEGLRLKIYKDTEGYYTIGIGHLLTKSPSLNAAKSELDKAIGRNTNGVITKDEAEKLFNQDVDAAVRGILRNAKLKPVYDSLDAVRRAALINMVFQMGETGVAGFTNSLRMLQQKRWDEAAVNLAKSRWYNQTPNRAKRVITTFRTGTWDAYAADEVWVVGMGIVMSLIVLAIVFGNVLVITAIAKFERLQTVTNYFITSLACADLVMGLAVVPFGAAHILTKTWTFGNFWCEFWTSIDVLCVTASIETLCVIAVDRYFAITSPFKYQSLLTKNKARVIILMVWIVSGLTSFLPIQMHWYRATHQEAINCYAEETCCDFFTNQAYAIASSIVSFYVPLVIMVFVYSRVFQEAKRQLQKIDKSEGRFHVQNLSQVEQDGRTGHGLRRSSKFCLKEHKALKTLGIIMGTFTLCWLPFFIVNIVHVIQDNLIRKEVYILLNWIGYVNSGFNPLIYCRSPDFRIAFQELLCLRRSSLKAYGNGYSSNGNTGEQSG';
 
-        expect(ali1).toEqual('------------------------------------------------------------------------------------------------------------------------------------------------MNGTEGPNFYVPFSNKTGVVRSPFEA---PQYYLAEPWQFSM--LAAYMFLLIMLGFPINFLTLYVTVQHKKLRTPLNYILLNLAVADLFMVFGGFTTTLYTSLH---GYFVFGPTGCNLEGFFATLGGEIALWSLVVLAIERYVVVCKPMS-NFRFGENHAIMGVAFTWVMA-LACAAPPLVGWSRYI-PEGMQC----SCGIDYYTPHEETNNESFVIYMFVVHFIIPLIVIFFCYGQLV----------------FTVKEAAAQQQESATTQ----------KAEKEVTRMVIIMVIAFLICWLPYAGVAFYIFTHQGSDFGPIFMTIPAFFAKTSAVYNPVIYIMMNKQFRNCMVTTLCCGKNPLGDDEASTTVSKTETSQVAPA');
-        expect(ali2).toEqual('DYKDDDDAENLYFQGNIFEMLRIDEGLRLKIYKDTEGYYTIGIGHLLTKSPSLNAAKSELDKAIGRNTNGVITKDEAEKLFNQDVDAAVRGILRNAKLKPVYDSLDAVRRAALINMVFQMGETGVAGFTNSLRMLQQKRWDEAAVNLAKS-RWYNQTPNRAKRVITTFRTGTWDAYAADEVWVVGMGIVMSLIVLAIVFG---NVLVITAIAKFERLQTVTNYFITSLACADLVM---GLAVVPFGAAHILTKTWTFGNFWCEFWTSIDVLCVTASIETLCVIAVDRYFAITSPFKYQSLLTKNKARVIILMVWIVSGLTSFLPIQMHWYRATHQEAINCYAEETC-CDFFT------NQAYAIASSIVSFYVPLVIMVFVYSRVFQEAKRQLQKIDKSEGRFHVQNLSQVEQDGRTGHGLRRSSKFCLKEHKALKTLGIIMG-TFTLCWLPFF-IVNIVHVIQDNLIRKEVYILLNWIGYVNSGFNPLIY-CRSPDFRIAFQELLCLRRSSL--KAYGNGYSSNGNTGEQSG');
+        const { aliA, aliB, score } = align(seqA, seqB, {
+            gapPenalty: 11,
+            gapExtensionPenalty: 11,
+            substMatrix: 'blosum62'
+        });
+
+        expect(aliA).toEqual('------------------------------------------------------------------------------------------------------------------------------------------------MNGTEGPNFYVPFSNKTGVVRSPFEA---PQYYLAEPWQFSM--LAAYMFLLIMLGFPINFLTLYVTVQHKKLRTPLNYILLNLAVADLFMVFGGFTTTLYTSLH---GYFVFGPTGCNLEGFFATLGGEIALWSLVVLAIERYVVVCKPMS-NFRFGENHAIMGVAFTWVMA-LACAAPPLVGWSRYI-PEGMQC----SCGIDYYTPHEETNNESFVIYMFVVHFIIPLIVIFFCYGQLV----------------FTVKEAAAQQQESATTQ----------KAEKEVTRMVIIMVIAFLICWLPYAGVAFYIFTHQGSDFGPIFMTIPAFFAKTSAVYNPVIYIMMNKQFRNCMVTTLCCGKNPLGDDEASTTVSKTETSQVAPA');
+        expect(aliB).toEqual('DYKDDDDAENLYFQGNIFEMLRIDEGLRLKIYKDTEGYYTIGIGHLLTKSPSLNAAKSELDKAIGRNTNGVITKDEAEKLFNQDVDAAVRGILRNAKLKPVYDSLDAVRRAALINMVFQMGETGVAGFTNSLRMLQQKRWDEAAVNLAKS-RWYNQTPNRAKRVITTFRTGTWDAYAADEVWVVGMGIVMSLIVLAIVFG---NVLVITAIAKFERLQTVTNYFITSLACADLVM---GLAVVPFGAAHILTKTWTFGNFWCEFWTSIDVLCVTASIETLCVIAVDRYFAITSPFKYQSLLTKNKARVIILMVWIVSGLTSFLPIQMHWYRATHQEAINCYAEETC-CDFFT------NQAYAIASSIVSFYVPLVIMVFVYSRVFQEAKRQLQKIDKSEGRFHVQNLSQVEQDGRTGHGLRRSSKFCLKEHKALKTLGIIMG-TFTLCWLPFF-IVNIVHVIQDNLIRKEVYILLNWIGYVNSGFNPLIY-CRSPDFRIAFQELLCLRRSSL--KAYGNGYSSNGNTGEQSG');
         expect(score).toEqual(118);
     });
 });

+ 26 - 26
src/mol-model/sequence/alignment/alignment.ts

@@ -13,14 +13,14 @@ const DefaultAlignmentOptions = {
 };
 export type AlignmentOptions = typeof DefaultAlignmentOptions;
 
-export function align(seq1: string, seq2: string, options: Partial<AlignmentOptions> = {}) {
+export function align(seqA: ArrayLike<string>, seqB: ArrayLike<string>, options: Partial<AlignmentOptions> = {}) {
     const o = { ...DefaultAlignmentOptions, ...options };
-    const alignment = new Alignment(seq1, seq2, o);
+    const alignment = new Alignment(seqA, seqB, o);
     alignment.calculate();
     alignment.trace();
     return {
-        ali1: alignment.ali1,
-        ali2: alignment.ali2,
+        aliA: alignment.aliA,
+        aliB: alignment.aliB,
         score: alignment.score,
     };
 }
@@ -31,16 +31,16 @@ class Alignment {
 
     n: number; m: number
     S: number[][]; V: number[][]; H: number[][]
-    ali1: string; ali2: string;
+    aliA: ArrayLike<string>; aliB: ArrayLike<string>;
     score: number
 
-    constructor (readonly seq1: string, readonly seq2: string, options: AlignmentOptions) {
+    constructor (readonly seqA: ArrayLike<string>, readonly seqB: ArrayLike<string>, options: AlignmentOptions) {
         this.gapPenalty = options.gapPenalty;
         this.gapExtensionPenalty = options.gapExtensionPenalty;
         this.substMatrix = SubstitutionMatrices[options.substMatrix];
 
-        this.n = this.seq1.length;
-        this.m = this.seq2.length;
+        this.n = this.seqA.length;
+        this.m = this.seqB.length;
     }
 
     private initMatrices () {
@@ -78,8 +78,8 @@ class Alignment {
     }
 
     private makeScoreFn () {
-        const seq1 = this.seq1;
-        const seq2 = this.seq2;
+        const seq1 = this.seqA;
+        const seq2 = this.seqB;
 
         const substMatrix = this.substMatrix;
 
@@ -137,8 +137,8 @@ class Alignment {
     }
 
     trace () {
-        this.ali1 = '';
-        this.ali2 = '';
+        this.aliA = '';
+        this.aliB = '';
 
         const scoreFn = this.makeScoreFn();
 
@@ -160,8 +160,8 @@ class Alignment {
         while (i > 0 && j > 0) {
             if (mat === 'S') {
                 if (this.S[i][j] === this.S[i - 1][j - 1] + scoreFn(i - 1, j - 1)) {
-                    this.ali1 = this.seq1[i - 1] + this.ali1;
-                    this.ali2 = this.seq2[j - 1] + this.ali2;
+                    this.aliA = this.seqA[i - 1] + this.aliA;
+                    this.aliB = this.seqB[j - 1] + this.aliB;
                     --i;
                     --j;
                     mat = 'S';
@@ -175,13 +175,13 @@ class Alignment {
                 }
             } else if (mat === 'V') {
                 if (this.V[i][j] === this.V[i - 1][j] + this.gapExtensionPenalty) {
-                    this.ali1 = this.seq1[i - 1] + this.ali1;
-                    this.ali2 = '-' + this.ali2;
+                    this.aliA = this.seqA[i - 1] + this.aliA;
+                    this.aliB = '-' + this.aliB;
                     --i;
                     mat = 'V';
                 } else if (this.V[i][j] === this.S[i - 1][j] + this.gap(0)) {
-                    this.ali1 = this.seq1[i - 1] + this.ali1;
-                    this.ali2 = '-' + this.ali2;
+                    this.aliA = this.seqA[i - 1] + this.aliA;
+                    this.aliB = '-' + this.aliB;
                     --i;
                     mat = 'S';
                 } else {
@@ -189,13 +189,13 @@ class Alignment {
                 }
             } else if (mat === 'H') {
                 if (this.H[i][j] === this.H[i][j - 1] + this.gapExtensionPenalty) {
-                    this.ali1 = '-' + this.ali1;
-                    this.ali2 = this.seq2[j - 1] + this.ali2;
+                    this.aliA = '-' + this.aliA;
+                    this.aliB = this.seqB[j - 1] + this.aliB;
                     --j;
                     mat = 'H';
                 } else if (this.H[i][j] === this.S[i][j - 1] + this.gap(0)) {
-                    this.ali1 = '-' + this.ali1;
-                    this.ali2 = this.seq2[j - 1] + this.ali2;
+                    this.aliA = '-' + this.aliA;
+                    this.aliB = this.seqB[j - 1] + this.aliB;
                     --j;
                     mat = 'S';
                 } else {
@@ -205,14 +205,14 @@ class Alignment {
         }
 
         while (i > 0) {
-            this.ali1 = this.seq1[i - 1] + this.ali1;
-            this.ali2 = '-' + this.ali2;
+            this.aliA = this.seqA[i - 1] + this.aliA;
+            this.aliB = '-' + this.aliB;
             --i;
         }
 
         while (j > 0) {
-            this.ali1 = '-' + this.ali1;
-            this.ali2 = this.seq2[j - 1] + this.ali2;
+            this.aliA = '-' + this.aliA;
+            this.aliB = this.seqB[j - 1] + this.aliB;
             --j;
         }
     }

+ 100 - 0
src/mol-model/sequence/alignment/sequence.ts

@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureElement, Unit } from '../../structure/structure';
+import { AlignmentOptions, align } from './alignment';
+import { OrderedSet } from '../../../mol-data/int';
+
+export { AlignSequences };
+
+namespace AlignSequences {
+    export type Input = {
+        a: StructureElement.Loci.Element,
+        b: StructureElement.Loci.Element
+    }
+    /** `a` and `b` contain matching pairs, i.e. `a.indices[0]` aligns with `b.indices[0]` */
+    export type Result = {
+        a: StructureElement.Loci.Element,
+        b: StructureElement.Loci.Element,
+        score: number
+    }
+
+    function createSeqIdIndicesMap(element: StructureElement.Loci.Element) {
+        const seqIds = new Map<number, StructureElement.UnitIndex[]>();
+        if (!Unit.isAtomic(element.unit)) return seqIds;
+
+        const { label_seq_id } = element.unit.model.atomicHierarchy.residues;
+        const { residueIndex } = element.unit;
+        for (let i = 0, il = OrderedSet.size(element.indices); i < il; ++i) {
+            const uI = OrderedSet.getAt(element.indices, i);
+            const eI = element.unit.elements[uI];
+            const seqId = label_seq_id.value(residueIndex[eI]);
+            if (seqIds.has(seqId)) seqIds.get(seqId)!.push(uI);
+            else seqIds.set(seqId, [uI]);
+        }
+        return seqIds;
+    }
+
+    export function compute(input: Input, options: Partial<AlignmentOptions> = {}): Result {
+        const seqA = getSequence(input.a.unit);
+        const seqB = getSequence(input.b.unit);
+
+        const seqIdIndicesA = createSeqIdIndicesMap(input.a);
+        const seqIdIndicesB = createSeqIdIndicesMap(input.b);
+
+        const indicesA: StructureElement.UnitIndex[] = [];
+        const indicesB: StructureElement.UnitIndex[] = [];
+        const { aliA, aliB, score } = align(seqA.code.toArray(), seqB.code.toArray(), options);
+
+        let seqIdxA = 0, seqIdxB = 0;
+        for (let i = 0, il = aliA.length; i < il; ++i) {
+            if (aliA[i] === '-' || aliB[i] === '-') {
+                if (aliA[i] !== '-') seqIdxA += 1;
+                if (aliB[i] !== '-') seqIdxB += 1;
+                continue;
+            }
+
+            const seqIdA = seqA.seqId.value(seqIdxA);
+            const seqIdB = seqB.seqId.value(seqIdxB);
+
+            if (seqIdIndicesA.has(seqIdA) && seqIdIndicesB.has(seqIdB)) {
+                const iA = seqIdIndicesA.get(seqIdA)!;
+                const iB = seqIdIndicesB.get(seqIdB)!;
+                // use min length to guard against alternate locations
+                for (let j = 0, jl = Math.min(iA.length, iB.length); j < jl; ++j) {
+                    indicesA.push(iA[j]);
+                    indicesB.push(iB[j]);
+                }
+            }
+
+            seqIdxA += 1, seqIdxB += 1;
+        }
+
+        const outA = OrderedSet.intersect(OrderedSet.ofSortedArray(indicesA), input.a.indices);
+        const outB = OrderedSet.intersect(OrderedSet.ofSortedArray(indicesB), input.b.indices);
+
+        return {
+            a: { unit: input.a.unit, indices: outA },
+            b: { unit: input.b.unit, indices: outB },
+            score
+        };
+    }
+}
+
+function entityKey(unit: Unit) {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return unit.model.atomicHierarchy.index.getEntityFromChain(unit.chainIndex[unit.elements[0]]);
+        case Unit.Kind.Spheres:
+            return unit.model.coarseHierarchy.spheres.entityKey[unit.elements[0]];
+        case Unit.Kind.Gaussians:
+            return unit.model.coarseHierarchy.gaussians.entityKey[unit.elements[0]];
+    }
+}
+
+function getSequence(unit: Unit) {
+    return unit.model.sequence.byEntityKey[entityKey(unit)].sequence;
+}

+ 33 - 2
src/mol-model/structure/structure/util/superposition.ts

@@ -1,14 +1,16 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
 import StructureElement from '../element';
 import { OrderedSet } from '../../../../mol-data/int';
+import { AlignSequences } from '../../../sequence/alignment/sequence';
 
-export function superposeStructures(xs: StructureElement.Loci[]): MinimizeRmsd.Result[] {
+export function superpose(xs: StructureElement.Loci[]): MinimizeRmsd.Result[] {
     const ret: MinimizeRmsd.Result[] = [];
     if (xs.length <= 0) return ret;
 
@@ -24,6 +26,35 @@ export function superposeStructures(xs: StructureElement.Loci[]): MinimizeRmsd.R
     return ret;
 }
 
+type AlignAndSuperposeResult = MinimizeRmsd.Result & { alignmentScore: number };
+
+export function alignAndSuperpose(xs: StructureElement.Loci[]): AlignAndSuperposeResult[] {
+    const ret: AlignAndSuperposeResult[] = [];
+    if (xs.length <= 0) return ret;
+
+    for (let i = 1; i < xs.length; i++) {
+
+        const { a, b, score } = AlignSequences.compute({
+            a: xs[0].elements[0],
+            b: xs[i].elements[0]
+        });
+
+        const lociA = StructureElement.Loci(xs[0].structure, [a]);
+        const lociB = StructureElement.Loci(xs[i].structure, [b]);
+        const n = OrderedSet.size(a.indices);
+
+        ret.push({
+            ...MinimizeRmsd.compute({
+                a: getPositionTable(lociA, n),
+                b: getPositionTable(lociB, n)
+            }),
+            alignmentScore: score
+        });
+    }
+
+    return ret;
+}
+
 function getPositionTable(xs: StructureElement.Loci, n: number): MinimizeRmsd.Positions {
     const ret = MinimizeRmsd.Positions.empty(n);
     let o = 0;

+ 1 - 1
src/mol-plugin-state/manager/structure/selection.ts

@@ -30,7 +30,7 @@ interface StructureSelectionManagerState {
 }
 
 const boundaryHelper = new BoundaryHelper('98');
-const HISTORY_CAPACITY = 8;
+const HISTORY_CAPACITY = 24;
 
 export type StructureSelectionModifier = 'add' | 'remove' | 'intersect' | 'set'
 

+ 2 - 0
src/mol-plugin-ui/controls.tsx

@@ -30,6 +30,7 @@ import { StructureSelectionActionsControls } from './structure/selection';
 import { StructureSourceControls } from './structure/source';
 import { VolumeStreamingControls, VolumeSourceControls } from './structure/volume';
 import { PluginConfig } from '../mol-plugin/config';
+import { StructureSuperpositionControls } from './structure/superposition';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
     state = { show: false, label: '' }
@@ -296,6 +297,7 @@ export class DefaultStructureTools extends PluginUIComponent {
 
             <StructureSourceControls />
             <StructureMeasurementsControls />
+            <StructureSuperpositionControls />
             <StructureComponentControls />
             <VolumeStreamingControls />
             <VolumeSourceControls />

+ 1 - 1
src/mol-plugin-ui/skin/base/components/controls.scss

@@ -239,7 +239,7 @@
 // TODO : get rid of the important
 .msp-control-group-header {
     background: $default-background;
-    > button {
+    > button, div {
         padding-left: 4px; // $control-spacing / 2 !important;
         text-align: left;
         height: 24px !important; // 2 * $row-height / 3 !important;

+ 314 - 0
src/mol-plugin-ui/structure/superposition.tsx

@@ -0,0 +1,314 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import FlipToFrontIcon from '@material-ui/icons/FlipToFront';
+import HelpOutline from '@material-ui/icons/HelpOutline';
+import LinearScaleIcon from '@material-ui/icons/LinearScale';
+import ScatterPlotIcon from '@material-ui/icons/ScatterPlot';
+import ArrowDownward from '@material-ui/icons/ArrowDownward';
+import ArrowUpward from '@material-ui/icons/ArrowUpward';
+import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
+import Tune from '@material-ui/icons/Tune';
+import { CollapsableControls, PurePluginUIComponent } from '../base';
+import { Icon } from '../controls/icons';
+import { Button, ToggleButton, IconButton } from '../controls/common';
+import { StructureElement, StructureSelection, QueryContext, Structure } from '../../mol-model/structure';
+import { Mat4 } from '../../mol-math/linear-algebra';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { StateObjectRef, StateObjectCell, StateSelection } from '../../mol-state';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { alignAndSuperpose, superpose } from '../../mol-model/structure/structure/util/superposition';
+import { StructureSelectionQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
+import { structureElementStatsLabel, elementLabel } from '../../mol-theme/label';
+import { ParameterControls } from '../controls/parameters';
+import { stripTags } from '../../mol-util/string';
+import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
+
+// TODO not working with already transformed structures in the general case
+
+export class StructureSuperpositionControls extends CollapsableControls {
+    defaultState() {
+        return {
+            isCollapsed: false,
+            header: 'Superposition',
+            brand: { accent: 'gray' as const, svg: FlipToFrontIcon },
+            isHidden: true
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
+            this.setState({ isHidden: sel.structures.length < 2 });
+        });
+    }
+
+    renderControls() {
+        return <>
+            <SuperpositionControls />
+        </>;
+    }
+}
+
+export const StructureSuperpositionParams = {
+    alignSequences: PD.Boolean(true, { isEssential: true, description: 'Perform a sequence alignment and use the aligned residue pairs to guide the 3D superposition.' }),
+};
+const DefaultStructureSuperpositionOptions = PD.getDefaultValues(StructureSuperpositionParams);
+export type StructureSuperpositionOptions = PD.ValuesFor<typeof StructureSuperpositionParams>
+
+const SuperpositionTag = 'SuperpositionTransform';
+
+type SuperpositionControlsState = {
+    isBusy: boolean,
+    action?: 'byChains' | 'byAtoms' | 'options',
+    options: StructureSuperpositionOptions
+}
+
+interface LociEntry {
+    loci: StructureElement.Loci,
+    label: string,
+    cell: StateObjectCell<PluginStateObject.Molecule.Structure>
+};
+
+interface AtomsLociEntry extends LociEntry {
+    atoms: StructureSelectionHistoryEntry[]
+};
+
+export class SuperpositionControls extends PurePluginUIComponent<{}, SuperpositionControlsState> {
+    state: SuperpositionControlsState = {
+        isBusy: false,
+        action: undefined,
+        options: DefaultStructureSuperpositionOptions
+    }
+
+    componentDidMount() {
+        this.subscribe(this.selection.events.changed, () => {
+            this.forceUpdate();
+        });
+
+        this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
+            this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
+            this.setState({ isBusy: v });
+        });
+    }
+
+    get selection() {
+        return this.plugin.managers.structure.selection;
+    }
+
+    async transform(s: StateObjectRef<PluginStateObject.Molecule.Structure>, matrix: Mat4) {
+        const r = StateObjectRef.resolveAndCheck(this.plugin.state.data, s);
+        if (!r) return;
+        const o = StateSelection.findTagInSubtree(this.plugin.state.data.tree, r.transform.ref, SuperpositionTag);
+        const params = {
+            transform: {
+                name: 'matrix' as const,
+                params: { data: matrix, transpose: false }
+            }
+        };
+        // TODO add .insertOrUpdate to StateBuilder?
+        const b = o
+            ? this.plugin.state.data.build().to(o).update(params)
+            : this.plugin.state.data.build().to(s)
+                .insert(StateTransforms.Model.TransformStructureConformation, params, { tags: SuperpositionTag });
+        await this.plugin.runTask(this.plugin.state.data.updateTree(b));
+    }
+
+    superposeChains = async () => {
+        const { query } = StructureSelectionQueries.trace;
+        const entries = this.chainEntries;
+
+        const traceLocis: StructureElement.Loci[] = [];
+        for (const e of entries) {
+            const s = StructureElement.Loci.toStructure(e.loci);
+            const loci = StructureSelection.toLociWithSourceUnits(query(new QueryContext(s)));
+            traceLocis.push(loci);
+        }
+
+        const transforms = this.state.options.alignSequences
+            ? alignAndSuperpose(traceLocis)
+            : superpose(traceLocis);
+
+        const eA = entries[0];
+        for (let i = 1, il = traceLocis.length; i < il; ++i) {
+            const eB = entries[i];
+            const { bTransform, rmsd } = transforms[i - 1];
+            await this.transform(eB.cell, bTransform);
+            const labelA = stripTags(eA.label);
+            const labelB = stripTags(eB.label);
+            this.plugin.log.info(`Superposed [${labelA}] and [${labelB}] with RMSD ${rmsd.toFixed(2)}.`);
+        }
+    }
+
+    superposeAtoms = async () => {
+        const entries = this.atomEntries;
+
+        const atomLocis = entries.map(e => e.loci);
+        const transforms = superpose(atomLocis);
+
+        const eA = entries[0];
+        for (let i = 1, il = atomLocis.length; i < il; ++i) {
+            const eB = entries[i];
+            const { bTransform, rmsd } = transforms[i - 1];
+            await this.transform(eB.cell, bTransform);
+            const labelA = stripTags(eA.label);
+            const labelB = stripTags(eB.label);
+            this.plugin.log.info(`Superposed [${labelA}] and [${labelB}] with RMSD ${rmsd.toFixed(2)}.`);
+        }
+    }
+
+    toggleByChains = () => this.setState({ action: this.state.action === 'byChains' ? void 0 : 'byChains' });
+    toggleByAtoms = () => this.setState({ action: this.state.action === 'byAtoms' ? void 0 : 'byAtoms' });
+    toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options' });
+
+    highlight(loci: StructureElement.Loci) {
+        this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci }, false);
+    }
+
+    moveHistory(e: StructureSelectionHistoryEntry, direction: 'up' | 'down') {
+        this.plugin.managers.structure.selection.modifyHistory(e, direction);
+    }
+
+    focusLoci(loci: StructureElement.Loci) {
+        this.plugin.managers.camera.focusLoci(loci);
+    }
+
+    lociEntry(e: LociEntry, idx: number) {
+        return <div className='msp-flex-row' key={idx}>
+            <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
+                <span dangerouslySetInnerHTML={{ __html: e.label }} />
+            </Button>
+        </div>;
+    }
+
+    historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
+        const history = this.plugin.managers.structure.selection.additionsHistory;
+        return <div className='msp-flex-row' key={e.id}>
+            <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
+                {idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
+            </Button>
+            {history.length > 1 && <IconButton svg={ArrowUpward} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />}
+            {history.length > 1 && <IconButton svg={ArrowDownward} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'down')} flex='20px' title={'Move down'} />}
+            <IconButton svg={DeleteOutlined} small={true} className='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} flex title={'Remove'} />
+        </div>;
+    }
+
+    atomsLociEntry(e: AtomsLociEntry, idx: number) {
+        return <div key={idx}>
+            <div className='msp-control-group-header'>
+                <div className='msp-no-overflow' title={e.label}>{e.label}</div>
+            </div>
+            <div className='msp-control-offset'>
+                {e.atoms.map((h, i) => this.historyEntry(h, i))}
+            </div>
+        </div>;
+    }
+
+    get chainEntries() {
+        const entries: LociEntry[] = [];
+        this.plugin.managers.structure.selection.entries.forEach(({ selection }, ref) => {
+            const cell = StateObjectRef.resolveAndCheck(this.plugin.state.data, ref);
+            if (!cell || StructureElement.Loci.isEmpty(selection)) return;
+
+            // only single chain selections
+            // TODO wrongly assumes unit is equal to chain
+            if (selection.elements.length > 1) return;
+
+            const stats = StructureElement.Stats.ofLoci(selection);
+            const counts = structureElementStatsLabel(stats, { countsOnly: true });
+            const chain = elementLabel(stats.firstElementLoc, { reverse: true, granularity: 'chain' }).split('|');
+            const label = `${counts} | ${chain[0]} | ${chain[chain.length - 1]}`;
+            entries.push({ loci: selection, label, cell });
+        });
+        return entries;
+    }
+
+    get atomEntries() {
+        // TODO have stable order of structureEntries, independent of history order
+        const structureEntries = new Map<Structure, StructureSelectionHistoryEntry[]>();
+        const history = this.plugin.managers.structure.selection.additionsHistory;
+
+        for (let i = 0, il = history.length; i < il; ++i) {
+            const e = history[i];
+            if (StructureElement.Loci.size(e.loci) !== 1) continue;
+
+            const k = e.loci.structure;
+            if (structureEntries.has(k)) structureEntries.get(k)!.push(e);
+            else structureEntries.set(k, [e]);
+        }
+
+        const entries: AtomsLociEntry[] = [];
+        structureEntries.forEach((atoms, structure) => {
+            const cell = this.plugin.helpers.substructureParent.get(structure);
+            const parent = cell?.obj?.data;
+            if (!cell || !parent) return;
+
+            const elements: StructureElement.Loci['elements'][0][] = [];
+            for (let i = 0, il = atoms.length; i < il; ++i) {
+                // note, we don't do loci union here to keep order of selected atoms
+                elements.push(atoms[i].loci.elements[0]);
+            }
+
+            const loci = StructureElement.Loci(parent, elements);
+            const label = `${loci.structure.label}`;
+            entries.push({ loci, label, cell, atoms });
+        });
+        return entries;
+    }
+
+    addByChains() {
+        const entries = this.chainEntries;
+        return <>
+            {entries.length > 0 && <div className='msp-control-offset'>
+                {entries.map((e, i) => this.lociEntry(e, i))}
+            </div>}
+            {entries.length < 2 && <div className='msp-control-offset msp-help-text'>
+                <div className='msp-help-description'><Icon svg={HelpOutline} inline />Add 2 or more selections from separate structures. Selections must be limited to single chains or parts of single chains.</div>
+            </div>}
+            {entries.length > 1 && <Button title='Superpose structures by selected chains.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeChains} style={{ marginTop: '1px' }}>
+                Superpose
+            </Button>}
+        </>;
+    }
+
+    addByAtoms() {
+        const entries = this.atomEntries;
+        return <>
+            {entries.length > 0 && <div className='msp-control-offset'>
+                {entries.map((e, i) => this.atomsLociEntry(e, i))}
+            </div>}
+            {entries.length < 2 && <div className='msp-control-offset msp-help-text'>
+                <div className='msp-help-description'><Icon svg={HelpOutline} inline />Add 1 or more selections from separate structures. Selections must be limited to single atoms.</div>
+            </div>}
+            {entries.length > 1 && <Button title='Superpose structures by selected atoms.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeAtoms} style={{ marginTop: '1px' }}>
+                Superpose
+            </Button>}
+        </>;
+    }
+
+    private setOptions = (values: StructureSuperpositionOptions) => {
+        this.setState({ options: values });
+    }
+
+    render() {
+        return <>
+            <div className='msp-flex-row'>
+                <ToggleButton icon={LinearScaleIcon} label='By Chains' toggle={this.toggleByChains} isSelected={this.state.action === 'byChains'} disabled={this.state.isBusy} />
+                <ToggleButton icon={ScatterPlotIcon} label='By Atoms' toggle={this.toggleByAtoms} isSelected={this.state.action === 'byAtoms'} disabled={this.state.isBusy} />
+                <ToggleButton icon={Tune} label='' title='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} style={{ flex: '0 0 40px', padding: 0 }} />
+            </div>
+            {this.state.action === 'byChains' && this.addByChains()}
+            {this.state.action === 'byAtoms' && this.addByAtoms()}
+            {this.state.action === 'options' && <div className='msp-control-offset'>
+                <ParameterControls params={StructureSuperpositionParams} values={this.state.options} onChangeValues={this.setOptions} isDisabled={this.state.isBusy} />
+            </div>}
+        </>;
+    }
+}