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

Lukas Pravda's avatar
Lukas Pravda committed
68 69 70
        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
71

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

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

84

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

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

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

Lukas Pravda's avatar
Lukas Pravda committed
93 94 95 96 97 98 99 100 101
            if (node.id === hash) {
                this.selectedResidueHash = hash;
                this.nodeHighlight(node, index, group);
                return;
            }
        });

    }

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

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

        this.links.attr('opacity', 1);
        this.nodes.attr('opacity', 1);

Lukas Pravda's avatar
Lukas Pravda committed
115 116
    }

117 118 119 120 121
    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
122 123
    }

124 125 126 127 128 129
    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
130

131 132 133 134 135 136
    //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
137 138
    }

139 140 141
    private nodeMouseoutEventHandler(x: Model.InteractionNode, i: number, g: any) {
        if (!this.nodeDragged) {
            this.nodeDim(x, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
142

143 144
            this.fireExternalNullEvent(Config.interactionMouseoutEvent);
        }
145 146 147

        this.links.attr('opacity', 1);
        this.nodes.attr('opacity', 1);
Lukas Pravda's avatar
Lukas Pravda committed
148
    }
Lukas Pravda's avatar
Lukas Pravda committed
149

Lukas Pravda's avatar
Lukas Pravda committed
150
    private dragHandler = d3.drag()
151
        .filter((x: Model.InteractionNode) => !x.static)
Lukas Pravda's avatar
Lukas Pravda committed
152 153 154 155 156 157 158
        .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) => {
159 160
            this.nodeDragged = true;

Lukas Pravda's avatar
Lukas Pravda committed
161 162 163 164
            x.fx = d3.event.x;
            x.fy = d3.event.y;
        })
        .on('end', (x: Model.InteractionNode) => {
165 166
            this.nodeDragged = false;

Lukas Pravda's avatar
Lukas Pravda committed
167 168 169 170 171 172 173
            if (!d3.event.active) this.simulation.alphaTarget(0);
            x.fx = d3.event.x;
            x.fy = d3.event.y;
        });

    // #endregion event handlers

174
    // #region public methods
Lukas Pravda's avatar
Lukas Pravda committed
175 176 177 178 179 180 181 182 183 184 185 186
    /**
     * 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
187
        this.pdbId = pdbid;
Lukas Pravda's avatar
Lukas Pravda committed
188
        let url = Resources.boundMoleculeAPI(pdbid, bmId, this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
189

Lukas Pravda's avatar
Lukas Pravda committed
190
        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
191
            .catch(e => this.processError(e, 'No interactions data are available.'))
192 193 194
            .then((data: any) => this.addBoundMoleculeInteractions(data, bmId))
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
    }

    /**
     * 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
211
        let url = Resources.carbohydratePolymerAPI(pdbid, bmId, entityId, this.environment);
212 213 214 215

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

Lukas Pravda's avatar
Lukas Pravda committed
220 221 222 223 224 225 226 227 228 229 230 231 232 233
    /**
     * 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
234
        let url = Resources.ligandInteractionsAPI(pdbId, chainId, resId, this.environment);
Lukas Pravda's avatar
Lukas Pravda committed
235

Lukas Pravda's avatar
Lukas Pravda committed
236
        d3.json(url)
Lukas Pravda's avatar
Lukas Pravda committed
237
            .catch(e => this.processError(e, 'No interactions data are available.'))
238 239 240
            .then((data: any) => this.addLigandInteractions(data))
            .then(() => new Promise(resolve => setTimeout(resolve, 1500)))
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
241 242
    }

Lukas Pravda's avatar
Lukas Pravda committed
243 244 245 246 247 248 249 250 251
    /**
     * Download ligand structure given the anotation generated by the 
     * PDBeChem process.
     *
     * @param {string} ligandId
     * @returns
     * @memberof Visualization
     */
    public async initLigandDisplay(ligandId: string) {
252
        const ligandUrl = Resources.ligandAnnotationAPI(ligandId, this.environment);
253

Lukas Pravda's avatar
Lukas Pravda committed
254
        return d3.json(ligandUrl)
Lukas Pravda's avatar
Lukas Pravda committed
255
            .catch(e => this.processError(e, `Component ${ligandId} was not found.`))
256
            .then((d: any) => this.addDepiction(d))
Lukas Pravda's avatar
Lukas Pravda committed
257
            .then(() => this.centerScene());
Lukas Pravda's avatar
Lukas Pravda committed
258 259
    }

