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