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