Merge branch 'feature/JAL-3551Pymol' into develop
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 29 May 2020 15:33:17 +0000 (16:33 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 29 May 2020 15:33:17 +0000 (16:33 +0100)
57 files changed:
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java
src/jalview/api/structures/JalviewStructureDisplayI.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AppletJmol.java
src/jalview/appletgui/AppletJmolBinding.java
src/jalview/appletgui/ExtJmol.java
src/jalview/appletgui/UserDefinedColours.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/pymol/PymolCommands.java [new file with mode: 0644]
src/jalview/ext/pymol/PymolManager.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/AtomSpecModel.java [deleted file]
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/ChimeraListener.java
src/jalview/ext/rbvi/chimera/ChimeraXCommands.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AppJmol.java
src/jalview/gui/AppJmolBinding.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/ChimeraXViewFrame.java [new file with mode: 0644]
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/JalviewChimeraXBindingModel.java [new file with mode: 0644]
src/jalview/gui/Preferences.java
src/jalview/gui/PymolBindingModel.java [new file with mode: 0644]
src/jalview/gui/PymolViewer.java [new file with mode: 0644]
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/ViewSelectionMenu.java
src/jalview/javascript/MouseOverStructureListener.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/project/Jalview2XML.java
src/jalview/structure/AtomSpec.java
src/jalview/structure/AtomSpecModel.java [new file with mode: 0644]
src/jalview/structure/StructureCommand.java [new file with mode: 0644]
src/jalview/structure/StructureCommandI.java [new file with mode: 0644]
src/jalview/structure/StructureCommandsBase.java [new file with mode: 0644]
src/jalview/structure/StructureCommandsI.java [new file with mode: 0644]
src/jalview/structure/StructureSelectionManager.java
src/jalview/structures/models/AAStructureBindingModel.java
src/jalview/ws/HttpClientUtils.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/pymol/PymolCommandsTest.java [new file with mode: 0644]
test/jalview/ext/pymol/PymolManagerTest.java [new file with mode: 0644]
test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java [deleted file]
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraConnect.java
test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java [new file with mode: 0644]
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/structure/AtomSpecModelTest.java [new file with mode: 0644]
test/jalview/structure/AtomSpecTest.java
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java

index 1762a06..ca6ae63 100644 (file)
@@ -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.<br>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.<br/>Please enter the path to Chimera (if installed),<br/>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.<br>Double-click to browse for file.
+label.invalid_viewer_path = Path not found or not executable
+label.viewer_missing = Structure viewer not found.<br/>Please enter the path to the executable (if installed),<br/>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}.<br>Do you want to close the Chimera window as well?
+label.confirm_close_viewer = This will close Jalview''s connection to {0}.<br>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}
index d3c3355..624e619 100644 (file)
@@ -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}.<br>¿Quieres cerrar la ventana Chimera también?
+label.confirm_close_viewer=Cerrará la conexión de Jalview a {0}.<br>¿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.<br/>Por favor, introduzca la ruta de la ejecutable,<br/>o descargar e instalar el programa.
 warn.delete_all=<html>Borrar todas las secuencias cerrará la ventana del alineamiento.<br>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.<br/>Por favor, introduzca la ruta de Chimera,<br/>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
index a910a5a..31f5dc8 100644 (file)
@@ -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<String> getSelectedResidueSpecs()
   {
     List<String> selectedResidues = new ArrayList<>();
-    List<String> chimeraReply = sendChimeraCommand(
-            "list selection level residue", true);
+
+    // in fact 'listinfo' (undocumented) works in ChimeraX
+    String command = (isChimeraX
+            ? "info"
+            : "list") + " selection level residue";
+    List<String> 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<ChimeraModel> getModelList()
   {
     List<ChimeraModel> modelList = new ArrayList<>();
-    List<String> list = sendChimeraCommand("list models type molecule",
-            true);
+    String command = "list models type "
+            + (isChimeraX ? "AtomicStructure" : "molecule");
+    List<String> 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<String> getAttrList()
   {
     List<String> attributes = new ArrayList<>();
-    final List<String> reply = sendChimeraCommand("list resattr", true);
+    String command = (isChimeraX ? "info " : "list ") + "resattr";
+    final List<String> 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<String> 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<NameValuePair> 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<String> 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;
+  }
 }
index 22c9098..5cf8a73 100644 (file)
@@ -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<String> getChimeraPaths()
+  public static List<String> getChimeraPaths(boolean isChimeraX)
   {
     List<String> 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;
   }
index 8f778f7..d8c8371 100644 (file)
@@ -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);
+
 }
