jiehou commited on
Commit
8607410
·
verified ·
1 Parent(s): a83e037

Upload visualization.py

Browse files
Files changed (1) hide show
  1. visualization.py +915 -0
visualization.py ADDED
@@ -0,0 +1,915 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 3D Visualization Module for RNA Structure Comparison
3
+ Uses py3Dmol for interactive molecular visualization
4
+ """
5
+
6
+ import numpy as np
7
+ from pathlib import Path
8
+ from rmsd_utils import (
9
+ parse_residue_atoms,
10
+ translate_rotate_coords,
11
+ calculate_COM,
12
+ get_backbone_sugar_and_selectbase_coords_fixed
13
+ )
14
+
15
+
16
+ def create_structure_visualization(ref_path, query_path, ref_window_indices, query_window_indices,
17
+ rotation_matrix, ref_com, query_com, rmsd=None,
18
+ ref_name=None, query_name=None, ref_sequence=None, query_sequence=None):
19
+ """
20
+ Create an interactive 3D visualization of aligned structures.
21
+
22
+ Args:
23
+ ref_path: Path to reference motif PDB file
24
+ query_path: Path to query motif PDB file
25
+ ref_window_indices: List of residue indices for the reference window
26
+ query_window_indices: List of residue indices for the query window
27
+ rotation_matrix: Rotation matrix from RMSD calculation
28
+ ref_com: Center of mass of reference window
29
+ query_com: Center of mass of query window
30
+ rmsd: RMSD value (optional, for display)
31
+ ref_name: Reference structure name (optional, for display)
32
+ query_name: Query structure name (optional, for display)
33
+ ref_sequence: Reference sequence (optional, for display)
34
+ query_sequence: Query sequence (optional, for display)
35
+
36
+ Returns:
37
+ HTML string containing the py3Dmol visualization
38
+ """
39
+
40
+ # Extract simple names if not provided
41
+ if ref_name is None:
42
+ ref_name = Path(ref_path).stem
43
+ if query_name is None:
44
+ query_name = Path(query_path).stem
45
+
46
+ # Read PDB files
47
+ with open(ref_path) as f:
48
+ ref_pdb = f.read()
49
+
50
+ with open(query_path) as f:
51
+ query_pdb_full = f.read()
52
+
53
+ # Extract only the window residues from both structures
54
+ ref_residues = parse_residue_atoms(ref_path)
55
+ query_residues = parse_residue_atoms(query_path)
56
+
57
+ ref_window_pdb = extract_window_pdb(ref_path, ref_window_indices)
58
+ query_window_pdb = extract_window_pdb(query_path, query_window_indices)
59
+
60
+ # Parse window coordinates for transformation
61
+ from rmsd_utils import get_backbone_sugar_coords_from_residue, get_base_coords_from_residue
62
+
63
+ ref_window_coords = []
64
+ for idx in ref_window_indices:
65
+ if idx < len(ref_residues):
66
+ residue = ref_residues[idx]
67
+ backbone_coords = get_backbone_sugar_coords_from_residue(residue)
68
+ ref_window_coords.extend(backbone_coords)
69
+ base_coords = get_base_coords_from_residue(residue)
70
+ ref_window_coords.extend(base_coords)
71
+ ref_window_coords = np.asarray(ref_window_coords)
72
+
73
+ query_window_coords = []
74
+ for idx in query_window_indices:
75
+ if idx < len(query_residues):
76
+ residue = query_residues[idx]
77
+ backbone_coords = get_backbone_sugar_coords_from_residue(residue)
78
+ query_window_coords.extend(backbone_coords)
79
+ base_coords = get_base_coords_from_residue(residue)
80
+ query_window_coords.extend(base_coords)
81
+ query_window_coords = np.asarray(query_window_coords)
82
+
83
+ # Transform query window to align with reference window
84
+ # Proper alignment: translate to origin, rotate, translate to reference position
85
+ # Note: We need both query_com and ref_com for proper alignment
86
+ transformed_query_pdb = transform_pdb_string(
87
+ query_window_pdb,
88
+ rotation_matrix,
89
+ query_com,
90
+ ref_com # Add reference COM for proper alignment
91
+ )
92
+
93
+ # Create py3Dmol visualization
94
+ html = f"""
95
+ <!DOCTYPE html>
96
+ <html>
97
+ <head>
98
+ <script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>
99
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
100
+ <style>
101
+ #container {{
102
+ width: 100%;
103
+ height: 700px;
104
+ position: relative;
105
+ border: 1px solid #ddd;
106
+ }}
107
+ .control-panel {{
108
+ position: absolute;
109
+ top: 10px;
110
+ right: 10px;
111
+ background: rgba(255, 255, 255, 0.95);
112
+ padding: 15px;
113
+ border-radius: 8px;
114
+ font-family: Arial, sans-serif;
115
+ font-size: 13px;
116
+ z-index: 1000;
117
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
118
+ max-width: 220px;
119
+ }}
120
+ .control-panel h4 {{
121
+ margin: 0 0 10px 0;
122
+ font-size: 14px;
123
+ color: #333;
124
+ }}
125
+ .control-section {{
126
+ margin-bottom: 12px;
127
+ padding-bottom: 12px;
128
+ border-bottom: 1px solid #eee;
129
+ }}
130
+ .control-section:last-child {{
131
+ border-bottom: none;
132
+ margin-bottom: 0;
133
+ }}
134
+ .control-section label {{
135
+ display: block;
136
+ margin: 6px 0;
137
+ cursor: pointer;
138
+ }}
139
+ .control-section input[type="checkbox"] {{
140
+ margin-right: 8px;
141
+ }}
142
+ .control-section select {{
143
+ width: 100%;
144
+ padding: 4px;
145
+ margin-top: 5px;
146
+ border: 1px solid #ccc;
147
+ border-radius: 4px;
148
+ }}
149
+ .legend {{
150
+ position: absolute;
151
+ top: 10px;
152
+ left: 10px;
153
+ background: rgba(255, 255, 255, 0.95);
154
+ padding: 15px;
155
+ border-radius: 8px;
156
+ font-family: Arial, sans-serif;
157
+ font-size: 13px;
158
+ z-index: 1000;
159
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
160
+ }}
161
+ .legend h4 {{
162
+ margin: 0 0 10px 0;
163
+ font-size: 14px;
164
+ color: #333;
165
+ }}
166
+ .legend-item {{
167
+ margin: 6px 0;
168
+ display: flex;
169
+ align-items: center;
170
+ }}
171
+ .color-box {{
172
+ width: 24px;
173
+ height: 16px;
174
+ margin-right: 10px;
175
+ border: 1px solid #333;
176
+ border-radius: 2px;
177
+ }}
178
+ .rmsd-info {{
179
+ position: absolute;
180
+ bottom: 10px;
181
+ left: 10px;
182
+ background: rgba(255, 255, 255, 0.95);
183
+ padding: 12px 15px;
184
+ border-radius: 8px;
185
+ font-family: Arial, sans-serif;
186
+ font-size: 13px;
187
+ z-index: 1000;
188
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
189
+ max-width: 450px;
190
+ }}
191
+ .info-row {{
192
+ margin: 4px 0;
193
+ line-height: 1.4;
194
+ }}
195
+ .info-label {{
196
+ font-weight: bold;
197
+ color: #555;
198
+ }}
199
+ .info-value {{
200
+ color: #333;
201
+ font-family: 'Courier New', monospace;
202
+ }}
203
+ .section-title {{
204
+ font-weight: bold;
205
+ color: #555;
206
+ margin-bottom: 5px;
207
+ font-size: 12px;
208
+ text-transform: uppercase;
209
+ }}
210
+ .download-section {{
211
+ position: absolute;
212
+ bottom: 10px;
213
+ right: 10px;
214
+ background: rgba(255, 255, 255, 0.95);
215
+ padding: 10px;
216
+ border-radius: 8px;
217
+ font-family: Arial, sans-serif;
218
+ z-index: 1000;
219
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
220
+ }}
221
+ .download-btn {{
222
+ background: #4A90E2;
223
+ color: white;
224
+ border: none;
225
+ padding: 8px 16px;
226
+ border-radius: 4px;
227
+ cursor: pointer;
228
+ font-size: 13px;
229
+ font-weight: bold;
230
+ }}
231
+ .download-btn:hover {{
232
+ background: #357ABD;
233
+ }}
234
+ </style>
235
+ </head>
236
+ <body>
237
+ <div id="container"></div>
238
+
239
+ <div class="legend">
240
+ <h4>🧬 Structures</h4>
241
+ <div class="legend-item">
242
+ <div class="color-box" style="background: #4A90E2;"></div>
243
+ <span>Reference</span>
244
+ </div>
245
+ <div class="legend-item">
246
+ <div class="color-box" style="background: #E94B3C;"></div>
247
+ <span>Query (Aligned)</span>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="control-panel">
252
+ <h4>⚙️ Display Options</h4>
253
+
254
+ <div class="control-section">
255
+ <div class="section-title">Structures</div>
256
+ <label>
257
+ <input type="checkbox" id="showRef" checked onchange="updateDisplay()">
258
+ Reference
259
+ </label>
260
+ <label>
261
+ <input type="checkbox" id="showQuery" checked onchange="updateDisplay()">
262
+ Query
263
+ </label>
264
+ </div>
265
+
266
+ <div class="control-section">
267
+ <div class="section-title">Style</div>
268
+ <select id="styleMode" onchange="updateDisplay()">
269
+ <option value="sticks">Sticks</option>
270
+ <option value="cartoon">Cartoon</option>
271
+ <option value="spheres">Spheres</option>
272
+ <option value="lines">Lines</option>
273
+ <option value="cartoon_sticks">Cartoon + Sticks</option>
274
+ </select>
275
+ </div>
276
+
277
+ <div class="control-section">
278
+ <div class="section-title">Components</div>
279
+ <label>
280
+ <input type="checkbox" id="showBackbone" checked onchange="updateDisplay()">
281
+ Backbone/Sugar
282
+ </label>
283
+ <label>
284
+ <input type="checkbox" id="showBases" checked onchange="updateDisplay()">
285
+ Bases
286
+ </label>
287
+ </div>
288
+
289
+ <div class="control-section">
290
+ <div class="section-title">Labels</div>
291
+ <label>
292
+ <input type="checkbox" id="showLabels" onchange="updateDisplay()">
293
+ Residue Labels
294
+ </label>
295
+ <label>
296
+ <input type="checkbox" id="showNumbers" onchange="updateDisplay()">
297
+ Residue Numbers
298
+ </label>
299
+ <label>
300
+ <input type="checkbox" id="showAtoms" onchange="updateDisplay()">
301
+ Atom Names
302
+ </label>
303
+ <select id="atomLabelMode" style="margin-top: 5px; font-size: 11px;" onchange="updateDisplay()">
304
+ <option value="all">All Atoms</option>
305
+ <option value="backbone">Backbone Only</option>
306
+ <option value="sidechain">Bases Only</option>
307
+ </select>
308
+ </div>
309
+
310
+ <div class="control-section">
311
+ <div class="section-title">Background</div>
312
+ <select id="bgColor" onchange="updateBackground()">
313
+ <option value="white">White</option>
314
+ <option value="black">Black</option>
315
+ <option value="gray">Gray</option>
316
+ </select>
317
+ </div>
318
+
319
+ <div class="control-section">
320
+ <div class="section-title">Annotation Font Size</div>
321
+ <select id="annotationFontSize">
322
+ <option value="small">Small (18pt/16pt/14pt)</option>
323
+ <option value="medium">Medium (22pt/18pt/16pt)</option>
324
+ <option value="large" selected>Large (28pt/22pt/18pt)</option>
325
+ <option value="xlarge">Extra Large (36pt/28pt/22pt)</option>
326
+ </select>
327
+ </div>
328
+ </div>
329
+
330
+ <div class="rmsd-info">
331
+ <div class="info-row">
332
+ <span class="info-label">RMSD:</span>
333
+ <span style="color: #E94B3C; font-weight: bold; font-size: 14px;">{f"{rmsd:.3f}" if rmsd is not None else "N/A"} Å</span>
334
+ </div>
335
+ <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ddd;">
336
+ <div class="info-row">
337
+ <span class="info-label">Reference:</span>
338
+ <span class="info-value">{ref_name}</span>
339
+ </div>
340
+ {f'<div class="info-row" style="margin-left: 15px; font-size: 12px;"><span class="info-label">Seq:</span> <span class="info-value">{ref_sequence}</span></div>' if ref_sequence else ''}
341
+ </div>
342
+ <div style="margin-top: 6px;">
343
+ <div class="info-row">
344
+ <span class="info-label">Query:</span>
345
+ <span class="info-value">{query_name}</span>
346
+ </div>
347
+ {f'<div class="info-row" style="margin-left: 15px; font-size: 12px;"><span class="info-label">Seq:</span> <span class="info-value">{query_sequence}</span></div>' if query_sequence else ''}
348
+ </div>
349
+ </div>
350
+
351
+ <div class="download-section">
352
+ <button class="download-btn" onclick="downloadImage()">📷 Download PNG</button>
353
+ </div>
354
+
355
+ <script>
356
+ let viewer = null;
357
+ let refModel = null;
358
+ let queryModel = null;
359
+ const refPDB = `{ref_window_pdb}`;
360
+ const queryPDB = `{transformed_query_pdb}`;
361
+
362
+ // RNA backbone atoms
363
+ const backboneAtoms = ['P', 'OP1', 'OP2', "O5'", "C5'", "C4'", "O4'", "C3'", "O3'", "C2'", "O2'", "C1'"];
364
+
365
+ function initViewer() {{
366
+ try {{
367
+ viewer = $3Dmol.createViewer("container", {{
368
+ backgroundColor: 'white'
369
+ }});
370
+
371
+ if (!refPDB || refPDB.length < 10) {{
372
+ throw new Error("Reference PDB data is empty");
373
+ }}
374
+
375
+ if (!queryPDB || queryPDB.length < 10) {{
376
+ throw new Error("Query PDB data is empty");
377
+ }}
378
+
379
+ updateDisplay();
380
+ viewer.zoomTo();
381
+ viewer.render();
382
+
383
+ }} catch (error) {{
384
+ console.error("Error initializing viewer:", error);
385
+ document.getElementById("container").innerHTML =
386
+ '<div style="padding: 20px; color: red; text-align: center;">Error loading visualization: ' + error.message + '</div>';
387
+ }}
388
+ }}
389
+
390
+ function updateBackground() {{
391
+ const bgColor = document.getElementById('bgColor').value;
392
+ viewer.setBackgroundColor(bgColor);
393
+ viewer.render();
394
+ }}
395
+
396
+ function updateDisplay() {{
397
+ if (!viewer) return;
398
+
399
+ try {{
400
+ // Clear everything
401
+ viewer.removeAllModels();
402
+ viewer.removeAllLabels();
403
+
404
+ const showRef = document.getElementById('showRef').checked;
405
+ const showQuery = document.getElementById('showQuery').checked;
406
+ const showBackbone = document.getElementById('showBackbone').checked;
407
+ const showBases = document.getElementById('showBases').checked;
408
+ const showLabels = document.getElementById('showLabels').checked;
409
+ const showNumbers = document.getElementById('showNumbers').checked;
410
+ const showAtoms = document.getElementById('showAtoms').checked;
411
+ const styleMode = document.getElementById('styleMode').value;
412
+
413
+ // Reference structure (blue)
414
+ if (showRef) {{
415
+ refModel = viewer.addModel(refPDB, "pdb");
416
+ applyStyle(refModel, '#4A90E2', '#5BA3F5', styleMode, showBackbone, showBases);
417
+
418
+ if (showLabels || showNumbers) {{
419
+ addResidueLabels(refModel, '#4A90E2', showLabels, showNumbers);
420
+ }}
421
+ if (showAtoms) {{
422
+ addAtomLabels(refModel, '#4A90E2');
423
+ }}
424
+ }}
425
+
426
+ // Query structure (red)
427
+ if (showQuery) {{
428
+ queryModel = viewer.addModel(queryPDB, "pdb");
429
+ applyStyle(queryModel, '#E94B3C', '#FF6B6B', styleMode, showBackbone, showBases);
430
+
431
+ if (showLabels || showNumbers) {{
432
+ addResidueLabels(queryModel, '#E94B3C', showLabels, showNumbers);
433
+ }}
434
+ if (showAtoms) {{
435
+ addAtomLabels(queryModel, '#E94B3C');
436
+ }}
437
+ }}
438
+
439
+ viewer.zoomTo();
440
+ viewer.render();
441
+
442
+ }} catch (error) {{
443
+ console.error("Error updating display:", error);
444
+ }}
445
+ }}
446
+
447
+ function applyStyle(model, backboneColor, baseColor, styleMode, showBackbone, showBases) {{
448
+ // Clear any existing styles
449
+ viewer.setStyle({{model: model}}, {{}});
450
+
451
+ if (styleMode === 'cartoon') {{
452
+ // Cartoon representation
453
+ viewer.setStyle({{model: model}}, {{
454
+ cartoon: {{
455
+ color: backboneColor,
456
+ thickness: 0.5,
457
+ opacity: 0.8
458
+ }}
459
+ }});
460
+ }} else if (styleMode === 'cartoon_sticks') {{
461
+ // Cartoon + sticks for bases
462
+ viewer.setStyle({{model: model}}, {{
463
+ cartoon: {{
464
+ color: backboneColor,
465
+ thickness: 0.5,
466
+ opacity: 0.7
467
+ }}
468
+ }});
469
+ if (showBases) {{
470
+ viewer.addStyle({{model: model, not: {{atom: backboneAtoms}}}}, {{
471
+ stick: {{
472
+ color: baseColor,
473
+ radius: 0.15
474
+ }}
475
+ }});
476
+ }}
477
+ }} else if (styleMode === 'spheres') {{
478
+ // Sphere representation
479
+ if (showBackbone) {{
480
+ viewer.setStyle({{model: model, atom: backboneAtoms}}, {{
481
+ sphere: {{
482
+ color: backboneColor,
483
+ radius: 0.4
484
+ }}
485
+ }});
486
+ }}
487
+ if (showBases) {{
488
+ viewer.addStyle({{model: model, not: {{atom: backboneAtoms}}}}, {{
489
+ sphere: {{
490
+ color: baseColor,
491
+ radius: 0.35
492
+ }}
493
+ }});
494
+ }}
495
+ }} else if (styleMode === 'lines') {{
496
+ // Line representation
497
+ if (showBackbone) {{
498
+ viewer.setStyle({{model: model, atom: backboneAtoms}}, {{
499
+ line: {{
500
+ color: backboneColor,
501
+ linewidth: 2
502
+ }}
503
+ }});
504
+ }}
505
+ if (showBases) {{
506
+ viewer.addStyle({{model: model, not: {{atom: backboneAtoms}}}}, {{
507
+ line: {{
508
+ color: baseColor,
509
+ linewidth: 2
510
+ }}
511
+ }});
512
+ }}
513
+ }} else {{
514
+ // Stick representation (default)
515
+ if (showBackbone) {{
516
+ viewer.setStyle({{model: model, atom: backboneAtoms}}, {{
517
+ stick: {{
518
+ color: backboneColor,
519
+ radius: 0.2
520
+ }},
521
+ sphere: {{
522
+ color: backboneColor,
523
+ radius: 0.3
524
+ }}
525
+ }});
526
+ }}
527
+ if (showBases) {{
528
+ viewer.addStyle({{model: model, not: {{atom: backboneAtoms}}}}, {{
529
+ stick: {{
530
+ color: baseColor,
531
+ radius: 0.15
532
+ }},
533
+ sphere: {{
534
+ color: baseColor,
535
+ radius: 0.25
536
+ }}
537
+ }});
538
+ }}
539
+ }}
540
+ }}
541
+
542
+ function addResidueLabels(model, color, showLabels, showNumbers) {{
543
+ const atoms = viewer.selectedAtoms({{model: model}});
544
+ const residues = {{}};
545
+
546
+ // Group atoms by residue
547
+ atoms.forEach(atom => {{
548
+ const key = atom.chain + '_' + atom.resi;
549
+ if (!residues[key]) {{
550
+ residues[key] = atom;
551
+ }}
552
+ }});
553
+
554
+ // Add labels for each residue
555
+ Object.values(residues).forEach(atom => {{
556
+ let labelText = '';
557
+ if (showLabels && showNumbers) {{
558
+ labelText = atom.resn + atom.resi;
559
+ }} else if (showLabels) {{
560
+ labelText = atom.resn;
561
+ }} else if (showNumbers) {{
562
+ labelText = atom.resi.toString();
563
+ }}
564
+
565
+ if (labelText) {{
566
+ viewer.addLabel(labelText, {{
567
+ position: atom,
568
+ backgroundColor: color,
569
+ backgroundOpacity: 0.7,
570
+ fontColor: 'white',
571
+ fontSize: 11,
572
+ fontWeight: 'bold',
573
+ showBackground: true,
574
+ borderRadius: 3
575
+ }});
576
+ }}
577
+ }});
578
+ }}
579
+
580
+ function addAtomLabels(model, color) {{
581
+ const atomLabelMode = document.getElementById('atomLabelMode').value;
582
+ const atoms = viewer.selectedAtoms({{model: model}});
583
+
584
+ // Filter atoms based on mode
585
+ let filteredAtoms = atoms;
586
+ if (atomLabelMode === 'backbone') {{
587
+ // Only backbone atoms
588
+ filteredAtoms = atoms.filter(atom => backboneAtoms.includes(atom.atom));
589
+ }} else if (atomLabelMode === 'sidechain') {{
590
+ // Only base/sidechain atoms (not backbone)
591
+ filteredAtoms = atoms.filter(atom => !backboneAtoms.includes(atom.atom));
592
+ }}
593
+ // 'all' mode uses all atoms (no filtering)
594
+
595
+ // Add label for each atom
596
+ filteredAtoms.forEach(atom => {{
597
+ // Use atom name (e.g., P, C1', N1, O4, etc.)
598
+ const atomName = atom.atom;
599
+
600
+ viewer.addLabel(atomName, {{
601
+ position: atom,
602
+ backgroundColor: color,
603
+ backgroundOpacity: 0.6,
604
+ fontColor: 'white',
605
+ fontSize: 9,
606
+ fontWeight: 'normal',
607
+ showBackground: true,
608
+ borderRadius: 2,
609
+ borderThickness: 0.5
610
+ }});
611
+ }});
612
+ }}
613
+
614
+ function downloadImage() {{
615
+ try {{
616
+ // Generate filename with metadata
617
+ var refName = "{ref_name}".replace('.pdb', '');
618
+ var queryName = "{query_name}".replace('.pdb', '');
619
+ var rmsdValue = "{f'{rmsd:.3f}' if rmsd is not None else 'NA'}";
620
+ var refSeq = "{ref_sequence if ref_sequence else ''}";
621
+ var querySeq = "{query_sequence if query_sequence else ''}";
622
+
623
+ var filenameOriginal = 'alignment_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
624
+ var filenameAnnotated = 'annotated_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
625
+
626
+ // Get selected font size
627
+ const fontSizeSelect = document.getElementById('annotationFontSize');
628
+ const fontSizeOption = fontSizeSelect ? fontSizeSelect.value : 'large';
629
+
630
+ // Define font sizes based on selection (all values are at 2x scale for high resolution)
631
+ let fontSizes;
632
+ switch(fontSizeOption) {{
633
+ case 'small':
634
+ fontSizes = {{ rmsd: 36, name: 32, seq: 28 }}; // 18pt/16pt/14pt at 2x
635
+ break;
636
+ case 'medium':
637
+ fontSizes = {{ rmsd: 44, name: 36, seq: 32 }}; // 22pt/18pt/16pt at 2x
638
+ break;
639
+ case 'large':
640
+ fontSizes = {{ rmsd: 56, name: 44, seq: 36 }}; // 28pt/22pt/18pt at 2x
641
+ break;
642
+ case 'xlarge':
643
+ fontSizes = {{ rmsd: 72, name: 56, seq: 44 }}; // 36pt/28pt/22pt at 2x
644
+ break;
645
+ default:
646
+ fontSizes = {{ rmsd: 56, name: 44, seq: 36 }}; // Default to large
647
+ }}
648
+
649
+ // Ensure viewer is rendered
650
+ if (viewer) {{
651
+ viewer.render();
652
+ }}
653
+
654
+ // Get the container element
655
+ const container = document.getElementById('container');
656
+
657
+ if (!container) {{
658
+ alert('Container not ready. Please wait and try again.');
659
+ return;
660
+ }}
661
+
662
+ // Use html2canvas to capture the entire container with overlays
663
+ html2canvas(container, {{
664
+ backgroundColor: '#ffffff',
665
+ scale: 2, // Higher resolution
666
+ logging: false,
667
+ useCORS: true,
668
+ allowTaint: true
669
+ }}).then(function(canvas) {{
670
+ // Create ANNOTATED version
671
+ const annotatedCanvas = document.createElement('canvas');
672
+ annotatedCanvas.width = canvas.width;
673
+ annotatedCanvas.height = canvas.height;
674
+ const ctx = annotatedCanvas.getContext('2d');
675
+
676
+ // Draw the original image onto new canvas
677
+ ctx.drawImage(canvas, 0, 0);
678
+
679
+ // Add annotations
680
+ const margin = 30; // Scaled for 2x resolution
681
+ const padding = 24;
682
+ const lineSpacing = 16;
683
+
684
+ // Prepare annotation text with selected font sizes
685
+ const annotations = [
686
+ {{ text: 'RMSD: ' + rmsdValue + ' Å', fontSize: fontSizes.rmsd, fontFamily: 'bold Arial', color: '#E94B3C' }},
687
+ {{ text: '', fontSize: 20, fontFamily: 'Arial', color: '#333' }}, // Spacer
688
+ {{ text: 'Reference: ' + refName, fontSize: fontSizes.name, fontFamily: 'Arial', color: '#333' }},
689
+ {{ text: ' Seq: ' + refSeq, fontSize: fontSizes.seq, fontFamily: 'Courier New, monospace', color: '#666' }},
690
+ {{ text: '', fontSize: 20, fontFamily: 'Arial', color: '#333' }}, // Spacer
691
+ {{ text: 'Query: ' + queryName, fontSize: fontSizes.name, fontFamily: 'Arial', color: '#333' }},
692
+ {{ text: ' Seq: ' + querySeq, fontSize: fontSizes.seq, fontFamily: 'Courier New, monospace', color: '#666' }}
693
+ ];
694
+
695
+ // Calculate box dimensions
696
+ let maxWidth = 0;
697
+ let totalHeight = padding * 2;
698
+ const textMetrics = [];
699
+
700
+ annotations.forEach(ann => {{
701
+ if (ann.text) {{
702
+ ctx.font = ann.fontSize + 'px ' + ann.fontFamily;
703
+ const metrics = ctx.measureText(ann.text);
704
+ const height = ann.fontSize * 1.2; // Approximate height
705
+ textMetrics.push({{ width: metrics.width, height: height }});
706
+ maxWidth = Math.max(maxWidth, metrics.width);
707
+ totalHeight += height + lineSpacing;
708
+ }} else {{
709
+ textMetrics.push({{ width: 0, height: lineSpacing / 2 }});
710
+ totalHeight += lineSpacing / 2;
711
+ }}
712
+ }});
713
+
714
+ const boxWidth = maxWidth + padding * 2;
715
+ const boxHeight = totalHeight;
716
+
717
+ // Position box in bottom-left
718
+ const boxX = margin;
719
+ const boxY = annotatedCanvas.height - boxHeight - margin;
720
+
721
+ // Draw semi-transparent white background with rounded corners
722
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
723
+ const radius = 16;
724
+ ctx.beginPath();
725
+ ctx.moveTo(boxX + radius, boxY);
726
+ ctx.lineTo(boxX + boxWidth - radius, boxY);
727
+ ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + radius);
728
+ ctx.lineTo(boxX + boxWidth, boxY + boxHeight - radius);
729
+ ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - radius, boxY + boxHeight);
730
+ ctx.lineTo(boxX + radius, boxY + boxHeight);
731
+ ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - radius);
732
+ ctx.lineTo(boxX, boxY + radius);
733
+ ctx.quadraticCurveTo(boxX, boxY, boxX + radius, boxY);
734
+ ctx.closePath();
735
+ ctx.fill();
736
+
737
+ // Draw border
738
+ ctx.strokeStyle = 'rgba(200, 200, 200, 0.95)';
739
+ ctx.lineWidth = 2;
740
+ ctx.stroke();
741
+
742
+ // Draw text
743
+ let currentY = boxY + padding;
744
+ annotations.forEach((ann, idx) => {{
745
+ if (ann.text) {{
746
+ ctx.font = ann.fontSize + 'px ' + ann.fontFamily;
747
+ ctx.fillStyle = ann.color;
748
+ ctx.fillText(ann.text, boxX + padding, currentY + textMetrics[idx].height * 0.8);
749
+ currentY += textMetrics[idx].height + lineSpacing;
750
+ }} else {{
751
+ currentY += textMetrics[idx].height;
752
+ }}
753
+ }});
754
+
755
+ // Download ONLY the annotated PNG
756
+ const annotatedDataURL = annotatedCanvas.toDataURL('image/png');
757
+ const linkAnnotated = document.createElement('a');
758
+ linkAnnotated.download = filenameAnnotated;
759
+ linkAnnotated.href = annotatedDataURL;
760
+ document.body.appendChild(linkAnnotated);
761
+ linkAnnotated.click();
762
+ document.body.removeChild(linkAnnotated);
763
+
764
+ }}).catch(function(error) {{
765
+ console.error('html2canvas error:', error);
766
+ alert('Error creating images. Please try again.');
767
+ }});
768
+
769
+ }} catch (error) {{
770
+ console.error('PNG download error:', error);
771
+ alert('Error downloading PNG: ' + error.message);
772
+ }}
773
+ }}
774
+
775
+ // Initialize on load
776
+ initViewer();
777
+ </script>
778
+ </body>
779
+ </html>
780
+ """
781
+
782
+ return html
783
+
784
+
785
+ def extract_window_pdb(pdb_path, window_indices):
786
+ """
787
+ Extract specific residues from a PDB file based on window indices.
788
+
789
+ Args:
790
+ pdb_path: Path to PDB file
791
+ window_indices: List of residue indices (0-based)
792
+
793
+ Returns:
794
+ String containing PDB data for only the specified residues
795
+ """
796
+ with open(pdb_path) as f:
797
+ lines = f.readlines()
798
+
799
+ # Get all residue numbers from the file
800
+ residues = parse_residue_atoms(pdb_path)
801
+
802
+ if not residues:
803
+ # If parsing failed, return original file
804
+ return ''.join(lines)
805
+
806
+ residue_numbers = [res['resnum'] for res in residues]
807
+
808
+ # Map window indices to actual residue numbers
809
+ target_resnums = set()
810
+ for idx in window_indices:
811
+ if idx < len(residue_numbers):
812
+ target_resnums.add(residue_numbers[idx])
813
+
814
+ if not target_resnums:
815
+ # If no valid residues, return original file
816
+ return ''.join(lines)
817
+
818
+ # Extract lines for these residues
819
+ window_lines = []
820
+ for line in lines:
821
+ if len(line) < 6:
822
+ continue
823
+
824
+ record = line[0:6].strip()
825
+ if record in ['ATOM', 'HETATM', 'HETAT']:
826
+ try:
827
+ # Handle different PDB formats
828
+ resnum_str = line[22:26].strip()
829
+ if resnum_str:
830
+ resnum = int(resnum_str)
831
+ if resnum in target_resnums:
832
+ window_lines.append(line)
833
+ except (ValueError, IndexError):
834
+ continue
835
+ elif record in ['HEADER', 'TITLE', 'MODEL', 'ENDMDL']:
836
+ window_lines.append(line)
837
+
838
+ # Always add END record
839
+ if window_lines and not any('END' in line for line in window_lines):
840
+ window_lines.append('END\n')
841
+
842
+ result = ''.join(window_lines)
843
+
844
+ # Debug: print info about extraction
845
+ if not result or len(result) < 50:
846
+ print(f"Warning: Empty or very small PDB extracted from {pdb_path}")
847
+ print(f" Window indices: {window_indices}")
848
+ print(f" Target residue numbers: {target_resnums}")
849
+ print(f" Result length: {len(result)}")
850
+ # Return full structure if extraction failed
851
+ return ''.join(lines)
852
+
853
+ return result
854
+
855
+
856
+ def transform_pdb_string(pdb_string, rotation_matrix, query_com, ref_com=None):
857
+ """
858
+ Apply rotation and translation to coordinates in a PDB string to align with reference.
859
+
860
+ The transformation aligns the query structure to the reference structure:
861
+ 1. Translate query to origin (subtract query_com)
862
+ 2. Apply rotation matrix
863
+ 3. Translate to reference position (add ref_com)
864
+
865
+ Args:
866
+ pdb_string: PDB format string
867
+ rotation_matrix: 3x3 rotation matrix
868
+ query_com: Center of mass of query structure (to translate FROM)
869
+ ref_com: Center of mass of reference structure (to translate TO), optional
870
+
871
+ Returns:
872
+ Transformed PDB string with aligned coordinates
873
+ """
874
+ lines = pdb_string.split('\n')
875
+ transformed_lines = []
876
+
877
+ # If ref_com not provided, just center at origin after rotation
878
+ if ref_com is None:
879
+ ref_com = np.array([0.0, 0.0, 0.0])
880
+
881
+ for line in lines:
882
+ if len(line) < 54:
883
+ transformed_lines.append(line)
884
+ continue
885
+
886
+ record = line[0:6].strip()
887
+ if record in ['ATOM', 'HETATM', 'HETAT']:
888
+ # Extract coordinates
889
+ try:
890
+ x = float(line[30:38].strip())
891
+ y = float(line[38:46].strip())
892
+ z = float(line[46:54].strip())
893
+
894
+ # Transform: (coord - query_com) @ rotation_matrix + ref_com
895
+ # This aligns query to reference coordinate system
896
+ coord = np.array([x, y, z])
897
+ centered = coord - query_com # Move query to origin
898
+ rotated = np.dot(centered, rotation_matrix) # Rotate
899
+ new_coord = rotated + ref_com # Move to reference position
900
+
901
+ # Write transformed line
902
+ new_line = (
903
+ line[:30] +
904
+ f"{new_coord[0]:8.3f}" +
905
+ f"{new_coord[1]:8.3f}" +
906
+ f"{new_coord[2]:8.3f}" +
907
+ line[54:]
908
+ )
909
+ transformed_lines.append(new_line)
910
+ except (ValueError, IndexError):
911
+ transformed_lines.append(line)
912
+ else:
913
+ transformed_lines.append(line)
914
+
915
+ return '\n'.join(transformed_lines)