Merge branch 'JAL-3878_ws-overhaul-3' into with_ws_overhaul-3
[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 import jalview.api.structures.JalviewStructureDisplayI;
30 import jalview.bin.Cache;
31 import jalview.bin.Console;
32 import jalview.datamodel.PDBEntry;
33 import jalview.datamodel.SequenceI;
34 import jalview.datamodel.StructureViewerModel;
35 import jalview.structure.StructureMapping;
36 import jalview.structure.StructureSelectionManager;
37 import jalview.util.MessageManager;
38 import jalview.util.Platform;
39 import jalview.ws.DBRefFetcher;
40 import jalview.ws.seqfetcher.DbSourceProxy;
41 import jalview.ws.sifts.SiftsSettings;
42
43
44
45 /**
46  * A proxy for handling structure viewers, that orchestrates adding selected
47  * structures, associated with sequences in Jalview, to an existing viewer, or
48  * opening a new one. Currently supports either Jmol or Chimera as the structure
49  * viewer.
50  * 
51  * @author jprocter
52  */
53 public class StructureViewer
54 {
55   
56   static
57   {
58     Platform.loadStaticResource("core/core_jvjmol.z.js",
59             "org.jmol.viewer.Viewer");
60   }
61
62
63   
64   
65   private static final String UNKNOWN_VIEWER_TYPE = "Unknown structure viewer type ";
66
67   StructureSelectionManager ssm;
68
69   /**
70    * decide if new structures are aligned to existing ones
71    */
72   private boolean superposeAdded = true;
73
74   public enum ViewerType
75   {
76     JMOL, CHIMERA, CHIMERAX, PYMOL
77   };
78
79   /**
80    * Constructor
81    * 
82    * @param structureSelectionManager
83    */
84   public StructureViewer(
85           StructureSelectionManager structureSelectionManager)
86   {
87     ssm = structureSelectionManager;
88   }
89
90   /**
91    * Factory to create a proxy for modifying existing structure viewer
92    * 
93    */
94   public static StructureViewer reconfigure(
95           JalviewStructureDisplayI display)
96   {
97     StructureViewer sv = new StructureViewer(display.getBinding().getSsm());
98     sv.sview = display;
99     return sv;
100   }
101
102   @Override
103   public String toString()
104   {
105     if (sview != null)
106     {
107       return sview.toString();
108     }
109     return "New View";
110   }
111
112   /**
113    * 
114    * @return ViewerType for currently configured structure viewer
115    */
116   public static ViewerType getViewerType()
117   {
118     String viewType = Cache.getDefault(Preferences.STRUCTURE_DISPLAY,
119             ViewerType.JMOL.name());
120     return ViewerType.valueOf(viewType);
121   }
122
123   public void setViewerType(ViewerType type)
124   {
125     Cache.setProperty(Preferences.STRUCTURE_DISPLAY, type.name());
126   }
127
128   /**
129    * View multiple PDB entries, each with associated sequences
130    * 
131    * @param pdbs
132    * @param seqs
133    * @param ap
134    * @return
135    */
136   public JalviewStructureDisplayI viewStructures(PDBEntry[] pdbs,
137           SequenceI[] seqs, AlignmentPanel ap)
138   {
139     JalviewStructureDisplayI viewer = onlyOnePdb(pdbs, seqs, ap);
140     if (viewer != null)
141     {
142       /*
143        * user added structure to an existing viewer - all done
144        */
145       return viewer;
146     }
147
148     ViewerType 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       new Thread(new Runnable()
160       {
161         @Override
162         public void run()
163         {
164
165           for (int pdbep = 0; pdbep < pdbsForFile.length; pdbep++)
166           {
167             PDBEntry pdb = pdbsForFile[pdbep];
168             if (!sview.addAlreadyLoadedFile(theSeqs[pdbep], null, ap,
169                     pdb.getId()))
170             {
171               sview.addToExistingViewer(pdb, theSeqs[pdbep], null, ap,
172                       pdb.getId());
173             }
174           }
175
176           sview.updateTitleAndMenus();
177         }
178       }).start();
179       return sview;
180     }
181
182     if (viewerType.equals(ViewerType.JMOL))
183     {
184       sview = new AppJmol(ap, superposeAdded, pdbsForFile, theSeqs);
185     }
186     else if (viewerType.equals(ViewerType.CHIMERA))
187     {
188       sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs,
189               ap);
190     }
191     else if (viewerType.equals(ViewerType.CHIMERAX))
192     {
193       sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs,
194               ap);
195     }
196     else if (viewerType.equals(ViewerType.PYMOL))
197     {
198       sview = new PymolViewer(pdbsForFile, superposeAdded, theSeqs, ap);
199     }
200     else
201     {
202       Console.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
203     }
204     return sview;
205   }
206
207   /**
208    * Converts the list of selected PDB entries (possibly including duplicates
209    * for multiple chains), and corresponding sequences, into a map of sequences
210    * for each distinct PDB file. Returns null if either argument is null, or
211    * their lengths do not match.
212    * 
213    * @param pdbs
214    * @param seqs
215    * @return
216    */
217   Map<PDBEntry, SequenceI[]> getSequencesForPdbs(PDBEntry[] pdbs,
218           SequenceI[] seqs)
219   {
220     if (pdbs == null || seqs == null || pdbs.length != seqs.length)
221     {
222       return null;
223     }
224
225     /*
226      * we want only one 'representative' PDBEntry per distinct file name
227      * (there may be entries for distinct chains)
228      */
229     Map<String, PDBEntry> pdbsSeen = new HashMap<>();
230
231     /*
232      * LinkedHashMap preserves order of PDB entries (significant if they
233      * will get superimposed to the first structure)
234      */
235     Map<PDBEntry, List<SequenceI>> pdbSeqs = new LinkedHashMap<>();
236     for (int i = 0; i < pdbs.length; i++)
237     {
238       PDBEntry pdb = pdbs[i];
239       SequenceI seq = seqs[i];
240       String pdbFile = pdb.getFile();
241       if (pdbFile == null || pdbFile.length() == 0)
242       {
243         pdbFile = pdb.getId();
244       }
245       if (!pdbsSeen.containsKey(pdbFile))
246       {
247         pdbsSeen.put(pdbFile, pdb);
248         pdbSeqs.put(pdb, new ArrayList<SequenceI>());
249       }
250       else
251       {
252         pdb = pdbsSeen.get(pdbFile);
253       }
254       List<SequenceI> seqsForPdb = pdbSeqs.get(pdb);
255       if (!seqsForPdb.contains(seq))
256       {
257         seqsForPdb.add(seq);
258       }
259     }
260
261     /*
262      * convert to Map<PDBEntry, SequenceI[]>
263      */
264     Map<PDBEntry, SequenceI[]> result = new LinkedHashMap<>();
265     for (Entry<PDBEntry, List<SequenceI>> entry : pdbSeqs.entrySet())
266     {
267       List<SequenceI> theSeqs = entry.getValue();
268       result.put(entry.getKey(),
269               theSeqs.toArray(new SequenceI[theSeqs.size()]));
270     }
271
272     return result;
273   }
274
275   /**
276    * A strictly temporary method pending JAL-1761 refactoring. Determines if all
277    * the passed PDB entries are the same (this is the case if selected sequences
278    * to view structure for are chains of the same structure). If so, calls the
279    * single-pdb version of viewStructures and returns the viewer, else returns
280    * null.
281    * 
282    * @param pdbs
283    * @param seqsForPdbs
284    * @param ap
285    * @return
286    */
287   private JalviewStructureDisplayI onlyOnePdb(PDBEntry[] pdbs,
288           SequenceI[] seqsForPdbs, AlignmentPanel ap)
289   {
290     List<SequenceI> seqs = new ArrayList<>();
291     if (pdbs == null || pdbs.length == 0)
292     {
293       return null;
294     }
295     int i = 0;
296     String firstFile = pdbs[0].getFile();
297     for (PDBEntry pdb : pdbs)
298     {
299       String pdbFile = pdb.getFile();
300       if (pdbFile == null || !pdbFile.equals(firstFile))
301       {
302         return null;
303       }
304       SequenceI pdbseq = seqsForPdbs[i++];
305       if (pdbseq != null)
306       {
307         seqs.add(pdbseq);
308       }
309     }
310     return viewStructures(pdbs[0], seqs.toArray(new SequenceI[seqs.size()]),
311             ap);
312   }
313
314   JalviewStructureDisplayI sview = null;
315
316   public JalviewStructureDisplayI viewStructures(PDBEntry pdb,
317           SequenceI[] seqsForPdb, AlignmentPanel ap)
318   {
319     if (sview != null)
320     {
321       sview.setAlignAddedStructures(superposeAdded);
322       String pdbId = pdb.getId();
323       if (!sview.addAlreadyLoadedFile(seqsForPdb, null, ap, pdbId))
324       {
325         sview.addToExistingViewer(pdb, seqsForPdb, null, ap, pdbId);
326       }
327       sview.updateTitleAndMenus();
328       sview.raiseViewer();
329       return sview;
330     }
331     ViewerType viewerType = getViewerType();
332     if (viewerType.equals(ViewerType.JMOL))
333     {
334       sview = new AppJmol(pdb, seqsForPdb, null, ap);
335     }
336     else if (viewerType.equals(ViewerType.CHIMERA))
337     {
338       sview = new ChimeraViewFrame(pdb, seqsForPdb, null, ap);
339     }
340     else if (viewerType.equals(ViewerType.CHIMERAX))
341     {
342       sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap);
343     }
344     else if (viewerType.equals(ViewerType.PYMOL))
345     {
346       sview = new PymolViewer(pdb, seqsForPdb, null, ap);
347     }
348     else
349     {
350       Console.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
351     }
352     return sview;
353   }
354
355   /**
356    * Creates a new panel controlling a structure viewer
357    * 
358    * @param type
359    * @param pdbf
360    * @param id
361    * @param sq
362    * @param alignPanel
363    * @param viewerData
364    * @param sessionFile
365    * @param vid
366    * @return
367    */
368   public static JalviewStructureDisplayI createView(ViewerType type,
369           AlignmentPanel alignPanel, StructureViewerModel viewerData,
370           String sessionFile, String vid)
371   {
372     JalviewStructureDisplayI viewer = null;
373
374     switch (type)
375     {
376     case JMOL:
377       viewer = new AppJmol(viewerData, alignPanel, sessionFile, vid);
378       // todo or construct and then openSession(sessionFile)?
379       break;
380     case CHIMERA:
381       viewer = new ChimeraViewFrame(viewerData, alignPanel, sessionFile,
382               vid);
383       break;
384     case CHIMERAX:
385       viewer = new ChimeraXViewFrame(viewerData, alignPanel, sessionFile,
386               vid);
387       break;
388     case PYMOL:
389       viewer = new PymolViewer(viewerData, alignPanel, sessionFile, vid);
390       break;
391     default:
392       Console.error(UNKNOWN_VIEWER_TYPE + type.toString());
393     }
394     return viewer;
395   }
396
397   public boolean isBusy()
398   {
399     if (sview != null)
400     {
401       if (!sview.hasMapping())
402       {
403         return true;
404       }
405     }
406     return false;
407   }
408
409   /**
410    * 
411    * @param pDBid
412    * @return true if view is already showing PDBid
413    */
414   public boolean hasPdbId(String pDBid)
415   {
416     if (sview == null)
417     {
418       return false;
419     }
420
421     return sview.getBinding().hasPdbId(pDBid);
422   }
423
424   public boolean isVisible()
425   {
426     return sview != null && sview.isVisible();
427   }
428
429   public void setSuperpose(boolean alignAddedStructures)
430   {
431     superposeAdded = alignAddedStructures;
432   }
433
434   /**
435    * Launch a minimal implementation of a StructureViewer.
436    * 
437    * @param alignPanel
438    * @param pdb
439    * @param seqs
440    * @return
441    */
442   public static StructureViewer launchStructureViewer(
443           AlignmentPanel alignPanel, PDBEntry pdb, SequenceI[] seqs)
444   {
445     return launchStructureViewer(alignPanel, new PDBEntry[] { pdb }, seqs,
446             false, null, null);
447   }
448
449   /**
450    * Adds PDB structures to a new or existing structure viewer
451    * 
452    * @param ssm
453    * @param pdbEntriesToView
454    * @param alignPanel
455    * @param sequences
456    * @return
457    */
458   protected static StructureViewer launchStructureViewer(
459           final AlignmentPanel ap, final PDBEntry[] pdbEntriesToView,
460           SequenceI[] sequences, boolean superimpose,
461           StructureViewer theViewer, IProgressIndicator pb)
462   {
463     final StructureSelectionManager ssm = ap.getStructureSelectionManager();
464     if (theViewer == null)
465       theViewer = new StructureViewer(ssm);
466     long progressId = sequences.hashCode();
467     if (pb != null)
468       pb.setProgressBar(MessageManager.getString(
469               "status.launching_3d_structure_viewer"), progressId);
470     theViewer.setSuperpose(superimpose);
471   
472     /*
473      * remember user's choice of superimpose or not
474      */
475     Cache.setProperty(StructureChooser.AUTOSUPERIMPOSE,
476             Boolean.valueOf(superimpose).toString());
477   
478     if (pb != null)
479       pb.setProgressBar(null, progressId);
480     if (SiftsSettings.isMapWithSifts())
481     {
482       List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
483       int p = 0;
484       // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
485       // real PDB ID. For moment, we can also safely do this if there is already
486       // a known mapping between the PDBEntry and the sequence.
487       for (SequenceI seq : sequences)
488       {
489         PDBEntry pdbe = pdbEntriesToView[p++];
490         if (pdbe != null && pdbe.getFile() != null)
491         {
492           StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
493           if (smm != null && smm.length > 0)
494           {
495             for (StructureMapping sm : smm)
496             {
497               if (sm.getSequence() == seq)
498               {
499                 continue;
500               }
501             }
502           }
503         }
504         if (seq.getPrimaryDBRefs().isEmpty())
505         {
506           seqsWithoutSourceDBRef.add(seq);
507           continue;
508         }
509       }
510       if (!seqsWithoutSourceDBRef.isEmpty())
511       {
512         int y = seqsWithoutSourceDBRef.size();
513         if (pb != null)
514           pb.setProgressBar(MessageManager.formatMessage(
515                   "status.fetching_dbrefs_for_sequences_without_valid_refs",
516                   y), progressId);
517         SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
518                 .toArray(new SequenceI[y]);
519         DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
520         dbRefFetcher.fetchDBRefs(true);
521   
522         if (pb != null)
523           pb.setProgressBar("Fetch complete.", progressId); // todo i18n
524       }
525     }
526     if (pdbEntriesToView.length > 1)
527     {
528       if (pb != null)
529         pb.setProgressBar(MessageManager.getString(
530                 "status.fetching_3d_structures_for_selected_entries"),
531                 progressId);
532       theViewer.viewStructures(pdbEntriesToView, sequences, ap);
533     }
534     else
535     {
536       if (pb != null)
537         pb.setProgressBar(MessageManager.formatMessage(
538                 "status.fetching_3d_structures_for",
539                 pdbEntriesToView[0].getId()), progressId);
540       theViewer.viewStructures(pdbEntriesToView[0], sequences, ap);
541     }
542     if (pb != null)
543       pb.setProgressBar(null, progressId);
544     // remember the last viewer we used...
545     StructureChooser.lastTargetedView = theViewer;
546     return theViewer;
547   }
548
549 }