b8d2cce838d85400166766ec0839453622c93e67
[jalview.git] / src / jalview / gui / StructureViewerBase.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.awt.Color;
24 import java.awt.Component;
25 import java.awt.event.ActionEvent;
26 import java.awt.event.ActionListener;
27 import java.awt.event.ItemEvent;
28 import java.awt.event.ItemListener;
29 import java.beans.PropertyVetoException;
30 import java.io.BufferedReader;
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.FileReader;
34 import java.io.IOException;
35 import java.io.PrintWriter;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Random;
39 import java.util.Vector;
40
41 import javax.swing.ButtonGroup;
42 import javax.swing.JCheckBoxMenuItem;
43 import javax.swing.JMenu;
44 import javax.swing.JMenuItem;
45 import javax.swing.JRadioButtonMenuItem;
46 import javax.swing.event.MenuEvent;
47 import javax.swing.event.MenuListener;
48
49 import jalview.api.AlignmentViewPanel;
50 import jalview.bin.Cache;
51 import jalview.bin.Console;
52 import jalview.datamodel.AlignmentI;
53 import jalview.datamodel.PDBEntry;
54 import jalview.datamodel.SequenceI;
55 import jalview.gui.JalviewColourChooser.ColourChooserListener;
56 import jalview.gui.StructureViewer.ViewerType;
57 import jalview.gui.ViewSelectionMenu.ViewSetProvider;
58 import jalview.io.DataSourceType;
59 import jalview.io.JalviewFileChooser;
60 import jalview.io.JalviewFileView;
61 import jalview.jbgui.GStructureViewer;
62 import jalview.schemes.ColourSchemeI;
63 import jalview.schemes.ColourSchemes;
64 import jalview.structure.StructureMapping;
65 import jalview.structures.models.AAStructureBindingModel;
66 import jalview.util.BrowserLauncher;
67 import jalview.util.MessageManager;
68 import jalview.ws.dbsources.EBIAlfaFold;
69 import jalview.ws.dbsources.Pdb;
70 import jalview.ws.utils.UrlDownloadClient;
71
72 /**
73  * Base class with common functionality for JMol, Chimera or other structure
74  * viewers.
75  * 
76  * @author gmcarstairs
77  *
78  */
79 public abstract class StructureViewerBase extends GStructureViewer
80         implements Runnable, ViewSetProvider
81 {
82   /*
83    * names for colour options (additional to Jalview colour schemes)
84    */
85   enum ViewerColour
86   {
87     BySequence, ByChain, ChargeCysteine, ByViewer
88   }
89
90   /**
91    * list of sequenceSet ids associated with the view
92    */
93   protected List<String> _aps = new ArrayList<>();
94
95   /**
96    * list of alignment panels to use for superposition
97    */
98   protected Vector<AlignmentViewPanel> _alignwith = new Vector<>();
99
100   /**
101    * list of alignment panels that are used for colouring structures by aligned
102    * sequences
103    */
104   protected Vector<AlignmentViewPanel> _colourwith = new Vector<>();
105
106   private String viewId = null;
107
108   private AlignmentPanel ap;
109
110   protected boolean alignAddedStructures = false;
111
112   protected volatile boolean _started = false;
113
114   protected volatile boolean addingStructures = false;
115
116   protected Thread worker = null;
117
118   protected boolean allChainsSelected = false;
119
120   protected JMenu viewSelectionMenu;
121
122   /**
123    * set after sequence colouring has been applied for this structure viewer.
124    * used to determine if the final sequence/structure mapping has been
125    * determined
126    */
127   protected volatile boolean seqColoursApplied = false;
128
129   private IProgressIndicator progressBar = null;
130
131   private Random random = new Random();
132
133   /**
134    * Default constructor
135    */
136   public StructureViewerBase()
137   {
138     super();
139     setFrameIcon(null);
140   }
141
142   /**
143    * @return true if added structures should be aligned to existing one(s)
144    */
145   @Override
146   public boolean isAlignAddedStructures()
147   {
148     return alignAddedStructures;
149   }
150
151   /**
152    * 
153    * @param true
154    *          if added structures should be aligned to existing one(s)
155    */
156   @Override
157   public void setAlignAddedStructures(boolean alignAdded)
158   {
159     alignAddedStructures = alignAdded;
160   }
161
162   /**
163    * called by the binding model to indicate when adding structures is happening
164    * or has been completed
165    * 
166    * @param addingStructures
167    */
168   public synchronized void setAddingStructures(boolean addingStructures)
169   {
170     this.addingStructures = addingStructures;
171   }
172
173   /**
174    * 
175    * @param ap2
176    * @return true if this Jmol instance is linked with the given alignPanel
177    */
178   public boolean isLinkedWith(AlignmentPanel ap2)
179   {
180     return _aps.contains(ap2.av.getSequenceSetId());
181   }
182
183   public boolean isUsedforaligment(AlignmentViewPanel ap2)
184   {
185
186     return (_alignwith != null) && _alignwith.contains(ap2);
187   }
188
189   @Override
190   public boolean isUsedForColourBy(AlignmentViewPanel ap2)
191   {
192     return (_colourwith != null) && _colourwith.contains(ap2);
193   }
194
195   /**
196    * 
197    * @return TRUE if the view is NOT being coloured by the alignment colours.
198    */
199   public boolean isColouredByViewer()
200   {
201     return !getBinding().isColourBySequence();
202   }
203
204   public String getViewId()
205   {
206     if (viewId == null)
207     {
208       viewId = System.currentTimeMillis() + "." + this.hashCode();
209     }
210     return viewId;
211   }
212
213   protected void setViewId(String viewId)
214   {
215     this.viewId = viewId;
216   }
217
218   protected void buildActionMenu()
219   {
220     if (_alignwith == null)
221     {
222       _alignwith = new Vector<>();
223     }
224     if (_alignwith.size() == 0 && ap != null)
225     {
226       _alignwith.add(ap);
227     }
228     ;
229     // TODO: refactor to allow concrete classes to register buttons for adding
230     // here
231     // currently have to override to add buttons back in after they are cleared
232     // in this loop
233     for (Component c : viewerActionMenu.getMenuComponents())
234     {
235       if (c != alignStructs)
236       {
237         viewerActionMenu.remove((JMenuItem) c);
238       }
239     }
240   }
241
242   @Override
243   public AlignmentPanel getAlignmentPanel()
244   {
245     return ap;
246   }
247
248   protected void setAlignmentPanel(AlignmentPanel alp)
249   {
250     this.ap = alp;
251   }
252
253   @Override
254   public AlignmentPanel[] getAllAlignmentPanels()
255   {
256     AlignmentPanel[] t, list = new AlignmentPanel[0];
257     for (String setid : _aps)
258     {
259       AlignmentPanel[] panels = PaintRefresher.getAssociatedPanels(setid);
260       if (panels != null)
261       {
262         t = new AlignmentPanel[list.length + panels.length];
263         System.arraycopy(list, 0, t, 0, list.length);
264         System.arraycopy(panels, 0, t, list.length, panels.length);
265         list = t;
266       }
267     }
268
269     return list;
270   }
271
272   /**
273    * set the primary alignmentPanel reference and add another alignPanel to the
274    * list of ones to use for colouring and aligning
275    * 
276    * @param nap
277    */
278   public void addAlignmentPanel(AlignmentPanel nap)
279   {
280     if (getAlignmentPanel() == null)
281     {
282       setAlignmentPanel(nap);
283     }
284     if (!_aps.contains(nap.av.getSequenceSetId()))
285     {
286       _aps.add(nap.av.getSequenceSetId());
287     }
288   }
289
290   /**
291    * remove any references held to the given alignment panel
292    * 
293    * @param nap
294    */
295   @Override
296   public void removeAlignmentPanel(AlignmentViewPanel nap)
297   {
298     try
299     {
300       _alignwith.remove(nap);
301       _colourwith.remove(nap);
302       if (getAlignmentPanel() == nap)
303       {
304         setAlignmentPanel(null);
305         for (AlignmentPanel aps : getAllAlignmentPanels())
306         {
307           if (aps != nap)
308           {
309             setAlignmentPanel(aps);
310             break;
311           }
312         }
313       }
314     } catch (Exception ex)
315     {
316     }
317     if (getAlignmentPanel() != null)
318     {
319       buildActionMenu();
320     }
321   }
322
323   public void useAlignmentPanelForSuperposition(AlignmentPanel nap)
324   {
325     addAlignmentPanel(nap);
326     if (!_alignwith.contains(nap))
327     {
328       _alignwith.add(nap);
329     }
330   }
331
332   public void excludeAlignmentPanelForSuperposition(AlignmentPanel nap)
333   {
334     if (_alignwith.contains(nap))
335     {
336       _alignwith.remove(nap);
337     }
338   }
339
340   public void useAlignmentPanelForColourbyseq(AlignmentPanel nap,
341           boolean enableColourBySeq)
342   {
343     useAlignmentPanelForColourbyseq(nap);
344     getBinding().setColourBySequence(enableColourBySeq);
345     seqColour.setSelected(enableColourBySeq);
346     viewerColour.setSelected(!enableColourBySeq);
347   }
348
349   public void useAlignmentPanelForColourbyseq(AlignmentPanel nap)
350   {
351     addAlignmentPanel(nap);
352     if (!_colourwith.contains(nap))
353     {
354       _colourwith.add(nap);
355     }
356   }
357
358   public void excludeAlignmentPanelForColourbyseq(AlignmentPanel nap)
359   {
360     if (_colourwith.contains(nap))
361     {
362       _colourwith.remove(nap);
363     }
364   }
365
366   public abstract ViewerType getViewerType();
367
368   /**
369    * add a new structure (with associated sequences and chains) to this viewer,
370    * retrieving it if necessary first.
371    * 
372    * @param pdbentry
373    * @param seqs
374    * @param chains
375    * @param align
376    *          if true, new structure(s) will be aligned using associated
377    *          alignment
378    * @param alignFrame
379    */
380   protected void addStructure(final PDBEntry pdbentry,
381           final SequenceI[] seqs, final String[] chains,
382           final IProgressIndicator alignFrame)
383   {
384     if (pdbentry.getFile() == null)
385     {
386       if (worker != null && worker.isAlive())
387       {
388         // a retrieval is in progress, wait around and add ourselves to the
389         // queue.
390         new Thread(new Runnable()
391         {
392           @Override
393           public void run()
394           {
395             while (worker != null && worker.isAlive() && _started)
396             {
397               try
398               {
399                 Thread.sleep(100 + ((int) Math.random() * 100));
400
401               } catch (Exception e)
402               {
403               }
404             }
405             // and call ourselves again.
406             addStructure(pdbentry, seqs, chains, alignFrame);
407           }
408         }).start();
409         return;
410       }
411     }
412     // otherwise, start adding the structure.
413     getBinding().addSequenceAndChain(new PDBEntry[] { pdbentry },
414             new SequenceI[][]
415             { seqs }, new String[][] { chains });
416     addingStructures = true;
417     _started = false;
418     worker = new Thread(this);
419     worker.start();
420     return;
421   }
422
423   protected boolean hasPdbId(String pdbId)
424   {
425     return getBinding().hasPdbId(pdbId);
426   }
427
428   /**
429    * Returns a list of any viewer of the instantiated type. The list is
430    * restricted to those linked to the given alignment panel if it is not null.
431    */
432   protected List<StructureViewerBase> getViewersFor(AlignmentPanel alp)
433   {
434     return Desktop.instance.getStructureViewers(alp, this.getClass());
435   }
436
437   @Override
438   public void addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
439           String[] chains, final AlignmentViewPanel apanel, String pdbId)
440   {
441     /*
442      * JAL-1742 exclude view with this structure already mapped (don't offer
443      * to align chain B to chain A of the same structure); code may defend
444      * against this possibility before we reach here
445      */
446     if (hasPdbId(pdbId))
447     {
448       return;
449     }
450     AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error
451                                                          // if this
452     // cast fails
453     useAlignmentPanelForSuperposition(alignPanel);
454     addStructure(pdbentry, seq, chains, alignPanel.alignFrame);
455   }
456
457   /**
458    * Adds mappings for the given sequences to an already opened PDB structure,
459    * and updates any viewers that have the PDB file
460    * 
461    * @param seq
462    * @param chains
463    * @param apanel
464    * @param pdbFilename
465    */
466   public void addSequenceMappingsToStructure(SequenceI[] seq,
467           String[] chains, final AlignmentViewPanel alpanel,
468           String pdbFilename)
469   {
470     AlignmentPanel apanel = (AlignmentPanel) alpanel;
471
472     // TODO : Fix multiple seq to one chain issue here.
473     /*
474      * create the mappings
475      */
476     apanel.getStructureSelectionManager().setMapping(seq, chains,
477             pdbFilename, DataSourceType.FILE, getProgressIndicator());
478
479     /*
480      * alert the FeatureRenderer to show new (PDB RESNUM) features
481      */
482     if (apanel.getSeqPanel().seqCanvas.fr != null)
483     {
484       apanel.getSeqPanel().seqCanvas.fr.featuresAdded();
485       // note - we don't do a refresh for structure here because we do it
486       // explicitly for all panels later on
487       apanel.paintAlignment(true, false);
488     }
489
490     /*
491      * add the sequences to any other viewers (of the same type) for this pdb
492      * file
493      */
494     // JBPNOTE: this looks like a binding routine, rather than a gui routine
495     for (StructureViewerBase viewer : getViewersFor(null))
496     {
497       AAStructureBindingModel bindingModel = viewer.getBinding();
498       for (int pe = 0; pe < bindingModel.getPdbCount(); pe++)
499       {
500         if (bindingModel.getPdbEntry(pe).getFile().equals(pdbFilename))
501         {
502           bindingModel.addSequence(pe, seq);
503           viewer.addAlignmentPanel(apanel);
504           /*
505            * add it to the set of alignments used for colouring structure by
506            * sequence
507            */
508           viewer.useAlignmentPanelForColourbyseq(apanel);
509           viewer.buildActionMenu();
510           apanel.getStructureSelectionManager()
511                   .sequenceColoursChanged(apanel);
512           break;
513         }
514       }
515     }
516   }
517
518   @Override
519   public boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
520           final AlignmentViewPanel apanel, String pdbId)
521   {
522     String alreadyMapped = apanel.getStructureSelectionManager()
523             .alreadyMappedToFile(pdbId);
524
525     if (alreadyMapped == null)
526     {
527       return false;
528     }
529
530     addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
531     return true;
532   }
533
534   void setChainMenuItems(List<String> chainNames)
535   {
536     chainMenu.removeAll();
537     if (chainNames == null || chainNames.isEmpty())
538     {
539       return;
540     }
541     JMenuItem menuItem = new JMenuItem(
542             MessageManager.getString("label.all"));
543     menuItem.addActionListener(new ActionListener()
544     {
545       @Override
546       public void actionPerformed(ActionEvent evt)
547       {
548         allChainsSelected = true;
549         for (int i = 0; i < chainMenu.getItemCount(); i++)
550         {
551           if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
552           {
553             ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
554           }
555         }
556         showSelectedChains();
557         allChainsSelected = false;
558       }
559     });
560
561     chainMenu.add(menuItem);
562
563     for (String chain : chainNames)
564     {
565       menuItem = new JCheckBoxMenuItem(chain, true);
566       menuItem.addItemListener(new ItemListener()
567       {
568         @Override
569         public void itemStateChanged(ItemEvent evt)
570         {
571           if (!allChainsSelected)
572           {
573             showSelectedChains();
574           }
575         }
576       });
577
578       chainMenu.add(menuItem);
579     }
580   }
581
582   /**
583    * Action on selecting one of Jalview's registered colour schemes
584    */
585   @Override
586   public void changeColour_actionPerformed(String colourSchemeName)
587   {
588     AlignmentI al = getAlignmentPanel().av.getAlignment();
589     ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(
590             colourSchemeName, getAlignmentPanel().av, al, null);
591     getBinding().colourByJalviewColourScheme(cs);
592   }
593
594   /**
595    * Builds the colour menu
596    */
597   protected void buildColourMenu()
598   {
599     colourMenu.removeAll();
600     AlignmentI al = getAlignmentPanel().av.getAlignment();
601
602     /*
603      * add colour by sequence, by chain, by charge and cysteine
604      */
605     colourMenu.add(seqColour);
606     colourMenu.add(chainColour);
607     colourMenu.add(chargeColour);
608     chargeColour.setEnabled(!al.isNucleotide());
609
610     /*
611      * add all 'simple' (per-residue) colour schemes registered to Jalview
612      */
613     ButtonGroup itemGroup = ColourMenuHelper.addMenuItems(colourMenu, this,
614             al, true);
615
616     /*
617      * add 'colour by viewer' (menu item text is set in subclasses)
618      */
619     viewerColour.setSelected(false);
620     viewerColour.addActionListener(new ActionListener()
621     {
622       @Override
623       public void actionPerformed(ActionEvent actionEvent)
624       {
625         viewerColour_actionPerformed();
626       }
627     });
628     colourMenu.add(viewerColour);
629
630     /*
631      * add 'set background colour'
632      */
633     JMenuItem backGround = new JMenuItem();
634     backGround
635             .setText(MessageManager.getString("action.background_colour"));
636     backGround.addActionListener(new ActionListener()
637     {
638       @Override
639       public void actionPerformed(ActionEvent actionEvent)
640       {
641         background_actionPerformed();
642       }
643     });
644     colourMenu.add(backGround);
645
646     /*
647      * add colour buttons to a group so their selection is
648      * mutually exclusive (background colour is a separate option)
649      */
650     itemGroup.add(seqColour);
651     itemGroup.add(chainColour);
652     itemGroup.add(chargeColour);
653     itemGroup.add(viewerColour);
654   }
655
656   /**
657    * Construct menu items
658    */
659   protected void initMenus()
660   {
661     AAStructureBindingModel binding = getBinding();
662
663     seqColour = new JRadioButtonMenuItem();
664     seqColour.setText(MessageManager.getString("action.by_sequence"));
665     seqColour.setName(ViewerColour.BySequence.name());
666     seqColour.setSelected(binding.isColourBySequence());
667     seqColour.addActionListener(new ActionListener()
668     {
669       @Override
670       public void actionPerformed(ActionEvent actionEvent)
671       {
672         seqColour_actionPerformed();
673       }
674     });
675
676     chainColour = new JRadioButtonMenuItem();
677     chainColour.setText(MessageManager.getString("action.by_chain"));
678     chainColour.setName(ViewerColour.ByChain.name());
679     chainColour.addActionListener(new ActionListener()
680     {
681       @Override
682       public void actionPerformed(ActionEvent actionEvent)
683       {
684         chainColour_actionPerformed();
685       }
686     });
687
688     chargeColour = new JRadioButtonMenuItem();
689     chargeColour.setText(MessageManager.getString("label.charge_cysteine"));
690     chargeColour.setName(ViewerColour.ChargeCysteine.name());
691     chargeColour.addActionListener(new ActionListener()
692     {
693       @Override
694       public void actionPerformed(ActionEvent actionEvent)
695       {
696         chargeColour_actionPerformed();
697       }
698     });
699
700     viewerColour = new JRadioButtonMenuItem();
701     viewerColour
702             .setText(MessageManager.getString("label.colour_with_viewer"));
703     viewerColour.setToolTipText(MessageManager
704             .getString("label.let_viewer_manage_structure_colours"));
705     viewerColour.setName(ViewerColour.ByViewer.name());
706     viewerColour.setSelected(!binding.isColourBySequence());
707
708     if (_colourwith == null)
709     {
710       _colourwith = new Vector<>();
711     }
712     if (_alignwith == null)
713     {
714       _alignwith = new Vector<>();
715     }
716
717     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
718             MessageManager.getString("label.colour_by"), this, _colourwith,
719             new ItemListener()
720             {
721               @Override
722               public void itemStateChanged(ItemEvent e)
723               {
724                 if (!seqColour.isSelected())
725                 {
726                   seqColour.doClick();
727                 }
728                 else
729                 {
730                   // update the viewer display now.
731                   seqColour_actionPerformed();
732                 }
733               }
734             });
735     viewMenu.add(seqColourBy);
736
737     final ItemListener handler = new ItemListener()
738     {
739       @Override
740       public void itemStateChanged(ItemEvent e)
741       {
742         if (_alignwith.isEmpty())
743         {
744           alignStructs.setEnabled(false);
745           alignStructs.setToolTipText(null);
746         }
747         else
748         {
749           alignStructs.setEnabled(true);
750           alignStructs.setToolTipText(MessageManager.formatMessage(
751                   "label.align_structures_using_linked_alignment_views",
752                   _alignwith.size()));
753         }
754       }
755     };
756     viewSelectionMenu = new ViewSelectionMenu(
757             MessageManager.getString("label.superpose_with"), this,
758             _alignwith, handler);
759     handler.itemStateChanged(null);
760     viewerActionMenu.add(viewSelectionMenu, 0);
761     viewerActionMenu.addMenuListener(new MenuListener()
762     {
763       @Override
764       public void menuSelected(MenuEvent e)
765       {
766         handler.itemStateChanged(null);
767       }
768
769       @Override
770       public void menuDeselected(MenuEvent e)
771       {
772       }
773
774       @Override
775       public void menuCanceled(MenuEvent e)
776       {
777       }
778     });
779
780     viewerActionMenu.setText(getViewerName());
781     helpItem.setText(MessageManager.formatMessage("label.viewer_help",
782             getViewerName()));
783
784     buildColourMenu();
785   }
786
787   /**
788    * Sends commands to the structure viewer to superimpose structures based on
789    * currently associated alignments. May optionally return an error message for
790    * the operation.
791    */
792   @Override
793   protected String alignStructsWithAllAlignPanels()
794   {
795     if (getAlignmentPanel() == null)
796     {
797       return null;
798     }
799
800     if (_alignwith.size() == 0)
801     {
802       _alignwith.add(getAlignmentPanel());
803     }
804
805     String reply = null;
806     try
807     {
808       reply = getBinding().superposeStructures(_alignwith);
809       if (reply != null && !reply.isEmpty())
810       {
811         String text = MessageManager
812                 .formatMessage("error.superposition_failed", reply);
813         statusBar.setText(text);
814       }
815     } catch (Exception e)
816     {
817       StringBuffer sp = new StringBuffer();
818       for (AlignmentViewPanel alignPanel : _alignwith)
819       {
820         sp.append("'" + alignPanel.getViewName() + "' ");
821       }
822       Console.info("Couldn't align structures with the " + sp.toString()
823               + "associated alignment panels.", e);
824     }
825     return reply;
826   }
827
828   /**
829    * Opens a colour chooser dialog, and applies the chosen colour to the
830    * background of the structure viewer
831    */
832   @Override
833   public void background_actionPerformed()
834   {
835     String ttl = MessageManager.getString("label.select_background_colour");
836     ColourChooserListener listener = new ColourChooserListener()
837     {
838       @Override
839       public void colourSelected(Color c)
840       {
841         getBinding().setBackgroundColour(c);
842       }
843     };
844     JalviewColourChooser.showColourChooser(this, ttl, null, listener);
845   }
846
847   @Override
848   public void viewerColour_actionPerformed()
849   {
850     if (viewerColour.isSelected())
851     {
852       // disable automatic sequence colouring.
853       getBinding().setColourBySequence(false);
854     }
855   }
856
857   @Override
858   public void chainColour_actionPerformed()
859   {
860     chainColour.setSelected(true);
861     getBinding().colourByChain();
862   }
863
864   @Override
865   public void chargeColour_actionPerformed()
866   {
867     chargeColour.setSelected(true);
868     getBinding().colourByCharge();
869   }
870
871   @Override
872   public void seqColour_actionPerformed()
873   {
874     AAStructureBindingModel binding = getBinding();
875     binding.setColourBySequence(seqColour.isSelected());
876     if (_colourwith == null)
877     {
878       _colourwith = new Vector<>();
879     }
880     if (binding.isColourBySequence())
881     {
882       if (!binding.isLoadingFromArchive())
883       {
884         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
885         {
886           // Make the currently displayed alignment panel the associated view
887           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
888         }
889       }
890       // Set the colour using the current view for the associated alignframe
891       for (AlignmentViewPanel alignPanel : _colourwith)
892       {
893         binding.colourBySequence(alignPanel);
894       }
895       seqColoursApplied = true;
896     }
897   }
898
899   @Override
900   public void pdbFile_actionPerformed()
901   {
902     // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
903     JalviewFileChooser chooser = new JalviewFileChooser(
904             Cache.getProperty("LAST_DIRECTORY"));
905
906     chooser.setFileView(new JalviewFileView());
907     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
908     chooser.setToolTipText(MessageManager.getString("action.save"));
909
910     int value = chooser.showSaveDialog(this);
911
912     if (value == JalviewFileChooser.APPROVE_OPTION)
913     {
914       BufferedReader in = null;
915       try
916       {
917         // TODO: cope with multiple PDB files in view
918         in = new BufferedReader(
919                 new FileReader(getBinding().getStructureFiles()[0]));
920         File outFile = chooser.getSelectedFile();
921
922         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
923         String data;
924         while ((data = in.readLine()) != null)
925         {
926           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
927           {
928             out.println(data);
929           }
930         }
931         out.close();
932       } catch (Exception ex)
933       {
934         ex.printStackTrace();
935       } finally
936       {
937         if (in != null)
938         {
939           try
940           {
941             in.close();
942           } catch (IOException e)
943           {
944             // ignore
945           }
946         }
947       }
948     }
949   }
950
951   @Override
952   public void viewMapping_actionPerformed()
953   {
954     CutAndPasteTransfer cap = new CutAndPasteTransfer();
955     try
956     {
957       cap.appendText(getBinding().printMappings());
958     } catch (OutOfMemoryError e)
959     {
960       new OOMWarning(
961               "composing sequence-structure alignments for display in text box.",
962               e);
963       cap.dispose();
964       return;
965     }
966     Desktop.addInternalFrame(cap,
967             MessageManager.getString("label.pdb_sequence_mapping"), 550,
968             600);
969   }
970
971   protected abstract String getViewerName();
972
973   /**
974    * Configures the title and menu items of the viewer panel.
975    */
976   @Override
977   public void updateTitleAndMenus()
978   {
979     AAStructureBindingModel binding = getBinding();
980     if (binding.hasFileLoadingError())
981     {
982       repaint();
983       return;
984     }
985     setChainMenuItems(binding.getChainNames());
986
987     this.setTitle(binding.getViewerTitle(getViewerName(), true));
988
989     /*
990      * enable 'Superpose with' if more than one mapped structure
991      */
992     viewSelectionMenu.setEnabled(false);
993     if (getBinding().getMappedStructureCount() > 1
994             && getBinding().getSequence().length > 1)
995     {
996       viewSelectionMenu.setEnabled(true);
997     }
998
999     /*
1000      * Show action menu if it has any enabled items
1001      */
1002     viewerActionMenu.setVisible(false);
1003     for (int i = 0; i < viewerActionMenu.getItemCount(); i++)
1004     {
1005       if (viewerActionMenu.getItem(i).isEnabled())
1006       {
1007         viewerActionMenu.setVisible(true);
1008         break;
1009       }
1010     }
1011
1012     if (!binding.isLoadingFromArchive())
1013     {
1014       seqColour_actionPerformed();
1015     }
1016   }
1017
1018   @Override
1019   public String toString()
1020   {
1021     return getTitle();
1022   }
1023
1024   @Override
1025   public boolean hasMapping()
1026   {
1027     if (worker != null && (addingStructures || _started))
1028     {
1029       return false;
1030     }
1031     if (getBinding() == null)
1032     {
1033       if (_aps == null || _aps.size() == 0)
1034       {
1035         // viewer has been closed, but we did at some point run.
1036         return true;
1037       }
1038       return false;
1039     }
1040     String[] pdbids = getBinding().getStructureFiles();
1041     if (pdbids == null)
1042     {
1043       return false;
1044     }
1045     int p = 0;
1046     for (String pdbid : pdbids)
1047     {
1048       StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
1049       if (sm != null && sm.length > 0 && sm[0] != null)
1050       {
1051         p++;
1052       }
1053     }
1054     // only return true if there is a mapping for every structure file we have
1055     // loaded
1056     if (p == 0 || p != pdbids.length)
1057     {
1058       return false;
1059     }
1060     // and that coloring has been applied
1061     return seqColoursApplied;
1062   }
1063
1064   @Override
1065   public void raiseViewer()
1066   {
1067     toFront();
1068   }
1069
1070   @Override
1071   public long startProgressBar(String msg)
1072   {
1073     // TODO would rather have startProgress/stopProgress as the
1074     // IProgressIndicator interface
1075     long tm = random.nextLong();
1076     if (progressBar != null)
1077     {
1078       progressBar.setProgressBar(msg, tm);
1079     }
1080     return tm;
1081   }
1082
1083   @Override
1084   public void stopProgressBar(String msg, long handle)
1085   {
1086     if (progressBar != null)
1087     {
1088       progressBar.setProgressBar(msg, handle);
1089     }
1090   }
1091
1092   protected IProgressIndicator getProgressIndicator()
1093   {
1094     return progressBar;
1095   }
1096
1097   protected void setProgressIndicator(IProgressIndicator pi)
1098   {
1099     progressBar = pi;
1100   }
1101
1102   public void setProgressMessage(String message, long id)
1103   {
1104     if (progressBar != null)
1105     {
1106       progressBar.setProgressBar(message, id);
1107     }
1108   }
1109
1110   @Override
1111   public void showConsole(boolean show)
1112   {
1113     // default does nothing
1114   }
1115
1116   /**
1117    * Show only the selected chain(s) in the viewer
1118    */
1119   protected void showSelectedChains()
1120   {
1121     List<String> toshow = new ArrayList<>();
1122     for (int i = 0; i < chainMenu.getItemCount(); i++)
1123     {
1124       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
1125       {
1126         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
1127         if (item.isSelected())
1128         {
1129           toshow.add(item.getText());
1130         }
1131       }
1132     }
1133     getBinding().showChains(toshow);
1134   }
1135
1136   /**
1137    * Tries to fetch a PDB file and save to a temporary local file. Returns the
1138    * saved file path if successful, or null if not.
1139    * 
1140    * @param processingEntry
1141    * @return
1142    */
1143   protected String fetchPdbFile(PDBEntry processingEntry)
1144   {
1145     String filePath = null;
1146     Pdb pdbclient = new Pdb();
1147     EBIAlfaFold afclient = new EBIAlfaFold();
1148     AlignmentI pdbseq = null;
1149     String pdbid = processingEntry.getId();
1150     long handle = System.currentTimeMillis()
1151             + Thread.currentThread().hashCode();
1152
1153     /*
1154      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
1155      */
1156     String msg = MessageManager.formatMessage("status.fetching_pdb",
1157             new Object[]
1158             { pdbid });
1159     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1160     // long hdl = startProgressBar(MessageManager.formatMessage(
1161     // "status.fetching_pdb", new Object[]
1162     // { pdbid }));
1163     try
1164     {
1165       if (afclient.isValidReference(pdbid))
1166       {
1167         pdbseq = afclient.getSequenceRecords(pdbid,
1168                 processingEntry.getRetrievalUrl());
1169       }
1170       else
1171       {
1172         if (processingEntry.hasRetrievalUrl())
1173         {
1174           String safePDBId = java.net.URLEncoder.encode(pdbid, "UTF-8")
1175                   .replace("%", "__");
1176
1177           // retrieve from URL to new local tmpfile
1178           File tmpFile = File.createTempFile(safePDBId,
1179                   "." + (PDBEntry.Type.MMCIF.toString().equals(
1180                           processingEntry.getType().toString()) ? "cif"
1181                                   : "pdb"));
1182           String fromUrl = processingEntry.getRetrievalUrl();
1183           UrlDownloadClient.download(fromUrl, tmpFile);
1184
1185           // may not need this check ?
1186           String file = tmpFile.getAbsolutePath();
1187           if (file != null)
1188           {
1189             pdbseq = EBIAlfaFold.importDownloadedStructureFromUrl(fromUrl,
1190                     tmpFile, pdbid, null, null, null);
1191           }
1192         }
1193         else
1194         {
1195           pdbseq = pdbclient.getSequenceRecords(pdbid);
1196         }
1197       }
1198     } catch (Exception e)
1199     {
1200       jalview.bin.Console.errPrintln(
1201               "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
1202     } finally
1203     {
1204       msg = pdbid + " " + MessageManager.getString("label.state_completed");
1205       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1206       // stopProgressBar(msg, hdl);
1207     }
1208     /*
1209      * If PDB data were saved and are not invalid (empty alignment), return the
1210      * file path.
1211      */
1212     if (pdbseq != null && pdbseq.getHeight() > 0)
1213     {
1214       // just use the file name from the first sequence's first PDBEntry
1215       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
1216               .elementAt(0).getFile()).getAbsolutePath();
1217       processingEntry.setFile(filePath);
1218     }
1219     return filePath;
1220   }
1221
1222   /**
1223    * If supported, saves the state of the structure viewer to a temporary file
1224    * and returns the file, else returns null
1225    * 
1226    * @return
1227    */
1228   public File saveSession()
1229   {
1230     if (getBinding() == null)
1231     {
1232       return null;
1233     }
1234     File session = getBinding().saveSession();
1235     long l = session.length();
1236     int wait = 50;
1237     do
1238     {
1239       try
1240       {
1241         Thread.sleep(5);
1242       } catch (InterruptedException e)
1243       {
1244       }
1245       long nextl = session.length();
1246       if (nextl != l)
1247       {
1248         wait = 50;
1249         l = nextl;
1250       }
1251     } while (--wait > 0);
1252     return session;
1253   }
1254
1255   private static boolean quitClose = false;
1256
1257   public static void setQuitClose(boolean b)
1258   {
1259     quitClose = b;
1260   }
1261
1262   @Override
1263   public boolean stillRunning()
1264   {
1265     AAStructureBindingModel binding = getBinding();
1266     return binding != null && binding.isViewerRunning();
1267   }
1268
1269   /**
1270    * Close down this instance of Jalview's Chimera viewer, giving the user the
1271    * option to close the associated Chimera window (process). They may wish to
1272    * keep it open until they have had an opportunity to save any work.
1273    * 
1274    * @param forceClose
1275    *          if true, close any linked Chimera process; if false, prompt first
1276    */
1277   @Override
1278   public void closeViewer(boolean forceClose)
1279   {
1280     AAStructureBindingModel binding = getBinding();
1281     if (stillRunning())
1282     {
1283       if (!forceClose)
1284       {
1285         String viewerName = getViewerName();
1286
1287         int confirm = JvOptionPane.CANCEL_OPTION;
1288         if (QuitHandler.quitting())
1289         {
1290           // already asked about closing external windows
1291           confirm = quitClose ? JvOptionPane.YES_OPTION
1292                   : JvOptionPane.NO_OPTION;
1293         }
1294         else
1295         {
1296           String prompt = MessageManager
1297                   .formatMessage("label.confirm_close_viewer", new Object[]
1298                   { binding.getViewerTitle(viewerName, false),
1299                       viewerName });
1300           prompt = JvSwingUtils.wrapTooltip(true, prompt);
1301           String title = MessageManager.getString("label.close_viewer");
1302           confirm = showCloseDialog(title, prompt);
1303         }
1304
1305         /*
1306          * abort closure if user hits escape or Cancel
1307          */
1308         if (confirm == JvOptionPane.CANCEL_OPTION
1309                 || confirm == JvOptionPane.CLOSED_OPTION)
1310         {
1311           // abort possible quit handling if CANCEL chosen
1312           if (confirm == JvOptionPane.CANCEL_OPTION)
1313           {
1314             try
1315             {
1316               // this is a bit futile
1317               this.setClosed(false);
1318             } catch (PropertyVetoException e)
1319             {
1320             }
1321             QuitHandler.abortQuit();
1322           }
1323           return;
1324         }
1325         forceClose = confirm == JvOptionPane.YES_OPTION;
1326       }
1327     }
1328     if (binding != null)
1329     {
1330       binding.closeViewer(forceClose);
1331     }
1332     setAlignmentPanel(null);
1333     _aps.clear();
1334     _alignwith.clear();
1335     _colourwith.clear();
1336     // TODO: check for memory leaks where instance isn't finalised because jmb
1337     // holds a reference to the window
1338     // jmb = null;
1339     dispose();
1340   }
1341
1342   private int showCloseDialog(final String title, final String prompt)
1343   {
1344     int confirmResponse = JvOptionPane.CANCEL_OPTION;
1345     confirmResponse = JvOptionPane.showConfirmDialog(this, prompt,
1346             MessageManager.getString("label.close_viewer"),
1347             JvOptionPane.YES_NO_CANCEL_OPTION,
1348             JvOptionPane.WARNING_MESSAGE);
1349     return confirmResponse;
1350   }
1351
1352   @Override
1353   public void showHelp_actionPerformed()
1354   {
1355     /*
1356     try
1357     {
1358     */
1359     String url = getBinding().getHelpURL();
1360     if (url != null)
1361     {
1362       BrowserLauncher.openURL(url);
1363     }
1364     /* 
1365     }
1366     catch (IOException ex)
1367     {
1368       System.err
1369               .println("Show " + getViewerName() + " failed with: "
1370                       + ex.getMessage());
1371     }
1372     */
1373   }
1374
1375   @Override
1376   public boolean hasViewerActionsMenu()
1377   {
1378     return viewerActionMenu != null && viewerActionMenu.isEnabled()
1379             && viewerActionMenu.getItemCount() > 0
1380             && viewerActionMenu.isVisible();
1381   }
1382
1383 }