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