model.ts 16.5 KB
Newer Older
Lukas Pravda's avatar
sync  
Lukas Pravda committed
1
namespace Model {
Lukas Pravda's avatar
Lukas Pravda committed
2
    "use strict";
Lukas Pravda's avatar
Lukas Pravda committed
3

4
    /**
Lukas Pravda's avatar
Lukas Pravda committed
5
     * Interaction type
6 7 8 9
     *
     * @export
     * @enum {number}
     */
Lukas Pravda's avatar
Lukas Pravda committed
10 11 12 13 14 15 16 17
    export enum InteractionType {
        AtomAtom,
        AtomPlane,
        PlanePlane,
        GroupPlane,
        GroupGroup
    }

18

Lukas Pravda's avatar
Lukas Pravda committed
19 20 21 22 23 24
    /**
     * What environment should be used
     *
     * @export
     * @enum {number}
     */
25
    export enum Environment {
Lukas Pravda's avatar
Lukas Pravda committed
26 27 28 29 30
        Production,
        Development,
        Internal
    }

Lukas Pravda's avatar
Lukas Pravda committed
31 32
    export class InteractionTypeUtil {
        public static parse(value: string) {
Lukas Pravda's avatar
Lukas Pravda committed
33 34 35 36 37
            if (value === 'atom_atom') return InteractionType.AtomAtom;
            else if (value === 'atom_plane') return InteractionType.AtomPlane;
            else if (value === 'plane_plane') return InteractionType.PlanePlane;
            else if (value === 'group_plane') return InteractionType.GroupPlane;
            else if (value === 'group_group') return InteractionType.GroupGroup;
Lukas Pravda's avatar
Lukas Pravda committed
38 39 40 41 42

            throw `Interaction type ${value} does not exist`;
        }
    }

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
    export class Interaction {
        sourceAtoms: string[];
        targetAtoms: string[];
        interactionType: InteractionType;
        interactionsClases: string[];
        distance: number;