index 1a46585..0fd0945 100644 (file)
@@ -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
index 7fda3c4..6665ec8 100644 (file)
  */
 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)
index f1c494e..c7ce994 100644 (file)
@@ -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;
   }
 
index 5a6d0d0..47f9df0 100644 (file)
@@ -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<String, Object> getJSpecViewProperty(String arg0)
   {
     return null;
   }
-
 }
index febe5f8..83d6fd6 100644 (file)
@@ -524,7 +524,7 @@ public class UserDefinedColours extends Panel
     }
     else if (jmol != null)
     {
-      jmol.setJalviewColourScheme(ucs);
+      jmol.colourByJalviewColourScheme(ucs);
     }
     else if (pdbcanvas != null)
     {
index 453152e..eee48df 100644 (file)
  */
 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<String> atomsPicked = new Vector<>();
-
-  private List<String> chainNames;
-
-  Hashtable<String, String> 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<String> 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<String> 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("<<Jmol<< " + s);
 
-  /**
-   * 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)
-   * @param hiddenCols
-   *          TODO
-   */
-  public void superposeStructures(AlignmentI alignment, int refStructure,
-          HiddenColumns hiddenCols)
-  {
-    superposeStructures(new AlignmentI[] { alignment },
-            new int[]
-            { refStructure }, new HiddenColumns[] { hiddenCols });
+    return s;
   }
 
-  /**
-   * {@inheritDoc}
-   */
   @Override
-  public String superposeStructures(AlignmentI[] _alignment,
-          int[] _refStructure, HiddenColumns[] _hiddenCols)
+  public List<String> 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<String> 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<String> 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<String> 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<String> 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("<<Jmol<< " + s);
-
-    return s;
-  }
-
-  @Override
   public int[] resizeInnerPanel(String data)
   {
     // Jalview doesn't honour resize panel requests
@@ -1477,4 +965,63 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     showConsole(false);
   }
 
+  @Override
+  protected String getModelIdForFile(String pdbFile)
+  {
+    if (modelFileNames == null)
+    {
+      return "";
+    }
+    for (int i = 0; i < modelFileNames.length; i++)
+    {
+      if (modelFileNames[i].equalsIgnoreCase(pdbFile))
+      {
+        return String.valueOf(i + 1);
+      }
+    }
+    return "";
+  }
+
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.JMOL;
+  }
+
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum + 1);
+  }
+
+  /**
+   * Returns ".spt" - the Jmol session file extension
+   * 
+   * @return
+   * @see https://chemapps.stolaf.edu/jmol/docs/#writemodel
+   */
+  @Override
+  public String getSessionFileExtension()
+  {
+    return ".spt";
+  }
+
+  @Override
+  public void selectionChanged(BS arg0)
+  {
+    // TODO Auto-generated method stub
+
+  }
+
+  @Override
+  public SequenceRenderer getSequenceRenderer(AlignmentViewPanel avp)
+  {
+    return new jalview.gui.SequenceRenderer(avp.getAlignViewport());
+  }
+
+  @Override
+  public String getHelpURL()
+  {
+    return "http://wiki.jmol.org"; // BH 2018
+  }
 }
index 603202a..085fbd5 100644 (file)
  */
 package jalview.ext.jmol;
 
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
@@ -28,49 +34,299 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsBase;
 import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
-
-import java.awt.Color;
-import java.util.ArrayList;
-import java.util.List;
+import jalview.util.Comparison;
+import jalview.util.Platform;
 
 /**
- * Routines for generating Jmol commands for Jalview/Jmol binding another
- * cruisecontrol test.
+ * Routines for generating Jmol commands for Jalview/Jmol binding
  * 
  * @author JimP
  * 
  */
