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