From c7fc7690c14127c28c641ea3ff8a8193652fec17 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Fri, 5 Dec 2014 08:40:45 +0000 Subject: [PATCH] JAL-1609 refactored colour by sequence command, now performs ok --- src/jalview/api/SequenceRenderer.java | 2 + src/jalview/appletgui/SequenceRenderer.java | 26 ++ src/jalview/ext/rbvi/chimera/ChimeraCommands.java | 329 +++++++++----------- src/jalview/gui/SequenceRenderer.java | 31 +- src/jalview/util/ColorUtils.java | 20 ++ .../ext/rbvi/chimera/ChimeraCommandsTest.java | 92 ++++++ test/jalview/gui/SequenceRendererTest.java | 36 +++ test/jalview/util/ColorUtilsTest.java | 13 + 8 files changed, 371 insertions(+), 178 deletions(-) create mode 100644 test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java create mode 100644 test/jalview/gui/SequenceRendererTest.java 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 3c47ed1..4afc526 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -27,16 +27,15 @@ import jalview.datamodel.SequenceI; 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.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.ArrayList; 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 @@ -59,31 +58,114 @@ public class ChimeraCommands SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr, AlignmentI alignment) { - String defAttrPath = null; - FileOutputStream fos = null; - try - { - File outFile = File.createTempFile("jalviewdefattr", ".xml"); - outFile.deleteOnExit(); - defAttrPath = outFile.getPath(); - fos = new FileOutputStream(outFile); - fos.write("attribute: jalviewclr\n".getBytes()); - } catch (IOException e1) - { - e1.printStackTrace(); - } - List cset = new ArrayList(); + Map>>> colourMap = buildColoursMap( + ssm, files, sequence, sr, fr, alignment); + 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) + { /* - * Map of { colour, positionSpecs} + * 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. */ - Map colranges = new LinkedHashMap(); - StringBuilder setAttributes = new StringBuilder(256); - String lastColour = "none"; - Color lastCol = null; + 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++) { - boolean startModel = true; StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]); if (mapping == null || mapping.length < 1) @@ -116,200 +198,93 @@ public class ChimeraCommands continue; } - Color col = getResidueColour(seq, r, sr, fr); + Color colour = sr.getResidueColour(seq, r, fr); + 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 = !col.equals(lastCol); + final boolean newColour = !colour.equals(lastColour); final boolean nonContig = lastPos + 1 != pos; - final boolean newChain = !mapping[m].getChain().equals(lastChain); - if (newColour || nonContig || startModel || newChain) + final boolean newChain = !chain.equals(lastChain); + if (newColour || nonContig || newChain) { - if (/* lastCol != null */startPos != -1) + if (startPos != -1) { - addColourRange(colranges, lastCol, pdbfnum, startPos, - lastPos, lastChain, startModel); - startModel = false; + addColourRange(colourMap, lastColour, pdbfnum, startPos, + lastPos, lastChain); } - // lastCol = null; startPos = pos; } - 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, pdbfnum, startPos, - lastPos, lastChain, false); + addColourRange(colourMap, lastColour, pdbfnum, startPos, + lastPos, lastChain); } break; } } } } - try - { - lastColour = buildColourCommands(cset, colranges, - fos, setAttributes); - } catch (IOException e) - { - e.printStackTrace(); - } - - try - { - fos.close(); - } catch (IOException e) - { - e.printStackTrace(); - } - - /* - * Send a rangeColor command, preceded by either defattr or setattr, - * whichever we end up preferring! - * - * rangecolor requires a minimum of two attribute values to operate on - */ - StringBuilder rangeColor = new StringBuilder(256); - rangeColor.append("rangecolor jalviewclr"); - int colourId = 0; - for (String colour : colranges.keySet()) - { - colourId++; - rangeColor.append(" " + colourId + " " + colour); - } - String rangeColorCommand = rangeColor.toString(); - if (rangeColorCommand.split(" ").length < 5) - { - rangeColorCommand += " max " + lastColour; - } - final String defAttrCommand = "defattr " + defAttrPath - + " raiseTool false"; - final String setAttrCommand = setAttributes.toString(); - final String attrCommand = false ? defAttrCommand : setAttrCommand; - cset.add(new StructureMappingcommandSet(ChimeraCommands.class, null, - new String[] - { attrCommand /* , rangeColorCommand */})); - - return cset.toArray(new StructureMappingcommandSet[cset.size()]); + return colourMap; } /** - * 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. + * Helper method to add one contiguous colour range to the colour map. * - * @param seq - * @param position - * @param sr - * @param fr - * @return - */ - protected static Color getResidueColour(final SequenceI seq, - int position, SequenceRenderer sr, FeatureRenderer fr) - { - Color col = sr.getResidueBoxColour(seq, position); - - if (fr != null) - { - col = fr.findFeatureColour(col, seq, position); - } - return col; - } - - /** - * Helper method to build the colour commands for one PDBfile. - * - * @param cset - * the list of commands to be added to - * @param colranges - * the map of colours to residue positions already determined - * @param fos - * file to write 'defattr' commands to - * @param setAttributes - * @throws IOException - */ - protected static String buildColourCommands( - List cset, - Map colranges, - FileOutputStream fos, StringBuilder setAttributes) - throws IOException - { - int colourId = 0; - String lastColour = null; - for (String colour : colranges.keySet()) - { - lastColour = colour; - colourId++; - /* - * Using color command directly is slow for larger structures. - * setAttributes.append("color #" + colour + " " + colranges.get(colour)+ - * ";"); - */ - setAttributes.append("color " + colour + " " + colranges.get(colour) - + ";"); - final String atomSpec = new String(colranges.get(colour)); - // setAttributes.append("setattr r jalviewclr " + colourId + " " - // + atomSpec + ";"); - fos.write(("\t" + atomSpec + "\t" + colourId + "\n").getBytes()); - } - return lastColour; - } - - /** - * Helper method to record a range of positions of the same colour. - * - * @param colranges + * @param colourMap * @param colour * @param model * @param startPos * @param endPos * @param chain - * @param changeModel */ - private static void addColourRange(Map colranges, - Color colour, int model, int startPos, int endPos, String chain, - boolean startModel) + protected static void addColourRange( + Map>>> colourMap, + Color colour, int model, int startPos, int endPos, String chain) { - 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()); - StringBuilder currange = colranges.get(colstring); - if (currange == null) - { - colranges.put(colstring, currange = new StringBuilder(256)); - } /* - * Format as (e.g.) #0:1-3.A,5.A,7-10.A,...#1:1-4.B,..etc + * Get/initialize map of data for the colour */ - // if (currange.length() > 0) - // { - // currange.append("|"); - // } - // currange.append("#" + model + ":" + ((startPos==endPos) ? startPos : - // startPos + "-" - // + endPos) + "." + chain); - if (currange.length() == 0) + Map>> colourData = colourMap + .get(colour); + if (colourData == null) { - currange.append("#" + model + ":"); + colourMap + .put(colour, + colourData = new TreeMap>>()); } - else if (startModel) + + /* + * Get/initialize map of data for the colour and model + */ + Map> modelData = colourData.get(model); + if (modelData == null) { - currange.append(",#" + model + ":"); + colourData.put(model, modelData = new TreeMap>()); } - else + + /* + * 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()); } - final String rangeSpec = (startPos == endPos) ? Integer - .toString(startPos) : (startPos + "-" + endPos); - currange.append(rangeSpec + "." + chain); + + /* + * Add the start/end positions + */ + chainData.add(new int[] + { startPos, endPos }); } } 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/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 + } } -- 1.7.10.2