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