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