manager.ts 36 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

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
    private nodeDragged: boolean;
31 32

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

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

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

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

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

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

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

61 62
        this.addMarkers();

Lukas Pravda's avatar
Lukas Pravda committed
63 64 65 66 67
        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
68
        if (uiParameters.zoom) this.zoomHandler = this.getZoomHandler();
Lukas Pravda's avatar
Lukas Pravda committed
69

Lukas Pravda's avatar
Lukas Pravda committed
70 71 72
        document.addEventListener(Config.molstarClickEvent, e => this.molstarClickEventHandler(e));
        document.addEventListener(Config.molstarMouseoverEvent, e => this.molstarClickEventHandler(e));
        document.addEventListener(Config.molstarMouseoutEvent, () => this.molstarMouseoutEventHandler());
Lukas Pravda's avatar
Lukas Pravda committed
73

Lukas Pravda's avatar
Lukas Pravda committed
74 75
        d3.select(this.parent).on('resize', () => this.resize());
        this.addMarkers();
Lukas Pravda's avatar
Lukas Pravda committed
76 77
    }

78
    // #region event handlers
79 80 81 82 83 84
    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
85

86

Lukas Pravda's avatar
Lukas Pravda committed
87 88 89 90 91 92 93
    /**
     * Handle molstar click event. Makes interaction node highlight
     *
     * @private
     * @param {*} e Data pased by molstar component
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
94
    private molstarClickEventHandler(e: any) {
95 96
        if (this.fullScreen) return;

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

Lukas Pravda's avatar
Lukas Pravda committed
99
        this.nodes?.each((node: Model.InteractionNode, index: number, group: any) => {
100
            this.nodeDim(node, index, group);
101

Lukas Pravda's avatar
Lukas Pravda committed
102 103 104 105 106 107 108 109 110
            if (node.id === hash) {
                this.selectedResidueHash = hash;
                this.nodeHighlight(node, index, group);
                return;
            }
        });

    }

Lukas Pravda's avatar
Lukas Pravda committed
111 112 113 114 115 116 117

    /**
     * Handles mouse leave molstar event. Removes interaction node highlight
     *
     * @private
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
118
    private molstarMouseoutEventHandler() {
119 120
        if (this.fullScreen) return;

Lukas Pravda's avatar
Lukas Pravda committed
121 122 123 124 125 126
        this.nodes?.each((node: Model.InteractionNode, index: number, group: any) => {
            if (node.id == this.selectedResidueHash) {
                this.nodeDim(node, index, group);
                return;
            }
        });
127 128
        this.links?.attr('opacity', 1);
        this.nodes?.attr('opacity', 1);
129

Lukas Pravda's avatar
Lukas Pravda committed
130 131
    }

132 133 134 135 136
    private linkMouseOverEventHandler(x: Model.Link, i: number, g: any) {
        if (!this.nodeDragged) {
            this.linkHighlight(x, i, g);
            this.fireExternalLinkEvent(x, Config.interactionMouseoverEvent);
        }
Lukas Pravda's avatar
Lukas Pravda committed
137 138
    }

139 140 141 142 143 144
    private linkMouseOutEventHandler(x: Model.Link, i: number, g: any) {
        if (!this.nodeDragged) {
            this.linkDim(x, i, g);
            this.fireExternalNullEvent(Config.interactionMouseoutEvent);
        }
    }
Lukas Pravda's avatar
Lukas Pravda committed
145

146 147 148 149 150 151
    //https://stackoverflow.com/questions/40722344/understanding-d3-with-an-example-mouseover-mouseup-with-multiple-arguments
    private nodeMouseoverEventHandler(x: Model.InteractionNode, i: number, g: any) {
        if (!this.nodeDragged) {
            this.nodeHighlight(x, i, g);
            this.fireExternalNodeEvent(x, Config.interactionMouseoverEvent);
        }
Lukas Pravda's avatar
Lukas Pravda committed
152 153
    }

154 155 156
    private nodeMouseoutEventHandler(x: Model.InteractionNode, i: number, g: any) {
        if (!this.nodeDragged) {
            this.nodeDim(x, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
157

158 159
            this.fireExternalNullEvent(Config.interactionMouseoutEvent);
        }
160

161 162
        this.links?.attr('opacity', 1);
        this.nodes?.attr('opacity', 1);
Lukas Pravda's avatar
Lukas Pravda committed
163
    }
Lukas Pravda's avatar
Lukas Pravda committed
164

Lukas Pravda's avatar
Lukas Pravda committed
165
    private dragHandler = d3.drag()
166
        .filter((x: Model.InteractionNode) => !x.static)
Lukas Pravda's avatar
Lukas Pravda committed
167 168 169 170 171 172 173
        .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) => {
174 175
            this.nodeDragged = true;

Lukas Pravda's avatar
Lukas Pravda committed
176 177 178 179
            x.fx = d3.event.x;
            x.fy = d3.event.y;
        })
        .on('end', (x: Model.InteractionNode) => {
180 181
            this.nodeDragged = false;

Lukas Pravda's avatar
Lukas Pravda committed
182 183 184 185 186 187 188
            if (!d3.event.active) this.simulation.alphaTarget(0);
            x.fx = d3.event.x;
            x.fy = d3.event.y;
        });

    // #endregion event handlers

189
    // #region public methods
Lukas Pravda's avatar
Lukas Pravda committed
190 191 192
    /**
     * Download bound molecule interactions data from PDBe Graph API end point
     * /pdb/bound_molecule_interactions
193
     *
Lukas Pravda's avatar
Lukas Pravda committed
194 195 196 197 198 199 200 201
     * 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
202
        this.pdbId = pdbid;
Lukas Pravda's avatar
Lukas Pravda committed
203
        let url = Resources.boundMoleculeAPI(pdbid, bmId, this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
204

Lukas Pravda's avatar
Lukas Pravda committed
205
        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
206
            .catch(e => this.processError(e, 'No interactions data are available.'))
207 208 209
            .then((data: any) => this.addBoundMoleculeInteractions(data, bmId))
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
210 211 212 213 214
    }

    /**
     * Download carbohydrate interactions data from PDBe Graph API end point
     * /pdb/carbohydrate_polymer_interactions
215
     *
216 217 218 219 220 221 222 223 224 225
     * 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
226
        let url = Resources.carbohydratePolymerAPI(pdbid, bmId, entityId, this.environment);
227 228

        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
229
            .catch(e => this.processError(e, 'No interactions data are available.'))
230
            .then((data: any) => this.addBoundMoleculeInteractions(data, bmId))
231 232
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
233
    }
Lukas Pravda's avatar
Lukas Pravda committed
234

Lukas Pravda's avatar
Lukas Pravda committed
235 236 237
    /**
     * Download ligand interactions data from PDBe Graph API end point
     * /pdb/bound_ligand_interactions.
238
     *
Lukas Pravda's avatar
Lukas Pravda committed
239 240
     * Correct parameters can be obtained using API call:
     * /pdb/bound_molecules
241
     *
Lukas Pravda's avatar
Lukas Pravda committed
242 243 244 245 246 247 248
     * @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
249
        let url = Resources.ligandInteractionsAPI(pdbId, chainId, resId, this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
250

Lukas Pravda's avatar
Lukas Pravda committed
251
        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
252
            .catch(e => this.processError(e, 'No interactions data are available.'))
253 254 255
            .then((data: any) => this.addLigandInteractions(data))
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
256 257
    }

Lukas Pravda's avatar
Lukas Pravda committed
258
    /**
259
     * Download ligand structure given the anotation generated by the
Lukas Pravda's avatar
Lukas Pravda committed
260 261 262 263 264 265 266
     * PDBeChem process.
     *
     * @param {string} ligandId
     * @returns
     * @memberof Visualization
     */
    public async initLigandDisplay(ligandId: string) {
267
        const ligandUrl = Resources.ligandAnnotationAPI(ligandId, this.environment);
268

Lukas Pravda's avatar
Lukas Pravda committed
269
        return d3.json(ligandUrl)
Lukas Pravda's avatar
Lukas Pravda committed
270
            .catch(e => this.processError(e, `Component ${ligandId} was not found.`))
271
            .then((d: any) => this.addDepiction(d))
Lukas Pravda's avatar
Lukas Pravda committed
272
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
273 274
    }