        constructor(srcAtoms: string[], targetAtoms: string[], type: InteractionType, intClasses: string[], d: number) {
            this.sourceAtoms = srcAtoms;
            this.targetAtoms = targetAtoms;
            this.interactionType = type;
            this.interactionsClases = intClasses;
            this.distance = d;
        }
    }

Lukas Pravda's avatar
Lukas Pravda committed
59 60 61 62
    /**
     * Data model representing a single residue, which is a part of the
     * bound molecule. This residue is unique in the entire binding site.
     */
63
    export class Residue {
Lukas Pravda's avatar
sync  
Lukas Pravda committed
64
        id: string
65
        chainId: string;
Lukas Pravda's avatar
Lukas Pravda committed
66
        authorResidueNumber: number;
67 68
        chemCompId: string;
        authorInsertionCode: string;
Lukas Pravda's avatar
sync  
Lukas Pravda committed
69 70
        isLigand: boolean;

Lukas Pravda's avatar
Lukas Pravda committed
71
        constructor(data: any, isLigand: boolean) {
72
            this.chainId = data.chain_id;
73
            this.authorResidueNumber = data.author_residue_number;
Lukas Pravda's avatar
Lukas Pravda committed
74 75 76
            this.chemCompId = data.chem_comp_id;
            this.authorInsertionCode = data.author_insertion_code;
            this.id = `${this.chainId}${this.authorResidueNumber}${(this.authorInsertionCode === ' ' ? '' : this.authorInsertionCode)}`;
77
            this.isLigand = isLigand;
Lukas Pravda's avatar
sync  
Lukas Pravda committed
78 79
        }

Lukas Pravda's avatar
Lukas Pravda committed
80 81 82 83 84
        /**
         * Infer type of the residue, which is then used as a CSS source
         * for proper residue aesthetics.
         */
        public getResidueType(): string {
85
            if (this.isLigand) {
Lukas Pravda's avatar
sync  
Lukas Pravda committed
86 87 88
                return 'ligand';
            }

89
            if (this.chemCompId === 'HOH') {
Lukas Pravda's avatar
sync  
Lukas Pravda committed
90 91
                return 'water';
            }
92

93
            let code = ResidueProvider.getInstance().getAminoAcidAbbreviation(this.chemCompId);
Lukas Pravda's avatar
sync  
Lukas Pravda committed
94

95
            for (let [key, value] of Config.aaTypes) {
96
                if (value.includes(code)) {
Lukas Pravda's avatar
Lukas Pravda committed
97 98
                    return key;
                }
Lukas Pravda's avatar
Lukas Pravda committed
99
            }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
100

101
            return 'other';
Lukas Pravda's avatar
sync  
Lukas Pravda committed
102 103
        }

Lukas Pravda's avatar
Lukas Pravda committed
104 105 106
        public equals(other: Residue): boolean {
            if (!(other instanceof Residue)) return false;

Lukas Pravda's avatar
Lukas Pravda committed
107 108 109
            return this.id === other.id;
        }

110
        public toString(): string {
111
            return this.id;
Lukas Pravda's avatar
Lukas Pravda committed
112
        }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
113 114
    }

Lukas Pravda's avatar
Lukas Pravda committed
115
    export class InteractionNode implements d3.SimulationNodeDatum {
116
        id: string;
Lukas Pravda's avatar
Lukas Pravda committed
117
        residue: Residue;
118 119
        scale: number; // how is the node scaled up in comparison to its original size
        static: boolean // whether or not can be dragged and droped
120

Lukas Pravda's avatar
Lukas Pravda committed
121 122 123 124 125 126
        index?: number;
        x?: number;
        y?: number;
        vx?: number;
        vy?: number;
        fx?: number;
127
        fy?: number;
Lukas Pravda's avatar
Lukas Pravda committed
128

129
        constructor(r: Residue, scale: number, id: string, x: number = undefined, y: number = undefined) {
Lukas Pravda's avatar
Lukas Pravda committed
130
            this.residue = r;
131
            this.id = id;
132
            this.static = Boolean(scale < 1.0);
Lukas Pravda's avatar
Lukas Pravda committed
133 134
            this.scale = scale;

Lukas Pravda's avatar
Lukas Pravda committed
135 136 137 138 139 140 141 142
            if (x !== undefined) {
                this.fx = x;
                this.x = x;
            }
            if (y !== undefined) {
                this.fy = y;
                this.y = y;
            }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
143
        }
Lukas Pravda's avatar
Lukas Pravda committed
144

Lukas Pravda's avatar
Lukas Pravda committed
145 146
        public equals(other: InteractionNode): boolean {
            if (!(other instanceof InteractionNode)) return false;
Lukas Pravda's avatar
Lukas Pravda committed
147

148
            return (this.id === other.id && this.fx == other.fx && this.fy == other.fy);
Lukas Pravda's avatar
sync  
Lukas Pravda committed
149
        }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
150

Lukas Pravda's avatar
Lukas Pravda committed
151
        public toString(): string {
152
            return `${this.residue.chemCompId} ${this.residue.chainId} ${this.residue.authorResidueNumber}${this.residue.authorInsertionCode}`;
153
        }
Lukas Pravda's avatar
Lukas Pravda committed
154

Lukas Pravda's avatar
Lukas Pravda committed
155
        public toTooltip(): string {
Lukas Pravda's avatar
Lukas Pravda committed
156 157
            return `<span>${this.toString()}</span>`;
        }
Lukas Pravda's avatar
Lukas Pravda committed
158 159
    }

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
    export abstract class Link {
        public source: InteractionNode;
        public target: InteractionNode;

        constructor(source: InteractionNode, target: InteractionNode) {
            this.source = source;
            this.target = target;
        }


        public containsBothNodes(a: InteractionNode, b: InteractionNode): boolean {
            let condA = this.source.equals(a) && this.target.equals(b);
            let condB = this.source.equals(b) && this.target.equals(a);

            return condA || condB;
        }
Lukas Pravda's avatar
Lukas Pravda committed
176

177 178 179 180 181 182 183 184 185 186 187 188 189 190

