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