-public class JmolCommands
+public class JmolCommands extends StructureCommandsBase
 {
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "select *; cartoons off; backbone");
+
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand("zoom 0");
+
+  private static final StructureCommand COLOUR_ALL_WHITE = new StructureCommand(
+          "select *;color white;");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "select *;color white;select ASP,GLU;color red;"
+                  + "select LYS,ARG;color blue;select CYS;color yellow");
+
+  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
+          "select *;color chain");
+
+  private static final String PIPE = "|";
+
+  private static final String HYPHEN = "-";
+
+  private static final String COLON = ":";
+
+  private static final String SLASH = "/";
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 1;
+  }
+
+  /**
+   * Returns a string representation of the given colour suitable for inclusion
+   * in Jmol commands
+   * 
+   * @param c
+   * @return
+   */
+  protected String getColourString(Color c)
+  {
+    return c == null ? null
+            : String.format("[%d,%d,%d]", c.getRed(), c.getGreen(),
+                    c.getBlue());
+  }
+
+  @Override
+  public StructureCommandI colourByChain()
+  {
+    return COLOUR_BY_CHAIN;
+  }
+
+  @Override
+  public List<StructureCommandI> colourByCharge()
+  {
+    return Arrays.asList(COLOUR_BY_CHARGE);
+  }
+
+  @Override
+  public List<StructureCommandI> colourByResidues(Map<String, Color> colours)
+  {
+    List<StructureCommandI> 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<StructureCommandI> showChains(List<String> 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
+   * 
+   * <pre>
+   * 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;
+   * </pre>
+   * 
+   * 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.
+   * <p>
+   * 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<StructureCommandI> 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
+   * 
+   * <pre>
+   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
+   * </pre>
    * 
-   * @returns Object[] { Object[] { <model being coloured>,
+   * 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<StructureCommandI> 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<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
+    List<String> cset = new ArrayList<>();
 
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
-      StringBuilder command = new StringBuilder();
-      StructureMappingcommandSet smc;
-      ArrayList<String> str = new ArrayList<String>();
+      StringBuilder command = new StringBuilder(128);
+      List<String> 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 (file)
index 0000000..3493d03
--- /dev/null
@@ -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.
+ * <p>
+ * 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<StructureCommandI> COLOR_BY_CHARGE = new ArrayList<>();
+
+  private static final List<StructureCommandI> 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<StructureCommandI> 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<StructureCommandI> showChains(List<String> toShow)
+  {
+    // https://pymolwiki.org/index.php/Show
+    List<StructureCommandI> 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<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec)
+  {
+    // https://pymolwiki.org/index.php/Super
+    List<StructureCommandI> 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:
+   * 
+   * <pre>
+   * modelId// chain/residues/
+   * </pre>
+   * 
+   * 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<int[]> 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<StructureCommandI> 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<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap)
+  {
+    List<StructureCommandI> 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
+   * 
+   * <pre>
+   * iterate 4zho//B/12-34,48-55/CA,jv_chain='primary'
+   * </pre>
+   * 
+   * @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.
+   * <p>
+   * The format of each command is
+   * 
+   * <pre>
+   * <blockquote> iterate atomspec, p.featureName='value' 
+   * e.g. iterate 4zho//A/23,28-29/CA, p.jv_Metal='Fe'
+   * </blockquote>
+   * </pre>
+   * 
+   * @param featureMap
+   * @return
+   */
+  @Override
+  public List<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> featureMap)
+  {
+    List<StructureCommandI> 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<Object, AtomSpecModel> 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("\\'", "&#39;");
+        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 (file)
index 0000000..e3b913b
--- /dev/null
@@ -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 = "<methodCall><methodName>";
+
+  private static final String POST2 = "</methodName><params>";
+
+  private static final String POST3 = "</params></methodCall>";
+
+  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<String> 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<String> getPymolPaths(String os)
+  {
+    List<String> 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<String> 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<String> 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 - <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("<parameter><value>").append(p)
+                .append("</value></parameter>");
+      }
+    }
+    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<String> 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 (file)
index 39d6704..0000000
+++ /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 <http://www.gnu.org/licenses/>.
- * 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
- * 
- * <pre>
- * #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
- * </pre>
- * 
- * where
- * <ul>
- * <li>#0 is a model number</li>
- * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
- * <li>.A is a chain identifier</li>
- * <li>residue ranges are separated by comma</li>
- * <li>atomspecs for distinct models are separated by | (or)</li>
- * </ul>
- * 
- * <pre>
- * &#64;see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
- * </pre>
- */
-public class AtomSpecModel
-{
-  private Map<Integer, Map<String, List<int[]>>> atomSpec;
-
-  /**
-   * Constructor
-   */
-  public AtomSpecModel()
-  {
-    atomSpec = new TreeMap<Integer, Map<String, List<int[]>>>();
-  }
-
-  /**
-   * 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<String, List<int[]>> modelData = atomSpec.get(model);
-    if (modelData == null)
-    {
-      atomSpec.put(model, modelData = new TreeMap<String, List<int[]>>());
-    }
-
-    /*
-     * Get/initialize map of data for colour, model and chain
-     */
-    List<int[]> chainData = modelData.get(chain);
-    if (chainData == null)
-    {
-      chainData = new ArrayList<int[]>();
-      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<String, List<int[]>> modelData = atomSpec.get(model);
-
-      for (String chain : modelData.keySet())
-      {
-        chain = " ".equals(chain) ? chain : chain.trim();
-
-        List<int[]> 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<int[]> 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);
-    }
-  }
-}
index 3caaac3..5beee56 100644 (file)
  */
 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<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
-            sequence, sr, viewPanel);
-
-    List<String> 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).
+   * <p>
+   * The format of each command is
    * 
    * <pre>
-   * <blockquote> 
-   * color colorname #modelnumber:range.chain 
-   * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
+   * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
+   * e.g. setattr r jv_chain &lt;value&gt; #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
    * </blockquote>
    * </pre>
    * 
-   * @param colourMap
+   * @param featureMap
    * @return
    */
-  protected static List<String> buildColourCommands(
-          Map<Object, AtomSpecModel> colourMap)
+  @Override
+  public List<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> 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<String> commands = new ArrayList<>();
-    StringBuilder sb = new StringBuilder(256);
-    boolean firstColour = true;
-    for (Object key : colourMap.keySet())
+    List<StructureCommandI> 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<Integer, Map<String, List<int[]>>> 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<String, List<int[]>> modelData = modelAndChainRanges
-              .get(model);
-      for (String chain : modelData.keySet())
+      Map<Object, AtomSpecModel> 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("\\'", "&#39;");
+        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
+   * 
    * <pre>
-   * 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
    * </pre>
+   * 
+   * @param attributeName
+   * @param attributeValue
+   * @param atomSpecModel
+   * @return
    */
-  protected static Map<Object, AtomSpecModel> 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<Object, AtomSpecModel> 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
-   * <ul>
-   * <li>a colour, when building a 'colour structure by sequence' command</li>
-   * <li>a feature value, when building a 'set Chimera attributes from features'
-   * command</li>
-   * </ul>
+   * 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<Object, AtomSpecModel> 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<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
-            ssm, files, seqs, viewPanel);
-
-    List<String> 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<StructureCommandI> colourByCharge()
+  {
+    return Arrays.asList(COLOUR_BY_CHARGE);
   }
 