        public containsNode(node: InteractionNode) {
            return this.source.equals(node) || this.target.equals(node);
        }


        public containsResidue(n: Residue): boolean {
            return (this.target.residue.equals(n) || this.source.residue.equals(n));

        }        

        abstract getLinkClass(): string;
        abstract hasClash(): boolean;
        abstract toTooltip(): string;
Lukas Pravda's avatar
Lukas Pravda committed
191 192
    }

193
    export class ResidueResidueLink extends Link {
194 195
        target: InteractionNode;
        source: InteractionNode
Lukas Pravda's avatar
Lukas Pravda committed
196
        interactions: Map<InteractionType, Array<string>>;
Lukas Pravda's avatar
Lukas Pravda committed
197

Lukas Pravda's avatar
Lukas Pravda committed
198
        constructor(source: InteractionNode, target: InteractionNode, interactions: any) {
199
            super(source, target);
Lukas Pravda's avatar
Lukas Pravda committed
200
            this.interactions = new Map<InteractionType, Array<string>>();
Lukas Pravda's avatar
sync  
Lukas Pravda committed
201

Lukas Pravda's avatar
Lukas Pravda committed
202 203
            Object.keys(interactions).forEach(x => {
                let intType = InteractionTypeUtil.parse(x);
Lukas Pravda's avatar
Lukas Pravda committed
204
                this.interactions.set(intType, interactions[x]);
Lukas Pravda's avatar
Lukas Pravda committed
205
            });
Lukas Pravda's avatar
sync  
Lukas Pravda committed
206 207
        }

Lukas Pravda's avatar
Lukas Pravda committed
208 209 210 211 212 213 214 215 216
        public hasClash(): boolean {
            this.interactions.forEach(x => {
                if (x.includes('clash')) {
                    return true
                }
            });
            return false;
        }

Lukas Pravda's avatar
Lukas Pravda committed
217
        public isBoundMoleculeLink(): boolean {
Lukas Pravda's avatar
Lukas Pravda committed
218 219 220 221 222 223 224
            return this.source.residue.isLigand && this.target.residue.isLigand && this.interactions.get(InteractionType.AtomAtom).includes('covalent');
        }

