manager.ts 34 KB
Newer Older
Lukas Pravda's avatar
Lukas Pravda committed
1
class Visualization {
Lukas Pravda's avatar
Lukas Pravda committed
2 3
    // component related
    private parent: HTMLElement;
Lukas Pravda's avatar
Lukas Pravda committed
4

Lukas Pravda's avatar
Lukas Pravda committed
5
    // #region svg properties
Lukas Pravda's avatar
Lukas Pravda committed
6 7
    private simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>;
    private svg: d3.Selection<any, {}, HTMLElement, any>;
Lukas Pravda's avatar
Lukas Pravda committed
8
    private canvas: d3.Selection<SVGGElement, {}, HTMLElement, any>;
Lukas Pravda's avatar
Lukas Pravda committed
9

Lukas Pravda's avatar
Lukas Pravda committed
10 11 12 13 14
    private depictionRoot: d3.Selection<SVGGElement, {}, HTMLElement, any>;
    private nodesRoot: d3.Selection<SVGGElement, {}, HTMLElement, any>;
    private linksRoot: d3.Selection<SVGGElement, {}, HTMLElement, any>;

    private zoomHandler: d3.ZoomBehavior<Element, unknown>
Lukas Pravda's avatar
Lukas Pravda committed
15 16

    private nodes: any;
Lukas Pravda's avatar
Lukas Pravda committed
17
    private links: any;
Lukas Pravda's avatar
Lukas Pravda committed
18
    // #endregion
Lukas Pravda's avatar
Lukas Pravda committed
19

Lukas Pravda's avatar
Lukas Pravda committed
20
    // #region data properties    
Lukas Pravda's avatar
Lukas Pravda committed
21
    private environment: Model.Environment;
Lukas Pravda's avatar
Lukas Pravda committed
22
    private pdbId: string;
23 24
    private bindingSites: Model.BindingSite[];
    private presentBindingSite: Model.BindingSite;
Lukas Pravda's avatar
Lukas Pravda committed
25
    private depiction: Depiction;
Lukas Pravda's avatar
Lukas Pravda committed
26

Lukas Pravda's avatar
Lukas Pravda committed
27
    private visualsMapper: VisualsMapper;
Lukas Pravda's avatar
Lukas Pravda committed
28
    private interactionsData: any;
Lukas Pravda's avatar
Lukas Pravda committed
29
    private selectedResidueHash: string;
30 31

