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