/*
* 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.jmol;
import jalview.datamodel.AlignmentAnnotation;
import jalview.datamodel.Annotation;
import jalview.datamodel.PDBEntry;
import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceI;
import jalview.io.AlignFile;
import jalview.io.FileParse;
import jalview.schemes.ResidueProperties;
import jalview.util.Comparison;
import jalview.util.MessageManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javajs.awt.Dimension;
import org.jmol.api.JmolStatusListener;
import org.jmol.api.JmolViewer;
import org.jmol.c.CBK;
import org.jmol.c.STR;
import org.jmol.modelset.Group;
import org.jmol.modelset.Model;
import org.jmol.modelset.ModelSet;
import org.jmol.modelsetbio.BioModel;
import org.jmol.modelsetbio.BioPolymer;
import org.jmol.modelsetbio.Monomer;
import org.jmol.viewer.Viewer;
/**
* Import and process PDB files with Jmol
*
* @author jprocter
*
*/
public class PDBFileWithJmol extends AlignFile implements
JmolStatusListener
{
Viewer viewer = null;
public PDBFileWithJmol(String inFile, String type) throws IOException
{
super(inFile, type);
}
public PDBFileWithJmol(FileParse fp) throws IOException
{
super(fp);
}
public PDBFileWithJmol()
{
}
/**
* create a headless jmol instance for dataprocessing
*
* @return
*/
private Viewer getJmolData()
{
if (viewer == null)
{
try
{
viewer = (Viewer) JmolViewer.allocateViewer(null, null, null, null,
null, "-x -o -n", this);
// ensure the 'new' (DSSP) not 'old' (Ramachandran) SS method is used
viewer.setBooleanProperty("defaultStructureDSSP", true);
} catch (ClassCastException x)
{
throw new Error(MessageManager.formatMessage(
"error.jmol_version_not_compatible_with_jalview_version",
new String[] { JmolViewer.getJmolVersion() }), x);
}
}
return viewer;
}
private void waitForScript(Viewer jmd)
{
while (jmd.isScriptExecuting())
{
try
{
Thread.sleep(50);
} catch (InterruptedException x)
{
}
}
}
/**
* Convert Jmol's secondary structure code to Jalview's, and stored it in the
* secondary structure arrays at the given sequence position
*
* @param proteinStructureSubType
* @param pos
* @param secstr
* @param secstrcode
*/
protected void setSecondaryStructure(STR proteinStructureSubType,
int pos, char[] secstr, char[] secstrcode)
{
switch (proteinStructureSubType)
{
case HELIX310:
secstr[pos] = '3';
break;
case HELIX:
case HELIXALPHA:
secstr[pos] = 'H';
break;
case HELIXPI:
secstr[pos] = 'P';
break;
case SHEET:
secstr[pos] = 'E';
break;
default:
secstr[pos] = 0;
}
switch (proteinStructureSubType)
{
case HELIX310:
case HELIXALPHA:
case HELIXPI:
case HELIX:
secstrcode[pos] = 'H';
break;
case SHEET:
secstrcode[pos] = 'E';
break;
default:
secstrcode[pos] = 0;
}
}
/**
* Convert any non-standard peptide codes to their standard code table
* equivalent. (Initial version only does Selenomethionine MSE->MET.)
*
* @param threeLetterCode
* @param seq
* @param pos
*/
protected void replaceNonCanonicalResidue(String threeLetterCode,
char[] seq, int pos)
{
String canonical = ResidueProperties
.getCanonicalAminoAcid(threeLetterCode);
if (canonical != null && !canonical.equalsIgnoreCase(threeLetterCode))
{
seq[pos] = ResidueProperties.getSingleCharacterCode(canonical);
}
}
/**
* Not implemented - returns null
*/
@Override
public String print()
{
return null;
}
/**
* Not implemented
*/
@Override
public void setCallbackFunction(String callbackType,
String callbackFunction)
{
}
@Override
public void notifyCallback(CBK cbType, Object[] data)
{
String strInfo = (data == null || data[1] == null ? null : data[1]
.toString());
switch (cbType)
{
case ECHO:
sendConsoleEcho(strInfo);
break;
case SCRIPT:
notifyScriptTermination((String) data[2],
((Integer) data[3]).intValue());
break;
case MEASURE:
String mystatus = (String) data[3];
if (mystatus.indexOf("Picked") >= 0
|| mystatus.indexOf("Sequence") >= 0)
{
// Picking mode
sendConsoleMessage(strInfo);
}
else if (mystatus.indexOf("Completed") >= 0)
{
sendConsoleEcho(strInfo.substring(strInfo.lastIndexOf(",") + 2,
strInfo.length() - 1));
}
break;
case MESSAGE:
sendConsoleMessage(data == null ? null : strInfo);
break;
case PICK:
sendConsoleMessage(strInfo);
break;
default:
break;
}
}
String lastConsoleEcho = "";
private void sendConsoleEcho(String string)
{
lastConsoleEcho += string;
lastConsoleEcho += "\n";
}
String lastConsoleMessage = "";
private void sendConsoleMessage(String string)
{
lastConsoleMessage += string;
lastConsoleMessage += "\n";
}
int lastScriptTermination = -1;
String lastScriptMessage = "";
private void notifyScriptTermination(String string, int intValue)
{
lastScriptMessage += string;
lastScriptMessage += "\n";
lastScriptTermination = intValue;
}
@Override
public boolean notifyEnabled(CBK callbackPick)
{
switch (callbackPick)
{
case MESSAGE:
case SCRIPT:
case ECHO:
case LOADSTRUCT:
case ERROR:
return true;
default:
return false;
}
}
/**
* Not implemented - returns null
*/
@Override
public String eval(String strEval)
{
return null;
}
/**
* Not implemented - returns null
*/
@Override
public float[][] functionXY(String functionName, int x, int y)
{
return null;
}
/**
* Not implemented - returns null
*/
@Override
public float[][][] functionXYZ(String functionName, int nx, int ny, int nz)
{
return null;
}
/**
* Not implemented - returns null
*/
@Override
public String createImage(String fileName, String imageType,
Object text_or_bytes, int quality)
{
return null;
}
/**
* Not implemented - returns null
*/
@Override
public Map getRegistryInfo()
{
return null;
}
/**
* Not implemented
*/
@Override
public void showUrl(String url)
{
}
/**
* Not implemented - returns null
*/
@Override
public Dimension resizeInnerPanel(String data)
{
return null;
}
@Override
public Map getJSpecViewProperty(String arg0)
{
return null;
}
/**
* Calls the Jmol library to parse the PDB file, and then inspects the
* resulting object model to generate Jalview-style sequences, with secondary
* structure annotation added where available (i.e. where it has been computed
* by Jmol using DSSP).
*
* @see jalview.io.AlignFile#parse()
*/
@Override
public void parse() throws IOException
{
Viewer jmolModel = getJmolData();
jmolModel.openReader(getDataName(), getDataName(), getReader());
waitForScript(jmolModel);
/*
* Convert one or more Jmol Model objects to Jalview sequences
*/
if (jmolModel.ms.mc > 0)
{
parseBiopolymers(jmolModel.ms);
}
}
/**
* Process the Jmol BioPolymer array and generate a Jalview sequence for each
* chain found (including any secondary structure annotation from DSSP)
*
* @param ms
* @throws IOException
*/
public void parseBiopolymers(ModelSet ms) throws IOException
{
int modelIndex = -1;
for (Model model : ms.am)
{
modelIndex++;
String modelTitle = (String) ms.getInfo(modelIndex, "title");
/*
* Chains can span BioPolymers, so first make a flattened list,
* and then work out the lengths of chains present
*/
List monomers = getMonomers(ms, (BioModel) model);
List chainLengths = getChainLengths(monomers);
/*
* now chop up the Monomer list to make Jalview Sequences
*/
int from = 0;
for (int length : chainLengths)
{
buildSequenceFromChain(monomers.subList(from, from + length), modelTitle);
from += length;
}
}
}
/**
* Helper method to construct a sequence for one chain and add it to the seqs
* list
*
* @param monomers
* a list of all monomers in the chain
* @param modelTitle
*/
protected void buildSequenceFromChain(List monomers, String modelTitle)
{
final int length = monomers.size();
/*
* arrays to hold sequence and secondary structure
*/
char[] seq = new char[length];
char[] secstr = new char[length];
char[] secstrcode = new char[length];
/*
* populate the sequence and secondary structure arrays
*/
extractJmolChainData(monomers, seq, secstr, secstrcode);
/*
* grab chain code and start position from first residue;
*/
String chainId = monomers.get(0).chain.getIDStr();
int firstResNum = monomers.get(0).getResno();
if (firstResNum < 1)
{
// Jalview doesn't like residue < 1, so force this to 1
System.err.println("Converting chain " + chainId + " first RESNUM ("
+ firstResNum + ") to 1");
firstResNum = 1;
}
/*
* convert any non-gap unknown residues to 'X'
*/
convertNonGapCharacters(seq);
/*
* construct and add the Jalview sequence
*/
String seqName = "" + getDataName() + "|" + modelTitle + "|"
+ chainId;
SequenceI sq = new Sequence(seqName, seq, firstResNum, firstResNum + length - 1);
seqs.add(sq);
/*
* add secondary structure predictions (if any)
*/
addSecondaryStructureAnnotation(modelTitle, sq, secstr, secstrcode,
chainId, firstResNum);
/*
* record the PDB id for the sequence
*/
addPdbid(sq, chainId);
}
/**
* Scans the list of (Jmol) Monomer objects, and adds the residue for each to
* the sequence array, and any converted secondary structure prediction to the
* secondary structure arrays
*
* @param monomers
* @param seq
* @param secstr
* @param secstrcode
*/
protected void extractJmolChainData(List monomers, char[] seq,
char[] secstr, char[] secstrcode)
{
int pos = 0;
for (Monomer monomer : monomers)
{
seq[pos] = monomer.getGroup1();
/*
* JAL-1828 replace a modified amino acid with its standard
* equivalent (e.g. MSE with MET->M) to maximise sequence matching
*/
replaceNonCanonicalResidue(monomer.getGroup3(), seq, pos);
/*
* if Jmol has derived a secondary structure prediction for
* this position, convert it to Jalview equivalent and save it
*/
setSecondaryStructure(monomer.getProteinStructureSubType(), pos,
secstr, secstrcode);
pos++;
}
}
/**
* Helper method that adds an AlignmentAnnotation for secondary structure to
* the sequence, provided at least one secondary structure prediction has been
* made
*
* @param modelTitle
* @param seq
* @param secstr
* @param secstrcode
* @param chainId
* @param firstResNum
* @return
*/
protected void addSecondaryStructureAnnotation(String modelTitle,
SequenceI sq, char[] secstr, char[] secstrcode,
String chainId, int firstResNum)
{
char[] seq = sq.getSequence();
boolean ssFound = false;
Annotation asecstr[] = new Annotation[seq.length + firstResNum - 1];
for (int p = 0; p < seq.length; p++)
{
if (secstr[p] >= 'A' && secstr[p] <= 'z')
{
asecstr[p] = new Annotation(String.valueOf(secstr[p]), null,
secstrcode[p], Float.NaN);
ssFound = true;
}
}
if (ssFound)
{
String mt = modelTitle == null ? getDataName() : modelTitle;
mt += chainId;
AlignmentAnnotation ann = new AlignmentAnnotation(
"Secondary Structure", "Secondary Structure for " + mt,
asecstr);
ann.belowAlignment = true;
ann.visible = true;
ann.autoCalculated = false;
ann.setCalcId(getClass().getName());
ann.adjustForAlignment();
ann.validateRangeAndDisplay();
annotations.add(ann);
sq.addAlignmentAnnotation(ann);
}
}
/**
* Replace any non-gap miscellaneous characters with 'X'
*
* @param seq
* @return
*/
protected void convertNonGapCharacters(char[] seq)
{
boolean isNa = Comparison.areNucleotide(new char[][] { seq });
int[] cinds = isNa ? ResidueProperties.nucleotideIndex
: ResidueProperties.aaIndex;
int nonGap = isNa ? ResidueProperties.maxNucleotideIndex
: ResidueProperties.maxProteinIndex;
for (int p = 0; p < seq.length; p++)
{
if (cinds[seq[p]] == nonGap)
{
seq[p] = 'X';
}
}
}
/**
* Add a PDBEntry giving the source of PDB data to the sequence
*
* @param sq
* @param chainId
*/
protected void addPdbid(SequenceI sq, String chainId)
{
PDBEntry pdbe = new PDBEntry();
pdbe.setFile(getDataName());
pdbe.setId(getDataName());
pdbe.setProperty(new Hashtable());
pdbe.setChainCode(chainId);
sq.addPDBId(pdbe);
}
/**
* Scans the list of Monomers (residue models), inspecting the chain id for
* each, and returns an array whose length is the number of chains, and values
* the length of each chain
*
* @param monomers
* @return
*/
protected List getChainLengths(List monomers)
{
List chainLengths = new ArrayList();
int lastChainId = -1;
int length = 0;
for (Monomer monomer : monomers)
{
int chainId = monomer.chain.chainID;
if (chainId != lastChainId && length > 0)
{
/*
* change of chain - record the length of the last one
*/
chainLengths.add(length);
length = 0;
}
lastChainId = chainId;
length++;
}
if (length > 0)
{
/*
* record the length of the final chain
*/
chainLengths.add(length);
}
return chainLengths;
}
/**
* Returns a flattened list of Monomer (residues) in order, across all
* BioPolymers in the model. This simplifies assembling chains which span
* BioPolymers. The result omits any alternate residues reported for the same
* sequence position (RESNUM value).
*
* @param ms
* @param model
* @return
*/
protected List getMonomers(ModelSet ms, BioModel model)
{
List result = new ArrayList();
int lastResNo = Integer.MIN_VALUE;
for (BioPolymer bp : model.bioPolymers)
{
for (int groupLeadAtoms : bp.getLeadAtomIndices())
{
Group group = ms.at[groupLeadAtoms].group;
if (group instanceof Monomer)
{
/*
* ignore alternate residue at same position
* example: 1ejg has residues A:LEU, B:ILE at RESNUM=25
*/
int resNo = group.getResno();
if (lastResNo != resNo)
{
result.add((Monomer) group);
}
lastResNo = resNo;
}
}
}
return result;
}
}