Merge branch 'features/JAL-1596ChimeraREST' into develop
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Sun, 28 Dec 2014 10:34:27 +0000 (10:34 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Sun, 28 Dec 2014 10:34:27 +0000 (10:34 +0000)
13 files changed:
.classpath
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java
src/jalview/api/SequenceRenderer.java
src/jalview/appletgui/SequenceRenderer.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/gui/SequenceRenderer.java
src/jalview/schemes/Blosum62ColourScheme.java
src/jalview/util/ColorUtils.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java [new file with mode: 0644]
test/jalview/gui/SequenceRendererTest.java [new file with mode: 0644]
test/jalview/util/ColorUtilsTest.java

index b75a654..4c39421 100644 (file)
@@ -55,6 +55,5 @@
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
        <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Plugin.jar"/>
        <classpathentry kind="lib" path="lib/jfreesvg-2.1.jar"/>
-       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.launching.macosx.MacOSXType/Java SE 6 [1.6.0_65-b14-462]"/>
        <classpathentry kind="output" path="classes"/>
 </classpath>
index a7440e7..b45404e 100644 (file)
@@ -1,14 +1,21 @@
 package ext.edu.ucsf.rbvi.strucviz2;
 
+import jalview.ws.HttpClientUtils;
+
 import java.awt.Color;
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.http.NameValuePair;
+import org.apache.http.message.BasicNameValuePair;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -20,10 +27,19 @@ import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
  */
 public class ChimeraManager
 {
+  private static final boolean debug = false;
+
+  /*
+   * true: use REST API (recommended), false: use stdout/stdin (deprecated)
+   */
+  // TODO remove once definitely happy with using REST
+  private static final boolean USE_REST = true;
+
+  private int chimeraRestPort;
 
   private Process chimera;
 
-  private ListenerThreads chimeraListenerThreads;
+  private ListenerThreads chimeraListenerThread;
 
   private Map<Integer, ChimeraModel> currentModelsMap;
 
@@ -36,7 +52,7 @@ public class ChimeraManager
   {
     this.structureManager = structureManager;
     chimera = null;
-    chimeraListenerThreads = null;
+    chimeraListenerThread = null;
     currentModelsMap = new HashMap<Integer, ChimeraModel>();
 
   }
@@ -354,7 +370,15 @@ public class ChimeraManager
   {
     chimera = null;
     currentModelsMap.clear();
-    chimeraListenerThreads = null;
+    if (USE_REST)
+    {
+      this.chimeraRestPort = 0;
+    }
+    else
+    {
+      chimeraListenerThread.requestStop();
+      chimeraListenerThread = null;
+    }
     structureManager.clearOnChimeraExit();
   }
 
@@ -525,12 +549,13 @@ public class ChimeraManager
         List<String> args = new ArrayList<String>();
         args.add(chimeraPath);
         args.add("--start");
-        args.add("ReadStdin");
+        args.add(USE_REST ? "RESTServer" : "ReadStdin");
         ProcessBuilder pb = new ProcessBuilder(args);
         chimera = pb.start();
         error = "";
         workingPath = chimeraPath;
-        logger.info("Strarting " + chimeraPath);
+        logger.info("Starting " + chimeraPath + " with "
+                + (USE_REST ? "REST API" : "stdin/stdout"));
         break;
       } catch (Exception e)
       {
@@ -541,10 +566,18 @@ public class ChimeraManager
     // If no error, then Chimera was launched successfully
     if (error.length() == 0)
     {
-      // Initialize the listener threads
-      chimeraListenerThreads = new ListenerThreads(chimera,
-              structureManager);
-      chimeraListenerThreads.start();
+      if (USE_REST)
+      {
+        this.chimeraRestPort = getPortNumber();
+        System.out.println("Chimera REST API on port " + chimeraRestPort);
+      }
+      else
+      {
+        // Initialize the listener threads
+        chimeraListenerThread = new ListenerThreads(chimera,
+                structureManager);
+        chimeraListenerThread.start();
+      }
       // structureManager.initChimTable();
       structureManager.setChimeraPathProperty(workingPath);
       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
@@ -559,6 +592,42 @@ public class ChimeraManager
   }
 
   /**
+   * Read and return the port number returned in the reply to --start RESTServer
+   */
+  private int getPortNumber()
+  {
+    int port = 0;
+    InputStream readChan = chimera.getInputStream();
+    BufferedReader lineReader = new BufferedReader(new InputStreamReader(
+            readChan));
+    String response = null;
+    try
+    {
+      // expect: REST server on host 127.0.0.1 port port_number
+      response = lineReader.readLine();
+      String [] tokens = response.split(" ");
+      if (tokens.length == 7 && "port".equals(tokens[5])) {
+        port = Integer.parseInt(tokens[6]);
+        logger.info("Chimera REST service listening on port "
+                + chimeraRestPort);
+      }
+    } catch (Exception e)
+    {
+      logger.error("Failed to get REST port number from " + response + ": "
+              + e.getMessage());
+    } finally
+    {
+      try
+      {
+        lineReader.close();
+      } catch (IOException e2)
+      {
+      }
+    }
+    return port;
+  }
+
+  /**
    * Determine the color that Chimera is using for this model.
    * 
    * @param model
@@ -683,7 +752,8 @@ public class ChimeraManager
    */
   public List<String> sendChimeraCommand(String command, boolean reply)
   {
-    if (!isChimeraLaunched())
+    if (!isChimeraLaunched() || command == null
+            || "".equals(command.trim()))
     {
       return null;
     }
@@ -699,38 +769,102 @@ public class ChimeraManager
       ;
     }
     busy = true;
+    long startTime = System.currentTimeMillis();
     try
     {
-      chimeraListenerThreads.clearResponse(command);
-      String text = command.concat("\n");
-      // System.out.println("send command to chimera: " + text);
-      try
+      if (USE_REST)
       {
-        // send the command
-        chimera.getOutputStream().write(text.getBytes());
-        chimera.getOutputStream().flush();
-      } catch (IOException e)
-      {
-        // logger.info("Unable to execute command: " + text);
-        // logger.info("Exiting...");
-        logger.warn("Unable to execute command: " + text);
-        logger.warn("Exiting...");
-        clearOnChimeraExit();
-        // busy = false;
-        return null;
+        return sendRestCommand(command);
       }
-      if (!reply)
+      else
       {
-        // busy = false;
-        return null;
+        return sendStdinCommand(command, reply);
       }
-      List<String> rsp = chimeraListenerThreads.getResponse(command);
-      // busy = false;
-      return rsp;
     } finally
     {
       busy = false;
+      if (debug)
+      {
+        System.out.println("Chimera command took "
+                + (System.currentTimeMillis() - startTime) + "ms: "
+                + command);
+      }
+
+    }
+  }
+
+  /**
+   * Sends the command to Chimera's REST API, and returns any response lines.
+   * 
+   * @param command
+   * @return
+   */
+  protected List<String> sendRestCommand(String command)
+  {
+    // TODO start a separate thread to do this so we don't block?
+    String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
+    List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
+    commands.add(new BasicNameValuePair("command", command));
+
+    List<String> reply = new ArrayList<String>();
+    BufferedReader response = null;
+    try {
+      response = HttpClientUtils.doHttpUrlPost(restUrl,
+              commands);
+      String line = "";
+      while ((line = response.readLine()) != null) {
+        reply.add(line);
+      }
+    } catch (Exception e)
+    {
+      logger.error("REST call " + command + " failed: " + e.getMessage());
+    } finally
+    {
+      if (response != null)
+      {
+        try
+        {
+          response.close();
+        } catch (IOException e)
+        {
+        }
+      }
+    }
+    return reply;
+  }
+
+  /**
+   * Send a command to stdin of Chimera process, and optionally read any
+   * responses.
+   * 
+   * @param command
+   * @param readReply
+   * @return
+   */
+  protected List<String> sendStdinCommand(String command, boolean readReply)
+  {
+    chimeraListenerThread.clearResponse(command);
+    String text = command.concat("\n");
+    try
+    {
+      // send the command
+      chimera.getOutputStream().write(text.getBytes());
+      chimera.getOutputStream().flush();
+    } catch (IOException e)
+    {
+      // logger.info("Unable to execute command: " + text);
+      // logger.info("Exiting...");
+      logger.warn("Unable to execute command: " + text);
+      logger.warn("Exiting...");
+      clearOnChimeraExit();
+      return null;
+    }
+    if (!readReply)
+    {
+      return null;
     }
+    List<String> rsp = chimeraListenerThread.getResponse(command);
+    return rsp;
   }
 
   public StructureManager getStructureManager()
index 883d536..2b2ce48 100644 (file)
@@ -21,207 +21,282 @@ import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
 /**
  * Reply listener thread
  */
-public class ListenerThreads extends Thread {
-       private InputStream readChan = null;
-       private BufferedReader lineReader = null;
-       private Process chimera = null;
-       private Map<String, List<String>> replyLog = null;
-       private Logger logger;
-       private StructureManager structureManager = null;
-
-       /**
-        * Create a new listener thread to read the responses from Chimera
-        * 
-        * @param chimera
-        *            a handle to the Chimera Process
-        * @param log
-        *            a handle to a List to post the responses to
-        * @param chimeraObject
-        *            a handle to the Chimera Object
-        */
-       public ListenerThreads(Process chimera, StructureManager structureManager) {
-               this.chimera = chimera;
-               this.structureManager = structureManager;
-               replyLog = new HashMap<String, List<String>>();
-               // Get a line-oriented reader
-               readChan = chimera.getInputStream();
-               lineReader = new BufferedReader(new InputStreamReader(readChan));
-               logger = LoggerFactory.getLogger(ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads.class);
-       }
-
-       /**
-        * Start the thread running
-        */
-       public void run() {
-               // System.out.println("ReplyLogListener running");
-               while (true) {
-                       try {
-                               chimeraRead();
-                       } catch (IOException e) {
-                               logger.warn("UCSF Chimera has exited: " + e.getMessage());
-                               return;
-                       }
-               }
-       }
-
-       public List<String> getResponse(String command) {
-               List<String> reply;
-               // System.out.println("getResponse: "+command);
+public class ListenerThreads extends Thread
+{
+  private BufferedReader lineReader = null;
+
+  private Process chimera = null;
+
+  private Map<String, List<String>> replyLog = null;
+
+  private Logger logger;
+
+  private StructureManager structureManager = null;
+
+  private boolean stopMe = false;
+
+  /**
+   * Create a new listener thread to read the responses from Chimera
+   * 
+   * @param chimera
+   *          a handle to the Chimera Process
+   * @param structureManager
+   *          a handle to the Chimera structure manager
+   */
+  public ListenerThreads(Process chimera, StructureManager structureManager)
+  {
+    this.chimera = chimera;
+    this.structureManager = structureManager;
+    replyLog = new HashMap<String, List<String>>();
+    // Get a line-oriented reader
+    InputStream readChan = chimera.getInputStream();
+    lineReader = new BufferedReader(new InputStreamReader(readChan));
+    logger = LoggerFactory
+            .getLogger(ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads.class);
+  }
+
+  /**
+   * Start the thread running
+   */
+  public void run()
+  {
+    // System.out.println("ReplyLogListener running");
+    while (!stopMe)
+    {
+      try
+      {
+        chimeraRead();
+      } catch (IOException e)
+      {
+        logger.warn("UCSF Chimera has exited: " + e.getMessage());
+        return;
+      } finally
+      {
+        if (lineReader != null)
+        {
+          try
+          {
+            lineReader.close();
+          } catch (IOException e)
+          {
+          }
+        }
+      }
+    }
+  }
+
+  public List<String> getResponse(String command)
+  {
+    List<String> reply;
+    // System.out.println("getResponse: "+command);
     // TODO do we need a maximum wait time before aborting?
-               while (!replyLog.containsKey(command)) {
-                       try {
-                               Thread.currentThread().sleep(100);
-                       } catch (InterruptedException e) {
-                       }
-               }
-
-               synchronized (replyLog) {
-                       reply = replyLog.get(command);
-                       // System.out.println("getResponse ("+command+") = "+reply);
-                       replyLog.remove(command);
-               }
-               return reply;
-       }
-
-       public void clearResponse(String command) {
-               try {
-                       Thread.currentThread().sleep(100);
-               } catch (InterruptedException e) {
-               }
-               if (replyLog.containsKey(command))
+    while (!replyLog.containsKey(command))
+    {
+      try
+      {
+        Thread.currentThread().sleep(100);
+      } catch (InterruptedException e)
+      {
+      }
+    }
+
+    synchronized (replyLog)
     {
+      reply = replyLog.get(command);
+      // System.out.println("getResponse ("+command+") = "+reply);
       replyLog.remove(command);
     }
-               return;
-       }
+    return reply;
+  }
 
-       /**
-        * Read input from Chimera
-        * 
-        * @return a List containing the replies from Chimera
-        */
-       private void chimeraRead() throws IOException {
-               if (chimera == null)
+  public void clearResponse(String command)
+  {
+    try
+    {
+      Thread.currentThread().sleep(100);
+    } catch (InterruptedException e)
+    {
+    }
+    if (replyLog.containsKey(command))
+    {
+      replyLog.remove(command);
+    }
+    return;
+  }
+
+  /**
+   * Read input from Chimera
+   * 
+   * @return a List containing the replies from Chimera
+   */
+  private void chimeraRead() throws IOException
+  {
+    if (chimera == null)
     {
       return;
     }
 
-               String line = null;
-               while ((line = lineReader.readLine()) != null) {
-                       // System.out.println("From Chimera-->" + line);
-                       if (line.startsWith("CMD")) {
-                               chimeraCommandRead(line.substring(4));
-                       } else if (line.startsWith("ModelChanged: ")) {
-                               (new ModelUpdater()).start();
-                       } else if (line.startsWith("SelectionChanged: ")) {
-                               (new SelectionUpdater()).start();
-                       } else if (line.startsWith("Trajectory residue network info:")) {
-                               (new NetworkUpdater(line)).start();
-                       }
-               }
-               return;
-       }
-
-       private void chimeraCommandRead(String command) throws IOException {
-               // Generally -- looking for:
-               // CMD command
-               // ........
-               // END
-               // We return the text in between
-               List<String> reply = new ArrayList<String>();
-               boolean updateModels = false;
-               boolean updateSelection = false;
-               boolean importNetwork = false;
-               String line = null;
-
-               synchronized (replyLog) {
-                       while ((line = lineReader.readLine()) != null) {
-                               // System.out.println("From Chimera (" + command + ") -->" + line);
-                               if (line.startsWith("CMD")) {
-                                       logger.warn("Got unexpected command from Chimera: " + line);
-
-                               } else if (line.startsWith("END")) {
-                                       break;
-                               }
-                               if (line.startsWith("ModelChanged: ")) {
-                                       updateModels = true;
-                               } else if (line.startsWith("SelectionChanged: ")) {
-                                       updateSelection = true;
-                               } else if (line.length() == 0) {
-                                       continue;
-                               } else if (!line.startsWith("CMD")) {
-                                       reply.add(line);
-                               } else if (line.startsWith("Trajectory residue network info:")) {
-                                       importNetwork = true;
-                               }
-                       }
-                       replyLog.put(command, reply);
-               }
-               if (updateModels)
+    String line = null;
+    while ((line = lineReader.readLine()) != null)
+    {
+      // System.out.println("From Chimera-->" + line);
+      if (line.startsWith("CMD"))
+      {
+        chimeraCommandRead(line.substring(4));
+      }
+      else if (line.startsWith("ModelChanged: "))
+      {
+        (new ModelUpdater()).start();
+      }
+      else if (line.startsWith("SelectionChanged: "))
+      {
+        (new SelectionUpdater()).start();
+      }
+      else if (line.startsWith("Trajectory residue network info:"))
+      {
+        (new NetworkUpdater(line)).start();
+      }
+    }
+    return;
+  }
+
+  private void chimeraCommandRead(String command) throws IOException
+  {
+    // Generally -- looking for:
+    // CMD command
+    // ........
+    // END
+    // We return the text in between
+    List<String> reply = new ArrayList<String>();
+    boolean updateModels = false;
+    boolean updateSelection = false;
+    boolean importNetwork = false;
+    String line = null;
+
+    synchronized (replyLog)
+    {
+      while ((line = lineReader.readLine()) != null)
+      {
+        // System.out.println("From Chimera (" + command + ") -->" + line);
+        if (line.startsWith("CMD"))
+        {
+          logger.warn("Got unexpected command from Chimera: " + line);
+
+        }
+        else if (line.startsWith("END"))
+        {
+          break;
+        }
+        if (line.startsWith("ModelChanged: "))
+        {
+          updateModels = true;
+        }
+        else if (line.startsWith("SelectionChanged: "))
+        {
+          updateSelection = true;
+        }
+        else if (line.length() == 0)
+        {
+          continue;
+        }
+        else if (!line.startsWith("CMD"))
+        {
+          reply.add(line);
+        }
+        else if (line.startsWith("Trajectory residue network info:"))
+        {
+          importNetwork = true;
+        }
+      }
+      replyLog.put(command, reply);
+    }
+    if (updateModels)
     {
       (new ModelUpdater()).start();
     }
-               if (updateSelection)
+    if (updateSelection)
     {
       (new SelectionUpdater()).start();
     }
-               if (importNetwork) {
-                       (new NetworkUpdater(line)).start();
-               }
-               return;
-       }
-
-       /**
-        * Model updater thread
-        */
-       class ModelUpdater extends Thread {
-
-               public ModelUpdater() {
-               }
-
-               public void run() {
-                       structureManager.updateModels();
-                       structureManager.modelChanged();
-               }
-       }
-
-       /**
-        * Selection updater thread
-        */
-       class SelectionUpdater extends Thread {
-
-               public SelectionUpdater() {
-               }
-
-               public void run() {
-                       try {
-                         logger.info("Responding to chimera selection");
-                         structureManager.chimeraSelectionChanged();
-                       } catch (Exception e) {
-                               logger.warn("Could not update selection", e);
-                       }
-               }
-       }
-
-       /**
-        * Selection updater thread
-        */
-       class NetworkUpdater extends Thread {
-
-               private String line;
-
-               public NetworkUpdater(String line) {
-                       this.line = line;
-               }
-
-               public void run() {
-                       try {
-//                             ((TaskManager<?, ?>) structureManager.getService(TaskManager.class))
-//                                             .execute(new ImportTrajectoryRINTaskFactory(structureManager, line)
-//                                                             .createTaskIterator());
-                       } catch (Exception e) {
-                               logger.warn("Could not import trajectory network", e);
-                       }
-               }
-       }
+    if (importNetwork)
+    {
+      (new NetworkUpdater(line)).start();
+    }
+    return;
+  }
+
+  /**
+   * Model updater thread
+   */
+  class ModelUpdater extends Thread
+  {
+
+    public ModelUpdater()
+    {
+    }
+
+    public void run()
+    {
+      structureManager.updateModels();
+      structureManager.modelChanged();
+    }
+  }
+
+  /**
+   * Selection updater thread
+   */
+  class SelectionUpdater extends Thread
+  {
+
+    public SelectionUpdater()
+    {
+    }
+
+    public void run()
+    {
+      try
+      {
+        logger.info("Responding to chimera selection");
+        structureManager.chimeraSelectionChanged();
+      } catch (Exception e)
+      {
+        logger.warn("Could not update selection", e);
+      }
+    }
+  }
+
+  /**
+   * Selection updater thread
+   */
+  class NetworkUpdater extends Thread
+  {
+
+    private String line;
+
+    public NetworkUpdater(String line)
+    {
+      this.line = line;
+    }
+
+    public void run()
+    {
+      try
+      {
+        // ((TaskManager<?, ?>) structureManager.getService(TaskManager.class))
+        // .execute(new ImportTrajectoryRINTaskFactory(structureManager, line)
+        // .createTaskIterator());
+      } catch (Exception e)
+      {
+        logger.warn("Could not import trajectory network", e);
+      }
+    }
+  }
+
+  /**
+   * Set a flag that this thread should clean up and exit.
+   */
+  public void requestStop()
+  {
+    this.stopMe = true;
+  }
 }
index fbd5e4a..0ca1758 100644 (file)
@@ -29,4 +29,6 @@ public interface SequenceRenderer
 
   Color getResidueBoxColour(SequenceI sequenceI, int r);
 
+  Color getResidueColour(SequenceI seq, int position, FeatureRenderer fr);
+
 }
index 697c0d1..92d5beb 100755 (executable)
@@ -20,6 +20,7 @@
  */
 package jalview.appletgui;
 
+import jalview.api.FeatureRenderer;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -86,6 +87,31 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     return resBoxColour;
   }
 
