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