2a43244cb6d6e3cff86e622900f930330205aec2
[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 java.util.Locale;
24
25 import jalview.datamodel.AlignmentAnnotation;
26 import jalview.datamodel.Annotation;
27 import jalview.datamodel.PDBEntry;
28 import jalview.datamodel.SequenceI;
29 import jalview.io.DataSourceType;
30 import jalview.io.FileParse;
31 import jalview.io.StructureFile;
32 import jalview.schemes.ResidueProperties;
33 import jalview.util.Format;
34 import jalview.util.MessageManager;
35
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Vector;
42
43 import org.jmol.api.JmolStatusListener;
44 import org.jmol.api.JmolViewer;
45 import org.jmol.c.CBK;
46 import org.jmol.c.STR;
47 import org.jmol.modelset.ModelSet;
48 import org.jmol.viewer.Viewer;
49
50 import com.stevesoft.pat.Regex;
51
52 import mc_view.Atom;
53 import mc_view.PDBChain;
54 import mc_view.Residue;
55
56 /**
57  * Import and process files with Jmol for file like PDB, mmCIF
58  * 
59  * @author jprocter
60  * 
61  */
62 public class JmolParser extends StructureFile implements JmolStatusListener
63 {
64   Viewer viewer = null;
65
66   private boolean alphaFoldModel;
67
68   public JmolParser(boolean immediate, Object inFile,
69           DataSourceType sourceType) throws IOException
70   {
71     // BH 2018 File or String for filename
72     super(immediate, inFile, sourceType);
73   }
74
75   public JmolParser(Object inFile, DataSourceType sourceType)
76           throws IOException
77   {
78     super(inFile, sourceType);
79   }
80
81   public JmolParser(FileParse fp) throws IOException
82   {
83     super(fp);
84   }
85
86   public JmolParser()
87   {
88   }
89
90   /**
91    * Calls the Jmol library to parse the PDB/mmCIF file, and then inspects the
92    * resulting object model to generate Jalview-style sequences, with secondary
93    * structure annotation added where available (i.e. where it has been computed
94    * by Jmol using DSSP).
95    * 
96    * @see jalview.io.AlignFile#parse()
97    */
98   @Override
99   public void parse() throws IOException
100   {
101     setChains(new Vector<PDBChain>());
102     Viewer jmolModel = getJmolData();
103     jmolModel.openReader(getDataName(), getDataName(), getReader());
104     waitForScript(jmolModel);
105
106     /*
107      * Convert one or more Jmol Model objects to Jalview sequences
108      */
109     if (jmolModel.ms.mc > 0)
110     {
111       // ideally we do this
112       // try
113       // {
114       // setStructureFileType(jmolModel.evalString("show _fileType"));
115       // } catch (Exception q)
116       // {
117       // }
118       // ;
119       // instead, we distinguish .cif from non-.cif by filename
120       setStructureFileType(getDataName().toLowerCase(Locale.ROOT).endsWith(".cif")
121               ? PDBEntry.Type.MMCIF.toString()
122               : "PDB");
123
124       transformJmolModelToJalview(jmolModel.ms);
125     }
126   }
127
128   /**
129    * create a headless jmol instance for dataprocessing
130    * 
131    * @return
132    */
133   private Viewer getJmolData()
134   {
135     if (viewer == null)
136     {
137       try
138       {
139         /*
140          * params -o (output to sysout) -n (nodisplay) -x (exit when finished)
141          * see http://wiki.jmol.org/index.php/Jmol_Application
142          */
143         
144         viewer = JalviewJmolBinding.getJmolData(this);
145         // ensure the 'new' (DSSP) not 'old' (Ramachandran) SS method is used
146         viewer.setBooleanProperty("defaultStructureDSSP", true);
147       } catch (ClassCastException x)
148       {
149         throw new Error(MessageManager.formatMessage(
150                 "error.jmol_version_not_compatible_with_jalview_version",
151                 new String[]
152                 { JmolViewer.getJmolVersion() }), x);
153       }
154     }
155     return viewer;
156   }
157   
158   public static Regex getNewAlphafoldValidator()
159   {
160     Regex validator =  new Regex("(AF-[A-Z]+[0-9]+[A-Z0-9]+-F1)");
161     validator.setIgnoreCase(true);
162     return validator;
163   }
164
165   PDBEntry.Type jmolFiletype=null;
166   /**
167    * resolve a jmol filetype string and update the jmolFiletype field accordingly
168    * @param jmolIdentifiedFileType
169    * @return true if filetype was identified as MMCIF, PDB
170    */
171   public boolean updateFileType(String jmolIdentifiedFileType)
172   {
173     if (jmolIdentifiedFileType == null 
174             || jmolIdentifiedFileType.trim().equals(""))
175     {
176       return false;
177     }
178     if ("mmcif".equalsIgnoreCase(jmolIdentifiedFileType)) {
179       jmolFiletype = PDBEntry.Type.MMCIF;
180       return true;
181     }
182     if ("pdb".equalsIgnoreCase(jmolIdentifiedFileType))
183     {
184       jmolFiletype = PDBEntry.Type.PDB;
185       return true;
186     } 
187     return false;
188   }
189   
190   public void transformJmolModelToJalview(ModelSet ms) throws IOException
191   {
192     try
193     {
194       Regex alphaFold = getNewAlphafoldValidator();
195       String lastID = "";
196       List<SequenceI> rna = new ArrayList<SequenceI>();
197       List<SequenceI> prot = new ArrayList<SequenceI>();
198       PDBChain tmpchain;
199       String pdbId = (String) ms.getInfo(0, "title");
200       boolean isMMCIF = false;
201       String jmolFileType_String = (String) ms.getInfo(0, "fileType");
202       if (updateFileType(jmolFileType_String))
203       {
204         setStructureFileType(jmolFiletype.toString());
205       }
206       
207       isMMCIF = PDBEntry.Type.MMCIF.equals(jmolFiletype);
208       
209       if (pdbId == null)
210       {
211         setId(safeName(getDataName()));
212         setPDBIdAvailable(false);
213       }
214       else
215       {
216         setId(pdbId);
217         setPDBIdAvailable(true);
218         alphaFoldModel = alphaFold.search(pdbId) && isMMCIF;  
219
220       }
221       List<Atom> significantAtoms = convertSignificantAtoms(ms);
222       for (Atom tmpatom : significantAtoms)
223       {
224         if (tmpatom.resNumIns.trim().equals(lastID))
225         {
226           // phosphorylated protein - seen both CA and P..
227           continue;
228         }
229         tmpchain = findChain(tmpatom.chain);
230         if (tmpchain != null)
231         {
232           tmpchain.atoms.addElement(tmpatom);
233         } else
234         {
235           String tempFString=null;
236           if (isAlphafoldModel())
237           {
238             tempFString = "Alphafold Reliability";
239           }
240
241           tmpchain = new PDBChain(getId(), tmpatom.chain,tempFString);
242           getChains().add(tmpchain);
243           tmpchain.atoms.addElement(tmpatom);
244         }
245         lastID = tmpatom.resNumIns.trim();
246       }
247       if (isParseImmediately())
248       {
249         // configure parsing settings from the static singleton
250         xferSettings();
251       }
252
253       makeResidueList();
254       makeCaBondList();
255
256       for (PDBChain chain : getChains())
257       {
258         SequenceI chainseq = postProcessChain(chain);
259         if (isRNA(chainseq))
260         {
261           rna.add(chainseq);
262         }
263         else
264         {
265           prot.add(chainseq);
266         }
267
268         // look at local setting for adding secondary tructure
269         if (predictSecondaryStructure)
270         {
271           createAnnotation(chainseq, chain, ms.at);
272         }
273       }
274     } catch (OutOfMemoryError er)
275     {
276       System.out.println(
277               "OUT OF MEMORY LOADING TRANSFORMING JMOL MODEL TO JALVIEW MODEL");
278       throw new IOException(MessageManager
279               .getString("exception.outofmemory_loading_mmcif_file"));
280     }
281   }
282
283   private boolean isAlphafoldModel()
284   {
285     return alphaFoldModel;
286   }
287
288   private List<Atom> convertSignificantAtoms(ModelSet ms)
289   {
290     List<Atom> significantAtoms = new ArrayList<Atom>();
291     HashMap<String, org.jmol.modelset.Atom> chainTerMap = new HashMap<String, org.jmol.modelset.Atom>();
292     org.jmol.modelset.Atom prevAtom = null;
293     for (org.jmol.modelset.Atom atom : ms.at)
294     {
295       if (atom.getAtomName().equalsIgnoreCase("CA")
296               || atom.getAtomName().equalsIgnoreCase("P"))
297       {
298         if (!atomValidated(atom, prevAtom, chainTerMap))
299         {
300           continue;
301         }
302         Atom curAtom = new Atom(atom.x, atom.y, atom.z);
303         curAtom.atomIndex = atom.getIndex();
304         curAtom.chain = atom.getChainIDStr();
305         curAtom.insCode = atom.group.getInsertionCode() == '\000' ? ' '
306                 : atom.group.getInsertionCode();
307         curAtom.name = atom.getAtomName();
308         curAtom.number = atom.getAtomNumber();
309         curAtom.resName = atom.getGroup3(true);
310         curAtom.resNumber = atom.getResno();
311         curAtom.occupancy = ms.occupancies != null
312                 ? ms.occupancies[atom.getIndex()]
313                 : Float.valueOf(atom.getOccupancy100());
314         String fmt = new Format("%4i").form(curAtom.resNumber);
315         curAtom.resNumIns = (fmt + curAtom.insCode);
316         curAtom.tfactor = atom.getBfactor100() / 100f;
317         curAtom.type = 0;
318         // significantAtoms.add(curAtom);
319         // ignore atoms from subsequent models
320         if (!significantAtoms.contains(curAtom))
321         {
322           significantAtoms.add(curAtom);
323         }
324         prevAtom = atom;
325       }
326     }
327     return significantAtoms;
328   }
329
330   private boolean atomValidated(org.jmol.modelset.Atom curAtom,
331           org.jmol.modelset.Atom prevAtom,
332           HashMap<String, org.jmol.modelset.Atom> chainTerMap)
333   {
334     // System.out.println("Atom: " + curAtom.getAtomNumber()
335     // + " Last atom index " + curAtom.group.lastAtomIndex);
336     if (chainTerMap == null || prevAtom == null)
337     {
338       return true;
339     }
340     String curAtomChId = curAtom.getChainIDStr();
341     String prevAtomChId = prevAtom.getChainIDStr();
342     // new chain encoutered
343     if (!prevAtomChId.equals(curAtomChId))
344     {
345       // On chain switch add previous chain termination to xTerMap if not exists
346       if (!chainTerMap.containsKey(prevAtomChId))
347       {
348         chainTerMap.put(prevAtomChId, prevAtom);
349       }
350       // if current atom belongs to an already terminated chain and the resNum
351       // diff < 5 then mark as valid and update termination Atom
352       if (chainTerMap.containsKey(curAtomChId))
353       {
354         if (curAtom.getResno() < chainTerMap.get(curAtomChId).getResno())
355         {
356           return false;
357         }
358         if ((curAtom.getResno()
359                 - chainTerMap.get(curAtomChId).getResno()) < 5)
360         {
361           chainTerMap.put(curAtomChId, curAtom);
362           return true;
363         }
364         return false;
365       }
366     }
367     // atom with previously terminated chain encountered
368     else if (chainTerMap.containsKey(curAtomChId))
369     {
370       if (curAtom.getResno() < chainTerMap.get(curAtomChId).getResno())
371       {
372         return false;
373       }
374       if ((curAtom.getResno()
375               - chainTerMap.get(curAtomChId).getResno()) < 5)
376       {
377         chainTerMap.put(curAtomChId, curAtom);
378         return true;
379       }
380       return false;
381     }
382     // HETATM with resNum jump > 2
383     return !(curAtom.isHetero()
384             && ((curAtom.getResno() - prevAtom.getResno()) > 2));
385   }
386
387   private void createAnnotation(SequenceI sequence, PDBChain chain,
388           org.jmol.modelset.Atom[] jmolAtoms)
389   {
390     char[] secstr = new char[sequence.getLength()];
391     char[] secstrcode = new char[sequence.getLength()];
392
393     // Ensure Residue size equals Seq size
394     if (chain.residues.size() != sequence.getLength())
395     {
396       return;
397     }
398     int annotIndex = 0;
399     for (Residue residue : chain.residues)
400     {
401       Atom repAtom = residue.getAtoms().get(0);
402       STR proteinStructureSubType = jmolAtoms[repAtom.atomIndex].group
403               .getProteinStructureSubType();
404       setSecondaryStructure(proteinStructureSubType, annotIndex, secstr,
405               secstrcode);
406       ++annotIndex;
407     }
408     addSecondaryStructureAnnotation(chain.pdbid, sequence, secstr,
409             secstrcode, chain.id, sequence.getStart());
410   }
411
412   /**
413    * Helper method that adds an AlignmentAnnotation for secondary structure to
414    * the sequence, provided at least one secondary structure assignment has been
415    * made
416    * 
417    * @param modelTitle
418    * @param seq
419    * @param secstr
420    * @param secstrcode
421    * @param chainId
422    * @param firstResNum
423    * @return
424    */
425   protected void addSecondaryStructureAnnotation(String modelTitle,
426           SequenceI sq, char[] secstr, char[] secstrcode, String chainId,
427           int firstResNum)
428   {
429     int length = sq.getLength();
430     boolean ssFound = false;
431     Annotation asecstr[] = new Annotation[length + firstResNum - 1];
432     for (int p = 0; p < length; p++)
433     {
434       if (secstr[p] >= 'A' && secstr[p] <= 'z')
435       {
436         try
437         {
438           asecstr[p] = new Annotation(String.valueOf(secstr[p]), null,
439                   secstrcode[p], Float.NaN);
440           ssFound = true;
441         } catch (Exception e)
442         {
443           // e.printStackTrace();
444         }
445       }
446     }
447
448     if (ssFound)
449     {
450       String mt = modelTitle == null ? getDataName() : modelTitle;
451       mt += chainId;
452       AlignmentAnnotation ann = new AlignmentAnnotation(
453               "Secondary Structure", "Secondary Structure for " + mt,
454               asecstr);
455       ann.belowAlignment = true;
456       ann.visible = true;
457       ann.autoCalculated = false;
458       ann.setCalcId(getClass().getName());
459       ann.adjustForAlignment();
460       ann.validateRangeAndDisplay();
461       annotations.add(ann);
462       sq.addAlignmentAnnotation(ann);
463     }
464   }
465
466   private void waitForScript(Viewer jmd)
467   {
468     while (jmd.isScriptExecuting())
469     {
470       try
471       {
472         Thread.sleep(50);
473
474       } catch (InterruptedException x)
475       {
476       }
477     }
478   }
479
480   /**
481    * Convert Jmol's secondary structure code to Jalview's, and stored it in the
482    * secondary structure arrays at the given sequence position
483    * 
484    * @param proteinStructureSubType
485    * @param pos
486    * @param secstr
487    * @param secstrcode
488    */
489   protected void setSecondaryStructure(STR proteinStructureSubType, int pos,
490           char[] secstr, char[] secstrcode)
491   {
492     switch (proteinStructureSubType)
493     {
494     case HELIX310:
495       secstr[pos] = '3';
496       break;
497     case HELIX:
498     case HELIXALPHA:
499       secstr[pos] = 'H';
500       break;
501     case HELIXPI:
502       secstr[pos] = 'P';
503       break;
504     case SHEET:
505       secstr[pos] = 'E';
506       break;
507     default:
508       secstr[pos] = 0;
509     }
510
511     switch (proteinStructureSubType)
512     {
513     case HELIX310:
514     case HELIXALPHA:
515     case HELIXPI:
516     case HELIX:
517       secstrcode[pos] = 'H';
518       break;
519     case SHEET:
520       secstrcode[pos] = 'E';
521       break;
522     default:
523       secstrcode[pos] = 0;
524     }
525   }
526
527   /**
528    * Convert any non-standard peptide codes to their standard code table
529    * equivalent. (Initial version only does Selenomethionine MSE->MET.)
530    * 
531    * @param threeLetterCode
532    * @param seq
533    * @param pos
534    */
535   protected void replaceNonCanonicalResidue(String threeLetterCode,
536           char[] seq, int pos)
537   {
538     String canonical = ResidueProperties
539             .getCanonicalAminoAcid(threeLetterCode);
540     if (canonical != null && !canonical.equalsIgnoreCase(threeLetterCode))
541     {
542       seq[pos] = ResidueProperties.getSingleCharacterCode(canonical);
543     }
544   }
545
546   /**
547    * Not implemented - returns null
548    */
549   @Override
550   public String print(SequenceI[] seqs, boolean jvSuffix)
551   {
552     return null;
553   }
554
555   /**
556    * Not implemented
557    */
558   @Override
559   public void setCallbackFunction(String callbackType,
560           String callbackFunction)
561   {
562   }
563
564   @Override
565   public void notifyCallback(CBK cbType, Object[] data)
566   {
567     String strInfo = (data == null || data[1] == null ? null
568             : data[1].toString());
569     switch (cbType)
570     {
571     case ECHO:
572       sendConsoleEcho(strInfo);
573       break;
574     case SCRIPT:
575       notifyScriptTermination((String) data[2],
576               ((Integer) data[3]).intValue());
577       break;
578     case MEASURE:
579       String mystatus = (String) data[3];
580       if (mystatus.indexOf("Picked") >= 0
581               || mystatus.indexOf("Sequence") >= 0)
582       {
583         // Picking mode
584         sendConsoleMessage(strInfo);
585       }
586       else if (mystatus.indexOf("Completed") >= 0)
587       {
588         sendConsoleEcho(strInfo.substring(strInfo.lastIndexOf(",") + 2,
589                 strInfo.length() - 1));
590       }
591       break;
592     case MESSAGE:
593       sendConsoleMessage(data == null ? null : strInfo);
594       break;
595     case PICK:
596       sendConsoleMessage(strInfo);
597       break;
598     default:
599       break;
600     }
601   }
602
603   String lastConsoleEcho = "";
604
605   private void sendConsoleEcho(String string)
606   {
607     lastConsoleEcho += string;
608     lastConsoleEcho += "\n";
609   }
610
611   String lastConsoleMessage = "";
612
613   private void sendConsoleMessage(String string)
614   {
615     lastConsoleMessage += string;
616     lastConsoleMessage += "\n";
617   }
618
619   int lastScriptTermination = -1;
620
621   String lastScriptMessage = "";
622
623   private void notifyScriptTermination(String string, int intValue)
624   {
625     lastScriptMessage += string;
626     lastScriptMessage += "\n";
627     lastScriptTermination = intValue;
628   }
629
630   @Override
631   public boolean notifyEnabled(CBK callbackPick)
632   {
633     switch (callbackPick)
634     {
635     case MESSAGE:
636     case SCRIPT:
637     case ECHO:
638     case LOADSTRUCT:
639     case ERROR:
640       return true;
641     default:
642       return false;
643     }
644   }
645
646   /**
647    * Not implemented - returns null
648    */
649   @Override
650   public String eval(String strEval)
651   {
652     return null;
653   }
654
655   /**
656    * Not implemented - returns null
657    */
658   @Override
659   public float[][] functionXY(String functionName, int x, int y)
660   {
661     return null;
662   }
663
664   /**
665    * Not implemented - returns null
666    */
667   @Override
668   public float[][][] functionXYZ(String functionName, int nx, int ny,
669           int nz)
670   {
671     return null;
672   }
673
674   /**
675    * Not implemented - returns null
676    */
677   @Override
678   public String createImage(String fileName, String imageType,
679           Object text_or_bytes, int quality)
680   {
681     return null;
682   }
683
684   /**
685    * Not implemented - returns null
686    */
687   @Override
688   public Map<String, Object> getRegistryInfo()
689   {
690     return null;
691   }
692
693   /**
694    * Not implemented
695    */
696   @Override
697   public void showUrl(String url)
698   {
699   }
700
701   /**
702    * Not implemented - returns null
703    */
704   @Override
705   public int[] resizeInnerPanel(String data)
706   {
707     return null;
708   }
709
710   @Override
711   public Map<String, Object> getJSpecViewProperty(String arg0)
712   {
713     return null;
714   }
715
716   public boolean isPredictSecondaryStructure()
717   {
718     return predictSecondaryStructure;
719   }
720
721   public void setPredictSecondaryStructure(
722           boolean predictSecondaryStructure)
723   {
724     this.predictSecondaryStructure = predictSecondaryStructure;
725   }
726
727   public boolean isVisibleChainAnnotation()
728   {
729     return visibleChainAnnotation;
730   }
731
732   public void setVisibleChainAnnotation(boolean visibleChainAnnotation)
733   {
734     this.visibleChainAnnotation = visibleChainAnnotation;
735   }
736
737 }