Spaces:
Sleeping
Sleeping
| """ | |
| Multi-Structure 3D Visualization Module | |
| Based on working pairwise visualization code | |
| """ | |
| import numpy as np | |
| from rmsd_utils import ( | |
| parse_residue_atoms, | |
| get_backbone_sugar_coords_from_residue, | |
| get_base_coords_from_residue | |
| ) | |
| def extract_window_pdb(pdb_path, window_indices): | |
| """ | |
| Extract specific residues from a PDB file based on window indices. | |
| Uses the WORKING approach from the original code. | |
| """ | |
| with open(pdb_path) as f: | |
| lines = f.readlines() | |
| # Get all residue numbers from the file | |
| residues = parse_residue_atoms(pdb_path) | |
| if not residues: | |
| return ''.join(lines) | |
| residue_numbers = [res['resnum'] for res in residues] | |
| # Map window indices to actual residue numbers | |
| target_resnums = set() | |
| for idx in window_indices: | |
| if idx < len(residue_numbers): | |
| target_resnums.add(residue_numbers[idx]) | |
| if not target_resnums: | |
| return ''.join(lines) | |
| # Extract lines for these residues | |
| window_lines = [] | |
| for line in lines: | |
| if len(line) < 6: | |
| continue | |
| record = line[0:6].strip() | |
| if record in ['ATOM', 'HETATM', 'HETAT']: | |
| try: | |
| resnum_str = line[22:26].strip() | |
| if resnum_str: | |
| resnum = int(resnum_str) | |
| if resnum in target_resnums: | |
| window_lines.append(line) | |
| except (ValueError, IndexError): | |
| continue | |
| elif record in ['HEADER', 'TITLE', 'MODEL', 'ENDMDL']: | |
| window_lines.append(line) | |
| # Always add END record | |
| if window_lines and not any('END' in line for line in window_lines): | |
| window_lines.append('END\n') | |
| result = ''.join(window_lines) | |
| if not result or len(result) < 50: | |
| return ''.join(lines) | |
| return result | |
| def transform_pdb_string(pdb_string, rotation_matrix, query_com, ref_com): | |
| """ | |
| Apply rotation and translation to align query with reference. | |
| Uses the WORKING transformation from original code. | |
| CRITICAL: This is right multiplication (coord @ R), NOT left multiplication (R @ coord) | |
| Args: | |
| pdb_string: PDB format string | |
| rotation_matrix: 3x3 rotation matrix from RMSD calculation | |
| query_com: Center of mass of query structure (translate FROM) | |
| ref_com: Center of mass of reference structure (translate TO) | |
| Returns: | |
| Transformed PDB string with aligned coordinates | |
| """ | |
| lines = pdb_string.split('\n') | |
| transformed_lines = [] | |
| for line in lines: | |
| if len(line) < 54: | |
| transformed_lines.append(line) | |
| continue | |
| record = line[0:6].strip() | |
| if record in ['ATOM', 'HETATM', 'HETAT']: | |
| try: | |
| x = float(line[30:38].strip()) | |
| y = float(line[38:46].strip()) | |
| z = float(line[46:54].strip()) | |
| # Transform: (coord - query_com) @ rotation_matrix + ref_com | |
| # This is the WORKING approach from original code | |
| coord = np.array([x, y, z]) | |
| centered = coord - query_com # Move query to origin | |
| rotated = np.dot(centered, rotation_matrix) # RIGHT multiplication | |
| new_coord = rotated + ref_com # Move to reference position | |
| # Write transformed line | |
| new_line = ( | |
| line[:30] + | |
| f"{new_coord[0]:8.3f}" + | |
| f"{new_coord[1]:8.3f}" + | |
| f"{new_coord[2]:8.3f}" + | |
| line[54:] | |
| ) | |
| transformed_lines.append(new_line) | |
| except (ValueError, IndexError): | |
| transformed_lines.append(line) | |
| else: | |
| transformed_lines.append(line) | |
| return '\n'.join(transformed_lines) | |
| def create_pairwise_visualization(ref_path, query_path, ref_window, query_window, | |
| rotation_matrix, ref_com, query_com, rmsd, | |
| ref_name="Reference", query_name="Query"): | |
| """ | |
| Create interactive 3D visualization of two aligned structures (pairwise). | |
| Based on working original visualization with enhanced controls. | |
| Args: | |
| ref_path: Path to reference PDB file | |
| query_path: Path to query PDB file | |
| ref_window: List of residue indices for reference window | |
| query_window: List of residue indices for query window | |
| rotation_matrix: Rotation matrix from RMSD | |
| ref_com: Center of mass of reference | |
| query_com: Center of mass of query | |
| rmsd: RMSD value | |
| ref_name: Name of reference structure | |
| query_name: Name of query structure | |
| Returns: | |
| HTML string for py3Dmol visualization | |
| """ | |
| # Extract windows | |
| ref_pdb = extract_window_pdb(ref_path, ref_window) | |
| query_pdb = extract_window_pdb(query_path, query_window) | |
| # Transform query to align with reference | |
| transformed_query_pdb = transform_pdb_string( | |
| query_pdb, | |
| rotation_matrix, | |
| query_com, | |
| ref_com | |
| ) | |
| # Escape backticks | |
| ref_pdb_escaped = ref_pdb.replace('`', '\\`') | |
| transformed_query_pdb_escaped = transformed_query_pdb.replace('`', '\\`') | |
| # Create HTML with enhanced controls | |
| html = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script> | |
| <style> | |
| #container {{ | |
| width: 100%; | |
| height: 700px; | |
| position: relative; | |
| border: 1px solid #ddd; | |
| }} | |
| .control-panel {{ | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-family: Arial, sans-serif; | |
| font-size: 13px; | |
| z-index: 1000; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| max-width: 220px; | |
| }} | |
| .control-panel h4 {{ | |
| margin: 0 0 10px 0; | |
| font-size: 14px; | |
| color: #333; | |
| }} | |
| .control-section {{ | |
| margin-bottom: 12px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid #eee; | |
| }} | |
| .control-section:last-child {{ | |
| border-bottom: none; | |
| margin-bottom: 0; | |
| }} | |
| .control-section label {{ | |
| display: block; | |
| margin: 6px 0; | |
| cursor: pointer; | |
| }} | |
| .control-section input[type="checkbox"] {{ | |
| margin-right: 8px; | |
| }} | |
| .control-section select {{ | |
| width: 100%; | |
| padding: 4px; | |
| margin-top: 5px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| }} | |
| .legend {{ | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-family: Arial, sans-serif; | |
| font-size: 13px; | |
| z-index: 1000; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| .legend h4 {{ | |
| margin: 0 0 10px 0; | |
| font-size: 14px; | |
| color: #333; | |
| }} | |
| .legend-item {{ | |
| margin: 6px 0; | |
| display: flex; | |
| align-items: center; | |
| }} | |
| .color-box {{ | |
| width: 24px; | |
| height: 16px; | |
| margin-right: 10px; | |
| border: 1px solid #333; | |
| border-radius: 2px; | |
| }} | |
| .rmsd-info {{ | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| font-family: Arial, sans-serif; | |
| font-size: 13px; | |
| z-index: 1000; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| .section-title {{ | |
| font-weight: bold; | |
| color: #555; | |
| margin-bottom: 5px; | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| }} | |
| .download-btn {{ | |
| width: 100%; | |
| padding: 8px; | |
| background: #0066cc; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: bold; | |
| margin-top: 5px; | |
| }} | |
| .download-btn:hover {{ | |
| background: #0052a3; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="container"></div> | |
| <div class="legend"> | |
| <h4>🧬 Structures</h4> | |
| <div class="legend-item"> | |
| <div class="color-box" style="background: #4A90E2;"></div> | |
| <span>{ref_name}</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="color-box" style="background: #E94B3C;"></div> | |
| <span>{query_name} (Aligned)</span> | |
| </div> | |
| </div> | |
| <div class="rmsd-info"> | |
| <strong>RMSD: {rmsd:.3f} Å</strong> | |
| </div> | |
| <div class="control-panel"> | |
| <h4>⚙️ Display Options</h4> | |
| <div class="control-section"> | |
| <div class="section-title">Structures</div> | |
| <label> | |
| <input type="checkbox" id="showRef" checked onchange="updateDisplay()"> | |
| {ref_name} | |
| </label> | |
| <label> | |
| <input type="checkbox" id="showQuery" checked onchange="updateDisplay()"> | |
| {query_name} | |
| </label> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Style</div> | |
| <select id="styleMode" onchange="updateDisplay()"> | |
| <option value="sticks">Sticks</option> | |
| <option value="cartoon">Cartoon</option> | |
| <option value="spheres">Spheres</option> | |
| <option value="lines">Lines</option> | |
| </select> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Components</div> | |
| <label> | |
| <input type="checkbox" id="showBackbone" checked onchange="updateDisplay()"> | |
| Backbone/Sugar | |
| </label> | |
| <label> | |
| <input type="checkbox" id="showBases" checked onchange="updateDisplay()"> | |
| Bases | |
| </label> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Labels</div> | |
| <label> | |
| <input type="checkbox" id="showResidueLabels" onchange="updateDisplay()"> | |
| Residue Labels | |
| </label> | |
| <label> | |
| <input type="checkbox" id="showResidueNumbers" onchange="updateDisplay()"> | |
| Residue Numbers | |
| </label> | |
| <label> | |
| <input type="checkbox" id="showAtomNames" onchange="updateDisplay()"> | |
| Atom Names | |
| </label> | |
| <select id="atomLabelMode" onchange="updateDisplay()"> | |
| <option value="all">All Atoms</option> | |
| <option value="backbone">Backbone Only</option> | |
| <option value="sidechain">Base Only</option> | |
| </select> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Background</div> | |
| <select id="bgColor" onchange="updateBackground()"> | |
| <option value="white">White</option> | |
| <option value="black">Black</option> | |
| <option value="gray">Gray</option> | |
| </select> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Download</div> | |
| <button class="download-btn" onclick="downloadImage()">📸 Save as JPEG</button> | |
| </div> | |
| </div> | |
| <script> | |
| let viewer = $3Dmol.createViewer("container", {{backgroundColor: 'white'}}); | |
| const refPDB = `{ref_pdb_escaped}`; | |
| const queryPDB = `{transformed_query_pdb_escaped}`; | |
| const backboneAtoms = ['P', 'OP1', 'OP2', "O5'", "C5'", "C4'", "O4'", "C3'", "O3'", "C2'", "O2'", "C1'"]; | |
| function updateDisplay() {{ | |
| viewer.removeAllModels(); | |
| viewer.removeAllLabels(); | |
| const styleMode = document.getElementById('styleMode').value; | |
| const showBackbone = document.getElementById('showBackbone').checked; | |
| const showBases = document.getElementById('showBases').checked; | |
| const showResidueLabels = document.getElementById('showResidueLabels').checked; | |
| const showResidueNumbers = document.getElementById('showResidueNumbers').checked; | |
| const showAtomNames = document.getElementById('showAtomNames').checked; | |
| let modelIndex = 0; | |
| // Add reference if checked | |
| if (document.getElementById('showRef').checked) {{ | |
| viewer.addModel(refPDB, "pdb"); | |
| applyStyle(modelIndex, '#4A90E2', styleMode, showBackbone, showBases); | |
| if (showResidueLabels || showResidueNumbers) {{ | |
| addResidueLabels(modelIndex, '#4A90E2', showResidueLabels, showResidueNumbers); | |
| }} | |
| if (showAtomNames) {{ | |
| addAtomLabels(modelIndex, '#4A90E2'); | |
| }} | |
| modelIndex++; | |
| }} | |
| // Add query if checked | |
| if (document.getElementById('showQuery').checked) {{ | |
| viewer.addModel(queryPDB, "pdb"); | |
| applyStyle(modelIndex, '#E94B3C', styleMode, showBackbone, showBases); | |
| if (showResidueLabels || showResidueNumbers) {{ | |
| addResidueLabels(modelIndex, '#E94B3C', showResidueLabels, showResidueNumbers); | |
| }} | |
| if (showAtomNames) {{ | |
| addAtomLabels(modelIndex, '#E94B3C'); | |
| }} | |
| modelIndex++; | |
| }} | |
| viewer.zoomTo(); | |
| viewer.render(); | |
| }} | |
| function applyStyle(modelIndex, baseColor, styleMode, showBackbone, showBases) {{ | |
| const atoms = viewer.selectedAtoms({{model: modelIndex}}); | |
| atoms.forEach(atom => {{ | |
| const isBackbone = backboneAtoms.includes(atom.atom); | |
| // Skip if filtering out this type | |
| if (!showBackbone && isBackbone) return; | |
| if (!showBases && !isBackbone) return; | |
| if (styleMode === 'sticks') {{ | |
| viewer.setStyle({{model: modelIndex, serial: atom.serial}}, {{ | |
| stick: {{ | |
| color: baseColor, | |
| radius: 0.15 | |
| }} | |
| }}); | |
| }} else if (styleMode === 'cartoon') {{ | |
| viewer.setStyle({{model: modelIndex}}, {{ | |
| cartoon: {{ | |
| color: baseColor, | |
| opacity: 0.8 | |
| }} | |
| }}); | |
| }} else if (styleMode === 'spheres') {{ | |
| viewer.setStyle({{model: modelIndex, serial: atom.serial}}, {{ | |
| sphere: {{ | |
| color: baseColor, | |
| radius: 0.25 | |
| }} | |
| }}); | |
| }} else if (styleMode === 'lines') {{ | |
| viewer.setStyle({{model: modelIndex, serial: atom.serial}}, {{ | |
| line: {{ | |
| color: baseColor | |
| }} | |
| }}); | |
| }} | |
| }}); | |
| }} | |
| function addResidueLabels(modelIndex, color, showLabels, showNumbers) {{ | |
| const atoms = viewer.selectedAtoms({{model: modelIndex}}); | |
| const residues = {{}}; | |
| atoms.forEach(atom => {{ | |
| const key = atom.chain + '_' + atom.resi; | |
| if (!residues[key]) {{ | |
| residues[key] = atom; | |
| }} | |
| }}); | |
| Object.values(residues).forEach(atom => {{ | |
| let labelText = ''; | |
| if (showLabels && showNumbers) {{ | |
| labelText = atom.resn + atom.resi; | |
| }} else if (showLabels) {{ | |
| labelText = atom.resn; | |
| }} else if (showNumbers) {{ | |
| labelText = atom.resi.toString(); | |
| }} | |
| if (labelText) {{ | |
| viewer.addLabel(labelText, {{ | |
| position: atom, | |
| backgroundColor: color, | |
| backgroundOpacity: 0.7, | |
| fontColor: 'white', | |
| fontSize: 11, | |
| fontWeight: 'bold', | |
| showBackground: true, | |
| borderRadius: 3 | |
| }}); | |
| }} | |
| }}); | |
| }} | |
| function addAtomLabels(modelIndex, color) {{ | |
| const atomLabelMode = document.getElementById('atomLabelMode').value; | |
| const atoms = viewer.selectedAtoms({{model: modelIndex}}); | |
| let filteredAtoms = atoms; | |
| if (atomLabelMode === 'backbone') {{ | |
| filteredAtoms = atoms.filter(atom => backboneAtoms.includes(atom.atom)); | |
| }} else if (atomLabelMode === 'sidechain') {{ | |
| filteredAtoms = atoms.filter(atom => !backboneAtoms.includes(atom.atom)); | |
| }} | |
| filteredAtoms.forEach(atom => {{ | |
| viewer.addLabel(atom.atom, {{ | |
| position: atom, | |
| backgroundColor: color, | |
| backgroundOpacity: 0.6, | |
| fontColor: 'white', | |
| fontSize: 9, | |
| fontWeight: 'normal', | |
| showBackground: true, | |
| borderRadius: 2 | |
| }}); | |
| }}); | |
| }} | |
| function updateBackground() {{ | |
| const bgColor = document.getElementById('bgColor').value; | |
| viewer.setBackgroundColor(bgColor); | |
| viewer.render(); | |
| }} | |
| // Initialize | |
| updateDisplay(); | |
| // Download functionality | |
| window.viewer = viewer; | |
| function downloadImage() {{ | |
| try {{ | |
| const imgData = viewer.pngURI(); | |
| const img = new Image(); | |
| img.onload = function() {{ | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| canvas.toBlob(function(blob) {{ | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'rna_pairwise_comparison.jpg'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }}, 'image/jpeg', 0.95); | |
| }}; | |
| img.src = imgData; | |
| }} catch(e) {{ | |
| alert('Error: ' + e.message); | |
| }} | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html | |
| def create_multistructure_visualization(ref_path, ref_window, ref_com, query_data_list, ref_name="Reference"): | |
| """ | |
| Create interactive 3D visualization with multiple structures aligned to reference. | |
| Args: | |
| ref_path: Path to reference PDB file | |
| ref_window: List of residue indices for reference window | |
| ref_com: Center of mass of reference window | |
| query_data_list: List of dicts with keys: | |
| - name: Structure name | |
| - path: Path to PDB file | |
| - window: List of residue indices | |
| - rotation: Rotation matrix from RMSD | |
| - query_com: Center of mass | |
| - rmsd: RMSD value | |
| - sequence: Sequence string | |
| ref_name: Name of reference structure | |
| Returns: | |
| HTML string for py3Dmol visualization | |
| """ | |
| # Extract reference window | |
| ref_pdb = extract_window_pdb(ref_path, ref_window) | |
| # Color palette | |
| colors = [ | |
| '#E94B3C', # Red | |
| '#50C878', # Green | |
| '#FF9500', # Orange | |
| '#9B59B6', # Purple | |
| '#00CED1', # Cyan | |
| '#FF1493', # Pink | |
| '#FFD700', # Gold | |
| '#8B4513', # Brown | |
| '#708090' # Slate Gray | |
| ] | |
| # Transform all query structures | |
| transformed_queries = [] | |
| for idx, query_data in enumerate(query_data_list): | |
| # Extract query window | |
| query_pdb = extract_window_pdb(query_data['path'], query_data['window']) | |
| # Transform query to align with reference | |
| transformed_pdb = transform_pdb_string( | |
| query_pdb, | |
| query_data['rotation'], | |
| query_data['query_com'], | |
| ref_com | |
| ) | |
| color = colors[idx % len(colors)] | |
| transformed_queries.append({ | |
| 'pdb': transformed_pdb, | |
| 'name': query_data['name'], | |
| 'color': color, | |
| 'rmsd': query_data['rmsd'], | |
| 'sequence': query_data['sequence'] | |
| }) | |
| # Build JavaScript for model data | |
| models_js = f"const refPDB = `{ref_pdb}`;\n" | |
| for idx, tq in enumerate(transformed_queries): | |
| # Escape backticks | |
| pdb_escaped = tq['pdb'].replace('`', '\\`') | |
| models_js += f"const queryPDB{idx} = `{pdb_escaped}`;\n" | |
| # Build controls HTML | |
| controls_html = f'<label><input type="checkbox" id="showRef" checked onchange="updateDisplay()"> {ref_name} (Blue)</label>\n' | |
| for idx, tq in enumerate(transformed_queries): | |
| controls_html += f'<label><input type="checkbox" id="showQuery{idx}" checked onchange="updateDisplay()"> {tq["name"]} ({tq["color"]})</label>\n' | |
| # Build legend HTML | |
| legend_html = f'<div class="legend-item"><div class="color-box" style="background: #4A90E2;"></div><span>{ref_name}</span></div>\n' | |
| for tq in transformed_queries: | |
| legend_html += f'<div class="legend-item"><div class="color-box" style="background: {tq["color"]};"></div><span>{tq["name"]} ({tq["rmsd"]:.3f} Å)</span></div>\n' | |
| # Build update function | |
| update_js = ''' | |
| function updateDisplay() { | |
| viewer.removeAllModels(); | |
| const styleMode = document.getElementById('styleMode').value; | |
| const showLabels = document.getElementById('showLabels').checked; | |
| let modelIndex = 0; | |
| // Add reference if checked | |
| if (document.getElementById('showRef').checked) { | |
| viewer.addModel(refPDB, "pdb"); | |
| applyStyle(modelIndex, '#4A90E2', styleMode); | |
| if (showLabels) { | |
| addLabels(modelIndex, '#4A90E2'); | |
| } | |
| modelIndex++; | |
| } | |
| ''' | |
| # Add query structures | |
| for idx in range(len(transformed_queries)): | |
| color = transformed_queries[idx]['color'] | |
| update_js += f''' | |
| if (document.getElementById('showQuery{idx}').checked) {{ | |
| viewer.addModel(queryPDB{idx}, "pdb"); | |
| applyStyle(modelIndex, '{color}', styleMode); | |
| if (showLabels) {{ | |
| addLabels(modelIndex, '{color}'); | |
| }} | |
| modelIndex++; | |
| }} | |
| ''' | |
| update_js += ''' | |
| viewer.zoomTo(); | |
| viewer.render(); | |
| } | |
| function applyStyle(modelIndex, color, styleMode) { | |
| if (styleMode === 'sticks') { | |
| viewer.setStyle({model: modelIndex}, {stick: {color: color, radius: 0.15}}); | |
| } else if (styleMode === 'cartoon') { | |
| viewer.setStyle({model: modelIndex}, {cartoon: {color: color, opacity: 0.8}}); | |
| } else if (styleMode === 'spheres') { | |
| viewer.setStyle({model: modelIndex}, {sphere: {color: color, radius: 0.3}}); | |
| } else if (styleMode === 'lines') { | |
| viewer.setStyle({model: modelIndex}, {line: {color: color}}); | |
| } | |
| } | |
| function addLabels(modelIndex, color) { | |
| const atoms = viewer.selectedAtoms({model: modelIndex}); | |
| const residues = {}; | |
| atoms.forEach(atom => { | |
| const key = atom.chain + '_' + atom.resi; | |
| if (!residues[key]) { | |
| residues[key] = atom; | |
| } | |
| }); | |
| Object.values(residues).forEach(atom => { | |
| viewer.addLabel(atom.resn + atom.resi, { | |
| position: atom, | |
| backgroundColor: color, | |
| backgroundOpacity: 0.7, | |
| fontColor: 'white', | |
| fontSize: 10, | |
| fontWeight: 'bold', | |
| showBackground: true | |
| }); | |
| }); | |
| } | |
| ''' | |
| # Create HTML | |
| html = f''' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script> | |
| <style> | |
| #container {{ | |
| width: 100%; | |
| height: 700px; | |
| position: relative; | |
| border: 1px solid #ddd; | |
| }} | |
| .control-panel {{ | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-family: Arial, sans-serif; | |
| font-size: 13px; | |
| z-index: 1000; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| max-width: 250px; | |
| max-height: 600px; | |
| overflow-y: auto; | |
| }} | |
| .control-panel h4 {{ | |
| margin: 0 0 10px 0; | |
| font-size: 14px; | |
| color: #333; | |
| }} | |
| .control-section {{ | |
| margin-bottom: 12px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid #eee; | |
| }} | |
| .control-section:last-child {{ | |
| border-bottom: none; | |
| margin-bottom: 0; | |
| }} | |
| .control-section label {{ | |
| display: block; | |
| margin: 6px 0; | |
| cursor: pointer; | |
| }} | |
| .control-section input[type="checkbox"] {{ | |
| margin-right: 8px; | |
| }} | |
| .control-section select {{ | |
| width: 100%; | |
| padding: 4px; | |
| margin-top: 5px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| }} | |
| .legend {{ | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background: rgba(255, 255, 255, 0.95); | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-family: Arial, sans-serif; | |
| font-size: 13px; | |
| z-index: 1000; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| .legend h4 {{ | |
| margin: 0 0 10px 0; | |
| font-size: 14px; | |
| color: #333; | |
| }} | |
| .legend-item {{ | |
| margin: 6px 0; | |
| display: flex; | |
| align-items: center; | |
| }} | |
| .color-box {{ | |
| width: 24px; | |
| height: 16px; | |
| margin-right: 10px; | |
| border: 1px solid #333; | |
| border-radius: 2px; | |
| }} | |
| .section-title {{ | |
| font-weight: bold; | |
| color: #555; | |
| margin-bottom: 5px; | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| }} | |
| .download-btn {{ | |
| width: 100%; | |
| padding: 8px; | |
| background: #0066cc; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: bold; | |
| margin-top: 5px; | |
| }} | |
| .download-btn:hover {{ | |
| background: #0052a3; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="container"></div> | |
| <div class="legend"> | |
| <h4>🧬 Structures</h4> | |
| {legend_html} | |
| </div> | |
| <div class="control-panel"> | |
| <h4>⚙️ Display Options</h4> | |
| <div class="control-section"> | |
| <div class="section-title">Structures</div> | |
| {controls_html} | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Style</div> | |
| <select id="styleMode" onchange="updateDisplay()"> | |
| <option value="sticks">Sticks</option> | |
| <option value="cartoon">Cartoon</option> | |
| <option value="spheres">Spheres</option> | |
| <option value="lines">Lines</option> | |
| </select> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Labels</div> | |
| <label> | |
| <input type="checkbox" id="showLabels" onchange="updateDisplay()"> | |
| Show Residue Labels | |
| </label> | |
| </div> | |
| <div class="control-section"> | |
| <div class="section-title">Download</div> | |
| <button class="download-btn" onclick="downloadImage()">📸 Save as JPEG</button> | |
| </div> | |
| </div> | |
| <script> | |
| let viewer = $3Dmol.createViewer("container", {{backgroundColor: 'white'}}); | |
| {models_js} | |
| {update_js} | |
| // Initialize | |
| updateDisplay(); | |
| // Download functionality | |
| window.viewer = viewer; // Make viewer globally accessible | |
| function downloadImage() {{ | |
| try {{ | |
| // Use 3Dmol.js built-in PNG export | |
| const imgData = viewer.pngURI(); | |
| // Convert PNG to JPEG with white background | |
| const img = new Image(); | |
| img.onload = function() {{ | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| const ctx = canvas.getContext('2d'); | |
| // White background for JPEG | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| // Convert to JPEG and download | |
| canvas.toBlob(function(blob) {{ | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'rna_multi_structure_comparison.jpg'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }}, 'image/jpeg', 0.95); | |
| }}; | |
| img.src = imgData; | |
| }} catch(e) {{ | |
| alert('Error capturing image: ' + e.message); | |
| console.error(e); | |
| }} | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| return html | |