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