+  /**
+   * Get the residue colour at the given sequence position - as determined by
+   * the sequence group colour (if any), else the colour scheme, possibly
+   * overridden by a feature colour.
+   * 
+   * @param seq
+   * @param position
+   * @param fr
+   * @return
+   */
+  @Override
+  public Color getResidueColour(final SequenceI seq, int position,
+          FeatureRenderer fr)
+  {
+    // TODO replace 8 or so code duplications with calls to this method
+    // (refactored as needed)
+    Color col = getResidueBoxColour(seq, position);
+
+    if (fr != null)
+    {
+      col = fr.findFeatureColour(col, seq, position);
+    }
+    return col;
+  }
+
   void getBoxColour(ColourSchemeI cs, SequenceI seq, int i)
   {
     if (cs != null)
index d3c8c09..4afc526 100644 (file)
@@ -22,17 +22,20 @@ package jalview.ext.rbvi.chimera;
 
 import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
-import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
-import jalview.util.Format;
+import jalview.util.ColorUtils;
+import jalview.util.Comparison;
 
 import java.awt.Color;
 import java.util.ArrayList;
-import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 
 /**
  * Routines for generating Chimera commands for Jalview/Chimera binding
@@ -55,104 +58,233 @@ public class ChimeraCommands
           SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
           AlignmentI alignment)
   {
+    Map<Color, Map<Integer, Map<String, List<int[]>>>> colourMap = buildColoursMap(
+            ssm, files, sequence, sr, fr, alignment);
 
-    ArrayList<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
-    Hashtable<String,StringBuffer> colranges=new Hashtable<String,StringBuffer>();
+    List<String> colourCommands = buildColourCommands(colourMap);
+
+    StructureMappingcommandSet cs = new StructureMappingcommandSet(
+            ChimeraCommands.class, null,
+            colourCommands.toArray(new String[0]));
+
+    return new StructureMappingcommandSet[]
+    { cs };
+  }
+
+  /**
+   * 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
+   * 
+   * <blockquote> color colorname #modelnumber:range.chain e.g. color #00ff00
+   * #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
+   * 
+   * @see http
+   *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec
+   *      .html </pre>
+   * 
+   * @param colourMap
+   * @return
+   */
+  protected static List<String> buildColourCommands(
+          Map<Color, Map<Integer, Map<String, List<int[]>>>> colourMap)
+  {
+    /*
+     * 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<String>();
+    StringBuilder sb = new StringBuilder(256);
+    boolean firstColour = true;
+    for (Color colour : colourMap.keySet())
+    {
+      String colourCode = ColorUtils.toTkCode(colour);
+      if (!firstColour)
+      {
+        sb.append("; ");
+      }
+      sb.append("color ").append(colourCode).append(" ");
+      firstColour = false;
+      boolean firstModelForColour = true;
+      final Map<Integer, Map<String, List<int[]>>> colourData = colourMap.get(colour);
+      for (Integer model : colourData.keySet())
+      {
+        boolean firstPositionForModel = true;
+        if (!firstModelForColour)
+        {
+          sb.append("|");
+        }
+        firstModelForColour = false;
+        sb.append("#").append(model).append(":");
+
+        final Map<String, List<int[]>> modelData = colourData.get(model);
+        for (String chain : modelData.keySet())
+        {
+          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]);
+            }
+            sb.append(".").append(chain);
+            firstPositionForModel = false;
+          }
+        }
+      }
+    }
+    commands.add(sb.toString());
+    return commands;
+  }
+
+  /**
+   * <pre>
+   * Build a data structure which maps contiguous subsequences for each colour. 
+   * This generates a data structure from which 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)
+   * </pre>
+   */
+  protected static Map<Color, Map<Integer, Map<String, List<int[]>>>> buildColoursMap(
+          StructureSelectionManager ssm, String[] files,
+          SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
+          AlignmentI alignment)
+  {
+    Map<Color, Map<Integer, Map<String, List<int[]>>>> colourMap = new LinkedHashMap<Color, Map<Integer, Map<String, List<int[]>>>>();
+    Color lastColour = null;
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
-      float cols[] = new float[4];
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
-      StringBuffer command = new StringBuffer();
-      StructureMappingcommandSet smc;
-      ArrayList<String> str = new ArrayList<String>();
 
       if (mapping == null || mapping.length < 1)
+      {
         continue;
+      }
 
-      int startPos = -1, lastPos = -1, startModel = -1, lastModel = -1;
-      String startChain = "", lastChain = "";
-      Color lastCol = null;
+      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++)
         {
-          if (mapping[m].getSequence() == sequence[pdbfnum][s]
-                  && (sp = alignment.findIndex(sequence[pdbfnum][s])) > -1)
+          final SequenceI seq = sequence[pdbfnum][s];
+          if (mapping[m].getSequence() == seq
+                  && (sp = alignment.findIndex(seq)) > -1)
           {
             SequenceI asp = alignment.getSequenceAt(sp);
             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;
               }
               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
 
               if (pos < 1 || pos == lastPos)
+              {
                 continue;
+              }
 
-              Color col = sr.getResidueBoxColour(sequence[pdbfnum][s], r);
+              Color colour = sr.getResidueColour(seq, r, fr);
+              final String chain = mapping[m].getChain();
 
-              if (fr != null)
-                col = fr.findFeatureColour(col, sequence[pdbfnum][s], r);
-              if (lastCol != col || lastPos + 1 != pos
-                      || pdbfnum != lastModel
-                      || !mapping[m].getChain().equals(lastChain))
+              /*
+               * 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 (lastCol != null)
+                if (startPos != -1)
                 {
-                  addColourRange(colranges, lastCol,startModel,startPos,lastPos,lastChain); 
+                  addColourRange(colourMap, lastColour, pdbfnum, startPos,
+                          lastPos, lastChain);
                 }
-                lastCol = null;
                 startPos = pos;
-                startModel = pdbfnum;
-                startChain = mapping[m].getChain();
               }
-              lastCol = col;
+              lastColour = colour;
               lastPos = pos;
-              lastModel = pdbfnum;
-              lastChain = mapping[m].getChain();
+              lastChain = chain;
             }
             // final colour range
-            if (lastCol != null)
+            if (lastColour != null)
             {
-              addColourRange(colranges, lastCol,startModel,startPos,lastPos,lastChain); 
+              addColourRange(colourMap, lastColour, pdbfnum, startPos,
+                      lastPos, lastChain);
             }
             break;
           }
         }
       }
-      // Finally, add the command set ready to be returned.
-      StringBuffer coms=new StringBuffer();
-      for (String cr:colranges.keySet())
-      {
-        coms.append("color #"+cr+" "+colranges.get(cr)+";");
-      }
-      cset.add(new StructureMappingcommandSet(ChimeraCommands.class,
-              files[pdbfnum], new String[] { coms.toString() }));
     }
-    return cset.toArray(new StructureMappingcommandSet[cset.size()]);
+    return colourMap;
   }
 
-  private static void addColourRange(Hashtable<String, StringBuffer> colranges, Color lastCol, int startModel,
-          int startPos, int lastPos, String lastChain)
+  /**
+   * Helper method to add one contiguous colour range to the colour map.
+   * 
+   * @param colourMap
+   * @param colour
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  protected static void addColourRange(
+          Map<Color, Map<Integer, Map<String, List<int[]>>>> colourMap,
+          Color colour, int model, int startPos, int endPos, String chain)
   {
-    
-    String colstring = ((lastCol.getRed()< 16) ? "0":"")+Integer.toHexString(lastCol.getRed())
-            + ((lastCol.getGreen()< 16) ? "0":"")+Integer.toHexString(lastCol.getGreen())
-            + ((lastCol.getBlue()< 16) ? "0":"")+Integer.toHexString(lastCol.getBlue());
-    StringBuffer currange = colranges.get(colstring);
-    if (currange==null)
+    /*
+     * Get/initialize map of data for the colour
+     */
+    Map<Integer, Map<String, List<int[]>>> colourData = colourMap
+            .get(colour);
+    if (colourData == null)
+    {
+      colourMap
+              .put(colour,
+                      colourData = new TreeMap<Integer, Map<String, List<int[]>>>());
+    }
+
+    /*
+     * Get/initialize map of data for the colour and model
+     */
+    Map<String, List<int[]>> modelData = colourData.get(model);
+    if (modelData == null)
     {
-      colranges.put(colstring,currange = new StringBuffer());
+      colourData.put(model, modelData = new TreeMap<String, List<int[]>>());
     }
-    if (currange.length()>0)
+
+    /*
+     * Get/initialize map of data for colour, model and chain
+     */
+    List<int[]> chainData = modelData.get(chain);
+    if (chainData == null)
     {
-      currange.append("|");
+      modelData.put(chain, chainData = new ArrayList<int[]>());
     }
-    currange.append("#" + startModel + ":" + ((startPos==lastPos) ? startPos : startPos + "-"
-            + lastPos) + "." + lastChain);
+
+    /*
+     * Add the start/end positions
+     */
+    chainData.add(new int[]
+    { startPos, endPos });
   }
 
 }
index 82f5e5c..b5bfbaa 100644 (file)
@@ -58,6 +58,9 @@ public abstract class JalviewChimeraBinding extends
         SequenceStructureBinding, StructureSelectionManagerProvider
 
 {
+
+  private static final boolean debug = false;
+
   private static final String PHOSPHORUS = "P";
 
   private static final String ALPHACARBON = "CA";
@@ -654,10 +657,12 @@ public abstract class JalviewChimeraBinding extends
       }
       if (selectioncom.length() > 0)
       {
-        // TODO remove debug output
-        System.out.println("Select regions:\n" + selectioncom.toString());
-        System.out
-                .println("Superimpose command(s):\n" + command.toString());
+        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 "
                 + selectioncom.toString() + ";"+command.toString());
         // selcom.append("; ribbons; ");
@@ -669,7 +674,10 @@ public abstract class JalviewChimeraBinding extends
       {
         selectioncom.setLength(selectioncom.length() - 1);
       }
-      System.out.println("Select regions:\n" + selectioncom.toString());
+      if (debug)
+      {
+        System.out.println("Select regions:\n" + selectioncom.toString());
+      }
       allComs.append("; ~display all; chain @CA|P; ribbon "
               + selectioncom.toString() + "; focus");
       // evalStateCommand("select *; backbone; select "+selcom.toString()+"; cartoons; center "+selcom.toString());
@@ -954,8 +962,6 @@ public abstract class JalviewChimeraBinding extends
     }
   }
 