-  /**
-   * <pre>
-   * Helper method to build a map of 
-   *   { featureType, { feature value, AtomSpecModel } }
-   * </pre>
-   * 
-   * @param ssm
-   * @param files
-   * @param seqs
-   * @param viewPanel
-   * @return
-   */
-  protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
-          StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
-          AlignmentViewPanel viewPanel)
+  @Override
+  public String getResidueSpec(String residue)
   {
-    Map<String, Map<Object, AtomSpecModel>> 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<String> 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<StructureCommandI> showChains(List<String> 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<String> 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<StructureCommandI> 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
+   * <pre>
+   * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
+   * </pre>
+   * 
+   * where
+   * <ul>
+   * <li>#0 is a model number</li>
+   * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
+   * <li>.A is a chain identifier</li>
+   * <li>residue ranges are separated by comma</li>
+   * <li>atomspecs for distinct models are separated by | (or)</li>
+   * </ul>
+   * 
+   * <pre>
+   * 
+   * @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<String, Map<Object, AtomSpecModel>> 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<int[]> 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<Object, AtomSpecModel> 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<String> visibleFeatures,
-          StructureMapping mapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  protected void appendModel(StringBuilder sb, String model,
+          AtomSpecModel atomSpec, boolean alphaOnly)
   {
-    List<SequenceFeature> 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<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
-              sf.getEnd());
+    for (String chain : atomSpec.getChains(model))
+    {
+      chain = " ".equals(chain) ? chain : chain.trim();
 
-      if (!mappedRanges.isEmpty())
+      List<int[]> 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<Object, AtomSpecModel> 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).
-   * <p>
-   * The format of each command is
-   * 
-   * <pre>
-   * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
-   * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
-   * </blockquote>
-   * </pre>
-   * 
-   * @param featureMap
-   * @return
-   */
-  protected static List<String> buildSetAttributeCommands(
-          Map<String, Map<Object, AtomSpecModel>> featureMap)
-  {
-    List<String> 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<Object, AtomSpecModel> 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("\\'", "&#39;");
-        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<StructureCommandI> 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
-   * 
-   *         <pre>
-   * &#64;see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
-   *         </pre>
+   * Overrides the default method to concatenate colour commands into one
    */
-  protected static String makeAttributeName(String featureType)
+  @Override
+  public List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap)
   {
-    StringBuilder sb = new StringBuilder();
-    if (featureType != null)
+    List<StructureCommandI> 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);
   }
 
 }
