Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / gui / StructureViewer.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.gui;
22
23 import java.util.ArrayList;
24 import java.util.EnumSet;
25 import java.util.HashMap;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.Map.Entry;
31
32 import jalview.api.structures.JalviewStructureDisplayI;
33 import jalview.bin.Cache;
34 import jalview.bin.Console;
35 import jalview.datamodel.PDBEntry;
36 import jalview.datamodel.SequenceI;
37 import jalview.datamodel.StructureViewerModel;
38 import jalview.structure.StructureSelectionManager;
39
40 /**
41  * A proxy for handling structure viewers, that orchestrates adding selected
42  * structures, associated with sequences in Jalview, to an existing viewer, or
43  * opening a new one. Currently supports either Jmol or Chimera as the structure
44  * viewer.
45  * 
46  * @author jprocter
47  */
48 public class StructureViewer
49 {
50   private static final String UNKNOWN_VIEWER_TYPE = "Unknown structure viewer type ";
51
52   StructureSelectionManager ssm;
53
54   /**
55    * decide if new structures are aligned to existing ones
56    */
57   private boolean superposeAdded = true;
58
59   /**
60    * whether to open structures in their own thread or not
61    */
62   private boolean async = true;
63
64   public void setAsync(boolean b)
65   {
66     async = b;
67   }
68
69   public enum ViewerType
70   {
71     JMOL, CHIMERA, CHIMERAX, PYMOL;
72
73     public static ViewerType getFromString(String viewerString)
74     {
75       ViewerType viewerType = null;
76       if (!"none".equals(viewerString))
77       {
78         for (ViewerType v : EnumSet.allOf(ViewerType.class))
79         {
80           String name = v.name().toLowerCase(Locale.ROOT).replaceAll(" ",
81                   "");
82           if (viewerString.equals(name))
83           {
84             viewerType = v;
85             break;
86           }
87         }
88       }
89       return viewerType;
90     }
91
92   };
93
94   /**
95    * Constructor
96    * 
97    * @param structureSelectionManager
98    */
99   public StructureViewer(
100           StructureSelectionManager structureSelectionManager)
101   {
102     ssm = structureSelectionManager;
103   }
104
105   /**
106    * Factory to create a proxy for modifying existing structure viewer
107    * 
108    */
109   public static StructureViewer reconfigure(
110           JalviewStructureDisplayI display)
111   {
112     StructureViewer sv = new StructureViewer(display.getBinding().getSsm());
113     sv.sview = display;
114     return sv;
115   }
116
117   @Override
118   public String toString()
119   {
120     if (sview != null)
121     {
122       return sview.toString();
123     }
124     return "New View";
125   }
126
127   /**
128    * 
129    * @return ViewerType for currently configured structure viewer
130    */
131   public static ViewerType getViewerType()
132   {
133     String viewType = Cache.getDefault(Preferences.STRUCTURE_DISPLAY,
134             ViewerType.JMOL.name());
135     return ViewerType.valueOf(viewType);
136   }
137
138   public void setViewerType(ViewerType type)
139   {
140     Cache.setProperty(Preferences.STRUCTURE_DISPLAY, type.name());
141   }
142
143   /**
144    * View multiple PDB entries, each with associated sequences
145    * 
146    * @param pdbs
147    * @param seqs
148    * @param ap
149    * @return
150    */
151   public JalviewStructureDisplayI viewStructures(PDBEntry[] pdbs,
152           SequenceI[] seqs, AlignmentPanel ap)
153   {
154     return viewStructures(pdbs, seqs, ap, null);
155   }
156
157   public JalviewStructureDisplayI viewStructures(PDBEntry[] pdbs,
158           SequenceI[] seqs, AlignmentPanel ap, ViewerType viewerType)
159   {
160     JalviewStructureDisplayI viewer = onlyOnePdb(pdbs, seqs, ap);
161     if (viewer != null)
162     {
163       /*
164        * user added structure to an existing viewer - all done
165        */
166       return viewer;
167     }
168
169     if (viewerType == null)
170       viewerType = getViewerType();
171
172     Map<PDBEntry, SequenceI[]> seqsForPdbs = getSequencesForPdbs(pdbs,
173             seqs);
174     PDBEntry[] pdbsForFile = seqsForPdbs.keySet()
175             .toArray(new PDBEntry[seqsForPdbs.size()]);
176     SequenceI[][] theSeqs = seqsForPdbs.values()
177             .toArray(new SequenceI[seqsForPdbs.size()][]);
178     if (sview != null)
179     {
180       sview.setAlignAddedStructures(superposeAdded);
181
182       Runnable viewRunnable = new Runnable()
183       {
184         @Override
185         public void run()
186         {
187
188           for (int pdbep = 0; pdbep < pdbsForFile.length; pdbep++)
189           {
190             PDBEntry pdb = pdbsForFile[pdbep];
191             if (!sview.addAlreadyLoadedFile(theSeqs[pdbep], null, ap,
192                     pdb.getId()))
193             {
194               sview.addToExistingViewer(pdb, theSeqs[pdbep], null, ap,
195                       pdb.getId());
196             }
197           }
198
199           sview.updateTitleAndMenus();
200         }
201       };
202       if (async)
203       {
204         new Thread(viewRunnable).start();
205       }
206       else
207       {
208         viewRunnable.run();
209       }
210       return sview;
211     }
212
213     if (viewerType.equals(ViewerType.JMOL))
214     {
215       sview = new AppJmol(ap, superposeAdded, pdbsForFile, theSeqs);
216     }
217     else if (viewerType.equals(ViewerType.CHIMERA))
218     {
219       sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs,
220               ap);
221     }
222     else if (viewerType.equals(ViewerType.CHIMERAX))
223     {
224       sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs,
225               ap);
226     }
227     else if (viewerType.equals(ViewerType.PYMOL))
228     {
229       sview = new PymolViewer(pdbsForFile, superposeAdded, theSeqs, ap);
230     }
231     else
232     {
233       Console.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
234     }
235     return sview;
236   }
237
238   /**
239    * Converts the list of selected PDB entries (possibly including duplicates
240    * for multiple chains), and corresponding sequences, into a map of sequences
241    * for each distinct PDB file. Returns null if either argument is null, or
242    * their lengths do not match.
243    * 
244    * @param pdbs
245    * @param seqs
246    * @return
247    */
248   Map<PDBEntry, SequenceI[]> getSequencesForPdbs(PDBEntry[] pdbs,
249           SequenceI[] seqs)
250   {
251     if (pdbs == null || seqs == null || pdbs.length != seqs.length)
252     {
253       return null;
254     }
255
256     /*
257      * we want only one 'representative' PDBEntry per distinct file name
258      * (there may be entries for distinct chains)
259      */
260     Map<String, PDBEntry> pdbsSeen = new HashMap<>();
261
262     /*
263      * LinkedHashMap preserves order of PDB entries (significant if they
264      * will get superimposed to the first structure)
265      */
266     Map<PDBEntry, List<SequenceI>> pdbSeqs = new LinkedHashMap<>();
267     for (int i = 0; i < pdbs.length; i++)
268     {
269       PDBEntry pdb = pdbs[i];
270       SequenceI seq = seqs[i];
271       String pdbFile = pdb.getFile();
272       if (pdbFile == null || pdbFile.length() == 0)
273       {
274         pdbFile = pdb.getId();
275       }
276       if (!pdbsSeen.containsKey(pdbFile))
277       {
278         pdbsSeen.put(pdbFile, pdb);
279         pdbSeqs.put(pdb, new ArrayList<SequenceI>());
280       }
281       else
282       {
283         pdb = pdbsSeen.get(pdbFile);
284       }
285       List<SequenceI> seqsForPdb = pdbSeqs.get(pdb);
286       if (!seqsForPdb.contains(seq))
287       {
288         seqsForPdb.add(seq);
289       }
290     }
291
292     /*
293      * convert to Map<PDBEntry, SequenceI[]>
294      */
295     Map<PDBEntry, SequenceI[]> result = new LinkedHashMap<>();
296     for (Entry<PDBEntry, List<SequenceI>> entry : pdbSeqs.entrySet())
297     {
298       List<SequenceI> theSeqs = entry.getValue();
299       result.put(entry.getKey(),
300               theSeqs.toArray(new SequenceI[theSeqs.size()]));
301     }
302
303     return result;
304   }
305
306   /**
307    * A strictly temporary method pending JAL-1761 refactoring. Determines if all
308    * the passed PDB entries are the same (this is the case if selected sequences
309    * to view structure for are chains of the same structure). If so, calls the
310    * single-pdb version of viewStructures and returns the viewer, else returns
311    * null.
312    * 
313    * @param pdbs
314    * @param seqsForPdbs
315    * @param ap
316    * @return
317    */
318   private JalviewStructureDisplayI onlyOnePdb(PDBEntry[] pdbs,
319           SequenceI[] seqsForPdbs, AlignmentPanel ap)
320   {
321     List<SequenceI> seqs = new ArrayList<>();
322     if (pdbs == null || pdbs.length == 0)
323     {
324       return null;
325     }
326     int i = 0;
327     String firstFile = pdbs[0].getFile();
328     for (PDBEntry pdb : pdbs)
329     {
330       String pdbFile = pdb.getFile();
331       if (pdbFile == null || !pdbFile.equals(firstFile))
332       {
333         return null;
334       }
335       SequenceI pdbseq = seqsForPdbs[i++];
336       if (pdbseq != null)
337       {
338         seqs.add(pdbseq);
339       }
340     }
341     return viewStructures(pdbs[0], seqs.toArray(new SequenceI[seqs.size()]),
342             ap);
343   }
344
345   JalviewStructureDisplayI sview = null;
346
347   public JalviewStructureDisplayI getJalviewStructureDisplay()
348   {
349     return sview;
350   }
351
352   public JalviewStructureDisplayI viewStructures(PDBEntry pdb,
353           SequenceI[] seqsForPdb, AlignmentPanel ap)
354   {
355     return viewStructures(pdb, seqsForPdb, ap, null);
356   }
357
358   public JalviewStructureDisplayI viewStructures(PDBEntry pdb,
359           SequenceI[] seqsForPdb, AlignmentPanel ap, ViewerType viewerType)
360   {
361     if (sview != null)
362     {
363       sview.setAlignAddedStructures(superposeAdded);
364       String pdbId = pdb.getId();
365       if (!sview.addAlreadyLoadedFile(seqsForPdb, null, ap, pdbId))
366       {
367         sview.addToExistingViewer(pdb, seqsForPdb, null, ap, pdbId);
368       }
369       sview.updateTitleAndMenus();
370       sview.raiseViewer();
371       return sview;
372     }
373     if (viewerType == null)
374       viewerType = getViewerType();
375     if (viewerType.equals(ViewerType.JMOL))
376     {
377       sview = new AppJmol(pdb, seqsForPdb, null, ap);
378     }
379     else if (viewerType.equals(ViewerType.CHIMERA))
380     {
381       sview = new ChimeraViewFrame(pdb, seqsForPdb, null, ap);
382     }
383     else if (viewerType.equals(ViewerType.CHIMERAX))
384     {
385       sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap);
386     }
387     else if (viewerType.equals(ViewerType.PYMOL))
388     {
389       sview = new PymolViewer(pdb, seqsForPdb, null, ap);
390     }
391     else
392     {
393       Console.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
394     }
395     return sview;
396   }
397
398   /**
399    * Creates a new panel controlling a structure viewer
400    * 
401    * @param type
402    * @param alignPanel
403    * @param viewerData
404    * @param sessionFile
405    * @param vid
406    * @return
407    */
408   public static JalviewStructureDisplayI createView(ViewerType type,
409           AlignmentPanel alignPanel, StructureViewerModel viewerData,
410           String sessionFile, String vid)
411   {
412     JalviewStructureDisplayI viewer = null;
413     switch (type)
414     {
415     case JMOL:
416       viewer = new AppJmol(viewerData, alignPanel, sessionFile, vid);
417       // todo or construct and then openSession(sessionFile)?
418       break;
419     case CHIMERA:
420       viewer = new ChimeraViewFrame(viewerData, alignPanel, sessionFile,
421               vid);
422       break;
423     case CHIMERAX:
424       viewer = new ChimeraXViewFrame(viewerData, alignPanel, sessionFile,
425               vid);
426       break;
427     case PYMOL:
428       viewer = new PymolViewer(viewerData, alignPanel, sessionFile, vid);
429       break;
430     default:
431       Console.error(UNKNOWN_VIEWER_TYPE + type.toString());
432     }
433     return viewer;
434   }
435
436   public boolean isBusy()
437   {
438     if (sview != null)
439     {
440       if (!sview.hasMapping())
441       {
442         return true;
443       }
444     }
445     return false;
446   }
447
448   /**
449    * 
450    * @param pDBid
451    * @return true if view is already showing PDBid
452    */
453   public boolean hasPdbId(String pDBid)
454   {
455     if (sview == null)
456     {
457       return false;
458     }
459
460     return sview.getBinding().hasPdbId(pDBid);
461   }
462
463   public boolean isVisible()
464   {
465     return sview != null && sview.isVisible();
466   }
467
468   public void setSuperpose(boolean alignAddedStructures)
469   {
470     superposeAdded = alignAddedStructures;
471   }
472
473 }