538c93cfb5e8a3bb71a4f4f19b5705096cd49a4f
[jalview.git] / src / jalview / gui / FeatureTypeSettings.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.api.AlignmentViewPanel;
24 import jalview.api.FeatureColourI;
25 import jalview.bin.Cache;
26 import jalview.datamodel.GraphLine;
27 import jalview.datamodel.features.FeatureAttributes;
28 import jalview.datamodel.features.FeatureAttributes.Datatype;
29 import jalview.datamodel.features.FeatureMatcher;
30 import jalview.datamodel.features.FeatureMatcherI;
31 import jalview.datamodel.features.FeatureMatcherSet;
32 import jalview.datamodel.features.FeatureMatcherSetI;
33 import jalview.io.gff.SequenceOntologyFactory;
34 import jalview.io.gff.SequenceOntologyI;
35 import jalview.schemes.FeatureColour;
36 import jalview.util.ColorUtils;
37 import jalview.util.MessageManager;
38 import jalview.util.matcher.Condition;
39
40 import java.awt.BorderLayout;
41 import java.awt.Color;
42 import java.awt.Dimension;
43 import java.awt.FlowLayout;
44 import java.awt.GridLayout;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.FocusAdapter;
48 import java.awt.event.FocusEvent;
49 import java.awt.event.ItemEvent;
50 import java.awt.event.ItemListener;
51 import java.awt.event.MouseAdapter;
52 import java.awt.event.MouseEvent;
53 import java.text.DecimalFormat;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.Map.Entry;
60
61 import javax.swing.BorderFactory;
62 import javax.swing.BoxLayout;
63 import javax.swing.ButtonGroup;
64 import javax.swing.JButton;
65 import javax.swing.JCheckBox;
66 import javax.swing.JColorChooser;
67 import javax.swing.JComboBox;
68 import javax.swing.JLabel;
69 import javax.swing.JPanel;
70 import javax.swing.JRadioButton;
71 import javax.swing.JSlider;
72 import javax.swing.JTextField;
73 import javax.swing.SwingConstants;
74 import javax.swing.border.LineBorder;
75 import javax.swing.event.ChangeEvent;
76 import javax.swing.event.ChangeListener;
77 import javax.swing.plaf.basic.BasicArrowButton;
78
79 /**
80  * A dialog where the user can configure colour scheme, and any filters, for one
81  * feature type
82  * <p>
83  * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
84  * filter options)
85  */
86 public class FeatureTypeSettings extends JalviewDialog
87 {
88   /*
89    * 'top level' Sequence Ontology terms
90    */
91   private final static String SO_ROOTS = "sequence_variant,sequence_attribute,sequence_collection,sequence_feature";
92
93   private final static String LABEL_18N = MessageManager
94           .getString("label.label");
95
96   private final static String SCORE_18N = MessageManager
97           .getString("label.score");
98
99   private static final int RADIO_WIDTH = 130;
100
101   private static final String COLON = ":";
102
103   private static final int MAX_TOOLTIP_LENGTH = 50;
104
105   private static final int NO_COLOUR_OPTION = 0;
106
107   private static final int MIN_COLOUR_OPTION = 1;
108
109   private static final int MAX_COLOUR_OPTION = 2;
110
111   private static final int ABOVE_THRESHOLD_OPTION = 1;
112
113   private static final int BELOW_THRESHOLD_OPTION = 2;
114
115   private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
116           "##.##");
117
118   /*
119    * FeatureRenderer holds colour scheme and filters for feature types
120    */
121   private final FeatureRenderer fr; // todo refactor to allow interface type
122                                     // here
123
124   /*
125    * the view panel to update when settings change
126    */
127   private final AlignmentViewPanel ap;
128
129   private final String featureType;
130
131   /*
132    * the colour and filters to reset to on Cancel
133    * (including feature sub-types if modified)
134    */
135   private Map<String, FeatureColourI> originalColours;
136
137   private Map<String, FeatureMatcherSetI> originalFilters;
138
139   /*
140    * set flag to true when setting values programmatically,
141    * to avoid invocation of action handlers
142    */
143   private boolean adjusting = false;
144
145   /*
146    * minimum of the value range for graduated colour
147    * (may be for feature score or for a numeric attribute)
148    */
149   private float min;
150
151   /*
152    * maximum of the value range for graduated colour
153    */
154   private float max;
155
156   /*
157    * scale factor for conversion between absolute min-max and slider
158    */
159   private float scaleFactor;
160
161   /*
162    * radio button group, to select what to colour by:
163    * simple colour, by category (text), or graduated
164    */
165   private JRadioButton simpleColour = new JRadioButton();
166
167   private JRadioButton byCategory = new JRadioButton();
168
169   private JRadioButton graduatedColour = new JRadioButton();
170
171   /**
172    * colours and filters are shown in tabbed view or single content pane
173    */
174   JPanel coloursPanel, filtersPanel;
175
176   JPanel singleColour = new JPanel();
177
178   private JPanel minColour = new JPanel();
179
180   private JPanel maxColour = new JPanel();
181
182   private JComboBox<String> threshold = new JComboBox<>();
183
184   private JSlider slider = new JSlider();
185
186   private JTextField thresholdValue = new JTextField(20);
187
188   private JCheckBox thresholdIsMin = new JCheckBox();
189
190   private GraphLine threshline;
191
192   private ActionListener featureSettings = null;
193
194   private ActionListener changeColourAction;
195
196   /*
197    * choice of option for 'colour for no value'
198    */
199   private JComboBox<String> noValueCombo;
200
201   /*
202    * choice of what to colour by text (Label or attribute)
203    */
204   private JComboBox<String> colourByTextCombo;
205
206   /*
207    * choice of what to colour by range (Score or attribute)
208    */
209   private JComboBox<String> colourByRangeCombo;
210
211   private JRadioButton andFilters;
212
213   private JRadioButton orFilters;
214
215   /*
216    * filters for the currently selected feature type
217    */
218   private List<FeatureMatcherI> filters;
219
220   private JPanel chooseFiltersPanel;
221
222   /*
223    * the root Sequence Ontology terms (if any) that is a parent of
224    * the current feature type
225    */
226   private String rootSOTerm;
227
228   /*
229    * feature types present in Feature Renderer which have the same Sequence
230    * Ontology root parent as the one this editor is acting on
231    */
232   private final List<String> peerSoTerms;
233
234   /*
235    * if true, filter or colour settings are also applied to 
236    * any sub-types of rootSOTerm in the Sequence Ontology
237    */
238   private boolean applyFiltersToSubtypes;
239
240   private boolean applyColourToSubtypes;
241
242   /**
243    * Constructor
244    * 
245    * @param frender
246    * @param theType
247    */
248   public FeatureTypeSettings(FeatureRenderer frender, String theType)
249   {
250     this.fr = frender;
251     this.featureType = theType;
252     ap = fr.ap;
253
254     peerSoTerms = findSequenceOntologyPeers(this.featureType);
255
256     /*
257      * save original colours and filters for this feature type
258      * and any sub-types, to restore on Cancel
259      */
260     originalFilters = new HashMap<>();
261     originalFilters.put(theType, fr.getFeatureFilter(theType));
262     originalColours = new HashMap<>();
263     originalColours.put(theType, fr.getFeatureColours().get(theType));
264     for (String child : peerSoTerms)
265     {
266       originalFilters.put(child, fr.getFeatureFilter(child));
267       originalColours.put(child, fr.getFeatureColours().get(child));
268     }
269
270     adjusting = true;
271
272     try
273     {
274       initialise();
275     } catch (Exception ex)
276     {
277       ex.printStackTrace();
278       return;
279     }
280
281     updateColoursTab();
282
283     updateFiltersTab();
284
285     adjusting = false;
286
287     colourChanged(false);
288
289     String title = MessageManager
290             .formatMessage("label.display_settings_for", new String[]
291             { theType });
292     initDialogFrame(this, true, false, title, 580, 500);
293     waitForInput();
294   }
295
296   /**
297    * Answers a (possibly empty) list of feature types known to the Feature
298    * Renderer which share a top level Sequence Ontology parent with the current
299    * feature type. The current type is not included.
300    * 
301    * @return
302    */
303   protected List<String> findSequenceOntologyPeers(String featureType)
304   {
305     List<String> peers = new ArrayList<>();
306
307     /*
308      * first find the SO term (if any) that is the root
309      * parent of the current type
310      */
311     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
312     String[] roots = Cache.getDefault("SO_ROOTS", SO_ROOTS).split(",");
313     rootSOTerm = null;
314     for (String root : roots)
315     {
316       if (so.isA(featureType, root.trim()))
317       {
318         rootSOTerm = root;
319         break;
320       }
321     }
322     if (rootSOTerm == null)
323     {
324       /*
325        * feature type is not an SO term
326        */
327       return peers;
328     }
329
330     List<String> types = fr.getRenderOrder();
331     for (String type : types)
332     {
333       if (!type.equals(featureType) && so.isA(type, rootSOTerm))
334       {
335         peers.add(type);
336       }
337
338     }
339     Collections.sort(peers); // sort for ease of reading in tooltip
340     return peers;
341   }
342
343   /**
344    * Configures the widgets on the Colours tab according to the current feature
345    * colour scheme
346    */
347   private void updateColoursTab()
348   {
349     FeatureColourI fc = fr.getFeatureColours().get(featureType);
350
351     /*
352      * suppress action handling while updating values programmatically
353      */
354     adjusting = true;
355     try
356     {
357       /*
358        * single colour
359        */
360       if (fc.isSimpleColour())
361       {
362         singleColour.setBackground(fc.getColour());
363         singleColour.setForeground(fc.getColour());
364         simpleColour.setSelected(true);
365       }
366
367       /*
368        * colour by text (Label or attribute text)
369        */
370       if (fc.isColourByLabel())
371       {
372         byCategory.setSelected(true);
373         colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
374         if (fc.isColourByAttribute())
375         {
376           String[] attributeName = fc.getAttributeName();
377           colourByTextCombo.setSelectedItem(
378                   FeatureMatcher.toAttributeDisplayName(attributeName));
379         }
380         else
381         {
382           colourByTextCombo.setSelectedItem(LABEL_18N);
383         }
384       }
385       else
386       {
387         colourByTextCombo.setEnabled(false);
388       }
389
390       if (!fc.isGraduatedColour())
391       {
392         colourByRangeCombo.setEnabled(false);
393         minColour.setEnabled(false);
394         maxColour.setEnabled(false);
395         noValueCombo.setEnabled(false);
396         threshold.setEnabled(false);
397         slider.setEnabled(false);
398         thresholdValue.setEnabled(false);
399         thresholdIsMin.setEnabled(false);
400         return;
401       }
402
403       /*
404        * Graduated colour, by score or attribute value range
405        */
406       graduatedColour.setSelected(true);
407       updateColourMinMax(); // ensure min, max are set
408       colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
409       minColour.setEnabled(true);
410       maxColour.setEnabled(true);
411       noValueCombo.setEnabled(true);
412       threshold.setEnabled(true);
413       minColour.setBackground(fc.getMinColour());
414       maxColour.setBackground(fc.getMaxColour());
415
416       if (fc.isColourByAttribute())
417       {
418         String[] attributeName = fc.getAttributeName();
419         colourByRangeCombo.setSelectedItem(
420                 FeatureMatcher.toAttributeDisplayName(attributeName));
421       }
422       else
423       {
424         colourByRangeCombo.setSelectedItem(SCORE_18N);
425       }
426       Color noColour = fc.getNoColour();
427       if (noColour == null)
428       {
429         noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
430       }
431       else if (noColour.equals(fc.getMinColour()))
432       {
433         noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
434       }
435       else if (noColour.equals(fc.getMaxColour()))
436       {
437         noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
438       }
439
440       /*
441        * update min-max scaling if there is a range to work with,
442        * else disable the widgets (this shouldn't happen if only 
443        * valid options are offered in the combo box)
444        */
445       scaleFactor = (max == min) ? 1f : 100f / (max - min);
446       float range = (max - min) * scaleFactor;
447       slider.setMinimum((int) (min * scaleFactor));
448       slider.setMaximum((int) (max * scaleFactor));
449       slider.setMajorTickSpacing((int) (range / 10f));
450
451       threshline = new GraphLine((max - min) / 2f, "Threshold",
452               Color.black);
453       threshline.value = fc.getThreshold();
454
455       if (fc.hasThreshold())
456       {
457         threshold.setSelectedIndex(
458                 fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
459                         : BELOW_THRESHOLD_OPTION);
460         slider.setEnabled(true);
461         slider.setValue((int) (fc.getThreshold() * scaleFactor));
462         thresholdValue.setText(String.valueOf(fc.getThreshold()));
463         thresholdValue.setEnabled(true);
464         thresholdIsMin.setEnabled(true);
465       }
466       else
467       {
468         slider.setEnabled(false);
469         thresholdValue.setEnabled(false);
470         thresholdIsMin.setEnabled(false);
471       }
472       thresholdIsMin.setSelected(!fc.isAutoScaled());
473     } finally
474     {
475       adjusting = false;
476     }
477   }
478
479   /**
480    * Configures the initial layout
481    */
482   private void initialise()
483   {
484     this.setLayout(new BorderLayout());
485
486     /*
487      * an ActionListener that applies colour changes
488      */
489     changeColourAction = new ActionListener()
490     {
491       @Override
492       public void actionPerformed(ActionEvent e)
493       {
494         colourChanged(true);
495       }
496     };
497
498     /*
499      * first panel/tab: colour options
500      */
501     JPanel coloursPanel = initialiseColoursPanel();
502     this.add(coloursPanel, BorderLayout.NORTH);
503
504     /*
505      * second panel/tab: filter options
506      */
507     JPanel filtersPanel = initialiseFiltersPanel();
508     this.add(filtersPanel, BorderLayout.CENTER);
509
510     JPanel okCancelPanel = initialiseOkCancelPanel();
511
512     this.add(okCancelPanel, BorderLayout.SOUTH);
513   }
514
515   /**
516    * Updates the min-max range if Colour By selected item is Score, or an
517    * attribute, with a min-max range
518    */
519   protected void updateColourMinMax()
520   {
521     if (!graduatedColour.isSelected())
522     {
523       return;
524     }
525
526     String colourBy = (String) colourByRangeCombo.getSelectedItem();
527     float[] minMax = getMinMax(colourBy);
528
529     if (minMax != null)
530     {
531       min = minMax[0];
532       max = minMax[1];
533     }
534   }
535
536   /**
537    * Retrieves the min-max range:
538    * <ul>
539    * <li>of feature score, if colour or filter is by Score</li>
540    * <li>else of the selected attribute</li>
541    * </ul>
542    * 
543    * @param attName
544    * @return
545    */
546   private float[] getMinMax(String attName)
547   {
548     float[] minMax = null;
549     if (SCORE_18N.equals(attName))
550     {
551       minMax = fr.getMinMax().get(featureType)[0];
552     }
553     else
554     {
555       // colour by attribute range
556       minMax = FeatureAttributes.getInstance().getMinMax(featureType,
557               FeatureMatcher.fromAttributeDisplayName(attName));
558     }
559     return minMax;
560   }
561
562   /**
563    * Lay out fields for graduated colour (by score or attribute value)
564    * 
565    * @return
566    */
567   private JPanel initialiseGraduatedColourPanel()
568   {
569     JPanel graduatedColourPanel = new JPanel();
570     graduatedColourPanel.setLayout(
571             new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
572     JvSwingUtils.createTitledBorder(graduatedColourPanel,
573             MessageManager.getString("label.graduated_colour"), true);
574     graduatedColourPanel.setBackground(Color.white);
575
576     /*
577      * first row: graduated colour radio button, score/attribute drop-down
578      */
579     JPanel graduatedChoicePanel = new JPanel(
580             new FlowLayout(FlowLayout.LEFT));
581     graduatedChoicePanel.setBackground(Color.white);
582     graduatedColour = new JRadioButton(
583             MessageManager.getString("label.by_range_of") + COLON);
584     graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
585     graduatedColour.addItemListener(new ItemListener()
586     {
587       @Override
588       public void itemStateChanged(ItemEvent e)
589       {
590         if (graduatedColour.isSelected())
591         {
592           colourChanged(true);
593         }
594       }
595     });
596     graduatedChoicePanel.add(graduatedColour);
597
598     List<String[]> attNames = FeatureAttributes.getInstance()
599             .getAttributes(featureType);
600     colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
601     colourByRangeCombo.addItemListener(new ItemListener()
602     {
603       @Override
604       public void itemStateChanged(ItemEvent e)
605       {
606         colourChanged(true);
607       }
608     });
609
610     /*
611      * disable graduated colour option if no range found
612      */
613     graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
614
615     graduatedChoicePanel.add(colourByRangeCombo);
616     graduatedColourPanel.add(graduatedChoicePanel);
617
618     /*
619      * second row - min/max/no colours
620      */
621     JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
622     colourRangePanel.setBackground(Color.white);
623     graduatedColourPanel.add(colourRangePanel);
624
625     minColour.setFont(JvSwingUtils.getLabelFont());
626     minColour.setBorder(BorderFactory.createLineBorder(Color.black));
627     minColour.setPreferredSize(new Dimension(40, 20));
628     minColour.setToolTipText(MessageManager.getString("label.min_colour"));
629     minColour.addMouseListener(new MouseAdapter()
630     {
631       @Override
632       public void mousePressed(MouseEvent e)
633       {
634         if (minColour.isEnabled())
635         {
636           showColourChooser(minColour, "label.select_colour_minimum_value");
637         }
638       }
639     });
640
641     maxColour.setFont(JvSwingUtils.getLabelFont());
642     maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
643     maxColour.setPreferredSize(new Dimension(40, 20));
644     maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
645     maxColour.addMouseListener(new MouseAdapter()
646     {
647       @Override
648       public void mousePressed(MouseEvent e)
649       {
650         if (maxColour.isEnabled())
651         {
652           showColourChooser(maxColour, "label.select_colour_maximum_value");
653         }
654       }
655     });
656     maxColour.setBorder(new LineBorder(Color.black));
657
658     /*
659      * if not set, default max colour to last plain colour,
660      * and make min colour a pale version of max colour
661      */
662     FeatureColourI originalColour = originalColours.get(featureType);
663     Color max = originalColour.getMaxColour();
664     if (max == null)
665     {
666       max = originalColour.getColour();
667       minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
668     }
669     else
670     {
671       maxColour.setBackground(max);
672       minColour.setBackground(originalColour.getMinColour());
673     }
674
675     noValueCombo = new JComboBox<>();
676     noValueCombo.addItem(MessageManager.getString("label.no_colour"));
677     noValueCombo.addItem(MessageManager.getString("label.min_colour"));
678     noValueCombo.addItem(MessageManager.getString("label.max_colour"));
679     noValueCombo.addItemListener(new ItemListener()
680     {
681       @Override
682       public void itemStateChanged(ItemEvent e)
683       {
684         colourChanged(true);
685       }
686     });
687
688     JLabel minText = new JLabel(
689             MessageManager.getString("label.min_value") + COLON);
690     minText.setFont(JvSwingUtils.getLabelFont());
691     JLabel maxText = new JLabel(
692             MessageManager.getString("label.max_value") + COLON);
693     maxText.setFont(JvSwingUtils.getLabelFont());
694     JLabel noText = new JLabel(
695             MessageManager.getString("label.no_value") + COLON);
696     noText.setFont(JvSwingUtils.getLabelFont());
697
698     colourRangePanel.add(minText);
699     colourRangePanel.add(minColour);
700     colourRangePanel.add(maxText);
701     colourRangePanel.add(maxColour);
702     colourRangePanel.add(noText);
703     colourRangePanel.add(noValueCombo);
704
705     /*
706      * third row - threshold options and value
707      */
708     JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
709     thresholdPanel.setBackground(Color.white);
710     graduatedColourPanel.add(thresholdPanel);
711
712     threshold.addActionListener(changeColourAction);
713     threshold.setToolTipText(MessageManager
714             .getString("label.threshold_feature_display_by_score"));
715     threshold.addItem(MessageManager
716             .getString("label.threshold_feature_no_threshold")); // index 0
717     threshold.addItem(MessageManager
718             .getString("label.threshold_feature_above_threshold")); // index 1
719     threshold.addItem(MessageManager
720             .getString("label.threshold_feature_below_threshold")); // index 2
721
722     thresholdValue.addActionListener(new ActionListener()
723     {
724       @Override
725       public void actionPerformed(ActionEvent e)
726       {
727         thresholdValue_actionPerformed();
728       }
729     });
730     thresholdValue.addFocusListener(new FocusAdapter()
731     {
732       @Override
733       public void focusLost(FocusEvent e)
734       {
735         thresholdValue_actionPerformed();
736       }
737     });
738     slider.setPaintLabels(false);
739     slider.setPaintTicks(true);
740     slider.setBackground(Color.white);
741     slider.setEnabled(false);
742     slider.setOpaque(false);
743     slider.setPreferredSize(new Dimension(100, 32));
744     slider.setToolTipText(
745             MessageManager.getString("label.adjust_threshold"));
746
747     slider.addChangeListener(new ChangeListener()
748     {
749       @Override
750       public void stateChanged(ChangeEvent evt)
751       {
752         if (!adjusting)
753         {
754           thresholdValue
755                   .setText(String.valueOf(slider.getValue() / scaleFactor));
756           thresholdValue.setBackground(Color.white); // to reset red for invalid
757           sliderValueChanged();
758         }
759       }
760     });
761     slider.addMouseListener(new MouseAdapter()
762     {
763       @Override
764       public void mouseReleased(MouseEvent evt)
765       {
766         /*
767          * only update Overview and/or structure colouring
768          * when threshold slider drag ends (mouse up)
769          */
770         if (ap != null)
771         {
772           ap.paintAlignment(true, true);
773         }
774       }
775     });
776
777     thresholdValue.setEnabled(false);
778     thresholdValue.setColumns(7);
779
780     thresholdPanel.add(threshold);
781     thresholdPanel.add(slider);
782     thresholdPanel.add(thresholdValue);
783
784     thresholdIsMin.setBackground(Color.white);
785     thresholdIsMin
786             .setText(MessageManager.getString("label.threshold_minmax"));
787     thresholdIsMin.setToolTipText(MessageManager
788             .getString("label.toggle_absolute_relative_display_threshold"));
789     thresholdIsMin.addActionListener(changeColourAction);
790     thresholdPanel.add(thresholdIsMin);
791
792     return graduatedColourPanel;
793   }
794
795   /**
796    * Lay out OK and Cancel buttons
797    * 
798    * @return
799    */
800   private JPanel initialiseOkCancelPanel()
801   {
802     JPanel okCancelPanel = new JPanel();
803     // okCancelPanel.setBackground(Color.white);
804     okCancelPanel.add(ok);
805     okCancelPanel.add(cancel);
806     return okCancelPanel;
807   }
808
809   /**
810    * Lay out Colour options panel, containing
811    * <ul>
812    * <li>plain colour, with colour picker</li>
813    * <li>colour by text, with choice of Label or other attribute</li>
814    * <li>colour by range, of score or other attribute, when available</li>
815    * </ul>
816    * 
817    * @return
818    */
819   private JPanel initialiseColoursPanel()
820   {
821     JPanel colourByPanel = new JPanel();
822     colourByPanel.setBackground(Color.white);
823     colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
824     JvSwingUtils.createTitledBorder(colourByPanel,
825             MessageManager.getString("action.colour"), true);
826
827     /*
828      * option to apply colour to peer types as well (if there are any)
829      */
830     if (!peerSoTerms.isEmpty())
831     {
832       applyColourToSubtypes = false;
833       colourByPanel.add(initSubtypesPanel(false));
834     }
835
836     /*
837      * simple colour radio button and colour picker
838      */
839     JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
840     simpleColourPanel.setBackground(Color.white);
841     colourByPanel.add(simpleColourPanel);
842
843     simpleColour = new JRadioButton(
844             MessageManager.getString("label.simple_colour"));
845     simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
846     simpleColour.addItemListener(new ItemListener()
847     {
848       @Override
849       public void itemStateChanged(ItemEvent e)
850       {
851         if (simpleColour.isSelected() && !adjusting)
852         {
853           colourChanged(true);
854         }
855       }
856     });
857
858     singleColour.setFont(JvSwingUtils.getLabelFont());
859     singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
860     singleColour.setPreferredSize(new Dimension(40, 20));
861     FeatureColourI originalColour = originalColours.get(featureType);
862     singleColour.setBackground(originalColour.getColour());
863     singleColour.setForeground(originalColour.getColour());
864
865     singleColour.addMouseListener(new MouseAdapter()
866     {
867       @Override
868       public void mousePressed(MouseEvent e)
869       {
870         if (simpleColour.isSelected())
871         {
872           showColourChooser(singleColour, "label.select_colour");
873         }
874       }
875     });
876     simpleColourPanel.add(simpleColour); // radio button
877     simpleColourPanel.add(singleColour); // colour picker button
878
879     /*
880      * colour by text (category) radio button and drop-down choice list
881      */
882     JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
883     byTextPanel.setBackground(Color.white);
884     JvSwingUtils.createTitledBorder(byTextPanel,
885             MessageManager.getString("label.colour_by_text"), true);
886     colourByPanel.add(byTextPanel);
887     byCategory = new JRadioButton(
888             MessageManager.getString("label.by_text_of") + COLON);
889     byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
890     byCategory.addItemListener(new ItemListener()
891     {
892       @Override
893       public void itemStateChanged(ItemEvent e)
894       {
895         if (byCategory.isSelected())
896         {
897           colourChanged(true);
898         }
899       }
900     });
901     byTextPanel.add(byCategory);
902
903     List<String[]> attNames = FeatureAttributes.getInstance()
904             .getAttributes(featureType);
905     colourByTextCombo = populateAttributesDropdown(attNames, false, true);
906     colourByTextCombo.addItemListener(new ItemListener()
907     {
908       @Override
909       public void itemStateChanged(ItemEvent e)
910       {
911         colourChanged(true);
912       }
913     });
914     byTextPanel.add(colourByTextCombo);
915
916     /*
917      * graduated colour panel
918      */
919     JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
920     colourByPanel.add(graduatedColourPanel);
921
922     /*
923      * 3 radio buttons select between simple colour, 
924      * by category (text), or graduated
925      */
926     ButtonGroup bg = new ButtonGroup();
927     bg.add(simpleColour);
928     bg.add(byCategory);
929     bg.add(graduatedColour);
930
931     return colourByPanel;
932   }
933
934   /**
935    * Constructs and returns a panel with a checkbox for the option to apply any
936    * changes also to sub-types of the feature type
937    * 
938    * @return
939    */
940   protected JPanel initSubtypesPanel(final boolean forFilters)
941   {
942     JPanel toSubtypes = new JPanel(new FlowLayout(FlowLayout.LEFT));
943     toSubtypes.setBackground(Color.WHITE);
944     JCheckBox applyToSubtypesCB = new JCheckBox(MessageManager
945             .formatMessage("label.apply_to_subtypes", rootSOTerm));
946     applyToSubtypesCB.setToolTipText(getSubtypesTooltip());
947     applyToSubtypesCB.addActionListener(new ActionListener()
948     {
949       /*
950        * reset and reapply settings on toggle of checkbox
951        */
952       @Override
953       public void actionPerformed(ActionEvent e)
954       {
955         if (forFilters)
956         {
957           applyFiltersToSubtypes = applyToSubtypesCB.isSelected();
958           restoreOriginalFilters();
959           filtersChanged();
960         }
961         else
962         {
963           applyColourToSubtypes = applyToSubtypesCB.isSelected();
964           restoreOriginalColours();
965           colourChanged(true);
966         }
967       }
968     });
969     toSubtypes.add(applyToSubtypesCB);
970
971     return toSubtypes;
972   }
973
974   private void showColourChooser(JPanel colourPanel, String key)
975   {
976     Color col = JColorChooser.showDialog(this,
977             MessageManager.getString(key), colourPanel.getBackground());
978     if (col != null)
979     {
980       colourPanel.setBackground(col);
981       colourPanel.setForeground(col);
982     }
983     colourPanel.repaint();
984     colourChanged(true);
985   }
986
987   /**
988    * Constructs and sets the selected colour options as the colour for the
989    * feature type, and repaints the alignment, and optionally the Overview
990    * and/or structure viewer if open
991    * 
992    * @param updateStructsAndOverview
993    */
994   void colourChanged(boolean updateStructsAndOverview)
995   {
996     if (adjusting)
997     {
998       /*
999        * ignore action handlers while setting values programmatically
1000        */
1001       return;
1002     }
1003
1004     /*
1005      * ensure min-max range is for the latest choice of 
1006      * 'graduated colour by'
1007      */
1008     updateColourMinMax();
1009
1010     FeatureColourI acg = makeColourFromInputs();
1011
1012     /*
1013      * save the colour, and set on subtypes if selected
1014      */
1015     fr.setColour(featureType, acg);
1016     if (applyColourToSubtypes)
1017     {
1018       for (String child : peerSoTerms)
1019       {
1020         fr.setColour(child, acg);
1021       }
1022     }
1023     refreshFeatureSettings();
1024     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
1025
1026     updateColoursTab();
1027   }
1028
1029   /**
1030    * Converts the input values into an instance of FeatureColour
1031    * 
1032    * @return
1033    */
1034   private FeatureColourI makeColourFromInputs()
1035   {
1036     /*
1037      * min-max range is to (or from) threshold value if 
1038      * 'threshold is min/max' is selected 
1039      */
1040
1041     float thresh = 0f;
1042     try
1043     {
1044       thresh = Float.valueOf(thresholdValue.getText());
1045     } catch (NumberFormatException e)
1046     {
1047       // invalid inputs are already handled on entry
1048     }
1049     float minValue = min;
1050     float maxValue = max;
1051     final int thresholdOption = threshold.getSelectedIndex();
1052     if (thresholdIsMin.isSelected()
1053             && thresholdOption == ABOVE_THRESHOLD_OPTION)
1054     {
1055       minValue = thresh;
1056     }
1057     if (thresholdIsMin.isSelected()
1058             && thresholdOption == BELOW_THRESHOLD_OPTION)
1059     {
1060       maxValue = thresh;
1061     }
1062     Color noColour = null;
1063     if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
1064     {
1065       noColour = minColour.getBackground();
1066     }
1067     else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
1068     {
1069       noColour = maxColour.getBackground();
1070     }
1071
1072     /*
1073      * construct a colour that 'remembers' all the options, including
1074      * those not currently selected
1075      */
1076     FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
1077             minColour.getBackground(), maxColour.getBackground(), noColour,
1078             minValue, maxValue);
1079
1080     /*
1081      * easiest case - a single colour
1082      */
1083     if (simpleColour.isSelected())
1084     {
1085       ((FeatureColour) fc).setGraduatedColour(false);
1086       return fc;
1087     }
1088
1089     /*
1090      * next easiest case - colour by Label, or attribute text
1091      */
1092     if (byCategory.isSelected())
1093     {
1094       fc.setColourByLabel(true);
1095       String byWhat = (String) colourByTextCombo.getSelectedItem();
1096       if (!LABEL_18N.equals(byWhat))
1097       {
1098         fc.setAttributeName(
1099                 FeatureMatcher.fromAttributeDisplayName(byWhat));
1100       }
1101       return fc;
1102     }
1103
1104     /*
1105      * remaining case - graduated colour by score, or attribute value;
1106      * set attribute to colour by if selected
1107      */
1108     String byWhat = (String) colourByRangeCombo.getSelectedItem();
1109     if (!SCORE_18N.equals(byWhat))
1110     {
1111       fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
1112     }
1113
1114     /*
1115      * set threshold options and 'autoscaled' which is
1116      * false if 'threshold is min/max' is selected
1117      * else true (colour range is on actual range of values)
1118      */
1119     fc.setThreshold(thresh);
1120     fc.setAutoScaled(!thresholdIsMin.isSelected());
1121     fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
1122     fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
1123
1124     if (threshline == null)
1125     {
1126       /*
1127        * todo not yet implemented: visual indication of feature threshold
1128        */
1129       threshline = new GraphLine((max - min) / 2f, "Threshold",
1130               Color.black);
1131     }
1132
1133     return fc;
1134   }
1135
1136   @Override
1137   protected void raiseClosed()
1138   {
1139     refreshFeatureSettings();
1140   }
1141
1142   protected void refreshFeatureSettings()
1143   {
1144     if (this.featureSettings != null)
1145     {
1146       featureSettings.actionPerformed(new ActionEvent(this, 0, "REFRESH"));
1147     }
1148   }
1149
1150   /**
1151    * Action on OK is just to dismiss the dialog - any changes have already been
1152    * applied
1153    */
1154   @Override
1155   public void okPressed()
1156   {
1157   }
1158
1159   /**
1160    * Action on Cancel is to restore colour scheme and filters as they were when
1161    * the dialog was opened (including any feature sub-types that may have been
1162    * changed)
1163    */
1164   @Override
1165   public void cancelPressed()
1166   {
1167     restoreOriginalColours();
1168     restoreOriginalFilters();
1169     ap.paintAlignment(true, true);
1170   }
1171
1172   protected void restoreOriginalFilters()
1173   {
1174     for (Entry<String, FeatureMatcherSetI> entry : originalFilters
1175             .entrySet())
1176     {
1177       fr.setFeatureFilter(entry.getKey(), entry.getValue());
1178     }
1179   }
1180
1181   protected void restoreOriginalColours()
1182   {
1183     for (Entry<String, FeatureColourI> entry : originalColours.entrySet())
1184     {
1185       fr.setColour(entry.getKey(), entry.getValue());
1186     }
1187   }
1188
1189   /**
1190    * Action on text entry of a threshold value
1191    */
1192   protected void thresholdValue_actionPerformed()
1193   {
1194     try
1195     {
1196       /*
1197        * set 'adjusting' flag while moving the slider, so it 
1198        * doesn't then in turn change the value (with rounding)
1199        */
1200       adjusting = true;
1201       float f = Float.parseFloat(thresholdValue.getText());
1202       f = Float.max(f, this.min);
1203       f = Float.min(f, this.max);
1204       thresholdValue.setText(String.valueOf(f));
1205       slider.setValue((int) (f * scaleFactor));
1206       threshline.value = f;
1207       thresholdValue.setBackground(Color.white); // ok
1208       adjusting = false;
1209       colourChanged(true);
1210     } catch (NumberFormatException ex)
1211     {
1212       thresholdValue.setBackground(Color.red); // not ok
1213       adjusting = false;
1214     }
1215   }
1216
1217   /**
1218    * Action on change of threshold slider value. This may be done interactively
1219    * (by moving the slider), or programmatically (to update the slider after
1220    * manual input of a threshold value).
1221    */
1222   protected void sliderValueChanged()
1223   {
1224     threshline.value = getRoundedSliderValue();
1225
1226     /*
1227      * repaint alignment, but not Overview or structure,
1228      * to avoid overload while dragging the slider
1229      */
1230     colourChanged(false);
1231   }
1232
1233   /**
1234    * Converts the slider value to its absolute value by dividing by the
1235    * scaleFactor. Rounding errors are squashed by forcing min/max of slider
1236    * range to the actual min/max of feature score range
1237    * 
1238    * @return
1239    */
1240   private float getRoundedSliderValue()
1241   {
1242     int value = slider.getValue();
1243     float f = value == slider.getMaximum() ? max
1244             : (value == slider.getMinimum() ? min : value / scaleFactor);
1245     return f;
1246   }
1247
1248   void addActionListener(ActionListener listener)
1249   {
1250     if (featureSettings != null)
1251     {
1252       System.err.println(
1253               "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
1254     }
1255     featureSettings = listener;
1256   }
1257
1258   /**
1259    * A helper method to build the drop-down choice of attributes for a feature.
1260    * If 'withRange' is true, then Score, and any attributes with a min-max
1261    * range, are added. If 'withText' is true, Label and any known attributes are
1262    * added. This allows 'categorical numerical' attributes e.g. codon position
1263    * to be coloured by text.
1264    * <p>
1265    * Where metadata is available with a description for an attribute, that is
1266    * added as a tooltip.
1267    * <p>
1268    * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
1269    * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
1270    * <p>
1271    * This method does not add any ActionListener to the JComboBox.
1272    * 
1273    * @param attNames
1274    * @param withRange
1275    * @param withText
1276    */
1277   protected JComboBox<String> populateAttributesDropdown(
1278           List<String[]> attNames, boolean withRange, boolean withText)
1279   {
1280     List<String> displayAtts = new ArrayList<>();
1281     List<String> tooltips = new ArrayList<>();
1282
1283     if (withText)
1284     {
1285       displayAtts.add(LABEL_18N);
1286       tooltips.add(MessageManager.getString("label.description"));
1287     }
1288     if (withRange)
1289     {
1290       float[][] minMax = fr.getMinMax().get(featureType);
1291       if (minMax != null && minMax[0][0] != minMax[0][1])
1292       {
1293         displayAtts.add(SCORE_18N);
1294         tooltips.add(SCORE_18N);
1295       }
1296     }
1297
1298     FeatureAttributes fa = FeatureAttributes.getInstance();
1299     for (String[] attName : attNames)
1300     {
1301       float[] minMax = fa.getMinMax(featureType, attName);
1302       boolean hasRange = minMax != null && minMax[0] != minMax[1];
1303       if (!withText && !hasRange)
1304       {
1305         continue;
1306       }
1307       displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
1308       String desc = fa.getDescription(featureType, attName);
1309       if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
1310       {
1311         desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
1312       }
1313       tooltips.add(desc == null ? "" : desc);
1314     }
1315
1316     JComboBox<String> attCombo = JvSwingUtils
1317             .buildComboWithTooltips(displayAtts, tooltips);
1318
1319     return attCombo;
1320   }
1321
1322   /**
1323    * Populates initial layout of the feature attribute filters panel
1324    */
1325   private JPanel initialiseFiltersPanel()
1326   {
1327     filters = new ArrayList<>();
1328
1329     JPanel outerPanel = new JPanel();
1330     outerPanel.setLayout(new BoxLayout(outerPanel, BoxLayout.Y_AXIS));
1331     outerPanel.setBackground(Color.white);
1332
1333     /*
1334      * option to apply colour to peer types as well (if there are any)
1335      */
1336     if (!peerSoTerms.isEmpty())
1337     {
1338       applyFiltersToSubtypes = false;
1339       outerPanel.add(initSubtypesPanel(true));
1340     }
1341
1342     JPanel filtersPanel = new JPanel();
1343     filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
1344     filtersPanel.setBackground(Color.white);
1345     JvSwingUtils.createTitledBorder(filtersPanel,
1346             MessageManager.getString("label.filters"), true);
1347     outerPanel.add(filtersPanel);
1348
1349     JPanel andOrPanel = initialiseAndOrPanel();
1350     filtersPanel.add(andOrPanel);
1351
1352     /*
1353      * panel with filters - populated by refreshFiltersDisplay, 
1354      * which also sets the layout manager
1355      */
1356     chooseFiltersPanel = new JPanel();
1357     chooseFiltersPanel.setBackground(Color.white);
1358     filtersPanel.add(chooseFiltersPanel);
1359
1360     return outerPanel;
1361   }
1362
1363   /**
1364    * Lays out the panel with radio buttons to AND or OR filter conditions
1365    * 
1366    * @return
1367    */
1368   private JPanel initialiseAndOrPanel()
1369   {
1370     JPanel andOrPanel = new JPanel(new BorderLayout());
1371     andOrPanel.setBackground(Color.white);
1372
1373     andFilters = new JRadioButton(MessageManager.getString("label.and"));
1374     orFilters = new JRadioButton(MessageManager.getString("label.or"));
1375     ActionListener actionListener = new ActionListener()
1376     {
1377       @Override
1378       public void actionPerformed(ActionEvent e)
1379       {
1380         filtersChanged();
1381       }
1382     };
1383     andFilters.addActionListener(actionListener);
1384     orFilters.addActionListener(actionListener);
1385     ButtonGroup andOr = new ButtonGroup();
1386     andOr.add(andFilters);
1387     andOr.add(orFilters);
1388     andFilters.setSelected(true);
1389     andOrPanel.add(
1390             new JLabel(MessageManager.getString("label.join_conditions")));
1391     andOrPanel.add(andFilters);
1392     andOrPanel.add(orFilters);
1393
1394     return andOrPanel;
1395   }
1396
1397   /**
1398    * Builds a tooltip for the 'Apply to subtypes' checkbox with a list of
1399    * subtypes of this feature type
1400    * 
1401    * @return
1402    */
1403   protected String getSubtypesTooltip()
1404   {
1405     StringBuilder sb = new StringBuilder(20 * peerSoTerms.size());
1406     sb.append(MessageManager.getString("label.apply_also_to"));
1407     for (String child : peerSoTerms)
1408     {
1409       sb.append("<br>").append(child);
1410     }
1411     String tooltip = JvSwingUtils.wrapTooltip(true, sb.toString());
1412     return tooltip;
1413   }
1414
1415   /**
1416    * Refreshes the display to show any filters currently configured for the
1417    * selected feature type (editable, with 'remove' option), plus one extra row
1418    * for adding a condition. This should be called after a filter has been
1419    * removed, added or amended.
1420    */
1421   private void updateFiltersTab()
1422   {
1423     /*
1424      * clear the panel and list of filter conditions
1425      */
1426     chooseFiltersPanel.removeAll();
1427     filters.clear();
1428
1429     /*
1430      * look up attributes known for feature type
1431      */
1432     List<String[]> attNames = FeatureAttributes.getInstance()
1433             .getAttributes(featureType);
1434
1435     /*
1436      * if this feature type has filters set, load them first
1437      */
1438     FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
1439     if (featureFilters != null)
1440     {
1441       if (!featureFilters.isAnded())
1442       {
1443         orFilters.setSelected(true);
1444       }
1445       featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
1446     }
1447
1448     /*
1449      * and an empty filter for the user to populate (add)
1450      */
1451     filters.add(FeatureMatcher.NULL_MATCHER);
1452
1453     /*
1454      * use GridLayout to 'justify' rows to the top of the panel, until
1455      * there are too many to fit in, then fall back on BoxLayout
1456      */
1457     if (filters.size() <= 5)
1458     {
1459       chooseFiltersPanel.setLayout(new GridLayout(5, 1));
1460     }
1461     else
1462     {
1463       chooseFiltersPanel.setLayout(
1464               new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
1465     }
1466
1467     /*
1468      * render the conditions in rows, each in its own JPanel
1469      */
1470     int filterIndex = 0;
1471     for (FeatureMatcherI filter : filters)
1472     {
1473       JPanel row = addFilter(filter, attNames, filterIndex);
1474       chooseFiltersPanel.add(row);
1475       filterIndex++;
1476     }
1477
1478     this.validate();
1479     this.repaint();
1480   }
1481
1482   /**
1483    * A helper method that constructs a row (panel) with one filter condition:
1484    * <ul>
1485    * <li>a drop-down list of Label, Score and attribute names to choose
1486    * from</li>
1487    * <li>a drop-down list of conditions to choose from</li>
1488    * <li>a text field for input of a match pattern</li>
1489    * <li>optionally, a 'remove' button</li>
1490    * </ul>
1491    * The filter values are set as defaults for the input fields. The 'remove'
1492    * button is added unless the pattern is empty (incomplete filter condition).
1493    * <p>
1494    * Action handlers on these fields provide for
1495    * <ul>
1496    * <li>validate pattern field - should be numeric if condition is numeric</li>
1497    * <li>save filters and refresh display on any (valid) change</li>
1498    * <li>remove filter and refresh on 'Remove'</li>
1499    * <li>update conditions list on change of Label/Score/Attribute</li>
1500    * <li>refresh value field tooltip with min-max range on change of
1501    * attribute</li>
1502    * </ul>
1503    * 
1504    * @param filter
1505    * @param attNames
1506    * @param filterIndex
1507    * @return
1508    */
1509   protected JPanel addFilter(FeatureMatcherI filter,
1510           List<String[]> attNames, int filterIndex)
1511   {
1512     String[] attName = filter.getAttribute();
1513     Condition cond = filter.getMatcher().getCondition();
1514     String pattern = filter.getMatcher().getPattern();
1515
1516     JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
1517     filterRow.setBackground(Color.white);
1518
1519     /*
1520      * drop-down choice of attribute, with description as a tooltip 
1521      * if we can obtain it
1522      */
1523     final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
1524             true, true);
1525     String filterBy = setSelectedAttribute(attCombo, filter);
1526
1527     JComboBox<Condition> condCombo = new JComboBox<>();
1528
1529     JTextField patternField = new JTextField(8);
1530     patternField.setText(pattern);
1531
1532     /*
1533      * action handlers that validate and (if valid) apply changes
1534      */
1535     ActionListener actionListener = new ActionListener()
1536     {
1537       @Override
1538       public void actionPerformed(ActionEvent e)
1539       {
1540         if (validateFilter(patternField, condCombo))
1541         {
1542           if (updateFilter(attCombo, condCombo, patternField, filterIndex))
1543           {
1544             filtersChanged();
1545           }
1546         }
1547       }
1548     };
1549     ItemListener itemListener = new ItemListener()
1550     {
1551       @Override
1552       public void itemStateChanged(ItemEvent e)
1553       {
1554         actionListener.actionPerformed(null);
1555       }
1556     };
1557
1558     if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
1559     {
1560       attCombo.setSelectedIndex(0);
1561     }
1562     else
1563     {
1564       attCombo.setSelectedItem(
1565               FeatureMatcher.toAttributeDisplayName(attName));
1566     }
1567     attCombo.addItemListener(new ItemListener()
1568     {
1569       @Override
1570       public void itemStateChanged(ItemEvent e)
1571       {
1572         /*
1573          * on change of attribute, refresh the conditions list to
1574          * ensure it is appropriate for the attribute datatype
1575          */
1576         populateConditions((String) attCombo.getSelectedItem(),
1577                 (Condition) condCombo.getSelectedItem(), condCombo,
1578                 patternField);
1579         actionListener.actionPerformed(null);
1580       }
1581     });
1582
1583     filterRow.add(attCombo);
1584
1585     /*
1586      * drop-down choice of test condition
1587      */
1588     populateConditions(filterBy, cond, condCombo, patternField);
1589     condCombo.setPreferredSize(new Dimension(150, 20));
1590     condCombo.addItemListener(itemListener);
1591     filterRow.add(condCombo);
1592
1593     /*
1594      * pattern to match against
1595      */
1596     patternField.addActionListener(actionListener);
1597     patternField.addFocusListener(new FocusAdapter()
1598     {
1599       @Override
1600       public void focusLost(FocusEvent e)
1601       {
1602         actionListener.actionPerformed(null);
1603       }
1604     });
1605     filterRow.add(patternField);
1606
1607     /*
1608      * disable pattern field for condition 'Present / NotPresent'
1609      */
1610     Condition selectedCondition = (Condition) condCombo.getSelectedItem();
1611     patternField.setEnabled(selectedCondition.needsAPattern());
1612
1613     /*
1614      * if a numeric condition is selected, show the value range
1615      * as a tooltip on the value input field
1616      */
1617     setNumericHints(filterBy, selectedCondition, patternField);
1618
1619     /*
1620      * add remove button if filter is populated (non-empty pattern)
1621      */
1622     if (!patternField.isEnabled()
1623             || (pattern != null && pattern.trim().length() > 0))
1624     {
1625       // todo: gif for button drawing '-' or 'x'
1626       JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
1627       removeCondition
1628               .setToolTipText(MessageManager.getString("label.delete_row"));
1629       removeCondition.addActionListener(new ActionListener()
1630       {
1631         @Override
1632         public void actionPerformed(ActionEvent e)
1633         {
1634           filters.remove(filterIndex);
1635           filtersChanged();
1636         }
1637       });
1638       filterRow.add(removeCondition);
1639     }
1640
1641     return filterRow;
1642   }
1643
1644   /**
1645    * Sets the selected item in the Label/Score/Attribute drop-down to match the
1646    * filter
1647    * 
1648    * @param attCombo
1649    * @param filter
1650    */
1651   private String setSelectedAttribute(JComboBox<String> attCombo,
1652           FeatureMatcherI filter)
1653   {
1654     String item = null;
1655     if (filter.isByScore())
1656     {
1657       item = SCORE_18N;
1658     }
1659     else if (filter.isByLabel())
1660     {
1661       item = LABEL_18N;
1662     }
1663     else
1664     {
1665       item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
1666     }
1667     attCombo.setSelectedItem(item);
1668     return item;
1669   }
1670
1671   /**
1672    * If a numeric comparison condition is selected, retrieves the min-max range
1673    * for the value (score or attribute), and sets it as a tooltip on the value
1674    * field. If the field is currently empty, then pre-populates it with
1675    * <ul>
1676    * <li>the minimum value, if condition is > or >=</li>
1677    * <li>the maximum value, if condition is < or <=</li>
1678    * </ul>
1679    * 
1680    * @param attName
1681    * @param selectedCondition
1682    * @param patternField
1683    */
1684   private void setNumericHints(String attName, Condition selectedCondition,
1685           JTextField patternField)
1686   {
1687     patternField.setToolTipText("");
1688
1689     if (selectedCondition.isNumeric())
1690     {
1691       float[] minMax = getMinMax(attName);
1692       if (minMax != null)
1693       {
1694         String minFormatted = DECFMT_2_2.format(minMax[0]);
1695         String maxFormatted = DECFMT_2_2.format(minMax[1]);
1696         String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
1697         patternField.setToolTipText(tip);
1698         if (patternField.getText().isEmpty())
1699         {
1700           if (selectedCondition == Condition.GE
1701                   || selectedCondition == Condition.GT)
1702           {
1703             patternField.setText(minFormatted);
1704           }
1705           else
1706           {
1707             if (selectedCondition == Condition.LE
1708                     || selectedCondition == Condition.LT)
1709             {
1710               patternField.setText(maxFormatted);
1711             }
1712           }
1713         }
1714       }
1715     }
1716   }
1717
1718   /**
1719    * Populates the drop-down list of comparison conditions for the given
1720    * attribute name. The conditions added depend on the datatype of the
1721    * attribute values. The supplied condition is set as the selected item in the
1722    * list, provided it is in the list. If the pattern is now invalid
1723    * (non-numeric pattern for a numeric condition), it is cleared.
1724    * 
1725    * @param attName
1726    * @param cond
1727    * @param condCombo
1728    * @param patternField
1729    */
1730   private void populateConditions(String attName, Condition cond,
1731           JComboBox<Condition> condCombo, JTextField patternField)
1732   {
1733     Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
1734             FeatureMatcher.fromAttributeDisplayName(attName));
1735     if (LABEL_18N.equals(attName))
1736     {
1737       type = Datatype.Character;
1738     }
1739     else if (SCORE_18N.equals(attName))
1740     {
1741       type = Datatype.Number;
1742     }
1743
1744     /*
1745      * remove itemListener before starting
1746      */
1747     ItemListener listener = condCombo.getItemListeners()[0];
1748     condCombo.removeItemListener(listener);
1749     boolean condIsValid = false;
1750
1751     condCombo.removeAllItems();
1752     for (Condition c : Condition.values())
1753     {
1754       if ((c.isNumeric() && type == Datatype.Number)
1755               || (!c.isNumeric() && type != Datatype.Number))
1756       {
1757         condCombo.addItem(c);
1758         if (c == cond)
1759         {
1760           condIsValid = true;
1761         }
1762       }
1763     }
1764
1765     /*
1766      * set the selected condition (does nothing if not in the list)
1767      */
1768     if (condIsValid)
1769     {
1770       condCombo.setSelectedItem(cond);
1771     }
1772     else
1773     {
1774       condCombo.setSelectedIndex(0);
1775     }
1776
1777     /*
1778      * clear pattern if it is now invalid for condition
1779      */
1780     if (((Condition) condCombo.getSelectedItem()).isNumeric())
1781     {
1782       try
1783       {
1784         String pattern = patternField.getText().trim();
1785         if (pattern.length() > 0)
1786         {
1787           Float.valueOf(pattern);
1788         }
1789       } catch (NumberFormatException e)
1790       {
1791         patternField.setText("");
1792       }
1793     }
1794
1795     /*
1796      * restore the listener
1797      */
1798     condCombo.addItemListener(listener);
1799   }
1800
1801   /**
1802    * Answers true unless a numeric condition has been selected with a
1803    * non-numeric value. Sets the value field to RED with a tooltip if in error.
1804    * <p>
1805    * If the pattern is expected but is empty, this method returns false, but
1806    * does not mark the field as invalid. This supports selecting an attribute
1807    * for a new condition before a match pattern has been entered.
1808    * 
1809    * @param value
1810    * @param condCombo
1811    */
1812   protected boolean validateFilter(JTextField value,
1813           JComboBox<Condition> condCombo)
1814   {
1815     if (value == null || condCombo == null)
1816     {
1817       return true; // fields not populated
1818     }
1819
1820     Condition cond = (Condition) condCombo.getSelectedItem();
1821     if (!cond.needsAPattern())
1822     {
1823       return true;
1824     }
1825
1826     value.setBackground(Color.white);
1827     value.setToolTipText("");
1828     String v1 = value.getText().trim();
1829     if (v1.length() == 0)
1830     {
1831       // return false;
1832     }
1833
1834     if (cond.isNumeric() && v1.length() > 0)
1835     {
1836       try
1837       {
1838         Float.valueOf(v1);
1839       } catch (NumberFormatException e)
1840       {
1841         value.setBackground(Color.red);
1842         value.setToolTipText(
1843                 MessageManager.getString("label.numeric_required"));
1844         return false;
1845       }
1846     }
1847
1848     return true;
1849   }
1850
1851   /**
1852    * Constructs a filter condition from the given input fields, and replaces the
1853    * condition at filterIndex with the new one. Does nothing if the pattern
1854    * field is blank (unless the match condition is one that doesn't require a
1855    * pattern, e.g. 'Is present'). Answers true if the filter was updated, else
1856    * false.
1857    * <p>
1858    * This method may update the tooltip on the filter value field to show the
1859    * value range, if a numeric condition is selected. This ensures the tooltip
1860    * is updated when a numeric valued attribute is chosen on the last 'add a
1861    * filter' row.
1862    * 
1863    * @param attCombo
1864    * @param condCombo
1865    * @param valueField
1866    * @param filterIndex
1867    */
1868   protected boolean updateFilter(JComboBox<String> attCombo,
1869           JComboBox<Condition> condCombo, JTextField valueField,
1870           int filterIndex)
1871   {
1872     String attName = (String) attCombo.getSelectedItem();
1873     Condition cond = (Condition) condCombo.getSelectedItem();
1874     String pattern = valueField.getText().trim();
1875
1876     setNumericHints(attName, cond, valueField);
1877
1878     if (pattern.length() == 0 && cond.needsAPattern())
1879     {
1880       valueField.setEnabled(true); // ensure pattern field is enabled!
1881       return false;
1882     }
1883
1884     /*
1885      * Construct a matcher that operates on Label, Score, 
1886      * or named attribute
1887      */
1888     FeatureMatcherI km = null;
1889     if (LABEL_18N.equals(attName))
1890     {
1891       km = FeatureMatcher.byLabel(cond, pattern);
1892     }
1893     else if (SCORE_18N.equals(attName))
1894     {
1895       km = FeatureMatcher.byScore(cond, pattern);
1896     }
1897     else
1898     {
1899       km = FeatureMatcher.byAttribute(cond, pattern,
1900               FeatureMatcher.fromAttributeDisplayName(attName));
1901     }
1902
1903     filters.set(filterIndex, km);
1904
1905     return true;
1906   }
1907
1908   /**
1909    * Action on any change to feature filtering, namely
1910    * <ul>
1911    * <li>change of selected attribute</li>
1912    * <li>change of selected condition</li>
1913    * <li>change of match pattern</li>
1914    * <li>removal of a condition</li>
1915    * </ul>
1916    * The inputs are parsed into a combined filter and this is set for the
1917    * feature type, and the alignment redrawn.
1918    */
1919   protected void filtersChanged()
1920   {
1921     /*
1922      * update the filter conditions for the feature type
1923      */
1924     boolean anded = andFilters.isSelected();
1925     FeatureMatcherSetI combined = new FeatureMatcherSet();
1926
1927     for (FeatureMatcherI filter : filters)
1928     {
1929       String pattern = filter.getMatcher().getPattern();
1930       Condition condition = filter.getMatcher().getCondition();
1931       if (pattern.trim().length() > 0 || !condition.needsAPattern())
1932       {
1933         if (anded)
1934         {
1935           combined.and(filter);
1936         }
1937         else
1938         {
1939           combined.or(filter);
1940         }
1941       }
1942     }
1943
1944     /*
1945      * save the filter conditions in the FeatureRenderer
1946      * (note this might now be an empty filter with no conditions)
1947      */
1948     fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
1949     if (applyFiltersToSubtypes)
1950     {
1951       for (String child : peerSoTerms)
1952       {
1953         fr.setFeatureFilter(child, combined.isEmpty() ? null : combined);
1954       }
1955     }
1956
1957     refreshFeatureSettings();
1958     ap.paintAlignment(true, true);
1959
1960     updateFiltersTab();
1961   }
1962 }