Sfoglia il codice sorgente

Merge pull request #526 from MadCatX/ntc_tube_2

Add NtC Tube representation
Alexander Rose 2 anni fa
parent
commit
94e9462af8

+ 3 - 0
CHANGELOG.md

@@ -29,6 +29,9 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add `.getCenter` and `.center` to `Camera`
 - Add support to dim unmarked groups
 - Add support for marker edge strength
+- Factor out common code in `Dnatco` extension
+- Add `NtC tube` visual. Applicable for structures with NtC annotation
+- [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs`
 
 ## [v3.28.0] - 2022-12-20
 

+ 2 - 2
src/apps/viewer/app.ts

@@ -7,7 +7,7 @@
 
 import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
 import { CellPack } from '../../extensions/cellpack';
-import { DnatcoConfalPyramids } from '../../extensions/dnatco';
+import { DnatcoNtCs } from '../../extensions/dnatco';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
 import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
 import { GeometryExport } from '../../extensions/geo-export';
@@ -60,7 +60,7 @@ const Extensions = {
     'volseg': PluginSpec.Behavior(Volseg),
     'backgrounds': PluginSpec.Behavior(Backgrounds),
     'cellpack': PluginSpec.Behavior(CellPack),
-    'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
+    'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
     'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
     'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
     'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),

+ 55 - 0
src/extensions/dnatco/behavior.ts

@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ConfalPyramidsPreset } from './confal-pyramids/behavior';
+import { ConfalPyramidsColorThemeProvider } from './confal-pyramids/color';
+import { ConfalPyramidsProvider } from './confal-pyramids/property';
+import { ConfalPyramidsRepresentationProvider } from './confal-pyramids/representation';
+import { NtCTubePreset } from './ntc-tube/behavior';
+import { NtCTubeColorThemeProvider } from './ntc-tube/color';
+import { NtCTubeProvider } from './ntc-tube/property';
+import { NtCTubeRepresentationProvider } from './ntc-tube/representation';
+
+
+export const DnatcoNtCs = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
+    name: 'dnatco-ntcs',
+    category: 'custom-props',
+    display: {
+        name: 'DNATCO NtC Annotations',
+        description: 'DNATCO NtC Annotations',
+    },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
+        register(): void {
+            this.ctx.customModelProperties.register(ConfalPyramidsProvider, this.params.autoAttach);
+            this.ctx.customModelProperties.register(NtCTubeProvider, this.params.autoAttach);
+
+            this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
+            this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
+            this.ctx.representation.structure.themes.colorThemeRegistry.add(NtCTubeColorThemeProvider);
+            this.ctx.representation.structure.registry.add(NtCTubeRepresentationProvider);
+
+            this.ctx.builders.structure.representation.registerPreset(ConfalPyramidsPreset);
+            this.ctx.builders.structure.representation.registerPreset(NtCTubePreset);
+        }
+
+        unregister() {
+            this.ctx.customModelProperties.unregister(NtCTubeProvider.descriptor.name);
+
+            this.ctx.representation.structure.registry.remove(NtCTubeRepresentationProvider);
+            this.ctx.representation.structure.themes.colorThemeRegistry.remove(NtCTubeColorThemeProvider);
+
+            this.ctx.builders.structure.representation.unregisterPreset(NtCTubePreset);
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(true),
+        showToolTip: PD.Boolean(true)
+    })
+});
+

+ 219 - 0
src/extensions/dnatco/color.ts

@@ -0,0 +1,219 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { Color, ColorMap } from '../../mol-util/color';
+
+export const DefaultNtCClassColors = {
+    A: 0xFFC1C1,
+    B: 0xC8CFFF,
+    BII: 0x0059DA,
+    miB: 0x3BE8FB,
+    Z: 0x01F60E,
+    IC: 0xFA5CFB,
+    OPN: 0xE90000,
+    SYN: 0xFFFF01,
+    N: 0xF2F2F2,
+};
+export const ErrorColor = Color(0xFFA10A);
+
+export const NtCColors = ColorMap({
+    NANT_Upr: DefaultNtCClassColors.N,
+    NANT_Lwr: DefaultNtCClassColors.N,
+    AA00_Upr: DefaultNtCClassColors.A,
+    AA00_Lwr: DefaultNtCClassColors.A,
+    AA02_Upr: DefaultNtCClassColors.A,
+    AA02_Lwr: DefaultNtCClassColors.A,
+    AA03_Upr: DefaultNtCClassColors.A,
+    AA03_Lwr: DefaultNtCClassColors.A,
+    AA04_Upr: DefaultNtCClassColors.A,
+    AA04_Lwr: DefaultNtCClassColors.A,
+    AA08_Upr: DefaultNtCClassColors.A,
+    AA08_Lwr: DefaultNtCClassColors.A,
+    AA09_Upr: DefaultNtCClassColors.A,
+    AA09_Lwr: DefaultNtCClassColors.A,
+    AA01_Upr: DefaultNtCClassColors.A,
+    AA01_Lwr: DefaultNtCClassColors.A,
+    AA05_Upr: DefaultNtCClassColors.A,
+    AA05_Lwr: DefaultNtCClassColors.A,
+    AA06_Upr: DefaultNtCClassColors.A,
+    AA06_Lwr: DefaultNtCClassColors.A,
+    AA10_Upr: DefaultNtCClassColors.A,
+    AA10_Lwr: DefaultNtCClassColors.A,
+    AA11_Upr: DefaultNtCClassColors.A,
+    AA11_Lwr: DefaultNtCClassColors.A,
+    AA07_Upr: DefaultNtCClassColors.A,
+    AA07_Lwr: DefaultNtCClassColors.A,
+    AA12_Upr: DefaultNtCClassColors.A,
+    AA12_Lwr: DefaultNtCClassColors.A,
+    AA13_Upr: DefaultNtCClassColors.A,
+    AA13_Lwr: DefaultNtCClassColors.A,
+    AB01_Upr: DefaultNtCClassColors.A,
+    AB01_Lwr: DefaultNtCClassColors.B,
+    AB02_Upr: DefaultNtCClassColors.A,
+    AB02_Lwr: DefaultNtCClassColors.B,
+    AB03_Upr: DefaultNtCClassColors.A,
+    AB03_Lwr: DefaultNtCClassColors.B,
+    AB04_Upr: DefaultNtCClassColors.A,
+    AB04_Lwr: DefaultNtCClassColors.B,
+    AB05_Upr: DefaultNtCClassColors.A,
+    AB05_Lwr: DefaultNtCClassColors.B,
+    BA01_Upr: DefaultNtCClassColors.B,
+    BA01_Lwr: DefaultNtCClassColors.A,
+    BA05_Upr: DefaultNtCClassColors.B,
+    BA05_Lwr: DefaultNtCClassColors.A,
+    BA09_Upr: DefaultNtCClassColors.B,
+    BA09_Lwr: DefaultNtCClassColors.A,
+    BA08_Upr: DefaultNtCClassColors.BII,
+    BA08_Lwr: DefaultNtCClassColors.A,
+    BA10_Upr: DefaultNtCClassColors.B,
+    BA10_Lwr: DefaultNtCClassColors.A,
+    BA13_Upr: DefaultNtCClassColors.BII,
+    BA13_Lwr: DefaultNtCClassColors.A,
+    BA16_Upr: DefaultNtCClassColors.BII,
+    BA16_Lwr: DefaultNtCClassColors.A,
+    BA17_Upr: DefaultNtCClassColors.BII,
+    BA17_Lwr: DefaultNtCClassColors.A,
+    BB00_Upr: DefaultNtCClassColors.B,
+    BB00_Lwr: DefaultNtCClassColors.B,
+    BB01_Upr: DefaultNtCClassColors.B,
+    BB01_Lwr: DefaultNtCClassColors.B,
+    BB17_Upr: DefaultNtCClassColors.B,
+    BB17_Lwr: DefaultNtCClassColors.B,
+    BB02_Upr: DefaultNtCClassColors.B,
+    BB02_Lwr: DefaultNtCClassColors.B,
+    BB03_Upr: DefaultNtCClassColors.B,
+    BB03_Lwr: DefaultNtCClassColors.B,
+    BB11_Upr: DefaultNtCClassColors.B,
+    BB11_Lwr: DefaultNtCClassColors.B,
+    BB16_Upr: DefaultNtCClassColors.B,
+    BB16_Lwr: DefaultNtCClassColors.B,
+    BB04_Upr: DefaultNtCClassColors.B,
+    BB04_Lwr: DefaultNtCClassColors.BII,
+    BB05_Upr: DefaultNtCClassColors.B,
+    BB05_Lwr: DefaultNtCClassColors.BII,
+    BB07_Upr: DefaultNtCClassColors.BII,
+    BB07_Lwr: DefaultNtCClassColors.BII,
+    BB08_Upr: DefaultNtCClassColors.BII,
+    BB08_Lwr: DefaultNtCClassColors.BII,
+    BB10_Upr: DefaultNtCClassColors.miB,
+    BB10_Lwr: DefaultNtCClassColors.miB,
+    BB12_Upr: DefaultNtCClassColors.miB,
+    BB12_Lwr: DefaultNtCClassColors.miB,
+    BB13_Upr: DefaultNtCClassColors.miB,
+    BB13_Lwr: DefaultNtCClassColors.miB,
+    BB14_Upr: DefaultNtCClassColors.miB,
+    BB14_Lwr: DefaultNtCClassColors.miB,
+    BB15_Upr: DefaultNtCClassColors.miB,
+    BB15_Lwr: DefaultNtCClassColors.miB,
+    BB20_Upr: DefaultNtCClassColors.miB,
+    BB20_Lwr: DefaultNtCClassColors.miB,
+    IC01_Upr: DefaultNtCClassColors.IC,
+    IC01_Lwr: DefaultNtCClassColors.IC,
+    IC02_Upr: DefaultNtCClassColors.IC,
+    IC02_Lwr: DefaultNtCClassColors.IC,
+    IC03_Upr: DefaultNtCClassColors.IC,
+    IC03_Lwr: DefaultNtCClassColors.IC,
+    IC04_Upr: DefaultNtCClassColors.IC,
+    IC04_Lwr: DefaultNtCClassColors.IC,
+    IC05_Upr: DefaultNtCClassColors.IC,
+    IC05_Lwr: DefaultNtCClassColors.IC,
+    IC06_Upr: DefaultNtCClassColors.IC,
+    IC06_Lwr: DefaultNtCClassColors.IC,
+    IC07_Upr: DefaultNtCClassColors.IC,
+    IC07_Lwr: DefaultNtCClassColors.IC,
+    OP01_Upr: DefaultNtCClassColors.OPN,
+    OP01_Lwr: DefaultNtCClassColors.OPN,
+    OP02_Upr: DefaultNtCClassColors.OPN,
+    OP02_Lwr: DefaultNtCClassColors.OPN,
+    OP03_Upr: DefaultNtCClassColors.OPN,
+    OP03_Lwr: DefaultNtCClassColors.OPN,
+    OP04_Upr: DefaultNtCClassColors.OPN,
+    OP04_Lwr: DefaultNtCClassColors.OPN,
+    OP05_Upr: DefaultNtCClassColors.OPN,
+    OP05_Lwr: DefaultNtCClassColors.OPN,
+    OP06_Upr: DefaultNtCClassColors.OPN,
+    OP06_Lwr: DefaultNtCClassColors.OPN,
+    OP07_Upr: DefaultNtCClassColors.OPN,
+    OP07_Lwr: DefaultNtCClassColors.OPN,
+    OP08_Upr: DefaultNtCClassColors.OPN,
+    OP08_Lwr: DefaultNtCClassColors.OPN,
+    OP09_Upr: DefaultNtCClassColors.OPN,
+    OP09_Lwr: DefaultNtCClassColors.OPN,
+    OP10_Upr: DefaultNtCClassColors.OPN,
+    OP10_Lwr: DefaultNtCClassColors.OPN,
+    OP11_Upr: DefaultNtCClassColors.OPN,
+    OP11_Lwr: DefaultNtCClassColors.OPN,
+    OP12_Upr: DefaultNtCClassColors.OPN,
+    OP12_Lwr: DefaultNtCClassColors.OPN,
+    OP13_Upr: DefaultNtCClassColors.OPN,
+    OP13_Lwr: DefaultNtCClassColors.OPN,
+    OP14_Upr: DefaultNtCClassColors.OPN,
+    OP14_Lwr: DefaultNtCClassColors.OPN,
+    OP15_Upr: DefaultNtCClassColors.OPN,
+    OP15_Lwr: DefaultNtCClassColors.OPN,
+    OP16_Upr: DefaultNtCClassColors.OPN,
+    OP16_Lwr: DefaultNtCClassColors.OPN,
+    OP17_Upr: DefaultNtCClassColors.OPN,
+    OP17_Lwr: DefaultNtCClassColors.OPN,
+    OP18_Upr: DefaultNtCClassColors.OPN,
+    OP18_Lwr: DefaultNtCClassColors.OPN,
+    OP19_Upr: DefaultNtCClassColors.OPN,
+    OP19_Lwr: DefaultNtCClassColors.OPN,
+    OP20_Upr: DefaultNtCClassColors.OPN,
+    OP20_Lwr: DefaultNtCClassColors.OPN,
+    OP21_Upr: DefaultNtCClassColors.OPN,
+    OP21_Lwr: DefaultNtCClassColors.OPN,
+    OP22_Upr: DefaultNtCClassColors.OPN,
+    OP22_Lwr: DefaultNtCClassColors.OPN,
+    OP23_Upr: DefaultNtCClassColors.OPN,
+    OP23_Lwr: DefaultNtCClassColors.OPN,
+    OP24_Upr: DefaultNtCClassColors.OPN,
+    OP24_Lwr: DefaultNtCClassColors.OPN,
+    OP25_Upr: DefaultNtCClassColors.OPN,
+    OP25_Lwr: DefaultNtCClassColors.OPN,
+    OP26_Upr: DefaultNtCClassColors.OPN,
+    OP26_Lwr: DefaultNtCClassColors.OPN,
+    OP27_Upr: DefaultNtCClassColors.OPN,
+    OP27_Lwr: DefaultNtCClassColors.OPN,
+    OP28_Upr: DefaultNtCClassColors.OPN,
+    OP28_Lwr: DefaultNtCClassColors.OPN,
+    OP29_Upr: DefaultNtCClassColors.OPN,
+    OP29_Lwr: DefaultNtCClassColors.OPN,
+    OP30_Upr: DefaultNtCClassColors.OPN,
+    OP30_Lwr: DefaultNtCClassColors.OPN,
+    OP31_Upr: DefaultNtCClassColors.OPN,
+    OP31_Lwr: DefaultNtCClassColors.OPN,
+    OPS1_Upr: DefaultNtCClassColors.OPN,
+    OPS1_Lwr: DefaultNtCClassColors.OPN,
+    OP1S_Upr: DefaultNtCClassColors.OPN,
+    OP1S_Lwr: DefaultNtCClassColors.SYN,
+    AAS1_Upr: DefaultNtCClassColors.SYN,
+    AAS1_Lwr: DefaultNtCClassColors.A,
+    AB1S_Upr: DefaultNtCClassColors.A,
+    AB1S_Lwr: DefaultNtCClassColors.SYN,
+    AB2S_Upr: DefaultNtCClassColors.A,
+    AB2S_Lwr: DefaultNtCClassColors.SYN,
+    BB1S_Upr: DefaultNtCClassColors.B,
+    BB1S_Lwr: DefaultNtCClassColors.SYN,
+    BB2S_Upr: DefaultNtCClassColors.B,
+    BB2S_Lwr: DefaultNtCClassColors.SYN,
+    BBS1_Upr: DefaultNtCClassColors.SYN,
+    BBS1_Lwr: DefaultNtCClassColors.B,
+    ZZ01_Upr: DefaultNtCClassColors.Z,
+    ZZ01_Lwr: DefaultNtCClassColors.Z,
+    ZZ02_Upr: DefaultNtCClassColors.Z,
+    ZZ02_Lwr: DefaultNtCClassColors.Z,
+    ZZ1S_Upr: DefaultNtCClassColors.Z,
+    ZZ1S_Lwr: DefaultNtCClassColors.SYN,
+    ZZ2S_Upr: DefaultNtCClassColors.Z,
+    ZZ2S_Lwr: DefaultNtCClassColors.SYN,
+    ZZS1_Upr: DefaultNtCClassColors.SYN,
+    ZZS1_Lwr: DefaultNtCClassColors.Z,
+    ZZS2_Upr: DefaultNtCClassColors.SYN,
+    ZZS2_Lwr: DefaultNtCClassColors.Z,
+});
+

+ 6 - 50
src/extensions/dnatco/confal-pyramids/behavior.ts

@@ -6,23 +6,22 @@
  */
 
 import { ConfalPyramidsColorThemeProvider } from './color';
-import { ConfalPyramids, ConfalPyramidsProvider } from './property';
+import { ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsRepresentationProvider } from './representation';
-import { ConfalPyramidsTypes } from './types';
-import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
+import { Dnatco } from '../property';
+import { DnatcoTypes } from '../types';
 import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
 import { StateObjectRef } from '../../../mol-state';
 import { Task } from '../../../mol-task';
-import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 
-export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({
+export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-confal-pyramids',
     display: {
         name: 'Confal Pyramids', group: 'Annotation',
         description: 'Schematic depiction of conformer class and confal value.',
     },
     isApplicable(a) {
-        return a.data.models.length >= 1 && a.data.models.some(m => ConfalPyramids.isApplicable(m));
+        return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
     },
     params: () => StructureRepresentationPresetProvider.CommonParams,
     async apply(ref, params, plugin) {
@@ -48,50 +47,7 @@ export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider(
     }
 });
 
-export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
-    name: 'dnatco-confal-pyramids-prop',
-    category: 'custom-props',
-    display: {
-        name: 'Confal Pyramids',
-        description: 'Schematic depiction of conformer class and confal value.',
-    },
-    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
-        private provider = ConfalPyramidsProvider;
-
-        register(): void {
-            this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
-
-            this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
-            this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
-
-            this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset);
-        }
-
-        update(p: { autoAttach: boolean, showToolTip: boolean }) {
-            const updated = this.params.autoAttach !== p.autoAttach;
-            this.params.autoAttach = p.autoAttach;
-            this.params.showToolTip = p.showToolTip;
-            this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
-            return updated;
-        }
-
-        unregister() {
-            this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
-
-            this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
-            this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
-
-            this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset);
-        }
-    },
-    params: () => ({
-        autoAttach: PD.Boolean(true),
-        showToolTip: PD.Boolean(true)
-    })
-});
-
-export function confalPyramidLabel(halfPyramid: ConfalPyramidsTypes.HalfPyramid) {
-    const { step } = halfPyramid;
+export function confalPyramidLabel(step: DnatcoTypes.Step) {
     return `
         <b>${step.auth_asym_id_1}</b> |
         <b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}