Lukas Pravda's avatar
Lukas Pravda committed
260

Lukas Pravda's avatar
Lukas Pravda committed
261 262 263 264 265 266 267
    /**
     * Add depiction to the canvas from external resource.
     *
     * @param {*} depiction Content of annotation.json file generated by
     * the PDBeChem process.
     * @memberof Visualization
     */
268
    public addDepiction(depiction: any) {
Lukas Pravda's avatar
Lukas Pravda committed
269
        this.depiction = new Depiction(this.depictionRoot, depiction);
270
        this.depiction.draw();
Lukas Pravda's avatar
Lukas Pravda committed
271 272 273 274
    }


    /**
275 276
     * Add atom highlight to the ligand structure. The previous highlight
     * is going to be removed.
Lukas Pravda's avatar
Lukas Pravda committed
277
     *
278 279
     * @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
280 281 282 283 284 285
     * @memberof Visualization
     */
    public addLigandHighlight(highlight: string[], color: string = undefined) {
        this.depiction.highlightSubgraph(highlight, color);
    }

286 287 288 289 290 291 292 293 294 295 296 297
    /**
     * 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
298
    public toogleZoom(active: boolean) {
299
        this.zoomHandler = active ? this.getZoomHandler() : undefined;
Lukas Pravda's avatar
Lukas Pravda committed
300 301
    }

302

Lukas Pravda's avatar
Lukas Pravda committed
303 304 305 306 307 308 309 310 311 312 313 314 315 316
    /**
     * 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(() => {
317 318
                this.presentBindingSite = new Model.BindingSite().fromLigand(key, body, this.depiction);
                this.bindingSites.push(this.presentBindingSite);
Lukas Pravda's avatar
Lukas Pravda committed
319
                this.setupLigandScene();
Lukas Pravda's avatar
Lukas Pravda committed
320
            });
Lukas Pravda's avatar
Lukas Pravda committed
321
        } else {
322 323
            this.presentBindingSite = new Model.BindingSite().fromLigand(key, body, this.depiction);
            this.bindingSites.push(this.presentBindingSite);
Lukas Pravda's avatar
Lukas Pravda committed
324 325 326 327
            this.setupLigandScene();
        }
    }

328

Lukas Pravda's avatar
Lukas Pravda committed
329 330 331 332 333 334 335 336 337 338 339
    /**
     * 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;
340
        this.presentBindingSite = new Model.BindingSite().fromBoundMolecule(key, data[key][0]);
Lukas Pravda's avatar
Lukas Pravda committed
341

342 343 344
        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
345 346 347

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

350

Lukas Pravda's avatar
Lukas Pravda committed
351
    // #region menu functions
Lukas Pravda's avatar
Lukas Pravda committed
352
    public saveSvg() {
353
        d3.text(Resources.ligEnvCSSAPI(this.environment))
Lukas Pravda's avatar
Lukas Pravda committed
354
            .then(x => {
355
                let svgData = `
Lukas Pravda's avatar
Lukas Pravda committed
356 357 358 359 360 361
                <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
362

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

367 368
                downloadLink.href = svgUrl;
                downloadLink.download = this.getSVGName();
369

370 371 372 373
                document.body.appendChild(downloadLink);
                downloadLink.click();
                document.body.removeChild(downloadLink);
            });
Lukas Pravda's avatar
Lukas Pravda committed
374 375
    }

Lukas Pravda's avatar
Lukas Pravda committed
376

377 378 379 380 381
    /**
     * Download interactions data in the JSON format.
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
382
    public downloadInteractionsData(): void {
Lukas Pravda's avatar
Lukas Pravda committed
383
        let downloadLink = document.createElement('a');
Lukas Pravda's avatar
Lukas Pravda committed
384
        let dataBlob = new Blob([JSON.stringify(this.interactionsData, null, 4)], { type: 'application/json' });
Lukas Pravda's avatar
Lukas Pravda committed
385

Lukas Pravda's avatar
sync  
Lukas Pravda committed
386
        downloadLink.href = URL.createObjectURL(dataBlob);
387
        downloadLink.download = this.interactionsData === undefined ? 'no name.json' : `${this.pdbId}_${this.presentBindingSite.bmId}_interactions.json`;
Lukas Pravda's avatar
Lukas Pravda committed
388 389 390 391 392 393
        document.body.appendChild(downloadLink);
        downloadLink.click();
        document.body.removeChild(downloadLink);
    }


394 395 396 397 398
    /**
     * Reinitialize the scene (basicaly rerun the simulation to place interaction partners)
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
399
    public reinitialize() {
400

401 402 403
        if (this.bindingSites.length > 1 && this.depiction !== undefined) {
            this.presentBindingSite = this.bindingSites[0];
            this.bindingSites.pop();
404

405 406
            this.depictionRoot.selectAll('*').remove();
            this.depiction = undefined;
407

Lukas Pravda's avatar
Lukas Pravda committed
408
            this.nullNodesPositions();
409 410
            this.setupScene().then(() => this.centerScene());
        }
411
        else if (this.depiction === undefined) {
Lukas Pravda's avatar
Lukas Pravda committed
412
            this.nullNodesPositions();
413 414 415
            this.setupScene().then(() => this.centerScene());
        }
        else {
Lukas Pravda's avatar
Lukas Pravda committed
416
            this.nullNodesPositions();
417 418
            this.setupLigandScene().then(() => this.centerScene());
        }
Lukas Pravda's avatar
Lukas Pravda committed
419

420
        this.fireExternalNullEvent(Config.interactionHideLabelEvent);
Lukas Pravda's avatar
Lukas Pravda committed
421
    }
Lukas Pravda's avatar
Lukas Pravda committed
422

Lukas Pravda's avatar
Lukas Pravda committed
423

Lukas Pravda's avatar
Lukas Pravda committed
424

Lukas Pravda's avatar
Lukas Pravda committed
425
    /**
426
     * Center scene to the viewbox
Lukas Pravda's avatar
Lukas Pravda committed
427 428 429
     *
     * @memberof Visualization
     */