    private rProvider: ResidueProvider;
Lukas Pravda's avatar
Lukas Pravda committed
32

33
    public fullScreen: boolean;
Lukas Pravda's avatar
Lukas Pravda committed
34
    // #endregion
Lukas Pravda's avatar
Lukas Pravda committed
35

Lukas Pravda's avatar
Lukas Pravda committed
36
    constructor(element: HTMLElement, uiParameters: Config.UIParameters = undefined, env: string = "production") {
Lukas Pravda's avatar
Lukas Pravda committed
37
        this.parent = element;
Lukas Pravda's avatar
Lukas Pravda committed
38
        this.environment = this.parseEnvironment(env);
Lukas Pravda's avatar
Lukas Pravda committed
39 40
        this.parent.style.cssText += "display: block; height: 100%; width: 100%; position: relative;";

41 42
        this.visualsMapper = new VisualsMapper(this.environment);
        this.rProvider = ResidueProvider.getInstance(this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
43
        this.fullScreen = false;
44
        this.bindingSites = new Array<Model.BindingSite>();
Lukas Pravda's avatar
Lukas Pravda committed
45

Lukas Pravda's avatar
Lukas Pravda committed
46
        if (uiParameters === undefined) uiParameters = new Config.UIParameters();
Lukas Pravda's avatar
Lukas Pravda committed
47

Lukas Pravda's avatar
Lukas Pravda committed
48
        new UI(this.parent, this).register(uiParameters);
Lukas Pravda's avatar
Lukas Pravda committed
49

Lukas Pravda's avatar
Lukas Pravda committed
50 51
        this.svg = d3.select(this.parent)
            .append('div')
52
            .attr('id', 'pdb-lig-env-root')
Lukas Pravda's avatar
Lukas Pravda committed
53
            .append('svg')
Lukas Pravda's avatar
Lukas Pravda committed
54
            .style('background-color', 'white')
Lukas Pravda's avatar
Lukas Pravda committed
55
            .attr('xmlns', 'http://www.w3.org/2000/svg')
Lukas Pravda's avatar
Lukas Pravda committed
56
            .attr('width', '100%')
Lukas Pravda's avatar
Lukas Pravda committed
57 58 59 60 61 62 63
            .attr('height', '100%');

        this.canvas = this.svg.append('g').attr('id', 'vis-root');
        this.linksRoot = this.canvas.append('g').attr('id', 'links');
        this.depictionRoot = this.canvas.append('g').attr('id', 'depiction');
        this.nodesRoot = this.canvas.append('g').attr('id', 'nodes');

Lukas Pravda's avatar
Lukas Pravda committed
64
        if (uiParameters.zoom) this.zoomHandler = this.getZoomHandler();
Lukas Pravda's avatar
Lukas Pravda committed
65

Lukas Pravda's avatar
Lukas Pravda committed
66 67 68
        document.addEventListener(Config.molstarClickEvent, e => this.nodeMouseEnterEventHandler(e));
        document.addEventListener(Config.molstarMouseoverEvent, e => this.nodeMouseEnterEventHandler(e));
        document.addEventListener(Config.molstarMouseoutEvent, () => this.nodeMouseLeaveEventHandler());
Lukas Pravda's avatar
Lukas Pravda committed
69

Lukas Pravda's avatar
Lukas Pravda committed
70 71
        d3.select(this.parent).on('resize', () => this.resize());
        this.addMarkers();
Lukas Pravda's avatar
Lukas Pravda committed
72 73
    }

Lukas Pravda's avatar
Lukas Pravda committed
74
    // #region even handlers
75 76 77 78 79 80
    private getZoomHandler() {
        return d3.zoom()
            .scaleExtent([1 / 10, 10])
            .on('zoom', () => this.canvas
                .attr('transform', d3.event.transform));
    }
Lukas Pravda's avatar
Lukas Pravda committed
81

82

Lukas Pravda's avatar
Lukas Pravda committed
83
    private nodeMouseEnterEventHandler(e: any) {
84 85
        if (this.fullScreen) return;

Lukas Pravda's avatar
Lukas Pravda committed
86
        let hash = `${e.eventData.auth_asym_id}${e.eventData.auth_seq_id}${e.eventData.ins_code}`;
Lukas Pravda's avatar
Lukas Pravda committed
87

Lukas Pravda's avatar
Lukas Pravda committed
88
        this.nodes?.each((node: Model.InteractionNode, index: number, group: any) => {
89 90
            this.nodeDim(node, index, group);
            
Lukas Pravda's avatar
Lukas Pravda committed
91 92 93 94 95 96 97 98 99 100
            if (node.id === hash) {
                this.selectedResidueHash = hash;
                this.nodeHighlight(node, index, group);
                return;
            }
        });

    }


Lukas Pravda's avatar
Lukas Pravda committed
101
    private nodeMouseLeaveEventHandler() {
102 103
        if (this.fullScreen) return;

Lukas Pravda's avatar
Lukas Pravda committed
104 105 106 107 108 109
        this.nodes?.each((node: Model.InteractionNode, index: number, group: any) => {
            if (node.id == this.selectedResidueHash) {
                this.nodeDim(node, index, group);
                return;
            }
        });
Lukas Pravda's avatar
Lukas Pravda committed
110 111 112
    }

    private linkMouseClickEventHandler(x: Model.Link) {
Lukas Pravda's avatar
Lukas Pravda committed
113
        this.fireExternalLinkEvent(x, Config.interactionClickEvent);
Lukas Pravda's avatar
Lukas Pravda committed
114 115
    }

Lukas Pravda's avatar
Lukas Pravda committed
116 117 118 119
    private linkMouseOverEventHandler(x: Model.Link, index: number, group: any) {
        let parent = d3.select(group[index]).node().parentNode;
        d3.select(parent).classed('pdb-lig-env-svg-bond-highlighted', true);

Lukas Pravda's avatar
Lukas Pravda committed
120
        this.fireExternalLinkEvent(x, Config.interactionMouseoverEvent);
Lukas Pravda's avatar
Lukas Pravda committed
121 122
    }

Lukas Pravda's avatar
Lukas Pravda committed
123 124 125 126
    private linkMouseOutEventHandler(index: number, group: any) {
        let parent = d3.select(group[index]).node().parentNode;
        d3.select(parent).classed('pdb-lig-env-svg-bond-highlighted', false);

127
        this.fireExternalLinkLeaveEvent();
Lukas Pravda's avatar
Lukas Pravda committed
128
    }
Lukas Pravda's avatar
Lukas Pravda committed
129

Lukas Pravda's avatar
Lukas Pravda committed
130
    private dragHandler = d3.drag()
131
        .filter((x: Model.InteractionNode) => !x.static)
Lukas Pravda's avatar
Lukas Pravda committed
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
        .on('start', (x: Model.InteractionNode) => {
            if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();

            x.fx = x.x;
            x.fy = x.y;
        })
        .on('drag', (x: Model.InteractionNode) => {
            x.fx = d3.event.x;
            x.fy = d3.event.y;
        })
        .on('end', (x: Model.InteractionNode) => {
            if (!d3.event.active) this.simulation.alphaTarget(0);
            x.fx = d3.event.x;
            x.fy = d3.event.y;
        });

    // #endregion event handlers

150
    // #region public methods
Lukas Pravda's avatar
Lukas Pravda committed
151 152 153 154 155 156 157 158 159 160 161 162
    /**
     * Download bound molecule interactions data from PDBe Graph API end point
     * /pdb/bound_molecule_interactions
     * 
     * Correct parameters can be obtained using API call:
     * /pdb/bound_molecules
     *
     * @param {string} pdbid
     * @param {string} bmId bound molecule identifier: e.g. bm1, bm2, ...
     * @memberof Visualization
     */
    public initBoundMoleculeInteractions(pdbid: string, bmId: string) {
Lukas Pravda's avatar
Lukas Pravda committed
163
        this.pdbId = pdbid;
Lukas Pravda's avatar
Lukas Pravda committed
164
        let url = Resources.boundMoleculeAPI(pdbid, bmId, this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
165

Lukas Pravda's avatar
Lukas Pravda committed
166
        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
167
            .catch(e => this.processError(e, 'No interactions data are available.'))
168 169 170
            .then((data: any) => this.addBoundMoleculeInteractions(data, bmId))
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
    }

    /**
     * Download carbohydrate interactions data from PDBe Graph API end point
     * /pdb/carbohydrate_polymer_interactions
     * 
     * Correct parameters can be obtained using API call:
     * /pdb/bound_molecules
     *
     * @param {string} pdbid
     * @param {string} bmId bound molecule identifier: e.g. bm1, bm2, ...
     * @param {string} entityId
     * @memberof Visualization
     */
    public initCarbohydratePolymerInteractions(pdbid: string, bmId: string, entityId: string) {
        this.pdbId = pdbid;
Lukas Pravda's avatar
Lukas Pravda committed
187
        let url = Resources.carbohydratePolymerAPI(pdbid, bmId, entityId, this.environment);
188 189 190 191

        d3.json(url)
            .catch(e => this.processError(e, 'No interactions to display'))
            .then((data: any) => this.addBoundMoleculeInteractions(data, bmId))
192 193
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
194
    }
Lukas Pravda's avatar
Lukas Pravda committed
195

Lukas Pravda's avatar
Lukas Pravda committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209
    /**
     * Download ligand interactions data from PDBe Graph API end point
     * /pdb/bound_ligand_interactions.
     * 
     * Correct parameters can be obtained using API call:
     * /pdb/bound_molecules
     * 
     * @param {string} pdbId pdb id
     * @param {number} resId residue number aka: auth_seq_id
     * @param {string} chainId chain id aka: auth_asym_id
     * @memberof Visualization
     */
    public initLigandInteractions(pdbId: string, resId: number, chainId: string) {
        this.pdbId = pdbId;
Lukas Pravda's avatar
Lukas Pravda committed
210
        let url = Resources.ligandInteractionsAPI(pdbId, chainId, resId, this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
211

Lukas Pravda's avatar
Lukas Pravda committed
212
        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
213
            .catch(e => this.processError(e, 'No interactions data are available.'))
214 215 216
            .then((data: any) => this.addLigandInteractions(data))
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
217 218
    }

Lukas Pravda's avatar
Lukas Pravda committed
219 220 221 222 223 224 225 226 227
    /**
     * Download ligand structure given the anotation generated by the 
     * PDBeChem process.
     *
     * @param {string} ligandId
     * @returns
     * @memberof Visualization
     */
    public async initLigandDisplay(ligandId: string) {
228
        const ligandUrl = Resources.ligandAnnotationAPI(ligandId, this.environment);
229

Lukas Pravda's avatar
Lukas Pravda committed
230
        return d3.json(ligandUrl)
Lukas Pravda's avatar
Lukas Pravda committed
231
            .catch(e => this.processError(e, `Component ${ligandId} was not found.`))
232
            .then((d: any) => this.addDepiction(d, true))
Lukas Pravda's avatar
Lukas Pravda committed
233
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
234 235
    }

Lukas Pravda's avatar
Lukas Pravda committed
236

Lukas Pravda's avatar
Lukas Pravda committed
237 238 239 240 241
    /**
     * Add depiction to the canvas from external resource.
     *
     * @param {*} depiction Content of annotation.json file generated by
     * the PDBeChem process.
242 243
     * @param {*} withClarityNodes Control if shadow nodes should be drawn
     * in the background of nodes with labels
Lukas Pravda's avatar
Lukas Pravda committed
244 245
     * @memberof Visualization
     */
246
    public addDepiction(depiction: any, withClarityNodes: boolean) {
Lukas Pravda's avatar
Lukas Pravda committed
247
        this.depiction = new Depiction(this.depictionRoot, depiction);
248
        this.depiction.draw(withClarityNodes);
Lukas Pravda's avatar
Lukas Pravda committed
249 250 251 252
    }


    /**
253 254
     * Add atom highlight to the ligand structure. The previous highlight
     * is going to be removed.
Lukas Pravda's avatar
Lukas Pravda committed
255
     *
256 257
     * @param {string[]} highlight List of atom names to be highlighted.
     * @param {string} [color=undefined] Color in #HEXHEX format.
Lukas Pravda's avatar
Lukas Pravda committed
258 259 260 261 262 263
     * @memberof Visualization
     */
    public addLigandHighlight(highlight: string[], color: string = undefined) {
        this.depiction.highlightSubgraph(highlight, color);
    }

264 265 266 267 268 269 270 271 272 273 274 275
    /**
     * Add contours to the ligand structure. The previous contours are
     * going to be removed.
     *
     * @param {*} data
     * @memberof Visualization
     */
    public addContours(data: any) {
        this.depiction.addContour(data);
    }


Lukas Pravda's avatar
Lukas Pravda committed
276 277 278 279 280 281 282 283
    public toogleZoom(active: boolean) {
        if (active) {
            this.zoomHandler = this.getZoomHandler();
        } else {
            this.zoomHandler = undefined;
        }
    }

284

Lukas Pravda's avatar
Lukas Pravda committed
285 286 287 288 289 290 291 292 293 294 295 296 297 298
    /**
     * Add ligand interactions to the canvas
     *
     * @param {*} data Data content of the API end point 
     * /pdb/bound_ligand_interactions
     * @memberof Visualization
     */
    public addLigandInteractions(data: any) {
        let key = Object.keys(data)[0];
        let body = data[key][0];
        this.interactionsData = data;

        if (this.depiction === undefined || this.depiction.ccdId !== body.ligand.chem_comp_id) {
            this.initLigandDisplay(body.ligand.chem_comp_id).then(() => {
299 300
                this.presentBindingSite = new Model.BindingSite().fromLigand(key, body, this.depiction);
                this.bindingSites.push(this.presentBindingSite);
Lukas Pravda's avatar
Lukas Pravda committed
301
                this.setupLigandScene();
Lukas Pravda's avatar
Lukas Pravda committed
302
            });
Lukas Pravda's avatar
Lukas Pravda committed
303
        } else {
304 305
            this.presentBindingSite = new Model.BindingSite().fromLigand(key, body, this.depiction);
            this.bindingSites.push(this.presentBindingSite);
Lukas Pravda's avatar
Lukas Pravda committed
306 307 308 309
            this.setupLigandScene();
        }
    }

310

Lukas Pravda's avatar
Lukas Pravda committed
311 312 313 314 315 316 317 318 319 320 321
    /**
     * Add bound molecule interactions to the canvas.
     *
     * @param {*} data Data content of the API end point 
     * /pdb/bound_molecule_interactions
     * @param {string} bmId Bound molecule id
     * @memberof Visualization
     */
    public addBoundMoleculeInteractions(data: any, bmId: string) {
        let key = Object.keys(data)[0];
        this.interactionsData = data;
322
        this.presentBindingSite = new Model.BindingSite().fromBoundMolecule(key, data[key][0]);
Lukas Pravda's avatar
Lukas Pravda committed
323

324 325 326
        this.bindingSites.push(this.presentBindingSite);
        this.presentBindingSite.bmId = bmId;
        let ligands = this.presentBindingSite.residues.filter(x => x.isLigand);
Lukas Pravda's avatar
Lukas Pravda committed
327 328 329

        if (ligands.length === 1) this.initLigandInteractions(this.pdbId, ligands[0].authorResidueNumber, ligands[0].chainId);
        else this.setupScene();
Lukas Pravda's avatar
Lukas Pravda committed
330 331
    }

332

Lukas Pravda's avatar
Lukas Pravda committed
333
    // #region menu functions
Lukas Pravda's avatar
Lukas Pravda committed
334
    public saveSvg() {
335
        d3.text(Resources.ligEnvCSSAPI(this.environment))
Lukas Pravda's avatar
Lukas Pravda committed
336
            .then(x => {
337
                let svgData = `
Lukas Pravda's avatar
Lukas Pravda committed
338 339 340 341 342 343
                <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" style="background-color: white;">
                ${this.svg.html()}
                <style>
                /* <![CDATA[ */ \n ${x} \n /* ]]> */
                </style>
                </svg>`;
Lukas Pravda's avatar
Lukas Pravda committed
344

345 346 347
                let svgBlob = new Blob([svgData], { type: 'image/svg;charset=utf-8' });
                let svgUrl = URL.createObjectURL(svgBlob);
                let downloadLink = document.createElement('a');
Lukas Pravda's avatar
Lukas Pravda committed
348

349 350
                downloadLink.href = svgUrl;
                downloadLink.download = this.getSVGName();
351

352 353 354 355
                document.body.appendChild(downloadLink);
                downloadLink.click();
                document.body.removeChild(downloadLink);
            });
Lukas Pravda's avatar
Lukas Pravda committed
356 357
    }

Lukas Pravda's avatar
Lukas Pravda committed
358

359 360 361 362 363
    /**
     * Download interactions data in the JSON format.
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
364
    public downloadInteractionsData(): void {
Lukas Pravda's avatar
Lukas Pravda committed
365
        let downloadLink = document.createElement('a');
Lukas Pravda's avatar
Lukas Pravda committed
366
        let dataBlob = new Blob([JSON.stringify(this.interactionsData, null, 4)], { type: 'application/json' });
Lukas Pravda's avatar
Lukas Pravda committed
367

Lukas Pravda's avatar
sync  
Lukas Pravda committed
368
        downloadLink.href = URL.createObjectURL(dataBlob);
369
        downloadLink.download = this.interactionsData === undefined ? 'no name.json' : `${this.pdbId}_${this.presentBindingSite.bmId}_interactions.json`;
Lukas Pravda's avatar
Lukas Pravda committed
370 371 372 373 374 375
        document.body.appendChild(downloadLink);
        downloadLink.click();
        document.body.removeChild(downloadLink);
    }


376 377 378 379 380
    /**
     * Reinitialize the scene (basicaly rerun the simulation to place interaction partners)
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
381
    public reinitialize() {
382
        
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
        if (this.bindingSites.length > 1 && this.depiction !== undefined) {
            this.presentBindingSite = this.bindingSites[0];
            this.bindingSites.pop();
            
            this.depictionRoot.selectAll('*').remove();
            this.depiction = undefined;
            
            this.setupScene().then(() => this.centerScene());
        }
        else if (this.depiction === undefined) { 
            this.setupScene().then(() => this.centerScene());
        }
        else {
            this.setupLigandScene().then(() => this.centerScene());
        }
Lukas Pravda's avatar
Lukas Pravda committed
398

Lukas Pravda's avatar
Lukas Pravda committed
399
        this.hideLigandLabel();
Lukas Pravda's avatar
Lukas Pravda committed
400
    }
Lukas Pravda's avatar
Lukas Pravda committed
401

Lukas Pravda's avatar
Lukas Pravda committed
402

Lukas Pravda's avatar
Lukas Pravda committed
403

Lukas Pravda's avatar
Lukas Pravda committed
404
    /**
405
     * Center scene to the viewbox
Lukas Pravda's avatar
Lukas Pravda committed
406 407 408
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
409
    public centerScene() {
Lukas Pravda's avatar
Lukas Pravda committed
410
        // Get the bounding box
Lukas Pravda's avatar
Lukas Pravda committed
411 412 413
        if (this.nodes !== undefined) {
            let minX: any = d3.min(this.nodes.data().map((x) => x.x));
            let minY: any = d3.min(this.nodes.data().map((x) => x.y));
Lukas Pravda's avatar
Lukas Pravda committed
414

Lukas Pravda's avatar
Lukas Pravda committed
415 416
            let maxX: any = d3.max(this.nodes.data().map((x) => x.x));
            let maxY: any = d3.max(this.nodes.data().map((x) => x.y));
Lukas Pravda's avatar
Lukas Pravda committed
417

Lukas Pravda's avatar
Lukas Pravda committed
418
            this.computeBoundingBox(minX, maxX, minY, maxY);
Lukas Pravda's avatar
Lukas Pravda committed
419

Lukas Pravda's avatar
Lukas Pravda committed
420 421 422 423 424 425 426 427 428 429 430 431
        } else if (this.depiction !== undefined) {
            let minX: any = d3.min(this.depiction.atoms.map((x: Atom) => x.position.x));
            let minY: any = d3.min(this.depiction.atoms.map((x: Atom) => x.position.y));

            let maxX: any = d3.max(this.depiction.atoms.map((x: Atom) => x.position.x));
            let maxY: any = d3.max(this.depiction.atoms.map((x: Atom) => x.position.y));

            this.computeBoundingBox(minX, maxX, minY, maxY);
        }
    }

    private computeBoundingBox(minX: number, maxX: number, minY: number, maxY: number) {
Lukas Pravda's avatar
Lukas Pravda committed
432 433 434 435 436
        // The width and the height of the graph
        let molWidth = maxX - minX;
        let molHeight = maxY - minY;

        // how much larger the drawing area is than the width and the height
Lukas Pravda's avatar
Lukas Pravda committed
437 438
        let widthRatio = this.parent.offsetWidth / molWidth;
        let heightRatio = this.parent.offsetHeight / molHeight;
Lukas Pravda's avatar
Lukas Pravda committed
439 440 441

        // we need to fit it in both directions, so we scale according to
        // the direction in which we need to shrink the most
Lukas Pravda's avatar
Lukas Pravda committed
442
        let minRatio = Math.min(widthRatio, heightRatio) * 0.85;
Lukas Pravda's avatar
Lukas Pravda committed
443 444 445 446 447 448

        // the new dimensions of the molecule
        let newMolWidth = molWidth * minRatio;
        let newMolHeight = molHeight * minRatio;

        // translate so that it's in the center of the window
Lukas Pravda's avatar
Lukas Pravda committed
449 450
        let xTrans = -(minX) * minRatio + (this.parent.offsetWidth - newMolWidth) / 2;
        let yTrans = -(minY) * minRatio + (this.parent.offsetHeight - newMolHeight) / 2;
Lukas Pravda's avatar
Lukas Pravda committed
451 452

        // do the actual moving
Lukas Pravda's avatar
Lukas Pravda committed
453
        this.canvas.attr('transform', `translate(${xTrans}, ${yTrans}) scale(${minRatio})`);
Lukas Pravda's avatar
Lukas Pravda committed
454 455 456 457

        // tell the zoomer what we did so that next we zoom, it uses the
        // transformation we entered here        

Lukas Pravda's avatar
Lukas Pravda committed
458
        let translation = d3.zoomIdentity.translate(xTrans, yTrans).scale(minRatio);
Lukas Pravda's avatar
Lukas Pravda committed
459 460
        this.zoomHandler?.transform(this.svg, translation);

Lukas Pravda's avatar
Lukas Pravda committed
461
    };
462

Lukas Pravda's avatar
Lukas Pravda committed
463 464
    // #endregion menu functions

465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
    // #endregion public methods

    private resize() {
        this.svg
            .attr('width', this.parent.offsetWidth)
            .attr('height', this.parent.offsetHeight);

        if (this.depiction === undefined) {
            this.simulation
                .force('center', d3.forceCenter(this.parent.offsetWidth / 2, this.parent.offsetHeight / 2))
                .restart();
        } else this.simulation.restart();

        if (this.zoomHandler !== undefined) this.zoomHandler(this.svg);
    }

    private getSVGName(): string {
482
        if (this.presentBindingSite !== undefined) return `${this.presentBindingSite.bmId}.svg`;
483 484
        if (this.depiction !== undefined) return `${this.depiction.ccdId}.svg`;

485
        return 'blank.svg';
486 487
    }

Lukas Pravda's avatar
Lukas Pravda committed
488
    // #region fire events
489

Lukas Pravda's avatar
Lukas Pravda committed
490
    //https://stackoverflow.com/questions/40722344/understanding-d3-with-an-example-mouseover-mouseup-with-multiple-arguments
491
    private fireExternalNodeMouseEnterEvent(x: Model.InteractionNode, i: number, g: any) {
Lukas Pravda's avatar
Lukas Pravda committed
492
        this.nodeHighlight(x, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
493
        this.fireExternalNodeEvent(x, Config.interactionMouseoverEvent);
Lukas Pravda's avatar
Lukas Pravda committed
494 495
    }

496
    private fireExternalNodeMouseLeaveEvent(x: Model.InteractionNode, i: number, g: any) {
Lukas Pravda's avatar
Lukas Pravda committed
497
        this.nodeDim(x, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
498

Lukas Pravda's avatar
Lukas Pravda committed
499
        const e = new CustomEvent(Config.interactionMouseoutEvent, {
Lukas Pravda's avatar
Lukas Pravda committed
500 501 502 503
            bubbles: true,
            detail: {}
        });
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
504
    }
Lukas Pravda's avatar
sync  
Lukas Pravda committed
505

506
    private fireExternalLinkEvent(link: Model.Link, eventName: string) {
Lukas Pravda's avatar
Lukas Pravda committed
507 508
        let atomsSource = [];
        let atomsTarget = [];
Lukas Pravda's avatar
Lukas Pravda committed
509 510

        if (link instanceof Model.LigandResidueLink) {
Lukas Pravda's avatar
Lukas Pravda committed
511
            let tmpSrc = [].concat.apply([], link.interaction.map(x => x.sourceAtoms));
Lukas Pravda's avatar
Lukas Pravda committed
512
            atomsSource = [].concat.apply([], tmpSrc).filter((v, i, a) => a.indexOf(v) === i);
Lukas Pravda's avatar
Lukas Pravda committed
513

Lukas Pravda's avatar
Lukas Pravda committed
514
            let tmpTar = [].concat.apply([], link.interaction.map(x => x.targetAtoms));
Lukas Pravda's avatar
Lukas Pravda committed
515
            atomsTarget = [].concat.apply([], tmpTar).filter((v, i, a) => a.indexOf(v) === i);
Lukas Pravda's avatar
Lukas Pravda committed
516
        }
Lukas Pravda's avatar
Lukas Pravda committed
517

Lukas Pravda's avatar
Lukas Pravda committed
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
        const e = new CustomEvent(eventName, {
            bubbles: true,
            detail: {
                interacting_nodes: [
                    {
                        pdb_res_id: this.pdbId,
                        auth_asym_id: link.source.residue.chainId,
                        auth_seq_id: link.source.residue.authorResidueNumber,
                        auth_ins_code_id: link.source.residue.authorInsertionCode,
                        atoms: atomsSource
                    },
                    {
                        pdb_res_id: this.pdbId,
                        auth_asym_id: link.target.residue.chainId,
                        auth_seq_id: link.target.residue.authorResidueNumber,
                        auth_ins_code_id: link.target.residue.authorInsertionCode,
                        atoms: atomsTarget
                    }
536 537
                ],
                tooltip: link.toTooltip()
Lukas Pravda's avatar
Lukas Pravda committed
538 539 540 541
            }
        });
        this.parent.dispatchEvent(e);
    }
Lukas Pravda's avatar
Lukas Pravda committed
542

543
    private fireExternalNodeEvent(node: Model.InteractionNode, eventName: string) {
Lukas Pravda's avatar
Lukas Pravda committed
544
        const e = new CustomEvent(eventName, {
Lukas Pravda's avatar
Lukas Pravda committed
545 546
            bubbles: true,
            detail: {
Lukas Pravda's avatar
Lukas Pravda committed
547 548 549 550 551
                selected_node: {
                    pdb_res_id: this.pdbId,
                    auth_asym_id: node.residue.chainId,
                    auth_seq_id: node.residue.authorResidueNumber,
                    auth_ins_code_id: node.residue.authorInsertionCode
552 553
                },
                tooltip: node.toTooltip()
Lukas Pravda's avatar
Lukas Pravda committed
554 555
            }
        });
Lukas Pravda's avatar
Lukas Pravda committed
556

Lukas Pravda's avatar
Lukas Pravda committed
557
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
558 559
    }

560
    private fireExternalLinkLeaveEvent() {
Lukas Pravda's avatar
Lukas Pravda committed
561
        const e = new CustomEvent(Config.interactionMouseoutEvent, {
Lukas Pravda's avatar
Lukas Pravda committed
562 563 564
            bubbles: true,
            detail: {}
        });
Lukas Pravda's avatar
Lukas Pravda committed
565

Lukas Pravda's avatar
Lukas Pravda committed
566
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
567
    }
Lukas Pravda's avatar
Lukas Pravda committed
568

Lukas Pravda's avatar
Lukas Pravda committed
569
    // #endregion fire events
Lukas Pravda's avatar
Lukas Pravda committed
570 571


Lukas Pravda's avatar
Lukas Pravda committed
572 573 574 575 576 577 578 579 580
    //#region setup scene micromethods
    private wipeOutVisuals() {
        this.nodesRoot.selectAll('*').remove();
        this.linksRoot.selectAll('*').remove();
    }

    private setupLinks() {
        this.links = this.linksRoot
            .selectAll()
581
            .data(this.presentBindingSite.links)
Lukas Pravda's avatar
Lukas Pravda committed
582 583 584 585
            .enter().append('g');

        this.links
            .append('line')
586
            .classed('pdb-lig-env-svg-shadow-bond', (x: Model.Link) => x.getLinkClass() !== 'hydrophobic')
Lukas Pravda's avatar
Lukas Pravda committed
587
            .on('click', (x: Model.Link) => this.linkMouseClickEventHandler(x))
Lukas Pravda's avatar
Lukas Pravda committed
588 589
            .on('mouseenter', (x: Model.Link, index: number, group: any) => this.linkMouseOverEventHandler(x, index, group))
            .on('mouseleave', (_, index: number, group: any) => this.linkMouseOutEventHandler(index, group));
Lukas Pravda's avatar
Lukas Pravda committed
590 591 592

        this.links
            .append('line')
593
            .attr('class', (e: Model.Link) => `pdb-lig-env-svg-bond pdb-lig-env-svg-bond-${e.getLinkClass()}`)
594
            .attr('marker-mid', (e: Model.Link) => e.hasClash() ? 'url(#clash)' : '')
Lukas Pravda's avatar
Lukas Pravda committed
595 596
            .on('mouseenter', (x: Model.Link, y: any, z: any) => this.linkMouseOverEventHandler(x, y, z))
            .on('mouseleave', (_, index: number, group: any) => this.linkMouseOutEventHandler(index, group));
Lukas Pravda's avatar
Lukas Pravda committed
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
    }



    private addNodeLabels(selection: any) {
        selection
            .append('text')
            .style('text-anchor', 'middle')
            .style('dominant-baseline', 'central')
            .each(function (e: Model.InteractionNode) {
                let labels = [e.residue.chemCompId, `${e.residue.chainId} ${e.residue.authorResidueNumber}`];
                for (var i = 0; i < labels.length; i++) {
                    d3.select(this)
                        .append('tspan')
                        .attr('dy', (i * 20) - 4)
                        .attr('x', 0)
                        .text(labels[i]);
                }
            });
    }
    //#endregion

619 620 621 622 623 624 625 626 627 628
    /**
     * Initialize scene after user selected a part of bound molecule.
     *
     * @private
     * @param {Model.InteractionNode} n Interaction node user clicked to
     * @param {number} i index o the interaction node
     * @param {*} g group of interaction nodes
     * @returns
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
629
    private selectLigand(n: Model.InteractionNode, i: number, g: any) {
Lukas Pravda's avatar
Lukas Pravda committed
630
        this.fireExternalNodeEvent(n, Config.interactionClickEvent);
Lukas Pravda's avatar
Lukas Pravda committed
631

Lukas Pravda's avatar
Lukas Pravda committed
632
        if (!n.residue.isLigand) return;
Lukas Pravda's avatar
Lukas Pravda committed
633

634
        this.nodeDim(n, i, g);
635
        this.fireExternalNodeMouseLeaveEvent(n, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
636
        this.showLigandLabel(n);
Lukas Pravda's avatar
sync  
Lukas Pravda committed
637

Lukas Pravda's avatar
Lukas Pravda committed
638
        this.initLigandInteractions(this.pdbId, n.residue.authorResidueNumber, n.residue.chainId);
Lukas Pravda's avatar
Lukas Pravda committed
639
    }
Lukas Pravda's avatar
Lukas Pravda committed
640

641 642 643 644 645 646 647 648 649 650 651
    /**
     * Display error message on the SVG canvas if any of the resources
     * are not available.
     *
     * @private
     * @param {*} e error object
     * @param {string} msg Error message to display
     * @memberof Visualization
     */
    private processError(e: any, msg: string) {
        this.canvas.append('text')
652
            .classed('pdb-lig-env-svg-node', true)
Lukas Pravda's avatar
Lukas Pravda committed
653 654 655
            .attr('dominant-baseline', 'center')
            .attr('text-anchor', 'middle')
            .attr('x', this.parent.clientWidth / 2)
656 657
            .attr('y', this.parent.clientHeight / 2)
            .text(msg)
Lukas Pravda's avatar
Lukas Pravda committed
658

659 660
        throw e;

Lukas Pravda's avatar
Lukas Pravda committed
661
    }
662

Lukas Pravda's avatar
Lukas Pravda committed
663

664 665 666 667 668 669 670 671 672
    /**
     * Setup ligand scene for display of ligand and interactions.
     * This includes: setup of links, nodes, simulation and subscribing to relevant events.
     *
     * Depiction is expected to be downloaded already.
     *
     * @private
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
673
    private async setupLigandScene() {
Lukas Pravda's avatar
Lukas Pravda committed
674 675
        this.wipeOutVisuals();
        this.setupLinks();
Lukas Pravda's avatar
Lukas Pravda committed
676

677
        this.presentBindingSite.interactionNodes
Lukas Pravda's avatar
Lukas Pravda committed
678 679
            .filter((x: Model.InteractionNode) => !x.residue.isLigand)
            .forEach((x: Model.InteractionNode) => {
680
                let lnks = this.presentBindingSite.links
681
                    .filter((y: Model.LigandResidueLink) => y.containsNode(x))
682 683
                    .map((y: Model.LigandResidueLink) => [].concat.apply([], y.interaction.map(z => z.sourceAtoms)));

Lukas Pravda's avatar
Lukas Pravda committed
684 685
                let concated = [].concat.apply([], lnks);
                let position: Vector2D = this.depiction.getInitalNodePosition(concated);
686

Lukas Pravda's avatar
Lukas Pravda committed
687 688 689 690
                x.x = position.x + Math.random() * 110;
                x.y = position.y + Math.random() * 110;
            });

Lukas Pravda's avatar
Lukas Pravda committed
691

Lukas Pravda's avatar
Lukas Pravda committed
692
        // setup nodes; wait for resources to be ready
693
        this.presentBindingSite.interactionNodes.forEach(x => this.rProvider.downloadAnnotation(x.residue));
Lukas Pravda's avatar
Lukas Pravda committed
694 695
        await Promise.all(this.rProvider.downloadPromises);
        await Promise.all([this.visualsMapper.graphicsPromise, this.visualsMapper.mappingPromise]);
696

Lukas Pravda's avatar
Lukas Pravda committed
697
        this.nodes = this.nodesRoot.append('g')
Lukas Pravda's avatar
Lukas Pravda committed
698
            .selectAll()
699
            .data(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
700 701
            .enter().append('g');

702

Lukas Pravda's avatar
Lukas Pravda committed
703
        this.nodes.filter((x: Model.InteractionNode) => !x.residue.isLigand)
Lukas Pravda's avatar
Lukas Pravda committed
704
            .on('click', (x: Model.InteractionNode) => this.fireExternalNodeEvent(x, Config.interactionClickEvent))
705
            .attr('class', (x: Model.InteractionNode) => `pdb-lig-env-svg-node pdb-lig-env-svg-${x.residue.getResidueType()}-res`)
706 707
            .on('mouseenter', (x: Model.InteractionNode, i: number, g: any) => this.fireExternalNodeMouseEnterEvent(x, i, g))
            .on('mouseleave', (x: Model.InteractionNode, i: number, g: any) => this.fireExternalNodeMouseLeaveEvent(x, i, g));
Lukas Pravda's avatar
Lukas Pravda committed
708 709 710 711 712

        this.nodes.filter((x: Model.InteractionNode) => !x.residue.isLigand && this.visualsMapper.glycanMapping.has(x.residue.chemCompId))
            .html((e: Model.InteractionNode) => this.visualsMapper.getGlycanImage(e.residue.chemCompId));

        // draw rest
Lukas Pravda's avatar
Lukas Pravda committed
713
        this.nodes.filter((x: Model.InteractionNode) => !this.visualsMapper.glycanMapping.has(x.residue.chemCompId))
Lukas Pravda's avatar
Lukas Pravda committed
714
            .append('circle')
Lukas Pravda's avatar
Lukas Pravda committed
715
            .attr('r', (x: Model.InteractionNode) => x.scale * Config.nodeSize);
Lukas Pravda's avatar
Lukas Pravda committed
716

Lukas Pravda's avatar
Lukas Pravda committed
717 718
        let nodesWithText = this.nodes.filter((x: Model.InteractionNode) => !x.residue.isLigand);
        this.addNodeLabels(nodesWithText);
Lukas Pravda's avatar
Lukas Pravda committed
719

720 721
        let forceLink = d3.forceLink()
            .links(this.links.filter((x: Model.LigandResidueLink) => x.getLinkClass() !== 'hydrophobic'))
722 723
            .distance(70)
            .strength(0.5);
Lukas Pravda's avatar
Lukas Pravda committed
724

Lukas Pravda's avatar
Lukas Pravda committed
725
        let charge = d3.forceManyBody().strength(-100).distanceMin(40).distanceMax(80);
Lukas Pravda's avatar
Lukas Pravda committed
726
        let collision = d3.forceCollide().radius(45);
Lukas Pravda's avatar
Lukas Pravda committed
727

728
        this.simulation = d3.forceSimulation(this.presentBindingSite.interactionNodes)
729 730
            .force('link', forceLink)
            .force('charge', charge) //strength 
Lukas Pravda's avatar
Lukas Pravda committed
731
            .force('collision', collision)
732
            .on('tick', () => this.simulationStep());
Lukas Pravda's avatar
Lukas Pravda committed
733

Lukas Pravda's avatar
Lukas Pravda committed
734 735
        this.dragHandler(this.nodes);
        if (this.zoomHandler !== undefined) this.zoomHandler(this.svg, d3.zoomIdentity);
Lukas Pravda's avatar
Lukas Pravda committed
736 737
    }

Lukas Pravda's avatar
Lukas Pravda committed
738

739 740 741 742 743 744 745 746
    /**
     * Setup display of interactions for bound molecule. 
     * * This includes: setup of links, nodes, simulation and subscribing to relevant events.
     * No depiction is required for this step.
     *
     * @private
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
747
    private async setupScene() {
Lukas Pravda's avatar
Lukas Pravda committed
748 749
        this.wipeOutVisuals();
        this.setupLinks();
750

Lukas Pravda's avatar
Lukas Pravda committed
751
        // setup nodes; wait for resources to be ready
752
        this.presentBindingSite.interactionNodes.forEach(x => ResidueProvider.getInstance(this.environment).downloadAnnotation(x.residue));
Lukas Pravda's avatar
Lukas Pravda committed
753 754
        await Promise.all(this.rProvider.downloadPromises);
        await Promise.all([this.visualsMapper.graphicsPromise, this.visualsMapper.mappingPromise]);
755

Lukas Pravda's avatar
Lukas Pravda committed
756
        this.nodes = this.nodesRoot
Lukas Pravda's avatar
Lukas Pravda committed
757
            .selectAll()
758
            .data(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
759
            .enter().append('g')
760
            .attr('class', (e: Model.InteractionNode) => `pdb-lig-env-svg-node pdb-lig-env-svg-${e.residue.getResidueType()}-res`)
Lukas Pravda's avatar
Lukas Pravda committed
761
            .on('click', (x: Model.InteractionNode, i: number, g: any) => this.selectLigand(x, i, g))
762 763
            .on('mouseover', (x: Model.InteractionNode, i: number, g: any) => this.fireExternalNodeMouseEnterEvent(x, i, g))
            .on('mouseout', (x: Model.InteractionNode, i: number, g: any) => this.fireExternalNodeMouseLeaveEvent(x, i, g));
Lukas Pravda's avatar
Lukas Pravda committed
764

Lukas Pravda's avatar
Lukas Pravda committed
765

Lukas Pravda's avatar
Lukas Pravda committed
766
        this.nodes.filter((n: Model.InteractionNode) =>
Lukas Pravda's avatar
Lukas Pravda committed
767
            this.visualsMapper.glycanMapping.has(n.residue.chemCompId))
Lukas Pravda's avatar
Lukas Pravda committed
768
            .html((e: Model.InteractionNode) => this.visualsMapper.getGlycanImage(e.residue.chemCompId));
Lukas Pravda's avatar
Lukas Pravda committed
769

Lukas Pravda's avatar
Lukas Pravda committed
770
        this.nodes.filter((e: Model.InteractionNode) => !this.visualsMapper.glycanMapping.has(e.residue.chemCompId))
Lukas Pravda's avatar
Lukas Pravda committed
771
            .append('circle')
Lukas Pravda's avatar
Lukas Pravda committed
772
            .attr('r', Config.nodeSize);
Lukas Pravda's avatar
Lukas Pravda committed
773

Lukas Pravda's avatar
Lukas Pravda committed
774
        this.addNodeLabels(this.nodes);
Lukas Pravda's avatar
Lukas Pravda committed
775 776

        let forceLink = d3.forceLink()
777
            .links(this.presentBindingSite.links)
778
            .distance((x: Model.Link) => (<Model.InteractionNode>x.source).residue.isLigand && (<Model.InteractionNode>x.target).residue.isLigand ? 55 : 150)
Lukas Pravda's avatar
Lukas Pravda committed
779 780 781 782
            .strength(0.5);

        let charge = d3.forceManyBody().strength(-1000).distanceMin(55).distanceMax(250);
        let collision = d3.forceCollide(45);
Lukas Pravda's avatar
Lukas Pravda committed
783
        let center = d3.forceCenter(this.parent.offsetWidth / 2, this.parent.offsetHeight / 2);
Lukas Pravda's avatar
Lukas Pravda committed
784

785
        this.simulation = d3.forceSimulation(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
786 787 788 789
            .force('link', forceLink)
            .force('charge', charge) //strength 
            .force('collision', collision)
            .force('center', center)
790
            .on('tick', () => this.simulationStep());
Lukas Pravda's avatar
Lukas Pravda committed
791

Lukas Pravda's avatar
Lukas Pravda committed
792 793 794
        this.dragHandler(this.nodes);

        if (this.zoomHandler) this.zoomHandler(this.svg);
Lukas Pravda's avatar
Lukas Pravda committed
795
    }
Lukas Pravda's avatar
Lukas Pravda committed
796

Lukas Pravda's avatar
Lukas Pravda committed
797

798 799 800 801 802 803 804
    /**
     * This is a tick in a simulation that updates position of nodes and links
     * Depiction does not change its conformation.
     *
     * @private
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
805
    private simulationStep() {
Lukas Pravda's avatar
Lukas Pravda committed
806
        this.nodes.attr('transform', (n: Model.InteractionNode) => `translate(${n.x},${n.y}) scale(${n.scale})`);
Lukas Pravda's avatar
Lukas Pravda committed
807
        this.links.selectAll('line').attr('x1', (x: any) => x.source.x)
Lukas Pravda's avatar
Lukas Pravda committed
808
            .attr('y1', (x: any) => x.source.y)
809 810
            .attr('x2', (x: any) => x.target.x)
            .attr('y2', (x: any) => x.target.y);
Lukas Pravda's avatar
Lukas Pravda committed
811
    }
Lukas Pravda's avatar
Lukas Pravda committed
812

Lukas Pravda's avatar
Lukas Pravda committed
813