+ 5 - 211
src/extensions/dnatco/confal-pyramids/color.ts

@@ -5,8 +5,10 @@
  * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
  */
 
-import { ConfalPyramids, ConfalPyramidsProvider } from './property';
+import { ErrorColor, NtCColors } from '../color';
+import { ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsTypes as CPT } from './types';
+import { Dnatco } from '../property';
 import { Location } from '../../../mol-model/location';
 import { CustomProperty } from '../../../mol-model-props/common/custom-property';
 import { ColorTheme } from '../../../mol-theme/color';
@@ -19,215 +21,7 @@ import { ObjectKeys } from '../../../mol-util/type-helpers';
 
 const Description = 'Assigns colors to confal pyramids';
 
-const DefaultClassColors = {
-    A: 0xFFC1C1,
-    B: 0xC8CFFF,
-    BII: 0x0059DA,
-    miB: 0x3BE8FB,
-    Z: 0x01F60E,
-    IC: 0xFA5CFB,
-    OPN: 0xE90000,
-    SYN: 0xFFFF01,
-    N: 0xF2F2F2,
-};
-const ErrorColor = Color(0xFFA10A);
-
-const PyramidsColors = ColorMap({
-    NANT_Upr: DefaultClassColors.N,
-    NANT_Lwr: DefaultClassColors.N,
-    AA00_Upr: DefaultClassColors.A,
-    AA00_Lwr: DefaultClassColors.A,
-    AA02_Upr: DefaultClassColors.A,
-    AA02_Lwr: DefaultClassColors.A,
-    AA03_Upr: DefaultClassColors.A,
-    AA03_Lwr: DefaultClassColors.A,
-    AA04_Upr: DefaultClassColors.A,
-    AA04_Lwr: DefaultClassColors.A,
-    AA08_Upr: DefaultClassColors.A,
-    AA08_Lwr: DefaultClassColors.A,
-    AA09_Upr: DefaultClassColors.A,
-    AA09_Lwr: DefaultClassColors.A,
-    AA01_Upr: DefaultClassColors.A,
-    AA01_Lwr: DefaultClassColors.A,
-    AA05_Upr: DefaultClassColors.A,
-    AA05_Lwr: DefaultClassColors.A,
-    AA06_Upr: DefaultClassColors.A,
-    AA06_Lwr: DefaultClassColors.A,
-    AA10_Upr: DefaultClassColors.A,
-    AA10_Lwr: DefaultClassColors.A,
-    AA11_Upr: DefaultClassColors.A,
-    AA11_Lwr: DefaultClassColors.A,
-    AA07_Upr: DefaultClassColors.A,
-    AA07_Lwr: DefaultClassColors.A,
-    AA12_Upr: DefaultClassColors.A,
-    AA12_Lwr: DefaultClassColors.A,
-    AA13_Upr: DefaultClassColors.A,
-    AA13_Lwr: DefaultClassColors.A,
-    AB01_Upr: DefaultClassColors.A,
-    AB01_Lwr: DefaultClassColors.B,
-    AB02_Upr: DefaultClassColors.A,
-    AB02_Lwr: DefaultClassColors.B,
-    AB03_Upr: DefaultClassColors.A,
-    AB03_Lwr: DefaultClassColors.B,
-    AB04_Upr: DefaultClassColors.A,
-    AB04_Lwr: DefaultClassColors.B,
-    AB05_Upr: DefaultClassColors.A,
-    AB05_Lwr: DefaultClassColors.B,
-    BA01_Upr: DefaultClassColors.B,
-    BA01_Lwr: DefaultClassColors.A,
-    BA05_Upr: DefaultClassColors.B,
-    BA05_Lwr: DefaultClassColors.A,
-    BA09_Upr: DefaultClassColors.B,
-    BA09_Lwr: DefaultClassColors.A,
-    BA08_Upr: DefaultClassColors.BII,
-    BA08_Lwr: DefaultClassColors.A,
-    BA10_Upr: DefaultClassColors.B,
-    BA10_Lwr: DefaultClassColors.A,
-    BA13_Upr: DefaultClassColors.BII,
-    BA13_Lwr: DefaultClassColors.A,
-    BA16_Upr: DefaultClassColors.BII,
-    BA16_Lwr: DefaultClassColors.A,
-    BA17_Upr: DefaultClassColors.BII,
-    BA17_Lwr: DefaultClassColors.A,
-    BB00_Upr: DefaultClassColors.B,
-    BB00_Lwr: DefaultClassColors.B,
-    BB01_Upr: DefaultClassColors.B,
-    BB01_Lwr: DefaultClassColors.B,
-    BB17_Upr: DefaultClassColors.B,
-    BB17_Lwr: DefaultClassColors.B,
-    BB02_Upr: DefaultClassColors.B,
-    BB02_Lwr: DefaultClassColors.B,
-    BB03_Upr: DefaultClassColors.B,
-    BB03_Lwr: DefaultClassColors.B,
-    BB11_Upr: DefaultClassColors.B,
-    BB11_Lwr: DefaultClassColors.B,
-    BB16_Upr: DefaultClassColors.B,
-    BB16_Lwr: DefaultClassColors.B,
-    BB04_Upr: DefaultClassColors.B,
-    BB04_Lwr: DefaultClassColors.BII,
-    BB05_Upr: DefaultClassColors.B,
-    BB05_Lwr: DefaultClassColors.BII,
-    BB07_Upr: DefaultClassColors.BII,
-    BB07_Lwr: DefaultClassColors.BII,
-    BB08_Upr: DefaultClassColors.BII,
-    BB08_Lwr: DefaultClassColors.BII,
-    BB10_Upr: DefaultClassColors.miB,
-    BB10_Lwr: DefaultClassColors.miB,
-    BB12_Upr: DefaultClassColors.miB,
-    BB12_Lwr: DefaultClassColors.miB,
-    BB13_Upr: DefaultClassColors.miB,
-    BB13_Lwr: DefaultClassColors.miB,
-    BB14_Upr: DefaultClassColors.miB,
-    BB14_Lwr: DefaultClassColors.miB,
-    BB15_Upr: DefaultClassColors.miB,
-    BB15_Lwr: DefaultClassColors.miB,
-    BB20_Upr: DefaultClassColors.miB,
-    BB20_Lwr: DefaultClassColors.miB,
-    IC01_Upr: DefaultClassColors.IC,
-    IC01_Lwr: DefaultClassColors.IC,
-    IC02_Upr: DefaultClassColors.IC,
-    IC02_Lwr: DefaultClassColors.IC,
-    IC03_Upr: DefaultClassColors.IC,
-    IC03_Lwr: DefaultClassColors.IC,
-    IC04_Upr: DefaultClassColors.IC,
-    IC04_Lwr: DefaultClassColors.IC,
-    IC05_Upr: DefaultClassColors.IC,
-    IC05_Lwr: DefaultClassColors.IC,
-    IC06_Upr: DefaultClassColors.IC,
-    IC06_Lwr: DefaultClassColors.IC,
-    IC07_Upr: DefaultClassColors.IC,
-    IC07_Lwr: DefaultClassColors.IC,
-    OP01_Upr: DefaultClassColors.OPN,
-    OP01_Lwr: DefaultClassColors.OPN,
-    OP02_Upr: DefaultClassColors.OPN,
-    OP02_Lwr: DefaultClassColors.OPN,
-    OP03_Upr: DefaultClassColors.OPN,
-    OP03_Lwr: DefaultClassColors.OPN,
-    OP04_Upr: DefaultClassColors.OPN,
-    OP04_Lwr: DefaultClassColors.OPN,
-    OP05_Upr: DefaultClassColors.OPN,
-    OP05_Lwr: DefaultClassColors.OPN,
-    OP06_Upr: DefaultClassColors.OPN,
-    OP06_Lwr: DefaultClassColors.OPN,
-    OP07_Upr: DefaultClassColors.OPN,
-    OP07_Lwr: DefaultClassColors.OPN,
-    OP08_Upr: DefaultClassColors.OPN,
-    OP08_Lwr: DefaultClassColors.OPN,
-    OP09_Upr: DefaultClassColors.OPN,
-    OP09_Lwr: DefaultClassColors.OPN,
-    OP10_Upr: DefaultClassColors.OPN,
-    OP10_Lwr: DefaultClassColors.OPN,
-    OP11_Upr: DefaultClassColors.OPN,
-    OP11_Lwr: DefaultClassColors.OPN,
-    OP12_Upr: DefaultClassColors.OPN,
-    OP12_Lwr: DefaultClassColors.OPN,
-    OP13_Upr: DefaultClassColors.OPN,
-    OP13_Lwr: DefaultClassColors.OPN,
-    OP14_Upr: DefaultClassColors.OPN,
-    OP14_Lwr: DefaultClassColors.OPN,
-    OP15_Upr: DefaultClassColors.OPN,
-    OP15_Lwr: DefaultClassColors.OPN,
-    OP16_Upr: DefaultClassColors.OPN,
-    OP16_Lwr: DefaultClassColors.OPN,
-    OP17_Upr: DefaultClassColors.OPN,
-    OP17_Lwr: DefaultClassColors.OPN,
-    OP18_Upr: DefaultClassColors.OPN,
-    OP18_Lwr: DefaultClassColors.OPN,
-    OP19_Upr: DefaultClassColors.OPN,
-    OP19_Lwr: DefaultClassColors.OPN,
-    OP20_Upr: DefaultClassColors.OPN,
-    OP20_Lwr: DefaultClassColors.OPN,
-    OP21_Upr: DefaultClassColors.OPN,
-    OP21_Lwr: DefaultClassColors.OPN,
-    OP22_Upr: DefaultClassColors.OPN,
-    OP22_Lwr: DefaultClassColors.OPN,
-    OP23_Upr: DefaultClassColors.OPN,
-    OP23_Lwr: DefaultClassColors.OPN,
-    OP24_Upr: DefaultClassColors.OPN,
-    OP24_Lwr: DefaultClassColors.OPN,
-    OP25_Upr: DefaultClassColors.OPN,
-    OP25_Lwr: DefaultClassColors.OPN,
-    OP26_Upr: DefaultClassColors.OPN,
-    OP26_Lwr: DefaultClassColors.OPN,
-    OP27_Upr: DefaultClassColors.OPN,
-    OP27_Lwr: DefaultClassColors.OPN,
-    OP28_Upr: DefaultClassColors.OPN,
-    OP28_Lwr: DefaultClassColors.OPN,
-    OP29_Upr: DefaultClassColors.OPN,
-    OP29_Lwr: DefaultClassColors.OPN,
-    OP30_Upr: DefaultClassColors.OPN,
-    OP30_Lwr: DefaultClassColors.OPN,
-    OP31_Upr: DefaultClassColors.OPN,
-    OP31_Lwr: DefaultClassColors.OPN,
-    OPS1_Upr: DefaultClassColors.OPN,
-    OPS1_Lwr: DefaultClassColors.OPN,
-    OP1S_Upr: DefaultClassColors.OPN,
-    OP1S_Lwr: DefaultClassColors.SYN,
-    AAS1_Upr: DefaultClassColors.SYN,
-    AAS1_Lwr: DefaultClassColors.A,
-    AB1S_Upr: DefaultClassColors.A,
-    AB1S_Lwr: DefaultClassColors.SYN,
-    AB2S_Upr: DefaultClassColors.A,
-    AB2S_Lwr: DefaultClassColors.SYN,
-    BB1S_Upr: DefaultClassColors.B,
-    BB1S_Lwr: DefaultClassColors.SYN,
-    BB2S_Upr: DefaultClassColors.B,
-    BB2S_Lwr: DefaultClassColors.SYN,
-    BBS1_Upr: DefaultClassColors.SYN,
-    BBS1_Lwr: DefaultClassColors.B,
-    ZZ01_Upr: DefaultClassColors.Z,
-    ZZ01_Lwr: DefaultClassColors.Z,
-    ZZ02_Upr: DefaultClassColors.Z,
-    ZZ02_Lwr: DefaultClassColors.Z,
-    ZZ1S_Upr: DefaultClassColors.Z,
-    ZZ1S_Lwr: DefaultClassColors.SYN,
-    ZZ2S_Upr: DefaultClassColors.Z,
-    ZZ2S_Lwr: DefaultClassColors.SYN,
-    ZZS1_Upr: DefaultClassColors.SYN,
-    ZZS1_Lwr: DefaultClassColors.Z,
-    ZZS2_Upr: DefaultClassColors.SYN,
-    ZZS2_Lwr: DefaultClassColors.Z,
-});
+const PyramidsColors = ColorMap({ ...NtCColors });
 type PyramidsColors = typeof PyramidsColors;
 
 export const ConfalPyramidsColorThemeParams = {
@@ -272,7 +66,7 @@ export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramid
     factory: ConfalPyramidsColorTheme,
     getParams: getConfalPyramidsColorThemeParams,
     defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
     ensureCustomProperties: {
         attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
         detach: (data) => data.structure && ConfalPyramidsProvider.ref(data.structure.models[0], false)

+ 5 - 162
src/extensions/dnatco/confal-pyramids/property.ts

@@ -5,90 +5,18 @@
  * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
  */
 
-import { ConfalPyramidsTypes as CPT } from './types';
-import { Column, Table } from '../../../mol-data/db';
-import { toTable } from '../../../mol-io/reader/cif/schema';
+import { Dnatco, DnatcoParams, DnatcoSteps } from '../property';
 import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
 import { Model } from '../../../mol-model/structure';
 import { CustomProperty } from '../../../mol-model-props/common/custom-property';
 import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
-import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 
-export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
-
-export namespace ConfalPyramids {
-    export const Schema = {
-        ndb_struct_ntc_step: {
-            id: Column.Schema.int,
-            name: Column.Schema.str,
-            PDB_model_number: Column.Schema.int,
-            label_entity_id_1: Column.Schema.int,
-            label_asym_id_1: Column.Schema.str,
-            label_seq_id_1: Column.Schema.int,
-            label_comp_id_1: Column.Schema.str,
-            label_alt_id_1: Column.Schema.str,
-            label_entity_id_2: Column.Schema.int,
-            label_asym_id_2: Column.Schema.str,
-            label_seq_id_2: Column.Schema.int,
-            label_comp_id_2: Column.Schema.str,
-            label_alt_id_2: Column.Schema.str,
-            auth_asym_id_1: Column.Schema.str,
-            auth_seq_id_1: Column.Schema.int,
-            auth_asym_id_2: Column.Schema.str,
-            auth_seq_id_2: Column.Schema.int,
-            PDB_ins_code_1: Column.Schema.str,
-            PDB_ins_code_2: Column.Schema.str,
-        },
-        ndb_struct_ntc_step_summary: {
-            step_id: Column.Schema.int,
-            assigned_CANA: Column.Schema.str,
-            assigned_NtC: Column.Schema.str,
-            confal_score: Column.Schema.int,
-            euclidean_distance_NtC_ideal: Column.Schema.float,
-            cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
-            closest_CANA: Column.Schema.str,
-            closest_NtC: Column.Schema.str,
-            closest_step_golden: Column.Schema.str
-        }
-    };
-    export type Schema = typeof Schema;
-
-    export async function fromCif(ctx: CustomProperty.Context, model: Model, props: ConfalPyramidsProps): Promise<CustomProperty.Data<ConfalPyramids>> {
-        const info = PropertyWrapper.createInfo();
-        const data = getCifData(model);
-        if (data === undefined) return { value: { info, data: undefined } };
-
-        const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary);
-        return { value: { info, data: fromCif } };
-    }
-
-    function getCifData(model: Model) {
-        if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
-        if (!hasNdbStructNtcCategories(model)) return undefined;
-        return {
-            steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
-            stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
-        };
-    }
-
-    function hasNdbStructNtcCategories(model: Model): boolean {
-        if (!MmcifFormat.is(model.sourceData)) return false;
-        const names = (model.sourceData).data.frame.categoryNames;
-        return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
-    }
-
-    export function isApplicable(model?: Model): boolean {
-        return !!model && hasNdbStructNtcCategories(model);
-    }
-}
-
-export const ConfalPyramidsParams = {};
+export const ConfalPyramidsParams = { ...DnatcoParams };
 export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
 export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>;
 
-export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({
+export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, DnatcoSteps> = CustomModelProperty.createProvider({
     label: 'Confal Pyramids',
     descriptor: CustomPropertyDescriptor({
         name: 'confal_pyramids',
@@ -96,94 +24,9 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
     type: 'static',
     defaultParams: ConfalPyramidsParams,
     getParams: (data: Model) => ConfalPyramidsParams,
-    isApplicable: (data: Model) => ConfalPyramids.isApplicable(data),
+    isApplicable: (data: Model) => Dnatco.isApplicable(data),
     obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => {
         const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props };
-        return ConfalPyramids.fromCif(ctx, data, p);
+        return Dnatco.fromCif(ctx, data, p);
     }
 });
-
-type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
-
-function createPyramidsFromCif(
-    model: Model,
-    cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
-    stepsSummary: StepsSummaryTable
-): CPT.Steps {
-    const steps = new Array<CPT.Step>();
-    const mapping = new Array<CPT.MappedChains>();
-
-    const {
-        id, PDB_model_number, name,
-        auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
-        auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
-        _rowCount
-    } = cifSteps;
-
-    if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
-
-    for (let i = 0; i < _rowCount; i++) {
-        const {
-            NtC,
-            confal_score,
-            rmsd
-        } = getSummaryData(id.value(i), i, stepsSummary);
-        const modelNum = PDB_model_number.value(i);
-        const chainId = auth_asym_id_1.value(i);
-        const seqId = auth_seq_id_1.value(i);
-        const modelIdx = modelNum - 1;
-
-        if (mapping.length <= modelIdx || !mapping[modelIdx])
-            mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
-
-        const step = {
-            PDB_model_number: modelNum,
-            name: name.value(i),
-            auth_asym_id_1: chainId,
-            auth_seq_id_1: seqId,
-            label_comp_id_1: label_comp_id_1.value(i),
-            label_alt_id_1: label_alt_id_1.value(i),
-            PDB_ins_code_1: PDB_ins_code_1.value(i),
-            auth_asym_id_2: auth_asym_id_2.value(i),
-            auth_seq_id_2: auth_seq_id_2.value(i),
-            label_comp_id_2: label_comp_id_2.value(i),
-            label_alt_id_2: label_alt_id_2.value(i),
-            PDB_ins_code_2: PDB_ins_code_2.value(i),
-            confal_score,
-            NtC,
-            rmsd,
-        };
-
-        steps.push(step);
-
-        const mappedChains = mapping[modelIdx];
-        const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
-        const stepsForResidue = residuesOnChain.get(seqId) ?? [];
-        stepsForResidue.push(steps.length - 1);
-
-        residuesOnChain.set(seqId, stepsForResidue);
-        mappedChains.set(chainId, residuesOnChain);
-        mapping[modelIdx] = mappedChains;
-    }
-
-    return { steps, mapping };
-}
-
-function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
-    const {
-        step_id,
-        confal_score,
-        assigned_NtC,
-        cartesian_rmsd_closest_NtC_representative,
-    } = stepsSummary;
-
-    // Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
-    for (let j = i; j < stepsSummary._rowCount; j++) {
-        if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
-    }
-    // Safety net for cases where the previous assumption is not met
-    for (let j = 0; j < i; j++) {
-        if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
-    }
-    throw new Error('Inconsistent mmCIF data');
-}

+ 4 - 5
src/extensions/dnatco/confal-pyramids/representation.ts

@@ -5,9 +5,10 @@
  * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
  */
 
-import { ConfalPyramids, ConfalPyramidsProvider } from './property';
+import { ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsIterator } from './util';
 import { ConfalPyramidsTypes as CPT } from './types';
+import { Dnatco } from '../property';
 import { Interval } from '../../../mol-data/int';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
@@ -150,9 +151,7 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
     if (halfPyramidsCount <= groupId) return EmptyLoci;
 
     const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
-    const step = data.steps[idx];
-
-    return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
+    return CPT.Loci(data.steps, [idx]);
 }
 
 function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
@@ -197,7 +196,7 @@ export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvi
     defaultValues: PD.getDefaultValues(ConfalPyramidsParams),
     defaultColorTheme: { name: 'confal-pyramids' },
     defaultSizeTheme: { name: 'uniform' },
-    isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)),
+    isApplicable: (structure: Structure) => structure.models.some(m => Dnatco.isApplicable(m)),
     ensureCustomProperties: {
         attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true),
         detach: (data) => ConfalPyramidsProvider.ref(data.model, false),

