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