package jalview.structure;
import jalview.api.AlignViewportI;
import jalview.api.AlignmentViewPanel;
import jalview.api.FeatureRenderer;
import jalview.api.SequenceRenderer;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.HiddenColumns;
import jalview.datamodel.SequenceI;
import jalview.renderer.seqfeatures.FeatureColourFinder;
import jalview.util.Comparison;
import java.awt.Color;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A base class holding methods useful to all classes that implement commands
* for structure viewers
*
* @author gmcarstairs
*
*/
public abstract class StructureCommandsBase implements StructureCommandsI
{
private static final String CMD_SEPARATOR = ";";
/**
* Returns something that separates concatenated commands
*
* @return
*/
protected static String getCommandSeparator()
{
return CMD_SEPARATOR;
}
@Override
public String[] setAttributesForFeatures(StructureSelectionManager ssm,
String[] files, SequenceI[][] sequence, AlignmentViewPanel avp)
{
// default does nothing, override where this is implemented
return null;
}
/**
* Returns the lowest model number used by the structure viewer
*
* @return
*/
@Override
public int getModelStartNo()
{
return 0;
}
/**
*
* Build a data structure which records contiguous subsequences for each colour.
* From this we can easily generate the viewer 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)
*
*
* @param ssm
* @param files
* @param sequence
* @param sr
* @param viewPanel
* @return
*/
protected Map buildColoursMap(
StructureSelectionManager ssm, String[] files,
SequenceI[][] sequence, SequenceRenderer sr, AlignmentViewPanel viewPanel)
{
FeatureRenderer fr = viewPanel.getFeatureRenderer();
FeatureColourFinder finder = new FeatureColourFinder(fr);
AlignViewportI viewport = viewPanel.getAlignViewport();
HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
AlignmentI al = viewport.getAlignment();
Map colourMap = new LinkedHashMap<>();
Color lastColour = null;
for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
{
final int modelNumber = pdbfnum + getModelStartNo();
StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
if (mapping == null || mapping.length < 1)
{
continue;
}
int startPos = -1, lastPos = -1;
String lastChain = "";
for (int s = 0; s < sequence[pdbfnum].length; s++)
{
for (int sp, m = 0; m < mapping.length; m++)
{
final SequenceI seq = sequence[pdbfnum][s];
if (mapping[m].getSequence() == seq
&& (sp = al.findIndex(seq)) > -1)
{
SequenceI asp = al.getSequenceAt(sp);
for (int r = 0; r < asp.getLength(); r++)
{
// no mapping to gaps in sequence
if (Comparison.isGap(asp.getCharAt(r)))
{
continue;
}
int pos = mapping[m].getPDBResNum(asp.findPosition(r));
if (pos < 1 || pos == lastPos)
{
continue;
}
Color colour = sr.getResidueColour(seq, r, finder);
/*
* darker colour for hidden regions
*/
if (!cs.isVisible(r))
{
colour = Color.GRAY;
}
final String chain = mapping[m].getChain();
/*
* Just keep incrementing the end position for this colour range
* _unless_ colour, PDB model or chain has changed, or there is a
* gap in the mapped residue sequence
*/
final boolean newColour = !colour.equals(lastColour);
final boolean nonContig = lastPos + 1 != pos;
final boolean newChain = !chain.equals(lastChain);
if (newColour || nonContig || newChain)
{
if (startPos != -1)
{
addAtomSpecRange(colourMap, lastColour, modelNumber,
startPos, lastPos, lastChain);
}
startPos = pos;
}
lastColour = colour;
lastPos = pos;
lastChain = chain;
}
// final colour range
if (lastColour != null)
{
addAtomSpecRange(colourMap, lastColour, modelNumber, startPos,
lastPos, lastChain);
}
// break;
}
}
}
}
return colourMap;
}
/**
* Helper method to add one contiguous range to the AtomSpec model for the given
* value (creating the model if necessary). As used by Jalview, {@code value} is
*
* a colour, when building a 'colour structure by sequence' command
* a feature value, when building a 'set Chimera attributes from features'
* command
*
*
* @param map
* @param value
* @param model
* @param startPos
* @param endPos
* @param chain
*/
public static final void addAtomSpecRange(Map map,
Object value,
int model, int startPos, int endPos, String chain)
{
/*
* Get/initialize map of data for the colour
*/
AtomSpecModel atomSpec = map.get(value);
if (atomSpec == null)
{
atomSpec = new AtomSpecModel();
map.put(value, atomSpec);
}
atomSpec.addRange(model, startPos, endPos, chain);
}
/**
* Returns a colour formatted suitable for use in viewer command syntax
*
* @param colour
* @return
*/
protected abstract String getColourString(Color colour);
/**
* Traverse the map of colours/models/chains/positions to construct a list of
* 'color' commands (one per distinct colour used). The format of each command
* is specific to the structure viewer.
*
* @param colourMap
* @return
*/
public 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 (Object key : colourMap.keySet())
{
Color colour = (Color) key;
if (!firstColour)
{
sb.append(getCommandSeparator()).append(" ");
}
firstColour = false;
final AtomSpecModel colourData = colourMap.get(colour);
sb.append(getColourCommand(colourData, colour));
}
commands.add(sb.toString());
return commands;
}
/**
* Returns a command to colour the atoms represented by {@code atomSpecModel}
* with the colour specified by {@code colourCode}.
*
* @param atomSpecModel
* @param colour
* @return
*/
protected String getColourCommand(AtomSpecModel atomSpecModel, Color colour)
{
String atomSpec = getAtomSpec(atomSpecModel, false);
return getColourCommand(atomSpec, colour);
}
/**
* Returns a command to colour the atoms described (in viewer command syntax)
* by {@code atomSpec} with the colour specified by {@code colourCode}
*
* @param atomSpec
* @param colour
* @return
*/
protected abstract String getColourCommand(String atomSpec, Color colour);
@Override
public String colourByResidues(Map colours)
{
StringBuilder cmd = new StringBuilder(12 * colours.size());
for (Entry entry : colours.entrySet())
{
String residue = entry.getKey();
String atomSpec = getResidueSpec(residue);
cmd.append(getColourCommand(atomSpec, entry.getValue()));
cmd.append(getCommandSeparator());
}
return cmd.toString();
}
/**
* Helper method to append one start-end range to an atomspec string
*
* @param sb
* @param start
* @param end
* @param chain
* @param firstPositionForModel
*/
protected void appendRange(StringBuilder sb, int start, int end,
String chain, boolean firstPositionForModel, boolean isChimeraX)
{
if (!firstPositionForModel)
{
sb.append(",");
}
if (end == start)
{
sb.append(start);
}
else
{
sb.append(start).append("-").append(end);
}
if (!isChimeraX)
{
sb.append(".");
if (!" ".equals(chain))
{
sb.append(chain);
}
}
}
/**
* Returns the atom specifier meaning all occurrences of the given residue
*
* @param residue
* @return
*/
protected abstract String getResidueSpec(String residue);
}