+ 9 - 41
src/extensions/dnatco/confal-pyramids/types.ts

@@ -5,61 +5,29 @@
  * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
  */
 
+import { DnatcoTypes } from '../types';
 import { DataLocation } from '../../../mol-model/location';
 import { DataLoci } from '../../../mol-model/loci';
 import { confalPyramidLabel } from './behavior';
 
 export namespace ConfalPyramidsTypes {
-    export const DataTag = 'dnatco-confal-half-pyramid';
+    export interface Location extends DataLocation<DnatcoTypes.HalfStep, {}> {}
 
-    export type Step = {
-        PDB_model_number: number,
-        name: string,
-        auth_asym_id_1: string,
-        auth_seq_id_1: number,
-        label_comp_id_1: string,
-        label_alt_id_1: string,
-        PDB_ins_code_1: string,
-        auth_asym_id_2: string,
-        auth_seq_id_2: number,
-        label_comp_id_2: string,
-        label_alt_id_2: string,
-        PDB_ins_code_2: string,
-        confal_score: number,
-        NtC: string,
-        rmsd: number,
-    }
-
-    export type MappedChains = Map<string, MappedResidues>;
-    export type MappedResidues = Map<number, number[]>;
-
-    export interface Steps {
-        steps: Array<Step>,
-        mapping: MappedChains[],
-    }
-
-    export interface HalfPyramid {
-        step: Step,
-        isLower: boolean,
-    }
-
-    export interface Location extends DataLocation<HalfPyramid, {}> {}
-
-    export function Location(step: Step, isLower: boolean) {
-        return DataLocation(DataTag, { step, isLower }, {});
+    export function Location(step: DnatcoTypes.Step, isLower: boolean) {
+        return DataLocation(DnatcoTypes.DataTag, { step, isLower }, {});
     }
 
     export function isLocation(x: any): x is Location {
-        return !!x && x.kind === 'data-location' && x.tag === DataTag;
+        return !!x && x.kind === 'data-location' && x.tag === DnatcoTypes.DataTag;
     }
 
-    export interface Loci extends DataLoci<HalfPyramid, {}> {}
+    export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
 
-    export function Loci(data: HalfPyramid, elements: ReadonlyArray<{}>): Loci {
-        return DataLoci(DataTag, data, elements, undefined, () => confalPyramidLabel(data));
+    export function Loci(data: DnatcoTypes.Step[], elements: ReadonlyArray<number>): Loci {
+        return DataLoci(DnatcoTypes.DataTag, data, elements, undefined, () => elements[0] !== undefined ? confalPyramidLabel(data[elements[0]]) : '');
     }
 
     export function isLoci(x: any): x is Loci {
-        return !!x && x.kind === 'data-loci' && x.tag === DataTag;
+        return !!x && x.kind === 'data-loci' && x.tag === DnatcoTypes.DataTag;
     }
 }

+ 24 - 51
src/extensions/dnatco/confal-pyramids/util.ts

@@ -6,11 +6,10 @@
  */
 
 import { ConfalPyramidsProvider } from './property';
-import { ConfalPyramidsTypes as CPT } from './types';
+import { DnatcoTypes } from '../types';
+import { DnatcoUtil } from '../util';
 import { Segmentation } from '../../../mol-data/int';
-import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
-
-type Residue = Segmentation.Segment<ResidueIndex>;
+import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
 
 export type Pyramid = {
     O3: ElementIndex,
@@ -22,31 +21,17 @@ export type Pyramid = {
     stepIdx: number,
 };
 
-const EmptyStepIndices = new Array<number>();
-
-function copyResidue(r?: Residue) {
-    return r ? { index: r.index, start: r.start, end: r.end } : void 0;
-}
-
-function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
-    for (let eI = residue.start; eI < residue.end; eI++) {
-        loc.element = loc.unit.elements[eI];
-        const elName = StructureProperties.atom.label_atom_id(loc);
-        const elAltId = StructureProperties.atom.label_alt_id(loc);
-
-        if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
-            return loc.element;
-    }
-
-    return -1 as ElementIndex;
-}
-
-function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
-    const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
-    const P = getAtomIndex(loc, two, ['P'], altIdTwo);
-    const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
-    const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
-    const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
+function getPyramid(
+    loc: StructureElement.Location,
+    one: DnatcoUtil.Residue, two: DnatcoUtil.Residue,
+    altIdOne: string, altIdTwo: string,
+    insCodeOne: string, insCodeTwo: string,
+    confalScore: number, stepIdx: number): Pyramid {
+    const O3 = DnatcoUtil.getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne, insCodeOne);
+    const P = DnatcoUtil.getAtomIndex(loc, two, ['P'], altIdTwo, insCodeTwo);
+    const OP1 = DnatcoUtil.getAtomIndex(loc, two, ['OP1'], altIdTwo, insCodeTwo);
+    const OP2 = DnatcoUtil.getAtomIndex(loc, two, ['OP2'], altIdTwo, insCodeTwo);
+    const O5 = DnatcoUtil.getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo, insCodeTwo);
 
     return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
 }