index a0d74bc..40b0ff0 100644 (file)
@@ -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 (file)
index 0000000..a596da9
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ * 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<StructureCommandI> 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
+   * 
+   * <pre>
+   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
+   * </pre>
+   * 
+   * @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
+   * <p>
+   * #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<int[]> 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<StructureCommandI> showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public List<StructureCommandI> 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");
+  }
+}
index 00446f2..460b156 100644 (file)
  */
 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<String> chainNames = new ArrayList<String>();
-
-  private Hashtable<String, String> chainFile = new Hashtable<String, String>();
+  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<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
+  protected Map<String, List<ChimeraModel>> 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<ChimeraModel> modelsToMap = new ArrayList<ChimeraModel>();
-      List<ChimeraModel> oldList = viewer.getModelList();
+      List<ChimeraModel> modelsToMap = new ArrayList<>();
+      List<ChimeraModel> 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<ChimeraModel> 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<ChimeraModel> modelsToMap)
+  {
+    /*
+     * Chimera: query for actual models and find the one with
+     * matching model name - already set in viewer.openModel()
+     */
+    List<ChimeraModel> 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<String> 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
-   * <ul>
-   * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
-   * <li>Lysine and Arginine (positive charge) blue</li>
-   * <li>Cysteine - yellow</li>
-   * <li>all others - white</li>
-   * </ul>
-   */
-  @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<String> 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<String> 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.
-   * <p>
-   * Does nothing, and returns null, if the command is the same as the last one
-   * sent [why?].
    * 
    * @param command
    * @param getResponse
    */
-  public List<String> sendChimeraCommand(final String command,
+  @Override
+  public List<String> executeCommand(final StructureCommandI command,
           boolean getResponse)
   {
-    if (viewer == null)
+    if (chimeraManager == null || command == null)
     {
       // ? thread running after viewer shut down
       return null;
     }
     List<String> 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<String> lastReply = chimeraManager
+            .sendChimeraCommand(cmd, getResponse);
+    if (getResponse)
     {
-      // trim command or it may never find a match in the replyLog!!
-      List<String> 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<AtomSpec> 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<String> selection = viewer.getSelectedResidueSpecs();
+    List<String> 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<AtomSpec> convertStructureResiduesToAlignment(
           List<String> structureSelection)
   {
-    List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
+    boolean chimeraX = chimeraManager.isChimeraX();
+    List<AtomSpec> 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<String> 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 <color>" 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<String> 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<String> 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.
+   * <p>
+   * The syntax is: setattr r &lt;attName&gt; &lt;attValue&gt; &lt;atomSpec&gt;
+   * <p>
+   * 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<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap(
+            avp);
+    List<StructureCommandI> 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<StructureCommandI> 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<String> residues = sendChimeraCommand(cmd, true);
+    List<String> 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<String, String> getChainFile()
+  @Override
+  public String getModelIdForFile(String pdbFile)
   {
-    return chainFile;
+    List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
+    if (foundModels != null && !foundModels.isEmpty())
+    {
+      return String.valueOf(foundModels.get(0).getModelNumber());
+    }
+    return "";
   }
 
-  public List<ChimeraModel> getChimeraModelByChain(String chain)
+  /**
+   * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
+   * any which were added from Jalview
+   * 
+   * @return
+   */
+  public List<String> getChimeraAttributes()
+  {
+    List<String> atts = chimeraManager.getAttrList();
+    Iterator<String> 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<ChimeraModel> foundModels = getChimeraModelByChain(chain);
-    if (foundModels != null && !foundModels.isEmpty())
-    {
-      return foundModels.get(0).getModelNumber();
-    }
-    return -1;
+    return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide";
   }
 }
index 7818748..ef40261 100644 (file)
@@ -414,7 +414,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
     addKeyListener();
 
-    final List<AlignmentPanel> selviews = new ArrayList<>();
+    final List<AlignmentViewPanel> selviews = new ArrayList<>();
     final List<AlignmentPanel> origview = new ArrayList<>();
     final String menuLabel = MessageManager
             .getString("label.copy_format_from");
index e13df4a..7cf10e7 100644 (file)
  */
 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<File, StructureData> 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<SequenceI> 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<String> 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;
index 75b98bc..98787cb 100644 (file)
  */
 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<File> files)
   {
index c6d6e97..0e5675c 100644 (file)
  */
 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<String> atts = jmb.sendChimeraCommand("list resattr", true);
-    if (atts == null)
-    {
-      return;
-    }
+    List<String> 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
-   * <p>
-   * The syntax is: setattr r <attName> <attValue> <atomSpec>
-   * <p>
-   * 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<File, StructureData> 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<SequenceI> 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<String> 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 (file)
index 0000000..517eb4f
--- /dev/null
@@ -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);
+  }
+}
index 9d63c6a..49655a4 100644 (file)
@@ -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 (file)
index 0000000..c685f0f
--- /dev/null
@@ -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<String> getChimeraPaths()
+  {
+    return StructureManager.getChimeraPaths(true);
+  }
+
+  @Override
+  protected void addChimeraModel(PDBEntry pe,
+          List<ChimeraModel> 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);
+  }
+
+}
index 04b83a3..c61c70e 100755 (executable)
  */
 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<String> paths = StructureManager.getChimeraPaths();