Lukas Pravda's avatar
Lukas Pravda committed
430
    public centerScene() {
Lukas Pravda's avatar
Lukas Pravda committed
431
        // Get the bounding box
Lukas Pravda's avatar
Lukas Pravda committed
432 433 434
        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
435

Lukas Pravda's avatar
Lukas Pravda committed
436 437
            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
438

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

Lukas Pravda's avatar
Lukas Pravda committed
441 442 443 444 445 446 447 448 449 450 451 452
        } 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
453 454 455 456 457
        // 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
458 459
        let widthRatio = this.parent.offsetWidth / molWidth;
        let heightRatio = this.parent.offsetHeight / molHeight;
Lukas Pravda's avatar
Lukas Pravda committed
460 461 462

        // 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
463
        let minRatio = Math.min(widthRatio, heightRatio) * 0.85;
Lukas Pravda's avatar
Lukas Pravda committed
464 465 466 467 468 469

        // 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
470 471
        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
472 473

        // do the actual moving
Lukas Pravda's avatar
Lukas Pravda committed
474
        this.canvas.attr('transform', `translate(${xTrans}, ${yTrans}) scale(${minRatio})`);
Lukas Pravda's avatar
Lukas Pravda committed
475 476 477 478

        // 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
479
        let translation = d3.zoomIdentity.translate(xTrans, yTrans).scale(minRatio);
Lukas Pravda's avatar
Lukas Pravda committed
480 481
        this.zoomHandler?.transform(this.svg, translation);

Lukas Pravda's avatar
Lukas Pravda committed
482
    };
483

Lukas Pravda's avatar
Lukas Pravda committed
484 485
    // #endregion menu functions

486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
    // #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 {
503
        if (this.presentBindingSite !== undefined) return `${this.presentBindingSite.bmId}.svg`;
504 505
        if (this.depiction !== undefined) return `${this.depiction.ccdId}.svg`;

506
        return 'blank.svg';
507 508
    }

509
    private nullNodesPositions() {
Lukas Pravda's avatar
Lukas Pravda committed
510 511 512 513 514 515 516 517 518
        this.presentBindingSite.interactionNodes.forEach((x: Model.InteractionNode) => {
            if (!x.static) {
                x.fx = null;
                x.fy = null;
            }
        });

    }

Lukas Pravda's avatar
Lukas Pravda committed
519
    // #region fire events