@@ -54,39 +39,25 @@ function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue,
 export class ConfalPyramidsIterator {
     private chainIt: Segmentation.SegmentIterator<ChainIndex>;
     private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
-    private residueOne?: Residue;
-    private residueTwo: Residue;
-    private data?: CPT.Steps;
+    private residueOne?: DnatcoUtil.Residue;
+    private residueTwo: DnatcoUtil.Residue;
+    private data?: DnatcoTypes.Steps;
     private loc: StructureElement.Location;
 
-    private getStepIndices(r: Residue) {
-        this.loc.element = this.loc.unit.elements[r.start];
-
-        const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
-        const chainId = StructureProperties.chain.auth_asym_id(this.loc);
-        const seqId = StructureProperties.residue.auth_seq_id(this.loc);
-
-        const chains = this.data!.mapping[modelIdx];
-        if (!chains) return EmptyStepIndices;
-        const residues = chains.get(chainId);
-        if (!residues) return EmptyStepIndices;
-        return residues.get(seqId) ?? EmptyStepIndices;
-    }
-
     private moveStep() {
-        this.residueOne = copyResidue(this.residueTwo);
-        this.residueTwo = copyResidue(this.residueIt.move())!;
+        this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
+        this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
 
         return this.toPyramids(this.residueOne!, this.residueTwo);
     }
 
-    private toPyramids(one: Residue, two: Residue) {
-        const indices = this.getStepIndices(one);
+    private toPyramids(one: DnatcoUtil.Residue, two: DnatcoUtil.Residue) {
+        const indices = DnatcoUtil.getStepIndices(this.data!, this.loc, one);
 
         const points = [];
         for (const idx of indices) {
             const step = this.data!.steps[idx];
-            points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
+            points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.PDB_ins_code_1, step.PDB_ins_code_2, step.confal_score, idx));
         }
 
         return points;
@@ -121,6 +92,8 @@ export class ConfalPyramidsIterator {
             return this.moveStep();
         } else {
             this.residueIt.setSegment(this.chainIt.move());
+            if (this.residueIt.hasNext)
+                this.residueTwo = this.residueIt.move();
             return this.moveStep();
         }
     }