        public toTooltip(): string {
            let values = new Array<string>();

            for (let value of this.interactions.values()) {
225
                values = values.concat(value);
Lukas Pravda's avatar
Lukas Pravda committed
226
            }
Lukas Pravda's avatar
Lukas Pravda committed
227 228
            let result = values.reduce((x, y) => `${x}, ${y}`)
            return `<ul>${result}</ul>`
Lukas Pravda's avatar
Lukas Pravda committed
229
        }
230

Lukas Pravda's avatar
Lukas Pravda committed
231 232 233 234 235 236 237
        /**
         * Check all the interactions, which are a part of the contact
         * and determine their class. This is later on used for selection
         * of the correct aesthetics with CSS.
         */
        public getLinkClass(): string {
            if (this.isBoundMoleculeLink()) return 'ligand';
238

Lukas Pravda's avatar
Lukas Pravda committed
239
            // JS map preserves order of elements
Lukas Pravda's avatar
Lukas Pravda committed
240
            for (let [key, value] of Config.interactionsClasses) {
Lukas Pravda's avatar
Lukas Pravda committed
241 242 243 244
                for (let interactionDetails of this.interactions.values()) {
                    if (interactionDetails.filter(x => -1 !== value.indexOf(x)).length > 0) {
                        return key;
                    }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
245 246
                }
            }
Lukas Pravda's avatar
Lukas Pravda committed
247
            return 'other';
Lukas Pravda's avatar
sync  
Lukas Pravda committed
248
        }
Lukas Pravda's avatar
Lukas Pravda committed
249
    }
250 251


252
    export class LigandResidueLink extends Link {
253
        interaction: Array<Interaction>;
Lukas Pravda's avatar
Lukas Pravda committed
254

255

Lukas Pravda's avatar
Lukas Pravda committed
256
        constructor(begin: InteractionNode, end: InteractionNode, beginAtoms: string[], endAtoms: string[],
Lukas Pravda's avatar
Lukas Pravda committed
257
            interactionType: string, interactionDetails: string[], distance: number) {
Lukas Pravda's avatar
Lukas Pravda committed
258

259
            super(begin, end);
260 261 262 263 264
            this.interaction = new Array<Interaction>();
            this.interaction.push(
                new Interaction(beginAtoms, endAtoms, InteractionTypeUtil.parse(interactionType.replace('-', '_')), interactionDetails, distance)
            );
        }
Lukas Pravda's avatar
Lukas Pravda committed
265

266 267 268 269
        public addInteraction(beginAtoms: string[], endAtoms: string[], interactionType: string, interactionDetails: string[], distance: number): void {
            this.interaction.push(
                new Interaction(beginAtoms, endAtoms, InteractionTypeUtil.parse(interactionType.replace('-', '_')), interactionDetails, distance)
            );
Lukas Pravda's avatar
sync  
Lukas Pravda committed
270 271
        }

272

Lukas Pravda's avatar
Lukas Pravda committed
273
        /**
Lukas Pravda's avatar
Lukas Pravda committed
274 275 276
         * Check all the interactions, which are a part of the contact
         * and determine their class. This is later on used for selection
         * of the correct aesthetics with CSS.
Lukas Pravda's avatar
Lukas Pravda committed
277
         */
Lukas Pravda's avatar
Lukas Pravda committed
278 279
        public getLinkClass(): string {
            // JS map preserves order of elements
280 281
            let allInteractions = this.interaction.map(x => x.interactionsClases).reduce((x, y) => x.concat(y));

Lukas Pravda's avatar
Lukas Pravda committed
282
            for (let [key, value] of Config.interactionsClasses) {
283 284
                if (allInteractions.filter(x => -1 !== value.indexOf(x)).length > 0) {
                    return key;
Lukas Pravda's avatar
Lukas Pravda committed
285
                }
Lukas Pravda's avatar
Lukas Pravda committed
286
            }
Lukas Pravda's avatar
Lukas Pravda committed
287
            return 'other';
Lukas Pravda's avatar
Lukas Pravda committed
288
        }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
289

Lukas Pravda's avatar
Lukas Pravda committed
290
        public hasClash(): boolean {
291 292 293 294 295 296 297 298 299 300 301 302
            this.interaction.forEach(x => {
                x.interactionsClases.forEach(y => {
                    if (y.includes('clash')) {
                        return true
                    }
                });
            });
            return false;

        }

