maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
boolean hasSubModels = maps != null && maps.size() > 1;
return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
}
/**
* Launch Chimera, unless an instance linked to this object is already
* running. Returns true if Chimera is successfully launched, or already
* running, else false.
*
* @return
*/
public boolean launchChimera()
{
if (viewer.isChimeraLaunched())
{
return true;
}
boolean launched = viewer
.launchChimera(StructureManager.getChimeraPaths());
if (launched)
{
startChimeraProcessMonitor();
}
else
{
log("Failed to launch Chimera!");
}
return launched;
}
/**
* Answers true if the Chimera process is still running, false if ended or not
* started.
*
* @return
*/
public boolean isChimeraRunning()
{
return viewer.isChimeraLaunched();
}
/**
* Send a command to Chimera, and optionally log and return any responses.
*
* Does nothing, and returns null, if the command is the same as the last one
* sent [why?].
*
* @param command
* @param getResponse
*/
public List sendChimeraCommand(final String command,
boolean getResponse)
{
if (viewer == null)
{
// ? thread running after viewer shut down
return null;
}
List reply = null;
viewerCommandHistory(false);
if (true /*lastCommand == null || !lastCommand.equals(command)*/)
{
// trim command or it may never find a match in the replyLog!!
List lastReply = viewer.sendChimeraCommand(command.trim(),
getResponse);
if (getResponse)
{
reply = lastReply;
if (debug)
{
log("Response from command ('" + command + "') was:\n"
+ lastReply);
}
}
}
viewerCommandHistory(true);
return reply;
}
/**
* Send a Chimera command asynchronously in a new thread. If the progress
* message is not null, display this message while the command is executing.
*
* @param command
* @param progressMsg
*/
protected abstract void sendAsynchronousCommand(String command,
String progressMsg);
/**
* Sends a set of colour commands to the structure viewer
*
* @param colourBySequenceCommands
*/
@Override
protected void colourBySequence(
StructureMappingcommandSet[] colourBySequenceCommands)
{
for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
{
for (String command : cpdbbyseq.commands)
{
sendAsynchronousCommand(command, COLOURING_CHIMERA);
}
}
}
/**
* @param files
* @param sr
* @param viewPanel
* @return
*/
@Override
protected StructureMappingcommandSet[] getColourBySequenceCommands(
String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
{
return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
getSequence(), sr, viewPanel);
}
/**
* @param command
*/
protected void executeWhenReady(String command)
{
waitForChimera();
sendChimeraCommand(command, false);
waitForChimera();
}
private void waitForChimera()
{
while (viewer != null && viewer.isBusy())
{
try
{
Thread.sleep(15);
} catch (InterruptedException q)
{
}
}
}
// End StructureListener
// //////////////////////////
/**
* instruct the Jalview binding to update the pdbentries vector if necessary
* prior to matching the viewer's contents to the list of structure files
* Jalview knows about.
*/
public abstract void refreshPdbEntries();
/**
* map between index of model filename returned from getPdbFile and the first
* index of models from this file in the viewer. Note - this is not trimmed -
* use getPdbFile to get number of unique models.
*/
private int _modelFileNameMap[];
// ////////////////////////////////
// /StructureListener
@Override
public synchronized String[] getStructureFiles()
{
if (viewer == null)
{
return new String[0];
}
return chimeraMaps.keySet()
.toArray(modelFileNames = new String[chimeraMaps.size()]);
}
/**
* Construct and send a command to highlight zero, one or more atoms. We do
* this by sending an "rlabel" command to show the residue label at that
* position.
*/
@Override
public void highlightAtoms(List atoms)
{
if (atoms == null || atoms.size() == 0)
{
return;
}
StringBuilder cmd = new StringBuilder(128);
boolean first = true;
boolean found = false;
for (AtomSpec atom : atoms)
{
int pdbResNum = atom.getPdbResNum();
String chain = atom.getChain();
String pdbfile = atom.getPdbFile();
List cms = chimeraMaps.get(pdbfile);
if (cms != null && !cms.isEmpty())
{
if (first)
{
cmd.append("rlabel #").append(cms.get(0).getModelNumber())
.append(":");
}
else
{
cmd.append(",");
}
first = false;
cmd.append(pdbResNum);
if (!chain.equals(" "))
{
cmd.append(".").append(chain);
}
found = true;
}
}
String command = cmd.toString();
/*
* avoid repeated commands for the same residue
*/
if (command.equals(lastHighlightCommand))
{
return;
}
/*
* unshow the label for the previous residue
*/
if (lastHighlightCommand != null)
{
viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
}
if (found)
{
viewer.sendChimeraCommand(command, false);
}
this.lastHighlightCommand = command;
}
/**
* Query Chimera for its current selection, and highlight it on the alignment
*/
public void highlightChimeraSelection()
{
/*
* Ask Chimera for its current selection
*/
List selection = viewer.getSelectedResidueSpecs();
/*
* Parse model number, residue and chain for each selected position,
* formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
*/
List atomSpecs = convertStructureResiduesToAlignment(
selection);
/*
* Broadcast the selection (which may be empty, if the user just cleared all
* selections)
*/
getSsm().mouseOverStructure(atomSpecs);
}
/**
* Converts a list of Chimera atomspecs to a list of AtomSpec representing the
* corresponding residues (if any) in Jalview
*
* @param structureSelection
* @return
*/
protected List convertStructureResiduesToAlignment(
List structureSelection)
{
List atomSpecs = new ArrayList();
for (String atomSpec : structureSelection)
{
try
{
AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec);
String pdbfilename = getPdbFileForModel(spec.getModelNumber());
spec.setPdbFile(pdbfilename);
atomSpecs.add(spec);
} catch (IllegalArgumentException e)
{
System.err.println("Failed to parse atomspec: " + atomSpec);
}
}
return atomSpecs;
}
/**
* @param modelId
* @return
*/
protected String getPdbFileForModel(int modelId)
{
/*
* Work out the pdbfilename from the model number
*/
String pdbfilename = modelFileNames[0];
findfileloop: for (String pdbfile : this.chimeraMaps.keySet())
{
for (ChimeraModel cm : chimeraMaps.get(pdbfile))
{
if (cm.getModelNumber() == modelId)
{
pdbfilename = pdbfile;
break findfileloop;
}
}
}
return pdbfilename;
}
private void log(String message)
{
System.err.println("## Chimera log: " + message);
}
private void viewerCommandHistory(boolean enable)
{
// log("(Not yet implemented) History "
// + ((debug || enable) ? "on" : "off"));
}
public long getLoadNotifiesHandled()
{
return loadNotifiesHandled;
}
@Override
public void setJalviewColourScheme(ColourSchemeI cs)
{
colourBySequence = false;
if (cs == null)
{
return;
}
// Chimera expects RBG values in the range 0-1
final double normalise = 255D;
viewerCommandHistory(false);
StringBuilder command = new StringBuilder(128);
List residueSet = ResidueProperties.getResidues(isNucleotide(),
false);
for (String resName : residueSet)
{
char res = resName.length() == 3
? ResidueProperties.getSingleCharacterCode(resName)
: resName.charAt(0);
Color col = cs.findColour(res, 0, null, null, 0f);
command.append("color " + col.getRed() / normalise + ","
+ col.getGreen() / normalise + "," + col.getBlue() / normalise
+ " ::" + resName + ";");
}
sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
viewerCommandHistory(true);
}
/**
* called when the binding thinks the UI needs to be refreshed after a Chimera
* state change. this could be because structures were loaded, or because an
* error has occurred.
*/
public abstract void refreshGUI();
@Override
public void setLoadingFromArchive(boolean loadingFromArchive)
{
this.loadingFromArchive = loadingFromArchive;
}
/**
*
* @return true if Chimeral is still restoring state or loading is still going
* on (see setFinsihedLoadingFromArchive)
*/
@Override
public boolean isLoadingFromArchive()
{
return loadingFromArchive && !loadingFinished;
}
/**
* modify flag which controls if sequence colouring events are honoured by the
* binding. Should be true for normal operation
*
* @param finishedLoading
*/
@Override
public void setFinishedLoadingFromArchive(boolean finishedLoading)
{
loadingFinished = finishedLoading;
}
/**
* Send the Chimera 'background solid " command.
*
* @see https
* ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
* .html
* @param col
*/
@Override
public void setBackgroundColour(Color col)
{
viewerCommandHistory(false);
double normalise = 255D;
final String command = "background solid " + col.getRed() / normalise
+ "," + col.getGreen() / normalise + ","
+ col.getBlue() / normalise + ";";
viewer.sendChimeraCommand(command, false);
viewerCommandHistory(true);
}
/**
* Ask Chimera to save its session to the given file. Returns true if
* successful, else false.
*
* @param filepath
* @return
*/
public boolean saveSession(String filepath)
{
if (isChimeraRunning())
{
List reply = viewer.sendChimeraCommand("save " + filepath,
true);
if (reply.contains("Session written"))
{
return true;
}
else
{
Cache.log
.error("Error saving Chimera session: " + reply.toString());
}
}
return false;
}
/**
* Ask Chimera to open a session file. Returns true if successful, else false.
* The filename must have a .py extension for this command to work.
*
* @param filepath
* @return
*/
public boolean openSession(String filepath)
{
sendChimeraCommand("open " + filepath, true);
// todo: test for failure - how?
return true;
}
/**
* Returns a list of chains mapped in this viewer. Note this list is not
* currently scoped per structure.
*
* @return
*/
@Override
public List getChainNames()
{
return chainNames;
}
/**
* Send a 'focus' command to Chimera to recentre the visible display
*/
public void focusView()
{
sendChimeraCommand("focus", false);
}
/**
* Send a 'show' command for all atoms in the currently selected columns
*
* TODO: pull up to abstract structure viewer interface
*
* @param vp
*/
public void highlightSelection(AlignmentViewPanel vp)
{
List cols = vp.getAlignViewport().getColumnSelection()
.getSelected();
AlignmentI alignment = vp.getAlignment();
StructureSelectionManager sm = getSsm();
for (SequenceI seq : alignment.getSequences())
{
/*
* convert selected columns into sequence positions
*/
int[] positions = new int[cols.size()];
int i = 0;
for (Integer col : cols)
{
positions[i++] = seq.findPosition(col);
}
sm.highlightStructure(this, seq, positions);
}
}
/**
* Constructs and send commands to Chimera to set attributes on residues for
* features visible in Jalview
*
* @param avp
* @return
*/
public int sendFeaturesToViewer(AlignmentViewPanel avp)
{
// TODO refactor as required to pull up to an interface
AlignmentI alignment = avp.getAlignment();
String[] files = getStructureFiles();
if (files == null)
{
return 0;
}
StructureMappingcommandSet commandSet = ChimeraCommands
.getSetAttributeCommandsForFeatures(getSsm(), files,
getSequence(), avp);
String[] commands = commandSet.commands;
if (commands.length > 10)
{
sendCommandsByFile(commands);
}
else
{
for (String command : commands)
{
sendAsynchronousCommand(command, null);
}
}
return commands.length;
}
/**
* Write commands to a temporary file, and send a command to Chimera to open
* the file as a commands script. For use when sending a large number of
* separate commands would overload the REST interface mechanism.
*
* @param commands
*/
protected void sendCommandsByFile(String[] commands)
{
try
{
File tmp = File.createTempFile("chim", ".com");
tmp.deleteOnExit();
PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
for (String command : commands)
{
out.println(command);
}
out.flush();
out.close();
String path = tmp.getAbsolutePath();
sendAsynchronousCommand("open cmd:" + path, null);
} catch (IOException e)
{
System.err.println("Sending commands to Chimera via file failed with "
+ e.getMessage());
}
}
/**
* Get Chimera residues which have the named attribute, find the mapped
* positions in the Jalview sequence(s), and set as sequence features
*
* @param attName
* @param alignmentPanel
*/
public void copyStructureAttributesToFeatures(String attName,
AlignmentViewPanel alignmentPanel)
{
// todo pull up to AAStructureBindingModel (and interface?)
/*
* ask Chimera to list residues with the attribute, reporting its value
*/
// this alternative command
// list residues spec ':*/attName' attr attName
// doesn't report 'None' values (which is good), but
// fails for 'average.bfactor' (which is bad):
String cmd = "list residues attr '" + attName + "'";
List residues = sendChimeraCommand(cmd, true);
boolean featureAdded = createFeaturesForAttributes(attName, residues);
if (featureAdded)
{
alignmentPanel.getFeatureRenderer().featuresAdded();
}
}
/**
* Create features in Jalview for the given attribute name and structure
* residues.
*
*
* The residue list should be 0, 1 or more reply lines of the format:
* residue id #0:5.A isHelix -155.000836316 index 5
* or
* residue id #0:6.A isHelix None
*
*
* @param attName
* @param residues
* @return
*/
protected boolean createFeaturesForAttributes(String attName,
List residues)
{
boolean featureAdded = false;
String featureGroup = getViewerFeatureGroup();
for (String residue : residues)
{
AtomSpec spec = null;
String[] tokens = residue.split(" ");
if (tokens.length < 5)
{
continue;
}
String atomSpec = tokens[2];
String attValue = tokens[4];
/*
* ignore 'None' (e.g. for phi) or 'False' (e.g. for isHelix)
*/
if ("None".equalsIgnoreCase(attValue)
|| "False".equalsIgnoreCase(attValue))
{
continue;
}
try
{
spec = AtomSpec.fromChimeraAtomspec(atomSpec);
} catch (IllegalArgumentException e)
{
System.err.println("Problem parsing atomspec " + atomSpec);
continue;
}
String chainId = spec.getChain();
String description = attValue;
float score = Float.NaN;
try
{
score = Float.valueOf(attValue);
description = chainId;
} catch (NumberFormatException e)
{
// was not a float value
}
String pdbFile = getPdbFileForModel(spec.getModelNumber());
spec.setPdbFile(pdbFile);
List atoms = Collections.singletonList(spec);
/*
* locate the mapped position in the alignment (if any)
*/
SearchResultsI sr = getSsm()
.findAlignmentPositionsForStructurePositions(atoms);
/*
* expect one matched alignment position, or none
* (if the structure position is not mapped)
*/
for (SearchResultMatchI m : sr.getResults())
{
SequenceI seq = m.getSequence();
int start = m.getStart();
int end = m.getEnd();
SequenceFeature sf = new SequenceFeature(attName, description,
start, end, score, featureGroup);
// todo: should SequenceFeature have an explicit property for chain?
// note: repeating the action shouldn't duplicate features
featureAdded |= seq.addSequenceFeature(sf);
}
}
return featureAdded;
}
/**
* Answers the feature group name to apply to features created in Jalview from
* Chimera attributes
*
* @return
*/
protected String getViewerFeatureGroup()
{
// todo pull up to interface
return CHIMERA_FEATURE_GROUP;
}
public Hashtable getChainFile()
{
return chainFile;
}
public List getChimeraModelByChain(String chain)
{
return chimeraMaps.get(chainFile.get(chain));
}
public int getModelNoForChain(String chain)
{
List foundModels = getChimeraModelByChain(chain);
if (foundModels != null && !foundModels.isEmpty())
{
return foundModels.get(0).getModelNumber();
}
return -1;
}
}