+ 2 - 2
src/extensions/dnatco/index.ts

@@ -1,8 +1,8 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Michal Malý <michal.maly@ibt.cas.cz>
  * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
  */
 
-export { DnatcoConfalPyramids } from './confal-pyramids/behavior';
+export { DnatcoNtCs } from './behavior';

+ 57 - 0
src/extensions/dnatco/ntc-tube/behavior.ts

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { NtCTubeColorThemeProvider } from './color';
+import { NtCTubeProvider } from './property';
+import { NtCTubeRepresentationProvider } from './representation';
+import { DnatcoTypes } from '../types';
+import { Dnatco } from '../property';
+import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
+import { StateObjectRef } from '../../../mol-state';
+import { Task } from '../../../mol-task';
+
+export const NtCTubePreset = StructureRepresentationPresetProvider({
+    id: 'preset-structure-representation-ntc-tube',
+    display: {
+        name: 'NtC Tube', group: 'Annotation',
+        description: 'NtC Tube',
+    },
+    isApplicable(a) {
+        return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
+    },
+    params: () => StructureRepresentationPresetProvider.CommonParams,
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        const model = structureCell?.obj?.data.model;
+        if (!structureCell || !model) return {};
+
+        await plugin.runTask(Task.create('NtC tube', async runtime => {
+            await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
+        }));
+
+        const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
+
+        const tube = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'NtC Tube' });
+        const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+
+        let tubeRepr;
+        if (representations)
+            tubeRepr = builder.buildRepresentation(update, tube, { type: NtCTubeRepresentationProvider, typeParams, color: NtCTubeColorThemeProvider }, { tag: 'ntc-tube' });
+
+        await update.commit({ revertOnError: true });
+        return { components: { ...components, tube }, representations: { ...representations, tubeRepr } };
+    }
+});
+
+export function NtCTubeSegmentLabel(step: DnatcoTypes.Step) {
+    return `
+        <b>${step.auth_asym_id_1}</b> |
+        <b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
+           ${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
+        <i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
+    `;
+}

+ 90 - 0
src/extensions/dnatco/ntc-tube/color.ts

@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { ErrorColor, NtCColors } from '../color';
+import { NtCTubeProvider } from './property';
+import { NtCTubeTypes as NTT } from './types';
+import { Dnatco } from '../property';
+import { Location } from '../../../mol-model/location';
+import { CustomProperty } from '../../../mol-model-props/common/custom-property';
+import { ColorTheme } from '../../../mol-theme/color';
+import { ThemeDataContext } from '../../../mol-theme/theme';
+import { Color, ColorMap } from '../../../mol-util/color';
+import { getColorMapParams } from '../../../mol-util/color/params';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { TableLegend } from '../../../mol-util/legend';
+import { ObjectKeys } from '../../../mol-util/type-helpers';
+
+const Description = 'Assigns colors to NtC Tube segments';
+
+const NtCTubeColors = ColorMap({
+    ...NtCColors,
+    residueMarker: Color(0x222222),
+    stepBoundaryMarker: Color(0x656565),
+});
+type NtCTubeColors = typeof NtCTubeColors;
+
+export const NtCTubeColorThemeParams = {
+    colors: PD.MappedStatic('default', {
+        'default': PD.EmptyGroup(),
+        'custom': PD.Group(getColorMapParams(NtCTubeColors))
+    }),
+    markResidueBoundaries: PD.Boolean(true),
+    markSegmentBoundaries: PD.Boolean(true),
+};
+export type NtCTubeColorThemeParams = typeof NtCTubeColorThemeParams;
+
+export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) {
+    return PD.clone(NtCTubeColorThemeParams);
+}
+
+export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> {
+    const colorMap = props.colors.name === 'default' ? NtCTubeColors : props.colors.params;
+
+    function color(location: Location, isSecondary: boolean): Color {
+        if (NTT.isLocation(location)) {
+            const { data } = location;
+            const { step, kind } = data;
+            let key;
+            if (kind === 'upper')
+                key = step.NtC + '_Upr' as keyof NtCTubeColors;
+            else if (kind === 'lower')
+                key = step.NtC + '_Lwr' as keyof NtCTubeColors;
+            else if (kind === 'residue-boundary')
+                key = (!props.markResidueBoundaries ? step.NtC + '_Lwr' : 'residueMarker') as keyof NtCTubeColors;
+            else /* segment-boundary */
+                key = (!props.markSegmentBoundaries ? step.NtC + '_Lwr' : 'stepBoundaryMarker') as keyof NtCTubeColors;
+
+            return colorMap[key] ?? ErrorColor;
+        }
+
+        return ErrorColor;
+    }
+
+    return {
+        factory: NtCTubeColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: Description,
+        legend: TableLegend(ObjectKeys(colorMap).map(k => [k.replace('_', ' '), colorMap[k]] as [string, Color]).concat([['Error', ErrorColor]])),
+    };
+}
+
+export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
+    name: 'ntc-tube',
+    label: 'NtC Tube',
+    category: ColorTheme.Category.Residue,
+    factory: NtCTubeColorTheme,
+    getParams: getNtCTubeColorThemeParams,
+    defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
+    ensureCustomProperties: {
+        attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? NtCTubeProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
+        detach: (data) => data.structure && NtCTubeProvider.ref(data.structure.models[0], false)
+    }
+};

+ 44 - 0
src/extensions/dnatco/ntc-tube/property.ts

@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { NtCTubeTypes as NTT } from './types';
+import { Dnatco, DnatcoParams } from '../property';
+import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
+import { Model } from '../../../mol-model/structure';
+import { CustomProperty } from '../../../mol-model-props/common/custom-property';
+import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
+import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+
+export const NtCTubeParams = { ...DnatcoParams };
+export type NtCTubeParams = typeof NtCTubeParams;
+export type NtCTubeProps = PD.Values<NtCTubeParams>;
+export type NtCTubeData = PropertyWrapper<NTT.Data | undefined>;
+
+async function fromCif(ctx: CustomProperty.Context, model: Model, props: NtCTubeProps): Promise<CustomProperty.Data<NtCTubeData>> {
+    const info = PropertyWrapper.createInfo();
+    const data = Dnatco.getCifData(model);
+    if (data === undefined) return { value: { info, data: undefined } };
+
+    const steps = Dnatco.getStepsFromCif(model, data.steps, data.stepsSummary);
+    return { value: { info, data: { data: steps } } };
+}
+
+export const NtCTubeProvider: CustomModelProperty.Provider<NtCTubeParams, NtCTubeData> = CustomModelProperty.createProvider({
+    label: 'NtC Tube',
+    descriptor: CustomPropertyDescriptor({
+        name: 'ntc-tube',
+    }),
+    type: 'static',
+    defaultParams: NtCTubeParams,
+    getParams: (data: Model) => NtCTubeParams,
+    isApplicable: (data: Model) => Dnatco.isApplicable(data),
+    obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<NtCTubeProps>) => {
+        const p = { ...PD.getDefaultValues(NtCTubeParams), ...props };
+        return fromCif(ctx, data, p);
+    }
+});

+ 454 - 0
src/extensions/dnatco/ntc-tube/representation.ts

