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