/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Jalview is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
package jalview.ext.rbvi.chimera;
import jalview.api.AlignViewportI;
import jalview.api.AlignmentViewPanel;
import jalview.api.structures.JalviewStructureDisplayI;
import jalview.bin.Cache;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.HiddenColumns;
import jalview.datamodel.PDBEntry;
import jalview.datamodel.SearchResultMatchI;
import jalview.datamodel.SearchResultsI;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
import jalview.httpserver.AbstractRequestHandler;
import jalview.io.DataSourceType;
import jalview.schemes.ColourSchemeI;
import jalview.schemes.ResidueProperties;
import jalview.structure.AtomSpec;
import jalview.structure.StructureMappingcommandSet;
import jalview.structure.StructureSelectionManager;
import jalview.structures.models.AAStructureBindingModel;
import jalview.util.MessageManager;
import jalview.util.StructureCommands;
import java.awt.Color;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.BindException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
public abstract class JalviewChimeraBinding extends AAStructureBindingModel
{
public static final String CHIMERA_FEATURE_GROUP = "Chimera";
// Chimera clause to exclude alternate locations in atom selection
private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
private static final String COLOURING_CHIMERA = MessageManager
.getString("status.colouring_chimera");
private static final boolean debug = false;
private static final String PHOSPHORUS = "P";
private static final String ALPHACARBON = "CA";
private Hashtable chainFile = new Hashtable<>();
/*
* Object through which we talk to Chimera
*/
private ChimeraManager viewer;
/*
* Object which listens to Chimera notifications
*/
private AbstractRequestHandler chimeraListener;
/*
* set if chimera state is being restored from some source - instructs binding
* not to apply default display style when structure set is updated for first
* time.
*/
private boolean loadingFromArchive = false;
/*
* flag to indicate if the Chimera viewer should ignore sequence colouring
* events from the structure manager because the GUI is still setting up
*/
private boolean loadingFinished = true;
/*
* Map of ChimeraModel objects keyed by PDB full local file name
*/
private Map> chimeraMaps = new LinkedHashMap<>();
String lastHighlightCommand;
/*
* incremented every time a load notification is successfully handled -
* lightweight mechanism for other threads to detect when they can start
* referring to new structures.
*/
private long loadNotifiesHandled = 0;
private Thread chimeraMonitor;
/**
* Open a PDB structure file in Chimera and set up mappings from Jalview.
*
* We check if the PDB model id is already loaded in Chimera, if so don't
* reopen it. This is the case if Chimera has opened a saved session file.
*
* @param pe
* @return
*/
public boolean openFile(PDBEntry pe)
{
String file = pe.getFile();
try
{
List modelsToMap = new ArrayList<>();
List oldList = viewer.getModelList();
boolean alreadyOpen = false;
/*
* If Chimera already has this model, don't reopen it, but do remap it.
*/
for (ChimeraModel open : oldList)
{
if (open.getModelName().equals(pe.getId()))
{
alreadyOpen = true;
modelsToMap.add(open);
}
}
/*
* If Chimera doesn't yet have this model, ask it to open it, and retrieve
* the model name(s) added by Chimera.
*/
if (!alreadyOpen)
{
viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
List newList = viewer.getModelList();
// JAL-1728 newList.removeAll(oldList) does not work
for (ChimeraModel cm : newList)
{
if (cm.getModelName().equals(pe.getId()))
{
modelsToMap.add(cm);
}
}
}
chimeraMaps.put(file, modelsToMap);
if (getSsm() != null)
{
getSsm().addStructureViewerListener(this);
}
return true;
} catch (Exception q)
{
log("Exception when trying to open model " + file + "\n"
+ q.toString());
q.printStackTrace();
}
return false;
}
/**
* Constructor
*
* @param ssm
* @param pdbentry
* @param sequenceIs
* @param protocol
*/
public JalviewChimeraBinding(StructureSelectionManager ssm,
PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
DataSourceType protocol)
{
super(ssm, pdbentry, sequenceIs, protocol);
viewer = new ChimeraManager(new StructureManager(true));
}
/**
* Starts a thread that waits for the Chimera process to finish, so that we
* can then close the associated resources. This avoids leaving orphaned
* Chimera viewer panels in Jalview if the user closes Chimera.
*/
protected void startChimeraProcessMonitor()
{
final Process p = viewer.getChimeraProcess();
chimeraMonitor = new Thread(new Runnable()
{
@Override
public void run()
{
try
{
p.waitFor();
JalviewStructureDisplayI display = getViewer();
if (display != null)
{
display.closeViewer(false);
}
} catch (InterruptedException e)
{
// exit thread if Chimera Viewer is closed in Jalview
}
}
});
chimeraMonitor.start();
}
/**
* Start a dedicated HttpServer to listen for Chimera notifications, and tell
* it to start listening
*/
public void startChimeraListener()
{
try
{
chimeraListener = new ChimeraListener(this);
viewer.startListening(chimeraListener.getUri());
} catch (BindException e)
{
System.err.println(
"Failed to start Chimera listener: " + e.getMessage());
}
}
/**
* Close down the Jalview viewer and listener, and (optionally) the associated
* Chimera window.
*/
public void closeViewer(boolean closeChimera)
{
getSsm().removeStructureViewerListener(this, this.getStructureFiles());
if (closeChimera)
{
viewer.exitChimera();
}
if (this.chimeraListener != null)
{
chimeraListener.shutdown();
chimeraListener = null;
}
viewer = null;
if (chimeraMonitor != null)
{
chimeraMonitor.interrupt();
}
releaseUIResources();
}
@Override
public void colourByChain()
{
colourBySequence = false;
sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA);
}
/**
* Constructs and sends a Chimera command to colour by charge
*
*
Aspartic acid and Glutamic acid (negative charge) red
*
Lysine and Arginine (positive charge) blue
*
Cysteine - yellow
*
all others - white
*
*/
@Override
public void colourByCharge()
{
colourBySequence = false;
String command = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
sendAsynchronousCommand(command, COLOURING_CHIMERA);
}
/**
* {@inheritDoc}
*/
@Override
public String superposeStructures(AlignmentI[] _alignment,
int[] _refStructure, HiddenColumns[] _hiddenCols)
{
StringBuilder allComs = new StringBuilder(128);
String[] files = getStructureFiles();
if (!waitForFileLoad(files))
{
return null;
}
refreshPdbEntries();
StringBuilder selectioncom = new StringBuilder(256);
for (int a = 0; a < _alignment.length; a++)
{
int refStructure = _refStructure[a];
AlignmentI alignment = _alignment[a];
HiddenColumns hiddenCols = _hiddenCols[a];
if (refStructure >= files.length)
{
System.err.println("Ignoring invalid reference structure value "
+ refStructure);
refStructure = -1;
}
/*
* 'matched' bit i will be set for visible alignment columns i where
* all sequences have a residue with a mapping to the PDB structure
*/
BitSet matched = new BitSet();
for (int m = 0; m < alignment.getWidth(); m++)
{
if (hiddenCols == null || hiddenCols.isVisible(m))
{
matched.set(m);
}
}
SuperposeData[] structures = new SuperposeData[files.length];
for (int f = 0; f < files.length; f++)
{
structures[f] = new SuperposeData(alignment.getWidth());
}
/*
* Calculate the superposable alignment columns ('matched'), and the
* corresponding structure residue positions (structures.pdbResNo)
*/
int candidateRefStructure = findSuperposableResidues(alignment,
matched, structures);
if (refStructure < 0)
{
/*
* If no reference structure was specified, pick the first one that has
* a mapping in the alignment
*/
refStructure = candidateRefStructure;
}
int nmatched = matched.cardinality();
if (nmatched < 4)
{
return MessageManager.formatMessage("label.insufficient_residues",
nmatched);
}
/*
* Generate select statements to select regions to superimpose structures
*/
String[] selcom = new String[files.length];
for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
{
String chainCd = "." + structures[pdbfnum].chain;
int lpos = -1;
boolean run = false;
StringBuilder molsel = new StringBuilder();
int nextColumnMatch = matched.nextSetBit(0);
while (nextColumnMatch != -1)
{
int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
if (lpos != pdbResNum - 1)
{
/*
* discontiguous - append last residue now
*/
if (lpos != -1)
{
molsel.append(String.valueOf(lpos));
molsel.append(chainCd);
molsel.append(",");
}
run = false;
}
else
{
/*
* extending a contiguous run
*/
if (!run)
{
/*
* start the range selection
*/
molsel.append(String.valueOf(lpos));
molsel.append("-");
}
run = true;
}
lpos = pdbResNum;
nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
}
/*
* and terminate final selection
*/
if (lpos != -1)
{
molsel.append(String.valueOf(lpos));
molsel.append(chainCd);
}
if (molsel.length() > 1)
{
selcom[pdbfnum] = molsel.toString();
selectioncom.append("#").append(String.valueOf(pdbfnum))
.append(":");
selectioncom.append(selcom[pdbfnum]);
selectioncom.append(" ");
if (pdbfnum < files.length - 1)
{
selectioncom.append("| ");
}
}
else
{
selcom[pdbfnum] = null;
}
}
StringBuilder command = new StringBuilder(256);
for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
{
if (pdbfnum == refStructure || selcom[pdbfnum] == null
|| selcom[refStructure] == null)
{
continue;
}
if (command.length() > 0)
{
command.append(";");
}
/*
* Form Chimera match command, from the 'new' structure to the
* 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
*
* match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
*
* @see
* https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
*/
command.append("match ").append(getModelSpec(pdbfnum))
.append(":");
command.append(selcom[pdbfnum]);
command.append("@").append(
structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
// JAL-1757 exclude alternate CA locations
command.append(NO_ALTLOCS);
command.append(" ").append(getModelSpec(refStructure)).append(":");
command.append(selcom[refStructure]);
command.append("@").append(
structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
command.append(NO_ALTLOCS);
}
if (selectioncom.length() > 0)
{
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 ")
.append(selectioncom.toString())*/
.append(";" + command.toString());
}
}
String error = null;
if (selectioncom.length() > 0)
{
// TODO: visually distinguish regions that were superposed
if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
{
selectioncom.setLength(selectioncom.length() - 1);
}
if (debug)
{
System.out.println("Select regions:\n" + selectioncom.toString());
}
allComs.append("; ~display "); // all");
if (!isShowAlignmentOnly())
{
allComs.append("; ribbon; chain @CA|P");
}
else
{
allComs.append("; ~ribbon");
}
allComs.append("; ribbon ").append(selectioncom.toString())
.append("; focus");
List chimeraReplies = sendChimeraCommand(allComs.toString(),
true);
for (String reply : chimeraReplies)
{
String lowerCase = reply.toLowerCase();
if (lowerCase.contains("unequal numbers of atoms")
|| lowerCase.contains("at least"))
{
error = reply;
}
}
}
return error;
}
/**
* Helper method to construct model spec in Chimera format:
*
*
#0 (#1 etc) for a PDB file with no sub-models
*
#0.1 (#1.1 etc) for a PDB file with sub-models
*
* Note for now we only ever choose the first of multiple models. This
* corresponds to the hard-coded Jmol equivalent (compare {1.1}). Refactor in
* future if there is a need to select specific sub-models.
*
* @param pdbfnum
* @return
*/
@Override
public String getModelSpec(int pdbfnum)
{
if (pdbfnum < 0 || pdbfnum >= getPdbCount())
{
return "";
}
/*
* For now, the test for having sub-models is whether multiple Chimera
* models are mapped for the PDB file; the models are returned as a response
* to the Chimera command 'list models type molecule', see
* ChimeraManager.getModelList().
*/
List maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
boolean hasSubModels = maps != null && maps.size() > 1;
String spec = "#" + String.valueOf(pdbfnum);
return hasSubModels ? spec + ".1" : spec;
}
/**
* 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);
/**
* Constructs a set of colour commands and sends them to the structure viewer
*
* @param viewPanel
*/
@Override
protected void colourBySequence(AlignmentViewPanel viewPanel)
{
Map