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