Quellcode durchsuchen

Merge pull request #39 from MadCatX/confal_v2

Confal v2
Alexander Rose vor 4 Jahren
Ursprung
Commit
47ba54199f

+ 10 - 0
src/extensions/dnatco/README.md

@@ -0,0 +1,10 @@
+## DNATCO Extensions
+
+### Confal Pyramids
+
+The Confal Pyramids extensions displays tetrahedron-like pyramids. These pyramids are a simple visual representation of nucleotide conformer classes that can be assigned to individual steps in nucleic acid structures.
+
+For more information, see:
+* [Černý et al., Nucleic Acids Research, 44, W284 (2016)](http://dx.doi.org/10.1093/nar/gkw381)
+* [Schneider et al., Acta Cryst D, 74, 52-64 (2018)](http://dx.doi.org/10.1107/S2059798318000050)
+* [Schneider et al., Genes, 8(10), 278, (2017)](http://dx.doi.org/10.3390/genes8100278)

+ 103 - 0
src/extensions/dnatco/confal-pyramids/behavior.ts

@@ -0,0 +1,103 @@
+/**
+ * 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 { ConfalPyramidsColorThemeProvider } from './color';
+import { ConfalPyramids, ConfalPyramidsProvider } from './property';
+import { ConfalPyramidsRepresentationProvider } from './representation';
+import { Loci } from '../../../mol-model/loci';
+import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
+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({
+    id: 'preset-structure-representation-confal-pyramids',
+    display: {
+        name: 'Confal Pyramids',
+        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));
+    },
+    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('Confal Pyramids', async runtime => {
+            await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
+        }));
+
+        const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
+
+        const pyramids = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'Confal Pyramids' });
+        const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+
+        let pyramidsRepr;
+        if (representations)
+            pyramidsRepr = builder.buildRepresentation(update, pyramids,  { type: ConfalPyramidsRepresentationProvider, typeParams, color: ConfalPyramidsColorThemeProvider }, { tag: 'confal-pyramdis' } );
+
+        await update.commit({ revertOnError: true });
+        return  { components: { ...components, pyramids }, representations: { ...representations, pyramidsRepr } };
+    }
+});
+
+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;
+
+        private labelConfalPyramids = {
+            label: (loci: Loci): string | undefined => {
+                if (!this.params.showToolTip) return void 0;
+
+                /* TODO: Implement this */
+                return void 0;
+            }
+        }
+
+        register(): void {
+            this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
+            this.ctx.managers.lociLabels.addProvider(this.labelConfalPyramids);
+
+            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.managers.lociLabels.removeProvider(this.labelConfalPyramids);
+
+            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)
+    })
+});

+ 178 - 0
src/extensions/dnatco/confal-pyramids/color.ts

