702c0b13c728bb64949c82862e04984e1168c1fb
[jalview.git] / src / jalview / ext / jmol / JmolParser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ext.jmol;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.Annotation;
25 import jalview.datamodel.DBRefEntry;
26 import jalview.datamodel.DBRefSource;
27 import jalview.datamodel.PDBEntry;
28 import jalview.datamodel.Sequence;
29 import jalview.datamodel.SequenceI;
30 import jalview.io.FileParse;
31 import jalview.io.StructureFile;
32 import jalview.schemes.ResidueProperties;
33 import jalview.util.Comparison;
34 import jalview.util.MessageManager;
35
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.Hashtable;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Vector;
42
43 import javajs.awt.Dimension;
44
45 import org.jmol.api.JmolStatusListener;
46 import org.jmol.api.JmolViewer;
47 import org.jmol.c.CBK;
48 import org.jmol.c.STR;
49 import org.jmol.modelset.Group;
50 import org.jmol.modelset.Model;
51 import org.jmol.modelset.ModelSet;
52 import org.jmol.modelsetbio.BioModel;
53 import org.jmol.modelsetbio.BioPolymer;
54 import org.jmol.modelsetbio.Monomer;
55 import org.jmol.viewer.Viewer;
56
57 import MCview.Atom;
58 import MCview.PDBChain;
59
60 /**
61  * Import and process files with Jmol for file like PDB, mmCIF
62  * 
63  * @author jprocter
64  * 
65  */
66 public class JmolParser extends StructureFile implements JmolStatusListener
67 {
68   Viewer viewer = null;
69
70   public JmolParser(boolean addAlignmentAnnotations,
71           boolean predictSecondaryStructure, boolean externalSecStr,
72           String inFile, String type) throws IOException
73   {
74     super(inFile, type);
75     this.visibleChainAnnotation = addAlignmentAnnotations;
76     this.predictSecondaryStructure = predictSecondaryStructure;
77     this.externalSecondaryStructure = externalSecStr;
78   }
79
80   public JmolParser(boolean addAlignmentAnnotations,
81           boolean predictSecondaryStructure, boolean externalSecStr,
82           FileParse fp) throws IOException
83   {
84     super(fp);
85     this.visibleChainAnnotation = addAlignmentAnnotations;
86     this.predictSecondaryStructure = predictSecondaryStructure;
87     this.externalSecondaryStructure = externalSecStr;
88   }
89
90   public JmolParser(FileParse fp) throws IOException
91   {
92     super(fp);
93   }
94
95   public JmolParser(String inFile, String type) throws IOException
96   {
97     super(inFile, type);
98   }
99
100   public JmolParser()
101   {
102   }
103
104   /**
105    * create a headless jmol instance for dataprocessing
106    * 
107    * @return
108    */
109   private Viewer getJmolData()
110   {
111     if (viewer == null)
112     {
113       try
114       {
115         viewer = (Viewer) JmolViewer.allocateViewer(null, null, null, null,
116                 null, "-x -o -n", this);
117         // ensure the 'new' (DSSP) not 'old' (Ramachandran) SS method is used
118         viewer.setBooleanProperty("defaultStructureDSSP", true);
119       } catch (ClassCastException x)
120       {
121         throw new Error(MessageManager.formatMessage(
122                 "error.jmol_version_not_compatible_with_jalview_version",
123                 new String[] { JmolViewer.getJmolVersion() }), x);
124       }
125     }
126     return viewer;
127   }
128
129   private void waitForScript(Viewer jmd)
130   {
131     while (jmd.isScriptExecuting())
132     {
133       try
134       {
135         Thread.sleep(50);
136
137       } catch (InterruptedException x)
138       {
139       }
140     }
141   }
142
143   /**
144    * Convert Jmol's secondary structure code to Jalview's, and stored it in the
145    * secondary structure arrays at the given sequence position
146    * 
147    * @param proteinStructureSubType
148    * @param pos
149    * @param secstr
150    * @param secstrcode
151    */
152   protected void setSecondaryStructure(STR proteinStructureSubType,
153           int pos, char[] secstr, char[] secstrcode)
154   {
155     switch (proteinStructureSubType)
156     {
157     case HELIX310:
158       secstr[pos] = '3';
159       break;
160     case HELIX:
161     case HELIXALPHA:
162       secstr[pos] = 'H';
163       break;
164     case HELIXPI:
165       secstr[pos] = 'P';
166       break;
167     case SHEET:
168       secstr[pos] = 'E';
169       break;
170     default:
171       secstr[pos] = 0;
172     }
173
174     switch (proteinStructureSubType)
175     {
176     case HELIX310:
177     case HELIXALPHA:
178     case HELIXPI:
179     case HELIX:
180       secstrcode[pos] = 'H';
181       break;
182     case SHEET:
183       secstrcode[pos] = 'E';
184       break;
185     default:
186       secstrcode[pos] = 0;
187     }
188   }
189
190   /**
191    * Convert any non-standard peptide codes to their standard code table
192    * equivalent. (Initial version only does Selenomethionine MSE->MET.)
193    * 
194    * @param threeLetterCode
195    * @param seq
196    * @param pos
197    */
198   protected void replaceNonCanonicalResidue(String threeLetterCode,
199           char[] seq, int pos)
200   {
201     String canonical = ResidueProperties
202             .getCanonicalAminoAcid(threeLetterCode);
203     if (canonical != null && !canonical.equalsIgnoreCase(threeLetterCode))
204     {
205       seq[pos] = ResidueProperties.getSingleCharacterCode(canonical);
206     }
207   }
208
209   /**
210    * Not implemented - returns null
211    */
212   @Override
213   public String print()
214   {
215     return null;
216   }
217
218   /**
219    * Not implemented
220    */
221   @Override
222   public void setCallbackFunction(String callbackType,
223           String callbackFunction)
224   {
225   }
226
227   @Override
228   public void notifyCallback(CBK cbType, Object[] data)
229   {
230     String strInfo = (data == null || data[1] == null ? null : data[1]
231             .toString());
232     switch (cbType)
233     {
234     case ECHO:
235       sendConsoleEcho(strInfo);
236       break;
237     case SCRIPT:
238       notifyScriptTermination((String) data[2],
239               ((Integer) data[3]).intValue());
240       break;
241     case MEASURE:
242       String mystatus = (String) data[3];
243       if (mystatus.indexOf("Picked") >= 0
244               || mystatus.indexOf("Sequence") >= 0)
245       {
246         // Picking mode
247         sendConsoleMessage(strInfo);
248       }
249       else if (mystatus.indexOf("Completed") >= 0)
250       {
251         sendConsoleEcho(strInfo.substring(strInfo.lastIndexOf(",") + 2,
252                 strInfo.length() - 1));
253       }
254       break;
255     case MESSAGE:
256       sendConsoleMessage(data == null ? null : strInfo);
257       break;
258     case PICK:
259       sendConsoleMessage(strInfo);
260       break;
261     default:
262       break;
263     }
264   }
265
266   String lastConsoleEcho = "";
267
268   private void sendConsoleEcho(String string)
269   {
270     lastConsoleEcho += string;
271     lastConsoleEcho += "\n";
272   }
273
274   String lastConsoleMessage = "";
275
276   private void sendConsoleMessage(String string)
277   {
278     lastConsoleMessage += string;
279     lastConsoleMessage += "\n";
280   }
281
282   int lastScriptTermination = -1;
283
284   String lastScriptMessage = "";
285
286   private void notifyScriptTermination(String string, int intValue)
287   {
288     lastScriptMessage += string;
289     lastScriptMessage += "\n";
290     lastScriptTermination = intValue;
291   }
292
293   @Override
294   public boolean notifyEnabled(CBK callbackPick)
295   {
296     switch (callbackPick)
297     {
298     case MESSAGE:
299     case SCRIPT:
300     case ECHO:
301     case LOADSTRUCT:
302     case ERROR:
303       return true;
304     default:
305       return false;
306     }
307   }
308
309   /**
310    * Not implemented - returns null
311    */
312   @Override
313   public String eval(String strEval)
314   {
315     return null;
316   }
317
318   /**
319    * Not implemented - returns null
320    */
321   @Override
322   public float[][] functionXY(String functionName, int x, int y)
323   {
324     return null;
325   }
326
327   /**
328    * Not implemented - returns null
329    */
330   @Override
331   public float[][][] functionXYZ(String functionName, int nx, int ny, int nz)
332   {
333     return null;
334   }
335
336   /**
337    * Not implemented - returns null
338    */
339   @Override
340   public String createImage(String fileName, String imageType,
341           Object text_or_bytes, int quality)
342   {
343     return null;
344   }
345
346   /**
347    * Not implemented - returns null
348    */
349   @Override
350   public Map<String, Object> getRegistryInfo()
351   {
352     return null;
353   }
354
355   /**
356    * Not implemented
357    */
358   @Override
359   public void showUrl(String url)
360   {
361   }
362
363   /**
364    * Not implemented - returns null
365    */
366   @Override
367   public Dimension resizeInnerPanel(String data)
368   {
369     return null;
370   }
371
372   @Override
373   public Map<String, Object> getJSpecViewProperty(String arg0)
374   {
375     return null;
376   }
377
378   /**
379    * Calls the Jmol library to parse the PDB file, and then inspects the
380    * resulting object model to generate Jalview-style sequences, with secondary
381    * structure annotation added where available (i.e. where it has been computed
382    * by Jmol using DSSP).
383    * 
384    * @see jalview.io.AlignFile#parse()
385    */
386   @Override
387   public void parse() throws IOException
388   {
389
390     setChains(new Vector<PDBChain>());
391     Viewer jmolModel = getJmolData();
392     jmolModel.openReader(getDataName(), getDataName(), getReader());
393     waitForScript(jmolModel);
394
395     /*
396      * Convert one or more Jmol Model objects to Jalview sequences
397      */
398     if (jmolModel.ms.mc > 0)
399     {
400       // parseBiopolymer(jmolModel.ms);
401       transformJmolModelToJalview(jmolModel.ms);
402     }
403   }
404
405   /**
406    * Process the Jmol BioPolymer array and generate a Jalview sequence for each
407    * chain found (including any secondary structure annotation from DSSP)
408    * 
409    * @param ms
410    * @throws IOException
411    */
412   public void parseBiopolymer(ModelSet ms) throws IOException
413   {
414     int modelIndex = -1;
415     for (Model model : ms.am)
416     {
417       modelIndex++;
418       String modelTitle = (String) ms.getInfo(modelIndex, "title");
419       /*
420        * Chains can span BioPolymers, so first make a flattened list, and then
421        * work out the lengths of chains present
422        */
423       List<Monomer> monomers = getMonomers(ms, (BioModel) model);
424       List<Integer> chainLengths = getChainLengths(monomers);
425
426       /*
427        * now chop up the Monomer list to make Jalview Sequences
428        */
429       int from = 0;
430       for (int length : chainLengths)
431       {
432         buildSequenceFromChain(monomers.subList(from, from + length),
433                 modelTitle);
434         from += length;
435       }
436     }
437   }
438
439   public void transformJmolModelToJalview(ModelSet ms)
440   {
441     try
442     {
443       String lastID = "";
444       List<SequenceI> rna = new ArrayList<SequenceI>();
445       List<SequenceI> prot = new ArrayList<SequenceI>();
446       PDBChain tmpchain;
447       String pdbId = (String) ms.getInfo(0, "title");
448       setId(pdbId);
449       List<Atom> significantAtoms = convertSignificantAtoms(ms);
450       for (Atom tmpatom : significantAtoms)
451       {
452         try
453         {
454           tmpchain = findChain(tmpatom.chain);
455           if (tmpatom.resNumIns.trim().equals(lastID))
456           {
457             // phosphorylated protein - seen both CA and P..
458             continue;
459           }
460           tmpchain.atoms.addElement(tmpatom);
461         } catch (Exception e)
462         {
463           tmpchain = new PDBChain(pdbId, tmpatom.chain);
464           getChains().add(tmpchain);
465           tmpchain.atoms.addElement(tmpatom);
466         }
467         lastID = tmpatom.resNumIns.trim();
468       }
469       makeResidueList();
470       makeCaBondList();
471
472       if (getId() == null)
473       {
474         setId(inFile.getName());
475       }
476       for (PDBChain chain : getChains())
477       {
478         SequenceI chainseq = postProcessChain(chain);
479         if (isRNA(chainseq))
480         {
481           rna.add(chainseq);
482         }
483         else
484         {
485           prot.add(chainseq);
486         }
487       }
488     } catch (OutOfMemoryError er)
489     {
490       System.out
491               .println("OUT OF MEMORY LOADING TRANSFORMING JMOL MODEL TO JALVIEW MODEL");
492       // throw new IOException(
493       // MessageManager
494       // .getString("exception.outofmemory_loading_pdb_file"));
495     }
496   }
497
498   private List<Atom> convertSignificantAtoms(ModelSet ms)
499   {
500     List<Atom> significantAtoms = new ArrayList<Atom>();
501     for (org.jmol.modelset.Atom atom : ms.at)
502     {
503       if (atom.getAtomName().equalsIgnoreCase("CA")
504               || atom.getAtomName().equalsIgnoreCase("P"))
505       {
506         Atom curAtom = new Atom(atom.x, atom.y, atom.z);
507         curAtom.atomIndex = atom.getIndex();
508         curAtom.chain = atom.getChainIDStr();
509         curAtom.insCode = atom.group.getInsertionCode();
510         curAtom.name = atom.getAtomName();
511         curAtom.number = atom.getAtomNumber();
512         curAtom.resName = atom.getGroup3(true);
513         curAtom.resNumber = atom.getResno();
514         curAtom.occupancy = ms.occupancies != null ? ms.occupancies[atom
515                 .getIndex()] : Float.valueOf(atom.getOccupancy100());
516         curAtom.resNumIns = "" + curAtom.resNumber + curAtom.insCode;
517         curAtom.tfactor = 0;
518         curAtom.type = 0;
519         significantAtoms.add(curAtom);
520       }
521     }
522     return significantAtoms;
523   }
524
525   /**
526    * Helper method to construct a sequence for one chain and add it to the seqs
527    * list
528    * 
529    * @param monomers
530    *          a list of all monomers in the chain
531    * @param modelTitle
532    */
533   protected void buildSequenceFromChain(List<Monomer> monomers,
534           String modelTitle)
535   {
536     final int length = monomers.size();
537
538     /*
539      * arrays to hold sequence and secondary structure
540      */
541     char[] seq = new char[length];
542     char[] secstr = new char[length];
543     char[] secstrcode = new char[length];
544
545     /*
546      * populate the sequence and secondary structure arrays
547      */
548     extractJmolChainData(monomers, seq, secstr, secstrcode);
549
550     /*
551      * grab chain code and start position from first residue;
552      */
553     String chainId = monomers.get(0).chain.getIDStr();
554     int firstResNum = monomers.get(0).getResno();
555     if (firstResNum < 1)
556     {
557       // Jalview doesn't like residue < 1, so force this to 1
558       System.err.println("Converting chain " + chainId + " first RESNUM ("
559               + firstResNum + ") to 1");
560       firstResNum = 1;
561     }
562
563     /*
564      * convert any non-gap unknown residues to 'X'
565      */
566     convertNonGapCharacters(seq);
567
568     /*
569      * construct and add the Jalview sequence
570      */
571     String seqName = "" + modelTitle + "|" + chainId;
572     int start = firstResNum;
573     int end = firstResNum + length - 1;
574
575     SequenceI sq = new Sequence(seqName, seq, start, end);
576
577     addPdbid(sq, modelTitle, chainId);
578
579     addSourceDBref(sq, modelTitle, start, end);
580
581     seqs.add(sq);
582
583     /*
584      * add secondary structure predictions (if any)
585      */
586     if (isPredictSecondaryStructure())
587     {
588       addSecondaryStructureAnnotation(modelTitle, sq, secstr, secstrcode,
589               chainId, firstResNum);
590     }
591
592   }
593
594   /**
595    * Add a source db ref entry for the given sequence.
596    * 
597    * @param sq
598    * @param accessionId
599    * @param start
600    * @param end
601    */
602   protected void addSourceDBref(SequenceI sq, String accessionId,
603           int start, int end)
604   {
605     DBRefEntry sourceDBRef = new DBRefEntry();
606     sourceDBRef.setAccessionId(accessionId);
607     sourceDBRef.setSource(DBRefSource.MMCIF);
608     sourceDBRef.setStartRes(start);
609     sourceDBRef.setEndRes(end);
610     sq.setSourceDBRef(sourceDBRef);
611     sq.addDBRef(sourceDBRef);
612   }
613
614   /**
615    * Add a PDBEntry giving the source of PDB data to the sequence
616    * 
617    * @param sq
618    * @param id
619    * @param chainId
620    */
621   protected void addPdbid(SequenceI sq, String id, String chainId)
622   {
623     PDBEntry entry = new PDBEntry();
624     entry.setId(id);
625     entry.setType(PDBEntry.Type.MMCIF);
626     entry.setProperty(new Hashtable());
627     if (chainId != null)
628     {
629       // entry.getProperty().put("CHAIN", chains.elementAt(i).id);
630       entry.setChainCode(String.valueOf(chainId));
631     }
632     if (inFile != null)
633     {
634       entry.setFile(inFile.getAbsolutePath());
635     }
636     else
637     {
638       // TODO: decide if we should dump the datasource to disk
639       entry.setFile(getDataName());
640     }
641
642     sq.addPDBId(entry);
643   }
644
645   /**
646    * Scans the list of (Jmol) Monomer objects, and adds the residue for each to
647    * the sequence array, and any converted secondary structure prediction to the
648    * secondary structure arrays
649    * 
650    * @param monomers
651    * @param seq
652    * @param secstr
653    * @param secstrcode
654    */
655   protected void extractJmolChainData(List<Monomer> monomers, char[] seq,
656           char[] secstr, char[] secstrcode)
657   {
658     int pos = 0;
659     for (Monomer monomer : monomers)
660     {
661       seq[pos] = monomer.getGroup1();
662
663       /*
664        * JAL-1828 replace a modified amino acid with its standard equivalent
665        * (e.g. MSE with MET->M) to maximise sequence matching
666        */
667       replaceNonCanonicalResidue(monomer.getGroup3(), seq, pos);
668
669       /*
670        * if Jmol has derived a secondary structure prediction for this position,
671        * convert it to Jalview equivalent and save it
672        */
673       setSecondaryStructure(monomer.getProteinStructureSubType(), pos,
674               secstr, secstrcode);
675       pos++;
676     }
677   }
678
679   /**
680    * Helper method that adds an AlignmentAnnotation for secondary structure to
681    * the sequence, provided at least one secondary structure prediction has been
682    * made
683    * 
684    * @param modelTitle
685    * @param seq
686    * @param secstr
687    * @param secstrcode
688    * @param chainId
689    * @param firstResNum
690    * @return
691    */
692   protected void addSecondaryStructureAnnotation(String modelTitle,
693           SequenceI sq, char[] secstr, char[] secstrcode, String chainId,
694           int firstResNum)
695   {
696     char[] seq = sq.getSequence();
697     boolean ssFound = false;
698     Annotation asecstr[] = new Annotation[seq.length + firstResNum - 1];
699     for (int p = 0; p < seq.length; p++)
700     {
701       if (secstr[p] >= 'A' && secstr[p] <= 'z')
702       {
703         asecstr[p] = new Annotation(String.valueOf(secstr[p]), null,
704                 secstrcode[p], Float.NaN);
705         ssFound = true;
706       }
707     }
708
709     if (ssFound)
710     {
711       String mt = modelTitle == null ? getDataName() : modelTitle;
712       mt += chainId;
713       AlignmentAnnotation ann = new AlignmentAnnotation(
714               "Secondary Structure", "Secondary Structure for " + mt,
715               asecstr);
716       ann.belowAlignment = true;
717       ann.visible = true;
718       ann.autoCalculated = false;
719       ann.setCalcId(getClass().getName());
720       ann.adjustForAlignment();
721       ann.validateRangeAndDisplay();
722       annotations.add(ann);
723       sq.addAlignmentAnnotation(ann);
724     }
725   }
726
727   /**
728    * Replace any non-gap miscellaneous characters with 'X'
729    * 
730    * @param seq
731    * @return
732    */
733   protected void convertNonGapCharacters(char[] seq)
734   {
735     boolean isNa = Comparison.areNucleotide(new char[][] { seq });
736     int[] cinds = isNa ? ResidueProperties.nucleotideIndex
737             : ResidueProperties.aaIndex;
738     int nonGap = isNa ? ResidueProperties.maxNucleotideIndex
739             : ResidueProperties.maxProteinIndex;
740
741     for (int p = 0; p < seq.length; p++)
742     {
743       if (cinds[seq[p]] == nonGap)
744       {
745         seq[p] = 'X';
746       }
747     }
748   }
749
750   /**
751    * Scans the list of Monomers (residue models), inspecting the chain id for
752    * each, and returns an array whose length is the number of chains, and values
753    * the length of each chain
754    * 
755    * @param monomers
756    * @return
757    */
758   protected List<Integer> getChainLengths(List<Monomer> monomers)
759   {
760     List<Integer> chainLengths = new ArrayList<Integer>();
761     int lastChainId = -1;
762     int length = 0;
763
764     for (Monomer monomer : monomers)
765     {
766       int chainId = monomer.chain.chainID;
767       if (chainId != lastChainId && length > 0)
768       {
769         /*
770          * change of chain - record the length of the last one
771          */
772         chainLengths.add(length);
773         length = 0;
774       }
775       lastChainId = chainId;
776       length++;
777     }
778     if (length > 0)
779     {
780       /*
781        * record the length of the final chain
782        */
783       chainLengths.add(length);
784     }
785
786     return chainLengths;
787   }
788
789   /**
790    * Returns a flattened list of Monomer (residues) in order, across all
791    * BioPolymers in the model. This simplifies assembling chains which span
792    * BioPolymers. The result omits any alternate residues reported for the same
793    * sequence position (RESNUM value).
794    * 
795    * @param ms
796    * @param model
797    * @return
798    */
799   protected List<Monomer> getMonomers(ModelSet ms, BioModel model)
800   {
801     List<Monomer> result = new ArrayList<Monomer>();
802     int lastResNo = Integer.MIN_VALUE;
803
804     for (BioPolymer bp : model.bioPolymers)
805     {
806       for (int groupLeadAtoms : bp.getLeadAtomIndices())
807       {
808         Group group = ms.at[groupLeadAtoms].group;
809         if (group instanceof Monomer)
810         {
811           /*
812            * ignore alternate residue at same position example: 1ejg has
813            * residues A:LEU, B:ILE at RESNUM=25
814            */
815           int resNo = group.getResno();
816           if (lastResNo != resNo)
817           {
818             result.add((Monomer) group);
819           }
820           lastResNo = resNo;
821         }
822       }
823     }
824     return result;
825   }
826
827   public boolean isPredictSecondaryStructure()
828   {
829     return predictSecondaryStructure;
830   }
831
832   public void setPredictSecondaryStructure(boolean predictSecondaryStructure)
833   {
834     this.predictSecondaryStructure = predictSecondaryStructure;
835   }
836
837 }