-    paths.add(0, chimeraPath.getText());
+    String viewerPath = "";
+    List<String> 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 (file)
index 0000000..264a49c
--- /dev/null
@@ -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<String> structureFiles = new ArrayList<>();
+
+  /*
+   * lookup from file path to PyMOL object name
+   */
+  Map<String, String> 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<AtomSpec> 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<String> 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<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap(
+            avp);
+    List<StructureCommandI> 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 (file)
index 0000000..c5a4c9a
--- /dev/null
@@ -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<File, StructureData> 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<SequenceI> 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<PDBEntry> filePDB = new ArrayList<>();
+    List<Integer> 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));
+  }
+
+}
index 0c8354b..617706a 100644 (file)
  */
 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()
index 418a84d..33a122c 100644 (file)
  */
 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<AlignmentPanel> _alignwith = new Vector<>();
+  protected Vector<AlignmentViewPanel> _alignwith = new Vector<>();
 
   /**
    * list of alignment panels that are used for colouring structures by aligned
    * sequences
    */
-  protected Vector<AlignmentPanel> _colourwith = new Vector<>();
+  protected Vector<AlignmentViewPanel> _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<String> 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());
+    }
+  }
+
 }
index 2a7743a..a1529fc 100644 (file)
@@ -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<AlignmentPanel> _selectedviews;
+  private List<AlignmentViewPanel> _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<AlignmentPanel> selectedviews,
+          final List<AlignmentViewPanel> selectedviews,
           final ItemListener handler)
   {
     super(title);
index 6071933..8d83e75 100644 (file)
@@ -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<String[]> ccomands = new ArrayList<String[]>();
-      ArrayList<String> pdbfn = new ArrayList<String>();
-      StructureMappingcommandSet[] colcommands = JmolCommands
-              .getColourBySequenceCommand(ssm, modelSet, sequence, sr,
+      ArrayList<String[]> ccomands = new ArrayList<>();
+      ArrayList<String> 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;
index 6de3888..ae6727a 100755 (executable)
  */
 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<String> 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);
     }
index dfee3e2..73180ee 100644 (file)
  */
 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()
   {
 
   }
index 6340e64..ccd9ab0 100644 (file)
@@ -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<String> 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<String, StructureViewerModel> 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<Entry<File, StructureData>> fileData = data.getFileData()
-            .entrySet();
-    List<PDBEntry> pdbs = new ArrayList<>();
-    List<SequenceI[]> allseqs = new ArrayList<>();
-    for (Entry<File, StructureData> 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<SequenceI> 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<String, StructureViewerModel> 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<String> pdbfilenames = new ArrayList<>();
-    List<SequenceI[]> seqmaps = new ArrayList<>();
-    List<String> pdbids = new ArrayList<>();
-    StringBuilder newFileLoc = new StringBuilder(64);
-    int cp = 0, ncp, ecp;
-    Map<File, StructureData> 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<SequenceI> 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<SequenceI> 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<String, StructureViewerModel> 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<File, StructureData> 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
index f20cd31..8b8161f 100644 (file)
@@ -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)
    * 
+   * <pre>
+   * Chimera format: 
+   *    #1.2:12-20.A     model 1, submodel 2, chain A, atoms 12-20
+   * ChimeraX format:
+   *    #1.2/A:12-20
+   * </pre>
+   * 
    * @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 (file)
