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