@@ -0,0 +1,178 @@
+/**
+ * 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 { ConfalPyramids, ConfalPyramidsProvider } from './property';
+import { ConfalPyramidsTypes as CPT } from './types';
+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 } from '../../../mol-util/color';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+
+const DefaultColor = Color(0xCCCCCC);
+const Description = 'Assigns colors to confal pyramids';
+const ErrorColor = Color(0xFFA10A);
+
+type ConformerClasses = 'A' | 'B' | 'BII' | 'miB' | 'Z' | 'IC' | 'OPN' | 'SYN' | 'N';
+
+const ColorMapping: ReadonlyMap<ConformerClasses, Color> = new Map([
+    ['A', Color(0xFFC1C1)],
+    ['B', Color(0xC8CFFF)],
+    ['BII', Color(0x0059DA)],
+    ['miB', Color(0x3BE8FB)],
+    ['Z',  Color(0x01F60E)],
+    ['IC', Color(0xFA5CFB)],
+    ['OPN', Color(0xE90000)],
+    ['SYN', Color(0xFFFF01)],
+    ['N', Color(0xF2F2F2)],
+]);
+
+const NtCToClasses: ReadonlyMap<string, [ConformerClasses, ConformerClasses]> = new Map([
+    ['NANT', ['N', 'N']],
+    ['AA00', ['A', 'A']],
+    ['AA02', ['A', 'A']],
+    ['AA03', ['A', 'A']],
+    ['AA04', ['A', 'A']],
+    ['AA08', ['A', 'A']],
+    ['AA09', ['A', 'A']],
+    ['AA01', ['A', 'A']],
+    ['AA05', ['A', 'A']],
+    ['AA06', ['A', 'A']],
+    ['AA10', ['A', 'A']],
+    ['AA11', ['A', 'A']],
+    ['AA07', ['A', 'A']],
+    ['AA12', ['A', 'A']],
+    ['AA13', ['A', 'A']],
+    ['AB01', ['A', 'B']],
+    ['AB02', ['A', 'B']],
+    ['AB03', ['A', 'B']],
+    ['AB04', ['A', 'B']],
+    ['AB05', ['A', 'B']],
+    ['BA01', ['B', 'A']],
+    ['BA05', ['B', 'A']],
+    ['BA09', ['B', 'A']],
+    ['BA08', ['BII', 'A']],
+    ['BA10', ['B', 'A']],
+    ['BA13', ['BII', 'A']],
+    ['BA16', ['BII', 'A']],
+    ['BA17', ['BII', 'A']],
+    ['BB00', ['B', 'B']],
+    ['BB01', ['B', 'B']],
+    ['BB17', ['B', 'B']],
+    ['BB02', ['B', 'B']],
+    ['BB03', ['B', 'B']],
+    ['BB11', ['B', 'B']],
+    ['BB16', ['B', 'B']],
+    ['BB04', ['B', 'BII']],
+    ['BB05', ['B', 'BII']],
+    ['BB07', ['BII', 'BII']],
+    ['BB08', ['BII', 'BII']],
+    ['BB10', ['miB', 'miB']],
+    ['BB12', ['miB', 'miB']],
+    ['BB13', ['miB', 'miB']],
+    ['BB14', ['miB', 'miB']],
+    ['BB15', ['miB', 'miB']],
+    ['BB20', ['miB', 'miB']],
+    ['IC01', ['IC', 'IC']],
+    ['IC02', ['IC', 'IC']],
+    ['IC03', ['IC', 'IC']],
+    ['IC04', ['IC', 'IC']],
+    ['IC05', ['IC', 'IC']],
+    ['IC06', ['IC', 'IC']],
+    ['IC07', ['IC', 'IC']],
+    ['OP01', ['OPN', 'OPN']],
+    ['OP02', ['OPN', 'OPN']],
+    ['OP03', ['OPN', 'OPN']],
+    ['OP04', ['OPN', 'OPN']],
+    ['OP05', ['OPN', 'OPN']],
+    ['OP06', ['OPN', 'OPN']],
+    ['OP07', ['OPN', 'OPN']],
+    ['OP08', ['OPN', 'OPN']],
+    ['OP09', ['OPN', 'OPN']],
+    ['OP10', ['OPN', 'OPN']],
+    ['OP11', ['OPN', 'OPN']],
+    ['OP12', ['OPN', 'OPN']],
+    ['OP13', ['OPN', 'OPN']],
+    ['OP14', ['OPN', 'OPN']],
+    ['OP15', ['OPN', 'OPN']],
+    ['OP16', ['OPN', 'OPN']],
+    ['OP17', ['OPN', 'OPN']],
+    ['OP18', ['OPN', 'OPN']],
+    ['OP19', ['OPN', 'OPN']],
+    ['OP20', ['OPN', 'OPN']],
+    ['OP21', ['OPN', 'OPN']],
+    ['OP22', ['OPN', 'OPN']],
+    ['OP23', ['OPN', 'OPN']],
+    ['OP24', ['OPN', 'OPN']],
+    ['OP25', ['OPN', 'OPN']],
+    ['OP26', ['OPN', 'OPN']],
+    ['OP27', ['OPN', 'OPN']],
+    ['OP28', ['OPN', 'OPN']],
+    ['OP29', ['OPN', 'OPN']],
+    ['OP30', ['OPN', 'OPN']],
+    ['OP31', ['OPN', 'OPN']],
+    ['OPS1', ['OPN', 'OPN']],
+    ['OP1S', ['OPN', 'SYN']],
+    ['AAS1', ['SYN', 'A']],
+    ['AB1S', ['A', 'SYN']],
+    ['AB2S', ['A', 'SYN']],
+    ['BB1S', ['B', 'SYN']],
+    ['BB2S', ['B', 'SYN']],
+    ['BBS1', ['SYN', 'B']],
+    ['ZZ01', ['Z', 'Z']],
+    ['ZZ02', ['Z', 'Z']],
+    ['ZZ1S', ['Z', 'SYN']],
+    ['ZZ2S', ['Z', 'SYN']],
+    ['ZZS1', ['SYN', 'Z']],
+    ['ZZS2', ['SYN', 'Z']],
+]);
+
+function getConformerColor(ntc: string, useLower: boolean): Color {
+    const item = NtCToClasses.get(ntc);
+    if (!item) return ErrorColor;
+    return ColorMapping.get(useLower ? item[1] : item[0]) ?? ErrorColor;
+}
+
+export const ConfalPyramidsColorThemeParams = {};
+export type ConfalPyramidsColorThemeParams = typeof ConfalPyramidsColorThemeParams
+export function getConfalPyramidsColorThemeParams(ctx: ThemeDataContext) {
+    return ConfalPyramidsColorThemeParams; // TODO return copy
+}
+
+export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values<ConfalPyramidsColorThemeParams>): ColorTheme<ConfalPyramidsColorThemeParams> {
+    function color(location: Location, isSecondary: boolean): Color {
+        if (CPT.isLocation(location)) {
+            const { pyramid, isLower } = location.data;
+            return getConformerColor(pyramid.NtC, isLower);
+        }
+
+        return DefaultColor;
+    }
+
+    return {
+        factory: ConfalPyramidsColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: Description,
+    };
+}
+
+export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramidsColorThemeParams, 'confal-pyramids'> = {
+    name: 'confal-pyramids',
+    label: 'Confal Pyramids',
+    category: ColorTheme.Category.Residue,
+    factory: ConfalPyramidsColorTheme,
+    getParams: getConfalPyramidsColorThemeParams,
+    defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.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 && data.structure.models[0].customProperties.reference(ConfalPyramidsProvider.descriptor, false)
+    }
+};

+ 172 - 0
src/extensions/dnatco/confal-pyramids/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 { ConfalPyramidsTypes as CPT } from './types';
+import { Column, Table } from '../../../mol-data/db';
+import { toTable } from '../../../mol-io/reader/cif/schema';
+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.PyramidsData | 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)) throw new Error('Data format must be mmCIF');
+        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 type ConfalPyramidsParams = typeof ConfalPyramidsParams;
+export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>;
+
+export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({
+    label: 'Confal Pyramids',
+    descriptor: CustomPropertyDescriptor({
+        name: 'confal_pyramids',
+    }),
+    type: 'static',
+    defaultParams: ConfalPyramidsParams,
+    getParams: (data: Model) => ConfalPyramidsParams,
+    isApplicable: (data: Model) => ConfalPyramids.isApplicable(data),
+    obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => {
+        const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props };
+        return ConfalPyramids.fromCif(ctx, data, p);
+    }
+});
+
+type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
+
+function createPyramidsFromCif(model: Model,
+    steps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
+    stepsSummary: StepsSummaryTable): CPT.PyramidsData {
+    const pyramids = new Array<CPT.Pyramid>();
+    const names = new Map<string, number>();
+    const locations = new Array<CPT.Location>();
+    let hasMultipleModels = false;
+
+    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 } = steps;
+
+    if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
+
+    for (let i = 0; i < _rowCount; i++) {
+        const model_num = PDB_model_number.value(i);
+        if (model_num !== model.modelNum) {
+            hasMultipleModels = true;
+            continue; // We are only interested in data for the current model
+        }
+
+        const { _NtC, _confal_score } = getNtCAndConfalScore(id.value(i), i, stepsSummary);
+
+        const pyramid = {
+            PDB_model_number: model_num,
+            name: name.value(i),
+            auth_asym_id_1: auth_asym_id_1.value(i),
+            auth_seq_id_1: auth_seq_id_1.value(i),
+            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: _confal_score,
+            NtC: _NtC
+        };
+
+        pyramids.push(pyramid);
+        names.set(pyramid.name, pyramids.length - 1);
+
+        locations.push(CPT.Location(pyramid, false));
+        locations.push(CPT.Location(pyramid, true));
+    }
+
+    return { pyramids, names, locations, hasMultipleModels };
+}
+
+function getNtCAndConfalScore(id: number, i: number, stepsSummary: StepsSummaryTable) {
+    const { step_id, confal_score, assigned_NtC } = 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) };
+    }
+    // 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) };
+    }
+    throw new Error('Inconsistent mmCIF data');
+}

+ 186 - 0
src/extensions/dnatco/confal-pyramids/representation.ts

@@ -0,0 +1,186 @@
+/**
+ * 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 { ConfalPyramids, ConfalPyramidsProvider } from './property';
+import { ConfalPyramidsUtil } from './util';
+import { ConfalPyramidsTypes as CPT } from './types';
+import { Interval } from '../../../mol-data/int';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { PrimitiveBuilder } from '../../../mol-geo/primitive/primitive';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
+import { Structure, StructureProperties, Unit } from '../../../mol-model/structure';
+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 { StructureGroup, UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
+import { VisualUpdateState } from '../../../mol-repr/util';
+import { VisualContext } from '../../../mol-repr/visual';
+import { getAltResidueLociFromId } from '../../../mol-repr/structure/visual/util/common';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
+import { NullLocation } from '../../../mol-model/location';
+
+const t = Mat4.identity();
+const w = Vec3.zero();
+const mp = Vec3.zero();
+
+function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) {
+    Vec3.sub(mp, v, w);
+    Vec3.scale(mp, mp, 0.5);
+    Vec3.add(mp, mp, w);
+}
+
+function shiftVertex(vec: Vec3, ref: Vec3, scale: number) {
+    Vec3.sub(w, vec, ref);
+    Vec3.scale(w, w, scale);
+    Vec3.add(vec, vec, w);
+}
+
+const ConfalPyramidsMeshParams = {
+    ...UnitsMeshParams
+};
+type ConfalPyramidsMeshParams = typeof ConfalPyramidsMeshParams;
+
+function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationIterator {
+    const { structure, group } = structureGroup;
+    const instanceCount = group.units.length;
+
+    const prop = ConfalPyramidsProvider.get(structure.model).value;
+    if (prop === undefined || prop.data === undefined) {
+        return LocationIterator(0, 1, () => NullLocation);
+    }
+
+    const { locations } = prop.data;
+
+    const getLocation = (groupIndex: number, instanceIndex: number) => {
+        if (locations.length <= groupIndex) return NullLocation;
+        return locations[groupIndex];
+    };
+    return LocationIterator(locations.length, instanceCount, getLocation);
+}
+
+function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
+
+    const prop = ConfalPyramidsProvider.get(structure.model).value;
+    if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
+
+    const { pyramids } = prop.data;
+    if (pyramids.length === 0) return Mesh.createEmpty(mesh);
+
+    const mb = MeshBuilder.createState(512, 512, mesh);
+
+    const handler = (pyramid: CPT.Pyramid, first: ConfalPyramidsUtil.FirstResidueAtoms, second: ConfalPyramidsUtil.SecondResidueAtoms, firsLocIndex: number, secondLocIndex: number) => {
+        if (firsLocIndex === -1 || secondLocIndex === -1)
+            throw new Error('Invalid location index');
+
+        const scale = (pyramid.confal_score - 20.0) / 100.0;
+        const O3 = first.O3.pos;
+        const OP1 = second.OP1.pos; const OP2 = second.OP2.pos; const O5 = second.O5.pos; const P = second.P.pos;
+
+        shiftVertex(O3, P, scale);
+        shiftVertex(OP1, P, scale);
+        shiftVertex(OP2, P, scale);
+        shiftVertex(O5, P, scale);
+        calcMidpoint(mp, O3, O5);
+
+        mb.currentGroup = firsLocIndex;
+        let pb = PrimitiveBuilder(3);
+        /* Upper part (for first residue in step) */
+        pb.add(O3, OP1, OP2);
+        pb.add(O3, mp, OP1);
+        pb.add(O3, OP2, mp);
+        MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
+
+        /* Lower part (for second residue in step */
+        mb.currentGroup = secondLocIndex;
+        pb = PrimitiveBuilder(3);
+        pb.add(mp, O5, OP1);
+        pb.add(mp, OP2, O5);
+        pb.add(O5, OP2, OP1);
+        MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
+    };
+
+    const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler);
+    walker.walk();
+
+    return MeshBuilder.getMesh(mb);
+}
+
+function getConfalPyramidLoci(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 prop = ConfalPyramidsProvider.get(structure.model).value;
+    if (prop === undefined || prop.data === undefined) return EmptyLoci;
+
+    const { locations } = prop.data;
+
+    if (locations.length <= groupId) return EmptyLoci;
+    const altId = StructureProperties.atom.label_alt_id(CPT.toElementLocation(locations[groupId]));
+    const rI = unit.residueIndex[locations[groupId].element.element];
+
+    return getAltResidueLociFromId(structure, unit, rI, altId);
+}
+
+function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+    return false; // TODO: Implement me
+}
+
+function ConfalPyramidsVisual(materialId: number): UnitsVisual<ConfalPyramidsMeshParams> {
+    return UnitsMeshVisual<ConfalPyramidsMeshParams>({
+        defaultProps: PD.getDefaultValues(ConfalPyramidsMeshParams),
+        createGeometry: createConfalPyramidsMesh,
+        createLocationIterator: createConfalPyramidsIterator,
+        getLoci: getConfalPyramidLoci,
+        eachLocation: eachConfalPyramid,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ConfalPyramidsMeshParams>, currentProps: PD.Values<ConfalPyramidsMeshParams>) => {
+        }
+    }, materialId);
+}
+const ConfalPyramidsVisuals = {
+    'confal-pyramids-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, UnitsMeshParams>) => UnitsRepresentation('Confal Pyramids Symbol Mesh', ctx, getParams, ConfalPyramidsVisual),
+};
+
+export const ConfalPyramidsParams = {
+    ...UnitsMeshParams
+};
+export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
+export function getConfalPyramidsParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(ConfalPyramidsParams);
+}
+
+export type ConfalPyramidsRepresentation = StructureRepresentation<ConfalPyramidsParams>;
+export function ConfalPyramidsRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ConfalPyramidsParams>): ConfalPyramidsRepresentation {
+    const repr = Representation.createMulti('Confal Pyramids', ctx, getParams, StructureRepresentationStateBuilder, ConfalPyramidsVisuals as unknown as Representation.Def<Structure, ConfalPyramidsParams>);
+    return repr;
+}
+
+export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvider({
+    name: 'confal-pyramids',
+    label: 'Confal Pyramids',
+    description: 'Displays schematic depiction of conformer classes and confal values',
+    factory: ConfalPyramidsRepresentation,
+    getParams: getConfalPyramidsParams,
+    defaultValues: PD.getDefaultValues(ConfalPyramidsParams),
+    defaultColorTheme: { name: 'confal-pyramids' },
+    defaultSizeTheme: { name: 'uniform' },
+    isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)),
+    ensureCustomProperties: {
+        attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true),
+        detach: (data) => ConfalPyramidsProvider.ref(data.model, false),
+    }
+});