-  boolean debug = false;
-
   private void log(String message)
   {
     System.err.println("## Chimera log: " + message);
index bcbebbd..177fc83 100755 (executable)
@@ -20,6 +20,7 @@
  */
 package jalview.gui;
 
+import jalview.api.FeatureRenderer;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -80,11 +81,12 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     // If EPS graphics, stringWidth will be a double, not an int
     double dwidth = fm.getStringBounds("M", g).getWidth();
 
-    monospacedFont = (dwidth == fm.getStringBounds("|", g).getWidth() && (float) av.charWidth == dwidth);
+    monospacedFont = (dwidth == fm.getStringBounds("|", g).getWidth() && av.charWidth == dwidth);
 
     this.renderGaps = renderGaps;
   }
 
+  @Override
   public Color getResidueBoxColour(SequenceI seq, int i)
   {
     allGroups = av.getAlignment().findAllGroups(seq);
@@ -105,6 +107,31 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
   }
 
   /**
+   * Get the residue colour at the given sequence position - as determined by
+   * the sequence group colour (if any), else the colour scheme, possibly
+   * overridden by a feature colour.
+   * 
+   * @param seq
+   * @param position
+   * @param fr
+   * @return
+   */
+  @Override
+  public Color getResidueColour(final SequenceI seq, int position,
+          FeatureRenderer fr)
+  {
+    // TODO replace 8 or so code duplications with calls to this method
+    // (refactored as needed)
+    Color col = getResidueBoxColour(seq, position);
+
+    if (fr != null)
+    {
+      col = fr.findFeatureColour(col, seq, position);
+    }
+    return col;
+  }
+
+  /**
    * DOCUMENT ME!
    * 
    * @param cs
@@ -188,7 +215,9 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
           int y1)
   {
     if (seq == null)
+     {
       return; // fix for racecondition
+    }
     int i = start;
     int length = seq.getLength();
 
index 41f7781..70c7685 100755 (executable)
 package jalview.schemes;
 
 import jalview.analysis.AAFrequency;
-
-import java.awt.Color;
-import java.util.Map;
-
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
 
+import java.awt.Color;
+import java.util.Map;
+
 public class Blosum62ColourScheme extends ResidueColourScheme
 {
   public Blosum62ColourScheme()
@@ -59,6 +58,7 @@ public class Blosum62ColourScheme extends ResidueColourScheme
 
       if (max.indexOf(res) > -1)
       {
+        // TODO use a constant here?
         currentColour = new Color(154, 154, 255);
       }
       else
@@ -74,6 +74,7 @@ public class Blosum62ColourScheme extends ResidueColourScheme
 
         if (c > 0)
         {
+          // TODO use a constant here?
           currentColour = new Color(204, 204, 255);
         }
         else
index fd76086..44ce010 100644 (file)
@@ -60,6 +60,26 @@ public class ColorUtils
   }
 
   /**
+   * Convert to Tk colour code format
+   * 
+   * @param colour
+   * @return
+   * @see http
+   *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/colortool.html#
+   *      tkcode
+   */
+  public static final String toTkCode(Color colour)
+  {
+    String colstring = "#" + ((colour.getRed() < 16) ? "0" : "")
+            + Integer.toHexString(colour.getRed())
+            + ((colour.getGreen() < 16) ? "0" : "")
+            + Integer.toHexString(colour.getGreen())
+            + ((colour.getBlue() < 16) ? "0" : "")
+            + Integer.toHexString(colour.getBlue());
+    return colstring;
+  }
+
+  /**
    * Returns a colour three shades darker. Note you can't guarantee that
    * brighterThan reverses this, as darkerThan may result in black.
    * 
diff --git a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
new file mode 100644 (file)
index 0000000..7dfbba1
--- /dev/null
@@ -0,0 +1,92 @@
+package jalview.ext.rbvi.chimera;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.awt.Color;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class ChimeraCommandsTest
+{
+  @Test
+  public void testAddColourRange()
+  {
+    Map<Color, Map<Integer, Map<String, List<int[]>>>> map = new LinkedHashMap<Color, Map<Integer, Map<String, List<int[]>>>>();
+    ChimeraCommands.addColourRange(map, Color.pink, 1, 2, 4, "A");
+    ChimeraCommands.addColourRange(map, Color.pink, 1, 8, 8, "A");
+    ChimeraCommands.addColourRange(map, Color.pink, 1, 5, 7, "B");
+    ChimeraCommands.addColourRange(map, Color.red, 1, 3, 5, "A");
+    ChimeraCommands.addColourRange(map, Color.red, 0, 1, 4, "B");
+    ChimeraCommands.addColourRange(map, Color.orange, 0, 5, 9, "C");
+
+    // three colours mapped
+    assertEquals(3, map.keySet().size());
+
+    // Red has two models, Pink and Orange one each
+    assertEquals(2, map.get(Color.red).keySet().size());
+    assertEquals(1, map.get(Color.orange).keySet().size());
+    assertEquals(1, map.get(Color.pink).keySet().size());
+
+    // pink model 1 has two chains, red.0 / red.1 / orange.0 one each
+    assertEquals(2, map.get(Color.pink).get(1).keySet().size());
+    assertEquals(1, map.get(Color.red).get(0).keySet().size());
+    assertEquals(1, map.get(Color.red).get(1).keySet().size());
+    assertEquals(1, map.get(Color.orange).get(0).keySet().size());
+
+    // inspect positions
+    List<int[]> posList = map.get(Color.pink).get(1).get("A");
+    assertEquals(2, posList.size());
+    assertTrue(Arrays.equals(new int[]
+      { 2, 4 }, posList.get(0)));
+    assertTrue(Arrays.equals(new int[]
+      { 8, 8 }, posList.get(1)));
+
+    posList = map.get(Color.pink).get(1).get("B");
+    assertEquals(1, posList.size());
+    assertTrue(Arrays.equals(new int[]
+      { 5, 7 }, posList.get(0)));
+
+    posList = map.get(Color.red).get(0).get("B");
+    assertEquals(1, posList.size());
+    assertTrue(Arrays.equals(new int[]
+      { 1, 4 }, posList.get(0)));
+
+    posList = map.get(Color.red).get(1).get("A");
+    assertEquals(1, posList.size());
+    assertTrue(Arrays.equals(new int[]
+      { 3, 5 }, posList.get(0)));
+
+    posList = map.get(Color.orange).get(0).get("C");
+    assertEquals(1, posList.size());
+    assertTrue(Arrays.equals(new int[]
+      { 5, 9 }, posList.get(0)));
+  }
+
+  @Test
+  public void testBuildColourCommands()
+  {
+
+    Map<Color, Map<Integer, Map<String, List<int[]>>>> map = new LinkedHashMap<Color, Map<Integer, Map<String, List<int[]>>>>();
+    ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B");
+    ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B");
+    ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A");
+    ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A");
+    ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A");
+
+    // Colours should appear in the Chimera command in the order in which
+    // they were added; within colour, by model, by chain, and positions as
+    // added
+    String command = ChimeraCommands.buildColourCommands(map).get(0);
+    assertEquals(
+            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B; color #ffff00 #1:8.A,3-5.A; color #ff0000 #0:3-5.A",
+            command);
+  }
+}
diff --git a/test/jalview/gui/SequenceRendererTest.java b/test/jalview/gui/SequenceRendererTest.java
new file mode 100644 (file)
index 0000000..3f8b96a
--- /dev/null
@@ -0,0 +1,36 @@
+package jalview.gui;
+
+import static org.junit.Assert.assertEquals;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.ZappoColourScheme;
+
+import java.awt.Color;
+
+import org.junit.Test;
+
+public class SequenceRendererTest
+{
+
+  @Test
+  public void testGetResidueBoxColour_zappo()
+  {
+    SequenceI seq = new Sequence("name", "MATVLGSPRAPAFF"); // FER1_MAIZE...
+    AlignmentI al = new Alignment(new SequenceI[]
+      { seq });
+    final AlignViewport av = new AlignViewport(al);
+    SequenceRenderer sr = new SequenceRenderer(av);
+    av.setGlobalColourScheme(new ZappoColourScheme());
+
+    // @see ResidueProperties.zappo
+    assertEquals(Color.pink, sr.getResidueColour(seq, 0, null)); // M
+    assertEquals(Color.green, sr.getResidueColour(seq, 2, null)); // T
+    assertEquals(Color.magenta, sr.getResidueColour(seq, 5, null)); // G
+    assertEquals(Color.orange, sr.getResidueColour(seq, 12, null)); // F
+  }
+  // TODO more tests for getResidueBoxColour covering groups, feature rendering,
+  // gaps, overview...
+
+}
index da2e6ca..3bbcf27 100644 (file)
@@ -39,4 +39,17 @@ public class ColorUtilsTest
             ColorUtils.brighterThan(darkColour));
     assertNull(ColorUtils.brighterThan(null));
   }
+
+  /**
+   * @see http://www.rtapo.com/notes/named_colors.html
+   */
+  @Test
+  public void testToTkCode()
+  {
+    assertEquals("#fffafa", ColorUtils.toTkCode(new Color(255, 250, 250))); // snow
+    assertEquals("#e6e6fa", ColorUtils.toTkCode(new Color(230, 230, 250))); // lavender
+    assertEquals("#dda0dd", ColorUtils.toTkCode(new Color(221, 160, 221))); // plum
+    assertEquals("#800080", ColorUtils.toTkCode(new Color(128, 0, 128))); // purple
+    assertEquals("#00ff00", ColorUtils.toTkCode(new Color(0, 255, 0))); // lime
+  }
 }