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