/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Jalview is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
package jalview.gui;
import jalview.api.AlignViewportI;
import jalview.api.AlignmentViewPanel;
import jalview.api.FeatureColourI;
import jalview.bin.Cache;
import jalview.datamodel.GraphLine;
import jalview.datamodel.features.FeatureAttributes;
import jalview.datamodel.features.FeatureAttributes.Datatype;
import jalview.datamodel.features.FeatureMatcher;
import jalview.datamodel.features.FeatureMatcherI;
import jalview.datamodel.features.FeatureMatcherSet;
import jalview.datamodel.features.FeatureMatcherSetI;
import jalview.schemes.FeatureColour;
import jalview.util.ColorUtils;
import jalview.util.MessageManager;
import jalview.util.matcher.Condition;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* A dialog where the user can configure colour scheme, and any filters, for one
* feature type
*
* (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
* filter options)
*/
public class FeatureTypeSettings extends JalviewDialog
{
private final static String LABEL_18N = MessageManager
.getString("label.label");
private final static String SCORE_18N = MessageManager
.getString("label.score");
private static final int RADIO_WIDTH = 130;
private static final String COLON = ":";
private static final int MAX_TOOLTIP_LENGTH = 50;
private static final int NO_COLOUR_OPTION = 0;
private static final int MIN_COLOUR_OPTION = 1;
private static final int MAX_COLOUR_OPTION = 2;
private static final int ABOVE_THRESHOLD_OPTION = 1;
private static final int BELOW_THRESHOLD_OPTION = 2;
private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
"##.##");
/*
* FeatureRenderer holds colour scheme and filters for feature types
*/
private final FeatureRenderer fr; // todo refactor to allow interface type
// here
/*
* the view panel to update when settings change
*/
private final AlignmentViewPanel ap;
private final String featureType;
/*
* the colour and filters to reset to on Cancel
*/
private final FeatureColourI originalColour;
private final FeatureMatcherSetI originalFilter;
/*
* set flag to true when setting values programmatically,
* to avoid invocation of action handlers
*/
private boolean adjusting = false;
/*
* minimum of the value range for graduated colour
* (may be for feature score or for a numeric attribute)
*/
private float min;
/*
* maximum of the value range for graduated colour
*/
private float max;
/*
* scale factor for conversion between absolute min-max and slider
*/
private float scaleFactor;
/*
* radio button group, to select what to colour by:
* simple colour, by category (text), or graduated
*/
private JRadioButton simpleColour = new JRadioButton();
private JRadioButton byCategory = new JRadioButton();
private JRadioButton graduatedColour = new JRadioButton();
/**
* colours and filters are shown in tabbed view or single content pane
*/
JPanel coloursPanel, filtersPanel;
JPanel singleColour = new JPanel();
private JPanel minColour = new JPanel();
private JPanel maxColour = new JPanel();
private JComboBox threshold = new JComboBox<>();
private JSlider slider = new JSlider();
private JTextField thresholdValue = new JTextField(20);
private JCheckBox thresholdIsMin = new JCheckBox();
private GraphLine threshline;
private ActionListener featureSettings = null;
private ActionListener changeColourAction;
/*
* choice of option for 'colour for no value'
*/
private JComboBox noValueCombo;
/*
* choice of what to colour by text (Label or attribute)
*/
private JComboBox colourByTextCombo;
/*
* choice of what to colour by range (Score or attribute)
*/
private JComboBox colourByRangeCombo;
private JRadioButton andFilters;
private JRadioButton orFilters;
/*
* filters for the currently selected feature type
*/
private List filters;
private JPanel chooseFiltersPanel;
/**
* Constructor
*
* @param frender
* @param theType
*/
public FeatureTypeSettings(FeatureRenderer frender, String theType)
{
this.fr = frender;
this.featureType = theType;
ap = fr.ap;
originalFilter = fr.getFeatureFilter(theType);
originalColour = fr.getFeatureColours().get(theType);
adjusting = true;
try
{
initialise();
} catch (Exception ex)
{
ex.printStackTrace();
return;
}
updateColoursTab();
updateFiltersTab();
adjusting = false;
colourChanged(false);
String title = MessageManager
.formatMessage("label.display_settings_for", new String[]
{ theType });
initDialogFrame(this, true, false, title, 580, 500);
waitForInput();
}
/**
* Configures the widgets on the Colours tab according to the current feature
* colour scheme
*/
private void updateColoursTab()
{
FeatureColourI fc = fr.getFeatureColours().get(featureType);
/*
* suppress action handling while updating values programmatically
*/
adjusting = true;
try
{
/*
* single colour
*/
if (fc.isSimpleColour())
{
singleColour.setBackground(fc.getColour());
singleColour.setForeground(fc.getColour());
simpleColour.setSelected(true);
}
/*
* colour by text (Label or attribute text)
*/
if (fc.isColourByLabel())
{
byCategory.setSelected(true);
colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
if (fc.isColourByAttribute())
{
String[] attributeName = fc.getAttributeName();
colourByTextCombo.setSelectedItem(
FeatureMatcher.toAttributeDisplayName(attributeName));
}
else
{
colourByTextCombo.setSelectedItem(LABEL_18N);
}
}
else
{
colourByTextCombo.setEnabled(false);
}
if (!fc.isGraduatedColour())
{
colourByRangeCombo.setEnabled(false);
minColour.setEnabled(false);
maxColour.setEnabled(false);
noValueCombo.setEnabled(false);
threshold.setEnabled(false);
slider.setEnabled(false);
thresholdValue.setEnabled(false);
thresholdIsMin.setEnabled(false);
return;
}
/*
* Graduated colour, by score or attribute value range
*/
graduatedColour.setSelected(true);
updateColourMinMax(); // ensure min, max are set
colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
minColour.setEnabled(true);
maxColour.setEnabled(true);
noValueCombo.setEnabled(true);
threshold.setEnabled(true);
minColour.setBackground(fc.getMinColour());
maxColour.setBackground(fc.getMaxColour());
if (fc.isColourByAttribute())
{
String[] attributeName = fc.getAttributeName();
colourByRangeCombo.setSelectedItem(
FeatureMatcher.toAttributeDisplayName(attributeName));
}
else
{
colourByRangeCombo.setSelectedItem(SCORE_18N);
}
Color noColour = fc.getNoColour();
if (noColour == null)
{
noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
}
else if (noColour.equals(fc.getMinColour()))
{
noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
}
else if (noColour.equals(fc.getMaxColour()))
{
noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
}
/*
* update min-max scaling if there is a range to work with,
* else disable the widgets (this shouldn't happen if only
* valid options are offered in the combo box)
*/
scaleFactor = (max == min) ? 1f : 100f / (max - min);
float range = (max - min) * scaleFactor;
slider.setMinimum((int) (min * scaleFactor));
slider.setMaximum((int) (max * scaleFactor));
slider.setMajorTickSpacing((int) (range / 10f));
threshline = new GraphLine((max - min) / 2f, "Threshold",
Color.black);
threshline.value = fc.getThreshold();
if (fc.hasThreshold())
{
threshold.setSelectedIndex(
fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
: BELOW_THRESHOLD_OPTION);
slider.setEnabled(true);
slider.setValue((int) (fc.getThreshold() * scaleFactor));
thresholdValue.setText(String.valueOf(fc.getThreshold()));
thresholdValue.setEnabled(true);
thresholdIsMin.setEnabled(true);
}
else
{
slider.setEnabled(false);
thresholdValue.setEnabled(false);
thresholdIsMin.setEnabled(false);
}
thresholdIsMin.setSelected(!fc.isAutoScaled());
} finally
{
adjusting = false;
}
}
/**
* Configures the initial layout
*/
private void initialise()
{
this.setLayout(new BorderLayout());
/*
* an ActionListener that applies colour changes
*/
changeColourAction = new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
colourChanged(true);
}
};
/*
* first panel/tab: colour options
*/
JPanel coloursPanel = initialiseColoursPanel();
this.add(coloursPanel, BorderLayout.NORTH);
/*
* second panel/tab: filter options
*/
JPanel filtersPanel = initialiseFiltersPanel();
this.add(filtersPanel, BorderLayout.CENTER);
JPanel okCancelPanel = initialiseOkCancelPanel();
this.add(okCancelPanel, BorderLayout.SOUTH);
}
/**
* Updates the min-max range if Colour By selected item is Score, or an
* attribute, with a min-max range
*/
protected void updateColourMinMax()
{
if (!graduatedColour.isSelected())
{
return;
}
String colourBy = (String) colourByRangeCombo.getSelectedItem();
float[] minMax = getMinMax(colourBy);
if (minMax != null)
{
min = minMax[0];
max = minMax[1];
}
}
/**
* Retrieves the min-max range:
*
* of feature score, if colour or filter is by Score
* else of the selected attribute
*
*
* @param attName
* @return
*/
private float[] getMinMax(String attName)
{
float[] minMax = null;
if (SCORE_18N.equals(attName))
{
minMax = fr.getMinMax().get(featureType)[0];
}
else
{
// colour by attribute range
minMax = FeatureAttributes.getInstance().getMinMax(featureType,
FeatureMatcher.fromAttributeDisplayName(attName));
}
return minMax;
}
/**
* Lay out fields for graduated colour (by score or attribute value)
*
* @return
*/
private JPanel initialiseGraduatedColourPanel()
{
JPanel graduatedColourPanel = new JPanel();
graduatedColourPanel.setLayout(
new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
JvSwingUtils.createTitledBorder(graduatedColourPanel,
MessageManager.getString("label.graduated_colour"), true);
graduatedColourPanel.setBackground(Color.white);
/*
* first row: graduated colour radio button, score/attribute drop-down
*/
JPanel graduatedChoicePanel = new JPanel(
new FlowLayout(FlowLayout.LEFT));
graduatedChoicePanel.setBackground(Color.white);
graduatedColour = new JRadioButton(
MessageManager.getString("label.by_range_of") + COLON);
graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
graduatedColour.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
if (graduatedColour.isSelected())
{
colourChanged(true);
}
}
});
graduatedChoicePanel.add(graduatedColour);
List attNames = FeatureAttributes.getInstance()
.getAttributes(featureType);
colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
colourByRangeCombo.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
colourChanged(true);
}
});
/*
* disable graduated colour option if no range found
*/
graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
graduatedChoicePanel.add(colourByRangeCombo);
graduatedColourPanel.add(graduatedChoicePanel);
/*
* second row - min/max/no colours
*/
JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
colourRangePanel.setBackground(Color.white);
graduatedColourPanel.add(colourRangePanel);
minColour.setFont(JvSwingUtils.getLabelFont());
minColour.setBorder(BorderFactory.createLineBorder(Color.black));
minColour.setPreferredSize(new Dimension(40, 20));
minColour.setToolTipText(MessageManager.getString("label.min_colour"));
minColour.addMouseListener(new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent e)
{
if (minColour.isEnabled())
{
showColourChooser(minColour, "label.select_colour_minimum_value");
}
}
});
maxColour.setFont(JvSwingUtils.getLabelFont());
maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
maxColour.setPreferredSize(new Dimension(40, 20));
maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
maxColour.addMouseListener(new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent e)
{
if (maxColour.isEnabled())
{
showColourChooser(maxColour, "label.select_colour_maximum_value");
}
}
});
maxColour.setBorder(new LineBorder(Color.black));
/*
* if not set, default max colour to last plain colour,
* and make min colour a pale version of max colour
*/
Color max = originalColour.getMaxColour();
if (max == null)
{
max = originalColour.getColour();
minColour.setBackground(ColorUtils.bleachColour(max, 0.9f));
}
else
{
maxColour.setBackground(max);
minColour.setBackground(originalColour.getMinColour());
}
noValueCombo = new JComboBox<>();
noValueCombo.addItem(MessageManager.getString("label.no_colour"));
noValueCombo.addItem(MessageManager.getString("label.min_colour"));
noValueCombo.addItem(MessageManager.getString("label.max_colour"));
noValueCombo.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
colourChanged(true);
}
});
JLabel minText = new JLabel(
MessageManager.getString("label.min_value") + COLON);
minText.setFont(JvSwingUtils.getLabelFont());
JLabel maxText = new JLabel(
MessageManager.getString("label.max_value") + COLON);
maxText.setFont(JvSwingUtils.getLabelFont());
JLabel noText = new JLabel(
MessageManager.getString("label.no_value") + COLON);
noText.setFont(JvSwingUtils.getLabelFont());
colourRangePanel.add(minText);
colourRangePanel.add(minColour);
colourRangePanel.add(maxText);
colourRangePanel.add(maxColour);
colourRangePanel.add(noText);
colourRangePanel.add(noValueCombo);
/*
* third row - threshold options and value
*/
JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
thresholdPanel.setBackground(Color.white);
graduatedColourPanel.add(thresholdPanel);
threshold.addActionListener(changeColourAction);
threshold.setToolTipText(MessageManager
.getString("label.threshold_feature_display_by_score"));
threshold.addItem(MessageManager
.getString("label.threshold_feature_no_threshold")); // index 0
threshold.addItem(MessageManager
.getString("label.threshold_feature_above_threshold")); // index 1
threshold.addItem(MessageManager
.getString("label.threshold_feature_below_threshold")); // index 2
thresholdValue.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
thresholdValue_actionPerformed();
}
});
thresholdValue.addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
thresholdValue_actionPerformed();
}
});
slider.setPaintLabels(false);
slider.setPaintTicks(true);
slider.setBackground(Color.white);
slider.setEnabled(false);
slider.setOpaque(false);
slider.setPreferredSize(new Dimension(100, 32));
slider.setToolTipText(
MessageManager.getString("label.adjust_threshold"));
slider.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent evt)
{
if (!adjusting)
{
thresholdValue
.setText(String.valueOf(slider.getValue() / scaleFactor));
thresholdValue.setBackground(Color.white); // to reset red for invalid
sliderValueChanged();
}
}
});
slider.addMouseListener(new MouseAdapter()
{
@Override
public void mouseReleased(MouseEvent evt)
{
/*
* only update Overview and/or structure colouring
* when threshold slider drag ends (mouse up)
*/
if (ap != null)
{
refreshDisplay(true);
}
}
});
thresholdValue.setEnabled(false);
thresholdValue.setColumns(7);
thresholdPanel.add(threshold);
thresholdPanel.add(slider);
thresholdPanel.add(thresholdValue);
thresholdIsMin.setBackground(Color.white);
thresholdIsMin
.setText(MessageManager.getString("label.threshold_minmax"));
thresholdIsMin.setToolTipText(MessageManager
.getString("label.toggle_absolute_relative_display_threshold"));
thresholdIsMin.addActionListener(changeColourAction);
thresholdPanel.add(thresholdIsMin);
return graduatedColourPanel;
}
/**
* Lay out OK and Cancel buttons
*
* @return
*/
private JPanel initialiseOkCancelPanel()
{
JPanel okCancelPanel = new JPanel();
// okCancelPanel.setBackground(Color.white);
okCancelPanel.add(ok);
okCancelPanel.add(cancel);
return okCancelPanel;
}
/**
* Lay out Colour options panel, containing
*
* plain colour, with colour picker
* colour by text, with choice of Label or other attribute
* colour by range, of score or other attribute, when available
*
*
* @return
*/
private JPanel initialiseColoursPanel()
{
JPanel colourByPanel = new JPanel();
colourByPanel.setBackground(Color.white);
colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
JvSwingUtils.createTitledBorder(colourByPanel,
MessageManager.getString("action.colour"), true);
/*
* simple colour radio button and colour picker
*/
JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
simpleColourPanel.setBackground(Color.white);
colourByPanel.add(simpleColourPanel);
simpleColour = new JRadioButton(
MessageManager.getString("label.simple_colour"));
simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
simpleColour.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
if (simpleColour.isSelected() && !adjusting)
{
colourChanged(true);
}
}
});
singleColour.setFont(JvSwingUtils.getLabelFont());
singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
singleColour.setPreferredSize(new Dimension(40, 20));
// if (originalColour.isGraduatedColour())
// {
// singleColour.setBackground(originalColour.getMaxColour());
// singleColour.setForeground(originalColour.getMaxColour());
// }
// else
// {
singleColour.setBackground(originalColour.getColour());
singleColour.setForeground(originalColour.getColour());
// }
singleColour.addMouseListener(new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent e)
{
if (simpleColour.isSelected())
{
showColourChooser(singleColour, "label.select_colour");
}
}
});
simpleColourPanel.add(simpleColour); // radio button
simpleColourPanel.add(singleColour); // colour picker button
/*
* colour by text (category) radio button and drop-down choice list
*/
JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
byTextPanel.setBackground(Color.white);
JvSwingUtils.createTitledBorder(byTextPanel,
MessageManager.getString("label.colour_by_text"), true);
colourByPanel.add(byTextPanel);
byCategory = new JRadioButton(
MessageManager.getString("label.by_text_of") + COLON);
byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
byCategory.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
if (byCategory.isSelected())
{
colourChanged(true);
}
}
});
byTextPanel.add(byCategory);
List attNames = FeatureAttributes.getInstance()
.getAttributes(featureType);
colourByTextCombo = populateAttributesDropdown(attNames, false, true);
colourByTextCombo.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
colourChanged(true);
}
});
byTextPanel.add(colourByTextCombo);
/*
* graduated colour panel
*/
JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
colourByPanel.add(graduatedColourPanel);
/*
* 3 radio buttons select between simple colour,
* by category (text), or graduated
*/
ButtonGroup bg = new ButtonGroup();
bg.add(simpleColour);
bg.add(byCategory);
bg.add(graduatedColour);
return colourByPanel;
}
private void showColourChooser(JPanel colourPanel, String key)
{
Color col = JColorChooser.showDialog(this,
MessageManager.getString(key), colourPanel.getBackground());
if (col != null)
{
colourPanel.setBackground(col);
colourPanel.setForeground(col);
}
colourPanel.repaint();
colourChanged(true);
}
/**
* Constructs and sets the selected colour options as the colour for the
* feature type, and repaints the alignment, and optionally the Overview
* and/or structure viewer if open
*
* @param updateStructsAndOverview
*/
void colourChanged(boolean updateStructsAndOverview)
{
if (adjusting)
{
/*
* ignore action handlers while setting values programmatically
*/
return;
}
/*
* ensure min-max range is for the latest choice of
* 'graduated colour by'
*/
updateColourMinMax();
FeatureColourI acg = makeColourFromInputs();
/*
* save the colour, and repaint stuff
*/
fr.setColour(featureType, acg);
refreshDisplay(updateStructsAndOverview);
updateColoursTab();
}
/**
* Converts the input values into an instance of FeatureColour
*
* @return
*/
private FeatureColourI makeColourFromInputs()
{
/*
* min-max range is to (or from) threshold value if
* 'threshold is min/max' is selected
*/
float thresh = 0f;
try
{
thresh = Float.valueOf(thresholdValue.getText());
} catch (NumberFormatException e)
{
// invalid inputs are already handled on entry
}
float minValue = min;
float maxValue = max;
final int thresholdOption = threshold.getSelectedIndex();
if (thresholdIsMin.isSelected()
&& thresholdOption == ABOVE_THRESHOLD_OPTION)
{
minValue = thresh;
}
if (thresholdIsMin.isSelected()
&& thresholdOption == BELOW_THRESHOLD_OPTION)
{
maxValue = thresh;
}
Color noColour = null;
if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
{
noColour = minColour.getBackground();
}
else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
{
noColour = maxColour.getBackground();
}
/*
* construct a colour that 'remembers' all the options, including
* those not currently selected
*/
FeatureColourI fc = new FeatureColour(singleColour.getBackground(),
minColour.getBackground(), maxColour.getBackground(), noColour,
minValue, maxValue);
/*
* easiest case - a single colour
*/
if (simpleColour.isSelected())
{
((FeatureColour) fc).setGraduatedColour(false);
return fc;
}
/*
* next easiest case - colour by Label, or attribute text
*/
if (byCategory.isSelected())
{
fc.setColourByLabel(true);
String byWhat = (String) colourByTextCombo.getSelectedItem();
if (!LABEL_18N.equals(byWhat))
{
fc.setAttributeName(
FeatureMatcher.fromAttributeDisplayName(byWhat));
}
return fc;
}
/*
* remaining case - graduated colour by score, or attribute value;
* set attribute to colour by if selected
*/
String byWhat = (String) colourByRangeCombo.getSelectedItem();
if (!SCORE_18N.equals(byWhat))
{
fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
}
/*
* set threshold options and 'autoscaled' which is
* false if 'threshold is min/max' is selected
* else true (colour range is on actual range of values)
*/
fc.setThreshold(thresh);
fc.setAutoScaled(!thresholdIsMin.isSelected());
fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
if (threshline == null)
{
/*
* todo not yet implemented: visual indication of feature threshold
*/
threshline = new GraphLine((max - min) / 2f, "Threshold",
Color.black);
}
return fc;
}
@Override
protected void raiseClosed()
{
if (this.featureSettings != null)
{
featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
}
}
/**
* Action on OK is just to dismiss the dialog - any changes have already been
* applied
*/
@Override
public void okPressed()
{
}
/**
* Action on Cancel is to restore colour scheme and filters as they were when
* the dialog was opened
*/
@Override
public void cancelPressed()
{
fr.setColour(featureType, originalColour);
fr.setFeatureFilter(featureType, originalFilter);
refreshDisplay(true);
}
/**
* Action on text entry of a threshold value
*/
protected void thresholdValue_actionPerformed()
{
try
{
/*
* set 'adjusting' flag while moving the slider, so it
* doesn't then in turn change the value (with rounding)
*/
adjusting = true;
float f = Float.parseFloat(thresholdValue.getText());
f = Float.max(f, this.min);
f = Float.min(f, this.max);
thresholdValue.setText(String.valueOf(f));
slider.setValue((int) (f * scaleFactor));
threshline.value = f;
thresholdValue.setBackground(Color.white); // ok
adjusting = false;
colourChanged(true);
} catch (NumberFormatException ex)
{
thresholdValue.setBackground(Color.red); // not ok
adjusting = false;
}
}
/**
* Action on change of threshold slider value. This may be done interactively
* (by moving the slider), or programmatically (to update the slider after
* manual input of a threshold value).
*/
protected void sliderValueChanged()
{
threshline.value = getRoundedSliderValue();
/*
* repaint alignment, but not Overview or structure,
* to avoid overload while dragging the slider
*/
colourChanged(false);
}
/**
* Converts the slider value to its absolute value by dividing by the
* scaleFactor. Rounding errors are squashed by forcing min/max of slider
* range to the actual min/max of feature score range
*
* @return
*/
private float getRoundedSliderValue()
{
int value = slider.getValue();
float f = value == slider.getMaximum() ? max
: (value == slider.getMinimum() ? min : value / scaleFactor);
return f;
}
void addActionListener(ActionListener listener)
{
if (featureSettings != null)
{
System.err.println(
"IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
}
featureSettings = listener;
}
/**
* A helper method to build the drop-down choice of attributes for a feature.
* If 'withRange' is true, then Score, and any attributes with a min-max
* range, are added. If 'withText' is true, Label and any known attributes are
* added. This allows 'categorical numerical' attributes e.g. codon position
* to be coloured by text.
*
* Where metadata is available with a description for an attribute, that is
* added as a tooltip.
*
* Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
* "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
*
* This method does not add any ActionListener to the JComboBox.
*
* @param attNames
* @param withRange
* @param withText
*/
protected JComboBox populateAttributesDropdown(
List attNames, boolean withRange, boolean withText)
{
List displayAtts = new ArrayList<>();
List tooltips = new ArrayList<>();
if (withText)
{
displayAtts.add(LABEL_18N);
tooltips.add(MessageManager.getString("label.description"));
}
if (withRange)
{
float[][] minMax = fr.getMinMax().get(featureType);
if (minMax != null && minMax[0][0] != minMax[0][1])
{
displayAtts.add(SCORE_18N);
tooltips.add(SCORE_18N);
}
}
FeatureAttributes fa = FeatureAttributes.getInstance();
for (String[] attName : attNames)
{
float[] minMax = fa.getMinMax(featureType, attName);
boolean hasRange = minMax != null && minMax[0] != minMax[1];
if (!withText && !hasRange)
{
continue;
}
displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
String desc = fa.getDescription(featureType, attName);
if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
{
desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
}
tooltips.add(desc == null ? "" : desc);
}
// now convert String List to Object List for buildComboWithTooltips
List displayAttsObjects = new ArrayList<>(displayAtts);
JComboBox attCombo = JvSwingUtils
.buildComboWithTooltips(displayAttsObjects, tooltips);
return attCombo;
}
/**
* Populates initial layout of the feature attribute filters panel
*/
private JPanel initialiseFiltersPanel()
{
filters = new ArrayList<>();
JPanel filtersPanel = new JPanel();
filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
filtersPanel.setBackground(Color.white);
JvSwingUtils.createTitledBorder(filtersPanel,
MessageManager.getString("label.filters"), true);
JPanel andOrPanel = initialiseAndOrPanel();
filtersPanel.add(andOrPanel);
/*
* panel with filters - populated by refreshFiltersDisplay,
* which also sets the layout manager
*/
chooseFiltersPanel = new JPanel();
chooseFiltersPanel.setBackground(Color.white);
filtersPanel.add(chooseFiltersPanel);
return filtersPanel;
}
/**
* Lays out the panel with radio buttons to AND or OR filter conditions
*
* @return
*/
private JPanel initialiseAndOrPanel()
{
JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
andOrPanel.setBackground(Color.white);
andFilters = new JRadioButton(MessageManager.getString("label.and"));
orFilters = new JRadioButton(MessageManager.getString("label.or"));
ActionListener actionListener = new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
filtersChanged();
}
};
andFilters.addActionListener(actionListener);
orFilters.addActionListener(actionListener);
ButtonGroup andOr = new ButtonGroup();
andOr.add(andFilters);
andOr.add(orFilters);
andFilters.setSelected(true);
andOrPanel.add(
new JLabel(MessageManager.getString("label.join_conditions")));
andOrPanel.add(andFilters);
andOrPanel.add(orFilters);
return andOrPanel;
}
/**
* Refreshes the display to show any filters currently configured for the
* selected feature type (editable, with 'remove' option), plus one extra row
* for adding a condition. This should be called after a filter has been
* removed, added or amended.
*/
private void updateFiltersTab()
{
/*
* clear the panel and list of filter conditions
*/
chooseFiltersPanel.removeAll();
filters.clear();
/*
* look up attributes known for feature type
*/
List attNames = FeatureAttributes.getInstance()
.getAttributes(featureType);
/*
* if this feature type has filters set, load them first
*/
FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
if (featureFilters != null)
{
if (!featureFilters.isAnded())
{
orFilters.setSelected(true);
}
featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
}
/*
* and an empty filter for the user to populate (add)
*/
filters.add(FeatureMatcher.NULL_MATCHER);
/*
* use GridLayout to 'justify' rows to the top of the panel, until
* there are too many to fit in, then fall back on BoxLayout
*/
if (filters.size() <= 5)
{
chooseFiltersPanel.setLayout(new GridLayout(5, 1));
}
else
{
chooseFiltersPanel.setLayout(
new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
}
/*
* render the conditions in rows, each in its own JPanel
*/
int filterIndex = 0;
for (FeatureMatcherI filter : filters)
{
JPanel row = addFilter(filter, attNames, filterIndex);
chooseFiltersPanel.add(row);
filterIndex++;
}
this.validate();
this.repaint();
}
/**
* A helper method that constructs a row (panel) with one filter condition:
*
* a drop-down list of Label, Score and attribute names to choose
* from
* a drop-down list of conditions to choose from
* a text field for input of a match pattern
* optionally, a 'remove' button
*
* The filter values are set as defaults for the input fields. The 'remove'
* button is added unless the pattern is empty (incomplete filter condition).
*
* Action handlers on these fields provide for
*
* validate pattern field - should be numeric if condition is numeric
* save filters and refresh display on any (valid) change
* remove filter and refresh on 'Remove'
* update conditions list on change of Label/Score/Attribute
* refresh value field tooltip with min-max range on change of
* attribute
*
*
* @param filter
* @param attNames
* @param filterIndex
* @return
*/
protected JPanel addFilter(FeatureMatcherI filter,
List attNames, int filterIndex)
{
String[] attName = filter.getAttribute();
Condition cond = filter.getMatcher().getCondition();
String pattern = filter.getMatcher().getPattern();
JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
filterRow.setBackground(Color.white);
/*
* drop-down choice of attribute, with description as a tooltip
* if we can obtain it
*/
final JComboBox attCombo = populateAttributesDropdown(attNames,
true, true);
String filterBy = setSelectedAttribute(attCombo, filter);
JComboBox condCombo = new JComboBox<>();
JTextField patternField = new JTextField(8);
patternField.setText(pattern);
/*
* action handlers that validate and (if valid) apply changes
*/
ActionListener actionListener = new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (validateFilter(patternField, condCombo))
{
if (updateFilter(attCombo, condCombo, patternField, filterIndex))
{
filtersChanged();
}
}
}
};
ItemListener itemListener = new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
actionListener.actionPerformed(null);
}
};
if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
{
attCombo.setSelectedIndex(0);
}
else
{
attCombo.setSelectedItem(
FeatureMatcher.toAttributeDisplayName(attName));
}
attCombo.addItemListener(new ItemListener()
{
@Override
public void itemStateChanged(ItemEvent e)
{
/*
* on change of attribute, refresh the conditions list to
* ensure it is appropriate for the attribute datatype
*/
populateConditions((String) attCombo.getSelectedItem(),
(Condition) condCombo.getSelectedItem(), condCombo,
patternField);
actionListener.actionPerformed(null);
}
});
filterRow.add(attCombo);
/*
* drop-down choice of test condition
*/
populateConditions(filterBy, cond, condCombo, patternField);
condCombo.setPreferredSize(new Dimension(150, 20));
condCombo.addItemListener(itemListener);
filterRow.add(condCombo);
/*
* pattern to match against
*/
patternField.addActionListener(actionListener);
patternField.addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
actionListener.actionPerformed(null);
}
});
filterRow.add(patternField);
/*
* disable pattern field for condition 'Present / NotPresent'
*/
Condition selectedCondition = (Condition) condCombo.getSelectedItem();
patternField.setEnabled(selectedCondition.needsAPattern());
/*
* if a numeric condition is selected, show the value range
* as a tooltip on the value input field
*/
setNumericHints(filterBy, selectedCondition, patternField);
/*
* add remove button if filter is populated (non-empty pattern)
*/
if (!patternField.isEnabled()
|| (pattern != null && pattern.trim().length() > 0))
{
JButton removeCondition = new JButton("\u2717"); // Dingbats cursive x
removeCondition.setToolTipText(
MessageManager.getString("label.delete_condition"));
removeCondition.setBorder(new EmptyBorder(0, 0, 0, 0));
removeCondition.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
filters.remove(filterIndex);
filtersChanged();
}
});
filterRow.add(removeCondition);
}
return filterRow;
}
/**
* Sets the selected item in the Label/Score/Attribute drop-down to match the
* filter
*
* @param attCombo
* @param filter
*/
private String setSelectedAttribute(JComboBox attCombo,
FeatureMatcherI filter)
{
String item = null;
if (filter.isByScore())
{
item = SCORE_18N;
}
else if (filter.isByLabel())
{
item = LABEL_18N;
}
else
{
item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
}
attCombo.setSelectedItem(item);
return item;
}
/**
* If a numeric comparison condition is selected, retrieves the min-max range
* for the value (score or attribute), and sets it as a tooltip on the value
* field. If the field is currently empty, then pre-populates it with
*
* the minimum value, if condition is > or >=
* the maximum value, if condition is < or <=
*
*
* @param attName
* @param selectedCondition
* @param patternField
*/
private void setNumericHints(String attName, Condition selectedCondition,
JTextField patternField)
{
patternField.setToolTipText("");
if (selectedCondition.isNumeric())
{
float[] minMax = getMinMax(attName);
if (minMax != null)
{
String minFormatted = DECFMT_2_2.format(minMax[0]);
String maxFormatted = DECFMT_2_2.format(minMax[1]);
String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
patternField.setToolTipText(tip);
if (patternField.getText().isEmpty())
{
if (selectedCondition == Condition.GE
|| selectedCondition == Condition.GT)
{
patternField.setText(minFormatted);
}
else
{
if (selectedCondition == Condition.LE
|| selectedCondition == Condition.LT)
{
patternField.setText(maxFormatted);
}
}
}
}
}
}
/**
* Populates the drop-down list of comparison conditions for the given
* attribute name. The conditions added depend on the datatype of the
* attribute values. The supplied condition is set as the selected item in the
* list, provided it is in the list. If the pattern is now invalid
* (non-numeric pattern for a numeric condition), it is cleared.
*
* @param attName
* @param cond
* @param condCombo
* @param patternField
*/
private void populateConditions(String attName, Condition cond,
JComboBox condCombo, JTextField patternField)
{
Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
FeatureMatcher.fromAttributeDisplayName(attName));
if (LABEL_18N.equals(attName))
{
type = Datatype.Character;
}
else if (SCORE_18N.equals(attName))
{
type = Datatype.Number;
}
/*
* remove itemListener before starting
*/
ItemListener listener = condCombo.getItemListeners()[0];
condCombo.removeItemListener(listener);
boolean condIsValid = false;
condCombo.removeAllItems();
for (Condition c : Condition.values())
{
if ((c.isNumeric() && type == Datatype.Number)
|| (!c.isNumeric() && type != Datatype.Number))
{
condCombo.addItem(c);
if (c == cond)
{
condIsValid = true;
}
}
}
/*
* set the selected condition (does nothing if not in the list)
*/
if (condIsValid)
{
condCombo.setSelectedItem(cond);
}
else
{
condCombo.setSelectedIndex(0);
}
/*
* clear pattern if it is now invalid for condition
*/
if (((Condition) condCombo.getSelectedItem()).isNumeric())
{
try
{
String pattern = patternField.getText().trim();
if (pattern.length() > 0)
{
Float.valueOf(pattern);
}
} catch (NumberFormatException e)
{
patternField.setText("");
}
}
/*
* restore the listener
*/
condCombo.addItemListener(listener);
}
/**
* Answers true unless a numeric condition has been selected with a
* non-numeric value. Sets the value field to RED with a tooltip if in error.
*
* If the pattern is expected but is empty, this method returns false, but
* does not mark the field as invalid. This supports selecting an attribute
* for a new condition before a match pattern has been entered.
*
* @param value
* @param condCombo
*/
protected boolean validateFilter(JTextField value,
JComboBox condCombo)
{
if (value == null || condCombo == null)
{
return true; // fields not populated
}
Condition cond = (Condition) condCombo.getSelectedItem();
if (!cond.needsAPattern())
{
return true;
}
value.setBackground(Color.white);
value.setToolTipText("");
String v1 = value.getText().trim();
if (v1.length() == 0)
{
// return false;
}
if (cond.isNumeric() && v1.length() > 0)
{
try
{
Float.valueOf(v1);
} catch (NumberFormatException e)
{
value.setBackground(Color.red);
value.setToolTipText(
MessageManager.getString("label.numeric_required"));
return false;
}
}
return true;
}
/**
* Constructs a filter condition from the given input fields, and replaces the
* condition at filterIndex with the new one. Does nothing if the pattern
* field is blank (unless the match condition is one that doesn't require a
* pattern, e.g. 'Is present'). Answers true if the filter was updated, else
* false.
*
* This method may update the tooltip on the filter value field to show the
* value range, if a numeric condition is selected. This ensures the tooltip
* is updated when a numeric valued attribute is chosen on the last 'add a
* filter' row.
*
* @param attCombo
* @param condCombo
* @param valueField
* @param filterIndex
*/
protected boolean updateFilter(JComboBox attCombo,
JComboBox condCombo, JTextField valueField,
int filterIndex)
{
String attName;
try
{
attName = (String) attCombo.getSelectedItem();
} catch (Exception e)
{
Cache.log.error("Problem casting Combo box entry to String");
attName = attCombo.getSelectedItem().toString();
}
Condition cond = (Condition) condCombo.getSelectedItem();
String pattern = valueField.getText().trim();
setNumericHints(attName, cond, valueField);
if (pattern.length() == 0 && cond.needsAPattern())
{
valueField.setEnabled(true); // ensure pattern field is enabled!
return false;
}
/*
* Construct a matcher that operates on Label, Score,
* or named attribute
*/
FeatureMatcherI km = null;
if (LABEL_18N.equals(attName))
{
km = FeatureMatcher.byLabel(cond, pattern);
}
else if (SCORE_18N.equals(attName))
{
km = FeatureMatcher.byScore(cond, pattern);
}
else
{
km = FeatureMatcher.byAttribute(cond, pattern,
FeatureMatcher.fromAttributeDisplayName(attName));
}
filters.set(filterIndex, km);
return true;
}
/**
* Action on any change to feature filtering, namely
*
* change of selected attribute
* change of selected condition
* change of match pattern
* removal of a condition
*
* The inputs are parsed into a combined filter and this is set for the
* feature type, and the alignment redrawn.
*/
protected void filtersChanged()
{
/*
* update the filter conditions for the feature type
*/
boolean anded = andFilters.isSelected();
FeatureMatcherSetI combined = new FeatureMatcherSet();
for (FeatureMatcherI filter : filters)
{
String pattern = filter.getMatcher().getPattern();
Condition condition = filter.getMatcher().getCondition();
if (pattern.trim().length() > 0 || !condition.needsAPattern())
{
if (anded)
{
combined.and(filter);
}
else
{
combined.or(filter);
}
}
}
/*
* save the filter conditions in the FeatureRenderer
* (note this might now be an empty filter with no conditions)
*/
fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
refreshDisplay(true);
updateFiltersTab();
}
/**
* Repaints alignment, structure and overview (if shown). If there is a
* complementary view which is showing this view's features, then also
* repaints that.
*
* @param updateStructsAndOverview
*/
void refreshDisplay(boolean updateStructsAndOverview)
{
ap.paintAlignment(true, updateStructsAndOverview);
AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
if (complement != null && complement.isShowComplementFeatures())
{
AlignFrame af2 = Desktop.getAlignFrameFor(complement);
af2.alignPanel.paintAlignment(true, updateStructsAndOverview);
}
}
}