@@ -0,0 +1,454 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { NtCTubeProvider } from './property';
+import { NtCTubeSegmentsIterator } from './util';
+import { NtCTubeTypes as NTT } from './types';
+import { Dnatco } from '../property';
+import { DnatcoTypes } from '../types';
+import { DnatcoUtil } from '../util';
+import { Interval } from '../../../mol-data/int';
+import { BaseGeometry, VisualQuality } from '../../../mol-geo/geometry/base';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
+import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
+import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { CylinderProps } from '../../../mol-geo/primitive/cylinder';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { smoothstep } from '../../../mol-math/interpolate';
+import { NullLocation } from '../../../mol-model/location';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
+import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
+import { structureUnion } from '../../../mol-model/structure/query/utils/structure-set';
+import { CustomProperty } from '../../../mol-model-props/common/custom-property';
+import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
+import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
+import { createCurveSegmentState, CurveSegmentState } from '../../../mol-repr/structure/visual/util/polymer';
+import { getStructureQuality, VisualUpdateState } from '../../../mol-repr/util';
+import { VisualContext } from '../../../mol-repr/visual';
+import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
+import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+
+const v3add = Vec3.add;
+const v3copy = Vec3.copy;
+const v3cross = Vec3.cross;
+const v3fromArray = Vec3.fromArray;
+const v3matchDirection = Vec3.matchDirection;
+const v3normalize = Vec3.normalize;
+const v3orthogonalize = Vec3.orthogonalize;
+const v3scale = Vec3.scale;
+const v3slerp = Vec3.slerp;
+const v3spline = Vec3.spline;
+const v3sub = Vec3.sub;
+const v3toArray = Vec3.toArray;
+
+const NtCTubeMeshParams = {
+    ...UnitsMeshParams,
+    linearSegments: PD.Numeric(4, { min: 2, max: 8, step: 1 }, BaseGeometry.CustomQualityParamInfo),
+    radialSegments: PD.Numeric(22, { min: 4, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
+    residueMarkerWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
+    segmentBoundaryWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
+};
+type NtCTubeMeshParams = typeof NtCTubeMeshParams;
+
+type QualityOptions = Exclude<VisualQuality, 'auto' | 'custom'>;
+const LinearSegmentCount: Record<QualityOptions, number> = {
+    highest: 6,
+    higher: 6,
+    high: 4,
+    medium: 4,
+    low: 3,
+    lower: 3,
+    lowest: 2,
+};
+const RadialSegmentCount: Record<QualityOptions, number> = {
+    highest: 32,
+    higher: 26,
+    high: 22,
+    medium: 18,
+    low: 14,
+    lower: 10,
+    lowest: 6,
+};
+
+const _curvePoint = Vec3();
+const _tanA = Vec3();
+const _tanB = Vec3();
+const _firstTangentVec = Vec3();
+const _lastTangentVec = Vec3();
+const _firstNormalVec = Vec3();
+const _lastNormalVec = Vec3();
+
+const _tmpNormal = Vec3();
+const _tangentVec = Vec3();
+const _normalVec = Vec3();
+const _binormalVec = Vec3();
+const _prevNormal = Vec3();
+const _nextNormal = Vec3();
+
+function interpolatePointsAndTangents(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, tRange: number[]) {
+    const { curvePoints, tangentVectors, linearSegments } = state;
+    const tension = 0.5;
+    const r = tRange[1] - tRange[0];
+
+    for (let j = 0; j <= linearSegments; ++j) {
+        const t = j * r / linearSegments + tRange[0];
+
+        v3spline(_curvePoint, p0, p1, p2, p3, t, tension);
+        v3spline(_tanA, p0, p1, p2, p3, t - 0.01, tension);
+        v3spline(_tanB, p0, p1, p2, p3, t + 0.01, tension);
+
+        v3toArray(_curvePoint, curvePoints, j * 3);
+        v3normalize(_tangentVec, v3sub(_tangentVec, _tanA, _tanB));
+        v3toArray(_tangentVec, tangentVectors, j * 3);
+    }
+}
+
+function interpolateNormals(state: CurveSegmentState, firstDirection: Vec3, lastDirection: Vec3) {
+    const { curvePoints, tangentVectors, normalVectors, binormalVectors } = state;
+
+    const n = curvePoints.length / 3;
+
+    v3fromArray(_firstTangentVec, tangentVectors, 0);
+    v3fromArray(_lastTangentVec, tangentVectors, (n - 1) * 3);
+
+    v3orthogonalize(_firstNormalVec, _firstTangentVec, firstDirection);
+    v3orthogonalize(_lastNormalVec, _lastTangentVec, lastDirection);
+    v3matchDirection(_lastNormalVec, _lastNormalVec, _firstNormalVec);
+
+    v3copy(_prevNormal, _firstNormalVec);
+
+    const n1 = n - 1;
+    for (let i = 0; i < n; ++i) {
+        const j = smoothstep(0, n1, i) * n1;
+        const t = i === 0 ? 0 : 1 / (n - j);
+
+        v3fromArray(_tangentVec, tangentVectors, i * 3);
+
+        v3orthogonalize(_normalVec, _tangentVec, v3slerp(_tmpNormal, _prevNormal, _lastNormalVec, t));
+        v3toArray(_normalVec, normalVectors, i * 3);
+
+        v3copy(_prevNormal, _normalVec);
+
+        v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
+        v3toArray(_binormalVec, binormalVectors, i * 3);
+    }
+
+    for (let i = 1; i < n1; ++i) {
+        v3fromArray(_prevNormal, normalVectors, (i - 1) * 3);
+        v3fromArray(_normalVec, normalVectors, i * 3);
+        v3fromArray(_nextNormal, normalVectors, (i + 1) * 3);
+
+        v3scale(_normalVec, v3add(_normalVec, _prevNormal, v3add(_normalVec, _nextNormal, _normalVec)), 1 / 3);
+        v3toArray(_normalVec, normalVectors, i * 3);
+
+        v3fromArray(_tangentVec, tangentVectors, i * 3);
+        v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
+        v3toArray(_binormalVec, binormalVectors, i * 3);
+    }
+}
+
+function interpolate(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, firstDir: Vec3, lastDir: Vec3, tRange = [0, 1]) {
+    interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange);
+    interpolateNormals(state, firstDir, lastDir);
+}
+
+function createNtCTubeSegmentsIterator(structureGroup: StructureGroup): LocationIterator {
+    const { structure, group } = structureGroup;
+    const instanceCount = group.units.length;
+
+    const data = NtCTubeProvider.get(structure.model)?.value?.data;
+    if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
+
+    const numBlocks = data.data.steps.length * 4;
+
+    const getLocation = (groupId: number, instanceId: number) => {
+        if (groupId > numBlocks) return NullLocation;
+        const stepIdx = Math.floor(groupId / 4);
+        const step = data.data.steps[stepIdx];
+        const r = groupId % 4;
+        const kind =
+            r === 0 ? 'upper' :
+                r === 1 ? 'lower' :
+                    r === 2 ? 'residue-boundary' : 'segment-boundary';
+
+        return NTT.Location({ step, kind });
+    };
+    return LocationIterator(totalMeshGroupsCount(data.data.steps) + 1, instanceCount, 1, getLocation);
+}
+
+function segmentCount(structure: Structure, props: PD.Values<NtCTubeMeshParams>): { linear: number, radial: number } {
+    const quality = props.quality;
+
+    if (quality === 'custom')
+        return { linear: props.linearSegments, radial: props.radialSegments };
+    else if (quality === 'auto') {
+        const autoQuality = getStructureQuality(structure) as QualityOptions;
+        return { linear: LinearSegmentCount[autoQuality], radial: RadialSegmentCount[autoQuality] };
+    } else
+        return { linear: LinearSegmentCount[quality], radial: RadialSegmentCount[quality] };
+}
+
+function stepBoundingSphere(step: DnatcoTypes.Step, struLoci: StructureElement.Loci): Sphere3D | undefined {
+    const one = DnatcoUtil.residueToLoci(step.auth_asym_id_1, step.auth_seq_id_1, step.label_alt_id_1, step.PDB_ins_code_1, struLoci, 'auth');
+    const two = DnatcoUtil.residueToLoci(step.auth_asym_id_2, step.auth_seq_id_2, step.label_alt_id_2, step.PDB_ins_code_2, struLoci, 'auth');
+
+    if (StructureElement.Loci.is(one) && StructureElement.Loci.is(two)) {
+        const union = structureUnion(struLoci.structure, [StructureElement.Loci.toStructure(one), StructureElement.Loci.toStructure(two)]);
+        return union.boundary.sphere;
+    }
+    return void 0;
+}
+
+function totalMeshGroupsCount(steps: DnatcoTypes.Step[]) {
+    // Each segment has two blocks, Residue Boundary marker and a Segment Boundary marker
+    return steps.length * 4 - 1; // Subtract one because the last Segment Boundary marker is not drawn
+}
+
+function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<NtCTubeMeshParams>, mesh?: Mesh) {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
+
+    const prop = NtCTubeProvider.get(structure.model).value;
+    if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
+
+    const { data } = prop.data;
+    if (data.steps.length === 0) return Mesh.createEmpty(mesh);
+
+    const MarkerLinearSegmentCount = 2;
+    const segCount = segmentCount(structure, props);
+    const vertexCount = Math.floor((segCount.linear * 4 * data.steps.length / structure.model.atomicHierarchy.chains._rowCount) * segCount.radial);
+    const chunkSize = Math.floor(vertexCount / 3);
+    const diameter = 1.0 * theme.size.props.value;
+
+    const mb = MeshBuilder.createState(vertexCount, chunkSize, mesh);
+
+    const state = createCurveSegmentState(segCount.linear);
+    const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state;
+    for (let idx = 0; idx <= segCount.linear; idx++) {
+        widthValues[idx] = diameter;
+        heightValues[idx] = diameter;
+    }
+    const [normals, binormals] = [binormalVectors, normalVectors]; // Needed so that the tube is not drawn from inside out
+
+    const markerState = createCurveSegmentState(MarkerLinearSegmentCount);
+    const { curvePoints: mCurvePoints, normalVectors: mNormalVectors, binormalVectors: mBinormalVectors, widthValues: mWidthValues, heightValues: mHeightValues } = markerState;
+    for (let idx = 0; idx <= MarkerLinearSegmentCount; idx++) {
+        mWidthValues[idx] = diameter;
+        mHeightValues[idx] = diameter;
+    }
+    const [mNormals, mBinormals] = [mBinormalVectors, mNormalVectors];
+
+    const firstDir = Vec3();
+    const lastDir = Vec3();
+    const markerDir = Vec3();
+
+    const residueMarkerWidth = props.residueMarkerWidth / 2;
+    const it = new NtCTubeSegmentsIterator(structure, unit);
+    while (it.hasNext) {
+        const segment = it.move();
+        if (!segment)
+            continue;
+
+        const { p_1, p0, p1, p2, p3, p4, pP } = segment;
+        const FirstBlockId = segment.stepIdx * 4;
+        const SecondBlockId = FirstBlockId + 1;
+        const ResidueMarkerId = FirstBlockId + 2;
+        const SegmentBoundaryMarkerId = FirstBlockId + 3;
+
+        const { rmShift, rmPos } = calcResidueMarkerShift(p2, p3, pP);
+
+        if (segment.firstInChain) {
+            v3normalize(firstDir, v3sub(firstDir, p2, p1));
+            v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
+        } else {
+            v3copy(firstDir, lastDir);
+            v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
+        }
+
+        // C5' -> O3' block
+        interpolate(state, p0, p1, p2, p3, firstDir, lastDir);
+        mb.currentGroup = FirstBlockId;
+        addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, segment.firstInChain || segment.followsGap, false, 'rounded');
+
+        // O3' -> C5' block
+        v3copy(firstDir, lastDir);
+        v3normalize(markerDir, v3sub(markerDir, p3, rmPos));
+        v3normalize(lastDir, v3sub(lastDir, p4, p3));
+
+        // From O3' to the residue marker
+        interpolate(state, p1, p2, p3, p4, firstDir, markerDir, [0, rmShift - residueMarkerWidth]);
+        mb.currentGroup = SecondBlockId;
+        addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
+
+        // Residue marker
+        interpolate(markerState, p1, p2, p3, p4, markerDir, markerDir, [rmShift - residueMarkerWidth, rmShift + residueMarkerWidth]);
+        mb.currentGroup = ResidueMarkerId;
+        addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
+
+        if (segment.capEnd) {
+            // From the residue marker to C5' of the end
+            interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1]);
+            mb.currentGroup = SecondBlockId;
+            addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, true, 'rounded');
+        } else {
+            // From the residue marker to C5' of the step boundary marker
+            interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1 - props.segmentBoundaryWidth]);
+            mb.currentGroup = SecondBlockId;
+            addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
+
+            // Step boundary marker
+            interpolate(markerState, p1, p2, p3, p4, lastDir, lastDir, [1 - props.segmentBoundaryWidth, 1]);
+            mb.currentGroup = SegmentBoundaryMarkerId;
+            addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
+        }
+
+        if (segment.followsGap) {
+            const cylinderProps: CylinderProps = {
+                radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial,
+            };
+            mb.currentGroup = FirstBlockId;
+            addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, cylinderProps);
+        }
+    }
+
+    const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1.05);
+
+    const m = MeshBuilder.getMesh(mb);
+    m.setBoundingSphere(boundingSphere);
+    return m;
+}
+
+const _rmvCO = Vec3();
+const _rmvPO = Vec3();
+const _rmPos = Vec3();
+const _HalfPi = Math.PI / 2;
+function calcResidueMarkerShift(pO: Vec3, pC: Vec3, pP: Vec3): { rmShift: number, rmPos: Vec3 } {
+    v3sub(_rmvCO, pC, pO);
+    v3sub(_rmvPO, pP, pO);
+
+    // Project position of P atom on the O3' -> C5' vector
+    const beta = Vec3.angle(_rmvPO, _rmvCO);
+    const alpha = _HalfPi - Math.abs(beta);
+    const lengthMO = Math.cos(alpha) * Vec3.magnitude(_rmvPO);
+    const shift = lengthMO / Vec3.magnitude(_rmvCO);
+
+    v3scale(_rmvCO, _rmvCO, shift);
+    v3add(_rmPos, _rmvCO, pO);
+
+    return { rmShift: shift, rmPos: _rmPos };
+}
+
+function getNtCTubeSegmentLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
+    const { groupId, objectId, instanceId } = pickingId;
+    if (objectId !== id) return EmptyLoci;
+
+    const { structure } = structureGroup;
+
+    const unit = structureGroup.group.units[instanceId];
+    if (!Unit.isAtomic(unit)) return EmptyLoci;
+
+    const data = NtCTubeProvider.get(structure.model)?.value?.data ?? undefined;
+    if (!data) return EmptyLoci;
+
+    const MeshGroupsCount = totalMeshGroupsCount(data.data.steps);
+    if (groupId > MeshGroupsCount) return EmptyLoci;
+
+    const stepIdx = Math.floor(groupId / 4);
+    const bs = stepBoundingSphere(data.data.steps[stepIdx], Structure.toStructureElementLoci(structure));
+
+    /*
+     * NOTE 1) Each step is drawn with 4 mesh groups. We need to divide/multiply by 4 to convert between steps and mesh groups.
+     * NOTE 2) Molstar will create a mesh only for the asymmetric unit. When the entire biological assembly
+     *         is displayed, Molstar just copies and transforms the mesh. This means that even though the mesh
+     *         might be displayed multiple times, groupIds of the individual blocks in the mesh will be the same.
+     *         If there are multiple copies of a mesh, Molstar needs to be able to tell which block belongs to which copy of the mesh.
+     *         To do that, Molstar adds an offset to groupIds of the copied meshes. Offset is calculated as follows:
+     *
+     *         offset = NumberOfBlocks * UnitIndex
+     *
+     *         "NumberOfBlocks" is the number of valid Location objects got from LocationIterator *or* the greatest groupId set by
+     *         the mesh generator - whichever is smaller.
+     *
+     *         UnitIndex is the index of the Unit the mesh belongs to, starting from 0. (See "unitMap" in the Structure object).
+     *         We can also get this index from the value "instanceId" of the "pickingId" object.
+     *
+     *         If this offset is not applied, picking a piece of one of the copied meshes would actually pick that piece in the original mesh.
+     *         This is particularly apparent with highlighting - hovering over items in a copied mesh incorrectly highlights those items in the source mesh.
+     *
+     *         Molstar can take advantage of the fact that ElementLoci has a reference to the Unit object attached to it. Since we cannot attach ElementLoci
+     *         to a step, we need to calculate the offseted groupId here and pass it as part of the DataLoci.
+     */
+    const offsetGroupId = stepIdx * 4 + (MeshGroupsCount + 1) * instanceId;
+    return NTT.Loci(data.data.steps, [stepIdx], [offsetGroupId], bs);
+}
+
+function eachNtCTubeSegment(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+    if (NTT.isLoci(loci)) {
+        const offsetGroupId = loci.elements[0];
+        return apply(Interval.ofBounds(offsetGroupId, offsetGroupId + 4));
+    }
+    return false;
+}
+
+function NtCTubeVisual(materialId: number): UnitsVisual<NtCTubeMeshParams> {
+    return UnitsMeshVisual<NtCTubeMeshParams>({
+        defaultProps: PD.getDefaultValues(NtCTubeMeshParams),
+        createGeometry: createNtCTubeMesh,
+        createLocationIterator: createNtCTubeSegmentsIterator,
+        getLoci: getNtCTubeSegmentLoci,
+        eachLocation: eachNtCTubeSegment,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NtCTubeMeshParams>, currentProps: PD.Values<NtCTubeMeshParams>) => {
+            state.createGeometry = (
+                newProps.quality !== currentProps.quality ||
+                newProps.residueMarkerWidth !== currentProps.residueMarkerWidth ||
+                newProps.segmentBoundaryWidth !== currentProps.segmentBoundaryWidth ||
+                newProps.doubleSided !== currentProps.doubleSided ||
+                newProps.alpha !== currentProps.alpha ||
+                newProps.linearSegments !== currentProps.linearSegments ||
+                newProps.radialSegments !== currentProps.radialSegments
+            );
+        }
+    }, materialId);
+
+}
+const NtCTubeVisuals = {
+    'ntc-tube-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeMeshParams>) => UnitsRepresentation('NtC Tube Mesh', ctx, getParams, NtCTubeVisual),
+};
+
+export const NtCTubeParams = {
+    ...NtCTubeMeshParams
+};
+export type NtCTubeParams = typeof NtCTubeParams;
+export function getNtCTubeParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(NtCTubeParams);
+}
+
+export type NtCTubeRepresentation = StructureRepresentation<NtCTubeParams>;
+export function NtCTubeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeParams>): NtCTubeRepresentation {
+    return Representation.createMulti('NtC Tube', ctx, getParams, StructureRepresentationStateBuilder, NtCTubeVisuals as unknown as Representation.Def<Structure, NtCTubeParams>);
+}
+
+export const NtCTubeRepresentationProvider = StructureRepresentationProvider({
+    name: 'ntc-tube',
+    label: 'NtC Tube',
+    description: 'Displays schematic representation of NtC conformers',
+    factory: NtCTubeRepresentation,
+    getParams: getNtCTubeParams,
+    defaultValues: PD.getDefaultValues(NtCTubeParams),
+    defaultColorTheme: { name: 'ntc-tube' },
+    defaultSizeTheme: { name: 'uniform', props: { value: 2.0 } },
+    isApplicable: (structure: Structure) => structure.models.every(m => Dnatco.isApplicable(m)),
+    ensureCustomProperties: {
+        attach: async (ctx: CustomProperty.Context, structure: Structure) => structure.models.forEach(m => NtCTubeProvider.attach(ctx, m, void 0, true)),
+        detach: (data) => data.models.forEach(m => NtCTubeProvider.ref(m, false)),
+    },
+});

