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