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