Lukas Pravda's avatar
Lukas Pravda committed
275

Lukas Pravda's avatar
Lukas Pravda committed
276 277 278 279 280 281 282
    /**
     * Add depiction to the canvas from external resource.
     *
     * @param {*} depiction Content of annotation.json file generated by
     * the PDBeChem process.
     * @memberof Visualization
     */
283
    public addDepiction(depiction: any) {
Lukas Pravda's avatar
Lukas Pravda committed
284
        this.depiction = new Depiction(this.depictionRoot, depiction);
285
        this.depiction.draw();
Lukas Pravda's avatar
Lukas Pravda committed
286 287
    }

288 289 290 291
    public toggleDepiction(atomNames: boolean) {
        this.depiction.draw(atomNames);
    }

Lukas Pravda's avatar
Lukas Pravda committed
292 293

    /**
294 295
     * Add atom highlight to the ligand structure. The previous highlight
     * is going to be removed.
Lukas Pravda's avatar
Lukas Pravda committed
296
     *
297 298
     * @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
299 300 301 302 303 304
     * @memberof Visualization
     */
    public addLigandHighlight(highlight: string[], color: string = undefined) {
        this.depiction.highlightSubgraph(highlight, color);
    }

305 306 307 308 309 310 311 312 313 314 315 316
    /**
     * 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
317
    public toogleZoom(active: boolean) {
318
        this.zoomHandler = active ? this.getZoomHandler() : undefined;
Lukas Pravda's avatar
Lukas Pravda committed
319 320
    }

321

Lukas Pravda's avatar
Lukas Pravda committed
322 323 324
    /**
     * Add ligand interactions to the canvas
     *
325
     * @param {*} data Data content of the API end point
Lukas Pravda's avatar
Lukas Pravda committed
326 327 328 329 330 331 332 333 334 335
     * /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(() => {
336 337
                this.presentBindingSite = new Model.BindingSite().fromLigand(key, body, this.depiction);
                this.bindingSites.push(this.presentBindingSite);
Lukas Pravda's avatar
Lukas Pravda committed
338
                this.setupLigandScene();
Lukas Pravda's avatar
Lukas Pravda committed
339
            });
Lukas Pravda's avatar
Lukas Pravda committed
340
        } else {
341 342
            this.presentBindingSite = new Model.BindingSite().fromLigand(key, body, this.depiction);
            this.bindingSites.push(this.presentBindingSite);
Lukas Pravda's avatar
Lukas Pravda committed
343 344 345 346
            this.setupLigandScene();
        }
    }

347

Lukas Pravda's avatar
Lukas Pravda committed
348 349 350
    /**
     * Add bound molecule interactions to the canvas.
     *
351
     * @param {*} data Data content of the API end point
Lukas Pravda's avatar
Lukas Pravda committed
352 353 354 355 356 357 358
     * /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;
359
        this.presentBindingSite = new Model.BindingSite().fromBoundMolecule(key, data[key][0]);
Lukas Pravda's avatar
Lukas Pravda committed
360

361 362 363
        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
364 365 366

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

369

Lukas Pravda's avatar
Lukas Pravda committed
370
    // #region menu functions
Lukas Pravda's avatar
Lukas Pravda committed
371 372 373 374 375 376
    /**
     * Export scene into an SVG components. It relies on the availability
     * of the external CSS for SVG styling. Otherwise it does not work.
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
377
    public saveSvg() {
378
        d3.text(Resources.ligEnvCSSAPI(this.environment))
Lukas Pravda's avatar
Lukas Pravda committed
379
            .then(x => {
380
                let svgData = `
Lukas Pravda's avatar
Lukas Pravda committed
381 382 383 384 385 386
                <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
387

388 389 390
                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
391

392 393
                downloadLink.href = svgUrl;
                downloadLink.download = this.getSVGName();
394

395 396 397 398
                document.body.appendChild(downloadLink);
                downloadLink.click();
                document.body.removeChild(downloadLink);
            });
Lukas Pravda's avatar
Lukas Pravda committed
399 400
    }

Lukas Pravda's avatar
Lukas Pravda committed
401

402 403 404 405 406
    /**
     * Download interactions data in the JSON format.
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
407
    public downloadInteractionsData(): void {
Lukas Pravda's avatar
Lukas Pravda committed
408
        let downloadLink = document.createElement('a');
Lukas Pravda's avatar
Lukas Pravda committed
409
        let dataBlob = new Blob([JSON.stringify(this.interactionsData, null, 4)], { type: 'application/json' });
Lukas Pravda's avatar
Lukas Pravda committed
410

Lukas Pravda's avatar
sync  
Lukas Pravda committed
411
        downloadLink.href = URL.createObjectURL(dataBlob);
Lukas Pravda's avatar
Lukas Pravda committed
412
        downloadLink.download = this.interactionsData === undefined ? 'no_name.json' : `${this.pdbId}_${this.presentBindingSite.bmId}_interactions.json`;
Lukas Pravda's avatar
Lukas Pravda committed
413 414 415 416 417 418
        document.body.appendChild(downloadLink);
        downloadLink.click();
        document.body.removeChild(downloadLink);
    }


419 420 421 422 423
    /**
     * Reinitialize the scene (basicaly rerun the simulation to place interaction partners)
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
424
    public reinitialize() {
425

426 427 428
        if (this.bindingSites.length > 1 && this.depiction !== undefined) {
            this.presentBindingSite = this.bindingSites[0];
            this.bindingSites.pop();
429

430 431
            this.depictionRoot.selectAll('*').remove();
            this.depiction = undefined;
432

Lukas Pravda's avatar
Lukas Pravda committed
433
            this.nullNodesPositions();
434 435
            this.setupScene().then(() => this.centerScene());
        }
436
        else if (this.depiction === undefined) {
Lukas Pravda's avatar
Lukas Pravda committed
437
            this.nullNodesPositions();
438 439 440
            this.setupScene().then(() => this.centerScene());
        }
        else {
Lukas Pravda's avatar
Lukas Pravda committed
441
            this.nullNodesPositions();
442 443
            this.setupLigandScene().then(() => this.centerScene());
        }
Lukas Pravda's avatar
Lukas Pravda committed
444

445
        this.fireExternalNullEvent(Config.interactionHideLabelEvent);
Lukas Pravda's avatar
Lukas Pravda committed
446
    }
Lukas Pravda's avatar
Lukas Pravda committed
447

Lukas Pravda's avatar
Lukas Pravda committed
448

Lukas Pravda's avatar
Lukas Pravda committed
449

Lukas Pravda's avatar
Lukas Pravda committed
450
    /**
451
     * Center scene to the viewbox
Lukas Pravda's avatar
Lukas Pravda committed
452 453 454
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
455
    public centerScene() {
Lukas Pravda's avatar
Lukas Pravda committed
456
        // Get the bounding box
Lukas Pravda's avatar
Lukas Pravda committed
457 458 459
        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
460

Lukas Pravda's avatar
Lukas Pravda committed
461 462
            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
463

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

Lukas Pravda's avatar
Lukas Pravda committed
466 467 468 469 470 471 472 473 474 475 476 477
        } 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
478
        // The width and the height of the graph
479 480
        let molWidth = Math.max((maxX - minX), this.parent.offsetWidth);
        let molHeight = Math.max((maxY - minY), this.parent.offsetHeight);
Lukas Pravda's avatar
Lukas Pravda committed
481 482

        // how much larger the drawing area is than the width and the height
Lukas Pravda's avatar
Lukas Pravda committed
483 484
        let widthRatio = this.parent.offsetWidth / molWidth;
        let heightRatio = this.parent.offsetHeight / molHeight;
Lukas Pravda's avatar
Lukas Pravda committed
485 486 487

        // 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
488
        let minRatio = Math.min(widthRatio, heightRatio) * 0.85;
Lukas Pravda's avatar
Lukas Pravda committed
489 490 491 492 493 494

        // 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
495 496
        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
497 498

        // do the actual moving
Lukas Pravda's avatar
Lukas Pravda committed
499
        this.canvas.attr('transform', `translate(${xTrans}, ${yTrans}) scale(${minRatio})`);
Lukas Pravda's avatar
Lukas Pravda committed
500 501

        // tell the zoomer what we did so that next we zoom, it uses the
502
        // transformation we entered here
Lukas Pravda's avatar
Lukas Pravda committed
503
        let translation = d3.zoomIdentity.translate(xTrans, yTrans).scale(minRatio);
Lukas Pravda's avatar
Lukas Pravda committed
504
        this.zoomHandler?.transform(this.svg, translation);
Lukas Pravda's avatar
Lukas Pravda committed
505
    }
506

Lukas Pravda's avatar
Lukas Pravda committed
507 508
    // #endregion menu functions

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
    // #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 {
526
        if (this.presentBindingSite !== undefined) return `${this.presentBindingSite.bmId}.svg`;
527 528
        if (this.depiction !== undefined) return `${this.depiction.ccdId}.svg`;

529
        return 'blank.svg';
530 531
    }

532
    private nullNodesPositions() {
Lukas Pravda's avatar
Lukas Pravda committed
533 534 535 536 537 538 539 540 541
        this.presentBindingSite.interactionNodes.forEach((x: Model.InteractionNode) => {
            if (!x.static) {
                x.fx = null;
                x.fy = null;
            }
        });

    }

Lukas Pravda's avatar
Lukas Pravda committed
542
    // #region fire events
543 544

    private fireExternalLinkEvent(link: Model.Link, eventName: string) {
Lukas Pravda's avatar
Lukas Pravda committed
545 546
        let atomsSource = [];
        let atomsTarget = [];
Lukas Pravda's avatar
Lukas Pravda committed
547 548

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

Lukas Pravda's avatar
Lukas Pravda committed
552
            let tmpTar = [].concat.apply([], link.interaction.map(x => x.targetAtoms));
Lukas Pravda's avatar
Lukas Pravda committed
553
            atomsTarget = [].concat.apply([], tmpTar).filter((v, i, a) => a.indexOf(v) === i);
Lukas Pravda's avatar
Lukas Pravda committed
554
        }
Lukas Pravda's avatar
Lukas Pravda committed
555

Lukas Pravda's avatar
Lukas Pravda committed
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        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
                    }
574 575
                ],
                tooltip: link.toTooltip()
Lukas Pravda's avatar
Lukas Pravda committed
576 577 578 579
            }
        });
        this.parent.dispatchEvent(e);
    }
Lukas Pravda's avatar
Lukas Pravda committed
580

581
    private fireExternalNodeEvent(node: Model.InteractionNode, eventName: string) {
Lukas Pravda's avatar
Lukas Pravda committed
582
        const e = new CustomEvent(eventName, {
Lukas Pravda's avatar
Lukas Pravda committed
583 584
            bubbles: true,
            detail: {
Lukas Pravda's avatar
Lukas Pravda committed
585 586 587 588 589
                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
590 591
                },
                tooltip: node.toTooltip()
Lukas Pravda's avatar
Lukas Pravda committed
592 593
            }
        });
Lukas Pravda's avatar
Lukas Pravda committed
594

Lukas Pravda's avatar
Lukas Pravda committed
595
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
596 597
    }

598 599
    private fireExternalNullEvent(eventName: string) {
        const e = new CustomEvent(eventName, {
Lukas Pravda's avatar
Lukas Pravda committed
600 601 602
            bubbles: true,
            detail: {}
        });
Lukas Pravda's avatar
Lukas Pravda committed
603

Lukas Pravda's avatar
Lukas Pravda committed
604
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
605
    }
Lukas Pravda's avatar
Lukas Pravda committed
606

Lukas Pravda's avatar
Lukas Pravda committed
607
    // #endregion fire events
Lukas Pravda's avatar
Lukas Pravda committed
608 609


Lukas Pravda's avatar
Lukas Pravda committed
610 611 612 613 614 615 616 617 618
    //#region setup scene micromethods
    private wipeOutVisuals() {
        this.nodesRoot.selectAll('*').remove();
        this.linksRoot.selectAll('*').remove();
    }

    private setupLinks() {
        this.links = this.linksRoot
            .selectAll()
619
            .data(this.presentBindingSite.links)
Lukas Pravda's avatar
Lukas Pravda committed
620 621 622 623
            .enter().append('g');

        this.links
            .append('line')
624
            .classed('pdb-lig-env-svg-shadow-bond', (x: Model.Link) => x.getLinkClass() !== 'hydrophobic')
Lukas Pravda's avatar
Lukas Pravda committed
625
            .on('mouseenter', (x: Model.Link, index: number, group: any) => this.linkMouseOverEventHandler(x, index, group))
626
            .on('mouseleave', (x: Model.Link, index: number, group: any) => this.linkMouseOutEventHandler(x, index, group));
Lukas Pravda's avatar
Lukas Pravda committed
627 628 629

        this.links
            .append('line')
630
            .attr('class', (e: Model.Link) => `pdb-lig-env-svg-bond pdb-lig-env-svg-bond-${e.getLinkClass()}`)
631
            .attr('marker-mid', (e: Model.Link) => e.hasClash() ? 'url(#clash)' : '')
Lukas Pravda's avatar
Lukas Pravda committed
632
            .on('mouseenter', (x: Model.Link, y: any, z: any) => this.linkMouseOverEventHandler(x, y, z))
633
            .on('mouseleave', (x: Model.Link, index: number, group: any) => this.linkMouseOutEventHandler(x, index, group));
Lukas Pravda's avatar
Lukas Pravda committed
634 635 636 637 638 639 640 641 642 643
    }



    private addNodeLabels(selection: any) {
        selection
            .append('text')
            .style('text-anchor', 'middle')
            .style('dominant-baseline', 'central')
            .each(function (e: Model.InteractionNode) {
644
                let labels = [e.residue.chemCompId, e.residue.authorResidueNumber];
645
                for (let i = 0; i < labels.length; i++) {
Lukas Pravda's avatar
Lukas Pravda committed
646 647
                    d3.select(this)
                        .append('tspan')
648
                        .attr('dy', (i * 30) - 10)
Lukas Pravda's avatar
Lukas Pravda committed
649 650 651 652 653 654 655
                        .attr('x', 0)
                        .text(labels[i]);
                }
            });
    }
    //#endregion

656 657 658 659 660 661 662 663 664 665
    /**
     * 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
666
    private selectLigand(n: Model.InteractionNode, i: number, g: any) {
Lukas Pravda's avatar
Lukas Pravda committed
667
        this.fireExternalNodeEvent(n, Config.interactionClickEvent);
Lukas Pravda's avatar
Lukas Pravda committed
668

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

671
        this.nodeDim(n, i, g);
672
        this.nodeMouseoutEventHandler(n, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
673
        this.showLigandLabel(n);
Lukas Pravda's avatar
sync  
Lukas Pravda committed
674

Lukas Pravda's avatar
Lukas Pravda committed
675
        this.initLigandInteractions(this.pdbId, n.residue.authorResidueNumber, n.residue.chainId);
Lukas Pravda's avatar
Lukas Pravda committed
676
    }
Lukas Pravda's avatar
Lukas Pravda committed
677

678 679 680 681 682 683 684 685 686 687 688
    /**
     * 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')
689
            .classed('pdb-lig-env-svg-node', true)
Lukas Pravda's avatar
Lukas Pravda committed
690 691 692
            .attr('dominant-baseline', 'center')
            .attr('text-anchor', 'middle')
            .attr('x', this.parent.clientWidth / 2)
693 694
            .attr('y', this.parent.clientHeight / 2)
            .text(msg)
Lukas Pravda's avatar
Lukas Pravda committed
695

696 697
        throw e;

Lukas Pravda's avatar
Lukas Pravda committed
698
    }
699

Lukas Pravda's avatar
Lukas Pravda committed
700

701 702 703 704 705 706 707 708 709
    /**
     * 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
710
    private async setupLigandScene() {
Lukas Pravda's avatar
Lukas Pravda committed
711 712
        this.wipeOutVisuals();
        this.setupLinks();
Lukas Pravda's avatar
Lukas Pravda committed
713

714
        this.presentBindingSite.interactionNodes
Lukas Pravda's avatar
Lukas Pravda committed
715 716
            .filter((x: Model.InteractionNode) => !x.residue.isLigand)
            .forEach((x: Model.InteractionNode) => {
Lukas Pravda's avatar
Lukas Pravda committed
717
                let links = this.presentBindingSite.links.filter((y: Model.LigandResidueLink) => y.containsNode(x) && y.getLinkClass() !== 'hydrophobic');
718

Lukas Pravda's avatar
Lukas Pravda committed
719 720
                links = links.length == 0 ? this.presentBindingSite.links.filter((y: Model.LigandResidueLink) => y.containsNode(x)) : links;
                let atom_names = links
721
                    .map((y: Model.LigandResidueLink) => [].concat.apply([], y.interaction.map(z => z.sourceAtoms)));
722

Lukas Pravda's avatar
Lukas Pravda committed
723
                let concated = [].concat.apply([], atom_names);
Lukas Pravda's avatar
Lukas Pravda committed
724
                let position: Vector2D = this.depiction.getInitalNodePosition(concated);
725

Lukas Pravda's avatar
Lukas Pravda committed
726 727
                x.x = position.x + Math.random() * 55;
                x.y = position.y + Math.random() * 55;
Lukas Pravda's avatar
Lukas Pravda committed
728 729
            });

Lukas Pravda's avatar
Lukas Pravda committed
730

Lukas Pravda's avatar
Lukas Pravda committed
731
        // setup nodes; wait for resources to be ready
732
        this.presentBindingSite.interactionNodes.forEach(x => this.rProvider.downloadAnnotation(x.residue));
Lukas Pravda's avatar
Lukas Pravda committed
733 734
        await Promise.all(this.rProvider.downloadPromises);
        await Promise.all([this.visualsMapper.graphicsPromise, this.visualsMapper.mappingPromise]);
735

Lukas Pravda's avatar
Lukas Pravda committed
736
        this.nodes = this.nodesRoot.append('g')
Lukas Pravda's avatar
Lukas Pravda committed
737
            .selectAll()
738
            .data(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
739 740
            .enter().append('g');

741

Lukas Pravda's avatar
Lukas Pravda committed
742
        this.nodes.filter((x: Model.InteractionNode) => !x.residue.isLigand)
743
            .attr('class', (x: Model.InteractionNode) => `pdb-lig-env-svg-node pdb-lig-env-svg-${x.residue.getResidueType()}-res`)
744 745
            .on('mouseenter', (x: Model.InteractionNode, i: number, g: any) => this.nodeMouseoverEventHandler(x, i, g))
            .on('mouseleave', (x: Model.InteractionNode, i: number, g: any) => this.nodeMouseoutEventHandler(x, i, g));
Lukas Pravda's avatar
Lukas Pravda committed
746 747 748 749 750

        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
751
        this.nodes.filter((x: Model.InteractionNode) => !this.visualsMapper.glycanMapping.has(x.residue.chemCompId))
Lukas Pravda's avatar
Lukas Pravda committed
752
            .append('circle')
Lukas Pravda's avatar
Lukas Pravda committed
753
            .attr('r', (x: Model.InteractionNode) => x.scale * Config.nodeSize);
Lukas Pravda's avatar
Lukas Pravda committed
754

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

758 759
        let forceLink = d3.forceLink()
            .links(this.links.filter((x: Model.LigandResidueLink) => x.getLinkClass() !== 'hydrophobic'))
Lukas Pravda's avatar
Lukas Pravda committed
760
            .distance(5);
Lukas Pravda's avatar
Lukas Pravda committed
761

Lukas Pravda's avatar
Lukas Pravda committed
762 763
        let charge = d3.forceManyBody().strength(-80).distanceMin(10).distanceMax(20);
        let collision = d3.forceCollide(50).iterations(10).strength(0.5);
Lukas Pravda's avatar
Lukas Pravda committed
764

765
        this.simulation = d3.forceSimulation(this.presentBindingSite.interactionNodes)
766
            .force('link', forceLink)
767
            .force('charge', charge) //strength
Lukas Pravda's avatar
Lukas Pravda committed
768
            .force('collision', collision)
769
            .on('tick', () => this.simulationStep());
Lukas Pravda's avatar
Lukas Pravda committed
770

Lukas Pravda's avatar
Lukas Pravda committed
771 772
        this.dragHandler(this.nodes);
        if (this.zoomHandler !== undefined) this.zoomHandler(this.svg, d3.zoomIdentity);
Lukas Pravda's avatar
Lukas Pravda committed
773 774
    }

Lukas Pravda's avatar
Lukas Pravda committed
775

776
    /**
777
     * Setup display of interactions for bound molecule.
778 779 780 781 782 783
     * * 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
784
    private async setupScene() {
Lukas Pravda's avatar
Lukas Pravda committed
785 786
        this.wipeOutVisuals();
        this.setupLinks();
787

Lukas Pravda's avatar
Lukas Pravda committed
788
        // setup nodes; wait for resources to be ready
789
        this.presentBindingSite.interactionNodes.forEach(x => this.rProvider.downloadAnnotation(x.residue));
Lukas Pravda's avatar
Lukas Pravda committed
790 791
        await Promise.all(this.rProvider.downloadPromises);
        await Promise.all([this.visualsMapper.graphicsPromise, this.visualsMapper.mappingPromise]);
792

Lukas Pravda's avatar
Lukas Pravda committed
793
        this.nodes = this.nodesRoot
Lukas Pravda's avatar
Lukas Pravda committed
794
            .selectAll()
795
            .data(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
796
            .enter().append('g')