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