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