index 0000000..1ef653e
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ * 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<from-to> ranges} }
+   */
+  private Map<String, Map<String, BitSet>> 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<String, BitSet> 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<String> getModels()
+  {
+    return atomSpec.keySet();
+  }
+
+  public int getModelCount()
+  {
+    return atomSpec.size();
+  }
+
+  public Iterable<String> 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<int[]> getRanges(String model, String chain)
+  {
+    List<int[]> 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 (file)
index 0000000..f7875ab
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..e39bbba
--- /dev/null
@@ -0,0 +1,14 @@
+package jalview.structure;
+
+import java.util.List;
+
+public interface StructureCommandI
+{
+  String getCommand();
+
+  List<String> 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 (file)
index 0000000..3c29fd4
--- /dev/null
@@ -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
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
+   * 
+   * @param map
+   * @param value
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public static final void addAtomSpecRange(Map<Object, AtomSpecModel> 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.
+   * <p>
+   * The default implementation returns a single command containing one command
+   * per colour, concatenated.
+   * 
+   * @param colourMap
+   * @return
+   */
+  @Override
+  public List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap)
+  {
+    List<StructureCommandI> 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<StructureCommandI> colourByResidues(
+          Map<String, Color> colours)
+  {
+    List<StructureCommandI> commands = new ArrayList<>();
+    for (Entry<String, Color> 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<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> 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 (file)
index 0000000..5a0db0a
--- /dev/null
@@ -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:
+   * <ul>
+   * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
+   * <li>Lysine and Arginine (positive charge) blue</li>
+   * <li>Cysteine - yellow</li>
+   * <li>all others - white</li>
+   * </ul>
+   * 
+   * @return
+   */
+  List<StructureCommandI> colourByCharge();
+
+  /**
+   * Returns the command to colour residues with the colours provided in the
+   * map, one per three letter residue code
+   * 
+   * @param colours
+   * @return
+   */
+  List<StructureCommandI> colourByResidues(Map<String, Color> 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<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> 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<StructureCommandI> showChains(List<String> 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<StructureCommandI> 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<StructureCommandI> 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<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> featureValues);
+
+  /**
+   * Returns command to open a saved structure viewer session file, or null if
+   * not supported
+   * 
+   * @param filepath
+   * @return
+   */
+  StructureCommandI openSession(String filepath);
+}
index 8c3816e..53644e9 100644 (file)
@@ -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);
index 2528286..5949847 100644 (file)
  */
 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<String> chainNames;
+
+  /*
+   * lookup of pdb file name by key "pdbid:chainCode"
+   */
+  private Map<String, String> 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<SequenceI> s = new ArrayList<SequenceI>();
-    List<String> c = new ArrayList<String>();
+    List<SequenceI> s = new ArrayList<>();
+    List<String> 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<PDBEntry> v = new ArrayList<PDBEntry>();
-    List<int[]> rtn = new ArrayList<int[]>();
+    List<PDBEntry> v = new ArrayList<>();
+    List<int[]> 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<String> getChainNames();
+  public List<String> 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<AlignmentViewPanel> 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<StructureCommandI> commands = commandGenerator
+                  .superposeStructures(refAtoms, atomSpec);
+          List<String> 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<String, Color> colours = new HashMap<>();
+    List<String> 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<StructureCommandI> 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.
+   * <p>
+   * If a progress message is supplied, this is displayed before command
+   * execution, and removed afterwards.
+   * 
+   * @param cmd
+   * @param getReply
+   * @param msg
+   * @return
+   */
+  private List<String> 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<String> 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<String> executeCommands(
+          List<StructureCommandI> 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<String> 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<String> response = getReply ? new ArrayList<>() : null;
+    try
+    {
+      for (StructureCommandI cmd : commands)
+      {
+        List<String> 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<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
+            alignmentv);
+
+    List<StructureCommandI> 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<String> 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<String> 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
+   * 
+   * <pre>
+   *   Model ids
+   *     Chains
+   *       Residue positions
+   * </pre>
+   * 
+   * Ordering is by order of addition (for colours), natural ordering (for
+   * models and chains)
+   * 
+   * @param ssm
+   * @param sequence
+   * @param viewPanel
+   * @return
+   */
+  protected Map<Object, AtomSpecModel> 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<Object, AtomSpecModel> 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
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
+   * 
+   * @param map
+   * @param value
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public static final void addAtomSpecRange(Map<Object, AtomSpecModel> 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;
+  }
+
+  /**
+   * <pre>
+   * Helper method to build a map of 
+   *   { featureType, { feature value, AtomSpecModel } }
+   * </pre>
+   * 
+   * @param viewPanel
+   * @return
+   */
+  protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
+          AlignmentViewPanel viewPanel)
+  {
+    Map<String, Map<Object, AtomSpecModel>> 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<String> 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<String> 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<String, Map<Object, AtomSpecModel>> 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<int[]> 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<Object, AtomSpecModel> 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<String> visibleFeatures,
+          StructureMapping mapping, SequenceI seq,
+          Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
+  {
+    List<SequenceFeature> 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<int[]> 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<Object, AtomSpecModel> 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;
+  }
 }
index b19d606..8f97226 100644 (file)
@@ -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<NameValuePair> 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;
+    }
+  }
 }
index e42b54f..1ade00e 100644 (file)
@@ -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<Integer, int[]> map = new HashMap<Integer, int[]>();
+    HashMap<Integer, int[]> 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<Object, AtomSpecModel> 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<StructureCommandI> 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<StructureCommandI> 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 (file)
index 0000000..38031b6
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ * 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<Object, AtomSpecModel> 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<StructureCommandI> 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<StructureCommandI> 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<StructureCommandI> 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<StructureCommandI> 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 (file)
index 0000000..c415ace
--- /dev/null
@@ -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,
+            "<methodCall><methodName>foobar</methodName><params></params></methodCall>");
+
+    req = PymolManager
+            .getPostRequest(new StructureCommand("foobar", "blue", "all"));
+    assertEquals(req, "<methodCall><methodName>foobar</methodName><params>"
+            + "<parameter><value>blue</value></parameter>"
+            + "<parameter><value>all</value></parameter>"
+            + "</params></methodCall>");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPymolPaths()
+  {
+    /*
+     * OSX
+     */
+    List<String> 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 (file)
index 63d5e4e..0000000
+++ /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.");
-
-  }
-
-}
index 06a09df..5fc9fdc 100644 (file)
@@ -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<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
-    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<Object, AtomSpecModel> 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<StructureCommandI> 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<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
-    Map<Object, AtomSpecModel> featureValues = new HashMap<Object, AtomSpecModel>();
+    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+    Map<Object, AtomSpecModel> 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<String> commands = ChimeraCommands
-            .buildSetAttributeCommands(featuresMap);
+    ChimeraCommands commandGenerator = new ChimeraCommands();
+    List<StructureCommandI> 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,
-            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
+            "<html>metal <a href=\"http:a.b.c/x\"> '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_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' #0:7-15.A"));
+    commands = commandGenerator.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    String expected3 = "setattr res jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;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<StructureCommandI> 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<Integer, int[]> map = new HashMap<Integer, int[]>();
-    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<StructureCommandI> 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");
   }
 }
