From: gmungoc Date: Sun, 28 Dec 2014 10:34:27 +0000 (+0000) Subject: Merge branch 'features/JAL-1596ChimeraREST' into develop X-Git-Tag: Jalview_2_9~124 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=e775f1627590b62ccad7cc5104667d347079497b;hp=4dad2ddfc361c0f408b8e2320e7186f985f3bb2d;p=jalview.git Merge branch 'features/JAL-1596ChimeraREST' into develop --- diff --git a/.classpath b/.classpath index b75a654..4c39421 100644 --- a/.classpath +++ b/.classpath @@ -55,6 +55,5 @@ - diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java index a7440e7..b45404e 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java @@ -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 currentModelsMap; @@ -36,7 +52,7 @@ public class ChimeraManager { this.structureManager = structureManager; chimera = null; - chimeraListenerThreads = null; + chimeraListenerThread = null; currentModelsMap = new HashMap(); } @@ -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 args = new ArrayList(); 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 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 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 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 commands = new ArrayList(1); + commands.add(new BasicNameValuePair("command", command)); + + List reply = new ArrayList(); + 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 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 rsp = chimeraListenerThread.getResponse(command); + return rsp; } public StructureManager getStructureManager() diff --git a/src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java b/src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java index 883d536..2b2ce48 100644 --- a/src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java +++ b/src/ext/edu/ucsf/rbvi/strucviz2/port/ListenerThreads.java @@ -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> 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>(); - // 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 getResponse(String command) { - List reply; - // System.out.println("getResponse: "+command); +public class ListenerThreads extends Thread +{ + private BufferedReader lineReader = null; + + private Process chimera = null; + + private Map> 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>(); + // 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 getResponse(String command) + { + List 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 reply = new ArrayList(); - 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 reply = new ArrayList(); + 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; + } } diff --git a/src/jalview/api/SequenceRenderer.java b/src/jalview/api/SequenceRenderer.java index fbd5e4a..0ca1758 100644 --- a/src/jalview/api/SequenceRenderer.java +++ b/src/jalview/api/SequenceRenderer.java @@ -29,4 +29,6 @@ public interface SequenceRenderer Color getResidueBoxColour(SequenceI sequenceI, int r); + Color getResidueColour(SequenceI seq, int position, FeatureRenderer fr); + } diff --git a/src/jalview/appletgui/SequenceRenderer.java b/src/jalview/appletgui/SequenceRenderer.java index 697c0d1..92d5beb 100755 --- a/src/jalview/appletgui/SequenceRenderer.java +++ b/src/jalview/appletgui/SequenceRenderer.java @@ -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) diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index d3c8c09..4afc526 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -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>>> colourMap = buildColoursMap( + ssm, files, sequence, sr, fr, alignment); - ArrayList cset = new ArrayList(); - Hashtable colranges=new Hashtable(); + List 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 + * + *
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 + * + * @param colourMap + * @return + */ + protected static List buildColourCommands( + Map>>> 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 commands = new ArrayList(); + 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>> 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> 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; + } + + /** + *
+   * 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)
+   * 
+ */ + protected static Map>>> buildColoursMap( + StructureSelectionManager ssm, String[] files, + SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr, + AlignmentI alignment) + { + Map>>> colourMap = new LinkedHashMap>>>(); + 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 str = new ArrayList(); 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 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>>> 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>> colourData = colourMap + .get(colour); + if (colourData == null) + { + colourMap + .put(colour, + colourData = new TreeMap>>()); + } + + /* + * Get/initialize map of data for the colour and model + */ + Map> modelData = colourData.get(model); + if (modelData == null) { - colranges.put(colstring,currange = new StringBuffer()); + colourData.put(model, modelData = new TreeMap>()); } - if (currange.length()>0) + + /* + * Get/initialize map of data for colour, model and chain + */ + List chainData = modelData.get(chain); + if (chainData == null) { - currange.append("|"); + modelData.put(chain, chainData = new ArrayList()); } - currange.append("#" + startModel + ":" + ((startPos==lastPos) ? startPos : startPos + "-" - + lastPos) + "." + lastChain); + + /* + * Add the start/end positions + */ + chainData.add(new int[] + { startPos, endPos }); } } diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java index 82f5e5c..b5bfbaa 100644 --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java @@ -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); diff --git a/src/jalview/gui/SequenceRenderer.java b/src/jalview/gui/SequenceRenderer.java index bcbebbd..177fc83 100755 --- a/src/jalview/gui/SequenceRenderer.java +++ b/src/jalview/gui/SequenceRenderer.java @@ -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(); diff --git a/src/jalview/schemes/Blosum62ColourScheme.java b/src/jalview/schemes/Blosum62ColourScheme.java index 41f7781..70c7685 100755 --- a/src/jalview/schemes/Blosum62ColourScheme.java +++ b/src/jalview/schemes/Blosum62ColourScheme.java @@ -21,14 +21,13 @@ 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 diff --git a/src/jalview/util/ColorUtils.java b/src/jalview/util/ColorUtils.java index fd76086..44ce010 100644 --- a/src/jalview/util/ColorUtils.java +++ b/src/jalview/util/ColorUtils.java @@ -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 index 0000000..7dfbba1 --- /dev/null +++ b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java @@ -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>>> map = new LinkedHashMap>>>(); + 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 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>>> map = new LinkedHashMap>>>(); + 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 index 0000000..3f8b96a --- /dev/null +++ b/test/jalview/gui/SequenceRendererTest.java @@ -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... + +} diff --git a/test/jalview/util/ColorUtilsTest.java b/test/jalview/util/ColorUtilsTest.java index da2e6ca..3bbcf27 100644 --- a/test/jalview/util/ColorUtilsTest.java +++ b/test/jalview/util/ColorUtilsTest.java @@ -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 + } }