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