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