Merge branch 'develop' into update_212_Dec_merge_with_21125_chamges
[jalview.git] / src / jalview / gui / AnnotationRowFilter.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.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.GraphLine;
25 import jalview.schemes.AnnotationColourGradient;
26 import jalview.util.MessageManager;
27
28 import java.awt.Color;
29 import java.awt.Dimension;
30 import java.awt.event.ActionEvent;
31 import java.awt.event.ActionListener;
32 import java.awt.event.FocusAdapter;
33 import java.awt.event.FocusEvent;
34 import java.awt.event.ItemEvent;
35 import java.awt.event.ItemListener;
36 import java.awt.event.MouseAdapter;
37 import java.awt.event.MouseEvent;
38 import java.math.BigDecimal;
39 import java.math.MathContext;
40 import java.util.HashMap;
41 import java.util.Map;
42 import java.util.Vector;
43
44 import javax.swing.JButton;
45 import javax.swing.JCheckBox;
46 import javax.swing.JComboBox;
47 import javax.swing.JInternalFrame;
48 import javax.swing.JPanel;
49 import javax.swing.JTextField;
50 import javax.swing.event.ChangeEvent;
51 import javax.swing.event.ChangeListener;
52
53 @SuppressWarnings("serial")
54 public abstract class AnnotationRowFilter extends JPanel
55 {
56   private static final String TWO_DP = "%.2f";
57
58   private final static MathContext FOUR_SIG_FIG = new MathContext(4);
59
60   protected AlignViewport av;
61
62   protected AlignmentPanel ap;
63
64   protected int[] annmap;
65
66   protected boolean adjusting = false;
67
68   protected JCheckBox seqAssociated = new JCheckBox();
69
70   protected JCheckBox percentThreshold = new JCheckBox();
71
72   protected Slider slider;
73
74   protected JTextField thresholdValue = new JTextField(20);
75
76   protected JInternalFrame frame;
77
78   protected JButton ok = new JButton();
79
80   protected JButton cancel = new JButton();
81
82   /**
83    * enabled if the user is dragging the slider - try to keep updates to a
84    * minimun
85    */
86   protected boolean sliderDragging = false;
87
88   protected JComboBox<String> threshold = new JComboBox<>();
89
90   protected JComboBox<String> annotations;
91
92   /*
93    * map from annotation to its menu item display label
94    * - so we know which item to pre-select on restore
95    */
96   private Map<AlignmentAnnotation, String> annotationLabels;
97
98   private AlignmentAnnotation currentAnnotation;
99
100   /**
101    * Constructor
102    * 
103    * @param viewport
104    * @param alignPanel
105    */
106   public AnnotationRowFilter(AlignViewport viewport,
107           final AlignmentPanel alignPanel)
108   {
109     this.av = viewport;
110     this.ap = alignPanel;
111     this.slider = new Slider(0f, 100f, 50f);
112
113     thresholdValue.addFocusListener(new FocusAdapter()
114     {
115       @Override
116       public void focusLost(FocusEvent e)
117       {
118         thresholdValue_actionPerformed();
119       }
120     });
121   }
122
123   protected void addSliderChangeListener()
124   {
125
126     slider.addChangeListener(new ChangeListener()
127     {
128       @Override
129       public void stateChanged(ChangeEvent evt)
130       {
131         if (!adjusting)
132         {
133           setThresholdValueText();
134           valueChanged(!sliderDragging);
135         }
136       }
137     });
138   }
139
140   /**
141    * update the text field from the threshold slider. preserves state of
142    * 'adjusting' so safe to call in init.
143    */
144   protected void setThresholdValueText()
145   {
146     boolean oldadj = adjusting;
147     adjusting = true;
148     if (percentThreshold.isSelected())
149     {
150       thresholdValue
151               .setText(String.format(TWO_DP, getSliderPercentageValue()));
152     }
153     else
154     {
155       /*
156        * round to 4 significant digits without trailing zeroes
157        */
158       float f = getSliderValue();
159       BigDecimal formatted = new BigDecimal(f).round(FOUR_SIG_FIG)
160               .stripTrailingZeros();
161       thresholdValue.setText(formatted.toPlainString());
162     }
163     adjusting = oldadj;
164   }
165
166   /**
167    * Answers the value of the slider position (descaled to 'true' value)
168    * 
169    * @return
170    */
171   protected float getSliderValue()
172   {
173     return slider.getSliderValue();
174   }
175
176   /**
177    * Sets the slider value (scaled from the true value to the slider range)
178    * 
179    * @param value
180    */
181   protected void setSliderValue(float value)
182   {
183     slider.setSliderValue(value);
184   }
185
186   /**
187    * Answers the value of the slider position as a percentage between minimum
188    * and maximum of its range
189    * 
190    * @return
191    */
192   protected float getSliderPercentageValue()
193   {
194     return slider.getSliderPercentageValue();
195   }
196
197   /**
198    * Sets the slider position for a given percentage value of its min-max range
199    * 
200    * @param pct
201    */
202   protected void setSliderPercentageValue(float pct)
203   {
204     slider.setSliderPercentageValue(pct);
205   }
206
207   protected void addSliderMouseListeners()
208   {
209
210     slider.addMouseListener(new MouseAdapter()
211     {
212       @Override
213       public void mousePressed(MouseEvent e)
214       {
215         sliderDragging = true;
216         super.mousePressed(e);
217       }
218
219       @Override
220       public void mouseDragged(MouseEvent e)
221       {
222         sliderDragging = true;
223         super.mouseDragged(e);
224       }
225
226       @Override
227       public void mouseReleased(MouseEvent evt)
228       {
229         sliderDragReleased();
230       }
231     });
232   }
233
234   /**
235    * Builds and returns a list of menu items (display text) for choice of
236    * annotation. Also builds maps between annotations, their positions in the
237    * list, and their display labels in the list.
238    * 
239    * @param isSeqAssociated
240    * @return
241    */
242   public Vector<String> getAnnotationItems(boolean isSeqAssociated)
243   {
244     annotationLabels = new HashMap<>();
245
246     Vector<String> list = new Vector<>();
247     int index = 1;
248     int[] anmap = new int[av.getAlignment()
249             .getAlignmentAnnotation().length];
250     seqAssociated.setEnabled(false);
251     for (int i = 0; i < av.getAlignment()
252             .getAlignmentAnnotation().length; i++)
253     {
254       AlignmentAnnotation annotation = av.getAlignment()
255               .getAlignmentAnnotation()[i];
256       if (annotation.sequenceRef == null)
257       {
258         if (isSeqAssociated)
259         {
260           continue;
261         }
262       }
263       else
264       {
265         seqAssociated.setEnabled(true);
266       }
267       String label = annotation.label;
268       // add associated sequence ID if available
269       if (!isSeqAssociated && annotation.sequenceRef != null)
270       {
271         label = label + "_" + annotation.sequenceRef.getName();
272       }
273       // make label unique
274       if (!list.contains(label))
275       {
276         anmap[list.size()] = i;
277         list.add(label);
278         annotationLabels.put(annotation, label);
279       }
280       else
281       {
282         if (!isSeqAssociated)
283         {
284           anmap[list.size()] = i;
285           label = label + "_" + (index++);
286           list.add(label);
287           annotationLabels.put(annotation, label);
288         }
289       }
290     }
291     this.annmap = new int[list.size()];
292     System.arraycopy(anmap, 0, this.annmap, 0, this.annmap.length);
293     return list;
294   }
295
296   protected int getSelectedThresholdItem(int indexValue)
297   {
298     int selectedThresholdItem = -1;
299     if (indexValue == 1)
300     {
301       selectedThresholdItem = AnnotationColourGradient.ABOVE_THRESHOLD;
302     }
303     else if (indexValue == 2)
304     {
305       selectedThresholdItem = AnnotationColourGradient.BELOW_THRESHOLD;
306     }
307     return selectedThresholdItem;
308   }
309
310   public void ok_actionPerformed()
311   {
312     try
313     {
314       frame.setClosed(true);
315     } catch (Exception ex)
316     {
317     }
318   }
319
320   public void cancel_actionPerformed()
321   {
322     reset();
323     ap.paintAlignment(true, true);
324     try
325     {
326       frame.setClosed(true);
327     } catch (Exception ex)
328     {
329     }
330   }
331
332   protected void thresholdCheck_actionPerformed()
333   {
334     updateView();
335   }
336
337   protected void selectedAnnotationChanged()
338   {
339     updateView();
340   }
341
342   protected void threshold_actionPerformed()
343   {
344     updateView();
345   }
346
347   /**
348    * Updates the slider position, and the display, for an update in the slider's
349    * text input field
350    */
351   protected void thresholdValue_actionPerformed()
352   {
353     try
354     {
355       float f = Float.parseFloat(thresholdValue.getText());
356       if (percentThreshold.isSelected())
357       {
358         setSliderPercentageValue(f);
359       }
360       else
361       {
362         setSliderValue(f);
363       }
364       updateView();
365     } catch (NumberFormatException ex)
366     {
367     }
368   }
369
370   protected void percentageValue_actionPerformed()
371   {
372     setThresholdValueText();
373   }
374
375   protected void thresholdIsMin_actionPerformed()
376   {
377     updateView();
378   }
379
380   protected void populateThresholdComboBox(JComboBox<String> thresh)
381   {
382     thresh.addItem(MessageManager
383             .getString("label.threshold_feature_no_threshold"));
384     thresh.addItem(MessageManager
385             .getString("label.threshold_feature_above_threshold"));
386     thresh.addItem(MessageManager
387             .getString("label.threshold_feature_below_threshold"));
388   }
389
390   /**
391    * Rebuilds the drop-down list of annotations to choose from when the 'per
392    * sequence only' checkbox is checked or unchecked.
393    * 
394    * @param anns
395    */
396   protected void seqAssociated_actionPerformed(JComboBox<String> anns)
397   {
398     adjusting = true;
399     String cursel = (String) anns.getSelectedItem();
400     boolean isvalid = false;
401     boolean isseqs = seqAssociated.isSelected();
402     anns.removeAllItems();
403     for (String anitem : getAnnotationItems(seqAssociated.isSelected()))
404     {
405       if (anitem.equals(cursel) || (isseqs && cursel.startsWith(anitem)))
406       {
407         isvalid = true;
408         cursel = anitem;
409       }
410       anns.addItem(anitem);
411     }
412     if (isvalid)
413     {
414       anns.setSelectedItem(cursel);
415     }
416     else
417     {
418       if (anns.getItemCount() > 0)
419       {
420         anns.setSelectedIndex(0);
421       }
422     }
423     adjusting = false;
424
425     updateView();
426   }
427   
428
429   protected void propagateSeqAssociatedThreshold(boolean allAnnotation,
430           AlignmentAnnotation annotation)
431   {
432     if (annotation.sequenceRef == null || annotation.threshold == null)
433     {
434       return;
435     }
436
437     float thr = annotation.threshold.value;
438     for (int i = 0; i < av.getAlignment()
439             .getAlignmentAnnotation().length; i++)
440     {
441       AlignmentAnnotation aa = av.getAlignment()
442               .getAlignmentAnnotation()[i];
443       if (aa.label.equals(annotation.label)
444               && (annotation.getCalcId() == null ? aa.getCalcId() == null
445                       : annotation.getCalcId().equals(aa.getCalcId())))
446       {
447         if (aa.threshold == null)
448         {
449           aa.threshold = new GraphLine(annotation.threshold);
450         }
451         else
452         {
453           aa.threshold.value = thr;
454         }
455       }
456     }
457   }
458
459   public AlignmentAnnotation getCurrentAnnotation()
460   {
461     return currentAnnotation;
462   }
463
464   protected void setCurrentAnnotation(AlignmentAnnotation annotation)
465   {
466     this.currentAnnotation = annotation;
467   }
468
469   /**
470    * update associated view model and trigger any necessary repaints.
471    * 
472    * @param updateAllAnnotation
473    */
474   protected abstract void valueChanged(boolean updateAllAnnotation);
475
476   protected abstract void updateView();
477
478   protected abstract void reset();
479
480   protected String getAnnotationMenuLabel(AlignmentAnnotation ann)
481   {
482     return annotationLabels.get(ann);
483   }
484
485   protected void jbInit()
486   {
487     ok.setOpaque(false);
488     ok.setText(MessageManager.getString("action.ok"));
489     ok.addActionListener(new ActionListener()
490     {
491       @Override
492       public void actionPerformed(ActionEvent e)
493       {
494         ok_actionPerformed();
495       }
496     });
497
498     cancel.setOpaque(false);
499     cancel.setText(MessageManager.getString("action.cancel"));
500     cancel.addActionListener(new ActionListener()
501     {
502       @Override
503       public void actionPerformed(ActionEvent e)
504       {
505         cancel_actionPerformed();
506       }
507     });
508
509     annotations.addItemListener(new ItemListener()
510     {
511       @Override
512       public void itemStateChanged(ItemEvent e)
513       {
514         selectedAnnotationChanged();
515       }
516     });
517     annotations.setToolTipText(
518             MessageManager.getString("info.select_annotation_row"));
519
520     threshold.addActionListener(new ActionListener()
521     {
522       @Override
523       public void actionPerformed(ActionEvent e)
524       {
525         threshold_actionPerformed();
526       }
527     });
528
529     thresholdValue.setEnabled(false);
530     thresholdValue.setColumns(7);
531     thresholdValue.addActionListener(new ActionListener()
532     {
533       @Override
534       public void actionPerformed(ActionEvent e)
535       {
536         thresholdValue_actionPerformed();
537       }
538     });
539
540     percentThreshold
541             .setText(MessageManager.getString("label.as_percentage"));
542     percentThreshold.addActionListener(new ActionListener()
543     {
544       @Override
545       public void actionPerformed(ActionEvent e)
546       {
547         if (!adjusting)
548         {
549           percentageValue_actionPerformed();
550         }
551       }
552     });
553     slider.setPaintLabels(false);
554     slider.setPaintTicks(true);
555     slider.setBackground(Color.white);
556     slider.setEnabled(false);
557     slider.setOpaque(false);
558     slider.setPreferredSize(new Dimension(100, 32));
559   }
560
561   public JComboBox<String> getThreshold()
562   {
563     return threshold;
564   }
565
566   public void setThreshold(JComboBox<String> thresh)
567   {
568     this.threshold = thresh;
569   }
570
571   public JComboBox<String> getAnnotations()
572   {
573     return annotations;
574   }
575
576   public void setAnnotations(JComboBox<String> anns)
577   {
578     this.annotations = anns;
579   }
580
581   protected void sliderDragReleased()
582   {
583     if (sliderDragging)
584     {
585       sliderDragging = false;
586       valueChanged(true);
587     }
588   }
589
590   /**
591    * Sets the min-max range and current value of the slider, with rescaling from
592    * true values to slider range as required
593    * 
594    * @param min
595    * @param max
596    * @param value
597    */
598   protected void setSliderModel(float min, float max, float value)
599   {
600     slider.setSliderModel(min, max, value);
601
602     /*
603      * tick mark every 10th position
604      */
605     slider.setMajorTickSpacing(
606             (slider.getMaximum() - slider.getMinimum()) / 10);
607   }
608 }