JAL-892 1 second delay for correct initial rendering; code tidy up
[jalview.git] / src / jalview / gui / AppVarna.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 jalview.analysis.AlignSeq;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.ColumnSelection;
26 import jalview.datamodel.RnaViewerModel;
27 import jalview.datamodel.SequenceGroup;
28 import jalview.datamodel.SequenceI;
29 import jalview.ext.varna.RnaModel;
30 import jalview.structure.SecondaryStructureListener;
31 import jalview.structure.SelectionListener;
32 import jalview.structure.SelectionSource;
33 import jalview.structure.StructureSelectionManager;
34 import jalview.structure.VamsasSource;
35 import jalview.util.Comparison;
36 import jalview.util.MessageManager;
37 import jalview.util.ShiftList;
38
39 import java.awt.BorderLayout;
40 import java.awt.Color;
41 import java.util.Collection;
42 import java.util.Hashtable;
43 import java.util.LinkedHashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48
49 import javax.swing.JInternalFrame;
50 import javax.swing.JSplitPane;
51 import javax.swing.event.InternalFrameAdapter;
52 import javax.swing.event.InternalFrameEvent;
53
54 import fr.orsay.lri.varna.VARNAPanel;
55 import fr.orsay.lri.varna.exceptions.ExceptionFileFormatOrSyntax;
56 import fr.orsay.lri.varna.exceptions.ExceptionLoadingFailed;
57 import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
58 import fr.orsay.lri.varna.interfaces.InterfaceVARNASelectionListener;
59 import fr.orsay.lri.varna.models.BaseList;
60 import fr.orsay.lri.varna.models.FullBackup;
61 import fr.orsay.lri.varna.models.annotations.HighlightRegionAnnotation;
62 import fr.orsay.lri.varna.models.rna.ModeleBase;
63 import fr.orsay.lri.varna.models.rna.RNA;
64
65 public class AppVarna extends JInternalFrame implements SelectionListener,
66         SecondaryStructureListener, InterfaceVARNASelectionListener,
67         VamsasSource
68 {
69   private static final Pattern PAIRS_PATTERN = Pattern
70           .compile("[^([{<>}])]");
71
72   private AppVarnaBinding vab;
73
74   private AlignmentPanel ap;
75
76   private String viewId;
77
78   private StructureSelectionManager ssm;
79
80   /*
81    * Lookup for sequence and annotation mapped to each RNA in the viewer. Using
82    * a linked hashmap means that order is preserved when saved to the project.
83    */
84   private Map<RNA, RnaModel> models = new LinkedHashMap<RNA, RnaModel>();
85
86   private Map<RNA, ShiftList> offsets = new Hashtable<RNA, ShiftList>();
87
88   private Map<RNA, ShiftList> offsetsInv = new Hashtable<RNA, ShiftList>();
89
90   private JSplitPane split;
91
92   private VarnaHighlighter mouseOverHighlighter = new VarnaHighlighter();
93
94   private VarnaHighlighter selectionHighlighter = new VarnaHighlighter();
95
96   private class VarnaHighlighter
97   {
98     private HighlightRegionAnnotation _lastHighlight;
99
100     private RNA _lastRNAhighlighted = null;
101
102     public VarnaHighlighter()
103     {
104
105     }
106
107     /**
108      * Constructor when restoring from Varna session, including any highlight
109      * state
110      * 
111      * @param rna
112      */
113     public VarnaHighlighter(RNA rna)
114     {
115       // TODO nice try but doesn't work; do we need a highlighter per model?
116       _lastRNAhighlighted = rna;
117       List<HighlightRegionAnnotation> highlights = rna.getHighlightRegion();
118       if (highlights != null && !highlights.isEmpty())
119       {
120         _lastHighlight = highlights.get(0);
121       }
122     }
123
124     public void highlightRegion(RNA rna, int start, int end)
125     {
126       clearLastSelection();
127       HighlightRegionAnnotation highlight = new HighlightRegionAnnotation(
128               rna.getBasesBetween(start, end));
129       rna.addHighlightRegion(highlight);
130       _lastHighlight = highlight;
131       _lastRNAhighlighted = rna;
132     }
133
134     public HighlightRegionAnnotation getLastHighlight()
135     {
136       return _lastHighlight;
137     }
138
139     /**
140      * Clears all structure selection and refreshes the display
141      */
142     public void clearSelection()
143     {
144       if (_lastRNAhighlighted != null)
145       {
146         _lastRNAhighlighted.getHighlightRegion().clear();
147         vab.updateSelectedRNA(_lastRNAhighlighted);
148         _lastRNAhighlighted = null;
149         _lastHighlight = null;
150       }
151     }
152
153     /**
154      * Clear the last structure selection
155      */
156     public void clearLastSelection()
157     {
158       if (_lastRNAhighlighted != null)
159       {
160         _lastRNAhighlighted.removeHighlightRegion(_lastHighlight);
161         _lastRNAhighlighted = null;
162         _lastHighlight = null;
163       }
164     }
165   }
166
167   /**
168    * Constructor
169    * 
170    * @param seq
171    *          the RNA sequence
172    * @param aa
173    *          the annotation with the secondary structure string
174    * @param ap
175    *          the AlignmentPanel creating this object
176    */
177   public AppVarna(SequenceI seq, AlignmentAnnotation aa, AlignmentPanel ap)
178   {
179     this(ap);
180
181     String sname = aa.sequenceRef == null ? "secondary structure (alignment)"
182             : seq.getName() + " structure";
183     String theTitle = sname
184             + (aa.sequenceRef == null ? " trimmed to " + seq.getName() : "");
185     theTitle = MessageManager.formatMessage("label.varna_params",
186             new String[]
187             { theTitle });
188     setTitle(theTitle);
189
190     String gappedTitle = sname + " (with gaps)";
191     RnaModel gappedModel = new RnaModel(gappedTitle, aa, seq, null, true);
192     addModel(gappedModel, gappedTitle);
193
194     String trimmedTitle = "trimmed " + sname;
195     RnaModel trimmedModel = new RnaModel(trimmedTitle, aa, seq, null, false);
196     addModel(trimmedModel, trimmedTitle);
197     vab.setSelectedIndex(0);
198   }
199
200
201   /**
202    * Constructor that links the viewer to a parent panel (but has no structures
203    * yet - use addModel to add them)
204    * 
205    * @param ap
206    */
207   protected AppVarna(AlignmentPanel ap)
208   {
209     this.ap = ap;
210     this.viewId = System.currentTimeMillis() + "." + this.hashCode();
211     vab = new AppVarnaBinding();
212     initVarna();
213
214     this.ssm = ap.getStructureSelectionManager();
215     ssm.addStructureViewerListener(this);
216     ssm.addSelectionListener(this);
217     addInternalFrameListener(new InternalFrameAdapter()
218     {
219       @Override
220       public void internalFrameClosed(InternalFrameEvent evt)
221       {
222         close();
223       }
224     });
225   }
226
227   /**
228    * Constructor given viewer data read from a saved project file
229    * 
230    * @param model
231    * @param ap
232    *          the (or a) parent alignment panel
233    */
234   public AppVarna(RnaViewerModel model, AlignmentPanel ap)
235   {
236     this(ap);
237     setTitle(model.title);
238     this.viewId = model.viewId;
239     setBounds(model.x, model.y, model.width, model.height);
240     this.split.setDividerLocation(model.dividerLocation);
241   }
242
243   /**
244    * Constructs a split pane with an empty selection list and display panel, and
245    * adds it to the desktop
246    */
247   public void initVarna()
248   {
249     VARNAPanel varnaPanel = vab.get_varnaPanel();
250     setBackground(Color.white);
251     split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
252             vab.getListPanel(), varnaPanel);
253     getContentPane().setLayout(new BorderLayout());
254     getContentPane().add(split, BorderLayout.CENTER);
255
256     varnaPanel.addSelectionListener(this);
257     jalview.gui.Desktop.addInternalFrame(this, "", getBounds().width,
258             getBounds().height);
259     this.pack();
260     showPanel(true);
261   }
262
263   public String replaceOddGaps(String oldStr)
264   {
265     Matcher matcher = PAIRS_PATTERN.matcher(oldStr);
266     String newStr = matcher.replaceAll(".");
267     return newStr;
268   }
269
270   /**
271    * Constructs a new RNA model from the given one, without gaps. Also
272    * calculates and saves a 'shift list'
273    * 
274    * @param rna
275    * @param name
276    * @return
277    */
278   public RNA trimRNA(RNA rna, String name)
279   {
280     ShiftList offset = new ShiftList();
281
282     RNA rnaTrim = new RNA(name);
283     try
284     {
285       rnaTrim.setRNA(rna.getSeq(), replaceOddGaps(rna.getStructDBN()));
286     } catch (ExceptionUnmatchedClosingParentheses e2)
287     {
288       e2.printStackTrace();
289     } catch (ExceptionFileFormatOrSyntax e3)
290     {
291       e3.printStackTrace();
292     }
293
294     String seq = rnaTrim.getSeq();
295     StringBuilder struc = new StringBuilder(256);
296     struc.append(rnaTrim.getStructDBN());
297     int ofstart = -1;
298     int sleng = seq.length();
299
300     for (int i = 0; i < sleng; i++)
301     {
302       if (Comparison.isGap(seq.charAt(i)))
303       {
304         if (ofstart == -1)
305         {
306           ofstart = i;
307         }
308         /*
309          * mark base or base pair in the structure with *
310          */
311         if (!rnaTrim.findPair(i).isEmpty())
312         {
313           int m = rnaTrim.findPair(i).get(1);
314           int l = rnaTrim.findPair(i).get(0);
315
316           struc.replace(m, m + 1, "*");
317           struc.replace(l, l + 1, "*");
318         }
319         else
320         {
321           struc.replace(i, i + 1, "*");
322         }
323       }
324       else
325       {
326         if (ofstart > -1)
327         {
328           offset.addShift(offset.shift(ofstart), ofstart - i);
329           ofstart = -1;
330         }
331       }
332     }
333     // final gap
334     if (ofstart > -1)
335     {
336       offset.addShift(offset.shift(ofstart), ofstart - sleng);
337       ofstart = -1;
338     }
339
340     /*
341      * remove the marked gaps from the structure
342      */
343     String newStruc = struc.toString().replace("*", "");
344
345     /*
346      * remove gaps from the sequence
347      */
348     String newSeq = AlignSeq.extractGaps(Comparison.GapChars, seq);
349
350     try
351     {
352       rnaTrim.setRNA(newSeq, newStruc);
353       registerOffset(rnaTrim, offset);
354     } catch (ExceptionUnmatchedClosingParentheses e)
355     {
356       e.printStackTrace();
357     } catch (ExceptionFileFormatOrSyntax e)
358     {
359       e.printStackTrace();
360     }
361     return rnaTrim;
362   }
363
364   /**
365    * Save the sequence to structure mapping, and also its inverse.
366    * 
367    * @param rnaTrim
368    * @param offset
369    */
370   private void registerOffset(RNA rnaTrim, ShiftList offset)
371   {
372     offsets.put(rnaTrim, offset);
373     offsetsInv.put(rnaTrim, offset.getInverse());
374   }
375
376   public void showPanel(boolean show)
377   {
378     this.setVisible(show);
379   }
380
381   /**
382    * If a mouseOver event from the AlignmentPanel is noticed the currently
383    * selected RNA in the VARNA window is highlighted at the specific position.
384    * To be able to remove it before the next highlight it is saved in
385    * _lastHighlight
386    * 
387    * @param sequence
388    * @param index
389    *          the aligned sequence position (base 0)
390    * @param position
391    *          the dataset sequence position (base 1)
392    */
393   @Override
394   public void mouseOverSequence(SequenceI sequence, final int index,
395           final int position)
396   {
397     RNA rna = vab.getSelectedRNA();
398     if (rna == null)
399     {
400       return;
401     }
402     RnaModel rnaModel = models.get(rna);
403     if (rnaModel.seq == sequence)
404     {
405       int highlightPos = rnaModel.gapped ? index : position - 1;
406       mouseOverHighlighter.highlightRegion(rna, highlightPos, highlightPos);
407       vab.updateSelectedRNA(rna);
408     }
409   }
410
411   @Override
412   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
413           SelectionSource source)
414   {
415     if (source != ap.av)
416     {
417       // ignore events from anything but our parent alignpanel
418       // TODO - reuse many-one panel-view system in jmol viewer
419       return;
420     }
421     RNA rna = vab.getSelectedRNA();
422     if (rna == null)
423     {
424       return;
425     }
426     if (seqsel != null && seqsel.getSize() > 0)
427     {
428       int start = seqsel.getStartRes(), end = seqsel.getEndRes();
429       ShiftList shift = offsets.get(rna);
430       if (shift != null)
431       {
432         start = shift.shift(start);
433         end = shift.shift(end);
434       }
435       selectionHighlighter.highlightRegion(rna, start, end);
436       selectionHighlighter.getLastHighlight().setOutlineColor(
437               seqsel.getOutlineColour());
438       // TODO - translate column markings to positions on structure if present.
439       vab.updateSelectedRNA(rna);
440     }
441     else
442     {
443       selectionHighlighter.clearSelection();
444     }
445   }
446
447   /**
448    * Respond to a change of the base hovered over in the Varna viewer
449    */
450   @Override
451   public void onHoverChanged(ModeleBase previousBase, ModeleBase newBase)
452   {
453     RNA rna = vab.getSelectedRNA();
454     ShiftList shift = offsetsInv.get(rna);
455     SequenceI seq = models.get(rna).seq;
456     if (newBase != null && seq != null)
457     {
458       if (shift != null)
459       {
460         int i = shift.shift(newBase.getIndex());
461         // System.err.println("shifted "+(arg1.getIndex())+" to "+i);
462         ssm.mouseOverVamsasSequence(seq, i, this);
463       }
464       else
465       {
466         ssm.mouseOverVamsasSequence(seq, newBase.getIndex(), this);
467       }
468     }
469   }
470
471   @Override
472   public void onSelectionChanged(BaseList arg0, BaseList arg1, BaseList arg2)
473   {
474     // TODO translate selected regions in VARNA to a selection on the
475     // alignpanel.
476
477   }
478
479   /**
480    * Returns the path to a temporary file containing a representation of the
481    * state of one Varna display
482    * 
483    * @param rna
484    * 
485    * @return
486    */
487   public String getStateInfo(RNA rna)
488   {
489     return vab.getStateInfo(rna);
490   }
491
492   public AlignmentPanel getAlignmentPanel()
493   {
494     return ap;
495   }
496
497   public String getViewId()
498   {
499     return viewId;
500   }
501
502   /**
503    * Returns true if any of the viewer's models (not necessarily the one
504    * currently displayed) is for the given sequence
505    * 
506    * @param seq
507    * @return
508    */
509   public boolean isListeningFor(SequenceI seq)
510   {
511     for (RnaModel model : models.values())
512     {
513       if (model.seq == seq)
514       {
515         return true;
516       }
517     }
518     return false;
519   }
520
521   /**
522    * Returns a value representing the horizontal split divider location
523    * 
524    * @return
525    */
526   public int getDividerLocation()
527   {
528     return split == null ? 0 : split.getDividerLocation();
529   }
530
531   /**
532    * Tidy up as necessary when the viewer panel is closed
533    */
534   protected void close()
535   {
536     /*
537      * Deregister as a listener, to release references to this object
538      */
539     if (ssm != null)
540     {
541       ssm.removeStructureViewerListener(AppVarna.this, null);
542       ssm.removeSelectionListener(AppVarna.this);
543     }
544   }
545
546   /**
547    * Returns the secondary structure annotation that this viewer displays for
548    * the given sequence
549    * 
550    * @return
551    */
552   public AlignmentAnnotation getAnnotation(SequenceI seq)
553   {
554     for (RnaModel model : models.values())
555     {
556       if (model.seq == seq)
557       {
558         return model.ann;
559       }
560     }
561     return null;
562   }
563
564   public int getSelectedIndex()
565   {
566     return this.vab.getSelectedIndex();
567   }
568
569   /**
570    * Returns the set of models shown by the viewer
571    * 
572    * @return
573    */
574   public Collection<RnaModel> getModels()
575   {
576     return models.values();
577   }
578
579   /**
580    * Add a model (e.g. loaded from project file)
581    * 
582    * @param rna
583    * @param modelName
584    */
585   public RNA addModel(RnaModel model, String modelName)
586   {
587     if (!model.ann.isValidStruc())
588     {
589       throw new IllegalArgumentException("Invalid RNA structure annotation");
590     }
591
592     /*
593      * opened on request in Jalview session
594      */
595     RNA rna = new RNA(modelName);
596     String struc = model.ann.getRNAStruc();
597     struc = replaceOddGaps(struc);
598
599     String strucseq = model.seq.getSequenceAsString();
600     try
601     {
602       rna.setRNA(strucseq, struc);
603     } catch (ExceptionUnmatchedClosingParentheses e2)
604     {
605       e2.printStackTrace();
606     } catch (ExceptionFileFormatOrSyntax e3)
607     {
608       e3.printStackTrace();
609     }
610
611     if (!model.gapped)
612     {
613       rna = trimRNA(rna, modelName);
614     }
615     models.put(rna, new RnaModel(modelName, model.ann, model.seq, rna,
616             model.gapped));
617     vab.addStructure(rna);
618     return rna;
619   }
620
621   /**
622    * Constructs a shift list that describes the gaps in the sequence
623    * 
624    * @param seq
625    * @return
626    */
627   protected ShiftList buildOffset(SequenceI seq)
628   {
629     // TODO refactor to avoid duplication with trimRNA()
630     // TODO JAL-1789 bugs in use of ShiftList here
631     ShiftList offset = new ShiftList();
632     int ofstart = -1;
633     int sleng = seq.getLength();
634     char[] seqChars = seq.getSequence();
635
636     for (int i = 0; i < sleng; i++)
637     {
638       if (Comparison.isGap(seqChars[i]))
639       {
640         if (ofstart == -1)
641         {
642           ofstart = i;
643         }
644       }
645       else
646       {
647         if (ofstart > -1)
648         {
649           offset.addShift(offset.shift(ofstart), ofstart - i);
650           ofstart = -1;
651         }
652       }
653     }
654     // final gap
655     if (ofstart > -1)
656     {
657       offset.addShift(offset.shift(ofstart), ofstart - sleng);
658       ofstart = -1;
659     }
660     return offset;
661   }
662
663   /**
664    * Set the selected index in the model selection list
665    * 
666    * @param selectedIndex
667    */
668   public void setInitialSelection(final int selectedIndex)
669   {
670     /*
671      * empirically it needs a second for Varna/AWT to finish loading/drawing
672      * models for this to work; SwingUtilities.invokeLater _not_ a solution;
673      * explanation and/or better solution welcome!
674      */
675     synchronized (this)
676     {
677       try
678       {
679         wait(1000);
680       } catch (InterruptedException e)
681       {
682         // meh
683       }
684     }
685     vab.setSelectedIndex(selectedIndex);
686   }
687
688
689   /**
690    * Add a model with associated Varna session file
691    * 
692    * @param rna
693    * @param modelName
694    */
695   public RNA addModelSession(RnaModel model, String modelName,
696           String sessionFile)
697   {
698     if (!model.ann.isValidStruc())
699     {
700       throw new IllegalArgumentException("Invalid RNA structure annotation");
701     }
702
703     try
704     {
705       FullBackup fromSession = vab.vp.loadSession(sessionFile);
706       vab.addStructure(fromSession.rna, fromSession.config);
707       RNA rna = fromSession.rna;
708       // copy the model, but now including the RNA object
709       RnaModel newModel = new RnaModel(model.title, model.ann, model.seq,
710               rna, model.gapped);
711       if (!model.gapped)
712       {
713         registerOffset(rna, buildOffset(model.seq));
714       }
715       models.put(rna, newModel);
716       // capture rna selection state when saved
717       selectionHighlighter = new VarnaHighlighter(rna);
718       return fromSession.rna;
719     } catch (ExceptionLoadingFailed e)
720     {
721       System.err
722               .println("Error restoring Varna session: " + e.getMessage());
723       return null;
724     }
725   }
726 }