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