520 521

    private fireExternalLinkEvent(link: Model.Link, eventName: string) {
Lukas Pravda's avatar
Lukas Pravda committed
522 523
        let atomsSource = [];
        let atomsTarget = [];
Lukas Pravda's avatar
Lukas Pravda committed
524 525

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

Lukas Pravda's avatar
Lukas Pravda committed
529
            let tmpTar = [].concat.apply([], link.interaction.map(x => x.targetAtoms));
Lukas Pravda's avatar
Lukas Pravda committed
530
            atomsTarget = [].concat.apply([], tmpTar).filter((v, i, a) => a.indexOf(v) === i);
Lukas Pravda's avatar
Lukas Pravda committed
531
        }
Lukas Pravda's avatar
Lukas Pravda committed
532

Lukas Pravda's avatar
Lukas Pravda committed
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
        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
                    }
551 552
                ],
                tooltip: link.toTooltip()
Lukas Pravda's avatar
Lukas Pravda committed
553 554 555 556
            }
        });
        this.parent.dispatchEvent(e);
    }
Lukas Pravda's avatar
Lukas Pravda committed
557

558
    private fireExternalNodeEvent(node: Model.InteractionNode, eventName: string) {
Lukas Pravda's avatar
Lukas Pravda committed
559
        const e = new CustomEvent(eventName, {
Lukas Pravda's avatar
Lukas Pravda committed
560 561
            bubbles: true,
            detail: {
Lukas Pravda's avatar
Lukas Pravda committed
562 563 564 565 566
                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
567 568
                },
                tooltip: node.toTooltip()
Lukas Pravda's avatar
Lukas Pravda committed
569 570
            }
        });
Lukas Pravda's avatar
Lukas Pravda committed
571

Lukas Pravda's avatar
Lukas Pravda committed
572
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
573 574
    }

575 576
    private fireExternalNullEvent(eventName: string) {
        const e = new CustomEvent(eventName, {
Lukas Pravda's avatar
Lukas Pravda committed
577 578 579
            bubbles: true,
            detail: {}
        });
Lukas Pravda's avatar
Lukas Pravda committed
580

Lukas Pravda's avatar
Lukas Pravda committed
581
        this.parent.dispatchEvent(e);
Lukas Pravda's avatar
Lukas Pravda committed
582
    }
Lukas Pravda's avatar
Lukas Pravda committed
583

Lukas Pravda's avatar
Lukas Pravda committed
584
    // #endregion fire events
Lukas Pravda's avatar
Lukas Pravda committed
585 586