+ 60 - 0
src/extensions/dnatco/confal-pyramids/types.ts

@@ -0,0 +1,60 @@
+/**
+ * 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 { DataLocation } from '../../../mol-model/location';
+import { ElementIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
+
+export namespace ConfalPyramidsTypes {
+    export type Pyramid = {
+        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
+    }
+
+    export interface PyramidsData {
+        pyramids: Array<Pyramid>,
+        names: Map<string, number>,
+        locations: Array<Location>,
+        hasMultipleModels: boolean
+    }
+
+    export interface LocationData {
+        readonly pyramid: Pyramid
+        readonly isLower: boolean;
+    }
+
+    export interface Element {
+        structure: Structure;
+        unit: Unit.Atomic;
+        element: ElementIndex;
+    }
+
+    export interface Location extends DataLocation<LocationData, Element> {}
+
+    export function Location(pyramid: Pyramid, isLower: boolean, structure?: Structure, unit?: Unit.Atomic, element?: ElementIndex) {
+        return DataLocation('pyramid', { pyramid, isLower }, { structure: structure as any, unit: unit as any, element: element as any });
+    }
+
+    export function isLocation(x: any): x is Location {
+        return !!x && x.kind === 'data-location' && x.tag === 'pyramid';
+    }
+
+    export function toElementLocation(location: Location) {
+        return StructureElement.Location.create(location.element.structure, location.element.unit, location.element.element);
+    }
+}

+ 299 - 0
src/extensions/dnatco/confal-pyramids/util.ts

@@ -0,0 +1,299 @@
+/**
+ * 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 { ConfalPyramidsProvider } from './property';
+import { ConfalPyramidsTypes as CPT } from './types';
+import { OrderedSet, Segmentation } from '../../../mol-data/int';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
+
+export namespace ConfalPyramidsUtil {
+    type Residue = Segmentation.Segment<ResidueIndex>;
+
+    export type AtomInfo = {
+        pos: Vec3,
+        index: ElementIndex,
+        fakeAltId: string,
+    };
+
+    export type FirstResidueAtoms = {
+        O3: AtomInfo,
+    };
+
+    export type SecondResidueAtoms = {
+        OP1: AtomInfo,
+        OP2: AtomInfo,
+        O5: AtomInfo,
+        P: AtomInfo,
+    };
+
+    type ResidueInfo = {
+        PDB_model_num: number,
+        asym_id: string,
+        auth_asym_id: string,
+        seq_id: number,
+        auth_seq_id: number,
+        comp_id: string,
+        alt_id: string,
+        ins_code: string,
+    };
+
+    export type Handler = (pyramid: CPT.Pyramid, first: FirstResidueAtoms, second: SecondResidueAtoms, firstLocIndex: number, secondLocIndex: number) => void;
+
+    function residueInfoFromLocation(loc: StructureElement.Location): ResidueInfo {
+        return {
+            PDB_model_num: StructureProperties.unit.model_num(loc),
+            asym_id: StructureProperties.chain.label_asym_id(loc),
+            auth_asym_id: StructureProperties.chain.auth_asym_id(loc),
+            seq_id: StructureProperties.residue.label_seq_id(loc),
+            auth_seq_id: StructureProperties.residue.auth_seq_id(loc),
+            comp_id: StructureProperties.atom.label_comp_id(loc),
+            alt_id: StructureProperties.atom.label_alt_id(loc),
+            ins_code: StructureProperties.residue.pdbx_PDB_ins_code(loc)
+        };
+    }
+
+    export function hasMultipleModels(unit: Unit.Atomic): boolean {
+        const prop = ConfalPyramidsProvider.get(unit.model).value;
+        if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
+        return prop.data.hasMultipleModels;
+    }
+
+    function getPossibleAltIdsIndices(eIFirst: ElementIndex, eILast: ElementIndex, structure: Structure, unit: Unit.Atomic): string[] {
+        const loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
+
+        const uIFirst = OrderedSet.indexOf(unit.elements, eIFirst);
+        const uILast = OrderedSet.indexOf(unit.elements, eILast);
+
+        const possibleAltIds: string[] = [];
+        for (let uI = uIFirst; uI <= uILast; uI++) {
+            loc.element = unit.elements[uI];
+            const altId = StructureProperties.atom.label_alt_id(loc);
+            if (altId !== '' && !possibleAltIds.includes(altId)) possibleAltIds.push(altId);
+        }
+
+        return possibleAltIds;
+    }
+
+    function getPossibleAltIdsResidue(residue: Residue, structure: Structure, unit: Unit.Atomic): string[] {
+        return getPossibleAltIdsIndices(unit.elements[residue.start], unit.elements[residue.end - 1], structure, unit);
+    }
+
+    class Utility {
+        protected getPyramidByName(name: string): { pyramid: CPT.Pyramid | undefined, index: number } {
+            const index = this.data.names.get(name);
+            if (index === undefined) return { pyramid: undefined, index: -1 };
+
+            return { pyramid: this.data.pyramids[index], index };
+        }
+
+        protected stepToName(entry_id: string, modelNum: number, locFirst: StructureElement.Location, locSecond: StructureElement.Location, fakeAltId_1: string, fakeAltId_2: string) {
+            const first = residueInfoFromLocation(locFirst);
+            const second = residueInfoFromLocation(locSecond);
+            const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
+            const alt_id_1 =  fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
+            const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
+            const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
+            const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';
+
+            return `${entry_id}${model_id}_${first.auth_asym_id}_${first.comp_id}${alt_id_1}_${first.auth_seq_id}${ins_code_1}_${second.comp_id}${alt_id_2}_${second.auth_seq_id}${ins_code_2}`;
+        }
+
+        constructor(unit: Unit.Atomic) {
+            const prop = ConfalPyramidsProvider.get(unit.model).value;
+            if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
+
+            this.data = prop.data;
+            this.hasMultipleModels = hasMultipleModels(unit);
+
+            this.entryId = unit.model.entryId.toLowerCase();
+            this.modelNum = unit.model.modelNum;
+        }
+
+        protected readonly data: CPT.PyramidsData
+        protected readonly hasMultipleModels: boolean;
+        protected readonly entryId: string;
+        protected readonly modelNum: number;
+    }
+
+    export class UnitWalker extends Utility {
+        private getAtomIndices(names: string[], residue: Residue): ElementIndex[] {
+            let rI = residue.start;
+            const rILast = residue.end - 1;
+            const indices: ElementIndex[] = [];
+
+            for (; rI !== rILast; rI++) {
+                const eI = this.unit.elements[rI];
+                const loc = StructureElement.Location.create(this.structure, this.unit, eI);
+                const thisName = StructureProperties.atom.label_atom_id(loc);
+                if (names.includes(thisName)) indices.push(eI);
+            }
+
+            if (indices.length === 0)
+                throw new Error(`Element ${name} not found on residue ${residue.index}`);
+
+            return indices;
+        }
+
+        private getAtomPositions(indices: ElementIndex[]): Vec3[] {
+            const pos = this.unit.conformation.invariantPosition;
+            const positions: Vec3[] = [];
+
+            for (const eI of indices) {
+                const v = Vec3.zero();
+                pos(eI, v);
+                positions.push(v);
+            }
+
+            return positions;
+        }
+
+        private handleStep(firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[]) {
+            const modelNum = this.hasMultipleModels ? this.modelNum : -1;
+            let ok = false;
+
+            const firstLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
+            const secondLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
+            for (let i = 0; i < firstAtoms.length; i++) {
+                const first = firstAtoms[i];
+                for (let j = 0; j < secondAtoms.length; j++) {
+                    const second = secondAtoms[j];
+                    firstLoc.element = first.O3.index;
+                    secondLoc.element = second.OP1.index;
+
+                    const name = this.stepToName(this.entryId, modelNum, firstLoc, secondLoc, first.O3.fakeAltId, second.OP1.fakeAltId);
+                    const { pyramid, index } = this.getPyramidByName(name);
+                    if (pyramid !== undefined) {
+                        const setLoc = (loc: CPT.Location, eI: ElementIndex) => {
+                            loc.element.structure = this.structure;
+                            loc.element.unit = this.unit;
+                            loc.element.element = eI;
+                        };
+
+                        const locIndex = index * 2;
+                        setLoc(this.data.locations[locIndex], firstLoc.element);
+                        setLoc(this.data.locations[locIndex + 1], secondLoc.element);
+                        this.handler(pyramid, first, second, locIndex, locIndex + 1);
+                        ok = true;
+                    }
+                }
+            }
+
+            if (!ok) throw new Error('Bogus step');
+        }
+
+        private processFirstResidue(residue: Residue, possibleAltIds: string[]) {
+            const indO3 = this.getAtomIndices(['O3\'', 'O3*'], residue);
+            const posO3 = this.getAtomPositions(indO3);
+
+            const altPos: FirstResidueAtoms[] = [
+                { O3: { pos: posO3[0], index: indO3[0], fakeAltId: '' } }
+            ];
+
+            for (let i = 1; i < indO3.length; i++) {
+                altPos.push({ O3: { pos: posO3[i], index: indO3[i], fakeAltId: '' } });
+            }
+
+            if (altPos.length === 1 && possibleAltIds.length > 1) {
+                /* We have some alternate positions on the residue but O3 does not have any - fake them */
+                altPos[0].O3.fakeAltId = possibleAltIds[0];
+
+                for (let i = 1; i < possibleAltIds.length; i++)
+                    altPos.push({ O3: { pos: posO3[0], index: indO3[0], fakeAltId: possibleAltIds[i] } });
+            }
+
+            return altPos;
+        }
+
+        private processSecondResidue(residue: Residue, possibleAltIds: string[]) {
+            const indOP1 = this.getAtomIndices(['OP1'], residue);
+            const indOP2 = this.getAtomIndices(['OP2'], residue);
+            const indO5 = this.getAtomIndices(['O5\'', 'O5*'], residue);
+            const indP = this.getAtomIndices(['P'], residue);
+
+            const posOP1 = this.getAtomPositions(indOP1);
+            const posOP2 = this.getAtomPositions(indOP2);
+            const posO5 = this.getAtomPositions(indO5);
+            const posP = this.getAtomPositions(indP);
+
+            const infoOP1: AtomInfo[] = [];
+            /* We use OP1 as "pivotal" atom. There is no specific reason
+             * to pick OP1, it is as good a choice as any other atom
+             */
+            if (indOP1.length === 1 && possibleAltIds.length > 1) {
+                /* No altIds on OP1, fake them */
+                for (const altId of possibleAltIds)
+                    infoOP1.push({ pos: posOP1[0], index: indOP1[0], fakeAltId: altId });
+            } else {
+                for (let i = 0; i < indOP1.length; i++)
+                    infoOP1.push({ pos: posOP1[i], index: indOP1[i], fakeAltId: '' });
+            }
+
+            const mkInfo = (i: number, indices: ElementIndex[], positions: Vec3[], altId: string) => {
+                if (i >= indices.length) {
+                    const last = indices.length - 1;
+                    return { pos: positions[last], index: indices[last], fakeAltId: altId };
+                }
+
+                return { pos: positions[i], index: indices[i], fakeAltId: altId };
+            };
+
+            const altPos: SecondResidueAtoms[] = [];
+            for (let i = 0; i < infoOP1.length; i++) {
+                const altId = infoOP1[i].fakeAltId;
+
+                const OP2 = mkInfo(i, indOP2, posOP2, altId);
+                const O5 = mkInfo(i, indO5, posO5, altId);
+                const P = mkInfo(i, indP, posP, altId);
+
+                altPos.push({ OP1: infoOP1[i], OP2, O5, P });
+            }
+
+            return altPos;
+        }
+
+        private step(residue: Residue): { firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[] } {
+            const firstPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit);
+            const firstAtoms = this.processFirstResidue(residue, firstPossibleAltIds);
+
+            residue = this.residueIt.move();
+
+            const secondPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit);
+            const secondAtoms = this.processSecondResidue(residue, secondPossibleAltIds);
+
+            return { firstAtoms, secondAtoms };
+        }
+
+        walk() {
+            while (this.chainIt.hasNext) {
+                this.residueIt.setSegment(this.chainIt.move());
+
+                let residue = this.residueIt.move();
+                while (this.residueIt.hasNext) {
+                    try {
+                        const { firstAtoms, secondAtoms } = this.step(residue);
+
+                        this.handleStep(firstAtoms, secondAtoms);
+                    } catch (error) {
+                        /* Skip and move along */
+                        residue = this.residueIt.move();
+                    }
+                }
+            }
+        }
+
+        constructor(private structure: Structure, private unit: Unit.Atomic, private handler: Handler) {
+            super(unit);
+
+            this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
+            this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+        }
+
+        private chainIt: Segmentation.SegmentIterator<ChainIndex>;
+        private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
+    }
+}

+ 8 - 0
src/extensions/dnatco/index.ts

@@ -0,0 +1,8 @@
+/**
+ * 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>
+ */
+
+export { DnatcoConfalPyramids } from './confal-pyramids/behavior';