JAL-1551 spotlessApply
[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         Alignment al = new Alignment(prot.toArray(new SequenceI[0]));
319         EBIAlfaFold.addAlphaFoldPAE(al, new File(this.getPAEMatrix()), 0,
320                 null, false, false, null);
321
322         if (al.getAlignmentAnnotation() != null)
323         {
324           for (AlignmentAnnotation alann : al.getAlignmentAnnotation())
325           {
326             annotations.add(alann);
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 }