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