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
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<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
+ Map<Color, Map<Integer, Map<String, List<int[]>>>> colourMap = buildColoursMap(
+ ssm, files, sequence, sr, fr, alignment);
+ 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)
+ {
/*
- * 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<String, StringBuilder> colranges = new LinkedHashMap<String, StringBuilder>();
- StringBuilder setAttributes = new StringBuilder(256);
- String lastColour = "none";
- Color lastCol = null;
+ 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++)
{
- boolean startModel = true;
StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
if (mapping == null || mapping.length < 1)
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<StructureMappingcommandSet> cset,
- Map<String, StringBuilder> 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<String, StringBuilder> colranges,
- Color colour, int model, int startPos, int endPos, String chain,
- boolean startModel)
+ 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 = "#" + ((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<Integer, Map<String, List<int[]>>> colourData = colourMap
+ .get(colour);
+ if (colourData == null)
{
- currange.append("#" + model + ":");
+ colourMap
+ .put(colour,
+ colourData = new TreeMap<Integer, Map<String, List<int[]>>>());
}
- else if (startModel)
+
+ /*
+ * Get/initialize map of data for the colour and model
+ */
+ Map<String, List<int[]>> modelData = colourData.get(model);
+ if (modelData == null)
{
- currange.append(",#" + model + ":");
+ colourData.put(model, modelData = new TreeMap<String, List<int[]>>());
}
- else
+
+ /*
+ * 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[]>());
}
- 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 });
}
}
--- /dev/null
+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);
+ }
+}