Lukas Pravda's avatar
Lukas Pravda committed
587 588 589 590 591 592 593 594 595
    //#region setup scene micromethods
    private wipeOutVisuals() {
        this.nodesRoot.selectAll('*').remove();
        this.linksRoot.selectAll('*').remove();
    }

    private setupLinks() {
        this.links = this.linksRoot
            .selectAll()
596
            .data(this.presentBindingSite.links)
Lukas Pravda's avatar
Lukas Pravda committed
597 598 599 600
            .enter().append('g');

        this.links
            .append('line')
601
            .classed('pdb-lig-env-svg-shadow-bond', (x: Model.Link) => x.getLinkClass() !== 'hydrophobic')
Lukas Pravda's avatar
Lukas Pravda committed
602
            .on('mouseenter', (x: Model.Link, index: number, group: any) => this.linkMouseOverEventHandler(x, index, group))
603
            .on('mouseleave', (x: Model.Link, index: number, group: any) => this.linkMouseOutEventHandler(x, index, group));
Lukas Pravda's avatar
Lukas Pravda committed
604 605 606

        this.links
            .append('line')
607
            .attr('class', (e: Model.Link) => `pdb-lig-env-svg-bond pdb-lig-env-svg-bond-${e.getLinkClass()}`)
608
            .attr('marker-mid', (e: Model.Link) => e.hasClash() ? 'url(#clash)' : '')
Lukas Pravda's avatar
Lukas Pravda committed
609
            .on('mouseenter', (x: Model.Link, y: any, z: any) => this.linkMouseOverEventHandler(x, y, z))
610
            .on('mouseleave', (x: Model.Link, index: number, group: any) => this.linkMouseOutEventHandler(x, index, group));
Lukas Pravda's avatar
Lukas Pravda committed
611 612 613 614 615 616 617 618 619 620 621
    }



    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}`];
622
                for (let i = 0; i < labels.length; i++) {
Lukas Pravda's avatar
Lukas Pravda committed
623 624 625 626 627 628 629 630 631 632
                    d3.select(this)
                        .append('tspan')
                        .attr('dy', (i * 20) - 4)
                        .attr('x', 0)
                        .text(labels[i]);
                }
            });
    }
    //#endregion

633 634 635 636 637 638 639 640 641 642
    /**
     * 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
643
    private selectLigand(n: Model.InteractionNode, i: number, g: any) {
Lukas Pravda's avatar
Lukas Pravda committed
644
        this.fireExternalNodeEvent(n, Config.interactionClickEvent);
Lukas Pravda's avatar
Lukas Pravda committed
645

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

648
        this.nodeDim(n, i, g);
649
        this.nodeMouseoutEventHandler(n, i, g);
Lukas Pravda's avatar
Lukas Pravda committed
650
        this.showLigandLabel(n);
Lukas Pravda's avatar
sync  
Lukas Pravda committed
651

Lukas Pravda's avatar
Lukas Pravda committed
652
        this.initLigandInteractions(this.pdbId, n.residue.authorResidueNumber, n.residue.chainId);
Lukas Pravda's avatar
Lukas Pravda committed
653
    }
Lukas Pravda's avatar
Lukas Pravda committed
654

655 656 657 658 659 660 661 662 663 664 665
    /**
     * 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')
666
            .classed('pdb-lig-env-svg-node', true)
Lukas Pravda's avatar
Lukas Pravda committed
667 668 669
            .attr('dominant-baseline', 'center')
            .attr('text-anchor', 'middle')
            .attr('x', this.parent.clientWidth / 2)
670 671
            .attr('y', this.parent.clientHeight / 2)
            .text(msg)
Lukas Pravda's avatar
Lukas Pravda committed
672

673 674
        throw e;

Lukas Pravda's avatar
Lukas Pravda committed
675
    }
676

Lukas Pravda's avatar
Lukas Pravda committed
677

678 679 680 681 682 683 684 685 686
    /**
     * 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
687
    private async setupLigandScene() {
Lukas Pravda's avatar
Lukas Pravda committed
688 689
        this.wipeOutVisuals();
        this.setupLinks();
Lukas Pravda's avatar
Lukas Pravda committed
690

691
        this.presentBindingSite.interactionNodes
Lukas Pravda's avatar
Lukas Pravda committed
692 693
            .filter((x: Model.InteractionNode) => !x.residue.isLigand)
            .forEach((x: Model.InteractionNode) => {
Lukas Pravda's avatar
Lukas Pravda committed
694 695 696 697
                let links = this.presentBindingSite.links.filter((y: Model.LigandResidueLink) => y.containsNode(x) && y.getLinkClass() !== 'hydrophobic');
                
                links = links.length == 0 ? this.presentBindingSite.links.filter((y: Model.LigandResidueLink) => y.containsNode(x)) : links;
                let atom_names = links
698
                    .map((y: Model.LigandResidueLink) => [].concat.apply([], y.interaction.map(z => z.sourceAtoms)));
Lukas Pravda's avatar
Lukas Pravda committed
699 700
                
                let concated = [].concat.apply([], atom_names);
Lukas Pravda's avatar
Lukas Pravda committed
701
                let position: Vector2D = this.depiction.getInitalNodePosition(concated);
702

Lukas Pravda's avatar
Lukas Pravda committed
703 704
                x.x = position.x + Math.random() * 55;
                x.y = position.y + Math.random() * 55;
Lukas Pravda's avatar
Lukas Pravda committed
705 706
            });

Lukas Pravda's avatar
Lukas Pravda committed
707

Lukas Pravda's avatar
Lukas Pravda committed
708
        // setup nodes; wait for resources to be ready
709
        this.presentBindingSite.interactionNodes.forEach(x => this.rProvider.downloadAnnotation(x.residue));
Lukas Pravda's avatar
Lukas Pravda committed
710 711
        await Promise.all(this.rProvider.downloadPromises);
        await Promise.all([this.visualsMapper.graphicsPromise, this.visualsMapper.mappingPromise]);
712

Lukas Pravda's avatar
Lukas Pravda committed
713
        this.nodes = this.nodesRoot.append('g')
Lukas Pravda's avatar
Lukas Pravda committed
714
            .selectAll()
715
            .data(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
716 717
            .enter().append('g');

718

Lukas Pravda's avatar
Lukas Pravda committed
719
        this.nodes.filter((x: Model.InteractionNode) => !x.residue.isLigand)
720
            .attr('class', (x: Model.InteractionNode) => `pdb-lig-env-svg-node pdb-lig-env-svg-${x.residue.getResidueType()}-res`)
721 722
            .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
723 724 725 726 727

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

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

735 736
        let forceLink = d3.forceLink()
            .links(this.links.filter((x: Model.LigandResidueLink) => x.getLinkClass() !== 'hydrophobic'))
Lukas Pravda's avatar
Lukas Pravda committed
737
            .distance(5);
Lukas Pravda's avatar
Lukas Pravda committed
738

Lukas Pravda's avatar
Lukas Pravda committed
739 740
        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
741

742
        this.simulation = d3.forceSimulation(this.presentBindingSite.interactionNodes)
743 744
            .force('link', forceLink)
            .force('charge', charge) //strength 
Lukas Pravda's avatar
Lukas Pravda committed
745
            .force('collision', collision)
746
            .on('tick', () => this.simulationStep());
Lukas Pravda's avatar
Lukas Pravda committed
747

Lukas Pravda's avatar
Lukas Pravda committed
748 749
        this.dragHandler(this.nodes);
        if (this.zoomHandler !== undefined) this.zoomHandler(this.svg, d3.zoomIdentity);
Lukas Pravda's avatar
Lukas Pravda committed
750 751
    }

Lukas Pravda's avatar
Lukas Pravda committed
752

753 754 755 756 757 758 759 760
    /**
     * 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
761
    private async setupScene() {
Lukas Pravda's avatar
Lukas Pravda committed
762 763
        this.wipeOutVisuals();
        this.setupLinks();
764

Lukas Pravda's avatar
Lukas Pravda committed
765
        // setup nodes; wait for resources to be ready
766
        this.presentBindingSite.interactionNodes.forEach(x => this.rProvider.downloadAnnotation(x.residue));
Lukas Pravda's avatar
Lukas Pravda committed
767 768
        await Promise.all(this.rProvider.downloadPromises);
        await Promise.all([this.visualsMapper.graphicsPromise, this.visualsMapper.mappingPromise]);
769

Lukas Pravda's avatar
Lukas Pravda committed
770
        this.nodes = this.nodesRoot
Lukas Pravda's avatar
Lukas Pravda committed
771
            .selectAll()
772
            .data(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
773
            .enter().append('g')
774
            .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
775
            .on('click', (x: Model.InteractionNode, i: number, g: any) => this.selectLigand(x, i, g))
776 777
            .on('mouseover', (x: Model.InteractionNode, i: number, g: any) => this.nodeMouseoverEventHandler(x, i, g))
            .on('mouseout', (x: Model.InteractionNode, i: number, g: any) => this.nodeMouseoutEventHandler(x, i, g));
Lukas Pravda's avatar
Lukas Pravda committed
778

Lukas Pravda's avatar
Lukas Pravda committed
779

Lukas Pravda's avatar
Lukas Pravda committed
780
        this.nodes.filter((n: Model.InteractionNode) =>
Lukas Pravda's avatar
Lukas Pravda committed
781
            this.visualsMapper.glycanMapping.has(n.residue.chemCompId))
Lukas Pravda's avatar
Lukas Pravda committed
782
            .html((e: Model.InteractionNode) => this.visualsMapper.getGlycanImage(e.residue.chemCompId));
Lukas Pravda's avatar
Lukas Pravda committed
783

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

Lukas Pravda's avatar
Lukas Pravda committed
788
        this.addNodeLabels(this.nodes);
Lukas Pravda's avatar
Lukas Pravda committed
789 790

        let forceLink = d3.forceLink()
791
            .links(this.presentBindingSite.links)
792
            .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
793 794 795 796
            .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
797
        let center = d3.forceCenter(this.parent.offsetWidth / 2, this.parent.offsetHeight / 2);
Lukas Pravda's avatar
Lukas Pravda committed
798

799
        this.simulation = d3.forceSimulation(this.presentBindingSite.interactionNodes)
Lukas Pravda's avatar
Lukas Pravda committed
800 801 802 803
            .force('link', forceLink)
            .force('charge', charge) //strength 
            .force('collision', collision)
            .force('center', center)
804
            .on('tick', () => this.simulationStep());
Lukas Pravda's avatar
Lukas Pravda committed
805

Lukas Pravda's avatar
Lukas Pravda committed
806 807 808
        this.dragHandler(this.nodes);

        if (this.zoomHandler) this.zoomHandler(this.svg);
Lukas Pravda's avatar
Lukas Pravda committed
809
    }
Lukas Pravda's avatar
Lukas Pravda committed
810