+ 51 - 0
src/extensions/dnatco/ntc-tube/types.ts

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { NtCTubeSegmentLabel } from './behavior';
+import { DnatcoTypes } from '../types';
+import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
+import { DataLocation } from '../../../mol-model/location';
+import { DataLoci } from '../../../mol-model/loci';
+
+export namespace NtCTubeTypes {
+    const DataTag = 'dnatco-tube-segment-data';
+    const DummyTag = 'dnatco-tube-dummy';
+
+    export type Data = {
+        data: DnatcoTypes.Steps,
+    }
+
+    export type TubeBlock = {
+        step: DnatcoTypes.Step,
+        kind: 'upper' | 'lower' | 'residue-boundary' | 'segment-boundary';
+    }
+
+    export interface Location extends DataLocation<TubeBlock> {}
+
+    export function Location(payload: TubeBlock) {
+        return DataLocation(DataTag, payload, {});
+    }
+
+    export function isLocation(x: any): x is Location {
+        return !!x && x.kind === 'data-location' && x.tag === DataTag;
+    }
+
+    export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
+    export interface DummyLoci extends DataLoci<{}, number> {}
+
+    export function Loci(data: DnatcoTypes.Step[], stepIndices: number[], elements: number[], boundingSphere?: Sphere3D): Loci {
+        return DataLoci(DataTag, data, elements, boundingSphere ? () => boundingSphere : undefined, () => stepIndices[0] !== undefined ? NtCTubeSegmentLabel(data[stepIndices[0]]) : '');
+    }
+
+    export function DummyLoci(): DummyLoci {
+        return DataLoci(DummyTag, {}, [], undefined, () => '');
+    }
+
+    export function isLoci(x: any): x is Loci {
+        return !!x && x.kind === 'data-loci' && x.tag === DataTag;
+    }
+}

+ 153 - 0
src/extensions/dnatco/ntc-tube/util.ts

@@ -0,0 +1,153 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { NtCTubeTypes as NTT } from './types';
+import { NtCTubeProvider } from './property';
+import { DnatcoUtil } from '../util';
+import { Segmentation, SortedArray } from '../../../mol-data/int';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
+
+function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) {
+    const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode);
+    if (eI !== -1)
+        loc.unit.conformation.invariantPosition(eI, vec);
+    else {
+        vec[0] = 0; vec[1] = 0; vec[2] = 0;
+    }
+}
+
+const p_1 = Vec3();
+const p0 = Vec3();
+const p1 = Vec3();
+const p2 = Vec3();
+const p3 = Vec3();
+const p4 = Vec3();
+const pP = Vec3();
+
+function getPoints(
+    loc: StructureElement.Location,
+    r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue,
+    altId0: string, altId1: string, altId2: string,
+    insCode0: string, insCode1: string, insCode2: string,
+) {
+    if (r0) getAtomPosition(p_1, loc, r0, ['C5\'', 'C5*'], altId0, insCode0);
+    r0 ? getAtomPosition(p0, loc, r0, ['O3\'', 'O3*'], altId0, insCode0) : getAtomPosition(p0, loc, r1, ['O5\'', 'O5*'], altId1, insCode1);
+    getAtomPosition(p1, loc, r1, ['C5\'', 'C5*'], altId1, insCode1);
+    getAtomPosition(p2, loc, r1, ['O3\'', 'O3*'], altId1, insCode1);
+    getAtomPosition(p3, loc, r2, ['C5\'', 'C5*'], altId2, insCode2);
+    getAtomPosition(p4, loc, r2, ['O3\'', 'O3*'], altId2, insCode2);
+    getAtomPosition(pP, loc, r2, ['P'], altId2, insCode2);
+
+    return { p_1, p0, p1, p2, p3, p4, pP };
+}
+
+function hasGapElements(r: DnatcoUtil.Residue, unit: Unit) {
+    for (let xI = r.start; xI < r.end; xI++) {
+        const eI = unit.elements[xI];
+        if (SortedArray.has(unit.gapElements, eI)) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+export type NtCTubeSegment = {
+    p_1: Vec3,
+    p0: Vec3,
+    p1: Vec3,
+    p2: Vec3,
+    p3: Vec3,
+    p4: Vec3,
+    pP: Vec3,
+    stepIdx: number,
+    followsGap: boolean,
+    firstInChain: boolean,
+    capEnd: boolean,
+}
+
+export class NtCTubeSegmentsIterator {
+    private chainIt: Segmentation.SegmentIterator<ChainIndex>;
+    private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
+    private residuePrev?: DnatcoUtil.Residue;
+    private residueOne?: DnatcoUtil.Residue;
+    private residueTwo: DnatcoUtil.Residue;
+    private data?: NTT.Data;
+    private altIdOne = '';
+    private insCodeOne = '';
+    private loc: StructureElement.Location;
+
+    private moveStep() {
+        this.residuePrev = DnatcoUtil.copyResidue(this.residueOne);
+        this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
+        this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
+
+        return this.toSegment(this.residuePrev, this.residueOne!, this.residueTwo);
+    }
+
+    private toSegment(r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue): NtCTubeSegment | undefined {
+        const indices = DnatcoUtil.getStepIndices(this.data!.data, this.loc, r1);
+        if (indices.length === 0)
+            return void 0;
+
+        const stepIdx = indices[0];
+        const step = this.data!.data.steps[stepIdx];
+
+        const altIdPrev = this.altIdOne;
+        const insCodePrev = this.insCodeOne;
+        this.altIdOne = step.label_alt_id_1;
+        this.insCodeOne = step.PDB_ins_code_1;
+        const altIdTwo = step.label_alt_id_2;
+        const insCodeTwo = step.PDB_ins_code_2;
+        const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit);
+
+        return {
+            ...getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo),
+            stepIdx,
+            followsGap,
+            firstInChain: !r0,
+            capEnd: !this.residueIt.hasNext || hasGapElements(r2, this.loc.unit),
+        };
+    }
+
+    constructor(structure: Structure, unit: Unit.Atomic) {
+        this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
+        this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+
+        const prop = NtCTubeProvider.get(unit.model).value;
+        this.data = prop?.data;
+
+        if (this.chainIt.hasNext) {
+            this.residueIt.setSegment(this.chainIt.move());
+            if (this.residueIt.hasNext)
+                this.residueTwo = this.residueIt.move();
+        }
+
+        this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
+    }
+
+    get hasNext() {
+        if (!this.data)
+            return false;
+        return this.residueIt.hasNext
+            ? true
+            : this.chainIt.hasNext;
+    }
+
+    move() {
+        if (this.residueIt.hasNext) {
+            return this.moveStep();
+        } else {
+            this.residuePrev = void 0; // Assume discontinuity when we switch chains
+            this.residueIt.setSegment(this.chainIt.move());
+            if (this.residueIt.hasNext)
+                this.residueTwo = this.residueIt.move();
+            return this.moveStep();
+        }
+    }
+}

