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