index 99394dc..b07a622 100644 (file)
@@ -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 (file)
index 0000000..210c61d
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ * 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<StructureCommandI> 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<Object, AtomSpecModel> 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<StructureCommandI> 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<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+    Map<Object, AtomSpecModel> 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<StructureCommandI> 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,
+            "<html>metal <a href=\"http:a.b.c/x\"> '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_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;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<StructureCommandI> 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<StructureCommandI> 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");
+  }
+}
index 734f7eb..d397a6b 100644 (file)
@@ -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<String> reply = binding.sendChimeraCommand("list resattr", true);
+    List<String> 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<SequenceFeature> 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<SequenceFeature> 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<SequenceFeature> fs = seq.getFeatures().findFeatures(res, res);
+    List<SequenceFeature> 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 (file)
index 0000000..394679f
--- /dev/null
@@ -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<int[]> 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);
+  }
+}
index ea53131..ff6e6cb 100644 (file)
@@ -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)
     {
index e59648f..9b240d3 100644 (file)
@@ -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<AlignedCodonFrame> set1 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> set1 = new ArrayList<>();
     set1.add(acf1);
     set1.add(acf2);
-    List<AlignedCodonFrame> set2 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> 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<int[]> structuremap1 = new ArrayList(
+    List<int[]> 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);
     }
   }
 
index af02d5e..c1ad03a 100644 (file)
  */
 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<AtomSpec> 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<String> 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<String> 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<String> 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<String> 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<Integer, int[]> 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<Object, AtomSpecModel> 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