+ 172 - 0
src/extensions/dnatco/property.ts

@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { DnatcoTypes } from './types';
+import { Column, Table } from '../../mol-data/db';
+import { toTable } from '../../mol-io/reader/cif/schema';
+import { Model } from '../../mol-model/structure';
+import { CustomProperty } from '../../mol-model-props/common/custom-property';
+import { PropertyWrapper } from '../../mol-model-props/common/wrapper';
+import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+
+export type DnatcoSteps = PropertyWrapper<DnatcoTypes.Steps | undefined>;
+
+export const DnatcoParams = {};
+export type DnatcoParams = typeof DnatcoParams;
+export type DnatcoProps = PD.Values<DnatcoParams>;
+
+export namespace Dnatco {
+    export const Schema = {
+        ndb_struct_ntc_step: {
+            id: Column.Schema.int,
+            name: Column.Schema.str,
+            PDB_model_number: Column.Schema.int,
+            label_entity_id_1: Column.Schema.int,
+            label_asym_id_1: Column.Schema.str,
+            label_seq_id_1: Column.Schema.int,
+            label_comp_id_1: Column.Schema.str,
+            label_alt_id_1: Column.Schema.str,
+            label_entity_id_2: Column.Schema.int,
+            label_asym_id_2: Column.Schema.str,
+            label_seq_id_2: Column.Schema.int,
+            label_comp_id_2: Column.Schema.str,
+            label_alt_id_2: Column.Schema.str,
+            auth_asym_id_1: Column.Schema.str,
+            auth_seq_id_1: Column.Schema.int,
+            auth_asym_id_2: Column.Schema.str,
+            auth_seq_id_2: Column.Schema.int,
+            PDB_ins_code_1: Column.Schema.str,
+            PDB_ins_code_2: Column.Schema.str,
+        },
+        ndb_struct_ntc_step_summary: {
+            step_id: Column.Schema.int,
+            assigned_CANA: Column.Schema.str,
+            assigned_NtC: Column.Schema.str,
+            confal_score: Column.Schema.int,
+            euclidean_distance_NtC_ideal: Column.Schema.float,
+            cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
+            closest_CANA: Column.Schema.str,
+            closest_NtC: Column.Schema.str,
+            closest_step_golden: Column.Schema.str
+        }
+    };
+    export type Schema = typeof Schema;
+
+    export function getStepsFromCif(
+        model: Model,
+        cifSteps: Table<typeof Dnatco.Schema.ndb_struct_ntc_step>,
+        stepsSummary: StepsSummaryTable
+    ): DnatcoTypes.Steps {
+        const steps = new Array<DnatcoTypes.Step>();
+        const mapping = new Array<DnatcoTypes.MappedChains>();
+
+        const {
+            id, PDB_model_number, name,
+            auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
+            auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
+            _rowCount
+        } = cifSteps;
+
+        if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
+
+        for (let i = 0; i < _rowCount; i++) {
+            const {
+                NtC,
+                confal_score,
+                rmsd
+            } = getSummaryData(id.value(i), i, stepsSummary);
+            const modelNum = PDB_model_number.value(i);
+            const chainId = auth_asym_id_1.value(i);
+            const seqId = auth_seq_id_1.value(i);
+            const modelIdx = modelNum - 1;
+
+            if (mapping.length <= modelIdx || !mapping[modelIdx])
+                mapping[modelIdx] = new Map<string, DnatcoTypes.MappedResidues>();
+
+            const step = {
+                PDB_model_number: modelNum,
+                name: name.value(i),
+                auth_asym_id_1: chainId,
+                auth_seq_id_1: seqId,
+                label_comp_id_1: label_comp_id_1.value(i),
+                label_alt_id_1: label_alt_id_1.value(i),
+                PDB_ins_code_1: PDB_ins_code_1.value(i),
+                auth_asym_id_2: auth_asym_id_2.value(i),
+                auth_seq_id_2: auth_seq_id_2.value(i),
+                label_comp_id_2: label_comp_id_2.value(i),
+                label_alt_id_2: label_alt_id_2.value(i),
+                PDB_ins_code_2: PDB_ins_code_2.value(i),
+                confal_score,
+                NtC,
+                rmsd,
+            };
+
+            steps.push(step);
+
+            const mappedChains = mapping[modelIdx];
+            const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
+            const stepsForResidue = residuesOnChain.get(seqId) ?? [];
+            stepsForResidue.push(steps.length - 1);
+
+            residuesOnChain.set(seqId, stepsForResidue);
+            mappedChains.set(chainId, residuesOnChain);
+            mapping[modelIdx] = mappedChains;
+        }
+
+        return { steps, mapping };
+    }
+
+    export async function fromCif(ctx: CustomProperty.Context, model: Model, props: DnatcoProps): Promise<CustomProperty.Data<DnatcoSteps>> {
+        const info = PropertyWrapper.createInfo();
+        const data = getCifData(model);
+        if (data === undefined) return { value: { info, data: undefined } };
+
+        const fromCif = getStepsFromCif(model, data.steps, data.stepsSummary);
+        return { value: { info, data: fromCif } };
+    }
+
+    export function getCifData(model: Model) {
+        if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
+        if (!hasNdbStructNtcCategories(model)) return undefined;
+        return {
+            steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
+            stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
+        };
+    }
+
+    function hasNdbStructNtcCategories(model: Model): boolean {
+        if (!MmcifFormat.is(model.sourceData)) return false;
+        const names = (model.sourceData).data.frame.categoryNames;
+        return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
+    }
+
+    export function isApplicable(model?: Model): boolean {
+        return !!model && hasNdbStructNtcCategories(model);
+    }
+}
+
+type StepsSummaryTable = Table<typeof Dnatco.Schema.ndb_struct_ntc_step_summary>;
+
+function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
+    const {
+        step_id,
+        confal_score,
+        assigned_NtC,
+        cartesian_rmsd_closest_NtC_representative,
+    } = stepsSummary;
+
+    // Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
+    for (let j = i; j < stepsSummary._rowCount; j++) {
+        if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
+    }
+    // Safety net for cases where the previous assumption is not met
+    for (let j = 0; j < i; j++) {
+        if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
+    }
+    throw new Error('Inconsistent mmCIF data');
+}

+ 41 - 0
src/extensions/dnatco/types.ts

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+export namespace DnatcoTypes {
+    export const DataTag = 'dnatco-confal-half-step';
+
+    export type Step = {
+        PDB_model_number: number,
+        name: string,
+        auth_asym_id_1: string,
+        auth_seq_id_1: number,
+        label_comp_id_1: string,
+        label_alt_id_1: string,
+        PDB_ins_code_1: string,
+        auth_asym_id_2: string,
+        auth_seq_id_2: number,
+        label_comp_id_2: string,
+        label_alt_id_2: string,
+        PDB_ins_code_2: string,
+        confal_score: number,
+        NtC: string,
+        rmsd: number,
+    }
+
+    export type MappedChains = Map<string, MappedResidues>;
+    export type MappedResidues = Map<number, number[]>;
+
+    export interface Steps {
+        steps: Array<Step>,
+        mapping: MappedChains[],
+    }
+
+    export interface HalfStep {
+        step: Step,
+        isLower: boolean,
+    }
+}

+ 117 - 0
src/extensions/dnatco/util.ts

@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <michal.maly@ibt.cas.cz>
+ * @author Jiří Černý <jiri.cerny@ibt.cas.cz>
+ */
+
+import { DnatcoTypes } from './types';
+import { OrderedSet, Segmentation } from '../../mol-data/int';
+import { EmptyLoci } from '../../mol-model/loci';
+import { ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
+
+const EmptyStepIndices = new Array<number>();
+
+export namespace DnatcoUtil {
+    export type Residue = Segmentation.Segment<ResidueIndex>;
+
+    export function copyResidue(r?: Residue) {
+        return r ? { index: r.index, start: r.start, end: r.end } : void 0;
+    }
+
+    export function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string, insCode: string): ElementIndex {
+        for (let eI = residue.start; eI < residue.end; eI++) {
+            loc.element = loc.unit.elements[eI];
+            const elName = StructureProperties.atom.label_atom_id(loc);
+            const elAltId = StructureProperties.atom.label_alt_id(loc);
+            const elInsCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
+
+            if (names.includes(elName) && (elAltId === altId || elAltId.length === 0) && (elInsCode === insCode))
+                return loc.element;
+        }
+
+        return -1 as ElementIndex;
+    }
+
+    export function getStepIndices(data: DnatcoTypes.Steps, loc: StructureElement.Location, r: DnatcoUtil.Residue) {
+        loc.element = loc.unit.elements[r.start];
+
+        const modelIdx = StructureProperties.unit.model_num(loc) - 1;
+        const chainId = StructureProperties.chain.auth_asym_id(loc);
+        const seqId = StructureProperties.residue.auth_seq_id(loc);
+        const insCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
+
+        const chains = data.mapping[modelIdx];
+        if (!chains) return EmptyStepIndices;
+        const residues = chains.get(chainId);
+        if (!residues) return EmptyStepIndices;
+        const indices = residues.get(seqId);
+        if (!indices) return EmptyStepIndices;
+
+        return insCode !== '' ? indices.filter(idx => data.steps[idx].PDB_ins_code_1 === insCode) : indices;
+    }
+
+    export function residueAltIds(structure: Structure, unit: Unit, residue: Residue) {
+        const altIds = new Array<string>();
+        const loc = StructureElement.Location.create(structure, unit);
+        for (let eI = residue.start; eI < residue.end; eI++) {
+            loc.element = OrderedSet.getAt(unit.elements, eI);
+            const altId = StructureProperties.atom.label_alt_id(loc);
+            if (altId !== '' && !altIds.includes(altId))
+                altIds.push(altId);
+        }
+
+        return altIds;
+    }
+
+    const _loc = StructureElement.Location.create();
+    export function residueToLoci(asymId: string, seqId: number, altId: string | undefined, insCode: string, loci: StructureElement.Loci, source: 'label' | 'auth') {
+        _loc.structure = loci.structure;
+        for (const e of loci.elements) {
+            _loc.unit = 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) {
+                        const _insCode = StructureProperties.residue.pdbx_PDB_ins_code(_loc);
+                        if (_insCode !== insCode)
+                            continue;
+                        if (altId) {
+                            const _altIds = residueAltIds(loci.structure, e.unit, residue);
+                            if (!_altIds.includes(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;
+    }
+}