From: gmungoc Date: Fri, 29 May 2020 15:33:17 +0000 (+0100) Subject: Merge branch 'feature/JAL-3551Pymol' into develop X-Git-Tag: Develop-2_11_2_0-d20201215~24^2~56 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=c38d37887ae51d63942ac4321f6769308188ed6b;hp=b04cbef0dac19e60f693e64fdb5855ebdc675f80;p=jalview.git Merge branch 'feature/JAL-3551Pymol' into develop --- diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index 1762a06..ca6ae63 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -268,11 +268,11 @@ label.autoadd_secstr = Add secondary structure annotation to alignment label.autoadd_temp = Add Temperature Factor annotation to alignment label.structure_viewer = Default structure viewer label.double_click_to_browse = Double-click to browse for file -label.chimera_path = Path to Chimera program -label.chimera_path_tip = Jalview will first try any path entered here, else standard installation locations.
Double-click to browse for file. -label.invalid_chimera_path = Chimera path not found or not executable -label.chimera_missing = Chimera structure viewer not found.
Please enter the path to Chimera (if installed),
or download and install UCSF Chimera. -label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure +label.viewer_path = Path to {0} program +label.viewer_path_tip = Jalview will first try any path entered here, else standard installation locations.
Double-click to browse for file. +label.invalid_viewer_path = Path not found or not executable +label.viewer_missing = Structure viewer not found.
Please enter the path to the executable (if installed),
or download and install the program. +label.open_viewer_failed = Error opening {0} - is it installed?\nCheck path in Preferences, Structure label.min_colour = Minimum Colour label.max_colour = Maximum Colour label.no_colour = No Colour @@ -502,10 +502,9 @@ label.insert_gaps = Insert {0} gaps label.delete_gap = Delete 1 gap label.delete_gaps = Delete {0} gaps label.sequence_details = Sequence Details -label.jmol_help = Jmol Help -label.chimera_help = Chimera Help +label.viewer_help = {0} Help label.close_viewer = Close Viewer -label.confirm_close_chimera = This will close Jalview''s connection to {0}.
Do you want to close the Chimera window as well? +label.confirm_close_viewer = This will close Jalview''s connection to {0}.
Do you want to close the {1} window as well? label.all = All label.sort_by = Sort alignment by label.sort_by_score = Sort by Score @@ -624,7 +623,6 @@ label.editing = Editing label.web_services = Web Services label.right_click_to_edit_currently_selected_parameter = Right click to edit currently selected parameter. label.let_jmol_manage_structure_colours = Let Jmol manage structure colours -label.let_chimera_manage_structure_colours = Let Chimera manage structure colours label.fetch_chimera_attributes = Fetch Chimera attributes label.fetch_chimera_attributes_tip = Copy Chimera attribute to Jalview feature label.marks_leaves_tree_not_associated_with_sequence = Marks leaves of tree not associated with a sequence @@ -711,14 +709,13 @@ label.associate_nodes_with = Associate Nodes With label.link_name = Link Name label.pdb_file = PDB file label.colour_with_jmol = Colour with Jmol -label.colour_with_chimera = Colour with Chimera +label.let_viewer_manage_structure_colours = Let viewer manage structure colours +label.colour_with_viewer = Colour in structure viewer label.superpose_structures = Superpose Structures error.superposition_failed = Superposition failed: {0} label.insufficient_residues = Not enough aligned residues ({0}) to perform superposition -label.jmol = Jmol -label.chimera = Chimera -label.create_chimera_attributes = Write Jalview features -label.create_chimera_attributes_tip = Set Chimera residue attributes for visible features +label.create_viewer_attributes = Write Jalview features +label.create_viewer_attributes_tip = Set structure residue attributes for Jalview features label.attributes_set = {0} attribute values set on Chimera label.sort_alignment_by_tree = Sort Alignment By Tree label.mark_unlinked_leaves = Mark Unlinked Leaves @@ -1124,7 +1121,7 @@ status.fetching_db_refs = Fetching db refs status.loading_cached_pdb_entries = Loading Cached PDB Entries status.searching_for_pdb_structures = Searching for PDB Structures status.opening_file_for = opening file for -status.colouring_chimera = Colouring Chimera +status.colouring_structures = Colouring structures label.font_doesnt_have_letters_defined = Font doesn't have letters defined\nso cannot be used\nwith alignment data label.font_too_small = Font size is too small label.error_loading_file_params = Error loading file {0} diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index d3c3355..624e619 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -465,7 +465,7 @@ label.insert_gaps = Insertar {0} huecos label.delete_gap = Borrar 1 hueco label.delete_gaps = Borrar {0} huecos label.sequence_details = Detalles de la secuencia -label.jmol_help = Ayuda de Jmol +label.viewer_help = Ayuda sobre {0} # Todos/Todas is gender-sensitive, but currently only used for feminine (cadena / anotación)! label.all = Todas label.sort_by = Ordenar por @@ -653,7 +653,6 @@ label.associate_nodes_with = Asociar nodos con label.link_name = Nombre del enalce label.pdb_file = Fichero PDB label.colour_with_jmol = Colorear con Jmol -label.jmol = Jmol label.sort_alignment_by_tree = Ordenar alineamiento por árbol label.mark_unlinked_leaves = Marcar las hojas como no enlazadas label.associate_leaves_with = Asociar hojas con @@ -1118,7 +1117,6 @@ label.autoadd_secstr=A action.annotations=Anotaciones label.nuc_alignment_colour=Color del Alineamiento Nucleotídico label.copy_format_from=Copiar formato de -label.chimera=Chimera label.create_chimera_attributes = Escribir características de Jalview label.create_chimera_attributes_tip = Establecer atributos en Chimera para características visibles label.attributes_set = {0} valores de atributos establecidos en Chimera @@ -1140,7 +1138,7 @@ label.invalid_search=Texto de b action.export_annotations=Exportar Anotaciones action.set_as_reference=Marcar como Referencia action.unmark_as_reference=Desmarcar como Referencia -label.chimera_failed=Error al abrir Chimera - está instalado?\nCompruebe ruta en Preferencias, Estructura +label.open_viewer_failed=Error al abrir {0} - está instalado?\nCompruebe ruta en Preferencias, Estructura label.find=Buscar label.select_pdb_file=Seleccionar Fichero PDB label.structures_filter=Filtro de Estructuras @@ -1153,7 +1151,6 @@ action.export_features=Exportar Caracter error.invalid_regex=Expresión regular inválida label.autoadd_temp=Añadir anotación factor de temperatura al alineamiento label.double_click_to_browse = Haga doble clic para buscar fichero -label.chimera_path_tip=Jalview intentará primero las rutas introducidas aquí, Y si no las rutas usuales de instalación label.structure_chooser=Selector de Estructuras label.structure_chooser_manual_association=Selector de Estructuras - asociación manual label.threshold_filter=Filtro de Umbral @@ -1161,11 +1158,11 @@ label.add_reference_annotations=A label.hide_insertions=Ocultar Inserciones info.change_threshold_mode_to_enable=Cambiar Modo de Umbral para Habilitar label.separate_multiple_query_values=Introducir uno o mas {0}s separados por punto y coma ";" -label.let_chimera_manage_structure_colours=Deja que Chimera maneje colores de estructuras label.fetch_chimera_attributes = Buscar atributos desde Chimera label.fetch_chimera_attributes_tip = Copiar atributo de Chimera a característica de Jalview label.view_rna_structure=Estructura 2D VARNA -label.colour_with_chimera=Colorear con Chimera +label.colour_with_viewer = Colorear con visualizador de estructuras +label.let_viewer_manage_structure_colours = Deja que el visualizador maneje los colores de estructuras label.superpose_structures = Superponer estructuras error.superposition_failed = Superposición fallido: {0} label.insufficient_residues = Residuos alineados ({0}) insuficentes para superponer @@ -1176,7 +1173,6 @@ tooltip.aacon_settings=Cambiar ajustes para c label.mark_as_representative=Marcar como representativa label.include_description=Incluir Descripción label.for=para -label.invalid_chimera_path=Ruta de Chimera no encontrada o no ejecutable info.search_in_annotation_label=Buscar en etiqueta de {0} info.search_in_annotation_description=Buscar en descripción de {0} label.select_many_views=Seleccionar múltiples vistas @@ -1189,20 +1185,22 @@ label.protein=Prote warn.oneseq_msainput_selection=La selección actual sólo contiene una única secuencia. ¿Quieres enviar todas las secuencias para la alineación en su lugar? label.use_rnaview=Usar RNAView para estructura secondaria label.search_all=Introducir uno o más valores de búsqueda separados por punto y coma ";" (Nota: buscará en toda la base de datos PDB) -label.confirm_close_chimera=Cerrará la conexión de Jalview a {0}.
¿Quieres cerrar la ventana Chimera también? +label.confirm_close_viewer=Cerrará la conexión de Jalview a {0}.
¿Quieres cerrar la ventana {1} también? tooltip.rnalifold_calculations=Se calcularán predicciones de estructura secondaria de RNA para el alineaminento, y se actualizarán si se efectuan cambios tooltip.rnalifold_settings=Modificar la configuración de la predicción RNAAlifold. Úselo para ocultar o mostrar resultados del cálculo de RNA, o cambiar parámetros de el plegado de RNA. label.show_selected_annotations=Mostrar anotaciones seleccionadas -status.colouring_chimera=Coloreando Chimera +status.colouring_structures=Coloreando estructuras label.configure_displayed_columns=Configurar Columnas Mostradas label.aacon_calculations=cálculos AACon label.pdb_web-service_error=Error de servicio web PDB exception.unable_to_detect_internet_connection=Jalview no puede detectar una conexión a Internet -label.chimera_path=Ruta de acceso a Chimera +label.viewer_path=Ruta de acceso a {0} +label.viewer_path_tip=Jalview intentará primero las rutas introducidas aquí, Y si no las rutas usuales de instalación +label.invalid_viewer_path=Ruta no encontrada o no ejecutable +label.viewer_missing=Visualizador de estructura no encontrado.
Por favor, introduzca la ruta de la ejecutable,
o descargar e instalar el programa. warn.delete_all=Borrar todas las secuencias cerrará la ventana del alineamiento.
Confirmar o Cancelar. label.select_all=Seleccionar Todos label.alpha_helix=Hélice Alfa -label.chimera_help=Ayuda para Chimera label.find_tip=Buscar alineamiento, selección o IDs de secuencia para una subsecuencia (sin huecos) label.structure_viewer=Visualizador por defecto label.embbed_biojson=Incrustar BioJSON al exportar HTML @@ -1214,7 +1212,6 @@ label.aacon_settings=Cambiar Ajustes AACon... tooltip.aacon_calculations=Actualizar cálculos AACon automáticamente. info.select_filter_option=Escoger Opción de Filtro / Entrada Manual info.invalid_msa_input_mininfo=Necesita por lo menos dos secuencias con al menos 3 residuos cada una, sin regiones ocultas entre ellas. -label.chimera_missing=Visualizador de estructura Chimera no encontrado.
Por favor, introduzca la ruta de Chimera,
o descargar e instalar la UCSF Chimera. exception.fts_server_unreachable=Jalview no puede conectar con el servidor {0}. \nPor favor asegúrese de que está conectado a Internet y vuelva a intentarlo. exception.outofmemory_loading_mmcif_file=Sin memoria al cargar el fichero mmCIF label.hide_columns_not_containing=Ocultar las columnas que no contengan diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java index a910a5a..31f5dc8 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java @@ -32,8 +32,6 @@ */ package ext.edu.ucsf.rbvi.strucviz2; -import jalview.ws.HttpClientUtils; - import java.awt.Color; import java.io.BufferedReader; import java.io.File; @@ -54,6 +52,7 @@ import org.slf4j.LoggerFactory; import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads; +import jalview.ws.HttpClientUtils; /** * This object maintains the Chimera communication information. @@ -254,7 +253,7 @@ public class ChimeraManager for (ChimeraModel chimeraModel : modelList) { // get model color - Color modelColor = getModelColor(chimeraModel); + Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel); if (modelColor != null) { chimeraModel.setModelColor(modelColor); @@ -265,7 +264,7 @@ public class ChimeraManager // chimeraSend("repr stick "+newModel.toSpec()); // Create the information we need for the navigator - if (type != ModelType.SMILES) + if (type != ModelType.SMILES && !isChimeraX()) { addResidues(chimeraModel); } @@ -334,7 +333,11 @@ public class ChimeraManager public void stopListening() { - sendChimeraCommand("listen stop models ; listen stop selection ", false); + // TODO send this command when viewer connection is closed in Jalview + String command = isChimeraX + ? "info notify stop models jalview; info notify stop selection jalview" + : "listen stop models ; listen stop selection "; + sendChimeraCommand(command, false); } /** @@ -344,9 +347,23 @@ public class ChimeraManager */ public void startListening(String uri) { - sendChimeraCommand("listen start models url " + uri - + ";listen start select prefix SelectionChanged url " + uri, - false); + /* + * listen for model changes + */ + String command = isChimeraX + ? ("info notify start models prefix ModelChanged jalview url " + + uri) + : ("listen start models url " + uri); + sendChimeraCommand(command, false); + + /* + * listen for selection changes + */ + command = isChimeraX + ? ("info notify start selection jalview prefix SelectionChanged url " + + uri) + : ("listen start select prefix SelectionChanged url " + uri); + sendChimeraCommand(command, false); } /** @@ -420,19 +437,26 @@ public class ChimeraManager public List getSelectedResidueSpecs() { List selectedResidues = new ArrayList<>(); - List chimeraReply = sendChimeraCommand( - "list selection level residue", true); + + // in fact 'listinfo' (undocumented) works in ChimeraX + String command = (isChimeraX + ? "info" + : "list") + " selection level residue"; + List chimeraReply = sendChimeraCommand(command, true); if (chimeraReply != null) { /* - * expect 0, 1 or more lines of the format + * expect 0, 1 or more lines of the format either + * Chimera: * residue id #0:43.A type GLY - * where we are only interested in the atomspec #0.43.A + * ChimeraX: + * residue id /A:89 name THR index 88 + * We are only interested in the atomspec (third token of the reply) */ for (String inputLine : chimeraReply) { String[] inputLineParts = inputLine.split("\\s+"); - if (inputLineParts.length == 5) + if (inputLineParts.length >= 5) { selectedResidues.add(inputLineParts[2]); } @@ -473,14 +497,21 @@ public class ChimeraManager public List getModelList() { List modelList = new ArrayList<>(); - List list = sendChimeraCommand("list models type molecule", - true); + String command = "list models type " + + (isChimeraX ? "AtomicStructure" : "molecule"); + List list = sendChimeraCommand(command, true); if (list != null) { for (String modelLine : list) { - ChimeraModel chimeraModel = new ChimeraModel(modelLine); - modelList.add(chimeraModel); + try + { + ChimeraModel chimeraModel = new ChimeraModel(modelLine); + modelList.add(chimeraModel); + } catch (NullPointerException e) + { + // hack for now + } } } return modelList; @@ -555,6 +586,7 @@ public class ChimeraManager { // ensure symbolic links are resolved chimeraPath = Paths.get(chimeraPath).toRealPath().toString(); + isChimeraX = chimeraPath.toLowerCase().contains("chimerax"); File path = new File(chimeraPath); // uncomment the next line to simulate Chimera not installed // path = new File(chimeraPath + "x"); @@ -567,8 +599,16 @@ public class ChimeraManager args.add(chimeraPath); // shows Chimera output window but suppresses REST responses: // args.add("--debug"); - args.add("--start"); - args.add("RESTServer"); + if (isChimeraX()) + { + args.add("--cmd"); + args.add("remote rest start"); + } + else + { + args.add("--start"); + args.add("RESTServer"); + } ProcessBuilder pb = new ProcessBuilder(args); chimera = pb.start(); error = ""; @@ -616,15 +656,23 @@ public class ChimeraManager { responses.append("\n" + response); // expect: REST server on host 127.0.0.1 port port_number + // ChimeraX is the same except "REST server started on host..." if (response.startsWith("REST server")) { String[] tokens = response.split(" "); - if (tokens.length == 7 && "port".equals(tokens[5])) + for (int i = 0; i < tokens.length - 1; i++) { - port = Integer.parseInt(tokens[6]); - break; + if ("port".equals(tokens[i])) + { + port = Integer.parseInt(tokens[i + 1]); + break; + } } } + if (port > 0) + { + break; // hack for hanging readLine() + } response = lineReader.readLine(); } } catch (Exception e) @@ -703,7 +751,8 @@ public class ChimeraManager public List getAttrList() { List attributes = new ArrayList<>(); - final List reply = sendChimeraCommand("list resattr", true); + String command = (isChimeraX ? "info " : "list ") + "resattr"; + final List reply = sendChimeraCommand(command, true); if (reply != null) { for (String inputLine : reply) @@ -762,6 +811,8 @@ public class ChimeraManager private volatile boolean busy = false; + private boolean isChimeraX; + /** * Send a command to Chimera. * @@ -775,7 +826,7 @@ public class ChimeraManager */ public List sendChimeraCommand(String command, boolean reply) { - // System.out.println("chimeradebug>> " + command); + System.out.println("chimeradebug>> " + command); if (!isChimeraLaunched() || command == null || "".equals(command.trim())) { @@ -822,14 +873,23 @@ public class ChimeraManager { String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run"; List commands = new ArrayList<>(1); + String method = isChimeraX() ? "GET" : "POST"; + if ("GET".equals(method)) + { + command = command.replace(" ", "+").replace("#", "%23") + .replace("|", "%7C").replace(";", "%3B"); + } commands.add(new BasicNameValuePair("command", command)); List reply = new ArrayList<>(); BufferedReader response = null; try { - response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS, - REST_REPLY_TIMEOUT_MS); + response = "GET".equals(method) + ? HttpClientUtils.doHttpGet(restUrl, commands, + CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS) + : HttpClientUtils.doHttpUrlPost(restUrl, commands, + CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS); String line = ""; while ((line = response.readLine()) != null) { @@ -901,4 +961,14 @@ public class ChimeraManager { return chimera; } + + public boolean isChimeraX() + { + return isChimeraX; + } + + public void setChimeraX(boolean b) + { + isChimeraX = b; + } } diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java index 22c9098..5cf8a73 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java @@ -32,9 +32,6 @@ */ package ext.edu.ucsf.rbvi.strucviz2; -import jalview.bin.Cache; -import jalview.gui.Preferences; - import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -49,6 +46,9 @@ import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jalview.bin.Cache; +import jalview.gui.Preferences; + /** * This object maintains the relationship between Chimera objects and Cytoscape * objects. @@ -56,6 +56,27 @@ import org.slf4j.LoggerFactory; public class StructureManager { + /* + * Version numbers to build Windows installation paths for + * Chimera https://www.cgl.ucsf.edu/chimera/download.html + * ChimeraX http://www.rbvi.ucsf.edu/chimerax/download.html#release + * https://www.rbvi.ucsf.edu/trac/ChimeraX/wiki/ChangeLog + * These are a fallback for Jalview users who don't save path in Preferences; + * these will need to be updated as new versions are released; + * deliberately not 'final' (so modifiable using Groovy). + * + * May 2020: 1.14 is Chimera latest, anticipating a few more... + * 0.93 is ChimeraX latest, 1.0 expected soon + */ + private static String[] CHIMERA_VERSIONS = new String[] { "1.16.2", + "1.16.1", "1.16", + "1.15.2", "1.15.1", "1.15", "1.14.2", "1.14.1", "1.14", + "1.13.1", "1.13", "1.12.2", "1.12.1", "1.12", "1.11.2", + "1.11.2", "1.11.1", "1.11" }; + + private static String[] CHIMERAX_VERSIONS = new String[] { "1.0", "0.93", + "0.92", "0.91", "0.9" }; + static final String[] defaultStructureKeys = { "Structure", "pdb", "pdbFileName", "PDB ID", "structure", "biopax.xref.PDB", "pdb_ids", "ModelName", "ModelNumber" }; @@ -896,7 +917,7 @@ public class StructureManager StructureSettings defaultSettings = null; // TODO: [Optional] Change priority of Chimera paths - public static List getChimeraPaths() + public static List getChimeraPaths(boolean isChimeraX) { List pathList = new ArrayList<>(); @@ -916,22 +937,35 @@ public class StructureManager // } /* - * Jalview addition: check if path set in user preferences. + * Jalview addition: check if path set in user preferences */ - String userPath = Cache.getDefault(Preferences.CHIMERA_PATH, null); + String userPath = Cache + .getDefault(isChimeraX ? Preferences.CHIMERAX_PATH + : Preferences.CHIMERA_PATH, null); if (userPath != null) { - pathList.add(0, userPath); + pathList.add(userPath); } + /* + * paths are based on getChimeraPaths() in + * Chimera: + * https://github.com/RBVI/structureViz2/blob/master/src/main/java/edu/ucsf/rbvi/structureViz2/internal/model/StructureManager.java + * ChimeraX: + * https://github.com/RBVI/structureVizX/blob/master/src/main/java/edu/ucsf/rbvi/structureVizX/internal/model/StructureManager.java + */ + String chimera = isChimeraX ? "ChimeraX" : "Chimera"; + String chimeraExe = isChimeraX ? "ChimeraX" : "chimera"; + // Add default installation paths String os = System.getProperty("os.name"); if (os.startsWith("Linux")) { - pathList.add("/usr/local/chimera/bin/chimera"); - pathList.add("/usr/local/bin/chimera"); - pathList.add("/usr/bin/chimera"); - pathList.add(System.getProperty("user.home") + "/opt/bin/chimera"); + // todo should this be /chimeraX/ for ChimeraX? not in structureVizX code + pathList.add("/usr/local/chimera/bin/" + chimeraExe); + pathList.add("/usr/local/bin/" + chimeraExe); + pathList.add("/usr/bin/" + chimeraExe); + pathList.add(System.getProperty("user.home") + "/opt/bin/" + chimeraExe); } else if (os.startsWith("Windows")) { @@ -939,18 +973,22 @@ public class StructureManager "C:\\Program Files", "\\Program Files (x86)", "C:\\Program Files (x86)" }) { - for (String version : new String[] { "1.11", "1.11.1", "1.11.2", - "1.12", "1.12.1", "1.12.2", "1.13" }) + String[] candidates = isChimeraX ? CHIMERAX_VERSIONS + : CHIMERA_VERSIONS; + for (String version : candidates) { - pathList.add(root + "\\Chimera " + version + "\\bin\\chimera"); - pathList.add( - root + "\\Chimera " + version + "\\bin\\chimera.exe"); + // TODO original code doesn't include version in path; which is right? + String path = String.format("%s\\%s %s\\bin\\%s", root, chimera, + version, chimeraExe); + pathList.add(path); + pathList.add(path + ".exe"); } } } else if (os.startsWith("Mac")) { - pathList.add("/Applications/Chimera.app/Contents/MacOS/chimera"); + pathList.add(String.format("/Applications/%s.app/Contents/MacOS/%s", + chimera, chimeraExe)); } return pathList; } diff --git a/src/jalview/api/structures/JalviewStructureDisplayI.java b/src/jalview/api/structures/JalviewStructureDisplayI.java index 8f778f7..d8c8371 100644 --- a/src/jalview/api/structures/JalviewStructureDisplayI.java +++ b/src/jalview/api/structures/JalviewStructureDisplayI.java @@ -23,7 +23,6 @@ package jalview.api.structures; import jalview.api.AlignmentViewPanel; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; -import jalview.schemes.ColourSchemeI; import jalview.structures.models.AAStructureBindingModel; public interface JalviewStructureDisplayI @@ -59,13 +58,6 @@ public interface JalviewStructureDisplayI void closeViewer(boolean closeExternalViewer); /** - * apply a colourscheme to the structures in the viewer - * - * @param colourScheme - */ - void setJalviewColourScheme(ColourSchemeI colourScheme); - - /** * * @return true if all background sequence/structure binding threads have * completed for this viewer instance @@ -125,4 +117,48 @@ public interface JalviewStructureDisplayI */ void raiseViewer(); + AlignmentViewPanel getAlignmentPanel(); + + /** + * Answers true if the given alignment view is used to colour structures by + * sequence, false if not + * + * @param ap + * @return + */ + boolean isUsedForColourBy(AlignmentViewPanel ap); + + /** + * If implemented, shows a command line console in the structure viewer + * + * @param show + * true to show, false to hide + */ + void showConsole(boolean show); + + /** + * Remove references to the given alignment view for this structure viewer + * + * @param avp + */ + void removeAlignmentPanel(AlignmentViewPanel avp); + + /** + * Updates the progress bar if there is one. Call stopProgressBar with the + * returned handle to remove the message. + * + * @param msg + * @return handle + */ + long startProgressBar(String msg); + + /** + * Ends the progress bar with the specified handle, leaving a message (if not + * null) on the status bar + * + * @param msg + * @param handle + */ + void stopProgressBar(String msg, long handle); + } diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index 1a46585..0fd0945 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -3964,7 +3964,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, * without an additional javascript library to exchange messages between the * distinct applets. See http://issues.jalview.org/browse/JAL-621 * - * @param viewer + * @param jmolViewer * JmolViewer instance * @param sequenceIds * - sequence Ids to search for associations diff --git a/src/jalview/appletgui/AppletJmol.java b/src/jalview/appletgui/AppletJmol.java index 7fda3c4..6665ec8 100644 --- a/src/jalview/appletgui/AppletJmol.java +++ b/src/jalview/appletgui/AppletJmol.java @@ -20,25 +20,6 @@ */ package jalview.appletgui; -import jalview.bin.JalviewLite; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceI; -import jalview.io.DataSourceType; -import jalview.io.FileParse; -import jalview.io.StructureFile; -import jalview.schemes.BuriedColourScheme; -import jalview.schemes.HelixColourScheme; -import jalview.schemes.HydrophobicColourScheme; -import jalview.schemes.PurinePyrimidineColourScheme; -import jalview.schemes.StrandColourScheme; -import jalview.schemes.TaylorColourScheme; -import jalview.schemes.TurnColourScheme; -import jalview.schemes.UserColourScheme; -import jalview.schemes.ZappoColourScheme; -import jalview.structure.StructureSelectionManager; -import jalview.util.MessageManager; - import java.awt.BorderLayout; import java.awt.CheckboxMenuItem; import java.awt.Color; @@ -64,6 +45,25 @@ import java.util.ArrayList; import java.util.List; import java.util.Vector; +import jalview.bin.JalviewLite; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.io.DataSourceType; +import jalview.io.FileParse; +import jalview.io.StructureFile; +import jalview.schemes.BuriedColourScheme; +import jalview.schemes.HelixColourScheme; +import jalview.schemes.HydrophobicColourScheme; +import jalview.schemes.PurinePyrimidineColourScheme; +import jalview.schemes.StrandColourScheme; +import jalview.schemes.TaylorColourScheme; +import jalview.schemes.TurnColourScheme; +import jalview.schemes.UserColourScheme; +import jalview.schemes.ZappoColourScheme; +import jalview.structure.StructureSelectionManager; +import jalview.util.MessageManager; + public class AppletJmol extends EmbmenuFrame implements // StructureListener, KeyListener, ActionListener, ItemListener @@ -307,7 +307,7 @@ public class AppletJmol extends EmbmenuFrame implements else if (protocol == DataSourceType.FILE || protocol == DataSourceType.URL) { - jmb.viewer.openFile(pdbentry.getFile()); + jmb.jmolViewer.openFile(pdbentry.getFile()); } else { @@ -350,7 +350,7 @@ public class AppletJmol extends EmbmenuFrame implements throw new Exception(MessageManager.getString( "exception.invalid_datasource_couldnt_obtain_reader")); } - jmb.viewer.openReader(pdbentry.getFile(), pdbentry.getId(), + jmb.jmolViewer.openReader(pdbentry.getFile(), pdbentry.getId(), freader); } catch (Exception e) { @@ -406,12 +406,12 @@ public class AppletJmol extends EmbmenuFrame implements } } } - jmb.centerViewer(toshow); + jmb.showChains(toshow); } void closeViewer() { - jmb.closeViewer(); + jmb.closeViewer(true); jmb = null; this.setVisible(false); } @@ -455,41 +455,41 @@ public class AppletJmol extends EmbmenuFrame implements else if (evt.getSource() == zappo) { setEnabled(zappo); - jmb.setJalviewColourScheme(new ZappoColourScheme()); + jmb.colourByJalviewColourScheme(new ZappoColourScheme()); } else if (evt.getSource() == taylor) { setEnabled(taylor); - jmb.setJalviewColourScheme(new TaylorColourScheme()); + jmb.colourByJalviewColourScheme(new TaylorColourScheme()); } else if (evt.getSource() == hydro) { setEnabled(hydro); - jmb.setJalviewColourScheme(new HydrophobicColourScheme()); + jmb.colourByJalviewColourScheme(new HydrophobicColourScheme()); } else if (evt.getSource() == helix) { setEnabled(helix); - jmb.setJalviewColourScheme(new HelixColourScheme()); + jmb.colourByJalviewColourScheme(new HelixColourScheme()); } else if (evt.getSource() == strand) { setEnabled(strand); - jmb.setJalviewColourScheme(new StrandColourScheme()); + jmb.colourByJalviewColourScheme(new StrandColourScheme()); } else if (evt.getSource() == turn) { setEnabled(turn); - jmb.setJalviewColourScheme(new TurnColourScheme()); + jmb.colourByJalviewColourScheme(new TurnColourScheme()); } else if (evt.getSource() == buried) { setEnabled(buried); - jmb.setJalviewColourScheme(new BuriedColourScheme()); + jmb.colourByJalviewColourScheme(new BuriedColourScheme()); } else if (evt.getSource() == purinepyrimidine) { - jmb.setJalviewColourScheme(new PurinePyrimidineColourScheme()); + jmb.colourByJalviewColourScheme(new PurinePyrimidineColourScheme()); } else if (evt.getSource() == user) { @@ -658,7 +658,7 @@ public class AppletJmol extends EmbmenuFrame implements { currentSize = this.getSize(); - if (jmb.viewer == null) + if (jmb.jmolViewer == null) { g.setColor(Color.black); g.fillRect(0, 0, currentSize.width, currentSize.height); @@ -669,7 +669,7 @@ public class AppletJmol extends EmbmenuFrame implements } else { - jmb.viewer.renderScreenImage(g, currentSize.width, + jmb.jmolViewer.renderScreenImage(g, currentSize.width, currentSize.height); } } @@ -693,9 +693,9 @@ public class AppletJmol extends EmbmenuFrame implements * * } */ - public void setJalviewColourScheme(UserColourScheme ucs) + public void colourByJalviewColourScheme(UserColourScheme ucs) { - jmb.setJalviewColourScheme(ucs); + jmb.colourByJalviewColourScheme(ucs); } public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment) diff --git a/src/jalview/appletgui/AppletJmolBinding.java b/src/jalview/appletgui/AppletJmolBinding.java index f1c494e..c7ce994 100644 --- a/src/jalview/appletgui/AppletJmolBinding.java +++ b/src/jalview/appletgui/AppletJmolBinding.java @@ -51,13 +51,6 @@ class AppletJmolBinding extends JalviewJmolBinding } @Override - public jalview.api.FeatureRenderer getFeatureRenderer( - AlignmentViewPanel alignment) - { - return appletJmolBinding.ap.getFeatureRenderer(); - } - - @Override public jalview.api.SequenceRenderer getSequenceRenderer( AlignmentViewPanel alignment) { @@ -154,7 +147,7 @@ class AppletJmolBinding extends JalviewJmolBinding Container consolePanel, String buttonsToShow) { JmolAppConsoleInterface appc = new AppletConsole(); - appc.start(viewer); + appc.start(jmolViewer); return appc; } diff --git a/src/jalview/appletgui/ExtJmol.java b/src/jalview/appletgui/ExtJmol.java index 5a6d0d0..47f9df0 100644 --- a/src/jalview/appletgui/ExtJmol.java +++ b/src/jalview/appletgui/ExtJmol.java @@ -21,7 +21,6 @@ package jalview.appletgui; import jalview.api.AlignmentViewPanel; -import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceI; @@ -76,21 +75,6 @@ public class ExtJmol extends JalviewJmolBinding } @Override - public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment) - { - AlignmentPanel alignPanel = (AlignmentPanel) alignment; - if (alignPanel.av.isShowSequenceFeatures()) - { - return alignPanel.getFeatureRenderer(); - } - else - { - return null; - } - } - - - @Override public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment) { return ((AlignmentPanel) alignment).getSequenceRenderer(); @@ -191,14 +175,8 @@ public class ExtJmol extends JalviewJmolBinding } @Override - public void releaseReferences(Object svl) - { - } - - @Override public Map getJSpecViewProperty(String arg0) { return null; } - } diff --git a/src/jalview/appletgui/UserDefinedColours.java b/src/jalview/appletgui/UserDefinedColours.java index febe5f8..83d6fd6 100644 --- a/src/jalview/appletgui/UserDefinedColours.java +++ b/src/jalview/appletgui/UserDefinedColours.java @@ -524,7 +524,7 @@ public class UserDefinedColours extends Panel } else if (jmol != null) { - jmol.setJalviewColourScheme(ucs); + jmol.colourByJalviewColourScheme(ucs); } else if (pdbcanvas != null) { diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index 453152e..eee48df 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -20,33 +20,12 @@ */ package jalview.ext.jmol; -import jalview.api.AlignmentViewPanel; -import jalview.api.FeatureRenderer; -import jalview.api.SequenceRenderer; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceI; -import jalview.gui.IProgressIndicator; -import jalview.io.DataSourceType; -import jalview.io.StructureFile; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ResidueProperties; -import jalview.structure.AtomSpec; -import jalview.structure.StructureMappingcommandSet; -import jalview.structure.StructureSelectionManager; -import jalview.structures.models.AAStructureBindingModel; -import jalview.util.MessageManager; - -import java.awt.Color; import java.awt.Container; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.io.File; import java.net.URL; import java.util.ArrayList; -import java.util.BitSet; -import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.StringTokenizer; @@ -58,50 +37,53 @@ import org.jmol.api.JmolSelectionListener; import org.jmol.api.JmolStatusListener; import org.jmol.api.JmolViewer; import org.jmol.c.CBK; -import org.jmol.script.T; import org.jmol.viewer.Viewer; +import jalview.api.AlignmentViewPanel; +import jalview.api.FeatureRenderer; +import jalview.api.SequenceRenderer; +import jalview.bin.Cache; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.gui.IProgressIndicator; +import jalview.gui.StructureViewer.ViewerType; +import jalview.io.DataSourceType; +import jalview.io.StructureFile; +import jalview.structure.AtomSpec; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureSelectionManager; +import jalview.structures.models.AAStructureBindingModel; +import javajs.util.BS; + public abstract class JalviewJmolBinding extends AAStructureBindingModel implements JmolStatusListener, JmolSelectionListener, ComponentListener { private String lastMessage; - boolean allChainsSelected = false; - /* * when true, try to search the associated datamodel for sequences that are * associated with any unknown structures in the Jmol view. */ private boolean associateNewStructs = false; - Vector atomsPicked = new Vector<>(); - - private List chainNames; - - Hashtable chainFile; - - /* - * the default or current model displayed if the model cannot be identified - * from the selection message - */ - int frameNo = 0; - - // protected JmolGenericPopup jmolpopup; // not used - remove? + private Vector atomsPicked = new Vector<>(); - String lastCommand; + private String lastCommand; - boolean loadedInline; + private boolean loadedInline; - StringBuffer resetLastRes = new StringBuffer(); + private StringBuffer resetLastRes = new StringBuffer(); - public Viewer viewer; + public Viewer jmolViewer; public JalviewJmolBinding(StructureSelectionManager ssm, PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol) { super(ssm, pdbentry, sequenceIs, protocol); + setStructureCommands(new JmolCommands()); /* * viewer = JmolViewer.allocateViewer(renderPanel, new SmarterJmolAdapter(), * "jalviewJmol", ap.av.applet .getDocumentBase(), @@ -116,9 +98,10 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { super(ssm, seqs); - viewer = theViewer; - viewer.setJmolStatusListener(this); - viewer.addSelectionListener(this); + jmolViewer = theViewer; + jmolViewer.setJmolStatusListener(this); + jmolViewer.addSelectionListener(this); + setStructureCommands(new JmolCommands()); } /** @@ -132,409 +115,32 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel return getViewerTitle("Jmol", true); } - /** - * prepare the view for a given set of models/chains. chainList contains - * strings of the form 'pdbfilename:Chaincode' - * - * @param chainList - * list of chains to make visible - */ - public void centerViewer(Vector chainList) - { - StringBuilder cmd = new StringBuilder(128); - int mlength, p; - for (String lbl : chainList) - { - mlength = 0; - do - { - p = mlength; - mlength = lbl.indexOf(":", p); - } while (p < mlength && mlength < (lbl.length() - 2)); - // TODO: lookup each pdb id and recover proper model number for it. - cmd.append(":" + lbl.substring(mlength + 1) + " /" - + (1 + getModelNum(chainFile.get(lbl))) + " or "); - } - if (cmd.length() > 0) - { - cmd.setLength(cmd.length() - 4); - } - evalStateCommand("select *;restrict " + cmd + ";cartoon;center " + cmd); - } - - public void closeViewer() - { - // remove listeners for all structures in viewer - getSsm().removeStructureViewerListener(this, this.getStructureFiles()); - if (viewer != null) - { - viewer.dispose(); - } - lastCommand = null; - viewer = null; - releaseUIResources(); - } - - @Override - public void colourByChain() - { - colourBySequence = false; - // TODO: colour by chain should colour each chain distinctly across all - // visible models - // TODO: http://issues.jalview.org/browse/JAL-628 - evalStateCommand("select *;color chain"); - } - - @Override - public void colourByCharge() - { - colourBySequence = false; - evalStateCommand("select *;color white;select ASP,GLU;color red;" - + "select LYS,ARG;color blue;select CYS;color yellow"); - } - - /** - * superpose the structures associated with sequences in the alignment - * according to their corresponding positions. - */ - public void superposeStructures(AlignmentI alignment) - { - superposeStructures(alignment, -1, null); - } - - /** - * superpose the structures associated with sequences in the alignment - * according to their corresponding positions. ded) - * - * @param refStructure - * - select which pdb file to use as reference (default is -1 - the - * first structure in the alignment) - */ - public void superposeStructures(AlignmentI alignment, int refStructure) + private String jmolScript(String script) { - superposeStructures(alignment, refStructure, null); - } + Cache.log.debug(">>Jmol>> " + script); + String s = jmolViewer.evalStringQuiet(script); // scriptWait(script); BH + Cache.log.debug("< executeCommand(StructureCommandI command, + boolean getReply) { - while (viewer.isScriptExecuting()) - { - try - { - Thread.sleep(10); - } catch (InterruptedException i) - { - } - } - - /* - * get the distinct structure files modelled - * (a file with multiple chains may map to multiple sequences) - */ - String[] files = getStructureFiles(); - if (!waitForFileLoad(files)) + if (command == null) { return null; } - - StringBuilder selectioncom = new StringBuilder(256); - // In principle - nSeconds specifies the speed of animation for each - // superposition - but is seems to behave weirdly, so we don't specify it. - String nSeconds = " "; - if (files.length > 10) - { - nSeconds = " 0.005 "; - } - else - { - nSeconds = " " + (2.0 / files.length) + " "; - // if (nSeconds).substring(0,5)+" "; - } - - // see JAL-1345 - should really automatically turn off the animation for - // large numbers of structures, but Jmol doesn't seem to allow that. - // nSeconds = " "; - // union of all aligned positions are collected together. - for (int a = 0; a < _alignment.length; a++) - { - int refStructure = _refStructure[a]; - AlignmentI alignment = _alignment[a]; - HiddenColumns hiddenCols = _hiddenCols[a]; - if (a > 0 && selectioncom.length() > 0 && !selectioncom - .substring(selectioncom.length() - 1).equals("|")) - { - selectioncom.append("|"); - } - // process this alignment - if (refStructure >= files.length) - { - System.err.println( - "Invalid reference structure value " + refStructure); - refStructure = -1; - } - - /* - * 'matched' bit j will be set for visible alignment columns j where - * all sequences have a residue with a mapping to the PDB structure - */ - BitSet matched = new BitSet(); - for (int m = 0; m < alignment.getWidth(); m++) - { - if (hiddenCols == null || hiddenCols.isVisible(m)) - { - matched.set(m); - } - } - - SuperposeData[] structures = new SuperposeData[files.length]; - for (int f = 0; f < files.length; f++) - { - structures[f] = new SuperposeData(alignment.getWidth()); - } - - /* - * Calculate the superposable alignment columns ('matched'), and the - * corresponding structure residue positions (structures.pdbResNo) - */ - int candidateRefStructure = findSuperposableResidues(alignment, - matched, structures); - if (refStructure < 0) - { - /* - * If no reference structure was specified, pick the first one that has - * a mapping in the alignment - */ - refStructure = candidateRefStructure; - } - - String[] selcom = new String[files.length]; - int nmatched = matched.cardinality(); - if (nmatched < 4) - { - return (MessageManager.formatMessage("label.insufficient_residues", - nmatched)); - } - - /* - * generate select statements to select regions to superimpose structures - */ - { - // TODO extract method to construct selection statements - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - String chainCd = ":" + structures[pdbfnum].chain; - int lpos = -1; - boolean run = false; - StringBuilder molsel = new StringBuilder(); - molsel.append("{"); - - int nextColumnMatch = matched.nextSetBit(0); - while (nextColumnMatch != -1) - { - int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch]; - if (lpos != pdbResNo - 1) - { - // discontinuity - if (lpos != -1) - { - molsel.append(lpos); - molsel.append(chainCd); - molsel.append("|"); - } - run = false; - } - else - { - // continuous run - and lpos >-1 - if (!run) - { - // at the beginning, so add dash - molsel.append(lpos); - molsel.append("-"); - } - run = true; - } - lpos = pdbResNo; - nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); - } - /* - * add final selection phrase - */ - if (lpos != -1) - { - molsel.append(lpos); - molsel.append(chainCd); - molsel.append("}"); - } - if (molsel.length() > 1) - { - selcom[pdbfnum] = molsel.toString(); - selectioncom.append("(("); - selectioncom.append(selcom[pdbfnum].substring(1, - selcom[pdbfnum].length() - 1)); - selectioncom.append(" )& "); - selectioncom.append(pdbfnum + 1); - selectioncom.append(".1)"); - if (pdbfnum < files.length - 1) - { - selectioncom.append("|"); - } - } - else - { - selcom[pdbfnum] = null; - } - } - } - StringBuilder command = new StringBuilder(256); - // command.append("set spinFps 10;\n"); - - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - if (pdbfnum == refStructure || selcom[pdbfnum] == null - || selcom[refStructure] == null) - { - continue; - } - command.append("echo "); - command.append("\"Superposing ("); - command.append(structures[pdbfnum].pdbId); - command.append(") against reference ("); - command.append(structures[refStructure].pdbId); - command.append(")\";\ncompare " + nSeconds); - command.append("{"); - command.append(Integer.toString(1 + pdbfnum)); - command.append(".1} {"); - command.append(Integer.toString(1 + refStructure)); - // conformation=1 excludes alternate locations for CA (JAL-1757) - command.append( - ".1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS "); - - // for (int s = 0; s < 2; s++) - // { - // command.append(selcom[(s == 0 ? pdbfnum : refStructure)]); - // } - command.append(selcom[pdbfnum]); - command.append(selcom[refStructure]); - command.append(" ROTATE TRANSLATE;\n"); - } - if (selectioncom.length() > 0) - { - // TODO is performing selectioncom redundant here? is done later on - // System.out.println("Select regions:\n" + selectioncom.toString()); - evalStateCommand("select *; cartoons off; backbone; select (" - + selectioncom.toString() + "); cartoons; "); - // selcom.append("; ribbons; "); - String cmdString = command.toString(); - // System.out.println("Superimpose command(s):\n" + cmdString); - - evalStateCommand(cmdString); - } - } - if (selectioncom.length() > 0) - {// finally, mark all regions that were superposed. - if (selectioncom.substring(selectioncom.length() - 1).equals("|")) - { - selectioncom.setLength(selectioncom.length() - 1); - } - // System.out.println("Select regions:\n" + selectioncom.toString()); - evalStateCommand("select *; cartoons off; backbone; select (" - + selectioncom.toString() + "); cartoons; "); - // evalStateCommand("select *; backbone; select "+selcom.toString()+"; - // cartoons; center "+selcom.toString()); - } - - return null; - } - - public void evalStateCommand(String command) - { + String cmd = command.getCommand(); jmolHistory(false); - if (lastCommand == null || !lastCommand.equals(command)) + if (lastCommand == null || !lastCommand.equals(cmd)) { - jmolScript(command + "\n"); + jmolScript(cmd + "\n"); } jmolHistory(true); - lastCommand = command; - } - - Thread colourby = null; - - /** - * Sends a set of colour commands to the structure viewer - * - * @param colourBySequenceCommands - */ - @Override - protected void colourBySequence( - final StructureMappingcommandSet[] colourBySequenceCommands) - { - if (colourby != null) - { - colourby.interrupt(); - colourby = null; - } - Thread colourby = new Thread(new Runnable() - { - @Override - public void run() - { - for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands) - { - for (String cbyseq : cpdbbyseq.commands) - { - executeWhenReady(cbyseq); - } - } - } - }); - colourby.start(); - this.colourby = colourby; - } - - /** - * @param files - * @param sr - * @param viewPanel - * @return - */ - @Override - protected StructureMappingcommandSet[] getColourBySequenceCommands( - String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel) - { - return JmolCommands.getColourBySequenceCommand(getSsm(), files, - getSequence(), sr, viewPanel); - } - - /** - * @param command - */ - protected void executeWhenReady(String command) - { - evalStateCommand(command); + lastCommand = cmd; + return null; } public void createImage(String file, String type, int quality) @@ -575,43 +181,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel return null; } - public Color getColour(int atomIndex, int pdbResNum, String chain, - String pdbfile) - { - if (getModelNum(pdbfile) < 0) - { - return null; - } - // TODO: verify atomIndex is selecting correct model. - // return new Color(viewer.getAtomArgb(atomIndex)); Jmol 12.2.4 - int colour = viewer.ms.at[atomIndex].atomPropertyInt(T.color); - return new Color(colour); - } - - /** - * instruct the Jalview binding to update the pdbentries vector if necessary - * prior to matching the jmol view's contents to the list of structure files - * Jalview knows about. - */ - public abstract void refreshPdbEntries(); - - private int getModelNum(String modelFileName) - { - String[] mfn = getStructureFiles(); - if (mfn == null) - { - return -1; - } - for (int i = 0; i < mfn.length; i++) - { - if (mfn[i].equalsIgnoreCase(modelFileName)) - { - return i; - } - } - return -1; - } - /** * map between index of model filename returned from getPdbFile and the first * index of models from this file in the viewer. Note - this is not trimmed - @@ -622,25 +191,32 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel @Override public synchronized String[] getStructureFiles() { - List mset = new ArrayList<>(); - if (viewer == null) + if (jmolViewer == null) { return new String[0]; } if (modelFileNames == null) { - int modelCount = viewer.ms.mc; + int modelCount = jmolViewer.ms.mc; String filePath = null; + List mset = new ArrayList<>(); for (int i = 0; i < modelCount; ++i) { - filePath = viewer.ms.getModelFileName(i); - if (!mset.contains(filePath)) + /* + * defensive check for null as getModelFileName can return null + * even when model count ms.mc is > 0 + */ + filePath = jmolViewer.ms.getModelFileName(i); + if (filePath != null && !mset.contains(filePath)) { mset.add(filePath); } } - modelFileNames = mset.toArray(new String[mset.size()]); + if (!mset.isEmpty()) + { + modelFileNames = mset.toArray(new String[mset.size()]); + } } return modelFileNames; @@ -690,55 +266,35 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel public void highlightAtom(int atomIndex, int pdbResNum, String chain, String pdbfile) { - if (modelFileNames == null) - { - return; - } - - // look up file model number for this pdbfile - int mdlNum = 0; - // may need to adjust for URLencoding here - we don't worry about that yet. - while (mdlNum < modelFileNames.length - && !pdbfile.equals(modelFileNames[mdlNum])) - { - mdlNum++; - } - if (mdlNum == modelFileNames.length) + String modelId = getModelIdForFile(pdbfile); + if (modelId.isEmpty()) { return; } jmolHistory(false); + StringBuilder selection = new StringBuilder(32); StringBuilder cmd = new StringBuilder(64); - cmd.append("select " + pdbResNum); // +modelNum - - resetLastRes.append("select " + pdbResNum); // +modelNum - - cmd.append(":"); - resetLastRes.append(":"); + selection.append("select ").append(String.valueOf(pdbResNum)); + selection.append(":"); if (!chain.equals(" ")) { - cmd.append(chain); - resetLastRes.append(chain); - } - { - cmd.append(" /" + (mdlNum + 1)); - resetLastRes.append("/" + (mdlNum + 1)); + selection.append(chain); } - cmd.append(";wireframe 100;" + cmd.toString() + " and not hetero;"); + selection.append(" /").append(modelId); - resetLastRes.append(";wireframe 0;" + resetLastRes.toString() - + " and not hetero; spacefill 0;"); + cmd.append(selection).append(";wireframe 100;").append(selection) + .append(" and not hetero;").append("spacefill 200;select none"); - cmd.append("spacefill 200;select none"); + resetLastRes.append(selection).append(";wireframe 0;").append(selection) + .append(" and not hetero; spacefill 0;"); jmolScript(cmd.toString()); jmolHistory(true); - } - boolean debug = true; + private boolean debug = true; private void jmolHistory(boolean enable) { @@ -756,7 +312,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // Then, construct pass a reader for the string to Jmol. // ((org.jmol.Viewer.Viewer) viewer).loadModelFromFile(fullPathName, // fileName, null, reader, false, null, null, 0); - viewer.openStringInline(string); + jmolViewer.openStringInline(string); } protected void mouseOverStructure(int atomIndex, final String strInfo) @@ -799,8 +355,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel chainId = " "; } - String pdbfilename = modelFileNames[frameNo]; // default is first or current - // model + String pdbfilename = modelFileNames[0]; // default is first model if (mdlSep > -1) { if (chainSeparator1 == -1) @@ -833,7 +388,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel if (pdbfilename == null) { - pdbfilename = new File(viewer.ms.getModelFileName(mnumber)) + pdbfilename = new File(jmolViewer.ms.getModelFileName(mnumber)) .getAbsolutePath(); } } @@ -859,7 +414,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ") .append(toks.nextToken()); sb.append("|").append(label).append("\""); - evalStateCommand(sb.toString()); + executeCommand(new StructureCommand(sb.toString()), false); } } @@ -925,7 +480,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } else { - viewer.evalString("select " + picked + ";label off"); + jmolViewer.evalString("select " + picked + ";label off"); atomsPicked.removeElement(picked); } jmolHistory(true); @@ -1041,10 +596,13 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel fileLoadingError = null; String[] oldmodels = modelFileNames; modelFileNames = null; - chainNames = new ArrayList<>(); - chainFile = new Hashtable<>(); boolean notifyLoaded = false; String[] modelfilenames = getStructureFiles(); + if (modelfilenames == null) + { + // Jmol is still loading files! + return; + } // first check if we've lost any structures if (oldmodels != null && oldmodels.length > 0) { @@ -1093,7 +651,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // calculate essential attributes for the pdb data imported inline. // prolly need to resolve modelnumber properly - for now just use our // 'best guess' - pdbfile = viewer.getData( + pdbfile = jmolViewer.getData( "" + (1 + _modelFileNameMap[modelnum]) + ".0", "PDB"); } // search pdbentries and sequences to find correct pdbentry for this @@ -1147,14 +705,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } if (matches) { - // add an entry for every chain in the model - for (int i = 0; i < pdb.getChains().size(); i++) - { - String chid = new String( - pdb.getId() + ":" + pdb.getChains().elementAt(i).id); - chainFile.put(chid, fileName); - chainNames.add(chid); - } + stashFoundChains(pdb, fileName); notifyLoaded = true; } } @@ -1164,7 +715,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel // this is a foreign pdb file that jalview doesn't know about - add // it to the dataset and try to find a home - either on a matching // sequence or as a new sequence. - String pdbcontent = viewer.getData("/" + (modelnum + 1) + ".1", + String pdbcontent = jmolViewer.getData("/" + (modelnum + 1) + ".1", "PDB"); // parse pdb file into a chain, etc. // locate best match for pdb in associated views and add mapping to @@ -1202,12 +753,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel setLoadingFromArchive(false); } - @Override - public List getChainNames() - { - return chainNames; - } - protected IProgressIndicator getIProgressIndicator() { return null; @@ -1252,35 +797,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } - @Override - public void setJalviewColourScheme(ColourSchemeI cs) - { - colourBySequence = false; - - if (cs == null) - { - return; - } - - jmolHistory(false); - StringBuilder command = new StringBuilder(128); - command.append("select *;color white;"); - List residueSet = ResidueProperties.getResidues(isNucleotide(), - false); - for (String resName : residueSet) - { - char res = resName.length() == 3 - ? ResidueProperties.getSingleCharacterCode(resName) - : resName.charAt(0); - Color col = cs.findColour(res, 0, null, null, 0f); - command.append("select " + resName + ";color[" + col.getRed() + "," - + col.getGreen() + "," + col.getBlue() + "];"); - } - - evalStateCommand(command.toString()); - jmolHistory(true); - } - public void showHelp() { showUrl("http://wiki.jmol.org" @@ -1296,13 +812,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel public abstract void showUrl(String url, String target); /** - * called when the binding thinks the UI needs to be refreshed after a Jmol - * state change. this could be because structures were loaded, or because an - * error has occured. - */ - public abstract void refreshGUI(); - - /** * called to show or hide the associated console window container. * * @param show @@ -1363,12 +872,12 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel { commandOptions = ""; } - viewer = (Viewer) JmolViewer.allocateViewer(renderPanel, + jmolViewer = (Viewer) JmolViewer.allocateViewer(renderPanel, (jmolfileio ? new SmarterJmolAdapter() : null), htmlName + ((Object) this).toString(), documentBase, codeBase, commandOptions, this); - viewer.setJmolStatusListener(this); // extends JmolCallbackListener + jmolViewer.setJmolStatusListener(this); // extends JmolCallbackListener try { @@ -1396,27 +905,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel protected org.jmol.api.JmolAppConsoleInterface console = null; @Override - public void setBackgroundColour(java.awt.Color col) - { - jmolHistory(false); - jmolScript("background [" + col.getRed() + "," + col.getGreen() + "," - + col.getBlue() + "];"); - jmolHistory(true); - } - - private String jmolScript(String script) - { - - System.err.println(">>Jmol>> " + script); - - String s = viewer.scriptWait(script); - - System.err.println("< colourByCharge() + { + return Arrays.asList(COLOUR_BY_CHARGE); + } + + @Override + public List colourByResidues(Map colours) + { + List cmds = super.colourByResidues(colours); + cmds.add(0, COLOUR_ALL_WHITE); + return cmds; + } + + @Override + public StructureCommandI setBackgroundColour(Color col) + { + return new StructureCommand("background " + getColourString(col)); + } + + @Override + public StructureCommandI focusView() + { + return FOCUS_VIEW; + } + + @Override + public List showChains(List toShow) + { + StringBuilder atomSpec = new StringBuilder(128); + boolean first = true; + for (String chain : toShow) + { + String[] tokens = chain.split(":"); + if (tokens.length == 2) + { + if (!first) + { + atomSpec.append(" or "); + } + first = false; + atomSpec.append(":").append(tokens[1]).append(" /").append(tokens[0]); + } + } + + String spec = atomSpec.toString(); + String command = "select *;restrict " + spec + ";cartoon;center " + + spec; + return Arrays.asList(new StructureCommand(command)); + } + + /** + * Returns a command to superpose atoms in {@code atomSpec} to those in + * {@code refAtoms}, restricted to alpha carbons only (Phosphorous for rna). + * For example + * + *
+   * compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} 
+   *         ATOMS {1-87:A}{2-54:A|61-94:A} ROTATE TRANSLATE 1.0;
+   * 
+ * + * where {@code conformation=1} excludes ALTLOC atom locations, and 1.0 is the + * time in seconds to animate the action. For this example, atoms in model 2 + * are moved towards atoms in model 1. + *

+ * The two atomspecs should each be for one model only, but may have more than + * one chain. The number of atoms specified should be the same for both + * models, though if not, Jmol may make a 'best effort' at superposition. + * + * @see https://chemapps.stolaf.edu/jmol/docs/#compare + */ + @Override + public List superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec) + { + StringBuilder sb = new StringBuilder(64); + String refModel = refAtoms.getModels().iterator().next(); + String model2 = atomSpec.getModels().iterator().next(); + sb.append(String.format("compare {%s.1} {%s.1}", model2, refModel)); + sb.append(" SUBSET {(*.CA | *.P) and conformation=1} ATOMS {"); + + /* + * command examples don't include modelspec with atoms, getAtomSpec does; + * it works, so leave it as it is for simplicity + */ + sb.append(getAtomSpec(atomSpec, true)).append("}{"); + sb.append(getAtomSpec(refAtoms, true)).append("}"); + sb.append(" ROTATE TRANSLATE "); + sb.append(getCommandSeparator()); + + /* + * show residues used for superposition as ribbon + */ + sb.append("select ").append(getAtomSpec(atomSpec, false)).append("|"); + sb.append(getAtomSpec(refAtoms, false)).append(getCommandSeparator()) + .append("cartoons"); + + return Arrays.asList(new StructureCommand(sb.toString())); + } + + @Override + public StructureCommandI openCommandFile(String path) + { + /* + * https://chemapps.stolaf.edu/jmol/docs/#script + * not currently used in Jalview + */ + return new StructureCommand("script " + path); + } + + @Override + public StructureCommandI saveSession(String filepath) + { + /* + * https://chemapps.stolaf.edu/jmol/docs/#writemodel + */ + return new StructureCommand("write STATE \"" + filepath + "\""); + } + + @Override + protected StructureCommandI colourResidues(String atomSpec, Color colour) + { + StringBuilder sb = new StringBuilder(atomSpec.length()+20); + sb.append("select ").append(atomSpec).append(getCommandSeparator()) + .append("color").append(getColourString(colour)); + return new StructureCommand(sb.toString()); + } + + @Override + protected String getResidueSpec(String residue) + { + return residue; + } /** - * Jmol utility which constructs the commands to colour chains by the given - * alignment + * Generates a Jmol atomspec string like + * + *

+   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
+   * 
* - * @returns Object[] { Object[] { , + * Parameter {@code alphaOnly} is not used here - this restriction is made by + * a separate clause in the {@code compare} (superposition) command. + */ + @Override + public String getAtomSpec(AtomSpecModel model, boolean alphaOnly) + { + StringBuilder sb = new StringBuilder(128); + + boolean first = true; + for (String modelNo : model.getModels()) + { + for (String chain : model.getChains(modelNo)) + { + for (int[] range : model.getRanges(modelNo, chain)) + { + if (!first) + { + sb.append(PIPE); + } + first = false; + if (range[0] == range[1]) + { + sb.append(range[0]); + } + else + { + sb.append(range[0]).append(HYPHEN).append(range[1]); + } + sb.append(COLON).append(chain.trim()).append(SLASH); + sb.append(String.valueOf(modelNo)).append(".1"); + } + } + } + + return sb.toString(); + } + + @Override + public List showBackbone() + { + return Arrays.asList(SHOW_BACKBONE); + } + + @Override + public StructureCommandI loadFile(String file) + { + // https://chemapps.stolaf.edu/jmol/docs/#loadfiles + return new StructureCommand("load FILES \"" + + Platform.escapeBackslashes(file) + "\""); + } + + /** + * Obsolete method, only referenced from + * jalview.javascript.MouseOverStructureListener * + * @param ssm + * @param files + * @param sequence + * @param sr + * @param viewPanel + * @return */ - public static StructureMappingcommandSet[] getColourBySequenceCommand( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, + @Deprecated + public String[] colourBySequence(StructureSelectionManager ssm, + String[] files, SequenceI[][] sequence, SequenceRenderer sr, AlignmentViewPanel viewPanel) { + // TODO delete method + FeatureRenderer fr = viewPanel.getFeatureRenderer(); FeatureColourFinder finder = new FeatureColourFinder(fr); AlignViewportI viewport = viewPanel.getAlignViewport(); HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); AlignmentI al = viewport.getAlignment(); - List cset = new ArrayList(); + List cset = new ArrayList<>(); for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) { StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); - StringBuilder command = new StringBuilder(); - StructureMappingcommandSet smc; - ArrayList str = new ArrayList(); + StringBuilder command = new StringBuilder(128); + List str = new ArrayList<>(); if (mapping == null || mapping.length < 1) { @@ -89,7 +345,7 @@ public class JmolCommands for (int r = 0; r < asp.getLength(); r++) { // no mapping to gaps in sequence - if (jalview.util.Comparison.isGap(asp.getCharAt(r))) + if (Comparison.isGap(asp.getCharAt(r))) { continue; } @@ -125,14 +381,10 @@ public class JmolCommands col = Color.GRAY; } - // todo JAL-3152 handle 'no chain' case without errors - boolean hasChain = true || mapping[m].getChain() != " "; - String chainSpec = hasChain + String newSelcom = (mapping[m].getChain() != " " ? ":" + mapping[m].getChain() - : ""; - String newSelcom = chainSpec + "/" + (pdbfnum + 1) + ".1" + ";color[" - + col.getRed() + "," + col.getGreen() + "," - + col.getBlue() + "]"; + : "") + "/" + (pdbfnum + 1) + ".1" + ";color" + + getColourString(col); if (command.length() > newSelcom.length() && command .substring(command.length() - newSelcom.length()) .equals(newSelcom)) @@ -167,15 +419,23 @@ public class JmolCommands str.add(command.toString()); command.setLength(0); } - // Finally, add the command set ready to be returned. - cset.add(new StructureMappingcommandSet(JmolCommands.class, - files[pdbfnum], str.toArray(new String[str.size()]))); + cset.addAll(str); } - return cset.toArray(new StructureMappingcommandSet[cset.size()]); + return cset.toArray(new String[cset.size()]); } - public static StringBuilder condenseCommand(StringBuilder command, int pos) + /** + * Helper method + * + * @param command + * @param pos + * @return + */ + @Deprecated + private static StringBuilder condenseCommand( + StringBuilder command, + int pos) { // work back to last 'select' @@ -210,4 +470,9 @@ public class JmolCommands return sb; } + @Override + public StructureCommandI openSession(String filepath) + { + return loadFile(filepath); + } } diff --git a/src/jalview/ext/pymol/PymolCommands.java b/src/jalview/ext/pymol/PymolCommands.java new file mode 100644 index 0000000..3493d03 --- /dev/null +++ b/src/jalview/ext/pymol/PymolCommands.java @@ -0,0 +1,320 @@ +package jalview.ext.pymol; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsBase; + +/** + * A class that generates commands to send to PyMol over its XML-RPC interface. + *

+ * Note that because the xml-rpc interface can only accept one command at a + * time, we can't concatenate commands, and must instead form and send them + * individually. + * + * @see https://pymolwiki.org/index.php/Category:Commands + * @see https://pymolwiki.org/index.php/RPC + */ +public class PymolCommands extends StructureCommandsBase +{ + private static final StructureCommand COLOUR_BY_CHAIN = new StructureCommand("spectrum", "chain"); + + private static final List COLOR_BY_CHARGE = new ArrayList<>(); + + private static final List SHOW_BACKBONE = new ArrayList<>(); + + static { + COLOR_BY_CHARGE.add(new StructureCommand("color", "white", "*")); + COLOR_BY_CHARGE + .add(new StructureCommand("color", "red", "resn ASP resn GLU")); + COLOR_BY_CHARGE.add( + new StructureCommand("color", "blue", "resn LYS resn ARG")); + COLOR_BY_CHARGE + .add(new StructureCommand("color", "yellow", "resn CYS")); + SHOW_BACKBONE.add(new StructureCommand("hide", "everything")); + SHOW_BACKBONE.add(new StructureCommand("show", "ribbon")); + } + + @Override + public StructureCommandI colourByChain() + { + return COLOUR_BY_CHAIN; + } + + @Override + public List colourByCharge() + { + return COLOR_BY_CHARGE; + } + + @Override + public StructureCommandI setBackgroundColour(Color col) + { + // https://pymolwiki.org/index.php/Bg_Color + return new StructureCommand("bg_color", getColourString(col)); + } + + /** + * Returns a colour formatted suitable for use in viewer command syntax. For + * example, red is {@code "0xff0000"}. + * + * @param c + * @return + */ + protected String getColourString(Color c) + { + return String.format("0x%02x%02x%02x", c.getRed(), c.getGreen(), + c.getBlue()); + } + + @Override + public StructureCommandI focusView() + { + // TODO what? + return null; + } + + @Override + public List showChains(List toShow) + { + // https://pymolwiki.org/index.php/Show + List commands = new ArrayList<>(); + commands.add(new StructureCommand("hide", "everything")); + commands.add(new StructureCommand("show", "lines")); + StringBuilder chains = new StringBuilder(); + for (String chain : toShow) + { + chains.append(" chain ").append(chain); + } + commands.add(new StructureCommand("show", "cartoon", chains.toString())); + return commands; + } + + @Override + public List superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec) + { + // https://pymolwiki.org/index.php/Super + List commands = new ArrayList<>(); + String refAtomsAlphaOnly = getAtomSpec(refAtoms, true); + String atomSpec2AlphaOnly = getAtomSpec(atomSpec, true); + commands.add(new StructureCommand("super", refAtomsAlphaOnly, + atomSpec2AlphaOnly)); + + /* + * and show superposed residues as cartoon + */ + String refAtomsAll = getAtomSpec(refAtoms, false); + String atomSpec2All = getAtomSpec(atomSpec, false); + commands.add(new StructureCommand("show", "cartoon", + refAtomsAll + " " + atomSpec2All)); + + return commands; + } + + @Override + public StructureCommandI openCommandFile(String path) + { + // https://pymolwiki.org/index.php/Run + return new StructureCommand("run", path); // should be .pml + } + + @Override + public StructureCommandI saveSession(String filepath) + { + // https://pymolwiki.org/index.php/Save#EXAMPLES + return new StructureCommand("save", filepath); // should be .pse + } + + /** + * Returns a selection string in PyMOL 'selection macro' format: + * + *

+   * modelId// chain/residues/
+   * 
+ * + * If more than one chain, makes a selection expression for each, and they are + * separated by spaces. + * + * @see https://pymolwiki.org/index.php/Selection_Macros + */ + @Override + public String getAtomSpec(AtomSpecModel model, boolean alphaOnly) + { + StringBuilder sb = new StringBuilder(64); + boolean first = true; + for (String modelId : model.getModels()) + { + for (String chain : model.getChains(modelId)) + { + if (!first) + { + sb.append(" "); + } + first = false; + List rangeList = model.getRanges(modelId, chain); + chain = chain.trim(); + sb.append(modelId).append("//").append(chain).append("/"); + boolean firstRange = true; + for (int[] range : rangeList) + { + if (!firstRange) + { + sb.append("+"); + } + firstRange = false; + sb.append(String.valueOf(range[0])); + if (range[0] != range[1]) + { + sb.append("-").append(String.valueOf(range[1])); + } + } + sb.append("/"); + if (alphaOnly) + { + sb.append("CA"); + } + } + } + return sb.toString(); + } + + @Override + public List showBackbone() + { + return SHOW_BACKBONE; + } + + @Override + protected StructureCommandI colourResidues(String atomSpec, Color colour) + { + // https://pymolwiki.org/index.php/Color + return new StructureCommand("color", getColourString(colour), atomSpec); + } + + @Override + protected String getResidueSpec(String residue) + { + // https://pymolwiki.org/index.php/Selection_Algebra + return "resn " + residue; + } + + @Override + public StructureCommandI loadFile(String file) + { + return new StructureCommand("load", file); + } + + /** + * Overrides the default implementation (which generates concatenated + * commands) to generate one per colour (because the XML-RPC interface to + * PyMOL only accepts one command at a time) + * + * @param colourMap + * @return + */ + @Override + public List colourBySequence( + Map colourMap) + { + List commands = new ArrayList<>(); + for (Object key : colourMap.keySet()) + { + Color colour = (Color) key; + final AtomSpecModel colourData = colourMap.get(colour); + commands.add(getColourCommand(colourData, colour)); + } + + return commands; + } + + /** + * Returns a viewer command to set the given atom property value on atoms + * specified by the AtomSpecModel, for example + * + *
+   * iterate 4zho//B/12-34,48-55/CA,jv_chain='primary'
+   * 
+ * + * @param attributeName + * @param attributeValue + * @param atomSpecModel + * @return + */ + protected StructureCommandI setAttribute(String attributeName, + String attributeValue, + AtomSpecModel atomSpecModel) + { + StringBuilder sb = new StringBuilder(128); + sb.append("p.").append(attributeName).append("='") + .append(attributeValue).append("'"); + String atomSpec = getAtomSpec(atomSpecModel, false); + return new StructureCommand("iterate", atomSpec, sb.toString()); + } + + /** + * Traverse the map of features/values/models/chains/positions to construct a + * list of 'set property' commands (one per distinct feature type and value). + * The values are stored in the 'p' dictionary of user-defined properties of + * each atom. + *

+ * The format of each command is + * + *

+   * 
iterate atomspec, p.featureName='value' + * e.g. iterate 4zho//A/23,28-29/CA, p.jv_Metal='Fe' + *
+ *
+ * + * @param featureMap + * @return + */ + @Override + public List setAttributes( + Map> featureMap) + { + List commands = new ArrayList<>(); + for (String featureType : featureMap.keySet()) + { + String attributeName = makeAttributeName(featureType); + + /* + * todo: clear down existing attributes for this feature? + */ + // commands.add(new StructureCommand("iterate", "all", + // "p."+attributeName+"='None'"); //? + + Map values = featureMap.get(featureType); + for (Object value : values.keySet()) + { + /* + * for each distinct value recorded for this feature type, + * add a command to set the attribute on the mapped residues + * Put values in single quotes, encoding any embedded single quotes + */ + AtomSpecModel atomSpecModel = values.get(value); + String featureValue = value.toString(); + featureValue = featureValue.replaceAll("\\'", "'"); + StructureCommandI cmd = setAttribute(attributeName, featureValue, + atomSpecModel); + commands.add(cmd); + } + } + + return commands; + } + + @Override + public StructureCommandI openSession(String filepath) + { + // https://pymolwiki.org/index.php/Load + // this version of the command has no dependency on file extension + return new StructureCommand("load", filepath, "", "0", "pse"); + } + +} diff --git a/src/jalview/ext/pymol/PymolManager.java b/src/jalview/ext/pymol/PymolManager.java new file mode 100644 index 0000000..e3b913b --- /dev/null +++ b/src/jalview/ext/pymol/PymolManager.java @@ -0,0 +1,321 @@ +package jalview.ext.pymol; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.SocketException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import jalview.bin.Cache; +import jalview.gui.Preferences; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; + +public class PymolManager +{ + private static final int RPC_REPLY_TIMEOUT_MS = 15000; + + private static final int CONNECTION_TIMEOUT_MS = 100; + + private static final String POST1 = ""; + + private static final String POST2 = ""; + + private static final String POST3 = ""; + + private Process pymolProcess; + + private int pymolXmlRpcPort; + + /** + * Returns a list of paths to try for the PyMOL executable. Any user + * preference is placed first, otherwise 'standard' paths depending on the + * operating system. + * + * @return + */ + public static List getPymolPaths() + { + return getPymolPaths(System.getProperty("os.name")); + } + + /** + * Returns a list of paths to try for the PyMOL executable. Any user + * preference is placed first, otherwise 'standard' paths depending on the + * operating system. + * + * @param os + * operating system as reported by environment variable + * {@code os.name} + * @return + */ + protected static List getPymolPaths(String os) + { + List pathList = new ArrayList<>(); + + String userPath = Cache + .getDefault(Preferences.PYMOL_PATH, null); + if (userPath != null) + { + pathList.add(userPath); + } + + /* + * add default installation paths + */ + String pymol = "PyMOL"; + if (os.startsWith("Linux")) + { + pathList.add("/usr/local/pymol/bin/" + pymol); + pathList.add("/usr/local/bin/" + pymol); + pathList.add("/usr/bin/" + pymol); + pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol); + } + else if (os.startsWith("Windows")) + { + // todo Windows installation path(s) + } + else if (os.startsWith("Mac")) + { + pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol); + } + return pathList; + } + + public boolean isPymolLaunched() + { + // TODO pull up generic methods for external viewer processes + boolean launched = false; + if (pymolProcess != null) + { + try + { + pymolProcess.exitValue(); + // if we get here, process has ended + } catch (IllegalThreadStateException e) + { + // ok - not yet terminated + launched = true; + } + } + return launched; + } + + public void exitPymol() + { + if (isPymolLaunched() && pymolProcess != null) + { + sendCommand(new StructureCommand("quit"), false); + } + pymolProcess = null; + // currentModelsMap.clear(); + this.pymolXmlRpcPort = 0; + } + + /** + * Sends the command to Pymol; if requested, tries to get and return any + * replies, else returns null + * + * @param command + * @param getReply + * @return + */ + public List sendCommand(StructureCommandI command, + boolean getReply) + { + String postBody = getPostRequest(command); + // System.out.println(postBody);// debug + String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort; + PrintWriter out = null; + BufferedReader in = null; + List result = getReply ? new ArrayList<>() : null; + + try + { + URL realUrl = new URL(rpcUrl); + HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("content-type", "text/xml"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(postBody); + out.flush(); + int rc = conn.getResponseCode(); + if (rc != HttpURLConnection.HTTP_OK) + { + Cache.log.error( + String.format("Error status from %s: %d", rpcUrl, rc)); + return result; + } + + InputStream inputStream = conn.getInputStream(); + if (getReply) + { + in = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = in.readLine()) != null) + { + result.add(line); + } + } + } catch (SocketException e) + { + // thrown when 'quit' command is sent to PyMol + Cache.log.warn(String.format("Request to %s returned %s", rpcUrl, + e.toString())); + } catch (Exception e) + { + e.printStackTrace(); + } finally + { + if (out != null) + { + out.close(); + } + } + return result; + } + + /** + * Builds the body of the XML-RPC format POST request to execute the command + * + * @param command + * @return + */ + static String getPostRequest(StructureCommandI command) + { + StringBuilder sb = new StringBuilder(64); + sb.append(POST1).append(command.getCommand()).append(POST2); + if (command.hasParameters()) + { + for (String p : command.getParameters()) + { + /* + * for now assuming all are string - element is optional + * refactor in future if other data types needed + * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm + */ + sb.append("").append(p) + .append(""); + } + } + sb.append(POST3); + return sb.toString(); + } + + public boolean launchPymol() + { + // todo pull up much of this + // Do nothing if already launched + if (isPymolLaunched()) + { + return true; + } + + String error = "Error message: "; + for (String pymolPath : getPymolPaths()) + { + try + { + // ensure symbolic links are resolved + pymolPath = Paths.get(pymolPath).toRealPath().toString(); + File path = new File(pymolPath); + // uncomment the next line to simulate Pymol not installed + // path = new File(pymolPath + "x"); + if (!path.canExecute()) + { + error += "File '" + path + "' does not exist.\n"; + continue; + } + List args = new ArrayList<>(); + args.add(pymolPath); + args.add("-R"); // https://pymolwiki.org/index.php/RPC + ProcessBuilder pb = new ProcessBuilder(args); + pymolProcess = pb.start(); + error = ""; + break; + } catch (Exception e) + { + // pPymol could not be started using this path + error += e.getMessage(); + } + } + if (error.length() == 0) + { + this.pymolXmlRpcPort = getPortNumber(); + System.out.println( + "PyMOL XMLRPC started on port " + pymolXmlRpcPort); + return (pymolXmlRpcPort > 0); + } + + // logger.warn(error); + return false; + } + + private int getPortNumber() + { + // TODO pull up most of this! + int port = 0; + InputStream readChan = pymolProcess.getInputStream(); + BufferedReader lineReader = new BufferedReader( + new InputStreamReader(readChan)); + StringBuilder responses = new StringBuilder(); + try + { + String response = lineReader.readLine(); + while (response != null) + { + responses.append("\n" + response); + // expect: xml-rpc server running on host localhost, port 9123 + if (response.contains("xml-rpc")) + { + String[] tokens = response.split(" "); + for (int i = 0; i < tokens.length - 1; i++) + { + if ("port".equals(tokens[i])) + { + port = Integer.parseInt(tokens[i + 1]); + break; + } + } + } + if (port > 0) + { + break; // hack for hanging readLine() + } + response = lineReader.readLine(); + } + } catch (Exception e) + { + System.err.println( + "Failed to get REST port number from " + responses + ": " + + e.getMessage()); + // logger.error("Failed to get REST port number from " + responses + ": " + // + e.getMessage()); + } finally + { + try + { + lineReader.close(); + } catch (IOException e2) + { + } + } + if (port == 0) + { + System.err.println("Failed to start PyMOL with XMLRPC, response was: " + + responses); + } + System.err.println("PyMOL started with XMLRPC on port " + port); + return port; + } + +} diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java deleted file mode 100644 index 39d6704..0000000 --- a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) - * Copyright (C) $$Year-Rel$$ The Jalview Authors - * - * This file is part of Jalview. - * - * Jalview is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 - * of the License, or (at your option) any later version. - * - * Jalview is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty - * of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Jalview. If not, see . - * The Jalview Authors are detailed in the 'AUTHORS' file. - */ -package jalview.ext.rbvi.chimera; - -import jalview.util.IntRangeComparator; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** - * A class to model a Chimera atomspec pattern, for example - * - *
- * #0:15.A,28.A,54.A,63.A,70-72.A,83-84.A,97-98.A|#1:2.A,6.A,11.A,13-14.A,70.A,82.A,96-97.A
- * 
- * - * where - *
    - *
  • #0 is a model number
  • - *
  • 15 or 70-72 is a residue number, or range of residue numbers
  • - *
  • .A is a chain identifier
  • - *
  • residue ranges are separated by comma
  • - *
  • atomspecs for distinct models are separated by | (or)
  • - *
- * - *
- * @see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
- * 
- */ -public class AtomSpecModel -{ - private Map>> atomSpec; - - /** - * Constructor - */ - public AtomSpecModel() - { - atomSpec = new TreeMap>>(); - } - - /** - * Adds one contiguous range to this atom spec - * - * @param model - * @param startPos - * @param endPos - * @param chain - */ - public void addRange(int model, int startPos, int endPos, String chain) - { - /* - * Get/initialize map of data for the colour and model - */ - Map> modelData = atomSpec.get(model); - if (modelData == null) - { - atomSpec.put(model, modelData = new TreeMap>()); - } - - /* - * Get/initialize map of data for colour, model and chain - */ - List chainData = modelData.get(chain); - if (chainData == null) - { - chainData = new ArrayList(); - modelData.put(chain, chainData); - } - - /* - * Add the start/end positions - */ - chainData.add(new int[] { startPos, endPos }); - // TODO add intelligently, using a RangeList class - } - - /** - * Returns the range(s) formatted as a Chimera atomspec - * - * @return - */ - public String getAtomSpec() - { - StringBuilder sb = new StringBuilder(128); - boolean firstModel = true; - for (Integer model : atomSpec.keySet()) - { - if (!firstModel) - { - sb.append("|"); - } - firstModel = false; - sb.append("#").append(model).append(":"); - - boolean firstPositionForModel = true; - final Map> modelData = atomSpec.get(model); - - for (String chain : modelData.keySet()) - { - chain = " ".equals(chain) ? chain : chain.trim(); - - List rangeList = modelData.get(chain); - - /* - * sort ranges into ascending start position order - */ - Collections.sort(rangeList, IntRangeComparator.ASCENDING); - - int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0]; - int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1]; - - Iterator iterator = rangeList.iterator(); - while (iterator.hasNext()) - { - int[] range = iterator.next(); - if (range[0] <= end + 1) - { - /* - * range overlaps or is contiguous with the last one - * - so just extend the end position, and carry on - * (unless this is the last in the list) - */ - end = Math.max(end, range[1]); - } - else - { - /* - * we have a break so append the last range - */ - appendRange(sb, start, end, chain, firstPositionForModel); - firstPositionForModel = false; - start = range[0]; - end = range[1]; - } - } - - /* - * and append the last range - */ - if (!rangeList.isEmpty()) - { - appendRange(sb, start, end, chain, firstPositionForModel); - firstPositionForModel = false; - } - } - } - return sb.toString(); - } - - /** - * @param sb - * @param start - * @param end - * @param chain - * @param firstPositionForModel - */ - protected void appendRange(StringBuilder sb, int start, int end, - String chain, boolean firstPositionForModel) - { - if (!firstPositionForModel) - { - sb.append(","); - } - if (end == start) - { - sb.append(start); - } - else - { - sb.append(start).append("-").append(end); - } - - sb.append("."); - if (!" ".equals(chain)) { - sb.append(chain); - } - } -} diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 3caaac3..5beee56 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -20,636 +20,390 @@ */ package jalview.ext.rbvi.chimera; -import jalview.api.AlignViewportI; -import jalview.api.AlignmentViewPanel; -import jalview.api.FeatureRenderer; -import jalview.api.SequenceRenderer; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; -import jalview.datamodel.MappedFeatures; -import jalview.datamodel.SequenceFeature; -import jalview.datamodel.SequenceI; -import jalview.gui.Desktop; -import jalview.renderer.seqfeatures.FeatureColourFinder; -import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; -import jalview.structure.StructureSelectionManager; -import jalview.util.ColorUtils; -import jalview.util.Comparison; - import java.awt.Color; import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.Arrays; import java.util.List; import java.util.Map; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsBase; +import jalview.util.ColorUtils; + /** * Routines for generating Chimera commands for Jalview/Chimera binding * * @author JimP * */ -public class ChimeraCommands +public class ChimeraCommands extends StructureCommandsBase { + private static final StructureCommand SHOW_BACKBONE = new StructureCommand( + "~display all;~ribbon;chain @CA|P"); - public static final String NAMESPACE_PREFIX = "jv_"; + private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand( + "color white;color red ::ASP,GLU;color blue ::LYS,ARG;color yellow ::CYS"); + + private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand( + "rainbow chain"); + + // Chimera clause to exclude alternate locations in atom selection + private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9"; + + @Override + public StructureCommandI colourResidues(String atomSpec, Color colour) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html + String colourCode = getColourString(colour); + return new StructureCommand("color " + colourCode + " " + atomSpec); + } /** - * Constructs Chimera commands to colour residues as per the Jalview alignment + * Returns a colour formatted suitable for use in viewer command syntax * - * @param ssm - * @param files - * @param sequence - * @param sr - * @param fr - * @param viewPanel + * @param colour * @return */ - public static StructureMappingcommandSet[] getColourBySequenceCommand( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, - AlignmentViewPanel viewPanel) + protected String getColourString(Color colour) { - Map colourMap = buildColoursMap(ssm, files, - sequence, sr, viewPanel); - - List colourCommands = buildColourCommands(colourMap); - - StructureMappingcommandSet cs = new StructureMappingcommandSet( - ChimeraCommands.class, null, - colourCommands.toArray(new String[colourCommands.size()])); - - return new StructureMappingcommandSet[] { cs }; + return ColorUtils.toTkCode(colour); } /** - * Traverse the map of colours/models/chains/positions to construct a list of - * 'color' commands (one per distinct colour used). The format of each command - * is + * Traverse the map of features/values/models/chains/positions to construct a + * list of 'setattr' commands (one per distinct feature type and value). + *

+ * The format of each command is * *

-   * 
- * color colorname #modelnumber:range.chain - * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... + *
setattr r " " #modelnumber:range.chain + * e.g. setattr r jv_chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... *
*
* - * @param colourMap + * @param featureMap * @return */ - protected static List buildColourCommands( - Map colourMap) + @Override + public List setAttributes( + Map> featureMap) { - /* - * This version concatenates all commands into a single String (semi-colon - * delimited). If length limit issues arise, refactor to return one color - * command per colour. - */ - List commands = new ArrayList<>(); - StringBuilder sb = new StringBuilder(256); - boolean firstColour = true; - for (Object key : colourMap.keySet()) + List commands = new ArrayList<>(); + for (String featureType : featureMap.keySet()) { - Color colour = (Color) key; - String colourCode = ColorUtils.toTkCode(colour); - if (!firstColour) - { - sb.append("; "); - } - sb.append("color ").append(colourCode).append(" "); - firstColour = false; - final AtomSpecModel colourData = colourMap.get(colour); - sb.append(colourData.getAtomSpec()); - } - commands.add(sb.toString()); - return commands; - } + String attributeName = makeAttributeName(featureType); - /** - * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and - * builds a Chimera format atom spec - * - * @param modelAndChainRanges - */ - protected static String getAtomSpec( - Map>> modelAndChainRanges) - { - StringBuilder sb = new StringBuilder(128); - boolean firstModelForColour = true; - for (Integer model : modelAndChainRanges.keySet()) - { - boolean firstPositionForModel = true; - if (!firstModelForColour) - { - sb.append("|"); - } - firstModelForColour = false; - sb.append("#").append(model).append(":"); + /* + * clear down existing attributes for this feature + */ + // 'problem' - sets attribute to None on all residues - overkill? + // commands.add("~setattr r " + attributeName + " :*"); - final Map> modelData = modelAndChainRanges - .get(model); - for (String chain : modelData.keySet()) + Map values = featureMap.get(featureType); + for (Object value : values.keySet()) { - boolean hasChain = !"".equals(chain.trim()); - for (int[] range : modelData.get(chain)) - { - if (!firstPositionForModel) - { - sb.append(","); - } - if (range[0] == range[1]) - { - sb.append(range[0]); - } - else - { - sb.append(range[0]).append("-").append(range[1]); - } - if (hasChain) - { - sb.append(".").append(chain); - } - firstPositionForModel = false; - } + /* + * for each distinct value recorded for this feature type, + * add a command to set the attribute on the mapped residues + * Put values in single quotes, encoding any embedded single quotes + */ + AtomSpecModel atomSpecModel = values.get(value); + String featureValue = value.toString(); + featureValue = featureValue.replaceAll("\\'", "'"); + StructureCommandI cmd = setAttribute(attributeName, featureValue, + atomSpecModel); + commands.add(cmd); } } - return sb.toString(); + + return commands; } /** + * Returns a viewer command to set the given residue attribute value on + * residues specified by the AtomSpecModel, for example + * *
-   * Build a data structure which records contiguous subsequences for each colour. 
-   * From this we can easily generate the Chimera command for colour by sequence.
-   * Color
-   *     Model number
-   *         Chain
-   *             list of start/end ranges
-   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
+   * setatr res jv_chain 'primary' #1:12-34,48-55.B
    * 
+ * + * @param attributeName + * @param attributeValue + * @param atomSpecModel + * @return */ - protected static Map buildColoursMap( - StructureSelectionManager ssm, String[] files, - SequenceI[][] sequence, SequenceRenderer sr, - AlignmentViewPanel viewPanel) + protected StructureCommandI setAttribute(String attributeName, + String attributeValue, + AtomSpecModel atomSpecModel) { - FeatureRenderer fr = viewPanel.getFeatureRenderer(); - FeatureColourFinder finder = new FeatureColourFinder(fr); - AlignViewportI viewport = viewPanel.getAlignViewport(); - HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); - AlignmentI al = viewport.getAlignment(); - Map colourMap = new LinkedHashMap<>(); - Color lastColour = null; - - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); - - if (mapping == null || mapping.length < 1) - { - continue; - } - - int startPos = -1, lastPos = -1; - String lastChain = ""; - for (int s = 0; s < sequence[pdbfnum].length; s++) - { - for (int sp, m = 0; m < mapping.length; m++) - { - final SequenceI seq = sequence[pdbfnum][s]; - if (mapping[m].getSequence() == seq - && (sp = al.findIndex(seq)) > -1) - { - SequenceI asp = al.getSequenceAt(sp); - for (int r = 0; r < asp.getLength(); r++) - { - // no mapping to gaps in sequence - if (Comparison.isGap(asp.getCharAt(r))) - { - continue; - } - int pos = mapping[m].getPDBResNum(asp.findPosition(r)); - - if (pos < 1 || pos == lastPos) - { - continue; - } - - Color colour = sr.getResidueColour(seq, r, finder); - - /* - * darker colour for hidden regions - */ - if (!cs.isVisible(r)) - { - colour = Color.GRAY; - } - - final String chain = mapping[m].getChain(); - - /* - * Just keep incrementing the end position for this colour range - * _unless_ colour, PDB model or chain has changed, or there is a - * gap in the mapped residue sequence - */ - final boolean newColour = !colour.equals(lastColour); - final boolean nonContig = lastPos + 1 != pos; - final boolean newChain = !chain.equals(lastChain); - if (newColour || nonContig || newChain) - { - if (startPos != -1) - { - addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, - lastPos, lastChain); - } - startPos = pos; - } - lastColour = colour; - lastPos = pos; - lastChain = chain; - } - // final colour range - if (lastColour != null) - { - addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, - lastPos, lastChain); - } - // break; - } - } - } - } - return colourMap; + StringBuilder sb = new StringBuilder(128); + sb.append("setattr res ").append(attributeName).append(" '") + .append(attributeValue).append("' "); + sb.append(getAtomSpec(atomSpecModel, false)); + return new StructureCommand(sb.toString()); } /** - * Helper method to add one contiguous range to the AtomSpec model for the given - * value (creating the model if necessary). As used by Jalview, {@code value} is - *
    - *
  • a colour, when building a 'colour structure by sequence' command
  • - *
  • a feature value, when building a 'set Chimera attributes from features' - * command
  • - *
+ * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied + * for a 'Jalview' namespace, and any non-alphanumeric character is converted + * to an underscore. * - * @param map - * @param value - * @param model - * @param startPos - * @param endPos - * @param chain + * @param featureType + * @return + * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html */ - protected static void addAtomSpecRange(Map map, - Object value, int model, int startPos, int endPos, String chain) + @Override + protected String makeAttributeName(String featureType) { + String attName = super.makeAttributeName(featureType); + /* - * Get/initialize map of data for the colour + * Chimera treats an attribute name ending in 'color' as colour-valued; + * Jalview doesn't, so prevent this by appending an underscore */ - AtomSpecModel atomSpec = map.get(value); - if (atomSpec == null) + if (attName.toUpperCase().endsWith("COLOR")) { - atomSpec = new AtomSpecModel(); - map.put(value, atomSpec); + attName += "_"; } - atomSpec.addRange(model, startPos, endPos, chain); + return attName; } - /** - * Constructs and returns Chimera commands to set attributes on residues - * corresponding to features in Jalview. Attribute names are the Jalview - * feature type, with a "jv_" prefix. - * - * @param ssm - * @param files - * @param seqs - * @param viewPanel - * @return - */ - public static StructureMappingcommandSet getSetAttributeCommandsForFeatures( - StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, - AlignmentViewPanel viewPanel) + @Override + public StructureCommandI colourByChain() { - Map> featureMap = buildFeaturesMap( - ssm, files, seqs, viewPanel); - - List commands = buildSetAttributeCommands(featureMap); - - StructureMappingcommandSet cs = new StructureMappingcommandSet( - ChimeraCommands.class, null, - commands.toArray(new String[commands.size()])); + return COLOUR_BY_CHAIN; + } - return cs; + @Override + public List colourByCharge() + { + return Arrays.asList(COLOUR_BY_CHARGE); } - /** - *
-   * Helper method to build a map of 
-   *   { featureType, { feature value, AtomSpecModel } }
-   * 
- * - * @param ssm - * @param files - * @param seqs - * @param viewPanel - * @return - */ - protected static Map> buildFeaturesMap( - StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, - AlignmentViewPanel viewPanel) + @Override + public String getResidueSpec(String residue) { - Map> theMap = new LinkedHashMap<>(); + return "::" + residue; + } - FeatureRenderer fr = viewPanel.getFeatureRenderer(); - if (fr == null) - { - return theMap; - } + @Override + public StructureCommandI setBackgroundColour(Color col) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor + return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col)); + } - AlignViewportI viewport = viewPanel.getAlignViewport(); - List visibleFeatures = fr.getDisplayedFeatureTypes(); + @Override + public StructureCommandI focusView() + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html + return new StructureCommand("focus"); + } + @Override + public List showChains(List toShow) + { /* - * if alignment is showing features from complement, we also transfer - * these features to the corresponding mapped structure residues + * Construct a chimera command like + * + * ~display #*;~ribbon #*;ribbon :.A,:.B */ - boolean showLinkedFeatures = viewport.isShowComplementFeatures(); - List complementFeatures = new ArrayList<>(); - FeatureRenderer complementRenderer = null; - if (showLinkedFeatures) + StringBuilder cmd = new StringBuilder(64); + boolean first = true; + for (String chain : toShow) { - AlignViewportI comp = fr.getViewport().getCodingComplement(); - if (comp != null) + String[] tokens = chain.split(":"); + if (tokens.length == 2) { - complementRenderer = Desktop.getAlignFrameFor(comp) - .getFeatureRenderer(); - complementFeatures = complementRenderer.getDisplayedFeatureTypes(); + String showChainCmd = tokens[0] + ":." + tokens[1]; + if (!first) + { + cmd.append(","); + } + cmd.append(showChainCmd); + first = false; } } - if (visibleFeatures.isEmpty() && complementFeatures.isEmpty()) - { - return theMap; - } - AlignmentI alignment = viewPanel.getAlignment(); - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); + /* + * could append ";focus" to this command to resize the display to fill the + * window, but it looks more helpful not to (easier to relate chains to the + * whole) + */ + final String command = "~display #*; ~ribbon #*; ribbon :" + + cmd.toString(); + return Arrays.asList(new StructureCommand(command)); + } - if (mapping == null || mapping.length < 1) - { - continue; - } + @Override + public List superposeStructures(AtomSpecModel ref, + AtomSpecModel spec) + { + /* + * Form Chimera match command to match spec to ref + * (the first set of atoms are moved on to the second) + * + * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA + * + * @see https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html + */ + StringBuilder cmd = new StringBuilder(); + String atomSpecAlphaOnly = getAtomSpec(spec, true); + String refSpecAlphaOnly = getAtomSpec(ref, true); + cmd.append("match ").append(atomSpecAlphaOnly).append(" ").append(refSpecAlphaOnly); - for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++) - { - for (int m = 0; m < mapping.length; m++) - { - final SequenceI seq = seqs[pdbfnum][seqNo]; - int sp = alignment.findIndex(seq); - StructureMapping structureMapping = mapping[m]; - if (structureMapping.getSequence() == seq && sp > -1) - { - /* - * found a sequence with a mapping to a structure; - * now scan its features - */ - if (!visibleFeatures.isEmpty()) - { - scanSequenceFeatures(visibleFeatures, structureMapping, seq, - theMap, pdbfnum); - } - if (showLinkedFeatures) - { - scanComplementFeatures(complementRenderer, structureMapping, - seq, theMap, pdbfnum); - } - } - } - } - } - return theMap; + /* + * show superposed residues as ribbon + */ + String atomSpec = getAtomSpec(spec, false); + String refSpec = getAtomSpec(ref, false); + cmd.append("; ribbon "); + cmd.append(atomSpec).append("|").append(refSpec).append("; focus"); + + return Arrays.asList(new StructureCommand(cmd.toString())); + } + + @Override + public StructureCommandI openCommandFile(String path) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html + return new StructureCommand("open cmd:" + path); + } + + @Override + public StructureCommandI saveSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html + return new StructureCommand("save " + filepath); } /** - * Scans visible features in mapped positions of the CDS/peptide complement, and - * adds any found to the map of attribute values/structure positions + * Returns the range(s) modelled by {@code atomSpec} formatted as a Chimera + * atomspec string, e.g. * - * @param complementRenderer - * @param structureMapping - * @param seq - * @param theMap - * @param modelNumber + *
+   * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
+   * 
+ * + * where + *
    + *
  • #0 is a model number
  • + *
  • 15 or 70-72 is a residue number, or range of residue numbers
  • + *
  • .A is a chain identifier
  • + *
  • residue ranges are separated by comma
  • + *
  • atomspecs for distinct models are separated by | (or)
  • + *
+ * + *
+   * 
+   * @param model
+   * @param alphaOnly
+   * @return
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
    */
-  protected static void scanComplementFeatures(
-          FeatureRenderer complementRenderer,
-          StructureMapping structureMapping, SequenceI seq,
-          Map> theMap, int modelNumber)
+  @Override
+  public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
   {
-    /*
-     * for each sequence residue mapped to a structure position...
-     */
-    for (int seqPos : structureMapping.getMapping().keySet())
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (String model : atomSpec.getModels())
     {
-      /*
-       * find visible complementary features at mapped position(s)
-       */
-      MappedFeatures mf = complementRenderer
-              .findComplementFeaturesAtResidue(seq, seqPos);
-      if (mf != null)
+      if (!firstModel)
       {
-        for (SequenceFeature sf : mf.features)
-        {
-          String type = sf.getType();
-
-          /*
-           * Don't copy features which originated from Chimera
-           */
-          if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-                  .equals(sf.getFeatureGroup()))
-          {
-            continue;
-          }
-
-          /*
-           * record feature 'value' (score/description/type) as at the
-           * corresponding structure position
-           */
-          List mappedRanges = structureMapping
-                  .getPDBResNumRanges(seqPos, seqPos);
-
-          if (!mappedRanges.isEmpty())
-          {
-            String value = sf.getDescription();
-            if (value == null || value.length() == 0)
-            {
-              value = type;
-            }
-            float score = sf.getScore();
-            if (score != 0f && !Float.isNaN(score))
-            {
-              value = Float.toString(score);
-            }
-            Map featureValues = theMap.get(type);
-            if (featureValues == null)
-            {
-              featureValues = new HashMap<>();
-              theMap.put(type, featureValues);
-            }
-            for (int[] range : mappedRanges)
-            {
-              addAtomSpecRange(featureValues, value, modelNumber, range[0],
-                      range[1], structureMapping.getChain());
-            }
-          }
-        }
+        sb.append("|");
       }
+      firstModel = false;
+      appendModel(sb, model, atomSpec, alphaOnly);
     }
+    return sb.toString();
   }
 
   /**
-   * Inspect features on the sequence; for each feature that is visible, determine
-   * its mapped ranges in the structure (if any) according to the given mapping,
-   * and add them to the map.
+   * A helper method to append an atomSpec string for atoms in the given model
    * 
-   * @param visibleFeatures
-   * @param mapping
-   * @param seq
-   * @param theMap
-   * @param modelNumber
+   * @param sb
+   * @param model
+   * @param atomSpec
+   * @param alphaOnly
    */
-  protected static void scanSequenceFeatures(List visibleFeatures,
-          StructureMapping mapping, SequenceI seq,
-          Map> theMap, int modelNumber)
+  protected void appendModel(StringBuilder sb, String model,
+          AtomSpecModel atomSpec, boolean alphaOnly)
   {
-    List sfs = seq.getFeatures().getPositionalFeatures(
-            visibleFeatures.toArray(new String[visibleFeatures.size()]));
-    for (SequenceFeature sf : sfs)
-    {
-      String type = sf.getType();
+    sb.append("#").append(model).append(":");
 
-      /*
-       * Don't copy features which originated from Chimera
-       */
-      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-              .equals(sf.getFeatureGroup()))
-      {
-        continue;
-      }
+    boolean firstPositionForModel = true;
 
-      List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
-              sf.getEnd());
+    for (String chain : atomSpec.getChains(model))
+    {
+      chain = " ".equals(chain) ? chain : chain.trim();
 
-      if (!mappedRanges.isEmpty())
+      List rangeList = atomSpec.getRanges(model, chain);
+      for (int[] range : rangeList)
       {
-        String value = sf.getDescription();
-        if (value == null || value.length() == 0)
-        {
-          value = type;
-        }
-        float score = sf.getScore();
-        if (score != 0f && !Float.isNaN(score))
-        {
-          value = Float.toString(score);
-        }
-        Map featureValues = theMap.get(type);
-        if (featureValues == null)
-        {
-          featureValues = new HashMap<>();
-          theMap.put(type, featureValues);
-        }
-        for (int[] range : mappedRanges)
-        {
-          addAtomSpecRange(featureValues, value, modelNumber, range[0],
-                  range[1], mapping.getChain());
-        }
+        appendRange(sb, range[0], range[1], chain, firstPositionForModel,
+                false);
+        firstPositionForModel = false;
       }
     }
-  }
-
-  /**
-   * Traverse the map of features/values/models/chains/positions to construct a
-   * list of 'setattr' commands (one per distinct feature type and value).
-   * 

- * The format of each command is - * - *

-   * 
setattr r " " #modelnumber:range.chain - * e.g. setattr r jv:chain #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,... - *
- *
- * - * @param featureMap - * @return - */ - protected static List buildSetAttributeCommands( - Map> featureMap) - { - List commands = new ArrayList<>(); - for (String featureType : featureMap.keySet()) + if (alphaOnly) { - String attributeName = makeAttributeName(featureType); - /* - * clear down existing attributes for this feature + * restrict to alpha carbon, no alternative locations + * (needed to ensuring matching atom counts for superposition) */ - // 'problem' - sets attribute to None on all residues - overkill? - // commands.add("~setattr r " + attributeName + " :*"); - - Map values = featureMap.get(featureType); - for (Object value : values.keySet()) - { - /* - * for each distinct value recorded for this feature type, - * add a command to set the attribute on the mapped residues - * Put values in single quotes, encoding any embedded single quotes - */ - StringBuilder sb = new StringBuilder(128); - String featureValue = value.toString(); - featureValue = featureValue.replaceAll("\\'", "'"); - sb.append("setattr r ").append(attributeName).append(" '") - .append(featureValue).append("' "); - sb.append(values.get(value).getAtomSpec()); - commands.add(sb.toString()); - } + // TODO @P instead if RNA - add nucleotide flag to AtomSpecModel? + sb.append("@CA").append(NO_ALTLOCS); } + } - return commands; + @Override + public List showBackbone() + { + return Arrays.asList(SHOW_BACKBONE); + } + + @Override + public StructureCommandI loadFile(String file) + { + return new StructureCommand("open " + file); } /** - * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied - * for a 'Jalview' namespace, and any non-alphanumeric character is converted - * to an underscore. - * - * @param featureType - * @return - * - *
-   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
-   *         
+ * Overrides the default method to concatenate colour commands into one */ - protected static String makeAttributeName(String featureType) + @Override + public List colourBySequence( + Map colourMap) { - StringBuilder sb = new StringBuilder(); - if (featureType != null) + List commands = new ArrayList<>(); + StringBuilder sb = new StringBuilder(colourMap.size() * 20); + boolean first = true; + for (Object key : colourMap.keySet()) { - for (char c : featureType.toCharArray()) + Color colour = (Color) key; + final AtomSpecModel colourData = colourMap.get(colour); + StructureCommandI command = getColourCommand(colourData, colour); + if (!first) { - sb.append(Character.isLetterOrDigit(c) ? c : '_'); + sb.append(getCommandSeparator()); } + first = false; + sb.append(command.getCommand()); } - String attName = NAMESPACE_PREFIX + sb.toString(); - /* - * Chimera treats an attribute name ending in 'color' as colour-valued; - * Jalview doesn't, so prevent this by appending an underscore - */ - if (attName.toUpperCase().endsWith("COLOR")) - { - attName += "_"; - } + commands.add(new StructureCommand(sb.toString())); + return commands; + } - return attName; + @Override + public StructureCommandI openSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html + // this version of the command has no dependency on file extension + return new StructureCommand("open chimera:" + filepath); } } diff --git a/src/jalview/ext/rbvi/chimera/ChimeraListener.java b/src/jalview/ext/rbvi/chimera/ChimeraListener.java index a0d74bc..40b0ff0 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraListener.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraListener.java @@ -114,17 +114,25 @@ public class ChimeraListener extends AbstractRequestHandler { // dumpRequest(request); String message = request.getParameter(CHIMERA_NOTIFICATION); - if (SELECTION_CHANGED.equals(message)) + if (message == null) { - this.chimeraBinding.highlightChimeraSelection(); + message = request.getParameter("chimerax_notification"); } - else if (message != null && message.startsWith(MODEL_CHANGED)) + if (message != null) { - processModelChanged(message.substring(MODEL_CHANGED.length())); - } - else - { - System.err.println("Unexpected chimeraNotification: " + message); + if (message.startsWith("SelectionChanged")) + { + this.chimeraBinding.highlightChimeraSelection(); + } + else if (message.startsWith(MODEL_CHANGED)) + { + System.err.println(message); + processModelChanged(message.substring(MODEL_CHANGED.length())); + } + else + { + System.err.println("Unexpected chimeraNotification: " + message); + } } } diff --git a/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java new file mode 100644 index 0000000..a596da9 --- /dev/null +++ b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java @@ -0,0 +1,233 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ +package jalview.ext.rbvi.chimera; + +import java.awt.Color; +import java.util.Arrays; +import java.util.List; + +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.util.ColorUtils; + +/** + * Routines for generating ChimeraX commands for Jalview/ChimeraX binding + */ +public class ChimeraXCommands extends ChimeraCommands +{ + private static final StructureCommand SHOW_BACKBONE = new StructureCommand( + "~display all;~ribbon;show @CA|P atoms"); + + private static final StructureCommand FOCUS_VIEW = new StructureCommand( + "view"); + + private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand( + "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow"); + + @Override + public List colourByCharge() + { + return Arrays.asList(COLOUR_BY_CHARGE); + } + + @Override + public String getResidueSpec(String residue) + { + return ":" + residue; + } + + @Override + public StructureCommandI setBackgroundColour(Color col) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/set.html + return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col)); + } + + @Override + public StructureCommandI colourResidues(String atomSpec, Color colour) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/color.html + String colourCode = getColourString(colour); + + return new StructureCommand("color " + atomSpec + " " + colourCode); + } + + @Override + public StructureCommandI focusView() + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/view.html + return FOCUS_VIEW; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public int getModelStartNo() + { + return 1; + } + + /** + * Returns a viewer command to set the given residue attribute value on + * residues specified by the AtomSpecModel, for example + * + *
+   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
+   * 
+ * + * @param attributeName + * @param attributeValue + * @param atomSpecModel + * @return + */ + @Override + protected StructureCommandI setAttribute(String attributeName, + String attributeValue, AtomSpecModel atomSpecModel) + { + StringBuilder sb = new StringBuilder(128); + sb.append("setattr ").append(getAtomSpec(atomSpecModel, false)); + sb.append(" res ").append(attributeName).append(" '") + .append(attributeValue).append("'"); + sb.append(" create true"); + return new StructureCommand(sb.toString()); + } + + @Override + public StructureCommandI openCommandFile(String path) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html + return new StructureCommand("open " + path); + } + + @Override + public StructureCommandI saveSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html + // note ChimeraX will append ".cxs" to the filepath! + return new StructureCommand("save " + filepath + " format session"); + } + + /** + * Returns the range(s) formatted as a ChimeraX atomspec, for example + *

+ * #1/A:2-20,30-40/B:10-20|#2/A:12-30 + * + * @return + */ + @Override + public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly) + { + StringBuilder sb = new StringBuilder(128); + boolean firstModel = true; + for (String model : atomSpec.getModels()) + { + if (!firstModel) + { + sb.append("|"); + } + firstModel = false; + appendModel(sb, model, atomSpec); + if (alphaOnly) + { + // TODO @P if RNA - add nucleotide flag to AtomSpecModel? + sb.append("@CA"); + } + // todo: is there ChimeraX syntax to exclude altlocs? + } + return sb.toString(); + } + + /** + * A helper method to append an atomSpec string for atoms in the given model + * + * @param sb + * @param model + * @param atomSpec + */ + protected void appendModel(StringBuilder sb, String model, + AtomSpecModel atomSpec) + { + sb.append("#").append(model); + + for (String chain : atomSpec.getChains(model)) + { + boolean firstPositionForChain = true; + sb.append("/").append(chain.trim()).append(":"); + List rangeList = atomSpec.getRanges(model, chain); + boolean first = true; + for (int[] range : rangeList) + { + if (!first) + { + sb.append(","); + } + first = false; + appendRange(sb, range[0], range[1], chain, firstPositionForChain, + true); + } + } + } + + @Override + public List showBackbone() + { + return Arrays.asList(SHOW_BACKBONE); + } + + @Override + public List superposeStructures(AtomSpecModel ref, + AtomSpecModel spec) + { + /* + * Form ChimeraX match command to match spec to ref + * + * match #1/A:2-94 toAtoms #2/A:1-93 + * + * @see https://www.cgl.ucsf.edu/chimerax/docs/user/commands/align.html + */ + StringBuilder cmd = new StringBuilder(); + String atomSpec = getAtomSpec(spec, true); + String refSpec = getAtomSpec(ref, true); + cmd.append("align ").append(atomSpec).append(" toAtoms ") + .append(refSpec); + + /* + * show superposed residues as ribbon, others as chain + */ + cmd.append("; ribbon "); + cmd.append(getAtomSpec(spec, false)).append("|"); + cmd.append(getAtomSpec(ref, false)).append("; view"); + + return Arrays.asList(new StructureCommand(cmd.toString())); + } + + @Override + public StructureCommandI openSession(String filepath) + { + // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html#composite + // this version of the command has no dependency on file extension + return new StructureCommand("open " + filepath + " format session"); + } +} diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java index 00446f2..460b156 100644 --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java @@ -20,37 +20,14 @@ */ package jalview.ext.rbvi.chimera; -import jalview.api.AlignmentViewPanel; -import jalview.api.SequenceRenderer; -import jalview.api.structures.JalviewStructureDisplayI; -import jalview.bin.Cache; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SearchResultMatchI; -import jalview.datamodel.SearchResultsI; -import jalview.datamodel.SequenceFeature; -import jalview.datamodel.SequenceI; -import jalview.httpserver.AbstractRequestHandler; -import jalview.io.DataSourceType; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ResidueProperties; -import jalview.structure.AtomSpec; -import jalview.structure.StructureMappingcommandSet; -import jalview.structure.StructureSelectionManager; -import jalview.structures.models.AAStructureBindingModel; -import jalview.util.MessageManager; - -import java.awt.Color; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.BindException; import java.util.ArrayList; -import java.util.BitSet; import java.util.Collections; -import java.util.Hashtable; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -59,31 +36,35 @@ import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager; import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel; import ext.edu.ucsf.rbvi.strucviz2.StructureManager; import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; +import jalview.api.AlignmentViewPanel; +import jalview.api.structures.JalviewStructureDisplayI; +import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SearchResultMatchI; +import jalview.datamodel.SearchResultsI; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.gui.StructureViewer.ViewerType; +import jalview.httpserver.AbstractRequestHandler; +import jalview.io.DataSourceType; +import jalview.structure.AtomSpec; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureSelectionManager; +import jalview.structures.models.AAStructureBindingModel; public abstract class JalviewChimeraBinding extends AAStructureBindingModel { - public static final String CHIMERA_FEATURE_GROUP = "Chimera"; - - // Chimera clause to exclude alternate locations in atom selection - private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9"; - - private static final String COLOURING_CHIMERA = MessageManager - .getString("status.colouring_chimera"); - - private static final boolean debug = false; - - private static final String PHOSPHORUS = "P"; - - private static final String ALPHACARBON = "CA"; + public static final String CHIMERA_SESSION_EXTENSION = ".py"; - private List chainNames = new ArrayList(); - - private Hashtable chainFile = new Hashtable(); + public static final String CHIMERA_FEATURE_GROUP = "Chimera"; /* * Object through which we talk to Chimera */ - private ChimeraManager viewer; + private ChimeraManager chimeraManager; /* * Object which listens to Chimera notifications @@ -91,39 +72,19 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel private AbstractRequestHandler chimeraListener; /* - * set if chimera state is being restored from some source - instructs binding - * not to apply default display style when structure set is updated for first - * time. - */ - private boolean loadingFromArchive = false; - - /* - * flag to indicate if the Chimera viewer should ignore sequence colouring - * events from the structure manager because the GUI is still setting up - */ - private boolean loadingFinished = true; - - /* * Map of ChimeraModel objects keyed by PDB full local file name */ - private Map> chimeraMaps = new LinkedHashMap>(); + protected Map> chimeraMaps = new LinkedHashMap<>(); String lastHighlightCommand; - /* - * incremented every time a load notification is successfully handled - - * lightweight mechanism for other threads to detect when they can start - * referring to new structures. - */ - private long loadNotifiesHandled = 0; - private Thread chimeraMonitor; /** * Open a PDB structure file in Chimera and set up mappings from Jalview. * - * We check if the PDB model id is already loaded in Chimera, if so don't - * reopen it. This is the case if Chimera has opened a saved session file. + * We check if the PDB model id is already loaded in Chimera, if so don't reopen + * it. This is the case if Chimera has opened a saved session file. * * @param pe * @return @@ -133,8 +94,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel String file = pe.getFile(); try { - List modelsToMap = new ArrayList(); - List oldList = viewer.getModelList(); + List modelsToMap = new ArrayList<>(); + List oldList = chimeraManager.getModelList(); boolean alreadyOpen = false; /* @@ -155,16 +116,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ if (!alreadyOpen) { - viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL); - List newList = viewer.getModelList(); - // JAL-1728 newList.removeAll(oldList) does not work - for (ChimeraModel cm : newList) - { - if (cm.getModelName().equals(pe.getId())) - { - modelsToMap.add(cm); - } - } + chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL); + addChimeraModel(pe, modelsToMap); } chimeraMaps.put(file, modelsToMap); @@ -184,6 +137,31 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** + * Adds the ChimeraModel corresponding to the given PDBEntry, based on model + * name matching PDB id + * + * @param pe + * @param modelsToMap + */ + protected void addChimeraModel(PDBEntry pe, + List modelsToMap) + { + /* + * Chimera: query for actual models and find the one with + * matching model name - already set in viewer.openModel() + */ + List newList = chimeraManager.getModelList(); + // JAL-1728 newList.removeAll(oldList) does not work + for (ChimeraModel cm : newList) + { + if (cm.getModelName().equals(pe.getId())) + { + modelsToMap.add(cm); + } + } + } + + /** * Constructor * * @param ssm @@ -196,17 +174,25 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel DataSourceType protocol) { super(ssm, pdbentry, sequenceIs, protocol); - viewer = new ChimeraManager(new StructureManager(true)); + chimeraManager = new ChimeraManager(new StructureManager(true)); + chimeraManager.setChimeraX(ViewerType.CHIMERAX.equals(getViewerType())); + setStructureCommands(new ChimeraCommands()); + } + + @Override + protected ViewerType getViewerType() + { + return ViewerType.CHIMERA; } /** - * Starts a thread that waits for the Chimera process to finish, so that we - * can then close the associated resources. This avoids leaving orphaned - * Chimera viewer panels in Jalview if the user closes Chimera. + * Starts a thread that waits for the Chimera process to finish, so that we can + * then close the associated resources. This avoids leaving orphaned Chimera + * viewer panels in Jalview if the user closes Chimera. */ protected void startChimeraProcessMonitor() { - final Process p = viewer.getChimeraProcess(); + final Process p = chimeraManager.getChimeraProcess(); chimeraMonitor = new Thread(new Runnable() { @@ -231,15 +217,15 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Start a dedicated HttpServer to listen for Chimera notifications, and tell - * it to start listening + * Start a dedicated HttpServer to listen for Chimera notifications, and tell it + * to start listening */ public void startChimeraListener() { try { chimeraListener = new ChimeraListener(this); - viewer.startListening(chimeraListener.getUri()); + chimeraManager.startListening(chimeraListener.getUri()); } catch (BindException e) { System.err.println( @@ -248,308 +234,28 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Tells Chimera to display only the specified chains - * - * @param toshow - */ - public void showChains(List toshow) - { - /* - * Construct a chimera command like - * - * ~display #*;~ribbon #*;ribbon :.A,:.B - */ - StringBuilder cmd = new StringBuilder(64); - boolean first = true; - for (String chain : toshow) - { - int modelNumber = getModelNoForChain(chain); - String showChainCmd = modelNumber == -1 ? "" - : modelNumber + ":." + chain.split(":")[1]; - if (!first) - { - cmd.append(","); - } - cmd.append(showChainCmd); - first = false; - } - - /* - * could append ";focus" to this command to resize the display to fill the - * window, but it looks more helpful not to (easier to relate chains to the - * whole) - */ - final String command = "~display #*; ~ribbon #*; ribbon :" - + cmd.toString(); - sendChimeraCommand(command, false); - } - - /** * Close down the Jalview viewer and listener, and (optionally) the associated * Chimera window. */ + @Override public void closeViewer(boolean closeChimera) { - getSsm().removeStructureViewerListener(this, this.getStructureFiles()); + super.closeViewer(closeChimera); if (closeChimera) { - viewer.exitChimera(); + chimeraManager.exitChimera(); } if (this.chimeraListener != null) { chimeraListener.shutdown(); chimeraListener = null; } - viewer = null; + chimeraManager = null; if (chimeraMonitor != null) { chimeraMonitor.interrupt(); } - releaseUIResources(); - } - - @Override - public void colourByChain() - { - colourBySequence = false; - sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA); - } - - /** - * Constructs and sends a Chimera command to colour by charge - *

    - *
  • Aspartic acid and Glutamic acid (negative charge) red
  • - *
  • Lysine and Arginine (positive charge) blue
  • - *
  • Cysteine - yellow
  • - *
  • all others - white
  • - *
- */ - @Override - public void colourByCharge() - { - colourBySequence = false; - String command = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS"; - sendAsynchronousCommand(command, COLOURING_CHIMERA); - } - - /** - * {@inheritDoc} - */ - @Override - public String superposeStructures(AlignmentI[] _alignment, - int[] _refStructure, HiddenColumns[] _hiddenCols) - { - StringBuilder allComs = new StringBuilder(128); - String[] files = getStructureFiles(); - - if (!waitForFileLoad(files)) - { - return null; - } - - refreshPdbEntries(); - StringBuilder selectioncom = new StringBuilder(256); - for (int a = 0; a < _alignment.length; a++) - { - int refStructure = _refStructure[a]; - AlignmentI alignment = _alignment[a]; - HiddenColumns hiddenCols = _hiddenCols[a]; - - if (refStructure >= files.length) - { - System.err.println("Ignoring invalid reference structure value " - + refStructure); - refStructure = -1; - } - - /* - * 'matched' bit i will be set for visible alignment columns i where - * all sequences have a residue with a mapping to the PDB structure - */ - BitSet matched = new BitSet(); - for (int m = 0; m < alignment.getWidth(); m++) - { - if (hiddenCols == null || hiddenCols.isVisible(m)) - { - matched.set(m); - } - } - - SuperposeData[] structures = new SuperposeData[files.length]; - for (int f = 0; f < files.length; f++) - { - structures[f] = new SuperposeData(alignment.getWidth()); - } - - /* - * Calculate the superposable alignment columns ('matched'), and the - * corresponding structure residue positions (structures.pdbResNo) - */ - int candidateRefStructure = findSuperposableResidues(alignment, - matched, structures); - if (refStructure < 0) - { - /* - * If no reference structure was specified, pick the first one that has - * a mapping in the alignment - */ - refStructure = candidateRefStructure; - } - - int nmatched = matched.cardinality(); - if (nmatched < 4) - { - return MessageManager.formatMessage("label.insufficient_residues", - nmatched); - } - - /* - * Generate select statements to select regions to superimpose structures - */ - String[] selcom = new String[files.length]; - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - String chainCd = "." + structures[pdbfnum].chain; - int lpos = -1; - boolean run = false; - StringBuilder molsel = new StringBuilder(); - - int nextColumnMatch = matched.nextSetBit(0); - while (nextColumnMatch != -1) - { - int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch]; - if (lpos != pdbResNum - 1) - { - /* - * discontiguous - append last residue now - */ - if (lpos != -1) - { - molsel.append(String.valueOf(lpos)); - molsel.append(chainCd); - molsel.append(","); - } - run = false; - } - else - { - /* - * extending a contiguous run - */ - if (!run) - { - /* - * start the range selection - */ - molsel.append(String.valueOf(lpos)); - molsel.append("-"); - } - run = true; - } - lpos = pdbResNum; - nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); - } - - /* - * and terminate final selection - */ - if (lpos != -1) - { - molsel.append(String.valueOf(lpos)); - molsel.append(chainCd); - } - if (molsel.length() > 1) - { - selcom[pdbfnum] = molsel.toString(); - selectioncom.append("#").append(String.valueOf(pdbfnum)) - .append(":"); - selectioncom.append(selcom[pdbfnum]); - selectioncom.append(" "); - if (pdbfnum < files.length - 1) - { - selectioncom.append("| "); - } - } - else - { - selcom[pdbfnum] = null; - } - } - - StringBuilder command = new StringBuilder(256); - for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) - { - if (pdbfnum == refStructure || selcom[pdbfnum] == null - || selcom[refStructure] == null) - { - continue; - } - if (command.length() > 0) - { - command.append(";"); - } - - /* - * Form Chimera match command, from the 'new' structure to the - * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons): - * - * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA - * - * @see - * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html - */ - command.append("match ").append(getModelSpec(pdbfnum)).append(":"); - command.append(selcom[pdbfnum]); - command.append("@").append( - structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON); - // JAL-1757 exclude alternate CA locations - command.append(NO_ALTLOCS); - command.append(" ").append(getModelSpec(refStructure)).append(":"); - command.append(selcom[refStructure]); - command.append("@").append( - structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON); - command.append(NO_ALTLOCS); - } - if (selectioncom.length() > 0) - { - if (debug) - { - System.out.println("Select regions:\n" + selectioncom.toString()); - System.out.println( - "Superimpose command(s):\n" + command.toString()); - } - allComs.append("~display all; chain @CA|P; ribbon ") - .append(selectioncom.toString()) - .append(";" + command.toString()); - } - } - - String error = null; - if (selectioncom.length() > 0) - { - // TODO: visually distinguish regions that were superposed - if (selectioncom.substring(selectioncom.length() - 1).equals("|")) - { - selectioncom.setLength(selectioncom.length() - 1); - } - if (debug) - { - System.out.println("Select regions:\n" + selectioncom.toString()); - } - allComs.append("; ~display all; chain @CA|P; ribbon ") - .append(selectioncom.toString()).append("; focus"); - List chimeraReplies = sendChimeraCommand(allComs.toString(), - true); - for (String reply : chimeraReplies) - { - if (reply.toLowerCase().contains("unequal numbers of atoms")) - { - error = reply; - } - } - } - return error; } /** @@ -569,7 +275,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { if (pdbfnum < 0 || pdbfnum >= getPdbCount()) { - return ""; + return "#" + pdbfnum; // temp hack for ChimeraX } /* @@ -592,13 +298,12 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ public boolean launchChimera() { - if (viewer.isChimeraLaunched()) + if (chimeraManager.isChimeraLaunched()) { return true; } - boolean launched = viewer - .launchChimera(StructureManager.getChimeraPaths()); + boolean launched = chimeraManager.launchChimera(getChimeraPaths()); if (launched) { startChimeraProcessMonitor(); @@ -611,143 +316,61 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** + * Returns a list of candidate paths to the Chimera program executable + * + * @return + */ + protected List getChimeraPaths() + { + return StructureManager.getChimeraPaths(false); + } + + /** * Answers true if the Chimera process is still running, false if ended or not * started. * * @return */ - public boolean isChimeraRunning() + @Override + public boolean isViewerRunning() { - return viewer.isChimeraLaunched(); + return chimeraManager.isChimeraLaunched(); } /** * Send a command to Chimera, and optionally log and return any responses. - *

- * Does nothing, and returns null, if the command is the same as the last one - * sent [why?]. * * @param command * @param getResponse */ - public List sendChimeraCommand(final String command, + @Override + public List executeCommand(final StructureCommandI command, boolean getResponse) { - if (viewer == null) + if (chimeraManager == null || command == null) { // ? thread running after viewer shut down return null; } List reply = null; - viewerCommandHistory(false); - if (true /*lastCommand == null || !lastCommand.equals(command)*/) + // trim command or it may never find a match in the replyLog!! + String cmd = command.getCommand().trim(); + List lastReply = chimeraManager + .sendChimeraCommand(cmd, getResponse); + if (getResponse) { - // trim command or it may never find a match in the replyLog!! - List lastReply = viewer.sendChimeraCommand(command.trim(), - getResponse); - if (getResponse) - { - reply = lastReply; - if (debug) - { - log("Response from command ('" + command + "') was:\n" - + lastReply); - } - } + reply = lastReply; + Cache.log.debug( + "Response from command ('" + cmd + "') was:\n" + lastReply); } - viewerCommandHistory(true); return reply; } - /** - * Send a Chimera command asynchronously in a new thread. If the progress - * message is not null, display this message while the command is executing. - * - * @param command - * @param progressMsg - */ - protected abstract void sendAsynchronousCommand(String command, - String progressMsg); - - /** - * Sends a set of colour commands to the structure viewer - * - * @param colourBySequenceCommands - */ - @Override - protected void colourBySequence( - StructureMappingcommandSet[] colourBySequenceCommands) - { - for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands) - { - for (String command : cpdbbyseq.commands) - { - sendAsynchronousCommand(command, COLOURING_CHIMERA); - } - } - } - - /** - * @param files - * @param sr - * @param viewPanel - * @return - */ - @Override - protected StructureMappingcommandSet[] getColourBySequenceCommands( - String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel) - { - return ChimeraCommands.getColourBySequenceCommand(getSsm(), files, - getSequence(), sr, viewPanel); - } - - /** - * @param command - */ - protected void executeWhenReady(String command) - { - waitForChimera(); - sendChimeraCommand(command, false); - waitForChimera(); - } - - private void waitForChimera() - { - while (viewer != null && viewer.isBusy()) - { - try - { - Thread.sleep(15); - } catch (InterruptedException q) - { - } - } - } - - // End StructureListener - // ////////////////////////// - - /** - * instruct the Jalview binding to update the pdbentries vector if necessary - * prior to matching the viewer's contents to the list of structure files - * Jalview knows about. - */ - public abstract void refreshPdbEntries(); - - /** - * map between index of model filename returned from getPdbFile and the first - * index of models from this file in the viewer. Note - this is not trimmed - - * use getPdbFile to get number of unique models. - */ - private int _modelFileNameMap[]; - - // //////////////////////////////// - // /StructureListener @Override public synchronized String[] getStructureFiles() { - if (viewer == null) + if (chimeraManager == null) { return new String[0]; } @@ -757,9 +380,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** - * Construct and send a command to highlight zero, one or more atoms. We do - * this by sending an "rlabel" command to show the residue label at that - * position. + * Construct and send a command to highlight zero, one or more atoms. We do this + * by sending an "rlabel" command to show the residue label at that position. */ @Override public void highlightAtoms(List atoms) @@ -769,6 +391,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel return; } + boolean forChimeraX = chimeraManager.isChimeraX(); StringBuilder cmd = new StringBuilder(128); boolean first = true; boolean found = false; @@ -783,18 +406,26 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { if (first) { - cmd.append("rlabel #").append(cms.get(0).getModelNumber()) - .append(":"); + cmd.append(forChimeraX ? "label #" : "rlabel #"); } else { cmd.append(","); } first = false; - cmd.append(pdbResNum); - if (!chain.equals(" ")) + if (forChimeraX) { - cmd.append(".").append(chain); + cmd.append(cms.get(0).getModelNumber()) + .append("/").append(chain).append(":").append(pdbResNum); + } + else + { + cmd.append(cms.get(0).getModelNumber()) + .append(":").append(pdbResNum); + if (!chain.equals(" ") && !forChimeraX) + { + cmd.append(".").append(chain); + } } found = true; } @@ -814,11 +445,11 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel */ if (lastHighlightCommand != null) { - viewer.sendChimeraCommand("~" + lastHighlightCommand, false); + chimeraManager.sendChimeraCommand("~" + lastHighlightCommand, false); } if (found) { - viewer.sendChimeraCommand(command, false); + chimeraManager.sendChimeraCommand(command, false); } this.lastHighlightCommand = command; } @@ -831,7 +462,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /* * Ask Chimera for its current selection */ - List selection = viewer.getSelectedResidueSpecs(); + List selection = chimeraManager.getSelectedResidueSpecs(); /* * Parse model number, residue and chain for each selected position, @@ -857,12 +488,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel protected List convertStructureResiduesToAlignment( List structureSelection) { - List atomSpecs = new ArrayList(); + boolean chimeraX = chimeraManager.isChimeraX(); + List atomSpecs = new ArrayList<>(); for (String atomSpec : structureSelection) { try { - AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec); + AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX); String pdbfilename = getPdbFileForModel(spec.getModelNumber()); spec.setPdbFile(pdbfilename); atomSpecs.add(spec); @@ -903,165 +535,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel System.err.println("## Chimera log: " + message); } - private void viewerCommandHistory(boolean enable) - { - // log("(Not yet implemented) History " - // + ((debug || enable) ? "on" : "off")); - } - - public long getLoadNotifiesHandled() - { - return loadNotifiesHandled; - } - - @Override - public void setJalviewColourScheme(ColourSchemeI cs) - { - colourBySequence = false; - - if (cs == null) - { - return; - } - - // Chimera expects RBG values in the range 0-1 - final double normalise = 255D; - viewerCommandHistory(false); - StringBuilder command = new StringBuilder(128); - - List residueSet = ResidueProperties.getResidues(isNucleotide(), - false); - for (String resName : residueSet) - { - char res = resName.length() == 3 - ? ResidueProperties.getSingleCharacterCode(resName) - : resName.charAt(0); - Color col = cs.findColour(res, 0, null, null, 0f); - command.append("color " + col.getRed() / normalise + "," - + col.getGreen() / normalise + "," + col.getBlue() / normalise - + " ::" + resName + ";"); - } - - sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA); - viewerCommandHistory(true); - } - - /** - * called when the binding thinks the UI needs to be refreshed after a Chimera - * state change. this could be because structures were loaded, or because an - * error has occurred. - */ - public abstract void refreshGUI(); - - @Override - public void setLoadingFromArchive(boolean loadingFromArchive) - { - this.loadingFromArchive = loadingFromArchive; - } - - /** - * - * @return true if Chimeral is still restoring state or loading is still going - * on (see setFinsihedLoadingFromArchive) - */ - @Override - public boolean isLoadingFromArchive() - { - return loadingFromArchive && !loadingFinished; - } - - /** - * modify flag which controls if sequence colouring events are honoured by the - * binding. Should be true for normal operation - * - * @param finishedLoading - */ - @Override - public void setFinishedLoadingFromArchive(boolean finishedLoading) - { - loadingFinished = finishedLoading; - } - - /** - * Send the Chimera 'background solid " command. - * - * @see https - * ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background - * .html - * @param col - */ - @Override - public void setBackgroundColour(Color col) - { - viewerCommandHistory(false); - double normalise = 255D; - final String command = "background solid " + col.getRed() / normalise - + "," + col.getGreen() / normalise + "," - + col.getBlue() / normalise + ";"; - viewer.sendChimeraCommand(command, false); - viewerCommandHistory(true); - } - - /** - * Ask Chimera to save its session to the given file. Returns true if - * successful, else false. - * - * @param filepath - * @return - */ - public boolean saveSession(String filepath) - { - if (isChimeraRunning()) - { - List reply = viewer.sendChimeraCommand("save " + filepath, - true); - if (reply.contains("Session written")) - { - return true; - } - else - { - Cache.log - .error("Error saving Chimera session: " + reply.toString()); - } - } - return false; - } - - /** - * Ask Chimera to open a session file. Returns true if successful, else false. - * The filename must have a .py extension for this command to work. - * - * @param filepath - * @return - */ - public boolean openSession(String filepath) - { - sendChimeraCommand("open " + filepath, true); - // todo: test for failure - how? - return true; - } - - /** - * Returns a list of chains mapped in this viewer. Note this list is not - * currently scoped per structure. - * - * @return - */ - @Override - public List getChainNames() - { - return chainNames; - } - - /** - * Send a 'focus' command to Chimera to recentre the visible display - */ - public void focusView() - { - sendChimeraCommand("focus", false); - } - /** * Send a 'show' command for all atoms in the currently selected columns * @@ -1092,7 +565,11 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel /** * Constructs and send commands to Chimera to set attributes on residues for - * features visible in Jalview + * features visible in Jalview. + *

+ * The syntax is: setattr r <attName> <attValue> <atomSpec> + *

+ * For example: setattr r jv_chain "Ferredoxin-1, Chloroplastic" #0:94.A * * @param avp * @return @@ -1100,54 +577,49 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel public int sendFeaturesToViewer(AlignmentViewPanel avp) { // TODO refactor as required to pull up to an interface - AlignmentI alignment = avp.getAlignment(); - String[] files = getStructureFiles(); - if (files == null) - { - return 0; - } - - StructureMappingcommandSet commandSet = ChimeraCommands - .getSetAttributeCommandsForFeatures(getSsm(), files, - getSequence(), avp); - String[] commands = commandSet.commands; - if (commands.length > 10) + Map> featureValues = buildFeaturesMap( + avp); + List commands = getCommandGenerator() + .setAttributes(featureValues); + if (commands.size() > 10) { sendCommandsByFile(commands); } else { - for (String command : commands) + for (StructureCommandI command : commands) { sendAsynchronousCommand(command, null); } } - return commands.length; + return commands.size(); } /** - * Write commands to a temporary file, and send a command to Chimera to open - * the file as a commands script. For use when sending a large number of - * separate commands would overload the REST interface mechanism. + * Write commands to a temporary file, and send a command to Chimera to open the + * file as a commands script. For use when sending a large number of separate + * commands would overload the REST interface mechanism. * * @param commands */ - protected void sendCommandsByFile(String[] commands) + protected void sendCommandsByFile(List commands) { try { - File tmp = File.createTempFile("chim", ".com"); + File tmp = File.createTempFile("chim", getCommandFileExtension()); tmp.deleteOnExit(); PrintWriter out = new PrintWriter(new FileOutputStream(tmp)); - for (String command : commands) + for (StructureCommandI command : commands) { - out.println(command); + out.println(command.getCommand()); } out.flush(); out.close(); String path = tmp.getAbsolutePath(); - sendAsynchronousCommand("open cmd:" + path, null); + StructureCommandI command = getCommandGenerator() + .openCommandFile(path); + sendAsynchronousCommand(command, null); } catch (IOException e) { System.err.println("Sending commands to Chimera via file failed with " @@ -1156,6 +628,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel } /** + * Returns the file extension required for a file of commands to be read by + * the structure viewer + * @return + */ + protected String getCommandFileExtension() + { + return ".com"; + } + + /** * Get Chimera residues which have the named attribute, find the mapped * positions in the Jalview sequence(s), and set as sequence features * @@ -1176,7 +658,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel // fails for 'average.bfactor' (which is bad): String cmd = "list residues attr '" + attName + "'"; - List residues = sendChimeraCommand(cmd, true); + List residues = executeCommand(new StructureCommand(cmd), true); boolean featureAdded = createFeaturesForAttributes(attName, residues); if (featureAdded) @@ -1205,6 +687,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel { boolean featureAdded = false; String featureGroup = getViewerFeatureGroup(); + boolean chimeraX = chimeraManager.isChimeraX(); for (String residue : residues) { @@ -1228,7 +711,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel try { - spec = AtomSpec.fromChimeraAtomspec(atomSpec); + spec = AtomSpec.fromChimeraAtomspec(atomSpec, chimeraX); } catch (IllegalArgumentException e) { System.err.println("Problem parsing atomspec " + atomSpec); @@ -1289,23 +772,54 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel return CHIMERA_FEATURE_GROUP; } - public Hashtable getChainFile() + @Override + public String getModelIdForFile(String pdbFile) { - return chainFile; + List foundModels = chimeraMaps.get(pdbFile); + if (foundModels != null && !foundModels.isEmpty()) + { + return String.valueOf(foundModels.get(0).getModelNumber()); + } + return ""; } - public List getChimeraModelByChain(String chain) + /** + * Answers a (possibly empty) list of attribute names in Chimera[X], excluding + * any which were added from Jalview + * + * @return + */ + public List getChimeraAttributes() + { + List atts = chimeraManager.getAttrList(); + Iterator it = atts.iterator(); + while (it.hasNext()) + { + if (it.next().startsWith(ChimeraCommands.NAMESPACE_PREFIX)) + { + /* + * attribute added from Jalview - exclude it + */ + it.remove(); + } + } + return atts; + } + + /** + * Returns the file extension to use for a saved viewer session file (.py) + * + * @return + */ + @Override + public String getSessionFileExtension() { - return chimeraMaps.get(chainFile.get(chain)); + return CHIMERA_SESSION_EXTENSION; } - public int getModelNoForChain(String chain) + @Override + public String getHelpURL() { - List foundModels = getChimeraModelByChain(chain); - if (foundModels != null && !foundModels.isEmpty()) - { - return foundModels.get(0).getModelNumber(); - } - return -1; + return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide"; } } diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 7818748..ef40261 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -414,7 +414,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, addKeyListener(); - final List selviews = new ArrayList<>(); + final List selviews = new ArrayList<>(); final List origview = new ArrayList<>(); final String menuLabel = MessageManager .getString("label.copy_format_from"); diff --git a/src/jalview/gui/AppJmol.java b/src/jalview/gui/AppJmol.java index e13df4a..7cf10e7 100644 --- a/src/jalview/gui/AppJmol.java +++ b/src/jalview/gui/AppJmol.java @@ -20,38 +20,39 @@ */ package jalview.gui; -import jalview.bin.Cache; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceI; -import jalview.gui.ImageExporter.ImageWriterI; -import jalview.gui.StructureViewer.ViewerType; -import jalview.structures.models.AAStructureBindingModel; -import jalview.util.BrowserLauncher; -import jalview.util.ImageMaker; -import jalview.util.MessageManager; -import jalview.util.Platform; -import jalview.ws.dbsources.Pdb; - import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.Vector; +import java.util.Map; -import javax.swing.JCheckBoxMenuItem; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.SwingUtilities; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; +import jalview.api.AlignmentViewPanel; +import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.datamodel.StructureViewerModel; +import jalview.datamodel.StructureViewerModel.StructureData; +import jalview.gui.ImageExporter.ImageWriterI; +import jalview.gui.StructureViewer.ViewerType; +import jalview.structure.StructureCommand; +import jalview.structures.models.AAStructureBindingModel; +import jalview.util.BrowserLauncher; +import jalview.util.ImageMaker; +import jalview.util.MessageManager; +import jalview.util.Platform; +import jalview.ws.dbsources.Pdb; + public class AppJmol extends StructureViewerBase { // ms to wait for Jmol to load files @@ -88,48 +89,55 @@ public class AppJmol extends StructureViewerBase * @param bounds * @param viewid */ - public AppJmol(String[] files, String[] ids, SequenceI[][] seqs, - AlignmentPanel ap, boolean usetoColour, boolean useToAlign, - boolean leaveColouringToJmol, String loadStatus, Rectangle bounds, - String viewid) + public AppJmol(StructureViewerModel viewerModel, AlignmentPanel ap, + String sessionFile, String viewid) { - PDBEntry[] pdbentrys = new PDBEntry[files.length]; - for (int i = 0; i < pdbentrys.length; i++) + Map pdbData = viewerModel.getFileData(); + PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()]; + SequenceI[][] seqs = new SequenceI[pdbData.size()][]; + int i = 0; + for (StructureData data : pdbData.values()) { - // PDBEntry pdbentry = new PDBEntry(files[i], ids[i]); - PDBEntry pdbentry = new PDBEntry(ids[i], null, PDBEntry.Type.PDB, - files[i]); + PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null, + PDBEntry.Type.PDB, data.getFilePath()); pdbentrys[i] = pdbentry; + List sequencesForPdb = data.getSeqList(); + seqs[i] = sequencesForPdb + .toArray(new SequenceI[sequencesForPdb.size()]); + i++; } - // / TODO: check if protocol is needed to be set, and if chains are + + // TODO: check if protocol is needed to be set, and if chains are // autodiscovered. jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(), pdbentrys, seqs, null); jmb.setLoadingFromArchive(true); addAlignmentPanel(ap); - if (useToAlign) + if (viewerModel.isAlignWithPanel()) { useAlignmentPanelForSuperposition(ap); } initMenus(); - if (leaveColouringToJmol || !usetoColour) + boolean useToColour = viewerModel.isColourWithAlignPanel(); + boolean leaveColouringToJmol = viewerModel.isColourByViewer(); + if (leaveColouringToJmol || !useToColour) { jmb.setColourBySequence(false); seqColour.setSelected(false); viewerColour.setSelected(true); } - else if (usetoColour) + else if (useToColour) { useAlignmentPanelForColourbyseq(ap); jmb.setColourBySequence(true); seqColour.setSelected(true); viewerColour.setSelected(false); } - this.setBounds(bounds); + + this.setBounds(viewerModel.getX(), viewerModel.getY(), + viewerModel.getWidth(), viewerModel.getHeight()); setViewId(viewid); - // jalview.gui.Desktop.addInternalFrame(this, "Loading File", - // bounds.width,bounds.height); this.addInternalFrameListener(new InternalFrameAdapter() { @@ -140,7 +148,10 @@ public class AppJmol extends StructureViewerBase closeViewer(false); } }); - initJmol(loadStatus); // pdbentry, seq, JBPCHECK! + StringBuilder cmd = new StringBuilder(); + cmd.append("load FILES ").append(QUOTE) + .append(Platform.escapeBackslashes(sessionFile)).append(QUOTE); + initJmol(cmd.toString()); } @Override @@ -148,22 +159,12 @@ public class AppJmol extends StructureViewerBase { super.initMenus(); - viewerActionMenu.setText(MessageManager.getString("label.jmol")); - viewerColour .setText(MessageManager.getString("label.colour_with_jmol")); viewerColour.setToolTipText(MessageManager .getString("label.let_jmol_manage_structure_colours")); } - IProgressIndicator progressBar = null; - - @Override - protected IProgressIndicator getIProgressIndicator() - { - return progressBar; - } - /** * display a single PDB structure in a new Jmol view * @@ -175,7 +176,7 @@ public class AppJmol extends StructureViewerBase public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains, final AlignmentPanel ap) { - progressBar = ap.alignFrame; + setProgressIndicator(ap.alignFrame); openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry }, new SequenceI[][] @@ -186,14 +187,17 @@ public class AppJmol extends StructureViewerBase PDBEntry[] pdbentrys, SequenceI[][] seqs) { - progressBar = ap.alignFrame; + setProgressIndicator(ap.alignFrame); jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(), pdbentrys, seqs, null); addAlignmentPanel(ap); useAlignmentPanelForColourbyseq(ap); alignAddedStructures = alignAdded; - useAlignmentPanelForSuperposition(ap); + if (pdbentrys.length > 1) + { + useAlignmentPanelForSuperposition(ap); + } jmb.setColourBySequence(true); setSize(400, 400); // probably should be a configurable/dynamic default here @@ -256,47 +260,12 @@ public class AppJmol extends StructureViewerBase { command = ""; } - jmb.evalStateCommand(command); - jmb.evalStateCommand("set hoverDelay=0.1"); + jmb.executeCommand(new StructureCommand(command), false); + jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false); jmb.setFinishedInit(true); } @Override - void showSelectedChains() - { - Vector toshow = new Vector<>(); - for (int i = 0; i < chainMenu.getItemCount(); i++) - { - if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem) - { - JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i); - if (item.isSelected()) - { - toshow.addElement(item.getText()); - } - } - } - jmb.centerViewer(toshow); - } - - @Override - public void closeViewer(boolean closeExternalViewer) - { - // Jmol does not use an external viewer - if (jmb != null) - { - jmb.closeViewer(); - } - setAlignmentPanel(null); - _aps.clear(); - _alignwith.clear(); - _colourwith.clear(); - // TODO: check for memory leaks where instance isn't finalised because jmb - // holds a reference to the window - jmb = null; - } - - @Override public void run() { _started = true; @@ -354,12 +323,12 @@ public class AppJmol extends StructureViewerBase cmd.append("loadingJalviewdata=true\nload APPEND "); cmd.append(filesString); cmd.append("\nloadingJalviewdata=null"); - final String command = cmd.toString(); + final StructureCommand command = new StructureCommand(cmd.toString()); lastnotify = jmb.getLoadNotifiesHandled(); try { - jmb.evalStateCommand(command); + jmb.executeCommand(command, false); } catch (OutOfMemoryError oomerror) { new OOMWarning("When trying to add structures to the Jmol viewer!", @@ -385,8 +354,10 @@ public class AppJmol extends StructureViewerBase try { Cache.log.debug("Waiting around for jmb notify."); - Thread.sleep(waitFor); waitTotal += waitFor; + + // Thread.sleep() throws an exception in JS + Thread.sleep(waitFor); } catch (Exception e) { } @@ -403,7 +374,7 @@ public class AppJmol extends StructureViewerBase } // refresh the sequence colours for the new structure(s) - for (AlignmentPanel ap : _colourwith) + for (AlignmentViewPanel ap : _colourwith) { jmb.updateColours(ap); } @@ -425,7 +396,7 @@ public class AppJmol extends StructureViewerBase @Override public void run() { - if (jmb.viewer.isScriptExecuting()) + if (jmb.jmolViewer.isScriptExecuting()) { SwingUtilities.invokeLater(this); try @@ -438,7 +409,7 @@ public class AppJmol extends StructureViewerBase } else { - alignStructs_withAllAlignPanels(); + alignStructsWithAllAlignPanels(); } } }); @@ -476,12 +447,9 @@ public class AppJmol extends StructureViewerBase AlignmentI pdbseq = null; pdbid = jmb.getPdbEntry(pi).getId(); long hdl = pdbid.hashCode() - System.currentTimeMillis(); - if (progressBar != null) - { - progressBar.setProgressBar(MessageManager - .formatMessage("status.fetching_pdb", new String[] - { pdbid }), hdl); - } + setProgressMessage(MessageManager + .formatMessage("status.fetching_pdb", new String[] + { pdbid }), hdl); try { pdbseq = pdbclient.getSequenceRecords(pdbid); @@ -494,12 +462,8 @@ public class AppJmol extends StructureViewerBase errormsgs.append("'").append(pdbid).append("'"); } finally { - if (progressBar != null) - { - progressBar.setProgressBar( - MessageManager.getString("label.state_completed"), - hdl); - } + setProgressMessage( + MessageManager.getString("label.state_completed"), hdl); } if (pdbseq != null) { @@ -563,6 +527,7 @@ public class AppJmol extends StructureViewerBase * * @param type */ + @Override public void makePDBImage(ImageMaker.TYPE type) { int width = getWidth(); @@ -572,17 +537,17 @@ public class AppJmol extends StructureViewerBase @Override public void exportImage(Graphics g) throws Exception { - jmb.viewer.renderScreenImage(g, width, height); + jmb.jmolViewer.renderScreenImage(g, width, height); } }; String view = MessageManager.getString("action.view").toLowerCase(); ImageExporter exporter = new ImageExporter(writer, - jmb.getIProgressIndicator(), type, getTitle()); + getProgressIndicator(), type, getTitle()); exporter.doExport(null, this, width, height, view); } @Override - public void showHelp_actionPerformed(ActionEvent actionEvent) + public void showHelp_actionPerformed() { try { @@ -590,12 +555,13 @@ public class AppJmol extends StructureViewerBase .openURL("http://wiki.jmol.org");//http://jmol.sourceforge.net/docs/JmolUserGuide/"); } catch (Exception ex) { + System.err.println("Show Jmol help failed with: " + ex.getMessage()); } } + @Override public void showConsole(boolean showConsole) { - if (showConsole) { if (splitPane == null) @@ -661,7 +627,7 @@ public class AppJmol extends StructureViewerBase } } } - else if (jmb == null || jmb.viewer == null || !jmb.isFinishedInit()) + else if (jmb == null || jmb.jmolViewer == null || !jmb.isFinishedInit()) { g.setColor(Color.black); g.fillRect(0, 0, currentSize.width, currentSize.height); @@ -672,7 +638,7 @@ public class AppJmol extends StructureViewerBase } else { - jmb.viewer.renderScreenImage(g, currentSize.width, + jmb.jmolViewer.renderScreenImage(g, currentSize.width, currentSize.height); } } @@ -685,12 +651,6 @@ public class AppJmol extends StructureViewerBase } @Override - public String getStateInfo() - { - return jmb == null ? null : jmb.viewer.getStateInfo(); - } - - @Override public ViewerType getViewerType() { return ViewerType.JMOL; diff --git a/src/jalview/gui/AppJmolBinding.java b/src/jalview/gui/AppJmolBinding.java index 75b98bc..98787cb 100644 --- a/src/jalview/gui/AppJmolBinding.java +++ b/src/jalview/gui/AppJmolBinding.java @@ -20,6 +20,16 @@ */ package jalview.gui; +import java.awt.Container; +import java.io.File; +import java.util.List; +import java.util.Map; + +import javax.swing.JComponent; + +import org.jmol.api.JmolAppConsoleInterface; +import org.openscience.jmol.app.jmolpanel.console.AppConsole; + import jalview.api.AlignmentViewPanel; import jalview.api.structures.JalviewStructureDisplayI; import jalview.bin.Cache; @@ -29,32 +39,16 @@ import jalview.ext.jmol.JalviewJmolBinding; import jalview.io.DataSourceType; import jalview.structure.StructureSelectionManager; import jalview.util.Platform; - -import java.awt.Container; -import java.io.File; -import java.util.List; -import java.util.Map; - -import org.jmol.api.JmolAppConsoleInterface; - import javajs.util.BS; public class AppJmolBinding extends JalviewJmolBinding { - protected AppJmol appJmolWindow; - public AppJmolBinding(AppJmol appJmol, StructureSelectionManager sSm, PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol) { super(sSm, pdbentry, sequenceIs, protocol); - appJmolWindow = appJmol; - } - - @Override - protected IProgressIndicator getIProgressIndicator() - { - return appJmolWindow.progressBar; + setViewer(appJmol); } @Override @@ -99,36 +93,26 @@ public class AppJmolBinding extends JalviewJmolBinding @Override public void refreshGUI() { + if (getMappedStructureCount() == 0) + { + // too soon! + return; + } // appJmolWindow.repaint(); javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - appJmolWindow.updateTitleAndMenus(); - // initiates a colourbySequence - // via seqColour_ActionPerformed. - appJmolWindow.revalidate(); + JalviewStructureDisplayI theViewer = getViewer(); + // invokes colourbySequence() via seqColour_ActionPerformed() + theViewer.updateTitleAndMenus(); + ((JComponent) theViewer).revalidate(); } }); } @Override - public void updateColours(Object source) - { - AlignmentPanel ap = (AlignmentPanel) source; - // ignore events from panels not used to colour this view - if (!appJmolWindow.isUsedforcolourby(ap)) - { - return; - } - if (!isLoadingFromArchive()) - { - colourBySequence(ap); - } - } - - @Override public void notifyScriptTermination(String strStatus, int msWalltime) { // todo - script termination doesn't happen ? @@ -152,35 +136,28 @@ public class AppJmolBinding extends JalviewJmolBinding @Override public void selectionChanged(BS arg0) { - // TODO Auto-generated method stub - - } - - @Override - public void refreshPdbEntries() - { - // TODO Auto-generated method stub - } @Override public void showConsole(boolean b) { - appJmolWindow.showConsole(b); + getViewer().showConsole(b); } @Override protected JmolAppConsoleInterface createJmolConsole( Container consolePanel, String buttonsToShow) { - viewer.setJmolCallbackListener(this); - return null;//BH can't do this yet. new AppConsole(viewer, consolePanel, buttonsToShow); + jmolViewer.setJmolCallbackListener(this); + // BH comment: can't do this yet [for JS only, or generally?] + return Platform.isJS() ? null + : new AppConsole(jmolViewer, consolePanel, buttonsToShow); } @Override protected void releaseUIResources() { - appJmolWindow = null; + setViewer(null); closeConsole(); } @@ -189,7 +166,7 @@ public class AppJmolBinding extends JalviewJmolBinding { if (svl instanceof SeqPanel) { - appJmolWindow.removeAlignmentPanel(((SeqPanel) svl).ap); + getViewer().removeAlignmentPanel(((SeqPanel) svl).ap); } } @@ -200,27 +177,6 @@ public class AppJmolBinding extends JalviewJmolBinding return null; } - @Override - public JalviewStructureDisplayI getViewer() - { - return appJmolWindow; - } - - @Override - public jalview.api.FeatureRenderer getFeatureRenderer( - AlignmentViewPanel alignment) - { - AlignmentPanel ap = (alignment == null) - ? appJmolWindow.getAlignmentPanel() - : (AlignmentPanel) alignment; - if (ap.av.isShowSequenceFeatures()) - { - return ap.av.getAlignPanel().getSeqPanel().seqCanvas.fr; - } - - return null; - } - @SuppressWarnings("unused") public void cacheFiles(List files) { diff --git a/src/jalview/gui/ChimeraViewFrame.java b/src/jalview/gui/ChimeraViewFrame.java index c6d6e97..0e5675c 100644 --- a/src/jalview/gui/ChimeraViewFrame.java +++ b/src/jalview/gui/ChimeraViewFrame.java @@ -20,43 +20,38 @@ */ package jalview.gui; -import jalview.api.FeatureRenderer; -import jalview.bin.Cache; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceI; -import jalview.ext.rbvi.chimera.ChimeraCommands; -import jalview.ext.rbvi.chimera.JalviewChimeraBinding; -import jalview.gui.StructureViewer.ViewerType; -import jalview.io.DataSourceType; -import jalview.io.StructureFile; -import jalview.structures.models.AAStructureBindingModel; -import jalview.util.BrowserLauncher; -import jalview.util.ImageMaker.TYPE; -import jalview.util.MessageManager; -import jalview.util.Platform; -import jalview.ws.dbsources.Pdb; - import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Random; +import java.util.Map; -import javax.swing.JCheckBoxMenuItem; import javax.swing.JInternalFrame; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; +import jalview.api.AlignmentViewPanel; +import jalview.api.FeatureRenderer; +import jalview.bin.Cache; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.datamodel.StructureViewerModel; +import jalview.datamodel.StructureViewerModel.StructureData; +import jalview.ext.rbvi.chimera.JalviewChimeraBinding; +import jalview.gui.StructureViewer.ViewerType; +import jalview.io.DataSourceType; +import jalview.io.StructureFile; +import jalview.structures.models.AAStructureBindingModel; +import jalview.util.ImageMaker.TYPE; +import jalview.util.MessageManager; +import jalview.util.Platform; + /** * GUI elements for handling an external chimera display * @@ -67,8 +62,6 @@ public class ChimeraViewFrame extends StructureViewerBase { private JalviewChimeraBinding jmb; - private IProgressIndicator progressBar = null; - /* * Path to Chimera session file. This is set when an open Jalview/Chimera * session is saved, or on restore from a Jalview project (if it holds the @@ -76,8 +69,6 @@ public class ChimeraViewFrame extends StructureViewerBase */ private String chimeraSessionFile = null; - private Random random = new Random(); - private int myWidth = 500; private int myHeight = 150; @@ -90,21 +81,13 @@ public class ChimeraViewFrame extends StructureViewerBase { super.initMenus(); - viewerActionMenu.setText(MessageManager.getString("label.chimera")); - - viewerColour - .setText(MessageManager.getString("label.colour_with_chimera")); - viewerColour.setToolTipText(MessageManager - .getString("label.let_chimera_manage_structure_colours")); - - helpItem.setText(MessageManager.getString("label.chimera_help")); savemenu.setVisible(false); // not yet implemented viewMenu.add(fitToWindow); JMenuItem writeFeatures = new JMenuItem( - MessageManager.getString("label.create_chimera_attributes")); + MessageManager.getString("label.create_viewer_attributes")); writeFeatures.setToolTipText(MessageManager - .getString("label.create_chimera_attributes_tip")); + .getString("label.create_viewer_attributes_tip")); writeFeatures.addActionListener(new ActionListener() { @Override @@ -139,34 +122,21 @@ public class ChimeraViewFrame extends StructureViewerBase */ protected void buildAttributesMenu(JMenu attributesMenu) { - List atts = jmb.sendChimeraCommand("list resattr", true); - if (atts == null) - { - return; - } + List atts = jmb.getChimeraAttributes(); attributesMenu.removeAll(); Collections.sort(atts); - for (String att : atts) + for (String attName : atts) { - final String attName = att.split(" ")[1]; - - /* - * ignore 'jv_*' attributes, as these are Jalview features that have - * been transferred to residue attributes in Chimera! - */ - if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX)) + JMenuItem menuItem = new JMenuItem(attName); + menuItem.addActionListener(new ActionListener() { - JMenuItem menuItem = new JMenuItem(attName); - menuItem.addActionListener(new ActionListener() + @Override + public void actionPerformed(ActionEvent e) { - @Override - public void actionPerformed(ActionEvent e) - { - getChimeraAttributes(attName); - } - }); - attributesMenu.add(menuItem); - } + getChimeraAttributes(attName); + } + }); + attributesMenu.add(menuItem); } } @@ -182,14 +152,12 @@ public class ChimeraViewFrame extends StructureViewerBase } /** - * Send a command to Chimera to create residue attributes for Jalview features - *

- * The syntax is: setattr r - *

- * For example: setattr r jv:chain "Ferredoxin-1, Chloroplastic" #0:94.A + * Sends command(s) to the structure viewer to create residue attributes for + * visible Jalview features */ protected void sendFeaturesToChimera() { + // todo pull up? int count = jmb.sendFeaturesToViewer(getAlignmentPanel()); statusBar.setText( MessageManager.formatMessage("label.attributes_set", count)); @@ -218,9 +186,9 @@ public class ChimeraViewFrame extends StructureViewerBase */ protected void createProgressBar() { - if (progressBar == null) + if (getProgressIndicator() == null) { - progressBar = new ProgressBar(statusPanel, statusBar); + setProgressIndicator(new ProgressBar(statusPanel, statusBar)); } } @@ -228,8 +196,7 @@ public class ChimeraViewFrame extends StructureViewerBase SequenceI[][] seqs) { createProgressBar(); - jmb = new JalviewChimeraBindingModel(this, - ap.getStructureSelectionManager(), pdbentrys, seqs, null); + jmb = newBindingModel(ap, pdbentrys, seqs); addAlignmentPanel(ap); useAlignmentPanelForColourbyseq(ap); @@ -257,6 +224,13 @@ public class ChimeraViewFrame extends StructureViewerBase } + protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap, + PDBEntry[] pdbentrys, SequenceI[][] seqs) + { + return new JalviewChimeraBindingModel(this, + ap.getStructureSelectionManager(), pdbentrys, seqs, null); + } + /** * Create a new viewer from saved session state data including Chimera session * file @@ -269,22 +243,34 @@ public class ChimeraViewFrame extends StructureViewerBase * @param colourBySequence * @param newViewId */ - public ChimeraViewFrame(String chimeraSessionFile, - AlignmentPanel alignPanel, PDBEntry[] pdbArray, - SequenceI[][] seqsArray, boolean colourByChimera, - boolean colourBySequence, String newViewId) + public ChimeraViewFrame(StructureViewerModel viewerData, + AlignmentPanel alignPanel, String sessionFile, String vid) { this(); - setViewId(newViewId); - this.chimeraSessionFile = chimeraSessionFile; + setViewId(vid); + this.chimeraSessionFile = sessionFile; + Map pdbData = viewerData.getFileData(); + PDBEntry[] pdbArray = new PDBEntry[pdbData.size()]; + SequenceI[][] seqsArray = new SequenceI[pdbData.size()][]; + int i = 0; + for (StructureData data : pdbData.values()) + { + PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null, + PDBEntry.Type.PDB, data.getFilePath()); + pdbArray[i] = pdbentry; + List sequencesForPdb = data.getSeqList(); + seqsArray[i] = sequencesForPdb + .toArray(new SequenceI[sequencesForPdb.size()]); + i++; + } openNewChimera(alignPanel, pdbArray, seqsArray); - if (colourByChimera) + if (viewerData.isColourByViewer()) { jmb.setColourBySequence(false); seqColour.setSelected(false); viewerColour.setSelected(true); } - else if (colourBySequence) + else if (viewerData.isColourWithAlignPanel()) { jmb.setColourBySequence(true); seqColour.setSelected(true); @@ -337,7 +323,8 @@ public class ChimeraViewFrame extends StructureViewerBase if (!jmb.launchChimera()) { JvOptionPane.showMessageDialog(Desktop.desktop, - MessageManager.getString("label.chimera_failed"), + MessageManager.formatMessage("label.open_viewer_failed", + getViewerName()), MessageManager.getString("label.error_loading_file"), JvOptionPane.ERROR_MESSAGE); this.dispose(); @@ -358,71 +345,6 @@ public class ChimeraViewFrame extends StructureViewerBase } /** - * Show only the selected chain(s) in the viewer - */ - @Override - void showSelectedChains() - { - List toshow = new ArrayList<>(); - for (int i = 0; i < chainMenu.getItemCount(); i++) - { - if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem) - { - JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i); - if (item.isSelected()) - { - toshow.add(item.getText()); - } - } - } - jmb.showChains(toshow); - } - - /** - * Close down this instance of Jalview's Chimera viewer, giving the user the - * option to close the associated Chimera window (process). They may wish to - * keep it open until they have had an opportunity to save any work. - * - * @param closeChimera - * if true, close any linked Chimera process; if false, prompt first - */ - @Override - public void closeViewer(boolean closeChimera) - { - if (jmb != null && jmb.isChimeraRunning()) - { - if (!closeChimera) - { - String prompt = MessageManager - .formatMessage("label.confirm_close_chimera", new Object[] - { jmb.getViewerTitle(getViewerName(), false) }); - prompt = JvSwingUtils.wrapTooltip(true, prompt); - int confirm = JvOptionPane.showConfirmDialog(this, prompt, - MessageManager.getString("label.close_viewer"), - JvOptionPane.YES_NO_CANCEL_OPTION); - /* - * abort closure if user hits escape or Cancel - */ - if (confirm == JvOptionPane.CANCEL_OPTION - || confirm == JvOptionPane.CLOSED_OPTION) - { - return; - } - closeChimera = confirm == JvOptionPane.YES_OPTION; - } - jmb.closeViewer(closeChimera); - } - setAlignmentPanel(null); - _aps.clear(); - _alignwith.clear(); - _colourwith.clear(); - // TODO: check for memory leaks where instance isn't finalised because jmb - // holds a reference to the window - jmb = null; - dispose(); - } - - /** * Open any newly added PDB structures in Chimera, having first fetched data * from PDB (if not already saved). */ @@ -550,8 +472,8 @@ public class ChimeraViewFrame extends StructureViewerBase pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos], jmb.getChains()[pos], pe.getFile(), protocol, - progressBar); - stashFoundChains(pdb, pe.getFile()); + getProgressIndicator()); + jmb.stashFoundChains(pdb, pe.getFile()); } catch (OutOfMemoryError oomerror) { @@ -585,7 +507,7 @@ public class ChimeraViewFrame extends StructureViewerBase } // refresh the sequence colours for the new structure(s) - for (AlignmentPanel ap : _colourwith) + for (AlignmentViewPanel ap : _colourwith) { jmb.updateColours(ap); } @@ -597,7 +519,7 @@ public class ChimeraViewFrame extends StructureViewerBase @Override public void run() { - alignStructs_withAllAlignPanels(); + alignStructsWithAllAlignPanels(); } }).start(); } @@ -607,105 +529,6 @@ public class ChimeraViewFrame extends StructureViewerBase worker = null; } - /** - * Fetch PDB data and save to a local file. Returns the full path to the file, - * or null if fetch fails. TODO: refactor to common with Jmol ? duplication - * - * @param processingEntry - * @return - * @throws Exception - */ - - private void stashFoundChains(StructureFile pdb, String file) - { - for (int i = 0; i < pdb.getChains().size(); i++) - { - String chid = new String( - pdb.getId() + ":" + pdb.getChains().elementAt(i).id); - jmb.getChainNames().add(chid); - jmb.getChainFile().put(chid, file); - } - } - - private String fetchPdbFile(PDBEntry processingEntry) throws Exception - { - String filePath = null; - Pdb pdbclient = new Pdb(); - AlignmentI pdbseq = null; - String pdbid = processingEntry.getId(); - long handle = System.currentTimeMillis() - + Thread.currentThread().hashCode(); - - /* - * Write 'fetching PDB' progress on AlignFrame as we are not yet visible - */ - String msg = MessageManager.formatMessage("status.fetching_pdb", - new Object[] - { pdbid }); - getAlignmentPanel().alignFrame.setProgressBar(msg, handle); - // long hdl = startProgressBar(MessageManager.formatMessage( - // "status.fetching_pdb", new Object[] - // { pdbid })); - try - { - pdbseq = pdbclient.getSequenceRecords(pdbid); - } catch (OutOfMemoryError oomerror) - { - new OOMWarning("Retrieving PDB id " + pdbid, oomerror); - } finally - { - msg = pdbid + " " + MessageManager.getString("label.state_completed"); - getAlignmentPanel().alignFrame.setProgressBar(msg, handle); - // stopProgressBar(msg, hdl); - } - /* - * If PDB data were saved and are not invalid (empty alignment), return the - * file path. - */ - if (pdbseq != null && pdbseq.getHeight() > 0) - { - // just use the file name from the first sequence's first PDBEntry - filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries() - .elementAt(0).getFile()).getAbsolutePath(); - processingEntry.setFile(filePath); - } - return filePath; - } - - /** - * Convenience method to update the progress bar if there is one. Be sure to - * call stopProgressBar with the returned handle to remove the message. - * - * @param msg - * @param handle - */ - public long startProgressBar(String msg) - { - // TODO would rather have startProgress/stopProgress as the - // IProgressIndicator interface - long tm = random.nextLong(); - if (progressBar != null) - { - progressBar.setProgressBar(msg, tm); - } - return tm; - } - - /** - * End the progress bar with the specified handle, leaving a message (if not - * null) on the status bar - * - * @param msg - * @param handle - */ - public void stopProgressBar(String msg, long handle) - { - if (progressBar != null) - { - progressBar.setProgressBar(msg, handle); - } - } - @Override public void makePDBImage(TYPE imageType) { @@ -714,93 +537,11 @@ public class ChimeraViewFrame extends StructureViewerBase } @Override - public void showHelp_actionPerformed(ActionEvent actionEvent) - { - try - { - BrowserLauncher - .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide"); - } catch (IOException ex) - { - } - } - - @Override public AAStructureBindingModel getBinding() { return jmb; } - /** - * Ask Chimera to save its session to the designated file path, or to a - * temporary file if the path is null. Returns the file path if successful, - * else null. - * - * @param filepath - * @see getStateInfo - */ - protected String saveSession(String filepath) - { - String pathUsed = filepath; - try - { - if (pathUsed == null) - { - File tempFile = File.createTempFile("chimera", ".py"); - tempFile.deleteOnExit(); - pathUsed = tempFile.getPath(); - } - boolean result = jmb.saveSession(pathUsed); - if (result) - { - this.chimeraSessionFile = pathUsed; - return pathUsed; - } - } catch (IOException e) - { - } - return null; - } - - /** - * Returns a string representing the state of the Chimera session. This is - * done by requesting Chimera to save its session to a temporary file, then - * reading the file contents. Returns an empty string on any error. - */ - @Override - public String getStateInfo() - { - String sessionFile = saveSession(null); - if (sessionFile == null) - { - return ""; - } - InputStream is = null; - try - { - File f = new File(sessionFile); - byte[] bytes = new byte[(int) f.length()]; - is = new FileInputStream(sessionFile); - is.read(bytes); - return new String(bytes); - } catch (IOException e) - { - return ""; - } finally - { - if (is != null) - { - try - { - is.close(); - } catch (IOException e) - { - // ignore - } - } - } - } - @Override protected void fitToWindow_actionPerformed() { @@ -818,26 +559,4 @@ public class ChimeraViewFrame extends StructureViewerBase { return "Chimera"; } - - /** - * Sends commands to align structures according to associated alignment(s). - * - * @return - */ - @Override - protected String alignStructs_withAllAlignPanels() - { - String reply = super.alignStructs_withAllAlignPanels(); - if (reply != null) - { - statusBar.setText("Superposition failed: " + reply); - } - return reply; - } - - @Override - protected IProgressIndicator getIProgressIndicator() - { - return progressBar; - } } diff --git a/src/jalview/gui/ChimeraXViewFrame.java b/src/jalview/gui/ChimeraXViewFrame.java new file mode 100644 index 0000000..517eb4f --- /dev/null +++ b/src/jalview/gui/ChimeraXViewFrame.java @@ -0,0 +1,64 @@ +package jalview.gui; + +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.datamodel.StructureViewerModel; +import jalview.gui.StructureViewer.ViewerType; + +/** + * A class for the gui frame through which Jalview interacts with the ChimeraX + * structure viewer. Mostly the same as ChimeraViewFrame with a few overrides + * for the differences. + * + * @author gmcarstairs + * + */ +public class ChimeraXViewFrame extends ChimeraViewFrame +{ + + public ChimeraXViewFrame(PDBEntry pdb, SequenceI[] seqsForPdb, + String[] chains, AlignmentPanel ap) + { + super(pdb, seqsForPdb, chains, ap); + } + + public ChimeraXViewFrame(PDBEntry[] pdbsForFile, boolean superposeAdded, + SequenceI[][] theSeqs, AlignmentPanel ap) + { + super(pdbsForFile, superposeAdded, theSeqs, ap); + } + + /** + * Constructor given a session file to be loaded + * + * @param viewerData + * @param alignPanel + * @param sessionFile + * @param vid + */ + public ChimeraXViewFrame(StructureViewerModel viewerData, + AlignmentPanel alignPanel, String sessionFile, String vid) + { + super(viewerData, alignPanel, sessionFile, vid); + } + + @Override + public ViewerType getViewerType() + { + return ViewerType.CHIMERAX; + } + + @Override + protected String getViewerName() + { + return "ChimeraX"; + } + + @Override + protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap, + PDBEntry[] pdbentrys, SequenceI[][] seqs) + { + return new JalviewChimeraXBindingModel(this, + ap.getStructureSelectionManager(), pdbentrys, seqs, null); + } +} diff --git a/src/jalview/gui/JalviewChimeraBindingModel.java b/src/jalview/gui/JalviewChimeraBindingModel.java index 9d63c6a..49655a4 100644 --- a/src/jalview/gui/JalviewChimeraBindingModel.java +++ b/src/jalview/gui/JalviewChimeraBindingModel.java @@ -27,33 +27,18 @@ import jalview.datamodel.SequenceI; import jalview.ext.rbvi.chimera.JalviewChimeraBinding; import jalview.io.DataSourceType; import jalview.structure.StructureSelectionManager; -import jalview.viewmodel.seqfeatures.FeatureRendererModel; +import javax.swing.JComponent; import javax.swing.SwingUtilities; public class JalviewChimeraBindingModel extends JalviewChimeraBinding { - private ChimeraViewFrame cvf; - public JalviewChimeraBindingModel(ChimeraViewFrame chimeraViewFrame, StructureSelectionManager ssm, PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol) { super(ssm, pdbentry, sequenceIs, protocol); - cvf = chimeraViewFrame; - } - - @Override - public FeatureRendererModel getFeatureRenderer(AlignmentViewPanel alignment) - { - AlignmentPanel ap = (alignment == null) ? cvf.getAlignmentPanel() - : (AlignmentPanel) alignment; - if (ap.av.isShowSequenceFeatures()) - { - return ap.getSeqPanel().seqCanvas.fr; - } - - return null; + setViewer(chimeraViewFrame); } @Override @@ -71,74 +56,10 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding @Override public void run() { - cvf.updateTitleAndMenus(); - cvf.revalidate(); + JalviewStructureDisplayI theViewer = getViewer(); + theViewer.updateTitleAndMenus(); + ((JComponent) theViewer).revalidate(); } }); } - - @Override - public void updateColours(Object source) - { - AlignmentPanel ap = (AlignmentPanel) source; - // ignore events from panels not used to colour this view - if (!cvf.isUsedforcolourby(ap)) - { - return; - } - if (!isLoadingFromArchive()) - { - colourBySequence(ap); - } - } - - @Override - public void releaseReferences(Object svl) - { - } - - @Override - protected void releaseUIResources() - { - } - - @Override - public void refreshPdbEntries() - { - } - - /** - * Send an asynchronous command to Chimera, in a new thread, optionally with - * an 'in progress' message in a progress bar somewhere - */ - @Override - protected void sendAsynchronousCommand(final String command, - final String progressMsg) - { - final long handle = progressMsg == null ? 0 - : cvf.startProgressBar(progressMsg); - SwingUtilities.invokeLater(new Runnable() - { - @Override - public void run() - { - try - { - sendChimeraCommand(command, false); - } finally - { - if (progressMsg != null) - { - cvf.stopProgressBar(null, handle); - } - } - } - }); - } - - @Override - public JalviewStructureDisplayI getViewer() - { - return cvf; - } } diff --git a/src/jalview/gui/JalviewChimeraXBindingModel.java b/src/jalview/gui/JalviewChimeraXBindingModel.java new file mode 100644 index 0000000..c685f0f --- /dev/null +++ b/src/jalview/gui/JalviewChimeraXBindingModel.java @@ -0,0 +1,91 @@ +package jalview.gui; + +import java.util.List; + +import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel; +import ext.edu.ucsf.rbvi.strucviz2.StructureManager; +import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.ext.rbvi.chimera.ChimeraXCommands; +import jalview.gui.StructureViewer.ViewerType; +import jalview.io.DataSourceType; +import jalview.structure.StructureCommand; +import jalview.structure.StructureSelectionManager; + +public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel +{ + + public static final String CHIMERAX_SESSION_EXTENSION = ".cxs"; + + public JalviewChimeraXBindingModel(ChimeraViewFrame chimeraViewFrame, + StructureSelectionManager ssm, PDBEntry[] pdbentry, + SequenceI[][] sequenceIs, DataSourceType protocol) + { + super(chimeraViewFrame, ssm, pdbentry, sequenceIs, protocol); + setStructureCommands(new ChimeraXCommands()); + } + + @Override + protected List getChimeraPaths() + { + return StructureManager.getChimeraPaths(true); + } + + @Override + protected void addChimeraModel(PDBEntry pe, + List modelsToMap) + { + /* + * ChimeraX hack: force chimera model name to pdbId here + */ + int modelNumber = chimeraMaps.size() + 1; + String command = "setattr #" + modelNumber + " models name " + + pe.getId(); + executeCommand(new StructureCommand(command), false); + modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL, + modelNumber, 0)); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + protected String getCommandFileExtension() + { + return ".cxc"; + } + + /** + * Returns the file extension to use for a saved viewer session file (.cxs) + * + * @return + * @see https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html#sesformat + */ + @Override + public String getSessionFileExtension() + { + return CHIMERAX_SESSION_EXTENSION; + } + + @Override + public String getHelpURL() + { + return "http://www.rbvi.ucsf.edu/chimerax/docs/user/index.html"; + } + + @Override + protected ViewerType getViewerType() + { + return ViewerType.CHIMERAX; + } + + @Override + protected String getModelId(int pdbfnum, String file) + { + return String.valueOf(pdbfnum + 1); + } + +} diff --git a/src/jalview/gui/Preferences.java b/src/jalview/gui/Preferences.java index 04b83a3..c61c70e 100755 --- a/src/jalview/gui/Preferences.java +++ b/src/jalview/gui/Preferences.java @@ -20,29 +20,6 @@ */ package jalview.gui; -import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder; -import jalview.bin.Cache; -import jalview.gui.Help.HelpId; -import jalview.gui.StructureViewer.ViewerType; -import jalview.io.BackupFiles; -import jalview.io.BackupFilesPresetEntry; -import jalview.io.FileFormatI; -import jalview.io.JalviewFileChooser; -import jalview.io.JalviewFileView; -import jalview.jbgui.GPreferences; -import jalview.jbgui.GSequenceLink; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ColourSchemes; -import jalview.schemes.ResidueColourScheme; -import jalview.urls.UrlLinkTableModel; -import jalview.urls.api.UrlProviderFactoryI; -import jalview.urls.api.UrlProviderI; -import jalview.urls.desktop.DesktopUrlProviderFactory; -import jalview.util.MessageManager; -import jalview.util.Platform; -import jalview.util.UrlConstants; -import jalview.ws.sifts.SiftsSettings; - import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -74,6 +51,29 @@ import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import ext.edu.ucsf.rbvi.strucviz2.StructureManager; +import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder; +import jalview.bin.Cache; +import jalview.ext.pymol.PymolManager; +import jalview.gui.Help.HelpId; +import jalview.gui.StructureViewer.ViewerType; +import jalview.io.BackupFiles; +import jalview.io.BackupFilesPresetEntry; +import jalview.io.FileFormatI; +import jalview.io.JalviewFileChooser; +import jalview.io.JalviewFileView; +import jalview.jbgui.GPreferences; +import jalview.jbgui.GSequenceLink; +import jalview.schemes.ColourSchemeI; +import jalview.schemes.ColourSchemes; +import jalview.schemes.ResidueColourScheme; +import jalview.urls.UrlLinkTableModel; +import jalview.urls.api.UrlProviderFactoryI; +import jalview.urls.api.UrlProviderI; +import jalview.urls.desktop.DesktopUrlProviderFactory; +import jalview.util.MessageManager; +import jalview.util.Platform; +import jalview.util.UrlConstants; +import jalview.ws.sifts.SiftsSettings; /** * DOCUMENT ME! @@ -105,6 +105,10 @@ public class Preferences extends GPreferences public static final String CHIMERA_PATH = "CHIMERA_PATH"; + public static final String CHIMERAX_PATH = "CHIMERAX_PATH"; + + public static final String PYMOL_PATH = "PYMOL_PATH"; + public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS"; public static final String SHOW_AUTOCALC_ABOVE = "SHOW_AUTOCALC_ABOVE"; @@ -337,7 +341,7 @@ public class Preferences extends GPreferences .setSelected(Cache.getDefault(SHOW_OV_HIDDEN_AT_START, false)); /* - * Set Structure tab defaults. + * Set Structure tab defaults */ final boolean structSelected = Cache.getDefault(STRUCT_FROM_PDB, false); structFromPdb.setSelected(structSelected); @@ -347,15 +351,52 @@ public class Preferences extends GPreferences addSecondaryStructure.setEnabled(structSelected); addTempFactor.setSelected(Cache.getDefault(ADD_TEMPFACT_ANN, false)); addTempFactor.setEnabled(structSelected); - structViewer.setSelectedItem( - Cache.getDefault(STRUCTURE_DISPLAY, ViewerType.JMOL.name())); - chimeraPath.setText(Cache.getDefault(CHIMERA_PATH, "")); - chimeraPath.addActionListener(new ActionListener() + + /* + * set choice of structure viewer, and path if saved as a preference; + * default to Jmol (first choice) if an unexpected value is found + */ + String viewerType = Cache.getDefault(STRUCTURE_DISPLAY, ViewerType.JMOL.name()); + structViewer.setSelectedItem(viewerType); + String viewerPath = ""; + ViewerType type = null; + try + { + type = ViewerType.valueOf(viewerType); + switch (type) + { + case JMOL: + break; + case CHIMERA: + viewerPath = Cache.getDefault(CHIMERA_PATH, ""); + break; + case CHIMERAX: + viewerPath = Cache.getDefault(CHIMERAX_PATH, ""); + break; + case PYMOL: + viewerPath = Cache.getDefault(PYMOL_PATH, ""); + break; + } + } catch (IllegalArgumentException e) + { + Cache.log.error("Unknown structure viewer type: " + viewerType + + ", defaulting to Jmol"); + type = ViewerType.JMOL; + } + structureViewerPath.setText(viewerPath); + + structureViewerPath.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - validateChimeraPath(); + if (validateViewerPath()) + { + Cache.setProperty(structViewer.getSelectedItem() + .equals(ViewerType.CHIMERAX.name()) + ? CHIMERAX_PATH + : CHIMERA_PATH, structureViewerPath.getText()); + } } }); @@ -707,9 +748,22 @@ public class Preferences extends GPreferences Boolean.toString(useRnaView.isSelected())); Cache.applicationProperties.setProperty(STRUCT_FROM_PDB, Boolean.toString(structFromPdb.isSelected())); + String viewer = structViewer.getSelectedItem().toString(); + String viewerPath = structureViewerPath.getText(); Cache.applicationProperties.setProperty(STRUCTURE_DISPLAY, - structViewer.getSelectedItem().toString()); - Cache.setOrRemove(CHIMERA_PATH, chimeraPath.getText()); + viewer); + if (viewer.equals(ViewerType.CHIMERA.name())) + { + Cache.setOrRemove(CHIMERA_PATH, viewerPath); + } + else if (viewer.equals(ViewerType.CHIMERAX.name())) + { + Cache.setOrRemove(CHIMERAX_PATH, viewerPath); + } + else if (viewer.equals(ViewerType.PYMOL.name())) + { + Cache.setOrRemove(PYMOL_PATH, viewerPath); + } Cache.applicationProperties.setProperty("MAP_WITH_SIFTS", Boolean.toString(siftsMapping.isSelected())); SiftsSettings.setMapWithSifts(siftsMapping.isSelected()); @@ -889,7 +943,7 @@ public class Preferences extends GPreferences @Override protected boolean validateStructure() { - return validateChimeraPath(); + return validateViewerPath(); } @@ -1213,19 +1267,20 @@ public class Preferences extends GPreferences } /** - * Returns true if chimera path is to a valid executable, else show an error - * dialog. + * Returns true if structure viewer path is to a valid executable, else shows + * an error dialog. Does nothing if the path is empty, as is the case for Jmol + * (built in to Jalview) or when Jalview is left to try default paths. */ - private boolean validateChimeraPath() + private boolean validateViewerPath() { - if (chimeraPath.getText().trim().length() > 0) + if (structureViewerPath.getText().trim().length() > 0) { - File f = new File(chimeraPath.getText()); + File f = new File(structureViewerPath.getText()); if (!f.canExecute()) { JvOptionPane.showInternalMessageDialog(Desktop.desktop, - MessageManager.getString("label.invalid_chimera_path"), - MessageManager.getString("label.invalid_name"), + MessageManager.getString("label.invalid_viewer_path"), + MessageManager.getString("label.invalid_viewer_path"), JvOptionPane.ERROR_MESSAGE); return false; } @@ -1234,23 +1289,57 @@ public class Preferences extends GPreferences } /** - * If Chimera is selected, check it can be found on default or user-specified - * path, if not show a warning/help dialog. + * If Chimera or ChimeraX or Pymol is selected, check it can be found on + * default or user-specified path, if not show a warning/help dialog */ @Override protected void structureViewer_actionPerformed(String selectedItem) { - if (!selectedItem.equals(ViewerType.CHIMERA.name())) + if (selectedItem.equals(ViewerType.JMOL.name())) { + structureViewerPath.setEnabled(false); + structureViewerPathLabel.setEnabled(false); return; } boolean found = false; + structureViewerPath.setEnabled(true); + structureViewerPathLabel.setEnabled(true); + structureViewerPathLabel.setText(MessageManager + .formatMessage("label.viewer_path", selectedItem)); /* - * Try user-specified and standard paths for Chimera executable. + * Try user-specified and standard paths for structure viewer executable */ - List paths = StructureManager.getChimeraPaths(); - paths.add(0, chimeraPath.getText()); + String viewerPath = ""; + List paths = null; + try + { + ViewerType viewerType = ViewerType.valueOf(selectedItem); + switch (viewerType) + { + case JMOL: + // dealt with above + break; + case CHIMERA: + viewerPath = Cache.getDefault(CHIMERA_PATH, ""); + paths = StructureManager.getChimeraPaths(false); + break; + case CHIMERAX: + viewerPath = Cache.getDefault(CHIMERAX_PATH, ""); + paths = StructureManager.getChimeraPaths(true); + break; + case PYMOL: + viewerPath = Cache.getDefault(PYMOL_PATH, ""); + paths = PymolManager.getPymolPaths(); + break; + } + } catch (IllegalArgumentException e) + { + // only valid entries should be in the drop-down + } + structureViewerPath.setText(viewerPath); + + paths.add(0, structureViewerPath.getText()); for (String path : paths) { if (new File(path.trim()).canExecute()) @@ -1259,12 +1348,13 @@ public class Preferences extends GPreferences break; } } + if (!found) { String[] options = { "OK", "Help" }; int showHelp = JvOptionPane.showInternalOptionDialog(Desktop.desktop, JvSwingUtils.wrapTooltip(true, - MessageManager.getString("label.chimera_missing")), + MessageManager.getString("label.viewer_missing")), "", JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE, null, options, options[0]); if (showHelp == JvOptionPane.NO_OPTION) diff --git a/src/jalview/gui/PymolBindingModel.java b/src/jalview/gui/PymolBindingModel.java new file mode 100644 index 0000000..264a49c --- /dev/null +++ b/src/jalview/gui/PymolBindingModel.java @@ -0,0 +1,241 @@ +package jalview.gui; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jalview.api.AlignmentViewPanel; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.ext.pymol.PymolCommands; +import jalview.ext.pymol.PymolManager; +import jalview.gui.StructureViewer.ViewerType; +import jalview.structure.AtomSpec; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommand; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureSelectionManager; +import jalview.structures.models.AAStructureBindingModel; + +public class PymolBindingModel extends AAStructureBindingModel +{ + /* + * format for labels shown on structures when mousing over sequence; + * see https://pymolwiki.org/index.php/Label#examples + * left not final so customisable e.g. with a Groovy script + */ + private static String LABEL_FORMAT = "\"%s %s\" % (resn,resi)"; + + private PymolManager pymolManager; + + private Thread pymolMonitor; + + /* + * full paths to structure files opened in PyMOL + */ + List structureFiles = new ArrayList<>(); + + /* + * lookup from file path to PyMOL object name + */ + Map pymolObjects = new HashMap<>(); + + private String lastLabelSpec; + + /** + * Constructor + * + * @param viewer + * @param ssm + * @param pdbentry + * @param sequenceIs + */ + public PymolBindingModel(StructureViewerBase viewer, + StructureSelectionManager ssm, PDBEntry[] pdbentry, + SequenceI[][] sequenceIs) + { + super(ssm, pdbentry, sequenceIs, null); + pymolManager = new PymolManager(); + setStructureCommands(new PymolCommands()); + setViewer(viewer); + } + + @Override + public String[] getStructureFiles() + { + return structureFiles.toArray(new String[structureFiles.size()]); + } + + @Override + public void highlightAtoms(List atoms) + { + /* + * https://pymolwiki.org/index.php/Label#examples + */ + StringBuilder sb = new StringBuilder(); + for (AtomSpec atom : atoms) + { + // todo promote to StructureCommandsI.showLabel() + // todo handle CA|P correctly + String modelId = getModelIdForFile(atom.getPdbFile()); + sb.append(String.format(" %s//%s/%d/CA", modelId, + atom.getChain(), + atom.getPdbResNum())); + } + String labelSpec = sb.toString(); + if (labelSpec.equals(lastLabelSpec)) + { + return; + } + StructureCommandI command = new StructureCommand("label", labelSpec, LABEL_FORMAT); + executeCommand(command, false); + + /* + * and remove the label(s) previously shown + */ + if (lastLabelSpec != null) + { + command = new StructureCommand("label", lastLabelSpec, ""); + executeCommand(command, false); + } + + lastLabelSpec = labelSpec; + } + + @Override + public SequenceRenderer getSequenceRenderer(AlignmentViewPanel avp) + { + return new SequenceRenderer(avp.getAlignViewport()); + } + + @Override + protected List executeCommand(StructureCommandI command, + boolean getReply) + { + // System.out.println(command.toString()); // debug + return pymolManager.sendCommand(command, getReply); + } + + @Override + protected String getModelIdForFile(String file) + { + return pymolObjects.containsKey(file) ? pymolObjects.get(file) : ""; + } + + @Override + protected ViewerType getViewerType() + { + return ViewerType.PYMOL; + } + + @Override + public boolean isViewerRunning() + { + return pymolManager.isPymolLaunched(); + } + + @Override + public void closeViewer(boolean closePymol) + { + super.closeViewer(closePymol); + if (closePymol) + { + pymolManager.exitPymol(); + } + pymolManager = null; + + if (pymolMonitor != null) + { + pymolMonitor.interrupt(); + } + } + + public boolean launchPymol() + { + if (pymolManager.isPymolLaunched()) + { + return true; + } + + boolean launched = pymolManager.launchPymol(); + if (launched) + { + // start listening for PyMOL selections - how?? + } + else + { + System.err.println("Failed to launch PyMOL!"); + } + return launched; + } + + public void openFile(PDBEntry pe) + { + // todo : check not already open, remap / rename, etc + String file = pe.getFile(); + StructureCommandI cmd = getCommandGenerator().loadFile(file); + + /* + * a second parameter sets the pdbid as the loaded PyMOL object name + */ + String pdbId = pe.getId(); + cmd.addParameter(pdbId); + + executeCommand(cmd, false); + + pymolObjects.put(file, pdbId); + if (!structureFiles.contains(file)) + { + structureFiles.add(file); + } + if (getSsm() != null) + { + getSsm().addStructureViewerListener(this); + } + + } + + @Override + protected String getModelId(int pdbfnum, String file) + { + return file; + } + + /** + * Returns the file extension to use for a saved viewer session file (.pse) + * + * @return + * @see https://pymolwiki.org/index.php/Save + */ + @Override + public String getSessionFileExtension() + { + return ".pse"; + } + + @Override + public String getHelpURL() + { + return "https://pymolwiki.org/"; + } + + /** + * Constructs and sends commands to set atom properties for visible Jalview + * features on residues mapped to structure + * + * @param avp + * @return + */ + public int sendFeaturesToViewer(AlignmentViewPanel avp) + { + // todo pull up this and JalviewChimeraBinding variant + Map> featureValues = buildFeaturesMap( + avp); + List commands = getCommandGenerator() + .setAttributes(featureValues); + executeCommands(commands, false, null); + return commands.size(); + } + +} diff --git a/src/jalview/gui/PymolViewer.java b/src/jalview/gui/PymolViewer.java new file mode 100644 index 0000000..c5a4c9a --- /dev/null +++ b/src/jalview/gui/PymolViewer.java @@ -0,0 +1,382 @@ +package jalview.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.swing.JInternalFrame; +import javax.swing.JMenuItem; +import javax.swing.event.InternalFrameAdapter; +import javax.swing.event.InternalFrameEvent; + +import jalview.api.AlignmentViewPanel; +import jalview.api.FeatureRenderer; +import jalview.bin.Cache; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.datamodel.StructureViewerModel; +import jalview.datamodel.StructureViewerModel.StructureData; +import jalview.gui.StructureViewer.ViewerType; +import jalview.io.DataSourceType; +import jalview.io.StructureFile; +import jalview.structures.models.AAStructureBindingModel; +import jalview.util.MessageManager; + +public class PymolViewer extends StructureViewerBase +{ + private static final int myWidth = 500; + + private static final int myHeight = 150; + + private PymolBindingModel binding; + + private String pymolSessionFile; + + public PymolViewer() + { + super(); + + /* + * closeViewer will decide whether or not to close this frame + * depending on whether user chooses to Cancel or not + */ + setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE); + } + + public PymolViewer(PDBEntry pdb, SequenceI[] seqs, Object object, + AlignmentPanel ap) + { + this(); + openNewPymol(ap, new PDBEntry[] { pdb }, + new SequenceI[][] + { seqs }); + } + + public PymolViewer(PDBEntry[] pe, boolean alignAdded, SequenceI[][] seqs, + AlignmentPanel ap) + { + this(); + setAlignAddedStructures(alignAdded); + openNewPymol(ap, pe, seqs); + } + + /** + * Constructor given a session file to be restored + * + * @param sessionFile + * @param alignPanel + * @param pdbArray + * @param seqsArray + * @param colourByPymol + * @param colourBySequence + * @param newViewId + */ + public PymolViewer(StructureViewerModel viewerModel, + AlignmentPanel alignPanel, String sessionFile, String vid) + { + // TODO convert to base/factory class method + this(); + setViewId(vid); + this.pymolSessionFile = sessionFile; + Map pdbData = viewerModel.getFileData(); + PDBEntry[] pdbArray = new PDBEntry[pdbData.size()]; + SequenceI[][] seqsArray = new SequenceI[pdbData.size()][]; + int i = 0; + for (StructureData data : pdbData.values()) + { + PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null, + PDBEntry.Type.PDB, data.getFilePath()); + pdbArray[i] = pdbentry; + List sequencesForPdb = data.getSeqList(); + seqsArray[i] = sequencesForPdb + .toArray(new SequenceI[sequencesForPdb.size()]); + i++; + } + + openNewPymol(alignPanel, pdbArray, seqsArray); + if (viewerModel.isColourByViewer()) + { + binding.setColourBySequence(false); + seqColour.setSelected(false); + viewerColour.setSelected(true); + } + else if (viewerModel.isColourWithAlignPanel()) + { + binding.setColourBySequence(true); + seqColour.setSelected(true); + viewerColour.setSelected(false); + } + } + + private void openNewPymol(AlignmentPanel ap, PDBEntry[] pe, + SequenceI[][] seqs) + { + createProgressBar(); + binding = new PymolBindingModel(this, ap.getStructureSelectionManager(), + pe, seqs); + addAlignmentPanel(ap); + useAlignmentPanelForColourbyseq(ap); + + if (pe.length > 1) + { + useAlignmentPanelForSuperposition(ap); + } + binding.setColourBySequence(true); + setSize(myWidth, myHeight); + initMenus(); + viewerActionMenu.setText("PyMOL"); + updateTitleAndMenus(); + + addingStructures = false; + worker = new Thread(this); + worker.start(); + + this.addInternalFrameListener(new InternalFrameAdapter() + { + @Override + public void internalFrameClosing( + InternalFrameEvent internalFrameEvent) + { + closeViewer(false); + } + }); + + } + + /** + * Create a helper to manage progress bar display + */ + protected void createProgressBar() + { + if (getProgressIndicator() == null) + { + setProgressIndicator(new ProgressBar(statusPanel, statusBar)); + } + } + + @Override + public void run() + { + // todo pull up much of this + + StringBuilder errormsgs = new StringBuilder(128); + List filePDB = new ArrayList<>(); + List filePDBpos = new ArrayList<>(); + String[] curfiles = binding.getStructureFiles(); // files currently in viewer + for (int pi = 0; pi < binding.getPdbCount(); pi++) + { + String file = null; + PDBEntry thePdbEntry = binding.getPdbEntry(pi); + if (thePdbEntry.getFile() == null) + { + /* + * Retrieve PDB data, save to file, attach to PDBEntry + */ + file = fetchPdbFile(thePdbEntry); + if (file == null) + { + errormsgs.append("'" + thePdbEntry.getId() + "' "); + } + } + else + { + /* + * got file already + */ + file = new File(thePdbEntry.getFile()).getAbsoluteFile() + .getPath(); + // todo - skip if already loaded in PyMOL + } + if (file != null) + { + filePDB.add(thePdbEntry); + filePDBpos.add(Integer.valueOf(pi)); + } + } + + if (!filePDB.isEmpty()) + { + /* + * at least one structure to add to viewer + */ + binding.setFinishedInit(false); + if (!addingStructures) + { + try + { + initPymol(); + } catch (Exception ex) + { + Cache.log.error("Couldn't open PyMOL viewer!", ex); + } + } + int num = -1; + for (PDBEntry pe : filePDB) + { + num++; + if (pe.getFile() != null) + { + try + { + int pos = filePDBpos.get(num).intValue(); + long startTime = startProgressBar(getViewerName() + " " + + MessageManager.getString("status.opening_file_for") + + " " + pe.getId()); + binding.openFile(pe); + binding.addSequence(pos, binding.getSequence()[pos]); + File fl = new File(pe.getFile()); + DataSourceType protocol = DataSourceType.URL; + try + { + if (fl.exists()) + { + protocol = DataSourceType.FILE; + } + } catch (Throwable e) + { + } finally + { + stopProgressBar("", startTime); + } + + StructureFile pdb = binding.getSsm().setMapping( + binding.getSequence()[pos], binding.getChains()[pos], + pe.getFile(), protocol, + getProgressIndicator()); + binding.stashFoundChains(pdb, pe.getFile()); + } catch (Exception ex) + { + Cache.log.error( + "Couldn't open " + pe.getFile() + " in Chimera viewer!", + ex); + } finally + { + // Cache.log.debug("File locations are " + files); + } + } + } + + binding.refreshGUI(); + binding.setFinishedInit(true); + binding.setLoadingFromArchive(false); + + /* + * ensure that any newly discovered features (e.g. RESNUM) + * are added to any open feature settings dialog + */ + FeatureRenderer fr = getBinding().getFeatureRenderer(null); + if (fr != null) + { + fr.featuresAdded(); + } + + // refresh the sequence colours for the new structure(s) + for (AlignmentViewPanel ap : _colourwith) + { + binding.updateColours(ap); + } + // do superposition if asked to + if (alignAddedStructures) + { + new Thread(new Runnable() + { + @Override + public void run() + { + alignStructsWithAllAlignPanels(); + } + }).start(); + } + addingStructures = false; + } + _started = false; + worker = null; + + } + + /** + * Launch PyMOL. If we have a session file name, send PyMOL the command to + * open its saved session file. + */ + void initPymol() + { + Desktop.addInternalFrame(this, + binding.getViewerTitle(getViewerName(), true), + getBounds().width, getBounds().height); + + if (!binding.launchPymol()) + { + JvOptionPane.showMessageDialog(Desktop.desktop, + MessageManager.formatMessage("label.open_viewer_failed", + getViewerName()), + MessageManager.getString("label.error_loading_file"), + JvOptionPane.ERROR_MESSAGE); + this.dispose(); + return; + } + + if (this.pymolSessionFile != null) + { + boolean opened = binding.openSession(pymolSessionFile); + if (!opened) + { + Cache.log.error( + "An error occurred opening PyMOL session file " + + pymolSessionFile); + } + } + // binding.startPymolListener(); + } + + @Override + public AAStructureBindingModel getBinding() + { + return binding; + } + + @Override + public ViewerType getViewerType() + { + return ViewerType.PYMOL; + } + + @Override + protected String getViewerName() + { + return "PyMOL"; + } + + @Override + protected void initMenus() + { + super.initMenus(); + + savemenu.setVisible(false); // not yet implemented + viewMenu.add(fitToWindow); + + JMenuItem writeFeatures = new JMenuItem( + MessageManager.getString("label.create_viewer_attributes")); + writeFeatures.setToolTipText(MessageManager + .getString("label.create_viewer_attributes_tip")); + writeFeatures.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + sendFeaturesToPymol(); + } + }); + viewerActionMenu.add(writeFeatures); + } + + protected void sendFeaturesToPymol() + { + int count = binding.sendFeaturesToViewer(getAlignmentPanel()); + statusBar.setText( + MessageManager.formatMessage("label.attributes_set", count)); + } + +} diff --git a/src/jalview/gui/StructureViewer.java b/src/jalview/gui/StructureViewer.java index 0c8354b..617706a 100644 --- a/src/jalview/gui/StructureViewer.java +++ b/src/jalview/gui/StructureViewer.java @@ -20,14 +20,6 @@ */ package jalview.gui; -import jalview.api.structures.JalviewStructureDisplayI; -import jalview.bin.Cache; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceI; -import jalview.datamodel.StructureViewerModel; -import jalview.structure.StructureSelectionManager; - -import java.awt.Rectangle; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; @@ -35,6 +27,13 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import jalview.api.structures.JalviewStructureDisplayI; +import jalview.bin.Cache; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.datamodel.StructureViewerModel; +import jalview.structure.StructureSelectionManager; + /** * A proxy for handling structure viewers, that orchestrates adding selected * structures, associated with sequences in Jalview, to an existing viewer, or @@ -56,7 +55,7 @@ public class StructureViewer public enum ViewerType { - JMOL, CHIMERA + JMOL, CHIMERA, CHIMERAX, PYMOL }; /** @@ -165,6 +164,15 @@ public class StructureViewer sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs, ap); } + else if (viewerType.equals(ViewerType.CHIMERAX)) + { + sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs, + ap); + } + else if (viewerType.equals(ViewerType.PYMOL)) + { + sview = new PymolViewer(pdbsForFile, superposeAdded, theSeqs, ap); + } else { Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString()); @@ -305,6 +313,14 @@ public class StructureViewer { sview = new ChimeraViewFrame(pdb, seqsForPdb, null, ap); } + else if (viewerType.equals(ViewerType.CHIMERAX)) + { + sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap); + } + else if (viewerType.equals(ViewerType.PYMOL)) + { + sview = new PymolViewer(pdb, seqsForPdb, null, ap); + } else { Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString()); @@ -313,42 +329,41 @@ public class StructureViewer } /** - * Create a new panel controlling a structure viewer. + * Creates a new panel controlling a structure viewer * * @param type - * @param pdbf - * @param id - * @param sq * @param alignPanel * @param viewerData - * @param fileloc - * @param rect + * @param sessionFile * @param vid * @return */ - public JalviewStructureDisplayI createView(ViewerType type, String[] pdbf, - String[] id, SequenceI[][] sq, AlignmentPanel alignPanel, - StructureViewerModel viewerData, String fileloc, Rectangle rect, - String vid) + public static JalviewStructureDisplayI createView(ViewerType type, + AlignmentPanel alignPanel, StructureViewerModel viewerData, + String sessionFile, String vid) { - final boolean useinViewerSuperpos = viewerData.isAlignWithPanel(); - final boolean usetoColourbyseq = viewerData.isColourWithAlignPanel(); - final boolean viewerColouring = viewerData.isColourByViewer(); - + JalviewStructureDisplayI viewer = null; switch (type) { case JMOL: - sview = new AppJmol(pdbf, id, sq, alignPanel, usetoColourbyseq, - useinViewerSuperpos, viewerColouring, fileloc, rect, vid); + viewer = new AppJmol(viewerData, alignPanel, sessionFile, vid); + // todo or construct and then openSession(sessionFile)? break; case CHIMERA: - Cache.log.error( - "Unsupported structure viewer type " + type.toString()); + viewer = new ChimeraViewFrame(viewerData, alignPanel, sessionFile, + vid); + break; + case CHIMERAX: + viewer = new ChimeraXViewFrame(viewerData, alignPanel, sessionFile, + vid); + break; + case PYMOL: + viewer = new PymolViewer(viewerData, alignPanel, sessionFile, vid); break; default: Cache.log.error(UNKNOWN_VIEWER_TYPE + type.toString()); } - return sview; + return viewer; } public boolean isBusy() diff --git a/src/jalview/gui/StructureViewerBase.java b/src/jalview/gui/StructureViewerBase.java index 418a84d..33a122c 100644 --- a/src/jalview/gui/StructureViewerBase.java +++ b/src/jalview/gui/StructureViewerBase.java @@ -20,26 +20,6 @@ */ package jalview.gui; -import jalview.api.AlignmentViewPanel; -import jalview.bin.Cache; -import jalview.datamodel.Alignment; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; -import jalview.datamodel.PDBEntry; -import jalview.datamodel.SequenceI; -import jalview.gui.JalviewColourChooser.ColourChooserListener; -import jalview.gui.StructureViewer.ViewerType; -import jalview.gui.ViewSelectionMenu.ViewSetProvider; -import jalview.io.DataSourceType; -import jalview.io.JalviewFileChooser; -import jalview.io.JalviewFileView; -import jalview.jbgui.GStructureViewer; -import jalview.schemes.ColourSchemeI; -import jalview.schemes.ColourSchemes; -import jalview.structure.StructureMapping; -import jalview.structures.models.AAStructureBindingModel; -import jalview.util.MessageManager; - import java.awt.Color; import java.awt.Component; import java.awt.event.ActionEvent; @@ -54,6 +34,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Random; import java.util.Vector; import javax.swing.ButtonGroup; @@ -64,6 +45,26 @@ import javax.swing.JRadioButtonMenuItem; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; +import jalview.api.AlignmentViewPanel; +import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceI; +import jalview.gui.JalviewColourChooser.ColourChooserListener; +import jalview.gui.StructureViewer.ViewerType; +import jalview.gui.ViewSelectionMenu.ViewSetProvider; +import jalview.io.DataSourceType; +import jalview.io.JalviewFileChooser; +import jalview.io.JalviewFileView; +import jalview.jbgui.GStructureViewer; +import jalview.schemes.ColourSchemeI; +import jalview.schemes.ColourSchemes; +import jalview.structure.StructureMapping; +import jalview.structures.models.AAStructureBindingModel; +import jalview.util.BrowserLauncher; +import jalview.util.MessageManager; +import jalview.ws.dbsources.Pdb; + /** * Base class with common functionality for JMol, Chimera or other structure * viewers. @@ -90,13 +91,13 @@ public abstract class StructureViewerBase extends GStructureViewer /** * list of alignment panels to use for superposition */ - protected Vector _alignwith = new Vector<>(); + protected Vector _alignwith = new Vector<>(); /** * list of alignment panels that are used for colouring structures by aligned * sequences */ - protected Vector _colourwith = new Vector<>(); + protected Vector _colourwith = new Vector<>(); private String viewId = null; @@ -121,6 +122,10 @@ public abstract class StructureViewerBase extends GStructureViewer */ protected volatile boolean seqColoursApplied = false; + private IProgressIndicator progressBar = null; + + private Random random = new Random(); + /** * Default constructor */ @@ -159,13 +164,14 @@ public abstract class StructureViewerBase extends GStructureViewer return _aps.contains(ap2.av.getSequenceSetId()); } - public boolean isUsedforaligment(AlignmentPanel ap2) + public boolean isUsedforaligment(AlignmentViewPanel ap2) { return (_alignwith != null) && _alignwith.contains(ap2); } - public boolean isUsedforcolourby(AlignmentPanel ap2) + @Override + public boolean isUsedForColourBy(AlignmentViewPanel ap2) { return (_colourwith != null) && _colourwith.contains(ap2); } @@ -193,8 +199,6 @@ public abstract class StructureViewerBase extends GStructureViewer this.viewId = viewId; } - public abstract String getStateInfo(); - protected void buildActionMenu() { if (_alignwith == null) @@ -215,6 +219,7 @@ public abstract class StructureViewerBase extends GStructureViewer } } + @Override public AlignmentPanel getAlignmentPanel() { return ap; @@ -267,7 +272,8 @@ public abstract class StructureViewerBase extends GStructureViewer * * @param nap */ - public void removeAlignmentPanel(AlignmentPanel nap) + @Override + public void removeAlignmentPanel(AlignmentViewPanel nap) { try { @@ -339,8 +345,6 @@ public abstract class StructureViewerBase extends GStructureViewer public abstract ViewerType getViewerType(); - protected abstract IProgressIndicator getIProgressIndicator(); - /** * add a new structure (with associated sequences and chains) to this viewer, * retrieving it if necessary first. @@ -449,7 +453,7 @@ public abstract class StructureViewerBase extends GStructureViewer * create the mappings */ apanel.getStructureSelectionManager().setMapping(seq, chains, - pdbFilename, DataSourceType.FILE, getIProgressIndicator()); + pdbFilename, DataSourceType.FILE, getProgressIndicator()); /* * alert the FeatureRenderer to show new (PDB RESNUM) features @@ -554,8 +558,6 @@ public abstract class StructureViewerBase extends GStructureViewer } } - abstract void showSelectedChains(); - /** * Action on selecting one of Jalview's registered colour schemes */ @@ -566,7 +568,7 @@ public abstract class StructureViewerBase extends GStructureViewer ColourSchemeI cs = ColourSchemes.getInstance() .getColourScheme(colourSchemeName, getAlignmentPanel().av, al, null); - getBinding().setJalviewColourScheme(cs); + getBinding().colourByJalviewColourScheme(cs); } /** @@ -600,7 +602,7 @@ public abstract class StructureViewerBase extends GStructureViewer @Override public void actionPerformed(ActionEvent actionEvent) { - viewerColour_actionPerformed(actionEvent); + viewerColour_actionPerformed(); } }); colourMenu.add(viewerColour); @@ -616,7 +618,7 @@ public abstract class StructureViewerBase extends GStructureViewer @Override public void actionPerformed(ActionEvent actionEvent) { - background_actionPerformed(actionEvent); + background_actionPerformed(); } }); colourMenu.add(backGround); @@ -647,7 +649,7 @@ public abstract class StructureViewerBase extends GStructureViewer @Override public void actionPerformed(ActionEvent actionEvent) { - seqColour_actionPerformed(actionEvent); + seqColour_actionPerformed(); } }); @@ -659,7 +661,7 @@ public abstract class StructureViewerBase extends GStructureViewer @Override public void actionPerformed(ActionEvent actionEvent) { - chainColour_actionPerformed(actionEvent); + chainColour_actionPerformed(); } }); @@ -671,12 +673,15 @@ public abstract class StructureViewerBase extends GStructureViewer @Override public void actionPerformed(ActionEvent actionEvent) { - chargeColour_actionPerformed(actionEvent); + chargeColour_actionPerformed(); } }); viewerColour = new JRadioButtonMenuItem(); - // text is set in overrides of this method + viewerColour + .setText(MessageManager.getString("label.colour_with_viewer")); + viewerColour.setToolTipText(MessageManager + .getString("label.let_viewer_manage_structure_colours")); viewerColour.setName(ViewerColour.ByViewer.name()); viewerColour.setSelected(!binding.isColourBySequence()); @@ -702,8 +707,8 @@ public abstract class StructureViewerBase extends GStructureViewer } else { - // update the Chimera display now. - seqColour_actionPerformed(null); + // update the viewer display now. + seqColour_actionPerformed(); } } }); @@ -714,10 +719,18 @@ public abstract class StructureViewerBase extends GStructureViewer @Override public void itemStateChanged(ItemEvent e) { - alignStructs.setEnabled(!_alignwith.isEmpty()); - alignStructs.setToolTipText(MessageManager.formatMessage( - "label.align_structures_using_linked_alignment_views", - _alignwith.size())); + if (_alignwith.isEmpty()) + { + alignStructs.setEnabled(false); + alignStructs.setToolTipText(null); + } + else + { + alignStructs.setEnabled(true); + alignStructs.setToolTipText(MessageManager.formatMessage( + "label.align_structures_using_linked_alignment_views", + _alignwith.size())); + } } }; viewSelectionMenu = new ViewSelectionMenu( @@ -744,13 +757,11 @@ public abstract class StructureViewerBase extends GStructureViewer } }); - buildColourMenu(); - } + viewerActionMenu.setText(getViewerName()); + helpItem.setText(MessageManager.formatMessage("label.viewer_help", + getViewerName())); - @Override - public void setJalviewColourScheme(ColourSchemeI cs) - { - getBinding().setJalviewColourScheme(cs); + buildColourMenu(); } /** @@ -759,12 +770,7 @@ public abstract class StructureViewerBase extends GStructureViewer * the operation. */ @Override - protected String alignStructs_actionPerformed(ActionEvent actionEvent) - { - return alignStructs_withAllAlignPanels(); - } - - protected String alignStructs_withAllAlignPanels() + protected String alignStructsWithAllAlignPanels() { if (getAlignmentPanel() == null) { @@ -779,19 +785,8 @@ public abstract class StructureViewerBase extends GStructureViewer String reply = null; try { - AlignmentI[] als = new Alignment[_alignwith.size()]; - HiddenColumns[] alc = new HiddenColumns[_alignwith.size()]; - int[] alm = new int[_alignwith.size()]; - int a = 0; - - for (AlignmentPanel alignPanel : _alignwith) - { - als[a] = alignPanel.av.getAlignment(); - alm[a] = -1; - alc[a++] = alignPanel.av.getAlignment().getHiddenColumns(); - } - reply = getBinding().superposeStructures(als, alm, alc); - if (reply != null) + reply = getBinding().superposeStructures(_alignwith); + if (reply != null && !reply.isEmpty()) { String text = MessageManager .formatMessage("error.superposition_failed", reply); @@ -800,9 +795,9 @@ public abstract class StructureViewerBase extends GStructureViewer } catch (Exception e) { StringBuffer sp = new StringBuffer(); - for (AlignmentPanel alignPanel : _alignwith) + for (AlignmentViewPanel alignPanel : _alignwith) { - sp.append("'" + alignPanel.alignFrame.getTitle() + "' "); + sp.append("'" + alignPanel.getViewName() + "' "); } Cache.log.info("Couldn't align structures with the " + sp.toString() + "associated alignment panels.", e); @@ -815,7 +810,7 @@ public abstract class StructureViewerBase extends GStructureViewer * background of the structure viewer */ @Override - public void background_actionPerformed(ActionEvent actionEvent) + public void background_actionPerformed() { String ttl = MessageManager.getString("label.select_background_colour"); ColourChooserListener listener = new ColourChooserListener() @@ -830,7 +825,7 @@ public abstract class StructureViewerBase extends GStructureViewer } @Override - public void viewerColour_actionPerformed(ActionEvent actionEvent) + public void viewerColour_actionPerformed() { if (viewerColour.isSelected()) { @@ -840,21 +835,21 @@ public abstract class StructureViewerBase extends GStructureViewer } @Override - public void chainColour_actionPerformed(ActionEvent actionEvent) + public void chainColour_actionPerformed() { chainColour.setSelected(true); getBinding().colourByChain(); } @Override - public void chargeColour_actionPerformed(ActionEvent actionEvent) + public void chargeColour_actionPerformed() { chargeColour.setSelected(true); getBinding().colourByCharge(); } @Override - public void seqColour_actionPerformed(ActionEvent actionEvent) + public void seqColour_actionPerformed() { AAStructureBindingModel binding = getBinding(); binding.setColourBySequence(seqColour.isSelected()); @@ -873,7 +868,7 @@ public abstract class StructureViewerBase extends GStructureViewer } } // Set the colour using the current view for the associated alignframe - for (AlignmentPanel alignPanel : _colourwith) + for (AlignmentViewPanel alignPanel : _colourwith) { binding.colourBySequence(alignPanel); } @@ -882,7 +877,7 @@ public abstract class StructureViewerBase extends GStructureViewer } @Override - public void pdbFile_actionPerformed(ActionEvent actionEvent) + public void pdbFile_actionPerformed() { // TODO: JAL-3048 not needed for Jalview-JS - save PDB file JalviewFileChooser chooser = new JalviewFileChooser( @@ -934,7 +929,7 @@ public abstract class StructureViewerBase extends GStructureViewer } @Override - public void viewMapping_actionPerformed(ActionEvent actionEvent) + public void viewMapping_actionPerformed() { CutAndPasteTransfer cap = new CutAndPasteTransfer(); try @@ -975,7 +970,7 @@ public abstract class StructureViewerBase extends GStructureViewer * enable 'Superpose with' if more than one mapped structure */ viewSelectionMenu.setEnabled(false); - if (getBinding().getStructureFiles().length > 1 + if (getBinding().getMappedStructureCount() > 1 && getBinding().getSequence().length > 1) { viewSelectionMenu.setEnabled(true); @@ -996,7 +991,7 @@ public abstract class StructureViewerBase extends GStructureViewer if (!binding.isLoadingFromArchive()) { - seqColour_actionPerformed(null); + seqColour_actionPerformed(); } } @@ -1049,4 +1044,199 @@ public abstract class StructureViewerBase extends GStructureViewer toFront(); } + @Override + public long startProgressBar(String msg) + { + // TODO would rather have startProgress/stopProgress as the + // IProgressIndicator interface + long tm = random.nextLong(); + if (progressBar != null) + { + progressBar.setProgressBar(msg, tm); + } + return tm; + } + + @Override + public void stopProgressBar(String msg, long handle) + { + if (progressBar != null) + { + progressBar.setProgressBar(msg, handle); + } + } + + protected IProgressIndicator getProgressIndicator() + { + return progressBar; + } + + protected void setProgressIndicator(IProgressIndicator pi) + { + progressBar = pi; + } + + protected void setProgressMessage(String message, long id) + { + if (progressBar != null) + { + progressBar.setProgressBar(message, id); + } + } + + @Override + public void showConsole(boolean show) + { + // default does nothing + } + + /** + * Show only the selected chain(s) in the viewer + */ + protected void showSelectedChains() + { + List toshow = new ArrayList<>(); + for (int i = 0; i < chainMenu.getItemCount(); i++) + { + if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem) + { + JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i); + if (item.isSelected()) + { + toshow.add(item.getText()); + } + } + } + getBinding().showChains(toshow); + } + + /** + * Tries to fetch a PDB file and save to a temporary local file. Returns the + * saved file path if successful, or null if not. + * + * @param processingEntry + * @return + */ + protected String fetchPdbFile(PDBEntry processingEntry) + { + String filePath = null; + Pdb pdbclient = new Pdb(); + AlignmentI pdbseq = null; + String pdbid = processingEntry.getId(); + long handle = System.currentTimeMillis() + + Thread.currentThread().hashCode(); + + /* + * Write 'fetching PDB' progress on AlignFrame as we are not yet visible + */ + String msg = MessageManager.formatMessage("status.fetching_pdb", + new Object[] + { pdbid }); + getAlignmentPanel().alignFrame.setProgressBar(msg, handle); + // long hdl = startProgressBar(MessageManager.formatMessage( + // "status.fetching_pdb", new Object[] + // { pdbid })); + try + { + pdbseq = pdbclient.getSequenceRecords(pdbid); + } catch (Exception e) + { + System.err.println( + "Error retrieving PDB id " + pdbid + ": " + e.getMessage()); + } finally + { + msg = pdbid + " " + MessageManager.getString("label.state_completed"); + getAlignmentPanel().alignFrame.setProgressBar(msg, handle); + // stopProgressBar(msg, hdl); + } + /* + * If PDB data were saved and are not invalid (empty alignment), return the + * file path. + */ + if (pdbseq != null && pdbseq.getHeight() > 0) + { + // just use the file name from the first sequence's first PDBEntry + filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries() + .elementAt(0).getFile()).getAbsolutePath(); + processingEntry.setFile(filePath); + } + return filePath; + } + + /** + * If supported, saves the state of the structure viewer to a temporary file + * and returns the file, else returns null + * + * @return + */ + public File saveSession() + { + // TODO: a wait loop to ensure the file is written fully before returning? + return getBinding() == null ? null : getBinding().saveSession(); + } + + /** + * Close down this instance of Jalview's Chimera viewer, giving the user the + * option to close the associated Chimera window (process). They may wish to + * keep it open until they have had an opportunity to save any work. + * + * @param forceClose + * if true, close any linked Chimera process; if false, prompt first + */ + @Override + public void closeViewer(boolean forceClose) + { + AAStructureBindingModel binding = getBinding(); + if (binding != null && binding.isViewerRunning()) + { + if (!forceClose) + { + String viewerName = getViewerName(); + String prompt = MessageManager + .formatMessage("label.confirm_close_viewer", new Object[] + { binding.getViewerTitle(viewerName, false), viewerName }); + prompt = JvSwingUtils.wrapTooltip(true, prompt); + int confirm = JvOptionPane.showConfirmDialog(this, prompt, + MessageManager.getString("label.close_viewer"), + JvOptionPane.YES_NO_CANCEL_OPTION); + /* + * abort closure if user hits escape or Cancel + */ + if (confirm == JvOptionPane.CANCEL_OPTION + || confirm == JvOptionPane.CLOSED_OPTION) + { + return; + } + forceClose = confirm == JvOptionPane.YES_OPTION; + } + binding.closeViewer(forceClose); + } + setAlignmentPanel(null); + _aps.clear(); + _alignwith.clear(); + _colourwith.clear(); + // TODO: check for memory leaks where instance isn't finalised because jmb + // holds a reference to the window + // jmb = null; + dispose(); + } + + @Override + public void showHelp_actionPerformed() + { + try + { + String url = getBinding().getHelpURL(); + if (url != null) + { + BrowserLauncher.openURL(url); + } + } catch (IOException ex) + { + System.err + .println("Show " + getViewerName() + " failed with: " + + ex.getMessage()); + } + } + } diff --git a/src/jalview/gui/ViewSelectionMenu.java b/src/jalview/gui/ViewSelectionMenu.java index 2a7743a..a1529fc 100644 --- a/src/jalview/gui/ViewSelectionMenu.java +++ b/src/jalview/gui/ViewSelectionMenu.java @@ -20,6 +20,7 @@ */ package jalview.gui; +import jalview.api.AlignmentViewPanel; import jalview.util.MessageManager; import java.awt.Component; @@ -56,7 +57,7 @@ public class ViewSelectionMenu extends JMenu private ViewSetProvider _allviews; - private List _selectedviews; + private List _selectedviews; private ItemListener _handler; @@ -79,7 +80,7 @@ public class ViewSelectionMenu extends JMenu * selection/deselection state */ public ViewSelectionMenu(String title, final ViewSetProvider allviews, - final List selectedviews, + final List selectedviews, final ItemListener handler) { super(title); diff --git a/src/jalview/javascript/MouseOverStructureListener.java b/src/jalview/javascript/MouseOverStructureListener.java index 6071933..8d83e75 100644 --- a/src/jalview/javascript/MouseOverStructureListener.java +++ b/src/jalview/javascript/MouseOverStructureListener.java @@ -30,7 +30,6 @@ import jalview.ext.jmol.JmolCommands; import jalview.structure.AtomSpec; import jalview.structure.StructureListener; import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; import jalview.util.HttpUtils; @@ -220,21 +219,22 @@ public class MouseOverStructureListener extends JSFunctionExec // Form a colour command from the given alignment panel for each distinct // structure - ArrayList ccomands = new ArrayList(); - ArrayList pdbfn = new ArrayList(); - StructureMappingcommandSet[] colcommands = JmolCommands - .getColourBySequenceCommand(ssm, modelSet, sequence, sr, + ArrayList ccomands = new ArrayList<>(); + ArrayList pdbfn = new ArrayList<>(); + String[] colcommands = new JmolCommands() + .colourBySequence(ssm, modelSet, sequence, sr, (AlignmentViewPanel) source); if (colcommands == null) { return; } int sz = 0; - for (jalview.structure.StructureMappingcommandSet ccset : colcommands) + // for (jalview.structure.StructureMappingcommandSet ccset : colcommands) + for (String command : colcommands) { - sz += ccset.commands.length; - ccomands.add(ccset.commands); - pdbfn.add(ccset.mapping); + // sz += ccset.commands.length; + // ccomands.add(command); // ccset.commands); + // pdbfn.add(ccset.mapping); } String mclass, mhandle; diff --git a/src/jalview/jbgui/GPreferences.java b/src/jalview/jbgui/GPreferences.java index 6de3888..ae6727a 100755 --- a/src/jalview/jbgui/GPreferences.java +++ b/src/jalview/jbgui/GPreferences.java @@ -20,22 +20,6 @@ */ package jalview.jbgui; -import jalview.bin.Cache; -import jalview.fts.core.FTSDataColumnPreferences; -import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource; -import jalview.fts.service.pdb.PDBFTSRestClient; -import jalview.gui.Desktop; -import jalview.gui.JalviewBooleanRadioButtons; -import jalview.gui.JvOptionPane; -import jalview.gui.JvSwingUtils; -import jalview.gui.StructureViewer.ViewerType; -import jalview.io.BackupFilenameParts; -import jalview.io.BackupFiles; -import jalview.io.BackupFilesPresetEntry; -import jalview.io.IntKeyStringValueEntry; -import jalview.util.MessageManager; -import jalview.util.Platform; - import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -87,6 +71,22 @@ import javax.swing.event.ChangeListener; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; +import jalview.bin.Cache; +import jalview.fts.core.FTSDataColumnPreferences; +import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource; +import jalview.fts.service.pdb.PDBFTSRestClient; +import jalview.gui.Desktop; +import jalview.gui.JalviewBooleanRadioButtons; +import jalview.gui.JvOptionPane; +import jalview.gui.JvSwingUtils; +import jalview.gui.StructureViewer.ViewerType; +import jalview.io.BackupFilenameParts; +import jalview.io.BackupFiles; +import jalview.io.BackupFilesPresetEntry; +import jalview.io.IntKeyStringValueEntry; +import jalview.util.MessageManager; +import jalview.util.Platform; + /** * Base class for the Preferences panel. * @@ -180,7 +180,9 @@ public class GPreferences extends JPanel protected JComboBox structViewer = new JComboBox<>(); - protected JTextField chimeraPath = new JTextField(); + protected JLabel structureViewerPathLabel; + + protected JTextField structureViewerPath = new JTextField(); protected ButtonGroup mappingMethod = new ButtonGroup(); @@ -1233,7 +1235,7 @@ public class GPreferences extends JPanel structureTab.setBorder(new TitledBorder( MessageManager.getString("label.structure_options"))); structureTab.setLayout(null); - final int width = 400; + final int width = 420; final int height = 22; final int lineSpacing = 25; int ypos = 15; @@ -1281,13 +1283,19 @@ public class GPreferences extends JPanel viewerLabel.setFont(LABEL_FONT); viewerLabel.setHorizontalAlignment(SwingConstants.LEFT); viewerLabel.setText(MessageManager.getString("label.structure_viewer")); - viewerLabel.setBounds(new Rectangle(10, ypos, 200, height)); + viewerLabel.setBounds(new Rectangle(10, ypos, 220, height)); structureTab.add(viewerLabel); + /* + * add all external viewers as options here - check + * when selected whether the program is installed + */ structViewer.setFont(LABEL_FONT); - structViewer.setBounds(new Rectangle(160, ypos, 120, height)); + structViewer.setBounds(new Rectangle(190, ypos, 120, height)); structViewer.addItem(ViewerType.JMOL.name()); structViewer.addItem(ViewerType.CHIMERA.name()); + structViewer.addItem(ViewerType.CHIMERAX.name()); + structViewer.addItem(ViewerType.PYMOL.name()); structViewer.addActionListener(new ActionListener() { @Override @@ -1300,35 +1308,38 @@ public class GPreferences extends JPanel structureTab.add(structViewer); ypos += lineSpacing; - JLabel pathLabel = new JLabel(); - pathLabel.setFont(new java.awt.Font("SansSerif", 0, 11)); - pathLabel.setHorizontalAlignment(SwingConstants.LEFT); - pathLabel.setText(MessageManager.getString("label.chimera_path")); - pathLabel.setBounds(new Rectangle(10, ypos, 140, height)); - structureTab.add(pathLabel); - - chimeraPath.setFont(LABEL_FONT); - chimeraPath.setText(""); + structureViewerPathLabel = new JLabel(); + structureViewerPathLabel.setFont(LABEL_FONT);// new Font("SansSerif", 0, 11)); + structureViewerPathLabel.setHorizontalAlignment(SwingConstants.LEFT); + structureViewerPathLabel.setText(MessageManager + .formatMessage("label.viewer_path", "Chimera(X)")); + structureViewerPathLabel.setBounds(new Rectangle(10, ypos, 170, height)); + structureViewerPathLabel.setEnabled(false); + structureTab.add(structureViewerPathLabel); + + structureViewerPath.setFont(LABEL_FONT); + structureViewerPath.setText(""); + structureViewerPath.setEnabled(false); final String tooltip = JvSwingUtils.wrapTooltip(true, - MessageManager.getString("label.chimera_path_tip")); - chimeraPath.setToolTipText(tooltip); - chimeraPath.setBounds(new Rectangle(160, ypos, 300, height)); - chimeraPath.addMouseListener(new MouseAdapter() + MessageManager.getString("label.viewer_path_tip")); + structureViewerPath.setToolTipText(tooltip); + structureViewerPath.setBounds(new Rectangle(190, ypos, 290, height)); + structureViewerPath.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) + if (structureViewerPath.isEnabled() && e.getClickCount() == 2) { String chosen = openFileChooser(); if (chosen != null) { - chimeraPath.setText(chosen); + structureViewerPath.setText(chosen); } } } }); - structureTab.add(chimeraPath); + structureTab.add(structureViewerPath); ypos += lineSpacing; nwMapping.setFont(LABEL_FONT); @@ -1343,7 +1354,7 @@ public class GPreferences extends JPanel MessageManager.getString("label.mapping_method")); mmTitledBorder.setTitleFont(LABEL_FONT); mappingPanel.setBorder(mmTitledBorder); - mappingPanel.setBounds(new Rectangle(10, ypos, 452, 45)); + mappingPanel.setBounds(new Rectangle(10, ypos, 472, 45)); // GridLayout mappingLayout = new GridLayout(); mappingPanel.setLayout(new GridLayout()); mappingPanel.add(nwMapping); @@ -1354,7 +1365,7 @@ public class GPreferences extends JPanel ypos += lineSpacing; FTSDataColumnPreferences docFieldPref = new FTSDataColumnPreferences( PreferenceSource.PREFERENCES, PDBFTSRestClient.getInstance()); - docFieldPref.setBounds(new Rectangle(10, ypos, 450, 120)); + docFieldPref.setBounds(new Rectangle(10, ypos, 470, 120)); structureTab.add(docFieldPref); /* @@ -1362,8 +1373,8 @@ public class GPreferences extends JPanel */ if (Platform.isJS()) { - pathLabel.setVisible(false); - chimeraPath.setVisible(false); + structureViewerPathLabel.setVisible(false); + structureViewerPath.setVisible(false); viewerLabel.setVisible(false); structViewer.setVisible(false); } diff --git a/src/jalview/jbgui/GStructureViewer.java b/src/jalview/jbgui/GStructureViewer.java index dfee3e2..73180ee 100644 --- a/src/jalview/jbgui/GStructureViewer.java +++ b/src/jalview/jbgui/GStructureViewer.java @@ -20,11 +20,6 @@ */ package jalview.jbgui; -import jalview.api.structures.JalviewStructureDisplayI; -import jalview.gui.ColourMenuHelper.ColourChangeListener; -import jalview.util.ImageMaker.TYPE; -import jalview.util.MessageManager; - import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; @@ -38,6 +33,11 @@ import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JRadioButtonMenuItem; +import jalview.api.structures.JalviewStructureDisplayI; +import jalview.gui.ColourMenuHelper.ColourChangeListener; +import jalview.util.ImageMaker.TYPE; +import jalview.util.MessageManager; + @SuppressWarnings("serial") public abstract class GStructureViewer extends JInternalFrame implements JalviewStructureDisplayI, ColourChangeListener @@ -109,7 +109,7 @@ public abstract class GStructureViewer extends JInternalFrame @Override public void actionPerformed(ActionEvent actionEvent) { - pdbFile_actionPerformed(actionEvent); + pdbFile_actionPerformed(); } }); @@ -142,7 +142,7 @@ public abstract class GStructureViewer extends JInternalFrame @Override public void actionPerformed(ActionEvent actionEvent) { - viewMapping_actionPerformed(actionEvent); + viewMapping_actionPerformed(); } }); @@ -166,13 +166,12 @@ public abstract class GStructureViewer extends JInternalFrame JMenu helpMenu = new JMenu(); helpMenu.setText(MessageManager.getString("action.help")); helpItem = new JMenuItem(); - helpItem.setText(MessageManager.getString("label.jmol_help")); helpItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { - showHelp_actionPerformed(actionEvent); + showHelp_actionPerformed(); } }); alignStructs = new JMenuItem(); @@ -183,7 +182,7 @@ public abstract class GStructureViewer extends JInternalFrame @Override public void actionPerformed(ActionEvent actionEvent) { - alignStructs_actionPerformed(actionEvent); + alignStructsWithAllAlignPanels(); } }); @@ -221,14 +220,13 @@ public abstract class GStructureViewer extends JInternalFrame { } - protected void viewerColour_actionPerformed(ActionEvent actionEvent) + protected void viewerColour_actionPerformed() { } - protected abstract String alignStructs_actionPerformed( - ActionEvent actionEvent); + protected abstract String alignStructsWithAllAlignPanels(); - public void pdbFile_actionPerformed(ActionEvent actionEvent) + public void pdbFile_actionPerformed() { } @@ -238,32 +236,32 @@ public abstract class GStructureViewer extends JInternalFrame } - public void viewMapping_actionPerformed(ActionEvent actionEvent) + public void viewMapping_actionPerformed() { } - public void seqColour_actionPerformed(ActionEvent actionEvent) + public void seqColour_actionPerformed() { } - public void chainColour_actionPerformed(ActionEvent actionEvent) + public void chainColour_actionPerformed() { } - public void chargeColour_actionPerformed(ActionEvent actionEvent) + public void chargeColour_actionPerformed() { } - public void background_actionPerformed(ActionEvent actionEvent) + public void background_actionPerformed() { } - public void showHelp_actionPerformed(ActionEvent actionEvent) + public void showHelp_actionPerformed() { } diff --git a/src/jalview/project/Jalview2XML.java b/src/jalview/project/Jalview2XML.java index 6340e64..ccd9ab0 100644 --- a/src/jalview/project/Jalview2XML.java +++ b/src/jalview/project/Jalview2XML.java @@ -24,6 +24,55 @@ import static jalview.math.RotatableMatrix.Axis.X; import static jalview.math.RotatableMatrix.Axis.Y; import static jalview.math.RotatableMatrix.Axis.Z; +import java.awt.Color; +import java.awt.Font; +import java.awt.Rectangle; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Vector; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; + +import javax.swing.JInternalFrame; +import javax.swing.SwingUtilities; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.Marshaller; +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; + import jalview.analysis.Conservation; import jalview.analysis.PCA; import jalview.analysis.scoremodels.ScoreModels; @@ -58,7 +107,6 @@ import jalview.gui.AlignFrame; import jalview.gui.AlignViewport; import jalview.gui.AlignmentPanel; import jalview.gui.AppVarna; -import jalview.gui.ChimeraViewFrame; import jalview.gui.Desktop; import jalview.gui.JvOptionPane; import jalview.gui.OOMWarning; @@ -150,55 +198,6 @@ import jalview.xml.binding.jalview.SequenceSet.SequenceSetProperties; import jalview.xml.binding.jalview.ThresholdType; import jalview.xml.binding.jalview.VAMSAS; -import java.awt.Color; -import java.awt.Font; -import java.awt.Rectangle; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.lang.reflect.InvocationTargetException; -import java.math.BigInteger; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.Vector; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.jar.JarOutputStream; - -import javax.swing.JInternalFrame; -import javax.swing.SwingUtilities; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.Marshaller; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.XMLGregorianCalendar; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamReader; - /** * Write out the current jalview desktop state as a Jalview XML stream. * @@ -1089,25 +1088,28 @@ public class Jalview2XML if (frames[f] instanceof StructureViewerBase) { StructureViewerBase viewFrame = (StructureViewerBase) frames[f]; - matchedFile = saveStructureState(ap, jds, pdb, entry, viewIds, + matchedFile = saveStructureViewer(ap, jds, pdb, entry, viewIds, matchedFile, viewFrame); /* * Only store each structure viewer's state once in the project * jar. First time through only (storeDS==false) */ String viewId = viewFrame.getViewId(); + String viewerType = viewFrame.getViewerType().toString(); if (!storeDS && !viewIds.contains(viewId)) { viewIds.add(viewId); - try + File viewerState = viewFrame.saveSession(); + if (viewerState != null) { - String viewerState = viewFrame.getStateInfo(); - writeJarEntry(jout, getViewerJarEntryName(viewId), - viewerState.getBytes()); - } catch (IOException e) + copyFileToJar(jout, viewerState.getPath(), + getViewerJarEntryName(viewId), viewerType); + } + else { - System.err.println( - "Error saving viewer state: " + e.getMessage()); + Cache.log.error("Failed to save viewer state for " + + + viewerType); } } } @@ -1129,7 +1131,7 @@ public class Jalview2XML if (!pdbfiles.contains(pdbId)) { pdbfiles.add(pdbId); - copyFileToJar(jout, matchedFile, pdbId); + copyFileToJar(jout, matchedFile, pdbId, pdbId); } } @@ -1676,7 +1678,7 @@ public class Jalview2XML // using save and then load try { - fileName = fileName.replace('\\', '/'); + fileName = fileName.replace('\\', '/'); System.out.println("Writing jar entry " + fileName); JarEntry entry = new JarEntry(fileName); jout.putNextEntry(entry); @@ -1977,7 +1979,7 @@ public class Jalview2XML String varnaStateFile = varna.getStateInfo(model.rna); jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter(); - copyFileToJar(jout, varnaStateFile, jarEntryName); + copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna"); rnaSessions.put(model, jarEntryName); } SecondaryStructure ss = new SecondaryStructure(); @@ -2001,59 +2003,48 @@ public class Jalview2XML * @param jout * @param infilePath * @param jarEntryName + * @param msg + * additional identifying info to log to the console */ protected void copyFileToJar(JarOutputStream jout, String infilePath, - String jarEntryName) + String jarEntryName, String msg) { - DataInputStream dis = null; - try + try (InputStream is = new FileInputStream(infilePath)) { File file = new File(infilePath); if (file.exists() && jout != null) { - dis = new DataInputStream(new FileInputStream(file)); - byte[] data = new byte[(int) file.length()]; - dis.readFully(data); - writeJarEntry(jout, jarEntryName, data); + System.out.println( + "Writing jar entry " + jarEntryName + " (" + msg + ")"); + jout.putNextEntry(new JarEntry(jarEntryName)); + copyAll(is, jout); + jout.closeEntry(); + // dis = new DataInputStream(new FileInputStream(file)); + // byte[] data = new byte[(int) file.length()]; + // dis.readFully(data); + // writeJarEntry(jout, jarEntryName, data); } } catch (Exception ex) { ex.printStackTrace(); - } finally - { - if (dis != null) - { - try - { - dis.close(); - } catch (IOException e) - { - // ignore - } - } } } /** - * Write the data to a new entry of given name in the output jar file + * Copies input to output, in 4K buffers; handles any data (text or binary) * - * @param jout - * @param jarEntryName - * @param data + * @param in + * @param out * @throws IOException */ - protected void writeJarEntry(JarOutputStream jout, String jarEntryName, - byte[] data) throws IOException + protected void copyAll(InputStream in, OutputStream out) + throws IOException { - if (jout != null) + byte[] buffer = new byte[4096]; + int bytesRead = 0; + while ((bytesRead = in.read(buffer)) != -1) { - jarEntryName = jarEntryName.replace('\\','/'); - System.out.println("Writing jar entry " + jarEntryName); - jout.putNextEntry(new JarEntry(jarEntryName)); - DataOutputStream dout = new DataOutputStream(jout); - dout.write(data, 0, data.length); - dout.flush(); - jout.closeEntry(); + out.write(buffer, 0, bytesRead); } } @@ -2070,7 +2061,7 @@ public class Jalview2XML * @param viewFrame * @return */ - protected String saveStructureState(AlignmentPanel ap, SequenceI jds, + protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds, Pdbids pdb, PDBEntry entry, List viewIds, String matchedFile, StructureViewerBase viewFrame) { @@ -2124,7 +2115,7 @@ public class Jalview2XML final String viewId = viewFrame.getViewId(); state.setViewId(viewId); state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap)); - state.setColourwithAlignPanel(viewFrame.isUsedforcolourby(ap)); + state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap)); state.setColourByJmol(viewFrame.isColouredByViewer()); state.setType(viewFrame.getViewerType().toString()); // pdb.addStructureState(state); @@ -3185,8 +3176,6 @@ public class Jalview2XML protected String copyJarEntry(jarInputStreamProvider jprovider, String jarEntryName, String prefix, String suffixModel) { - BufferedReader in = null; - PrintWriter out = null; String suffix = ".tmp"; if (suffixModel == null) { @@ -3197,33 +3186,24 @@ public class Jalview2XML { suffix = "." + suffixModel.substring(sfpos + 1); } - try - { - JarInputStream jin = jprovider.getJarInputStream(); - /* - * if (jprovider.startsWith("http://")) { jin = new JarInputStream(new - * URL(jprovider).openStream()); } else { jin = new JarInputStream(new - * FileInputStream(jprovider)); } - */ + try (JarInputStream jin = jprovider.getJarInputStream()) + { JarEntry entry = null; do { entry = jin.getNextJarEntry(); } while (entry != null && !entry.getName().equals(jarEntryName)); + if (entry != null) { - in = new BufferedReader(new InputStreamReader(jin, UTF_8)); + // in = new BufferedReader(new InputStreamReader(jin, UTF_8)); File outFile = File.createTempFile(prefix, suffix); outFile.deleteOnExit(); - out = new PrintWriter(new FileOutputStream(outFile)); - String data; - - while ((data = in.readLine()) != null) + try (OutputStream os = new FileOutputStream(outFile)) { - out.println(data); + copyAll(jin, os); } - out.flush(); String t = outFile.getAbsolutePath(); return t; } @@ -3234,22 +3214,6 @@ public class Jalview2XML } catch (Exception ex) { ex.printStackTrace(); - } finally - { - if (in != null) - { - try - { - in.close(); - } catch (IOException e) - { - // ignore - } - } - if (out != null) - { - out.close(); - } } return null; @@ -4320,10 +4284,15 @@ public class Jalview2XML } if (!structureViewers.containsKey(sviewid)) { + String viewerType = structureState.getType(); + if (viewerType == null) // pre Jalview 2.9 + { + viewerType = ViewerType.JMOL.toString(); + } structureViewers.put(sviewid, new StructureViewerModel(x, y, width, height, false, false, true, structureState.getViewId(), - structureState.getType())); + viewerType)); // Legacy pre-2.7 conversion JAL-823 : // do not assume any view has to be linked for colour by // sequence @@ -4424,246 +4393,17 @@ public class Jalview2XML return; } - /* - * From 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry - * "viewer_"+stateData.viewId - */ - if (ViewerType.CHIMERA.toString().equals(stateData.getType())) - { - createChimeraViewer(viewerData, af, jprovider); - } - else - { - /* - * else Jmol (if pre-2.9, stateData contains JMOL state string) - */ - createJmolViewer(viewerData, af, jprovider); - } - } - - /** - * Create a new Chimera viewer. - * - * @param data - * @param af - * @param jprovider - */ - protected void createChimeraViewer( - Entry viewerData, AlignFrame af, - jarInputStreamProvider jprovider) - { - StructureViewerModel data = viewerData.getValue(); - String chimeraSessionFile = data.getStateData(); - - /* - * Copy Chimera session from jar entry "viewer_"+viewId to a temporary file - * - * NB this is the 'saved' viewId as in the project file XML, _not_ the - * 'uniquified' sviewid used to reconstruct the viewer here - */ - String viewerJarEntryName = getViewerJarEntryName(data.getViewId()); - chimeraSessionFile = copyJarEntry(jprovider, viewerJarEntryName, - "chimera", ".py"); - - Set> fileData = data.getFileData() - .entrySet(); - List pdbs = new ArrayList<>(); - List allseqs = new ArrayList<>(); - for (Entry pdb : fileData) - { - String filePath = pdb.getValue().getFilePath(); - String pdbId = pdb.getValue().getPdbId(); - // pdbs.add(new PDBEntry(filePath, pdbId)); - pdbs.add(new PDBEntry(pdbId, null, PDBEntry.Type.PDB, filePath)); - final List seqList = pdb.getValue().getSeqList(); - SequenceI[] seqs = seqList.toArray(new SequenceI[seqList.size()]); - allseqs.add(seqs); - } - - boolean colourByChimera = data.isColourByViewer(); - boolean colourBySequence = data.isColourWithAlignPanel(); - - // TODO use StructureViewer as a factory here, see JAL-1761 - final PDBEntry[] pdbArray = pdbs.toArray(new PDBEntry[pdbs.size()]); - final SequenceI[][] seqsArray = allseqs - .toArray(new SequenceI[allseqs.size()][]); - String newViewId = viewerData.getKey(); - - ChimeraViewFrame cvf = new ChimeraViewFrame(chimeraSessionFile, - af.alignPanel, pdbArray, seqsArray, colourByChimera, - colourBySequence, newViewId); - cvf.setSize(data.getWidth(), data.getHeight()); - cvf.setLocation(data.getX(), data.getY()); - } - - /** - * Create a new Jmol window. First parse the Jmol state to translate filenames - * loaded into the view, and record the order in which files are shown in the - * Jmol view, so we can add the sequence mappings in same order. - * - * @param viewerData - * @param af - * @param jprovider - */ - protected void createJmolViewer( - final Entry viewerData, - AlignFrame af, jarInputStreamProvider jprovider) - { - final StructureViewerModel svattrib = viewerData.getValue(); - String state = svattrib.getStateData(); - - /* - * Pre-2.9: state element value is the Jmol state string - * - * 2.9+: @type is "JMOL", state data is in a Jar file member named "viewer_" - * + viewId - */ - if (ViewerType.JMOL.toString().equals(svattrib.getType())) - { - state = readJarEntry(jprovider, - getViewerJarEntryName(svattrib.getViewId())); - } - - List pdbfilenames = new ArrayList<>(); - List seqmaps = new ArrayList<>(); - List pdbids = new ArrayList<>(); - StringBuilder newFileLoc = new StringBuilder(64); - int cp = 0, ncp, ecp; - Map oldFiles = svattrib.getFileData(); - while ((ncp = state.indexOf("load ", cp)) > -1) - { - do - { - // look for next filename in load statement - newFileLoc.append(state.substring(cp, - ncp = (state.indexOf("\"", ncp + 1) + 1))); - String oldfilenam = state.substring(ncp, - ecp = state.indexOf("\"", ncp)); - // recover the new mapping data for this old filename - // have to normalize filename - since Jmol and jalview do - // filename - // translation differently. - StructureData filedat = oldFiles.get(new File(oldfilenam)); - if (filedat == null) - { - String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\"); - filedat = oldFiles.get(new File(reformatedOldFilename)); - } - newFileLoc.append(Platform.escapeBackslashes(filedat.getFilePath())); - pdbfilenames.add(filedat.getFilePath()); - pdbids.add(filedat.getPdbId()); - seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0])); - newFileLoc.append("\""); - cp = ecp + 1; // advance beyond last \" and set cursor so we can - // look for next file statement. - } while ((ncp = state.indexOf("/*file*/", cp)) > -1); - } - if (cp > 0) - { - // just append rest of state - newFileLoc.append(state.substring(cp)); - } - else - { - System.err.print("Ignoring incomplete Jmol state for PDB ids: "); - newFileLoc = new StringBuilder(state); - newFileLoc.append("; load append "); - for (File id : oldFiles.keySet()) - { - // add this and any other pdb files that should be present in - // the viewer - StructureData filedat = oldFiles.get(id); - newFileLoc.append(filedat.getFilePath()); - pdbfilenames.add(filedat.getFilePath()); - pdbids.add(filedat.getPdbId()); - seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0])); - newFileLoc.append(" \""); - newFileLoc.append(filedat.getFilePath()); - newFileLoc.append("\""); - - } - newFileLoc.append(";"); - } - - if (newFileLoc.length() == 0) - { - return; - } - int histbug = newFileLoc.indexOf("history = "); - if (histbug > -1) - { - /* - * change "history = [true|false];" to "history = [1|0];" - */ - histbug += 10; - int diff = histbug == -1 ? -1 : newFileLoc.indexOf(";", histbug); - String val = (diff == -1) ? null - : newFileLoc.substring(histbug, diff); - if (val != null && val.length() >= 4) - { - if (val.contains("e")) // eh? what can it be? - { - if (val.trim().equals("true")) - { - val = "1"; - } - else - { - val = "0"; - } - newFileLoc.replace(histbug, diff, val); - } - } - } - - final String[] pdbf = pdbfilenames - .toArray(new String[pdbfilenames.size()]); - final String[] id = pdbids.toArray(new String[pdbids.size()]); - final SequenceI[][] sq = seqmaps - .toArray(new SequenceI[seqmaps.size()][]); - final String fileloc = newFileLoc.toString(); - final String sviewid = viewerData.getKey(); - final AlignFrame alf = af; - final Rectangle rect = new Rectangle(svattrib.getX(), svattrib.getY(), - svattrib.getWidth(), svattrib.getHeight()); + String type = stateData.getType(); try { - javax.swing.SwingUtilities.invokeAndWait(new Runnable() - { - @Override - public void run() - { - JalviewStructureDisplayI sview = null; - try - { - sview = new StructureViewer( - alf.alignPanel.getStructureSelectionManager()) - .createView(StructureViewer.ViewerType.JMOL, - pdbf, id, sq, alf.alignPanel, svattrib, - fileloc, rect, sviewid); - addNewStructureViewer(sview); - } catch (OutOfMemoryError ex) - { - new OOMWarning("restoring structure view for PDB id " + id, - (OutOfMemoryError) ex.getCause()); - if (sview != null && sview.isVisible()) - { - sview.closeViewer(false); - sview.setVisible(false); - sview.dispose(); - } - } - } - }); - } catch (InvocationTargetException ex) - { - warn("Unexpected error when opening Jmol view.", ex); - - } catch (InterruptedException e) + ViewerType viewerType = ViewerType.valueOf(type); + createStructureViewer(viewerType, viewerData, af, jprovider); + } catch (IllegalArgumentException | NullPointerException e) { - // e.printStackTrace(); + // TODO JAL-3619 show error dialog / offer an alternative viewer + Cache.log.error( + "Invalid structure viewer type: " + type); } - } /** @@ -5520,7 +5260,7 @@ public class Jalview2XML addDatasetRef(vamsasSet.getDatasetId(), ds); } } - Vector dseqs = null; + Vector dseqs = null; if (!ignoreUnrefed) { // recovering an alignment View @@ -5548,7 +5288,7 @@ public class Jalview2XML // try even harder to restore dataset AlignmentI xtantDS = checkIfHasDataset(vamsasSet.getSequence()); // create a list of new dataset sequences - dseqs = new Vector(); + dseqs = new Vector<>(); } for (int i = 0, iSize = vamsasSet.getSequence().size(); i < iSize; i++) { @@ -5636,7 +5376,8 @@ public class Jalview2XML * vamsasSeq array ordering, to preserve ordering of dataset */ private void ensureJalviewDatasetSequence(Sequence vamsasSeq, - AlignmentI ds, Vector dseqs, boolean ignoreUnrefed, int vseqpos) + AlignmentI ds, Vector dseqs, boolean ignoreUnrefed, + int vseqpos) { // JBP TODO: Check this is called for AlCodonFrames to support recovery of // xRef Codon Maps @@ -6360,6 +6101,177 @@ public class Jalview2XML } /** + * Creates a new structure viewer window + * + * @param viewerType + * @param viewerData + * @param af + * @param jprovider + */ + protected void createStructureViewer( + ViewerType viewerType, final Entry viewerData, + AlignFrame af, jarInputStreamProvider jprovider) + { + final StructureViewerModel viewerModel = viewerData.getValue(); + String sessionFilePath = null; + + if (viewerType == ViewerType.JMOL) + { + sessionFilePath = rewriteJmolSession(viewerModel, jprovider); + } + else + { + String viewerJarEntryName = getViewerJarEntryName( + viewerModel.getViewId()); + sessionFilePath = copyJarEntry(jprovider, + viewerJarEntryName, + "viewerSession", ".tmp"); + } + final String sessionPath = sessionFilePath; + final String sviewid = viewerData.getKey(); + try + { + SwingUtilities.invokeAndWait(new Runnable() + { + @Override + public void run() + { + JalviewStructureDisplayI sview = null; + try + { + sview = StructureViewer.createView(viewerType, af.alignPanel, + viewerModel, sessionPath, sviewid); + addNewStructureViewer(sview); + } catch (OutOfMemoryError ex) + { + new OOMWarning("Restoring structure view for " + + viewerType, + (OutOfMemoryError) ex.getCause()); + if (sview != null && sview.isVisible()) + { + sview.closeViewer(false); + sview.setVisible(false); + sview.dispose(); + } + } + } + }); + } catch (InvocationTargetException | InterruptedException ex) + { + warn("Unexpected error when opening " + viewerType + + " structure viewer", ex); + } + } + + /** + * Rewrites a Jmol session script, saves it to a temporary file, and returns + * the path of the file. "load file" commands are rewritten to change the + * original PDB file names to those created as the Jalview project is loaded. + * + * @param svattrib + * @param jprovider + * @return + */ + private String rewriteJmolSession(StructureViewerModel svattrib, + jarInputStreamProvider jprovider) + { + String state = svattrib.getStateData(); // Jalview < 2.9 + if (state == null || state.isEmpty()) // Jalview >= 2.9 + { + String jarEntryName = getViewerJarEntryName(svattrib.getViewId()); + state = readJarEntry(jprovider, jarEntryName); + } + // TODO or simpler? for each key in oldFiles, + // replace key.getPath() in state with oldFiles.get(key).getFilePath() + // (allowing for different path escapings) + StringBuilder rewritten = new StringBuilder(state.length()); + int cp = 0, ncp, ecp; + Map oldFiles = svattrib.getFileData(); + while ((ncp = state.indexOf("load ", cp)) > -1) + { + do + { + // look for next filename in load statement + rewritten.append(state.substring(cp, + ncp = (state.indexOf("\"", ncp + 1) + 1))); + String oldfilenam = state.substring(ncp, + ecp = state.indexOf("\"", ncp)); + // recover the new mapping data for this old filename + // have to normalize filename - since Jmol and jalview do + // filename translation differently. + StructureData filedat = oldFiles.get(new File(oldfilenam)); + if (filedat == null) + { + String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\"); + filedat = oldFiles.get(new File(reformatedOldFilename)); + } + rewritten + .append(Platform.escapeBackslashes(filedat.getFilePath())); + rewritten.append("\""); + cp = ecp + 1; // advance beyond last \" and set cursor so we can + // look for next file statement. + } while ((ncp = state.indexOf("/*file*/", cp)) > -1); + } + if (cp > 0) + { + // just append rest of state + rewritten.append(state.substring(cp)); + } + else + { + System.err.print("Ignoring incomplete Jmol state for PDB ids: "); + rewritten = new StringBuilder(state); + rewritten.append("; load append "); + for (File id : oldFiles.keySet()) + { + // add pdb files that should be present in the viewer + StructureData filedat = oldFiles.get(id); + rewritten.append(" \"").append(filedat.getFilePath()).append("\""); + } + rewritten.append(";"); + } + + if (rewritten.length() == 0) + { + return null; + } + final String history = "history = "; + int historyIndex = rewritten.indexOf(history); + if (historyIndex > -1) + { + /* + * change "history = [true|false];" to "history = [1|0];" + */ + historyIndex += history.length(); + String val = rewritten.substring(historyIndex, historyIndex + 5); + if (val.startsWith("true")) + { + rewritten.replace(historyIndex, historyIndex + 4, "1"); + } + else if (val.startsWith("false")) + { + rewritten.replace(historyIndex, historyIndex + 5, "0"); + } + } + + try + { + File tmp = File.createTempFile("viewerSession", ".tmp"); + try (OutputStream os = new FileOutputStream(tmp)) + { + InputStream is = new ByteArrayInputStream( + rewritten.toString().getBytes()); + copyAll(is, os); + return tmp.getAbsolutePath(); + } + } catch (IOException e) + { + Cache.log.error("Error restoring Jmol session: " + e.toString()); + } + return null; + } + + /** * Populates an XML model of the feature colour scheme for one feature type * * @param featureType diff --git a/src/jalview/structure/AtomSpec.java b/src/jalview/structure/AtomSpec.java index f20cd31..8b8161f 100644 --- a/src/jalview/structure/AtomSpec.java +++ b/src/jalview/structure/AtomSpec.java @@ -43,52 +43,70 @@ public class AtomSpec * Parses a Chimera atomspec e.g. #1:12.A to construct an AtomSpec model (with * null pdb file name) * + *

+   * Chimera format: 
+   *    #1.2:12-20.A     model 1, submodel 2, chain A, atoms 12-20
+   * ChimeraX format:
+   *    #1.2/A:12-20
+   * 
+ * * @param spec + * @param chimeraX * @return * @throw IllegalArgumentException if the spec cannot be parsed, or represents * more than one residue + * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html + * @see http://rbvi.ucsf.edu/chimerax/docs/user/commands/atomspec.html */ - public static AtomSpec fromChimeraAtomspec(String spec) + public static AtomSpec fromChimeraAtomspec(String spec, boolean chimeraX) { - int colonPos = spec.indexOf(":"); - if (colonPos == -1) + int modelSeparatorPos = spec.indexOf(chimeraX ? "/" : ":"); + if (modelSeparatorPos == -1) { throw new IllegalArgumentException(spec); } int hashPos = spec.indexOf("#"); - if (hashPos == -1 && colonPos != 0) + if (hashPos == -1 && modelSeparatorPos != 0) { // # is missing but something precedes : - reject throw new IllegalArgumentException(spec); } - String modelSubmodel = spec.substring(hashPos + 1, colonPos); - int dotPos = modelSubmodel.indexOf("."); + String modelSubmodel = spec.substring(hashPos + 1, modelSeparatorPos); int modelId = 0; try { - modelId = Integer.valueOf(dotPos == -1 ? modelSubmodel - : modelSubmodel.substring(0, dotPos)); + int subModelPos = modelSubmodel.indexOf("."); + modelId = Integer.valueOf( + subModelPos > 0 ? modelSubmodel.substring(0, subModelPos) + : modelSubmodel); } catch (NumberFormatException e) { // ignore, default to model 0 } - String residueChain = spec.substring(colonPos + 1); - dotPos = residueChain.indexOf("."); + /* + * now process what follows the model, either + * Chimera: atoms.chain + * ChimeraX: chain:atoms + */ + String atomsAndChain = spec.substring(modelSeparatorPos + 1); + String[] tokens = atomsAndChain.split(chimeraX ? "\\:" : "\\."); + String atoms = tokens.length == 1 ? atomsAndChain + : (chimeraX ? tokens[1] : tokens[0]); int resNum = 0; try { - resNum = Integer.parseInt(dotPos == -1 ? residueChain - : residueChain.substring(0, dotPos)); + resNum = Integer.parseInt(atoms); } catch (NumberFormatException e) { // could be a range e.g. #1:4-7.B throw new IllegalArgumentException(spec); } - String chainId = dotPos == -1 ? "" : residueChain.substring(dotPos + 1); + String chainId = tokens.length == 1 ? "" + : (chimeraX ? tokens[0] : tokens[1]); return new AtomSpec(modelId, chainId, resNum, 0); } diff --git a/src/jalview/structure/AtomSpecModel.java b/src/jalview/structure/AtomSpecModel.java new file mode 100644 index 0000000..1ef653e --- /dev/null +++ b/src/jalview/structure/AtomSpecModel.java @@ -0,0 +1,128 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ +package jalview.structure; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * A class to model a set of models, chains and atom range positions + * + */ +public class AtomSpecModel +{ + /* + * { modelId, {chainCode, List ranges} } + */ + private Map> atomSpec; + + /** + * Constructor + */ + public AtomSpecModel() + { + atomSpec = new TreeMap<>(); + } + + /** + * Adds one contiguous range to this atom spec + * + * @param model + * @param startPos + * @param endPos + * @param chain + */ + public void addRange(String model, int startPos, int endPos, String chain) + { + /* + * Get/initialize map of data for the colour and model + */ + Map modelData = atomSpec.get(model); + if (modelData == null) + { + atomSpec.put(model, modelData = new TreeMap<>()); + } + + /* + * Get/initialize map of data for colour, model and chain + */ + BitSet chainData = modelData.get(chain); + if (chainData == null) + { + chainData = new BitSet(); + modelData.put(chain, chainData); + } + + /* + * Add the start/end positions + */ + chainData.set(startPos, endPos + 1); + } + + public Iterable getModels() + { + return atomSpec.keySet(); + } + + public int getModelCount() + { + return atomSpec.size(); + } + + public Iterable getChains(String model) + { + return atomSpec.containsKey(model) ? atomSpec.get(model).keySet() + : null; + } + + /** + * Returns a (possibly empty) ordered list of contiguous atom ranges for the + * given model and chain. + * + * @param model + * @param chain + * @return + */ + public List getRanges(String model, String chain) + { + List ranges = new ArrayList<>(); + if (atomSpec.containsKey(model)) + { + BitSet bs = atomSpec.get(model).get(chain); + int start = 0; + if (bs != null) + { + start = bs.nextSetBit(start); + int end = 0; + while (start != -1) + { + end = bs.nextClearBit(start); + ranges.add(new int[] { start, end - 1 }); + start = bs.nextSetBit(end); + } + } + } + return ranges; + } +} diff --git a/src/jalview/structure/StructureCommand.java b/src/jalview/structure/StructureCommand.java new file mode 100644 index 0000000..f7875ab --- /dev/null +++ b/src/jalview/structure/StructureCommand.java @@ -0,0 +1,75 @@ +package jalview.structure; + +import java.util.ArrayList; +import java.util.List; + +public class StructureCommand implements StructureCommandI +{ + private String command; + + private List parameters; + + public StructureCommand(String cmd, String... params) + { + command = cmd; + if (params != null) + { + for (String p : params) + { + addParameter(p); + } + } + } + + @Override + public void addParameter(String param) + { + if (parameters == null) + { + parameters = new ArrayList<>(); + } + parameters.add(param); + } + + @Override + public String getCommand() + { + return command; + } + + @Override + public List getParameters() + { + return parameters; + } + + @Override + public boolean hasParameters() + { + return parameters != null && !parameters.isEmpty(); + } + + @Override + public String toString() + { + if (!hasParameters()) + { + return command; + } + StringBuilder sb = new StringBuilder(32); + sb.append(command).append("("); + boolean first = true; + for (String p : parameters) + { + if (!first) + { + sb.append(","); + } + first = false; + sb.append(p); + } + sb.append(")"); + return sb.toString(); + } + +} diff --git a/src/jalview/structure/StructureCommandI.java b/src/jalview/structure/StructureCommandI.java new file mode 100644 index 0000000..e39bbba --- /dev/null +++ b/src/jalview/structure/StructureCommandI.java @@ -0,0 +1,14 @@ +package jalview.structure; + +import java.util.List; + +public interface StructureCommandI +{ + String getCommand(); + + List getParameters(); + + void addParameter(String param); + + boolean hasParameters(); +} diff --git a/src/jalview/structure/StructureCommandsBase.java b/src/jalview/structure/StructureCommandsBase.java new file mode 100644 index 0000000..3c29fd4 --- /dev/null +++ b/src/jalview/structure/StructureCommandsBase.java @@ -0,0 +1,226 @@ +package jalview.structure; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * A base class holding methods useful to all classes that implement commands + * for structure viewers + * + * @author gmcarstairs + * + */ +public abstract class StructureCommandsBase implements StructureCommandsI +{ + private static final String CMD_SEPARATOR = ";"; + public static final String NAMESPACE_PREFIX = "jv_"; + + /** + * Returns something that separates concatenated commands + * + * @return + */ + protected static String getCommandSeparator() + { + return CMD_SEPARATOR; + } + + /** + * Returns the lowest model number used by the structure viewer + * + * @return + */ + @Override + public int getModelStartNo() + { + return 0; + } + + /** + * Helper method to add one contiguous range to the AtomSpec model for the given + * value (creating the model if necessary). As used by Jalview, {@code value} is + *
    + *
  • a colour, when building a 'colour structure by sequence' command
  • + *
  • a feature value, when building a 'set Chimera attributes from features' + * command
  • + *
+ * + * @param map + * @param value + * @param model + * @param startPos + * @param endPos + * @param chain + */ + public static final void addAtomSpecRange(Map map, + Object value, String model, int startPos, int endPos, + String chain) + { + /* + * Get/initialize map of data for the colour + */ + AtomSpecModel atomSpec = map.get(value); + if (atomSpec == null) + { + atomSpec = new AtomSpecModel(); + map.put(value, atomSpec); + } + + atomSpec.addRange(model, startPos, endPos, chain); + } + + /** + * Makes a structure viewer attribute name for a Jalview feature type by + * prefixing it with "jv_", and replacing any non-alphanumeric characters with + * an underscore + * + * @param featureType + * @return + */ + protected String makeAttributeName(String featureType) + { + StringBuilder sb = new StringBuilder(); + if (featureType != null) + { + for (char c : featureType.toCharArray()) + { + sb.append(Character.isLetterOrDigit(c) ? c : '_'); + } + } + String attName = NAMESPACE_PREFIX + sb.toString(); + return attName; + } + + /** + * Traverse the map of colours/models/chains/positions to construct a list of + * 'color' commands (one per distinct colour used). The format of each command + * is specific to the structure viewer. + *

+ * The default implementation returns a single command containing one command + * per colour, concatenated. + * + * @param colourMap + * @return + */ + @Override + public List colourBySequence( + Map colourMap) + { + List commands = new ArrayList<>(); + StringBuilder sb = new StringBuilder(colourMap.size() * 20); + boolean first = true; + for (Object key : colourMap.keySet()) + { + Color colour = (Color) key; + final AtomSpecModel colourData = colourMap.get(colour); + StructureCommandI command = getColourCommand(colourData, colour); + if (!first) + { + sb.append(getCommandSeparator()); + } + first = false; + sb.append(command.getCommand()); + } + + commands.add(new StructureCommand(sb.toString())); + return commands; + } + + /** + * Returns a command to colour the atoms represented by {@code atomSpecModel} + * with the colour specified by {@code colourCode}. + * + * @param atomSpecModel + * @param colour + * @return + */ + protected StructureCommandI getColourCommand(AtomSpecModel atomSpecModel, + Color colour) + { + String atomSpec = getAtomSpec(atomSpecModel, false); + return colourResidues(atomSpec, colour); + } + + /** + * Returns a command to colour the atoms described (in viewer command syntax) + * by {@code atomSpec} with the colour specified by {@code colourCode} + * + * @param atomSpec + * @param colour + * @return + */ + protected abstract StructureCommandI colourResidues(String atomSpec, + Color colour); + + @Override + public List colourByResidues( + Map colours) + { + List commands = new ArrayList<>(); + for (Entry entry : colours.entrySet()) + { + commands.add(colourResidue(entry.getKey(), entry.getValue())); + } + return commands; + } + + private StructureCommandI colourResidue(String resName, Color col) + { + String atomSpec = getResidueSpec(resName); + return colourResidues(atomSpec, col); + } + + /** + * Helper method to append one start-end range to an atomspec string + * + * @param sb + * @param start + * @param end + * @param chain + * @param firstPositionForModel + */ + protected void appendRange(StringBuilder sb, int start, int end, + String chain, boolean firstPositionForModel, boolean isChimeraX) + { + if (!firstPositionForModel) + { + sb.append(","); + } + if (end == start) + { + sb.append(start); + } + else + { + sb.append(start).append("-").append(end); + } + + if (!isChimeraX) + { + sb.append("."); + if (!" ".equals(chain)) + { + sb.append(chain); + } + } + } + + /** + * Returns the atom specifier meaning all occurrences of the given residue + * + * @param residue + * @return + */ + protected abstract String getResidueSpec(String residue); + + @Override + public List setAttributes( + Map> featureValues) + { + // default does nothing, override where this is implemented + return null; + } +} diff --git a/src/jalview/structure/StructureCommandsI.java b/src/jalview/structure/StructureCommandsI.java new file mode 100644 index 0000000..5a0db0a --- /dev/null +++ b/src/jalview/structure/StructureCommandsI.java @@ -0,0 +1,169 @@ +package jalview.structure; + +import java.awt.Color; +import java.util.List; +import java.util.Map; + +/** + * Methods that generate commands that can be sent to a molecular structure + * viewer program (e.g. Jmol, Chimera, ChimeraX) + * + * @author gmcarstairs + * + */ +public interface StructureCommandsI +{ + /** + * Returns the command to colour by chain + * + * @return + */ + StructureCommandI colourByChain(); + + /** + * Returns the command to colour residues using a charge-based scheme: + *

    + *
  • Aspartic acid and Glutamic acid (negative charge) red
  • + *
  • Lysine and Arginine (positive charge) blue
  • + *
  • Cysteine - yellow
  • + *
  • all others - white
  • + *
+ * + * @return + */ + List colourByCharge(); + + /** + * Returns the command to colour residues with the colours provided in the + * map, one per three letter residue code + * + * @param colours + * @return + */ + List colourByResidues(Map colours); + + /** + * Returns the command to set the background colour of the structure viewer + * + * @param col + * @return + */ + StructureCommandI setBackgroundColour(Color col); + + /** + * Returns commands to colour mapped residues of structures according to + * Jalview's colouring (including feature colouring if applied). Parameter is + * a map from Color to a model of all residues assigned that colour. + * + * @param colourMap + * @return + */ + + List colourBySequence( + Map colourMap); + + /** + * Returns a command to centre the display in the structure viewer + * + * @return + */ + StructureCommandI focusView(); + + /** + * Returns a command to show only the selected chains. The items in the input + * list should be formatted as "modelid:chainid". + * + * @param toShow + * @return + */ + List showChains(List toShow); + + /** + * Returns a command to superpose structures by closest positioning of + * residues in {@code atomSpec} to the corresponding residues in + * {@code refAtoms}. If wanted, this may include commands to visually + * highlight the residues that were used for the superposition. + * + * @param refAtoms + * @param atomSpec + * @return + */ + List superposeStructures(AtomSpecModel refAtoms, + AtomSpecModel atomSpec); + + /** + * Returns a command to open a file of commands at the given path + * + * @param path + * @return + */ + StructureCommandI openCommandFile(String path); + + /** + * Returns a command to save the current viewer session state to the given + * file + * + * @param filepath + * @return + */ + StructureCommandI saveSession(String filepath); + + /** + * Returns a representation of the atom set represented by the model, in + * viewer syntax format. If {@code alphaOnly} is true, this is restricted to + * Alpha Carbon (peptide) or Phosphorous (rna) only + * + * @param model + * @param alphaOnly + * @return + */ + String getAtomSpec(AtomSpecModel model, boolean alphaOnly); + + /** + * Returns the lowest model number used by the structure viewer (likely 0 or + * 1) + * + * @return + */ + // TODO remove by refactoring so command generation is purely driven by + // AtomSpecModel objects derived in the binding classes? + int getModelStartNo(); + + /** + * Returns command(s) to show only the backbone of the peptide (cartoons in + * Jmol, chain in Chimera) + * + * @return + */ + List showBackbone(); + + /** + * Returns a command to open a file at the given path + * + * @param file + * @return + */ + // refactor if needed to distinguish loading data or session files + StructureCommandI loadFile(String file); + + /** + * Returns commands to set atom attributes or properties, given a map of + * Jalview features as {featureType, {featureValue, AtomSpecModel}}. The + * assumption is that one command can be constructed for each feature type and + * value combination, to apply it to one or more residues. + * + * @param featureValues + * @return + */ + List setAttributes( + Map> featureValues); + + /** + * Returns command to open a saved structure viewer session file, or null if + * not supported + * + * @param filepath + * @return + */ + StructureCommandI openSession(String filepath); +} diff --git a/src/jalview/structure/StructureSelectionManager.java b/src/jalview/structure/StructureSelectionManager.java index 8c3816e..53644e9 100644 --- a/src/jalview/structure/StructureSelectionManager.java +++ b/src/jalview/structure/StructureSelectionManager.java @@ -538,15 +538,14 @@ public class StructureSelectionManager pdb, maxChain, sqmpping, maxAlignseq, siftsClient); seqToStrucMapping.add(siftsMapping); maxChain.makeExactMapping(siftsMapping, seq); - maxChain.transferRESNUMFeatures(seq, "IEA: SIFTS");// FIXME: is this - // "IEA:SIFTS" ? + maxChain.transferRESNUMFeatures(seq, "IEA: SIFTS"); maxChain.transferResidueAnnotation(siftsMapping, null); ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0)); } catch (SiftsException e) { // fall back to NW alignment - System.err.println(e.getMessage()); + Cache.log.error(e.getMessage()); StructureMapping nwMapping = getNWMappings(seq, pdbFile, targetChainId, maxChain, pdb, maxAlignseq); seqToStrucMapping.add(nwMapping); diff --git a/src/jalview/structures/models/AAStructureBindingModel.java b/src/jalview/structures/models/AAStructureBindingModel.java index 2528286..5949847 100644 --- a/src/jalview/structures/models/AAStructureBindingModel.java +++ b/src/jalview/structures/models/AAStructureBindingModel.java @@ -20,30 +20,50 @@ */ package jalview.structures.models; +import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.SwingUtilities; + +import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; +import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; import jalview.api.StructureSelectionManagerProvider; import jalview.api.structures.JalviewStructureDisplayI; +import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.PDBEntry; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.ext.rbvi.chimera.JalviewChimeraBinding; +import jalview.gui.Desktop; +import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; +import jalview.io.StructureFile; +import jalview.renderer.seqfeatures.FeatureColourFinder; import jalview.schemes.ColourSchemeI; +import jalview.schemes.ResidueProperties; import jalview.structure.AtomSpec; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsI; import jalview.structure.StructureListener; import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; import jalview.util.Comparison; import jalview.util.MessageManager; -import java.awt.Color; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.List; - /** * * A base class to hold common function for protein structure model binding. @@ -57,10 +77,71 @@ public abstract class AAStructureBindingModel extends SequenceStructureBindingModel implements StructureListener, StructureSelectionManagerProvider { + /** + * Data bean class to simplify parameterisation in superposeStructures + */ + public static class SuperposeData + { + public String filename; + + public String pdbId; + + public String chain = ""; + + public boolean isRna; + + /* + * The pdb residue number (if any) mapped to columns of the alignment + */ + public int[] pdbResNo; // or use SparseIntArray? + + public String modelId; + + /** + * Constructor + * + * @param width + * width of alignment (number of columns that may potentially + * participate in superposition) + * @param model + * structure viewer model number + */ + public SuperposeData(int width, String model) + { + pdbResNo = new int[width]; + modelId = model; + } + } + + private static final int MIN_POS_TO_SUPERPOSE = 4; + + private static final String COLOURING_STRUCTURES = MessageManager + .getString("status.colouring_structures"); + + /* + * the Jalview panel through which the user interacts + * with the structure viewer + */ + private JalviewStructureDisplayI viewer; + + /* + * helper that generates command syntax + */ + private StructureCommandsI commandGenerator; private StructureSelectionManager ssm; /* + * modelled chains, formatted as "pdbid:chainCode" + */ + private List chainNames; + + /* + * lookup of pdb file name by key "pdbid:chainCode" + */ + private Map chainFile; + + /* * distinct PDB entries (pdb files) associated * with sequences */ @@ -88,42 +169,13 @@ public abstract class AAStructureBindingModel private boolean finishedInit = false; /** - * current set of model filenames loaded in the Jmol instance + * current set of model filenames loaded in the viewer */ protected String[] modelFileNames = null; public String fileLoadingError; /** - * Data bean class to simplify parameterisation in superposeStructures - */ - protected class SuperposeData - { - /** - * Constructor with alignment width argument - * - * @param width - */ - public SuperposeData(int width) - { - pdbResNo = new int[width]; - } - - public String filename; - - public String pdbId; - - public String chain = ""; - - public boolean isRna; - - /* - * The pdb residue number (if any) mapped to each column of the alignment - */ - public int[] pdbResNo; - } - - /** * Constructor * * @param ssm @@ -134,6 +186,8 @@ public abstract class AAStructureBindingModel { this.ssm = ssm; this.sequence = seqs; + chainNames = new ArrayList<>(); + chainFile = new HashMap<>(); } /** @@ -148,8 +202,7 @@ public abstract class AAStructureBindingModel PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol) { - this.ssm = ssm; - this.sequence = sequenceIs; + this(ssm, sequenceIs); this.nucleotide = Comparison.isNucleotide(sequenceIs); this.pdbEntry = pdbentry; this.protocol = protocol; @@ -331,7 +384,11 @@ public abstract class AAStructureBindingModel */ protected void releaseUIResources() { + } + @Override + public void releaseReferences(Object svl) + { } public boolean isColourBySequence() @@ -339,6 +396,25 @@ public abstract class AAStructureBindingModel return colourBySequence; } + /** + * Called when the binding thinks the UI needs to be refreshed after a + * structure viewer state change. This could be because structures were + * loaded, or because an error has occurred. Default does nothing, override as + * required. + */ + public void refreshGUI() + { + } + + /** + * Instruct the Jalview binding to update the pdbentries vector if necessary + * prior to matching the jmol view's contents to the list of structure files + * Jalview knows about. By default does nothing, override as required. + */ + public void refreshPdbEntries() + { + } + public void setColourBySequence(boolean colourBySequence) { this.colourBySequence = colourBySequence; @@ -355,8 +431,8 @@ public abstract class AAStructureBindingModel { Integer.valueOf(pe).toString() })); } final String nullChain = "TheNullChain"; - List s = new ArrayList(); - List c = new ArrayList(); + List s = new ArrayList<>(); + List c = new ArrayList<>(); if (getChains() == null) { setChains(new String[getPdbCount()][]); @@ -425,8 +501,8 @@ public abstract class AAStructureBindingModel public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe, SequenceI[][] seq, String[][] chns) { - List v = new ArrayList(); - List rtn = new ArrayList(); + List v = new ArrayList<>(); + List rtn = new ArrayList<>(); for (int i = 0; i < getPdbCount(); i++) { v.add(getPdbEntry(i)); @@ -581,7 +657,7 @@ public abstract class AAStructureBindingModel * @return */ protected int findSuperposableResidues(AlignmentI alignment, - BitSet matched, SuperposeData[] structures) + BitSet matched, AAStructureBindingModel.SuperposeData[] structures) { int refStructure = -1; String[] files = getStructureFiles(); @@ -639,7 +715,7 @@ public abstract class AAStructureBindingModel * for the same structure) */ s = seqCountForPdbFile; - break; + break; // fixme break out of two loops here! } } } @@ -732,11 +808,15 @@ public abstract class AAStructureBindingModel } /** - * Returns a list of chains mapped in this viewer. + * Returns a list of chains mapped in this viewer, formatted as + * "pdbid:chainCode" * * @return */ - public abstract List getChainNames(); + public List getChainNames() + { + return chainNames; + } /** * Returns the Jalview panel hosting the structure viewer (if any) @@ -745,34 +825,133 @@ public abstract class AAStructureBindingModel */ public JalviewStructureDisplayI getViewer() { - return null; + return viewer; } - public abstract void setJalviewColourScheme(ColourSchemeI cs); + public void setViewer(JalviewStructureDisplayI v) + { + viewer = v; + } /** * Constructs and sends a command to align structures against a reference * structure, based on one or more sequence alignments. May optionally return - * an error or warning message for the alignment command. - * - * @param alignments - * an array of alignments to process - * @param structureIndices - * an array of corresponding reference structures (index into pdb - * file array); if a negative value is passed, the first PDB file - * mapped to an alignment sequence is used as the reference for - * superposition - * @param hiddenCols - * an array of corresponding hidden columns for each alignment + * an error or warning message for the alignment command(s). + * + * @param alignWith + * an array of one or more alignment views to process * @return */ - public abstract String superposeStructures(AlignmentI[] alignments, - int[] structureIndices, HiddenColumns[] hiddenCols); + public String superposeStructures(List alignWith) + { + String error = ""; + String[] files = getStructureFiles(); + + if (!waitForFileLoad(files)) + { + return null; + } + refreshPdbEntries(); + + for (AlignmentViewPanel view : alignWith) + { + AlignmentI alignment = view.getAlignment(); + HiddenColumns hiddenCols = alignment.getHiddenColumns(); + + /* + * 'matched' bit i will be set for visible alignment columns i where + * all sequences have a residue with a mapping to their PDB structure + */ + BitSet matched = new BitSet(); + final int width = alignment.getWidth(); + for (int m = 0; m < width; m++) + { + if (hiddenCols == null || hiddenCols.isVisible(m)) + { + matched.set(m); + } + } + + AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length]; + for (int f = 0; f < files.length; f++) + { + structures[f] = new AAStructureBindingModel.SuperposeData(width, + getModelIdForFile(files[f])); + } + + /* + * Calculate the superposable alignment columns ('matched'), and the + * corresponding structure residue positions (structures.pdbResNo) + */ + int refStructure = findSuperposableResidues(alignment, + matched, structures); + + /* + * require at least 4 positions to be able to execute superposition + */ + int nmatched = matched.cardinality(); + if (nmatched < MIN_POS_TO_SUPERPOSE) + { + String msg = MessageManager.formatMessage("label.insufficient_residues", + nmatched); + error += view.getViewName() + ": " + msg + "; "; + continue; + } + + /* + * get a model of the superposable residues in the reference structure + */ + AtomSpecModel refAtoms = getAtomSpec(structures[refStructure], + matched); + + /* + * Show all as backbone before doing superposition(s) + * (residues used for matching will be shown as ribbon) + */ + // todo better way to ensure synchronous than setting getReply true!! + executeCommands(commandGenerator.showBackbone(), true, null); + + /* + * superpose each (other) structure to the reference in turn + */ + for (int i = 0; i < structures.length; i++) + { + if (i != refStructure) + { + AtomSpecModel atomSpec = getAtomSpec(structures[i], matched); + List commands = commandGenerator + .superposeStructures(refAtoms, atomSpec); + List replies = executeCommands(commands, true, null); + for (String reply : replies) + { + // return this error (Chimera only) to the user + if (reply.toLowerCase().contains("unequal numbers of atoms")) + { + error += "; " + reply; + } + } + } + } + } + + return error; + } - public abstract void setBackgroundColour(Color col); + private AtomSpecModel getAtomSpec(AAStructureBindingModel.SuperposeData superposeData, + BitSet matched) + { + AtomSpecModel model = new AtomSpecModel(); + int nextColumnMatch = matched.nextSetBit(0); + while (nextColumnMatch != -1) + { + int pdbResNum = superposeData.pdbResNo[nextColumnMatch]; + model.addRange(superposeData.modelId, pdbResNum, pdbResNum, + superposeData.chain); + nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1); + } - protected abstract StructureMappingcommandSet[] getColourBySequenceCommands( - String[] files, SequenceRenderer sr, AlignmentViewPanel avp); + return model; + } /** * returns the current sequenceRenderer that should be used to colour the @@ -785,42 +964,908 @@ public abstract class AAStructureBindingModel public abstract SequenceRenderer getSequenceRenderer( AlignmentViewPanel alignment); - protected abstract void colourBySequence( - StructureMappingcommandSet[] colourBySequenceCommands); + /** + * Sends a command to the structure viewer to colour each chain with a + * distinct colour (to the extent supported by the viewer) + */ + public void colourByChain() + { + colourBySequence = false; - public abstract void colourByChain(); + // TODO: JAL-628 colour chains distinctly across all visible models - public abstract void colourByCharge(); + executeCommand(commandGenerator.colourByChain(), false, + COLOURING_STRUCTURES); + } /** - * colour any structures associated with sequences in the given alignment - * using the getFeatureRenderer() and getSequenceRenderer() renderers but only - * if colourBySequence is enabled. + * Sends a command to the structure viewer to colour each chain with a + * distinct colour (to the extent supported by the viewer) */ - public void colourBySequence(AlignmentViewPanel alignmentv) + public void colourByCharge() { - if (!colourBySequence || !isLoadingFinished()) + colourBySequence = false; + + executeCommands(commandGenerator.colourByCharge(), false, + COLOURING_STRUCTURES); + } + + /** + * Sends a command to the structure to apply a colour scheme (defined in + * Jalview but not necessarily applied to the alignment), which defines a + * colour per residue letter. More complex schemes (e.g. that depend on + * consensus) cannot be used here and are ignored. + * + * @param cs + */ + public void colourByJalviewColourScheme(ColourSchemeI cs) + { + colourBySequence = false; + + if (cs == null || !cs.isSimple()) { return; } - if (getSsm() == null) + + /* + * build a map of {Residue3LetterCode, Color} + */ + Map colours = new HashMap<>(); + List residues = ResidueProperties.getResidues(isNucleotide(), + false); + for (String resName : residues) + { + char res = resName.length() == 3 + ? ResidueProperties.getSingleCharacterCode(resName) + : resName.charAt(0); + Color colour = cs.findColour(res, 0, null, null, 0f); + colours.put(resName, colour); + } + + /* + * pass to the command constructor, and send the command + */ + List cmd = commandGenerator + .colourByResidues(colours); + executeCommands(cmd, false, COLOURING_STRUCTURES); + } + + public void setBackgroundColour(Color col) + { + StructureCommandI cmd = commandGenerator.setBackgroundColour(col); + executeCommand(cmd, false, null); + } + + /** + * Sends one command to the structure viewer. If {@code getReply} is true, the + * command is sent synchronously, otherwise in a deferred thread. + *

+ * If a progress message is supplied, this is displayed before command + * execution, and removed afterwards. + * + * @param cmd + * @param getReply + * @param msg + * @return + */ + private List executeCommand(StructureCommandI cmd, + boolean getReply, String msg) + { + if (getReply) + { + /* + * synchronous (same thread) execution so reply can be returned + */ + final JalviewStructureDisplayI theViewer = getViewer(); + final long handle = msg == null ? 0 : theViewer.startProgressBar(msg); + try + { + return executeCommand(cmd, getReply); + } finally + { + if (msg != null) + { + theViewer.stopProgressBar(null, handle); + } + } + } + else + { + /* + * asynchronous (new thread) execution if no reply needed + */ + final JalviewStructureDisplayI theViewer = getViewer(); + final long handle = msg == null ? 0 : theViewer.startProgressBar(msg); + + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + try + { + executeCommand(cmd, false); + } finally + { + if (msg != null) + { + theViewer.stopProgressBar(null, handle); + } + } + } + }); + return null; + } + } + + /** + * Execute one structure viewer command. If {@code getReply} is true, may + * optionally return one or more reply messages, else returns null. + * + * @param cmd + * @param getReply + */ + protected abstract List executeCommand(StructureCommandI cmd, + boolean getReply); + + /** + * A helper method that converts list of commands to a vararg array + * + * @param commands + * @param getReply + * @param msg + */ + protected List executeCommands( + List commands, + boolean getReply, String msg) + { + return executeCommands(getReply, msg, + commands.toArray(new StructureCommandI[commands.size()])); + } + + /** + * Executes one or more structure viewer commands. If a progress message is + * provided, it is shown first, and removed after all commands have been run. + * + * @param getReply + * @param msg + * @param commands + * @return + */ + protected List executeCommands(boolean getReply, String msg, + StructureCommandI[] commands) + { + // todo: tidy this up + + /* + * show progress message if specified + */ + final JalviewStructureDisplayI theViewer = getViewer(); + final long handle = msg == null ? 0 : theViewer.startProgressBar(msg); + + List response = getReply ? new ArrayList<>() : null; + try + { + for (StructureCommandI cmd : commands) + { + List replies = executeCommand(cmd, getReply, null); + if (getReply && replies != null) + { + response.addAll(replies); + } + } + return response; + } finally + { + if (msg != null) + { + theViewer.stopProgressBar(null, handle); + } + } + } + + /** + * Colours any structures associated with sequences in the given alignment as + * coloured in the alignment view, provided colourBySequence is enabled + */ + public void colourBySequence(AlignmentViewPanel alignmentv) + { + if (!colourBySequence || !isLoadingFinished() || getSsm() == null) { return; } - String[] files = getStructureFiles(); + Map colourMap = buildColoursMap(ssm, sequence, + alignmentv); + + List colourBySequenceCommands = commandGenerator + .colourBySequence(colourMap); + executeCommands(colourBySequenceCommands, false, null); + } + + /** + * Centre the display in the structure viewer + */ + public void focusView() + { + executeCommand(commandGenerator.focusView(), false, null); + } - SequenceRenderer sr = getSequenceRenderer(alignmentv); + /** + * Generates and executes a command to show only specified chains in the + * structure viewer. The list of chains to show should contain entries + * formatted as "pdbid:chaincode". + * + * @param toShow + */ + public void showChains(List toShow) + { + // todo or reformat toShow list entries as modelNo:pdbId:chainCode ? - StructureMappingcommandSet[] colourBySequenceCommands = getColourBySequenceCommands( - files, sr, alignmentv); - colourBySequence(colourBySequenceCommands); + /* + * Reformat the pdbid:chainCode values as modelNo:chainCode + * since this is what is needed to construct the viewer command + * todo: find a less messy way to do this + */ + List showThese = new ArrayList<>(); + for (String chainId : toShow) + { + String[] tokens = chainId.split("\\:"); + if (tokens.length == 2) + { + String pdbFile = getFileForChain(chainId); + String model = getModelIdForFile(pdbFile); + showThese.add(model + ":" + tokens[1]); + } + } + executeCommands(commandGenerator.showChains(showThese), false, null); } + /** + * Answers the structure viewer's model id given a PDB file name. Returns an + * empty string if model id is not found. + * + * @param chainId + * @return + */ + protected abstract String getModelIdForFile(String chainId); + public boolean hasFileLoadingError() { return fileLoadingError != null && fileLoadingError.length() > 0; } - public abstract jalview.api.FeatureRenderer getFeatureRenderer( - AlignmentViewPanel alignment); + /** + * Returns the FeatureRenderer for the given alignment view, or null if + * feature display is turned off in the view. + * + * @param avp + * @return + */ + public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp) + { + AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel() + : avp; + if (ap == null) + { + return null; + } + return ap.getAlignViewport().isShowSequenceFeatures() + ? ap.getFeatureRenderer() + : null; + } + + protected void setStructureCommands(StructureCommandsI cmd) + { + commandGenerator = cmd; + } + + /** + * Records association of one chain id (formatted as "pdbid:chainCode") with + * the corresponding PDB file name + * + * @param chainId + * @param fileName + */ + public void addChainFile(String chainId, String fileName) + { + chainFile.put(chainId, fileName); + } + + /** + * Returns the PDB filename for the given chain id (formatted as + * "pdbid:chainCode"), or null if not found + * + * @param chainId + * @return + */ + protected String getFileForChain(String chainId) + { + return chainFile.get(chainId); + } + + @Override + public void updateColours(Object source) + { + AlignmentViewPanel ap = (AlignmentViewPanel) source; + // ignore events from panels not used to colour this view + if (!getViewer().isUsedForColourBy(ap)) + { + return; + } + if (!isLoadingFromArchive()) + { + colourBySequence(ap); + } + } + + public StructureCommandsI getCommandGenerator() + { + return commandGenerator; + } + + protected abstract ViewerType getViewerType(); + + /** + * Send a structure viewer command asynchronously in a new thread. If the + * progress message is not null, display this message while the command is + * executing. + * + * @param command + * @param progressMsg + */ + protected void sendAsynchronousCommand(StructureCommandI command, + String progressMsg) + { + final JalviewStructureDisplayI theViewer = getViewer(); + final long handle = progressMsg == null ? 0 + : theViewer.startProgressBar(progressMsg); + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + try + { + executeCommand(command, false, null); + } finally + { + if (progressMsg != null) + { + theViewer.stopProgressBar(null, handle); + } + } + } + }); + + } + + /** + * Builds a data structure which records mapped structure residues for each + * colour. From this we can easily generate the viewer commands for colour by + * sequence. Constructs and returns a map of {@code Color} to + * {@code AtomSpecModel}, where the atomspec model holds + * + *

+   *   Model ids
+   *     Chains
+   *       Residue positions
+   * 
+ * + * Ordering is by order of addition (for colours), natural ordering (for + * models and chains) + * + * @param ssm + * @param sequence + * @param viewPanel + * @return + */ + protected Map buildColoursMap( + StructureSelectionManager ssm, SequenceI[][] sequence, + AlignmentViewPanel viewPanel) + { + String[] files = getStructureFiles(); + SequenceRenderer sr = getSequenceRenderer(viewPanel); + FeatureRenderer fr = viewPanel.getFeatureRenderer(); + FeatureColourFinder finder = new FeatureColourFinder(fr); + AlignViewportI viewport = viewPanel.getAlignViewport(); + HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); + AlignmentI al = viewport.getAlignment(); + Map colourMap = new LinkedHashMap<>(); + Color lastColour = null; + + for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) + { + final String modelId = getModelIdForFile(files[pdbfnum]); + StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); + + if (mapping == null || mapping.length < 1) + { + continue; + } + + int startPos = -1, lastPos = -1; + String lastChain = ""; + for (int s = 0; s < sequence[pdbfnum].length; s++) + { + for (int sp, m = 0; m < mapping.length; m++) + { + final SequenceI seq = sequence[pdbfnum][s]; + if (mapping[m].getSequence() == seq + && (sp = al.findIndex(seq)) > -1) + { + SequenceI asp = al.getSequenceAt(sp); + for (int r = 0; r < asp.getLength(); r++) + { + // no mapping to gaps in sequence + if (Comparison.isGap(asp.getCharAt(r))) + { + continue; + } + int pos = mapping[m].getPDBResNum(asp.findPosition(r)); + + if (pos < 1 || pos == lastPos) + { + continue; + } + + Color colour = sr.getResidueColour(seq, r, finder); + + /* + * darker colour for hidden regions + */ + if (!cs.isVisible(r)) + { + colour = Color.GRAY; + } + + final String chain = mapping[m].getChain(); + + /* + * Just keep incrementing the end position for this colour range + * _unless_ colour, PDB model or chain has changed, or there is a + * gap in the mapped residue sequence + */ + final boolean newColour = !colour.equals(lastColour); + final boolean nonContig = lastPos + 1 != pos; + final boolean newChain = !chain.equals(lastChain); + if (newColour || nonContig || newChain) + { + if (startPos != -1) + { + addAtomSpecRange(colourMap, lastColour, modelId, + startPos, lastPos, lastChain); + } + startPos = pos; + } + lastColour = colour; + lastPos = pos; + lastChain = chain; + } + // final colour range + if (lastColour != null) + { + addAtomSpecRange(colourMap, lastColour, modelId, startPos, + lastPos, lastChain); + } + // break; + } + } + } + } + return colourMap; + } + + /** + * todo better refactoring (map lookup or similar to get viewer structure id) + * + * @param pdbfnum + * @param file + * @return + */ + protected String getModelId(int pdbfnum, String file) + { + return String.valueOf(pdbfnum); + } + + /** + * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the + * full PDB file path + * + * @param pdb + * @param file + */ + public void stashFoundChains(StructureFile pdb, String file) + { + for (int i = 0; i < pdb.getChains().size(); i++) + { + String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id; + addChainFile(chid, file); + getChainNames().add(chid); + } + } + + /** + * Helper method to add one contiguous range to the AtomSpec model for the given + * value (creating the model if necessary). As used by Jalview, {@code value} is + *
    + *
  • a colour, when building a 'colour structure by sequence' command
  • + *
  • a feature value, when building a 'set Chimera attributes from features' + * command
  • + *
+ * + * @param map + * @param value + * @param model + * @param startPos + * @param endPos + * @param chain + */ + public static final void addAtomSpecRange(Map map, + Object value, + String model, int startPos, int endPos, String chain) + { + /* + * Get/initialize map of data for the colour + */ + AtomSpecModel atomSpec = map.get(value); + if (atomSpec == null) + { + atomSpec = new AtomSpecModel(); + map.put(value, atomSpec); + } + + atomSpec.addRange(model, startPos, endPos, chain); + } + + /** + * Returns the file extension (including '.' separator) to use for a saved + * viewer session file. Default is to return null (not supported), override as + * required. + * + * @return + */ + public String getSessionFileExtension() + { + return null; + } + + /** + * If supported, saves the state of the structure viewer to a temporary file + * and returns the file. Returns null and logs an error on any failure. + * + * @return + */ + public File saveSession() + { + String prefix = getViewerType().toString(); + String suffix = getSessionFileExtension(); + File f = null; + try + { + f = File.createTempFile(prefix, suffix); + saveSession(f); + } catch (IOException e) + { + Cache.log.error(String.format("Error saving %s session: %s", + prefix, e.toString())); + } + + return f; + } + + /** + * Saves the structure viewer session to the given file + * + * @param f + */ + protected void saveSession(File f) + { + StructureCommandI cmd = commandGenerator + .saveSession(f.getPath()); + if (cmd != null) + { + executeCommand(cmd, false); + } + } + + /** + * Returns true if the viewer is an external structure viewer for which the + * process is still alive, else false (for Jmol, or an external viewer which + * the user has independently closed) + * + * @return + */ + public boolean isViewerRunning() + { + return false; + } + + /** + * Closes Jalview's structure viewer panel and releases associated resources. + * If it is managing an external viewer program, and {@code forceClose} is + * true, also shuts down that program. + * + * @param forceClose + */ + public void closeViewer(boolean forceClose) + { + getSsm().removeStructureViewerListener(this, this.getStructureFiles()); + releaseUIResources(); + + // add external viewer shutdown in overrides + // todo - or can maybe pull up to here + } + + /** + * Returns the URL of a help page for the structure viewer, or null if none is + * known + * + * @return + */ + public String getHelpURL() + { + return null; + } + + /** + *
+   * Helper method to build a map of 
+   *   { featureType, { feature value, AtomSpecModel } }
+   * 
+ * + * @param viewPanel + * @return + */ + protected Map> buildFeaturesMap( + AlignmentViewPanel viewPanel) + { + Map> theMap = new LinkedHashMap<>(); + String[] files = getStructureFiles(); + if (files == null) + { + return theMap; + } + + FeatureRenderer fr = viewPanel.getFeatureRenderer(); + if (fr == null) + { + return theMap; + } + + AlignViewportI viewport = viewPanel.getAlignViewport(); + List visibleFeatures = fr.getDisplayedFeatureTypes(); + + /* + * if alignment is showing features from complement, we also transfer + * these features to the corresponding mapped structure residues + */ + boolean showLinkedFeatures = viewport.isShowComplementFeatures(); + List complementFeatures = new ArrayList<>(); + FeatureRenderer complementRenderer = null; + if (showLinkedFeatures) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + if (comp != null) + { + complementRenderer = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + complementFeatures = complementRenderer.getDisplayedFeatureTypes(); + } + } + if (visibleFeatures.isEmpty() && complementFeatures.isEmpty()) + { + return theMap; + } + + AlignmentI alignment = viewPanel.getAlignment(); + SequenceI[][] seqs = getSequence(); + + for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) + { + String modelId = getModelIdForFile(files[pdbfnum]); + StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); + + if (mapping == null || mapping.length < 1) + { + continue; + } + + for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++) + { + for (int m = 0; m < mapping.length; m++) + { + final SequenceI seq = seqs[pdbfnum][seqNo]; + int sp = alignment.findIndex(seq); + StructureMapping structureMapping = mapping[m]; + if (structureMapping.getSequence() == seq && sp > -1) + { + /* + * found a sequence with a mapping to a structure; + * now scan its features + */ + if (!visibleFeatures.isEmpty()) + { + scanSequenceFeatures(visibleFeatures, structureMapping, seq, + theMap, modelId); + } + if (showLinkedFeatures) + { + scanComplementFeatures(complementRenderer, structureMapping, + seq, theMap, modelId); + } + } + } + } + } + return theMap; + } + + /** + * Ask the structure viewer to open a session file. Returns true if + * successful, else false (or not supported). + * + * @param filepath + * @return + */ + public boolean openSession(String filepath) + { + StructureCommandI cmd = getCommandGenerator().openSession(filepath); + if (cmd == null) + { + return false; + } + executeCommand(cmd, true); + // todo: test for failure - how? + return true; + } + + /** + * Scans visible features in mapped positions of the CDS/peptide complement, and + * adds any found to the map of attribute values/structure positions + * + * @param complementRenderer + * @param structureMapping + * @param seq + * @param theMap + * @param modelNumber + */ + protected static void scanComplementFeatures( + FeatureRenderer complementRenderer, + StructureMapping structureMapping, SequenceI seq, + Map> theMap, + String modelNumber) + { + /* + * for each sequence residue mapped to a structure position... + */ + for (int seqPos : structureMapping.getMapping().keySet()) + { + /* + * find visible complementary features at mapped position(s) + */ + MappedFeatures mf = complementRenderer + .findComplementFeaturesAtResidue(seq, seqPos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + String type = sf.getType(); + + /* + * Don't copy features which originated from Chimera + */ + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) + { + continue; + } + + /* + * record feature 'value' (score/description/type) as at the + * corresponding structure position + */ + List mappedRanges = structureMapping + .getPDBResNumRanges(seqPos, seqPos); + + if (!mappedRanges.isEmpty()) + { + String value = sf.getDescription(); + if (value == null || value.length() == 0) + { + value = type; + } + float score = sf.getScore(); + if (score != 0f && !Float.isNaN(score)) + { + value = Float.toString(score); + } + Map featureValues = theMap.get(type); + if (featureValues == null) + { + featureValues = new HashMap<>(); + theMap.put(type, featureValues); + } + for (int[] range : mappedRanges) + { + addAtomSpecRange(featureValues, value, modelNumber, range[0], + range[1], structureMapping.getChain()); + } + } + } + } + } + } + + /** + * Inspect features on the sequence; for each feature that is visible, + * determine its mapped ranges in the structure (if any) according to the + * given mapping, and add them to the map. + * + * @param visibleFeatures + * @param mapping + * @param seq + * @param theMap + * @param modelId + */ + protected static void scanSequenceFeatures(List visibleFeatures, + StructureMapping mapping, SequenceI seq, + Map> theMap, String modelId) + { + List sfs = seq.getFeatures().getPositionalFeatures( + visibleFeatures.toArray(new String[visibleFeatures.size()])); + for (SequenceFeature sf : sfs) + { + String type = sf.getType(); + + /* + * Don't copy features which originated from Chimera + */ + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) + { + continue; + } + + List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(), + sf.getEnd()); + + if (!mappedRanges.isEmpty()) + { + String value = sf.getDescription(); + if (value == null || value.length() == 0) + { + value = type; + } + float score = sf.getScore(); + if (score != 0f && !Float.isNaN(score)) + { + value = Float.toString(score); + } + Map featureValues = theMap.get(type); + if (featureValues == null) + { + featureValues = new HashMap<>(); + theMap.put(type, featureValues); + } + for (int[] range : mappedRanges) + { + addAtomSpecRange(featureValues, value, modelId, range[0], + range[1], mapping.getChain()); + } + } + } + } + + /** + * Returns the number of structure files in the structure viewer and mapped to + * Jalview. This may be zero if the files are still in the process of loading + * in the viewer. + * + * @return + */ + public int getMappedStructureCount() + { + String[] files = getStructureFiles(); + return files == null ? 0 : files.length; + } } diff --git a/src/jalview/ws/HttpClientUtils.java b/src/jalview/ws/HttpClientUtils.java index b19d606..8f97226 100644 --- a/src/jalview/ws/HttpClientUtils.java +++ b/src/jalview/ws/HttpClientUtils.java @@ -34,6 +34,7 @@ import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntity; @@ -167,4 +168,65 @@ public class HttpClientUtils return null; } } + + /** + * do an HTTP GET with URL-Encoded parameters passed in the Query string + * + * @param url + * @param vals + * @return Reader containing content, if any, or null if no entity returned. + * @throws IOException + * @throws ClientProtocolException + * @throws Exception + */ + public static BufferedReader doHttpGet(String url, + List vals, int connectionTimeoutMs, + int readTimeoutMs) throws ClientProtocolException, IOException + { + // todo use HttpClient 4.3 or later and class RequestConfig + HttpParams params = new BasicHttpParams(); + params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, + HttpVersion.HTTP_1_1); + if (connectionTimeoutMs > 0) + { + HttpConnectionParams.setConnectionTimeout(params, + connectionTimeoutMs); + } + if (readTimeoutMs > 0) + { + HttpConnectionParams.setSoTimeout(params, readTimeoutMs); + } + boolean first = true; + for (NameValuePair param : vals) + { + if (first) + { + url += "?"; + } + else + { + url += "&"; + } + url += param.getName(); + url += "="; + url += param.getValue(); + } + HttpClient httpclient = new DefaultHttpClient(params); + HttpGet httpGet = new HttpGet(url); + // UrlEncodedFormEntity ue = new UrlEncodedFormEntity(vals, "UTF-8"); + // httpGet.setEntity(ue); + HttpResponse response = httpclient.execute(httpGet); + HttpEntity resEntity = response.getEntity(); + + if (resEntity != null) + { + BufferedReader r = new BufferedReader( + new InputStreamReader(resEntity.getContent())); + return r; + } + else + { + return null; + } + } } diff --git a/test/jalview/ext/jmol/JmolCommandsTest.java b/test/jalview/ext/jmol/JmolCommandsTest.java index e42b54f..1ade00e 100644 --- a/test/jalview/ext/jmol/JmolCommandsTest.java +++ b/test/jalview/ext/jmol/JmolCommandsTest.java @@ -23,6 +23,15 @@ package jalview.ext.jmol; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; +import java.awt.Color; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; @@ -32,15 +41,12 @@ import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import jalview.gui.SequenceRenderer; import jalview.schemes.JalviewColourScheme; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsI; import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; import jalview.structure.StructureSelectionManager; -import java.util.HashMap; - -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; - public class JmolCommandsTest { @@ -52,24 +58,6 @@ public class JmolCommandsTest } @Test(groups = { "Functional" }) - public void testGetColourBySequenceCommand_noFeatures() - { - SequenceI seq1 = new Sequence("seq1", "MHRSQTRALK"); - SequenceI seq2 = new Sequence("seq2", "MRLEITQSGD"); - AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 }); - AlignFrame af = new AlignFrame(al, 800, 500); - SequenceRenderer sr = new SequenceRenderer(af.getViewport()); - SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } }; - String[] files = new String[] { "seq1.pdb", "seq2.pdb" }; - StructureSelectionManager ssm = new StructureSelectionManager(); - - // need some mappings! - - StructureMappingcommandSet[] commands = JmolCommands - .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel); - } - - @Test(groups = { "Functional" }) public void testGetColourBySequenceCommands_hiddenColumns() { /* @@ -91,11 +79,11 @@ public class JmolCommandsTest SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } }; String[] files = new String[] { "seq1.pdb", "seq2.pdb" }; StructureSelectionManager ssm = new StructureSelectionManager(); - + /* * map residues 1-10 to residues 21-30 (atoms 105-150) in structures */ - HashMap map = new HashMap(); + HashMap map = new HashMap<>(); for (int pos = 1; pos <= seq1.getLength(); pos++) { map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) }); @@ -106,37 +94,129 @@ public class JmolCommandsTest StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2", "B", map, null); ssm.addStructureMapping(sm2); - - StructureMappingcommandSet[] commands = JmolCommands - .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel); + + String[] commands = new JmolCommands().colourBySequence(ssm, files, + seqs, sr, af.alignPanel); assertEquals(commands.length, 2); - assertEquals(commands[0].commands.length, 1); - String chainACommand = commands[0].commands[0]; + String chainACommand = commands[0]; // M colour is #82827d == (130, 130, 125) (see strand.html help page) - assertTrue(chainACommand - .contains("select 21:A/1.1;color[130,130,125]")); // first one + assertTrue( + chainACommand.contains("select 21:A/1.1;color[130,130,125]")); // first + // one // H colour is #60609f == (96, 96, 159) assertTrue(chainACommand.contains(";select 22:A/1.1;color[96,96,159]")); // hidden columns are Gray (128, 128, 128) assertTrue(chainACommand .contains(";select 23-25:A/1.1;color[128,128,128]")); // S and G are both coloured #4949b6 == (73, 73, 182) - assertTrue(chainACommand - .contains(";select 26-30:A/1.1;color[73,73,182]")); + assertTrue( + chainACommand.contains(";select 26-30:A/1.1;color[73,73,182]")); - String chainBCommand = commands[1].commands[0]; + String chainBCommand = commands[1]; // M colour is #82827d == (130, 130, 125) - assertTrue(chainBCommand - .contains("select 21:B/2.1;color[130,130,125]")); + assertTrue( + chainBCommand.contains("select 21:B/2.1;color[130,130,125]")); // V colour is #ffff00 == (255, 255, 0) - assertTrue(chainBCommand -.contains(";select 22:B/2.1;color[255,255,0]")); + assertTrue(chainBCommand.contains(";select 22:B/2.1;color[255,255,0]")); // hidden columns are Gray (128, 128, 128) assertTrue(chainBCommand .contains(";select 23-25:B/2.1;color[128,128,128]")); // S and G are both coloured #4949b6 == (73, 73, 182) - assertTrue(chainBCommand - .contains(";select 26-30:B/2.1;color[73,73,182]")); + assertTrue( + chainBCommand.contains(";select 26-30:B/2.1;color[73,73,182]")); + } + + @Test(groups = "Functional") + public void testGetAtomSpec() + { + StructureCommandsI testee = new JmolCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, false), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1|8:A/1.1"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-4:A/1.1|8:A/1.1|5-7:B/1.1"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-7:B/1.1"); + model.addRange("2", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1"); + model.addRange("2", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1|5-9:C/2.1"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1"); + model.addRange("2", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1"); + model.addRange("5", 25, 35, " "); + assertEquals(testee.getAtomSpec(model, false), + "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1|25-35:/5.1"); + + } + + @Test(groups = { "Functional" }) + public void testColourBySequence() + { + Map map = new LinkedHashMap<>(); + JmolCommands.addAtomSpecRange(map, Color.blue, "1", 2, 5, "A"); + JmolCommands.addAtomSpecRange(map, Color.blue, "1", 7, 7, "B"); + JmolCommands.addAtomSpecRange(map, Color.blue, "1", 9, 23, "A"); + JmolCommands.addAtomSpecRange(map, Color.blue, "2", 1, 1, "A"); + JmolCommands.addAtomSpecRange(map, Color.blue, "2", 4, 7, "B"); + JmolCommands.addAtomSpecRange(map, Color.yellow, "2", 8, 8, "A"); + JmolCommands.addAtomSpecRange(map, Color.yellow, "2", 3, 5, "A"); + JmolCommands.addAtomSpecRange(map, Color.red, "1", 3, 5, "A"); + JmolCommands.addAtomSpecRange(map, Color.red, "1", 6, 9, "A"); + + // Colours should appear in the Jmol command in the order in which + // they were added; within colour, by model, by chain, ranges in start order + List commands = new JmolCommands() + .colourBySequence(map); + assertEquals(commands.size(), 1); + String expected1 = "select 2-5:A/1.1|9-23:A/1.1|7:B/1.1|1:A/2.1|4-7:B/2.1;color[0,0,255]"; + String expected2 = "select 3-5:A/2.1|8:A/2.1;color[255,255,0]"; + String expected3 = "select 3-9:A/1.1;color[255,0,0]"; + assertEquals(commands.get(0).getCommand(), + expected1 + ";" + expected2 + ";" + expected3); + } + + @Test(groups = { "Functional" }) + public void testSuperposeStructures() + { + StructureCommandsI testee = new JmolCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange("1", 12, 14, "A"); + ref.addRange("1", 18, 18, "B"); + ref.addRange("1", 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange("2", 15, 17, "B"); + toAlign.addRange("2", 20, 21, "B"); + toAlign.addRange("2", 22, 22, "C"); + List command = testee.superposeStructures(ref, + toAlign); + assertEquals(command.size(), 1); + String refSpec = "12-14:A/1.1|18:B/1.1|22-23:B/1.1"; + String toAlignSpec = "15-17:B/2.1|20-21:B/2.1|22:C/2.1"; + String expected = String.format( + "compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS {%s}{%s} ROTATE TRANSLATE ;select %s|%s;cartoons", + toAlignSpec, refSpec, toAlignSpec, refSpec); + assertEquals(command.get(0).getCommand(), expected); + } + + @Test(groups = "Functional") + public void testGetModelStartNo() + { + StructureCommandsI testee = new JmolCommands(); + assertEquals(testee.getModelStartNo(), 1); } } diff --git a/test/jalview/ext/pymol/PymolCommandsTest.java b/test/jalview/ext/pymol/PymolCommandsTest.java new file mode 100644 index 0000000..38031b6 --- /dev/null +++ b/test/jalview/ext/pymol/PymolCommandsTest.java @@ -0,0 +1,231 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ +package jalview.ext.pymol; + +import static org.testng.Assert.assertEquals; + +import java.awt.Color; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.Test; + +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsI; + +public class PymolCommandsTest +{ + + @Test(groups = { "Functional" }) + public void testColourBySequence() + { + + Map map = new LinkedHashMap<>(); + PymolCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A"); + PymolCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B"); + PymolCommands.addAtomSpecRange(map, Color.blue, "0", 9, 23, "A"); + PymolCommands.addAtomSpecRange(map, Color.blue, "1", 1, 1, "A"); + PymolCommands.addAtomSpecRange(map, Color.blue, "1", 4, 7, "B"); + PymolCommands.addAtomSpecRange(map, Color.yellow, "1", 8, 8, "A"); + PymolCommands.addAtomSpecRange(map, Color.yellow, "1", 3, 5, "A"); + PymolCommands.addAtomSpecRange(map, Color.red, "0", 3, 5, "A"); + PymolCommands.addAtomSpecRange(map, Color.red, "0", 6, 9, "A"); + + // Colours should appear in the Pymol command in the order in which + // they were added; within colour, by model, by chain, ranges in start order + List commands = new PymolCommands() + .colourBySequence(map); + assertEquals(commands.size(), 3); + assertEquals(commands.get(0).toString(), + "color(0x0000ff,0//A/2-5+9-23/ 0//B/7/ 1//A/1/ 1//B/4-7/)"); + assertEquals(commands.get(1).toString(), "color(0xffff00,1//A/3-5+8/)"); + assertEquals( + commands.get(2).toString(), "color(0xff0000,0//A/3-9/)"); + } + + @Test(groups = "Functional") + public void testGetAtomSpec() + { + StructureCommandsI testee = new PymolCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, false), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, false), "1//A/2-4/"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, false), "1//A/2-4+8/"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, false), "1//A/2-4+8/ 1//B/5-7/"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, false), "1//A/2-5+8/ 1//B/5-7/"); + model.addRange("0", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, false), + "0//B/1-4/ 1//A/2-5+8/ 1//B/5-7/"); + model.addRange("0", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, false), + "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-7/"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, false), + "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-10/"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, false), + "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-10/"); + model.addRange("0", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, false), + "0//B/1-4/ 0//C/3-10/ 1//A/2-5+8/ 1//B/5-10/"); + model.addRange("5", 25, 35, " "); + assertEquals(testee.getAtomSpec(model, false), + "0//B/1-4/ 0//C/3-10/ 1//A/2-5+8/ 1//B/5-10/ 5///25-35/"); + + } + + @Test(groups = { "Functional" }) + public void testSuperposeStructures() + { + StructureCommandsI testee = new PymolCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange("1", 12, 14, "A"); + ref.addRange("1", 18, 18, "B"); + ref.addRange("1", 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange("2", 15, 17, "B"); + toAlign.addRange("2", 20, 21, "B"); + toAlign.addRange("2", 22, 22, "C"); + List commands = testee.superposeStructures(ref, + toAlign); + assertEquals(commands.size(), 2); + String refSpecCA = "1//A/12-14/CA 1//B/18+22-23/CA"; + String toAlignSpecCA = "2//B/15-17+20-21/CA 2//C/22/CA"; + String refSpec = "1//A/12-14/ 1//B/18+22-23/"; + String toAlignSpec = "2//B/15-17+20-21/ 2//C/22/"; + String expected1 = String.format("super(%s,%s)", refSpecCA, + toAlignSpecCA); + String expected2 = String.format("show(cartoon,%s %s)", refSpec, + toAlignSpec); + assertEquals(commands.get(0).toString(), expected1); + assertEquals(commands.get(1).toString(), expected2); + } + + @Test(groups = "Functional") + public void testGetAtomSpec_alphaOnly() + { + StructureCommandsI testee = new PymolCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, true), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, true), "1//A/2-4/CA"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, true), "1//A/2-4+8/CA"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, true), + "1//A/2-4+8/CA 1//B/5-7/CA"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, true), + "1//A/2-5+8/CA 1//B/5-7/CA"); + model.addRange("0", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, true), + "0//B/1-4/CA 1//A/2-5+8/CA 1//B/5-7/CA"); + model.addRange("0", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, true), + "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-7/CA"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, true), + "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-10/CA"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, true), + "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-10/CA"); + model.addRange("0", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, true), + "0//B/1-4/CA 0//C/3-10/CA 1//A/2-5+8/CA 1//B/5-10/CA"); + model.addRange("5", 25, 35, " "); + assertEquals(testee.getAtomSpec(model, true), + "0//B/1-4/CA 0//C/3-10/CA 1//A/2-5+8/CA 1//B/5-10/CA 5///25-35/CA"); + } + + @Test(groups = "Functional") + public void testGetModelStartNo() + { + StructureCommandsI testee = new PymolCommands(); + assertEquals(testee.getModelStartNo(), 0); + } + + @Test(groups = "Functional") + public void testGetResidueSpec() + { + PymolCommands testee = new PymolCommands(); + assertEquals(testee.getResidueSpec("ALA"), "resn ALA"); + } + + @Test(groups = "Functional") + public void testShowBackbone() + { + PymolCommands testee = new PymolCommands(); + List cmds = testee.showBackbone(); + assertEquals(cmds.size(), 2); + assertEquals(cmds.get(0).toString(), "hide(everything)"); + assertEquals(cmds.get(1).toString(), "show(ribbon)"); + } + + @Test(groups = "Functional") + public void testColourByCharge() + { + PymolCommands testee = new PymolCommands(); + List cmds = testee.colourByCharge(); + assertEquals(cmds.size(), 4); + assertEquals(cmds.get(0).toString(), "color(white,*)"); + assertEquals(cmds.get(1).toString(), "color(red,resn ASP resn GLU)"); + assertEquals(cmds.get(2).toString(), "color(blue,resn LYS resn ARG)"); + assertEquals(cmds.get(3).toString(), "color(yellow,resn CYS)"); + } + + @Test(groups = "Functional") + public void testOpenCommandFile() + { + PymolCommands testee = new PymolCommands(); + assertEquals(testee.openCommandFile("commands.pml").toString(), + "run(commands.pml)"); + } + + @Test(groups = "Functional") + public void testSaveSession() + { + PymolCommands testee = new PymolCommands(); + assertEquals(testee.saveSession("somewhere.pse").toString(), + "save(somewhere.pse)"); + } + + @Test(groups = "Functional") + public void testColourByChain() + { + PymolCommands testee = new PymolCommands(); + assertEquals(testee.colourByChain().toString(), "spectrum(chain)"); + } + + @Test(groups = "Functional") + public void testGetColourCommand() + { + PymolCommands testee = new PymolCommands(); + assertEquals( + testee.colourResidues("something", Color.MAGENTA).toString(), + "color(0xff00ff,something)"); + } +} diff --git a/test/jalview/ext/pymol/PymolManagerTest.java b/test/jalview/ext/pymol/PymolManagerTest.java new file mode 100644 index 0000000..c415ace --- /dev/null +++ b/test/jalview/ext/pymol/PymolManagerTest.java @@ -0,0 +1,64 @@ +package jalview.ext.pymol; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.List; + +import org.testng.annotations.Test; + +import jalview.structure.StructureCommand; + +public class PymolManagerTest +{ + @Test(groups = "Functional") + public void testGetPostRequest() + { + String req = PymolManager + .getPostRequest(new StructureCommand("foobar")); + assertEquals(req, + "foobar"); + + req = PymolManager + .getPostRequest(new StructureCommand("foobar", "blue", "all")); + assertEquals(req, "foobar" + + "blue" + + "all" + + ""); + } + + @Test(groups = "Functional") + public void testGetPymolPaths() + { + /* + * OSX + */ + List paths = PymolManager.getPymolPaths("Mac OS X"); + assertEquals(paths.size(), 1); + assertTrue( + paths.contains("/Applications/PyMOL.app/Contents/MacOS/PyMOL")); + + /* + * Linux + */ + paths = PymolManager.getPymolPaths("Linux i386 1.5.0"); + assertTrue(paths.contains("/usr/local/pymol/bin/PyMOL")); + assertTrue(paths.contains("/usr/local/bin/PyMOL")); + assertTrue(paths.contains("/usr/bin/PyMOL")); + assertTrue(paths.contains("/usr/local/pymol/bin/PyMOL")); + assertTrue(paths + .contains(System.getProperty("user.home") + "/opt/bin/PyMOL")); + + /* + * Windows + */ + paths = PymolManager.getPymolPaths("Windows 10"); + assertTrue(paths.isEmpty()); // TODO - Windows paths + + /* + * Other + */ + paths = PymolManager.getPymolPaths("VAX/VMS"); + assertTrue(paths.isEmpty()); + } +} diff --git a/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java b/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java deleted file mode 100644 index 63d5e4e..0000000 --- a/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package jalview.ext.rbvi.chimera; - -import static org.testng.Assert.assertEquals; - -import org.testng.annotations.Test; - -public class AtomSpecModelTest -{ - @Test(groups = "Functional") - public void testGetAtomSpec() - { - AtomSpecModel model = new AtomSpecModel(); - assertEquals(model.getAtomSpec(), ""); - model.addRange(1, 2, 4, "A"); - assertEquals(model.getAtomSpec(), "#1:2-4.A"); - model.addRange(1, 8, 8, "A"); - assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A"); - model.addRange(1, 5, 7, "B"); - assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A,5-7.B"); - model.addRange(1, 3, 5, "A"); - assertEquals(model.getAtomSpec(), "#1:2-5.A,8.A,5-7.B"); - model.addRange(0, 1, 4, "B"); - assertEquals(model.getAtomSpec(), "#0:1-4.B|#1:2-5.A,8.A,5-7.B"); - model.addRange(0, 5, 9, "C"); - assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B"); - model.addRange(1, 8, 10, "B"); - assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); - model.addRange(1, 8, 9, "B"); - assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); - model.addRange(0, 3, 10, "C"); // subsumes 5-9 - assertEquals(model.getAtomSpec(), "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B"); - model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology - // modelling - assertEquals(model.getAtomSpec(), - "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35."); - - } - -} diff --git a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java index 06a09df..5fc9fdc 100644 --- a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java +++ b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java @@ -23,198 +23,295 @@ package jalview.ext.rbvi.chimera; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; -import jalview.datamodel.Alignment; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.ColumnSelection; -import jalview.datamodel.Sequence; -import jalview.datamodel.SequenceI; -import jalview.gui.AlignFrame; -import jalview.gui.JvOptionPane; -import jalview.gui.SequenceRenderer; -import jalview.schemes.JalviewColourScheme; -import jalview.structure.StructureMapping; -import jalview.structure.StructureMappingcommandSet; -import jalview.structure.StructureSelectionManager; - import java.awt.Color; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsI; + public class ChimeraCommandsTest { - @BeforeClass(alwaysRun = true) - public void setUpJvOptionPane() - { - JvOptionPane.setInteractiveMode(false); - JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION); - } - @Test(groups = { "Functional" }) - public void testBuildColourCommands() + public void testColourBySequence() { - Map map = new LinkedHashMap(); - ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A"); - ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B"); - ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A"); - ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A"); - ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B"); - ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A"); - ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A"); - ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A"); - ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A"); + Map map = new LinkedHashMap<>(); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 9, 23, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 1, 1, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 4, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, "1", 8, 8, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, "1", 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, "0", 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, "0", 6, 9, "A"); // Colours should appear in the Chimera command in the order in which // they were added; within colour, by model, by chain, ranges in start order - String command = ChimeraCommands.buildColourCommands(map).get(0); - assertEquals( - command, - "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B; color #ffff00 #1:3-5.A,8.A; color #ff0000 #0:3-9.A"); + List commands = new ChimeraCommands() + .colourBySequence(map); + assertEquals(commands.size(), 1); + assertEquals(commands.get(0).getCommand(), + "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B;color #ffff00 #1:3-5.A,8.A;color #ff0000 #0:3-9.A"); } @Test(groups = { "Functional" }) - public void testBuildSetAttributeCommands() + public void testSetAttributes() { /* * make a map of { featureType, {featureValue, {residue range specification } } } */ - Map> featuresMap = new LinkedHashMap>(); - Map featureValues = new HashMap(); + Map> featuresMap = new LinkedHashMap<>(); + Map featureValues = new HashMap<>(); /* * start with just one feature/value... */ featuresMap.put("chain", featureValues); - ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A"); - List commands = ChimeraCommands - .buildSetAttributeCommands(featuresMap); + ChimeraCommands commandGenerator = new ChimeraCommands(); + List commands = commandGenerator + .setAttributes(featuresMap); assertEquals(1, commands.size()); /* * feature name gets a jv_ namespace prefix * feature value is quoted in case it contains spaces */ - assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A"); + assertEquals(commands.get(0).getCommand(), + "setattr res jv_chain 'X' #0:8-20.A"); // add same feature value, overlapping range - ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A"); // same feature value, contiguous range - ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A"); - commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A"); + commands = commandGenerator.setAttributes(featuresMap); assertEquals(1, commands.size()); - assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A"); + assertEquals(commands.get(0).getCommand(), + "setattr res jv_chain 'X' #0:3-25.A"); // same feature value and model, different chain - ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B"); // same feature value and chain, different model - ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A"); - commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); + ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A"); + commands = commandGenerator.setAttributes(featuresMap); assertEquals(1, commands.size()); - assertEquals(commands.get(0), - "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A"); + String expected1 = "setattr res jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A"; + assertEquals(commands.get(0).getCommand(), expected1); // same feature, different value - ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A"); - commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); + ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A"); + commands = commandGenerator.setAttributes(featuresMap); assertEquals(2, commands.size()); // commands are ordered by feature type but not by value - // so use contains to test for the expected command: - assertTrue(commands - .contains("setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A")); - assertTrue(commands.contains("setattr r jv_chain 'Y' #0:40-50.A")); + // so test for the expected command in either order + String cmd1 = commands.get(0).getCommand(); + String cmd2 = commands.get(1).getCommand(); + assertTrue(cmd1.equals(expected1) || cmd2.equals(expected1)); + String expected2 = "setattr res jv_chain 'Y' #0:40-50.A"; + assertTrue(cmd1.equals(expected2) || cmd2.equals(expected2)); featuresMap.clear(); featureValues.clear(); featuresMap.put("side-chain binding!", featureValues); ChimeraCommands.addAtomSpecRange(featureValues, - "metal 'ion!", 0, 7, 15, + "metal 'ion!", "0", 7, 15, "A"); // feature names are sanitised to change non-alphanumeric to underscore // feature values are sanitised to encode single quote characters - commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); - assertTrue(commands - .contains("setattr r jv_side_chain_binding_ 'metal 'ion!' #0:7-15.A")); + commands = commandGenerator.setAttributes(featuresMap); + assertEquals(commands.size(), 1); + String expected3 = "setattr res jv_side_chain_binding_ 'metal 'ion!' #0:7-15.A"; + assertTrue(commands.get(0).getCommand().equals(expected3)); } /** * Tests for the method that prefixes and sanitises a feature name so it can - * be used as a valid, namespaced attribute name in Chimera + * be used as a valid, namespaced attribute name in Chimera or PyMol */ @Test(groups = { "Functional" }) public void testMakeAttributeName() { - assertEquals(ChimeraCommands.makeAttributeName(null), "jv_"); - assertEquals(ChimeraCommands.makeAttributeName(""), "jv_"); - assertEquals(ChimeraCommands.makeAttributeName("helix"), "jv_helix"); - assertEquals(ChimeraCommands.makeAttributeName("Hello World 24"), + ChimeraCommands testee = new ChimeraCommands(); + assertEquals(testee.makeAttributeName(null), "jv_"); + assertEquals(testee.makeAttributeName(""), "jv_"); + assertEquals(testee.makeAttributeName("helix"), "jv_helix"); + assertEquals(testee.makeAttributeName( + "Hello World 24"), "jv_Hello_World_24"); assertEquals( - ChimeraCommands.makeAttributeName("!this is-a_very*{odd(name"), + testee.makeAttributeName( + "!this is-a_very*{odd(name"), "jv__this_is_a_very__odd_name"); // name ending in color gets underscore appended - assertEquals(ChimeraCommands.makeAttributeName("helixColor"), + assertEquals(testee.makeAttributeName("helixColor"), "jv_helixColor_"); } + @Test(groups = "Functional") + public void testGetAtomSpec() + { + StructureCommandsI testee = new ChimeraCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, false), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A,5-7.B"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1:2-5.A,8.A,5-7.B"); + model.addRange("0", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B|#1:2-5.A,8.A,5-7.B"); + model.addRange("0", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B"); + model.addRange("0", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B"); + model.addRange("5", 25, 35, " "); + assertEquals(testee.getAtomSpec(model, false), + "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35."); + + } + @Test(groups = { "Functional" }) - public void testGetColourBySequenceCommands_hiddenColumns() + public void testSuperposeStructures() { - /* - * load these sequences, coloured by Strand propensity, - * with columns 2-4 hidden - */ - SequenceI seq1 = new Sequence("seq1", "MHRSQSSSGG"); - SequenceI seq2 = new Sequence("seq2", "MVRSNGGSSS"); - AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 }); - AlignFrame af = new AlignFrame(al, 800, 500); - af.changeColour_actionPerformed(JalviewColourScheme.Strand.toString()); - ColumnSelection cs = new ColumnSelection(); - cs.addElement(2); - cs.addElement(3); - cs.addElement(4); - af.getViewport().setColumnSelection(cs); - af.hideSelColumns_actionPerformed(null); - SequenceRenderer sr = new SequenceRenderer(af.getViewport()); - SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } }; - String[] files = new String[] { "seq1.pdb", "seq2.pdb" }; - StructureSelectionManager ssm = new StructureSelectionManager(); + StructureCommandsI testee = new ChimeraCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange("1", 12, 14, "A"); + ref.addRange("1", 18, 18, "B"); + ref.addRange("1", 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange("2", 15, 17, "B"); + toAlign.addRange("2", 20, 21, "B"); + toAlign.addRange("2", 22, 22, "C"); + List command = testee.superposeStructures(ref, + toAlign); + // qualifier to restrict match to CA and no altlocs + String carbonAlphas = "@CA&~@.B-Z&~@.2-9"; + String refSpec = "#1:12-14.A,18.B,22-23.B"; + String toAlignSpec = "#2:15-17.B,20-21.B,22.C"; + String expected = String.format( + "match %s%s %s%s; ribbon %s|%s; focus", toAlignSpec, + carbonAlphas, refSpec, carbonAlphas, toAlignSpec, refSpec); + assertEquals(command.get(0).getCommand(), expected); + } - /* - * map residues 1-10 to residues 21-30 (atoms 105-150) in structures - */ - HashMap map = new HashMap(); - for (int pos = 1; pos <= seq1.getLength(); pos++) - { - map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) }); - } - StructureMapping sm1 = new StructureMapping(seq1, "seq1.pdb", "pdb1", - "A", map, null); - ssm.addStructureMapping(sm1); - StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2", - "B", map, null); - ssm.addStructureMapping(sm2); - - StructureMappingcommandSet[] commands = ChimeraCommands - .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel); - assertEquals(1, commands.length); - assertEquals(1, commands[0].commands.length); - String theCommand = commands[0].commands[0]; - // M colour is #82827d (see strand.html help page) - assertTrue(theCommand.contains("color #82827d #0:21.A|#1:21.B")); - // H colour is #60609f - assertTrue(theCommand.contains("color #60609f #0:22.A")); - // V colour is #ffff00 - assertTrue(theCommand.contains("color #ffff00 #1:22.B")); - // hidden columns are Gray (128, 128, 128) - assertTrue(theCommand.contains("color #808080 #0:23-25.A|#1:23-25.B")); - // S and G are both coloured #4949b6 - assertTrue(theCommand.contains("color #4949b6 #0:26-30.A|#1:26-30.B")); + @Test(groups = "Functional") + public void testGetAtomSpec_alphaOnly() + { + StructureCommandsI testee = new ChimeraCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, true), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, true), + "#1:2-4.A@CA&~@.B-Z&~@.2-9"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, true), + "#1:2-4.A,8.A@CA&~@.B-Z&~@.2-9"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#1:2-4.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, true), + "#1:2-5.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9"); + model.addRange("0", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9"); + model.addRange("0", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,5-9.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,5-9.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,5-9.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9"); + model.addRange("0", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,3-10.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9"); + model.addRange("5", 25, 35, " "); // empty chain code + assertEquals(testee.getAtomSpec(model, true), + "#0:1-4.B,3-10.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9|#5:25-35.@CA&~@.B-Z&~@.2-9"); + + } + + @Test(groups = "Functional") + public void testGetModelStartNo() + { + StructureCommandsI testee = new ChimeraCommands(); + assertEquals(testee.getModelStartNo(), 0); + } + + @Test(groups = "Functional") + public void testGetResidueSpec() + { + ChimeraCommands testee = new ChimeraCommands(); + assertEquals(testee.getResidueSpec("ALA"), "::ALA"); + } + + @Test(groups = "Functional") + public void testShowBackbone() + { + ChimeraCommands testee = new ChimeraCommands(); + List cmds = testee.showBackbone(); + assertEquals(cmds.size(), 1); + assertEquals(cmds.get(0).getCommand(), + "~display all;~ribbon;chain @CA|P"); + } + + @Test(groups = "Functional") + public void testOpenCommandFile() + { + ChimeraCommands testee = new ChimeraCommands(); + assertEquals(testee.openCommandFile("nowhere").getCommand(), + "open cmd:nowhere"); + } + + @Test(groups = "Functional") + public void testSaveSession() + { + ChimeraCommands testee = new ChimeraCommands(); + assertEquals(testee.saveSession("somewhere").getCommand(), + "save somewhere"); + } + + @Test(groups = "Functional") + public void testGetColourCommand() + { + ChimeraCommands testee = new ChimeraCommands(); + assertEquals(testee.colourResidues("something", Color.MAGENTA) + .getCommand(), + "color #ff00ff something"); + } + + @Test(groups = "Functional") + public void testSetAttribute() + { + ChimeraCommands testee = new ChimeraCommands(); + AtomSpecModel model = new AtomSpecModel(); + model.addRange("1", 89, 92, "A"); + model.addRange("2", 12, 20, "B"); + model.addRange("2", 8, 9, "B"); + assertEquals(testee.setAttribute("phi", "27.3", model).getCommand(), + "setattr res phi '27.3' #1:89-92.A|#2:8-9.B,12-20.B"); } } diff --git a/test/jalview/ext/rbvi/chimera/ChimeraConnect.java b/test/jalview/ext/rbvi/chimera/ChimeraConnect.java index 99394dc..b07a622 100644 --- a/test/jalview/ext/rbvi/chimera/ChimeraConnect.java +++ b/test/jalview/ext/rbvi/chimera/ChimeraConnect.java @@ -47,7 +47,7 @@ public class ChimeraConnect final StructureManager structureManager = new StructureManager(true); ChimeraManager cm = new ChimeraManager(structureManager); assertTrue("Couldn't launch chimera", - cm.launchChimera(StructureManager.getChimeraPaths())); + cm.launchChimera(StructureManager.getChimeraPaths(false))); assertTrue(cm.isChimeraLaunched()); // Chimera process is alive // int n=0; // not sure of the point of this is unless the tester is loading models diff --git a/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java new file mode 100644 index 0000000..210c61d --- /dev/null +++ b/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java @@ -0,0 +1,318 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * The Jalview Authors are detailed in the 'AUTHORS' file. + */ +package jalview.ext.rbvi.chimera; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.awt.Color; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.Test; + +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureCommandsI; + +public class ChimeraXCommandsTest +{ + @Test(groups = { "Functional" }) + public void testColourByCharge() + { + List cmd = new ChimeraXCommands().colourByCharge(); + assertEquals(cmd.size(), 1); + assertEquals(cmd.get(0).getCommand(), + "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow"); + } + + @Test(groups = { "Functional" }) + public void testColourByChain() + { + StructureCommandI cmd = new ChimeraXCommands().colourByChain(); + assertEquals(cmd.getCommand(), "rainbow chain"); + } + + @Test(groups = { "Functional" }) + public void testFocusView() + { + StructureCommandI cmd = new ChimeraXCommands().focusView(); + assertEquals(cmd.getCommand(), "view"); + } + + @Test(groups = { "Functional" }) + public void testColourBySequence() + { + Map map = new LinkedHashMap<>(); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 2, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 7, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 9, 23, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "2", 1, 1, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, "2", 4, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, "2", 8, 8, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, "2", 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, "1", 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, "1", 6, 9, "A"); + + /* + * Colours should appear in the Chimera command in the order in which + * they were added; within colour, by model, by chain, ranges in start order + */ + List commands = new ChimeraXCommands() + .colourBySequence(map); + assertEquals(commands.size(), 1); + assertEquals(commands.get(0).getCommand(), + "color #1/A:2-5,9-23/B:7|#2/A:1/B:4-7 #0000ff;color #2/A:3-5,8 #ffff00;color #1/A:3-9 #ff0000"); + } + + @Test(groups = { "Functional" }) + public void testSetAttributes() + { + /* + * make a map of { featureType, {featureValue, {residue range specification } } } + */ + Map> featuresMap = new LinkedHashMap<>(); + Map featureValues = new HashMap<>(); + + /* + * start with just one feature/value... + */ + featuresMap.put("chain", featureValues); + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A"); + + ChimeraXCommands commandGenerator = new ChimeraXCommands(); + List commands = commandGenerator + .setAttributes(featuresMap); + assertEquals(commands.size(), 1); + + /* + * feature name gets a jv_ namespace prefix + * feature value is quoted in case it contains spaces + */ + assertEquals(commands.get(0).getCommand(), + "setattr #0/A:8-20 res jv_chain 'X' create true"); + + // add same feature value, overlapping range + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A"); + // same feature value, contiguous range + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A"); + commands = commandGenerator.setAttributes(featuresMap); + assertEquals(commands.size(), 1); + assertEquals(commands.get(0).getCommand(), + "setattr #0/A:3-25 res jv_chain 'X' create true"); + + // same feature value and model, different chain + ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B"); + // same feature value and chain, different model + ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A"); + commands = commandGenerator.setAttributes(featuresMap); + assertEquals(commands.size(), 1); + String expected1 = "setattr #0/A:3-25/B:21-25|#1/A:26-30 res jv_chain 'X' create true"; + assertEquals(commands.get(0).getCommand(), expected1); + + // same feature, different value + ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A"); + commands = commandGenerator.setAttributes(featuresMap); + assertEquals(2, commands.size()); + // commands are ordered by feature type but not by value + // so test for the expected command in either order + String cmd1 = commands.get(0).getCommand(); + String cmd2 = commands.get(1).getCommand(); + assertTrue( + cmd1.equals(expected1) || cmd2.equals(expected1)); + String expected2 = "setattr #0/A:40-50 res jv_chain 'Y' create true"; + assertTrue( + cmd1.equals(expected2) || cmd2.equals(expected2)); + + featuresMap.clear(); + featureValues.clear(); + featuresMap.put("side-chain binding!", featureValues); + ChimeraCommands.addAtomSpecRange(featureValues, + "metal 'ion!", "0", 7, 15, + "A"); + // feature names are sanitised to change non-alphanumeric to underscore + // feature values are sanitised to encode single quote characters + commands = commandGenerator.setAttributes(featuresMap); + assertEquals(commands.size(), 1); + String expected3 = "setattr #0/A:7-15 res jv_side_chain_binding_ 'metal 'ion!' create true"; + assertTrue(commands.get(0).getCommand().equals(expected3)); + } + + @Test(groups = { "Functional" }) + public void testSuperposeStructures() + { + StructureCommandsI testee = new ChimeraXCommands(); + AtomSpecModel ref = new AtomSpecModel(); + ref.addRange("1", 12, 14, "A"); + ref.addRange("1", 18, 18, "B"); + ref.addRange("1", 22, 23, "B"); + AtomSpecModel toAlign = new AtomSpecModel(); + toAlign.addRange("2", 15, 17, "B"); + toAlign.addRange("2", 20, 21, "B"); + toAlign.addRange("2", 22, 22, "C"); + List command = testee.superposeStructures(ref, + toAlign); + assertEquals(command.size(), 1); + String cmd = command.get(0).getCommand(); + String refSpec = "#1/A:12-14/B:18,22-23"; + String toAlignSpec = "#2/B:15-17,20-21/C:22"; + + /* + * superposition arguments include AlphaCarbon restriction, + * ribbon command does not + */ + String expected = String.format( + "align %s@CA toAtoms %s@CA; ribbon %s|%s; view", + toAlignSpec, refSpec, toAlignSpec, refSpec); + assertEquals(cmd, expected); + } + + @Test(groups = "Functional") + public void testGetAtomSpec() + { + StructureCommandsI testee = new ChimeraXCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, false), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4,8"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4,8/B:5-7"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, false), "#1/A:2-5,8/B:5-7"); + model.addRange("0", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0/B:1-4|#1/A:2-5,8/B:5-7"); + model.addRange("0", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, false), + "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-7"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-10"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, false), + "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-10"); + model.addRange("0", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, false), + "#0/B:1-4/C:3-10|#1/A:2-5,8/B:5-10"); + model.addRange("5", 25, 35, " "); + assertEquals(testee.getAtomSpec(model, false), + "#0/B:1-4/C:3-10|#1/A:2-5,8/B:5-10|#5/:25-35"); + } + + @Test(groups = "Functional") + public void testGetAtomSpec_alphaOnly() + { + StructureCommandsI testee = new ChimeraXCommands(); + AtomSpecModel model = new AtomSpecModel(); + assertEquals(testee.getAtomSpec(model, true), ""); + model.addRange("1", 2, 4, "A"); + assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4@CA"); + model.addRange("1", 8, 8, "A"); + assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4,8@CA"); + model.addRange("1", 5, 7, "B"); + assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4,8/B:5-7@CA"); + model.addRange("1", 3, 5, "A"); + assertEquals(testee.getAtomSpec(model, true), "#1/A:2-5,8/B:5-7@CA"); + model.addRange("0", 1, 4, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0/B:1-4@CA|#1/A:2-5,8/B:5-7@CA"); + model.addRange("0", 5, 9, "C"); + assertEquals(testee.getAtomSpec(model, true), + "#0/B:1-4/C:5-9@CA|#1/A:2-5,8/B:5-7@CA"); + model.addRange("1", 8, 10, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0/B:1-4/C:5-9@CA|#1/A:2-5,8/B:5-10@CA"); + model.addRange("1", 8, 9, "B"); + assertEquals(testee.getAtomSpec(model, true), + "#0/B:1-4/C:5-9@CA|#1/A:2-5,8/B:5-10@CA"); + model.addRange("0", 3, 10, "C"); // subsumes 5-9 + assertEquals(testee.getAtomSpec(model, true), + "#0/B:1-4/C:3-10@CA|#1/A:2-5,8/B:5-10@CA"); + model.addRange("5", 25, 35, " "); // empty chain code + assertEquals(testee.getAtomSpec(model, true), + "#0/B:1-4/C:3-10@CA|#1/A:2-5,8/B:5-10@CA|#5/:25-35@CA"); + } + + @Test(groups = "Functional") + public void testGetModelStartNo() + { + StructureCommandsI testee = new ChimeraXCommands(); + assertEquals(testee.getModelStartNo(), 1); + } + + @Test(groups = "Functional") + public void testGetResidueSpec() + { + ChimeraCommands testee = new ChimeraXCommands(); + assertEquals(testee.getResidueSpec("ALA"), ":ALA"); + } + + @Test(groups = "Functional") + public void testShowBackbone() + { + ChimeraCommands testee = new ChimeraXCommands(); + List showBackbone = testee.showBackbone(); + assertEquals(showBackbone.size(), 1); + assertEquals(showBackbone.get(0).getCommand(), + "~display all;~ribbon;show @CA|P atoms"); + } + + @Test(groups = "Functional") + public void testOpenCommandFile() + { + ChimeraCommands testee = new ChimeraXCommands(); + assertEquals(testee.openCommandFile("nowhere").getCommand(), + "open nowhere"); + } + + @Test(groups = "Functional") + public void testSaveSession() + { + ChimeraCommands testee = new ChimeraXCommands(); + assertEquals(testee.saveSession("somewhere").getCommand(), + "save session somewhere"); + } + + @Test(groups = "Functional") + public void testGetColourCommand() + { + ChimeraCommands testee = new ChimeraXCommands(); + assertEquals(testee.colourResidues("something", Color.MAGENTA) + .getCommand(), + "color something #ff00ff"); + } + + @Test(groups = "Functional") + public void testSetAttribute() + { + ChimeraCommands testee = new ChimeraXCommands(); + AtomSpecModel model = new AtomSpecModel(); + model.addRange("1", 89, 92, "A"); + model.addRange("2", 12, 20, "B"); + model.addRange("2", 8, 9, "B"); + assertEquals(testee.setAttribute("phi", "27.3", model).getCommand(), + "setattr #1/A:89-92|#2/B:8-9,12-20 res phi '27.3' create true"); + } +} diff --git a/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java b/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java index 734f7eb..d397a6b 100644 --- a/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java +++ b/test/jalview/ext/rbvi/chimera/JalviewChimeraView.java @@ -25,6 +25,16 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Vector; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + import jalview.api.FeatureRenderer; import jalview.api.structures.JalviewStructureDisplayI; import jalview.bin.Cache; @@ -41,22 +51,13 @@ import jalview.gui.StructureViewer; import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; import jalview.io.FileLoader; +import jalview.structure.StructureCommand; import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; import jalview.ws.sifts.SiftsClient; import jalview.ws.sifts.SiftsException; import jalview.ws.sifts.SiftsSettings; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.Vector; - -import org.testng.annotations.AfterClass; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; - @Test(singleThreaded = true) public class JalviewChimeraView { @@ -148,7 +149,7 @@ public class JalviewChimeraView } } - assertTrue(binding.isChimeraRunning(), "Failed to start Chimera"); + assertTrue(binding.isViewerRunning(), "Failed to start Chimera"); assertEquals(chimeraViewer.getBinding().getPdbCount(), 1); chimeraViewer.closeViewer(true); @@ -208,7 +209,7 @@ public class JalviewChimeraView } } while (!binding.isFinishedInit()); - assertTrue(binding.isChimeraRunning(), "Failed to launch Chimera"); + assertTrue(binding.isViewerRunning(), "Failed to launch Chimera"); assertEquals(binding.getPdbCount(), 1); @@ -290,7 +291,8 @@ public class JalviewChimeraView /* * ask Chimera for its residue attribute names */ - List reply = binding.sendChimeraCommand("list resattr", true); + List reply = binding + .executeCommand(new StructureCommand("list resattr"), true); // prefixed and sanitised attribute names for Jalview features: assertTrue(reply.contains("resattr jv_domain")); assertTrue(reply.contains("resattr jv_metal_ion_binding_site")); @@ -306,8 +308,9 @@ public class JalviewChimeraView * ask Chimera for residues with an attribute * 91 and 96 on sequence --> residues 40 and 45 on chains A and B */ - reply = binding.sendChimeraCommand( - "list resi att jv_metal_ion_binding_site", true); + reply = binding.executeCommand( + new StructureCommand("list resi att jv_metal_ion_binding_site"), + true); assertEquals(reply.size(), 4); assertTrue(reply .contains("residue id #0:40.A jv_metal_ion_binding_site \"Iron-Sulfur (2Fe-2S)\" index 40")); @@ -322,7 +325,8 @@ public class JalviewChimeraView * check attributes with score values * sequence positions 62 and 65 --> residues 11 and 14 on chains A and B */ - reply = binding.sendChimeraCommand("list resi att jv_kd", true); + reply = binding.executeCommand( + new StructureCommand("list resi att jv_kd"), true); assertEquals(reply.size(), 4); assertTrue(reply.contains("residue id #0:11.A jv_kd -2.1 index 11")); assertTrue(reply.contains("residue id #0:14.A jv_kd 3.6 index 14")); @@ -332,8 +336,9 @@ public class JalviewChimeraView /* * list residues with positive kd score */ - reply = binding.sendChimeraCommand( - "list resi spec :*/jv_kd>0 attr jv_kd", true); + reply = binding.executeCommand( + new StructureCommand("list resi spec :*/jv_kd>0 attr jv_kd"), + true); assertEquals(reply.size(), 2); assertTrue(reply.contains("residue id #0:14.A jv_kd 3.6 index 14")); assertTrue(reply.contains("residue id #0:14.B jv_kd 3.6 index 14")); @@ -397,13 +402,13 @@ public class JalviewChimeraView } } while (!binding.isFinishedInit()); - assertTrue(binding.isChimeraRunning(), "Failed to launch Chimera"); + assertTrue(binding.isViewerRunning(), "Failed to launch Chimera"); assertEquals(binding.getPdbCount(), 1); /* - * 'perform' menu action to copy visible features to - * attributes in Chimera + * 'perform' menu action to copy Chimera attributes + * to features in Jalview */ // TODO rename and pull up method to binding interface // once functionality is added for Jmol as well @@ -440,14 +445,9 @@ public class JalviewChimeraView binding.copyStructureAttributesToFeatures("phi", af.getViewport() .getAlignPanel()); fr.setVisible("phi"); - List fs = fer2Arath.getFeatures().findFeatures(54, 54); - assertEquals(fs.size(), 3); - /* - * order of returned features is not guaranteed - */ - assertTrue("RESNUM".equals(fs.get(0).getType()) - || "RESNUM".equals(fs.get(1).getType()) - || "RESNUM".equals(fs.get(2).getType())); + List fs = fer2Arath.getFeatures().findFeatures(54, 54, + "phi"); + assertEquals(fs.size(), 2); assertTrue(fs.contains(new SequenceFeature("phi", "A", 54, 54, -131.0713f, "Chimera"))); assertTrue(fs.contains(new SequenceFeature("phi", "B", 54, 54, @@ -473,11 +473,11 @@ public class JalviewChimeraView int res, String featureType) { String where = "at position " + res; - List fs = seq.getFeatures().findFeatures(res, res); + List fs = seq.getFeatures().findFeatures(res, res, + featureType); - assertEquals(fs.size(), 2, where); - assertEquals(fs.get(0).getType(), "RESNUM", where); - SequenceFeature sf = fs.get(1); + assertEquals(fs.size(), 1, where); + SequenceFeature sf = fs.get(0); assertEquals(sf.getType(), featureType, where); assertEquals(sf.getFeatureGroup(), "Chimera", where); assertEquals(sf.getDescription(), "True", where); diff --git a/test/jalview/structure/AtomSpecModelTest.java b/test/jalview/structure/AtomSpecModelTest.java new file mode 100644 index 0000000..394679f --- /dev/null +++ b/test/jalview/structure/AtomSpecModelTest.java @@ -0,0 +1,51 @@ +package jalview.structure; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.List; + +import org.testng.annotations.Test; + +public class AtomSpecModelTest +{ + @Test(groups="Functional") + public void testGetRanges() + { + AtomSpecModel model = new AtomSpecModel(); + assertFalse(model.getModels().iterator().hasNext()); + List ranges = model.getRanges("1", "A"); + assertTrue(ranges.isEmpty()); + + model.addRange("1", 12, 14, "A"); + assertTrue(model.getRanges("1", "B").isEmpty()); + assertTrue(model.getRanges("2", "A").isEmpty()); + ranges = model.getRanges("1", "A"); + assertEquals(ranges.size(), 1); + int[] range = ranges.get(0); + assertEquals(range[0], 12); + assertEquals(range[1], 14); + + /* + * add some ranges; they should be coalesced and + * ordered when retrieved + */ + model.addRange("1", 25, 25, "A"); + model.addRange("1", 20, 24, "A"); + model.addRange("1", 6, 8, "A"); + model.addRange("1", 13, 18, "A"); + model.addRange("1", 5, 6, "A"); + ranges = model.getRanges("1", "A"); + assertEquals(ranges.size(), 3); + range = ranges.get(0); + assertEquals(range[0], 5); + assertEquals(range[1], 8); + range = ranges.get(1); + assertEquals(range[0], 12); + assertEquals(range[1], 18); + range = ranges.get(2); + assertEquals(range[0], 20); + assertEquals(range[1], 25); + } +} diff --git a/test/jalview/structure/AtomSpecTest.java b/test/jalview/structure/AtomSpecTest.java index ea53131..ff6e6cb 100644 --- a/test/jalview/structure/AtomSpecTest.java +++ b/test/jalview/structure/AtomSpecTest.java @@ -9,23 +9,23 @@ import org.testng.annotations.Test; public class AtomSpecTest { @Test - public void testFromChimeraAtomSpec() + public void testFromChimeraAtomSpec_chimera() { - AtomSpec as = AtomSpec.fromChimeraAtomspec("#1:12.B"); + AtomSpec as = AtomSpec.fromChimeraAtomspec("#1:12.B", false); assertEquals(as.getModelNumber(), 1); assertEquals(as.getPdbResNum(), 12); assertEquals(as.getChain(), "B"); assertNull(as.getPdbFile()); // no model - default to zero - as = AtomSpec.fromChimeraAtomspec(":13.C"); + as = AtomSpec.fromChimeraAtomspec(":13.C", false); assertEquals(as.getModelNumber(), 0); assertEquals(as.getPdbResNum(), 13); assertEquals(as.getChain(), "C"); assertNull(as.getPdbFile()); // model.submodel - as = AtomSpec.fromChimeraAtomspec("#3.2:15"); + as = AtomSpec.fromChimeraAtomspec("#3.2:15", false); assertEquals(as.getModelNumber(), 3); assertEquals(as.getPdbResNum(), 15); assertEquals(as.getChain(), ""); @@ -34,7 +34,7 @@ public class AtomSpecTest String spec = "3:12.B"; try { - as = AtomSpec.fromChimeraAtomspec(spec); + as = AtomSpec.fromChimeraAtomspec(spec, false); fail("Expected exception for " + spec); } catch (IllegalArgumentException e) { @@ -44,7 +44,7 @@ public class AtomSpecTest spec = "#3:12-14.B"; try { - as = AtomSpec.fromChimeraAtomspec(spec); + as = AtomSpec.fromChimeraAtomspec(spec, false); fail("Expected exception for " + spec); } catch (IllegalArgumentException e) { @@ -54,7 +54,7 @@ public class AtomSpecTest spec = ""; try { - as = AtomSpec.fromChimeraAtomspec(spec); + as = AtomSpec.fromChimeraAtomspec(spec, false); fail("Expected exception for " + spec); } catch (IllegalArgumentException e) { @@ -64,7 +64,71 @@ public class AtomSpecTest spec = null; try { - as = AtomSpec.fromChimeraAtomspec(spec); + as = AtomSpec.fromChimeraAtomspec(spec, false); + fail("Expected exception for " + spec); + } catch (NullPointerException e) + { + // ok + } + } + + @Test + public void testFromChimeraAtomSpec_chimeraX() + { + AtomSpec as = AtomSpec.fromChimeraAtomspec("#1/B:12", true); + assertEquals(as.getModelNumber(), 1); + assertEquals(as.getPdbResNum(), 12); + assertEquals(as.getChain(), "B"); + assertNull(as.getPdbFile()); + + // no model - default to zero + as = AtomSpec.fromChimeraAtomspec("/C:13", true); + assertEquals(as.getModelNumber(), 0); + assertEquals(as.getPdbResNum(), 13); + assertEquals(as.getChain(), "C"); + assertNull(as.getPdbFile()); + + // model.submodel + as = AtomSpec.fromChimeraAtomspec("#3.2/:15", true); + assertEquals(as.getModelNumber(), 3); + assertEquals(as.getPdbResNum(), 15); + assertEquals(as.getChain(), ""); + assertNull(as.getPdbFile()); + + String spec = "3:12.B"; + try + { + as = AtomSpec.fromChimeraAtomspec(spec, true); + fail("Expected exception for " + spec); + } catch (IllegalArgumentException e) + { + // ok + } + + spec = "#3:12-14.B"; + try + { + as = AtomSpec.fromChimeraAtomspec(spec, true); + fail("Expected exception for " + spec); + } catch (IllegalArgumentException e) + { + // ok + } + + spec = ""; + try + { + as = AtomSpec.fromChimeraAtomspec(spec, true); + fail("Expected exception for " + spec); + } catch (IllegalArgumentException e) + { + // ok + } + + spec = null; + try + { + as = AtomSpec.fromChimeraAtomspec(spec, true); fail("Expected exception for " + spec); } catch (NullPointerException e) { diff --git a/test/jalview/structure/StructureSelectionManagerTest.java b/test/jalview/structure/StructureSelectionManagerTest.java index e59648f..9b240d3 100644 --- a/test/jalview/structure/StructureSelectionManagerTest.java +++ b/test/jalview/structure/StructureSelectionManagerTest.java @@ -124,10 +124,10 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase acf3.addMap(new Sequence("s3", "ttt"), new Sequence("p3", "p"), new MapList(new int[] { 1, 3 }, new int[] { 1, 1 }, 1, 1)); - List set1 = new ArrayList(); + List set1 = new ArrayList<>(); set1.add(acf1); set1.add(acf2); - List set2 = new ArrayList(); + List set2 = new ArrayList<>(); set2.add(acf2); set2.add(acf3); @@ -218,7 +218,7 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase assertEquals(1, pmap.getSeqs().size()); assertEquals("4IM2|A", pmap.getSeqs().get(0).getName()); - List structuremap1 = new ArrayList( + List structuremap1 = new ArrayList<>( sm.getMapping(P4IM2_MISSING)[0] .getPDBResNumRanges(seq.getStart(), seq.getEnd())); @@ -313,8 +313,7 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase // positional mapping to atoms for color by structure is still wrong, even // though panel looks correct. - StructureMappingcommandSet smcr[] = JmolCommands - .getColourBySequenceCommand(apssm, + String[] smcr = new JmolCommands().colourBySequence(apssm, new String[] { pdbe.getFile() }, new SequenceI[][] @@ -322,12 +321,10 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase new SequenceRenderer(alf.alignPanel.getAlignViewport()), alf.alignPanel); // Expected - all residues are white - for (StructureMappingcommandSet smm : smcr) + for (String c : smcr) { - for (String c : smm.commands) - { - System.out.println(c); - } + assertTrue(c.contains("color[255,255,255]")); + System.out.println(c); } } diff --git a/test/jalview/structures/models/AAStructureBindingModelTest.java b/test/jalview/structures/models/AAStructureBindingModelTest.java index af02d5e..c1ad03a 100644 --- a/test/jalview/structures/models/AAStructureBindingModelTest.java +++ b/test/jalview/structures/models/AAStructureBindingModelTest.java @@ -20,38 +20,46 @@ */ package jalview.structures.models; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; -import static org.testng.AssertJUnit.assertEquals; -import static org.testng.AssertJUnit.assertTrue; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.awt.Color; +import java.io.IOException; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; import jalview.api.AlignmentViewPanel; -import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; -import jalview.datamodel.HiddenColumns; +import jalview.datamodel.ColumnSelection; import jalview.datamodel.PDBEntry; import jalview.datamodel.PDBEntry.Type; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; +import jalview.ext.rbvi.chimera.ChimeraCommands; +import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; +import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; import jalview.io.FileFormats; -import jalview.schemes.ColourSchemeI; +import jalview.io.FileLoader; +import jalview.schemes.JalviewColourScheme; import jalview.structure.AtomSpec; -import jalview.structure.StructureMappingcommandSet; +import jalview.structure.AtomSpecModel; +import jalview.structure.StructureCommandI; +import jalview.structure.StructureMapping; import jalview.structure.StructureSelectionManager; -import jalview.structures.models.AAStructureBindingModel.SuperposeData; - -import java.awt.Color; -import java.io.IOException; -import java.util.Arrays; -import java.util.BitSet; -import java.util.List; - -import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; +import junit.extensions.PA; /** * Unit tests for non-abstract methods of abstract base class @@ -139,110 +147,55 @@ public class AAStructureBindingModelTest @Override public void updateColours(Object source) { - // TODO Auto-generated method stub - } @Override public void releaseReferences(Object svl) { - // TODO Auto-generated method stub - } @Override public String[] getStructureFiles() { - // TODO Auto-generated method stub - return null; - } - - @Override - public String superposeStructures(AlignmentI[] alignments, - int[] structureIndices, HiddenColumns[] hiddenCols) - { - // TODO Auto-generated method stub return null; } @Override - public void setJalviewColourScheme(ColourSchemeI cs) - { - // TODO Auto-generated method stub - - } - - @Override - public void setBackgroundColour(Color col) - { - // TODO Auto-generated method stub - - } - - @Override public void highlightAtoms(List atoms) { - // TODO Auto-generated method stub - } @Override public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment) { - // TODO Auto-generated method stub return null; } - + @Override - public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment) + protected List executeCommand(StructureCommandI command, + boolean getReply) { - // TODO Auto-generated method stub return null; } - + @Override - protected StructureMappingcommandSet[] getColourBySequenceCommands( - String[] files, SequenceRenderer sr, AlignmentViewPanel avp) + protected String getModelIdForFile(String chainId) { - // TODO Auto-generated method stub - return null; + return ""; } - + @Override - public List getChainNames() + protected ViewerType getViewerType() { - // TODO Auto-generated method stub return null; } - - @Override - protected void colourBySequence( - StructureMappingcommandSet[] colourBySequenceCommands) - { - // TODO Auto-generated method stub - - } - - @Override - public void colourByCharge() - { - // TODO Auto-generated method stub - - } - - @Override - public void colourByChain() - { - // TODO Auto-generated method stub - - } }; String[][] chains = binder.getChains(); assertFalse(chains == null || chains[0] == null, "No chains discovered by binding"); - assertEquals(2, chains[0].length); - assertEquals("A", chains[0][0]); - assertEquals("B", chains[0][1]); + assertEquals(chains[0].length, 2); + assertEquals(chains[0][0], "A"); + assertEquals(chains[0][1], "B"); } AAStructureBindingModel testee; @@ -281,12 +234,33 @@ public class AAStructureBindingModelTest ssm.setMapping(new SequenceI[] { seq3 }, null, PDB_3, DataSourceType.PASTE, null); - testee = new AAStructureBindingModel(ssm, pdbFiles, seqs, null) + testee = newBindingModel(pdbFiles, seqs, ssm, null); + } + + /** + * A helper method to construct the test target object + * + * @param pdbFiles + * @param seqs + * @param ssm + * @param alignPanel + */ + protected AAStructureBindingModel newBindingModel(PDBEntry[] pdbFiles, + SequenceI[][] seqs, + StructureSelectionManager ssm, AlignmentViewPanel avp) + { + AAStructureBindingModel model = new AAStructureBindingModel(ssm, + pdbFiles, seqs, null) { @Override public String[] getStructureFiles() { - return new String[] { "INLINE1YCS", "INLINE3A6S", "INLINE1OOT" }; + String[] files = new String[getPdbCount()]; + for (int i = 0; i < this.getPdbCount(); i++) + { + files[i] = getPdbEntry(i).getFile(); + } + return files; } @Override @@ -305,65 +279,46 @@ public class AAStructureBindingModelTest } @Override - public List getChainNames() - { - return null; - } - - @Override - public void setJalviewColourScheme(ColourSchemeI cs) - { - } - - @Override - public String superposeStructures(AlignmentI[] als, int[] alm, - HiddenColumns[] alc) - { - return null; - } - - @Override - public void setBackgroundColour(Color col) - { - } - - @Override - protected StructureMappingcommandSet[] getColourBySequenceCommands( - String[] files, SequenceRenderer sr, AlignmentViewPanel avp) - { - return null; - } - - @Override public SequenceRenderer getSequenceRenderer( - AlignmentViewPanel alignment) + AlignmentViewPanel avp) { - return null; + return avp == null ? null + : new jalview.gui.SequenceRenderer( + avp.getAlignViewport()); } @Override - protected void colourBySequence( - StructureMappingcommandSet[] colourBySequenceCommands) - { - } - - @Override - public void colourByChain() + protected List executeCommand(StructureCommandI command, + boolean getReply) { + return null; } + /* + * for this test, let structure model ids be 0, 1, ... + * corresponding to first, second etc pdbfile + */ @Override - public void colourByCharge() + protected String getModelIdForFile(String pdbfile) { + for (int i = 0; i < this.getPdbCount(); i++) + { + if (pdbfile.equals(this.getPdbEntry(i).getFile())) + { + return String.valueOf(i); + } + } + return ""; } @Override - public FeatureRenderer getFeatureRenderer( - AlignmentViewPanel alignment) + protected ViewerType getViewerType() { return null; } }; + PA.setValue(model, "commandGenerator", new ChimeraCommands()); + return model; } /** @@ -376,10 +331,10 @@ public class AAStructureBindingModelTest /* * create a data bean to hold data per structure file */ - SuperposeData[] structs = new SuperposeData[testee.getStructureFiles().length]; + AAStructureBindingModel.SuperposeData[] structs = new AAStructureBindingModel.SuperposeData[testee.getStructureFiles().length]; for (int i = 0; i < structs.length; i++) { - structs[i] = testee.new SuperposeData(al.getWidth()); + structs[i] = new AAStructureBindingModel.SuperposeData(al.getWidth(), "0"); } /* * initialise BitSet of 'superposable columns' to true (would be false for @@ -394,7 +349,7 @@ public class AAStructureBindingModelTest int refStructure = testee .findSuperposableResidues(al, matched, structs); - assertEquals(0, refStructure); + assertEquals(refStructure, 0); /* * only ungapped, structure-mapped columns are superposable @@ -406,27 +361,28 @@ public class AAStructureBindingModelTest assertTrue(matched.get(4)); assertTrue(matched.get(5)); // gap in second sequence - assertEquals("1YCS", structs[0].pdbId); - assertEquals("3A6S", structs[1].pdbId); - assertEquals("1OOT", structs[2].pdbId); - assertEquals("A", structs[0].chain); // ? struct has chains A _and_ B - assertEquals("B", structs[1].chain); - assertEquals("A", structs[2].chain); + assertEquals(structs[0].pdbId, "1YCS"); + assertEquals(structs[1].pdbId, "3A6S"); + assertEquals(structs[2].pdbId, "1OOT"); + assertEquals(structs[0].chain, "A"); // ? struct has chains A _and_ B + assertEquals(structs[1].chain, "B"); + assertEquals(structs[2].chain, "A"); // the 0's for unsuperposable positions propagate down the columns: - assertEquals("[0, 97, 98, 99, 100, 102]", - Arrays.toString(structs[0].pdbResNo)); - assertEquals("[0, 2, 0, 3, 4, 5]", Arrays.toString(structs[1].pdbResNo)); - assertEquals("[0, 8, 0, 0, 10, 12]", - Arrays.toString(structs[2].pdbResNo)); + assertEquals(Arrays.toString(structs[0].pdbResNo), + "[0, 97, 98, 99, 100, 102]"); + assertEquals(Arrays.toString(structs[1].pdbResNo), + "[0, 2, 0, 3, 4, 5]"); + assertEquals(Arrays.toString(structs[2].pdbResNo), + "[0, 8, 0, 0, 10, 12]"); } @Test(groups = { "Functional" }) public void testFindSuperposableResidues_hiddenColumn() { - SuperposeData[] structs = new SuperposeData[al.getHeight()]; + AAStructureBindingModel.SuperposeData[] structs = new AAStructureBindingModel.SuperposeData[al.getHeight()]; for (int i = 0; i < structs.length; i++) { - structs[i] = testee.new SuperposeData(al.getWidth()); + structs[i] = new AAStructureBindingModel.SuperposeData(al.getWidth(), "0"); } /* * initialise BitSet of 'superposable columns' to true (would be false for @@ -444,7 +400,7 @@ public class AAStructureBindingModelTest int refStructure = testee .findSuperposableResidues(al, matched, structs); - assertEquals(0, refStructure); + assertEquals(refStructure, 0); // only ungapped, structure-mapped columns are not superposable assertFalse(matched.get(0)); @@ -454,4 +410,100 @@ public class AAStructureBindingModelTest assertFalse(matched.get(4)); // superposable, but hidden, column assertTrue(matched.get(5)); } -} + + @Test(groups = { "Functional" }) + public void testBuildColoursMap() + { + /* + * load these sequences, coloured by Strand propensity, + * with columns 2-4 hidden + */ + String fasta = ">seq1\nMHRSQSSSGG\n>seq2\nMVRSNGGSSS"; + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(fasta, + DataSourceType.PASTE); + AlignmentI al = af.getViewport().getAlignment(); + af.changeColour_actionPerformed(JalviewColourScheme.Strand.toString()); + ColumnSelection cs = new ColumnSelection(); + cs.addElement(2); + cs.addElement(3); + cs.addElement(4); + af.getViewport().setColumnSelection(cs); + af.hideSelColumns_actionPerformed(null); + SequenceI seq1 = al.getSequenceAt(0); + SequenceI seq2 = al.getSequenceAt(1); + SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } }; + PDBEntry[] pdbFiles = new PDBEntry[2]; + pdbFiles[0] = new PDBEntry("PDB1", "A", Type.PDB, "seq1.pdb"); + pdbFiles[1] = new PDBEntry("PDB2", "B", Type.PDB, "seq2.pdb"); + StructureSelectionManager ssm = new StructureSelectionManager(); + + /* + * map residues 1-10 to residues 21-30 (atoms 105-150) in structures + */ + HashMap map = new HashMap<>(); + for (int pos = 1; pos <= seq1.getLength(); pos++) + { + map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) }); + } + StructureMapping sm1 = new StructureMapping(seq1, "seq1.pdb", "pdb1", + "A", map, null); + ssm.addStructureMapping(sm1); + StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2", + "B", map, null); + ssm.addStructureMapping(sm2); + + AAStructureBindingModel binding = newBindingModel(pdbFiles, seqs, ssm, + af.alignPanel); + + /* + * method under test builds a map of structures residues by colour + * verify the map holds what it should + */ + Map colours = binding.buildColoursMap(ssm, seqs, + af.alignPanel); + ChimeraCommands helper = new ChimeraCommands(); + + /* + * M colour is #82827d (see strand.html help page) + * sequence residue 1 mapped to structure residue 21 + */ + Color mColor = new Color(0x82827d); + AtomSpecModel atomSpec = colours.get(mColor); + assertNotNull(atomSpec); + assertEquals(helper.getAtomSpec(atomSpec, false), "#0:21.A|#1:21.B"); + + /* + * H colour is #60609f, seq1.2 mapped to structure 0 residue 22 + */ + Color hColor = new Color(0x60609f); + atomSpec = colours.get(hColor); + assertNotNull(atomSpec); + assertEquals(helper.getAtomSpec(atomSpec, false), "#0:22.A"); + + /* + * V colour is #ffff00, seq2.2 mapped to structure 1 residue 22 + */ + Color vColor = new Color(0xffff00); + atomSpec = colours.get(vColor); + assertNotNull(atomSpec); + assertEquals(helper.getAtomSpec(atomSpec, false), "#1:22.B"); + + /* + * hidden columns are Gray (128, 128, 128) + * sequence positions 3-5 mapped to structure residues 23-25 + */ + Color gray = new Color(128, 128, 128); + atomSpec = colours.get(gray); + assertNotNull(atomSpec); + assertEquals(helper.getAtomSpec(atomSpec, false), "#0:23-25.A|#1:23-25.B"); + + /* + * S and G are both coloured #4949b6, structure residues 26-30 + */ + Color sgColour = new Color(0x4949b6); + atomSpec = colours.get(sgColour); + assertNotNull(atomSpec); + assertEquals(helper.getAtomSpec(atomSpec, false), + "#0:26-30.A|#1:26-30.B"); + } +} \ No newline at end of file