6f72e10a7a790cba0cff2ad9811e01fc6d88cf99
[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    * Answers the value of the slider position as a percentage between minimum and
187    * maximum of its range
188    * 
189    * @return
190    */
191   protected float getSliderPercentageValue()
192   {
193     return slider.getSliderPercentageValue();
194   }
195
196   /**
197    * Sets the slider position for a given percentage value of its min-max range
198    * 
199    * @param pct
200    */
201   protected void setSliderPercentageValue(float pct)
202   {
203     slider.setSliderPercentageValue(pct);
204   }
205
206   protected void addSliderMouseListeners()
207   {
208
209     slider.addMouseListener(new MouseAdapter()
210     {
211       @Override
212       public void mousePressed(MouseEvent e)
213       {
214         sliderDragging = true;
215         super.mousePressed(e);
216       }
217
218       @Override
219       public void mouseDragged(MouseEvent e)
220       {
221         sliderDragging = true;
222         super.mouseDragged(e);
223       }
224
225       @Override
226       public void mouseReleased(MouseEvent evt)
227       {
228         sliderDragReleased();
229       }
230     });
231   }
232
233   /**
234    * Builds and returns a list of menu items (display text) for choice of
235    * annotation. Also builds maps between annotations, their positions in the
236    * list, and their display labels in the list.
237    * 
238    * @param isSeqAssociated
239    * @return
240    */
241   public Vector<String> getAnnotationItems(boolean isSeqAssociated)
242   {
243     annotationLabels = new HashMap<>();
244
245     Vector<String> list = new Vector<>();
246     int index = 1;
247     int[] anmap = new int[av.getAlignment()
248             .getAlignmentAnnotation().length];
249     seqAssociated.setEnabled(false);
250     for (int i = 0; i < av.getAlignment()
251             .getAlignmentAnnotation().length; i++)
252     {
253       AlignmentAnnotation annotation = av.getAlignment()
254               .getAlignmentAnnotation()[i];
255       if (annotation.sequenceRef == null)
256       {
257         if (isSeqAssociated)
258         {
259           continue;
260         }
261       }
262       else
263       {
264         seqAssociated.setEnabled(true);
265       }
266       String label = annotation.label;
267       // add associated sequence ID if available
268       if (!isSeqAssociated && annotation.sequenceRef != null)
269       {
270         label = label + "_" + annotation.sequenceRef.getName();
271       }
272       // make label unique
273       if (!list.contains(label))
274       {
275         anmap[list.size()] = i;
276         list.add(label);
277         annotationLabels.put(annotation, label);
278       }
279       else
280       {
281         if (!isSeqAssociated)
282         {
283           anmap[list.size()] = i;
284           label = label + "_" + (index++);
285           list.add(label);
286           annotationLabels.put(annotation, label);
287         }
288       }
289     }
290     this.annmap = new int[list.size()];
291     System.arraycopy(anmap, 0, this.annmap, 0, this.annmap.length);
292     return list;
293   }
294
295   protected int getSelectedThresholdItem(int indexValue)
296   {
297     int selectedThresholdItem = -1;
298     if (indexValue == 1)
299     {
300       selectedThresholdItem = AnnotationColourGradient.ABOVE_THRESHOLD;
301     }
302     else if (indexValue == 2)
303     {
304       selectedThresholdItem = AnnotationColourGradient.BELOW_THRESHOLD;
305     }
306     return selectedThresholdItem;
307   }
308
309   public void ok_actionPerformed()
310   {
311     try
312     {
313       frame.setClosed(true);
314     } catch (Exception ex)
315     {
316     }
317   }
318
319   public void cancel_actionPerformed()
320   {
321     reset();
322     ap.paintAlignment(true, true);
323     try
324     {
325       frame.setClosed(true);
326     } catch (Exception ex)
327     {
328     }
329   }
330
331   protected void thresholdCheck_actionPerformed()
332   {
333     updateView();
334   }
335
336   protected void selectedAnnotationChanged()
337   {
338     updateView();
339   }
340
341   protected void threshold_actionPerformed()
342   {
343     updateView();
344   }
345
346   /**
347    * Updates the slider position, and the display, for an update in the slider's
348    * text input field
349    */
350   protected void thresholdValue_actionPerformed()
351   {
352     try
353     {
354       float f = Float.parseFloat(thresholdValue.getText());
355       if (percentThreshold.isSelected())
356       {
357         setSliderPercentageValue(f);
358       }
359       else
360       {
361         setSliderValue(f);
362       }
363       updateView();
364     } catch (NumberFormatException ex)
365     {
366     }
367   }
368
369   protected void percentageValue_actionPerformed()
370   {
371     setThresholdValueText();
372   }
373
374   protected void thresholdIsMin_actionPerformed()
375   {
376     updateView();
377   }
378
379   protected void populateThresholdComboBox(JComboBox<String> thresh)
380   {
381     thresh.addItem(MessageManager
382             .getString("label.threshold_feature_no_threshold"));
383     thresh.addItem(MessageManager
384             .getString("label.threshold_feature_above_threshold"));
385     thresh.addItem(MessageManager
386             .getString("label.threshold_feature_below_threshold"));
387   }
388
389   /**
390    * Rebuilds the drop-down list of annotations to choose from when the 'per
391    * sequence only' checkbox is checked or unchecked.
392    * 
393    * @param anns
394    */
395   protected void seqAssociated_actionPerformed(JComboBox<String> anns)
396   {
397     adjusting = true;
398     String cursel = (String) anns.getSelectedItem();
399     boolean isvalid = false;
400     boolean isseqs = seqAssociated.isSelected();
401     anns.removeAllItems();
402     for (String anitem : getAnnotationItems(seqAssociated.isSelected()))
403     {
404       if (anitem.equals(cursel) || (isseqs && cursel.startsWith(anitem)))
405       {
406         isvalid = true;
407         cursel = anitem;
408       }
409       anns.addItem(anitem);
410     }
411     if (isvalid)
412     {
413       anns.setSelectedItem(cursel);
414     }
415     else
416     {
417       if (anns.getItemCount() > 0)
418       {
419         anns.setSelectedIndex(0);
420       }
421     }
422     adjusting = false;
423
424     updateView();
425   }
426
427   protected void propagateSeqAssociatedThreshold(boolean allAnnotation,
428           AlignmentAnnotation annotation)
429   {
430     if (annotation.sequenceRef == null || annotation.threshold == null)
431     {
432       return;
433     }
434
435     float thr = annotation.threshold.value;
436     for (int i = 0; i < av.getAlignment()
437             .getAlignmentAnnotation().length; i++)
438     {
439       AlignmentAnnotation aa = av.getAlignment()
440               .getAlignmentAnnotation()[i];
441       if (aa.label.equals(annotation.label)
442               && (annotation.getCalcId() == null ? aa.getCalcId() == null
443                       : annotation.getCalcId().equals(aa.getCalcId())))
444       {
445         if (aa.threshold == null)
446         {
447           aa.threshold = new GraphLine(annotation.threshold);
448         }
449         else
450         {
451           aa.threshold.value = thr;
452         }
453       }
454     }
455   }
456
457   public AlignmentAnnotation getCurrentAnnotation()
458   {
459     return currentAnnotation;
460   }
461
462   protected void setCurrentAnnotation(AlignmentAnnotation annotation)
463   {
464     this.currentAnnotation = annotation;
465   }
466
467   /**
468    * update associated view model and trigger any necessary repaints.
469    * 
470    * @param updateAllAnnotation
471    */
472   protected abstract void valueChanged(boolean updateAllAnnotation);
473
474   protected abstract void updateView();
475
476   protected abstract void reset();
477
478   protected String getAnnotationMenuLabel(AlignmentAnnotation ann)
479   {
480     return annotationLabels.get(ann);
481   }
482
483   protected void jbInit()
484   {
485     ok.setOpaque(false);
486     ok.setText(MessageManager.getString("action.ok"));
487     ok.addActionListener(new ActionListener()
488     {
489       @Override
490       public void actionPerformed(ActionEvent e)
491       {
492         ok_actionPerformed();
493       }
494     });
495
496     cancel.setOpaque(false);
497     cancel.setText(MessageManager.getString("action.cancel"));
498     cancel.addActionListener(new ActionListener()
499     {
500       @Override
501       public void actionPerformed(ActionEvent e)
502       {
503         cancel_actionPerformed();
504       }
505     });
506
507     annotations.addItemListener(new ItemListener()
508     {
509       @Override
510       public void itemStateChanged(ItemEvent e)
511       {
512         selectedAnnotationChanged();
513       }
514     });
515     annotations.setToolTipText(
516             MessageManager.getString("info.select_annotation_row"));
517
518     threshold.addActionListener(new ActionListener()
519     {
520       @Override
521       public void actionPerformed(ActionEvent e)
522       {
523         threshold_actionPerformed();
524       }
525     });
526
527     thresholdValue.setEnabled(false);
528     thresholdValue.setColumns(7);
529     thresholdValue.addActionListener(new ActionListener()
530     {
531       @Override
532       public void actionPerformed(ActionEvent e)
533       {
534         thresholdValue_actionPerformed();
535       }
536     });
537
538     percentThreshold
539             .setText(MessageManager.getString("label.as_percentage"));
540     percentThreshold.addActionListener(new ActionListener()
541     {
542       @Override
543       public void actionPerformed(ActionEvent e)
544       {
545         if (!adjusting)
546         {
547           percentageValue_actionPerformed();
548         }
549       }
550     });
551     slider.setPaintLabels(false);
552     slider.setPaintTicks(true);
553     slider.setBackground(Color.white);
554     slider.setEnabled(false);
555     slider.setOpaque(false);
556     slider.setPreferredSize(new Dimension(100, 32));
557   }
558
559   public JComboBox<String> getThreshold()
560   {
561     return threshold;
562   }
563
564   public void setThreshold(JComboBox<String> thresh)
565   {
566     this.threshold = thresh;
567   }
568
569   public JComboBox<String> getAnnotations()
570   {
571     return annotations;
572   }
573
574   public void setAnnotations(JComboBox<String> anns)
575   {
576     this.annotations = anns;
577   }
578
579   protected void sliderDragReleased()
580   {
581     if (sliderDragging)
582     {
583       sliderDragging = false;
584       valueChanged(true);
585     }
586   }
587
588   /**
589    * Sets the min-max range and current value of the slider, with rescaling from
590    * true values to slider range as required
591    * 
592    * @param min
593    * @param max
594    * @param value
595    */
596   protected void setSliderModel(float min, float max, float value)
597   {
598     slider.setSliderModel(min, max, value);
599
600     /*
601      * tick mark every 10th position
602      */
603     slider.setMajorTickSpacing(
604             (slider.getMaximum() - slider.getMinimum()) / 10);
605   }
606 }