        public toTooltip() {
303
            let msg = new Set();
304
            let rProvider = ResidueProvider.getInstance();
305

306
            this.interaction.forEach(x => {
Lukas Pravda's avatar
Lukas Pravda committed
307
                let isMainChain = !this.target.residue.isLigand && x.targetAtoms.every(y => Config.backboneAtoms.includes(y));
308 309 310
                let targetAbbreviation = rProvider.getAminoAcidAbbreviation(this.target.residue.chemCompId);
                let isResidue = Boolean(targetAbbreviation !== 'X');
                let interactionFlag = '';
311

312 313
                if (isMainChain && isResidue) interactionFlag = 'backbone';
                else if (!isMainChain && isResidue) interactionFlag = 'side chain';
314 315
                else interactionFlag = 'ligand';

316
                msg.add(`<li><span>${interactionFlag}</span> interaction (<b>${x.targetAtoms}</b> | ${x.interactionsClases}): ${x.distance}Å</li>`);
317 318
            });

319
            return `<ul>${Array.from(msg.values()).join('\n')}</ul>`;
320 321
        }

Lukas Pravda's avatar
Lukas Pravda committed
322
    }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
323

Lukas Pravda's avatar
Lukas Pravda committed
324

Lukas Pravda's avatar
Lukas Pravda committed
325 326 327 328 329 330 331
    /**
     * DataObject representing the entire binding site with the bound
     * molecule and all its interactions.
     */
    export class BindingSite {
        pdbId: string;
        bmId: string;
332

Lukas Pravda's avatar
Lukas Pravda committed
333 334
        residues: Residue[];
        interactionNodes: InteractionNode[];
335 336
        links: Link[];

337 338
        private tmpResidueSet: ObjectSet<Residue>;
        private tmpNodesSet: ObjectSet<InteractionNode>;
Lukas Pravda's avatar
Lukas Pravda committed
339

Lukas Pravda's avatar
Lukas Pravda committed
340 341 342
        constructor() {
            this.residues = new Array<Residue>();
            this.interactionNodes = new Array<InteractionNode>();
343
            this.links = new Array<Link>();
Lukas Pravda's avatar
Lukas Pravda committed
344
        }
345

346

Lukas Pravda's avatar
Lukas Pravda committed
347 348 349
        public fromBoundMolecule(pdbId: string, data: any): BindingSite {
            this.pdbId = pdbId;
            this.bmId = data.bm_id;
Lukas Pravda's avatar
Lukas Pravda committed
350

351 352 353 354
            this.tmpResidueSet = new ObjectSet<Residue>();
            this.tmpNodesSet = new ObjectSet<InteractionNode>();

            data.composition.ligands.forEach(x => this.tmpResidueSet.tryAdd(new Residue(x, true)));
Lukas Pravda's avatar
Lukas Pravda committed
355
            data.interactions.forEach(x => {
356
                let bgnNode = this.processResidueInteractionPartner(x.begin);
Lukas Pravda's avatar
Lukas Pravda committed
357
                let endNode = this.processResidueInteractionPartner(x.end);
358 359

                let link = new ResidueResidueLink(bgnNode, endNode, x.interactions)
Lukas Pravda's avatar
Lukas Pravda committed
360

Lukas Pravda's avatar
Lukas Pravda committed
361
                this.links.push(link);
Lukas Pravda's avatar
Lukas Pravda committed
362
            });
Lukas Pravda's avatar
Lukas Pravda committed
363

364 365 366 367 368 369 370 371 372 373 374 375 376
            let searchableNodes = Array.from(this.tmpNodesSet);
            data.composition.connections.forEach(pair => {
                let bgn = searchableNodes.find(x => x.residue.id === pair[0]);
                let end = searchableNodes.find(x => x.residue.id === pair[1]);

                let result = this.links.find(x => x.containsBothNodes(bgn, end));

                if (result === undefined) {
                    let link = new ResidueResidueLink(bgn, end, { 'atom_atom': ['covalent'] });
                    this.links.push(link);
                }
            });

377 378
            this.interactionNodes = Array.from(this.tmpNodesSet.values());

Lukas Pravda's avatar
Lukas Pravda committed
379
            return this;
Lukas Pravda's avatar
sync  
Lukas Pravda committed
380
        }
Lukas Pravda's avatar
Lukas Pravda committed
381

382

Lukas Pravda's avatar
Lukas Pravda committed
383
        private processResidueInteractionPartner(r: any): InteractionNode {
384 385 386 387
            let residue = new Residue(r, false);
            residue = this.tmpResidueSet.tryAdd(residue);
            let node = new InteractionNode(residue, 1.0, residue.id);
            node = this.tmpNodesSet.tryAdd(node);
Lukas Pravda's avatar
Lukas Pravda committed
388

389
            return node;
Lukas Pravda's avatar
Lukas Pravda committed
390 391
        }

392

Lukas Pravda's avatar
Lukas Pravda committed
393 394 395 396 397 398
        private processLigandInteractionPartner(r: Residue, d: Depiction, atom_names: string[]): InteractionNode {
            let scale = 0.0;
            if (r.isLigand) scale = atom_names.length > 1 ? 0.5 : 0.0
            else scale = 1.0

            let center = d.getCenter(atom_names);
399
            let id = `${r.id}_${atom_names.sort().reduce((x, y) => x + "_" + y)}`;
Lukas Pravda's avatar
Lukas Pravda committed
400

401 402 403 404
            let node = new InteractionNode(r, scale, id, center.x, center.y);
            node = this.tmpNodesSet.tryAdd(node);

            return node;
Lukas Pravda's avatar
Lukas Pravda committed
405 406 407 408 409
        }


        public fromLigand(pdbId: string, data: any, ligand: Depiction): BindingSite {
            this.pdbId = pdbId;
410
            this.bmId = `${data.ligand.chem_comp_id}_${data.ligand.chain_id}_${data.ligand.author_residue_number}`;
Lukas Pravda's avatar
Lukas Pravda committed
411

412 413
            this.tmpResidueSet = new ObjectSet<Residue>();
            this.tmpNodesSet = new ObjectSet<InteractionNode>();
414
            let tmpLinks = new Array<LigandResidueLink>();
415

Lukas Pravda's avatar
Lukas Pravda committed
416
            let ligandResidue = new Residue(data.ligand, true);
417
            this.tmpResidueSet.tryAdd(ligandResidue);
Lukas Pravda's avatar
Lukas Pravda committed
418

419 420
            ligand.atoms.forEach(x => this.processLigandInteractionPartner(ligandResidue, ligand, [x.name]));

Lukas Pravda's avatar
Lukas Pravda committed
421
            data.interactions.forEach(x => {
422
                let bgnNode = this.processLigandInteractionPartner(ligandResidue, ligand, x.ligand_atoms);
Lukas Pravda's avatar
Lukas Pravda committed
423
                let endNode = this.processResidueInteractionPartner(x.end);
424 425 426 427

                if (bgnNode.residue.equals(endNode.residue)) {
                    this.tmpNodesSet.delete(endNode);
                    return; // we do not want to show 'self interactions'  
428
                }
429

430
                let tmpLink = tmpLinks.find(x => x.containsBothNodes(bgnNode, endNode));
431 432 433 434

                if (tmpLink !== undefined) {
                    tmpLink.addInteraction(x.ligand_atoms, x.end.atom_names, x.interaction_type, x.interaction_details, x.distance);
                } else {
435
                    let link = new LigandResidueLink(bgnNode, endNode, x.ligand_atoms, x.end.atom_names, x.interaction_type, x.interaction_details, x.distance);
436
                    tmpLinks.push(link);
437
                }
Lukas Pravda's avatar
Lukas Pravda committed
438
            });
439 440

            this.interactionNodes = Array.from(this.tmpNodesSet);
441
            this.links = this.filterOutAromaticAtomAtomInteractions(tmpLinks);
442

Lukas Pravda's avatar
Lukas Pravda committed
443
            return this;
Lukas Pravda's avatar
Lukas Pravda committed
444
        }
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482

        /**
         * Filter out Aromatic atom atom interactions provided there is
         * an evidence of the same atom being part of the stacking information
         * 
         * This is for clarity purposes only.
         *
         * @private
         * @param {Array<LigandResidueLink>} src
         * @returns {Array<LigandResidueLink>}
         * @memberof BindingSite
         */
        private filterOutAromaticAtomAtomInteractions(src: Array<LigandResidueLink>): Array<LigandResidueLink> {
            let result = new Array<LigandResidueLink>();
            src.forEach((x: LigandResidueLink) => {
                let isAtomAtom = x.interaction.map(y => y.interactionType).every(z => z === InteractionType.AtomAtom);

                if (x.getLinkClass() === 'aromatic' && isAtomAtom) {
                    let targetAtoms = x.interaction.map(y => y.targetAtoms).reduce((a, b) => a.concat(b));
                    let otherInteractions = new Set(src.filter(y => y.target.equals(x.target)));
                    otherInteractions.delete(x);

                    let otherBoundAtoms = Array.from(otherInteractions)
                        .map(y => y.interaction)
                        .reduce((a, b) => a.concat(b))
                        .map(y => y.targetAtoms)
                        .reduce((a, b) => a.concat(b));

                    if (otherBoundAtoms.includes(targetAtoms[0])) {
                        return;
                    }
                }

                result.push(x);
            });

            return result;
        }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
483
    }
Lukas Pravda's avatar
Lukas Pravda committed
484

485
}