boolean isColourByAttribute();
/**
- * Answers the name of the attribute used for colouring if any, or null
+ * Answers the name of the attribute (and optional sub-attribute...) used for
+ * colouring if any, or null
*
* @return
*/
- String getAttributeName();
+ String[] getAttributeName();
/**
- * Sets the name of the attribute used for colouring if any, or null to remove
- * this property
+ * Sets the name of the attribute (and optional sub-attribute...) used for
+ * colouring if any, or null to remove this property
*
* @return
*/
- void setAttributeName(String name);
+ void setAttributeName(String... name);
}
List<String> getGroups(boolean visible);
/**
- * change visibility for a range of groups
+ * Set visibility for a list of groups
*
* @param toset
* @param visible
void setGroupVisibility(List<String> toset, boolean visible);
/**
- * change visibiilty of given group
+ * Set visibility of the given feature group
*
* @param group
* @param visible
void setGroupVisibility(String group, boolean visible);
/**
- * Returns features at the specified aligned column on the given sequence.
- * Non-positional features are not included. If the column has a gap, then
- * enclosing features are included (but not contact features).
+ * Returns visible features at the specified aligned column on the given
+ * sequence. Non-positional features are not included. If the column has a gap,
+ * then enclosing features are included (but not contact features).
*
* @param sequence
* @param column
private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
/*
- * map of otherDetails special keys, and their value fields' delimiter
- */
- private static final Map<String, String> INFO_KEYS = new HashMap<>();
-
- static
- {
- INFO_KEYS.put("CSQ", ",");
- // todo capture second level metadata (CSQ FORMAT)
- // and delimiter "|" so as to report in a table within a table?
- }
-
- /*
* ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
* name1=value1;name2=value2,value3;...etc
*/
if (sf.otherDetails != null)
{
- otherDetails = new HashMap<String, Object>();
+ otherDetails = new HashMap<>();
for (Entry<String, Object> entry : sf.otherDetails.entrySet())
{
otherDetails.put(entry.getKey(), entry.getValue());
}
if (sf.links != null && sf.links.size() > 0)
{
- links = new Vector<String>();
+ links = new Vector<>();
for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
{
links.addElement(sf.links.elementAt(i));
{
if (links == null)
{
- links = new Vector<String>();
+ links = new Vector<>();
}
if (!links.contains(labelLink))
/**
* Answers the value of the specified attribute as string, or null if no such
- * value
+ * value. If more than one attribute name is provided, tries to resolve as keys
+ * to nested maps. For example, if attribute "CSQ" holds a map of key-value
+ * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
+ * in that map.
*
* @param key
* @return
*/
- public String getValueAsString(String key)
+ public String getValueAsString(String... key)
{
if (otherDetails == null)
{
return null;
}
- Object value = otherDetails.get(key);
+ Object value = otherDetails.get(key[0]);
+ if (key.length > 1 && value instanceof Map<?, ?>)
+ {
+ value = ((Map) value).get(key[1]);
+ }
return value == null ? null : value.toString();
}
{
if (otherDetails == null)
{
- otherDetails = new HashMap<String, Object>();
+ otherDetails = new HashMap<>();
}
otherDetails.put(key, value);
.getAttributeName(key);
}
- FeatureAttributes.getInstance().addAttribute(this.type, key, attDesc,
- value.toString());
+ FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
+ key);
}
/*
{
continue; // to avoid double reporting
}
- if (INFO_KEYS.containsKey(key))
+
+ Object value = entry.getValue();
+ if (value instanceof Map<?, ?>)
{
/*
- * split selected INFO data by delimiter over multiple lines
+ * expand values in a Map attribute across separate lines
*/
- String delimiter = INFO_KEYS.get(key);
- String[] values = entry.getValue().toString().split(delimiter);
- for (String value : values)
+ Map<?, ?> values = (Map<?, ?>) value;
+ for (Entry<?, ?> e : values.entrySet())
{
- sb.append(String.format(ROW_DATA, key, "", value));
+ sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
+ .getValue().toString()));
}
}
else
- { // tried <td title="key"> but it failed to provide a tooltip :-(
+ {
+ // tried <td title="key"> but it failed to provide a tooltip :-(
String attDesc = null;
if (metadata != null)
{
attDesc = metadata.getAttributeName(key);
}
- String value = entry.getValue().toString();
- if (isValueInteresting(key, value, metadata))
+ String s = entry.getValue().toString();
+ if (isValueInteresting(key, s, metadata))
{
sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
- : attDesc, value));
+ : attDesc, s));
}
}
}
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.TreeMap;
/**
{
private static FeatureAttributes instance = new FeatureAttributes();
- private Map<String, Map<String, AttributeData>> attributes;
+ /*
+ * map, by feature type, of a map, by attribute name, of
+ * attribute description and min-max range (if known)
+ */
+ private Map<String, Map<String[], AttributeData>> attributes;
+
+ /*
+ * a case-insensitive comparator so that attributes are ordered e.g.
+ * AC
+ * af
+ * CSQ:AFR_MAF
+ * CSQ:Allele
+ */
+ private Comparator<String[]> comparator = new Comparator<String[]>()
+ {
+ @Override
+ public int compare(String[] o1, String[] o2)
+ {
+ int i = 0;
+ while (i < o1.length || i < o2.length)
+ {
+ if (o2.length <= i)
+ {
+ return o1.length <= i ? 0 : 1;
+ }
+ if (o1.length <= i)
+ {
+ return -1;
+ }
+ int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
+ if (comp != 0)
+ {
+ return comp;
+ }
+ i++;
+ }
+ return 0; // same length and all matched
+ }
+ };
private class AttributeData
{
}
/**
- * Answers the attributes known for the given feature type, in alphabetical
- * order (not case sensitive), or an empty set if no attributes are known
+ * Answers the attribute names known for the given feature type, in
+ * alphabetical order (not case sensitive), or an empty set if no attributes
+ * are known. An attribute name is typically 'simple' e.g. "AC", but may be
+ * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
*
* @param featureType
* @return
*/
- public List<String> getAttributes(String featureType)
+ public List<String[]> getAttributes(String featureType)
{
if (!attributes.containsKey(featureType))
{
- return Collections.<String> emptyList();
+ return Collections.<String[]> emptyList();
}
return new ArrayList<>(attributes.get(featureType).keySet());
* type, and updates the min-max for any numeric value
*
* @param featureType
- * @param attName
* @param description
* @param value
+ * @param attName
*/
- public void addAttribute(String featureType, String attName,
- String description, String value)
+ public void addAttribute(String featureType, String description,
+ Object value, String... attName)
{
if (featureType == null || attName == null)
{
return;
}
- Map<String, AttributeData> atts = attributes.get(featureType);
+ /*
+ * if attribute value is a map, drill down one more level to
+ * record its sub-fields
+ */
+ if (value instanceof Map<?, ?>)
+ {
+ for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+ {
+ String[] attNames = new String[attName.length + 1];
+ System.arraycopy(attName, 0, attNames, 0, attName.length);
+ attNames[attName.length] = entry.getKey().toString();
+ addAttribute(featureType, description, entry.getValue(), attNames);
+ }
+ return;
+ }
+
+ String valueAsString = value.toString();
+ Map<String[], AttributeData> atts = attributes.get(featureType);
if (atts == null)
{
- atts = new TreeMap<String, AttributeData>(
- String.CASE_INSENSITIVE_ORDER);
+ atts = new TreeMap<>(comparator);
attributes.put(featureType, atts);
}
AttributeData attData = atts.get(attName);
attData = new AttributeData();
atts.put(attName, attData);
}
- attData.addInstance(description, value);
+ attData.addInstance(description, valueAsString);
}
/**
* @param attName
* @return
*/
- public String getDescription(String featureType, String attName)
+ public String getDescription(String featureType, String... attName)
{
String desc = null;
- Map<String, AttributeData> atts = attributes.get(featureType);
+ Map<String[], AttributeData> atts = attributes.get(featureType);
if (atts != null)
{
AttributeData attData = atts.get(attName);
* @param attName
* @return
*/
- public float[] getMinMax(String featureType, String attName)
+ public float[] getMinMax(String featureType, String... attName)
{
- Map<String, AttributeData> atts = attributes.get(featureType);
+ Map<String[], AttributeData> atts = attributes.get(featureType);
if (atts != null)
{
AttributeData attData = atts.get(attName);
* @param attName
* @param description
*/
- public void addDescription(String featureType, String attName,
- String description)
+ public void addDescription(String featureType, String description,
+ String... attName)
{
if (featureType == null || attName == null)
{
return;
}
- Map<String, AttributeData> atts = attributes.get(featureType);
+ Map<String[], AttributeData> atts = attributes.get(featureType);
if (atts == null)
{
- atts = new TreeMap<String, AttributeData>(
- String.CASE_INSENSITIVE_ORDER);
+ atts = new TreeMap<>(comparator);
attributes.put(featureType, atts);
}
AttributeData attData = atts.get(attName);
public class FeatureColourChooser extends JalviewDialog
{
+ private static final String COLON = ":";
+
private static final int MAX_TOOLTIP_LENGTH = 50;
private static int NO_COLOUR_OPTION = 0;
this.fr = frender;
this.type = theType;
ap = fr.ap;
- String title = MessageManager
- .formatMessage("label.graduated_color_for_params", new String[]
- { theType });
+ String title = MessageManager.formatMessage("label.variable_color_for",
+ new String[] { theType });
initDialogFrame(this, true, blocking, title, 470, 300);
slider.addChangeListener(new ChangeListener()
{
byAttributeText.setSelected(true);
textAttributeCombo.setEnabled(true);
- textAttributeCombo.setSelectedItem(cs.getAttributeName());
+ String[] attributeName = cs.getAttributeName();
+ textAttributeCombo
+ .setSelectedItem(String.join(COLON, attributeName));
}
else
{
if (cs.isColourByAttribute())
{
byAttributeValue.setSelected(true);
- String attributeName = cs.getAttributeName();
- valueAttributeCombo.setSelectedItem(attributeName);
+ String[] attributeName = cs.getAttributeName();
+ valueAttributeCombo
+ .setSelectedItem(String.join(COLON, attributeName));
valueAttributeCombo.setEnabled(true);
updateMinMax();
}
else if (byAttributeValue.isSelected())
{
String attName = (String) valueAttributeCombo.getSelectedItem();
- minMax = FeatureAttributes.getInstance().getMinMax(type, attName);
+ String[] attNames = attName.split(COLON);
+ minMax = FeatureAttributes.getInstance().getMinMax(type, attNames);
}
if (minMax != null)
{
byAttributeValue.addActionListener(changeMinMaxAction);
byWhatPanel.add(byAttributeValue);
- List<String> attNames = FeatureAttributes.getInstance().getAttributes(
- type);
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(type);
valueAttributeCombo = populateAttributesDropdown(type, attNames, true);
/*
byAttributeText.addActionListener(changeColourAction);
byTextPanel.add(byAttributeText);
- List<String> attNames = FeatureAttributes.getInstance().getAttributes(
- type);
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(type);
textAttributeCombo = populateAttributesDropdown(type, attNames, false);
byTextPanel.add(textAttributeCombo);
{
attribute = (String) textAttributeCombo.getSelectedItem();
textAttributeCombo.setEnabled(true);
+ acg.setAttributeName(attribute.split(COLON));
}
else if (byAttributeValue.isSelected())
{
attribute = (String) valueAttributeCombo.getSelectedItem();
valueAttributeCombo.setEnabled(true);
+ acg.setAttributeName(attribute.split(COLON));
+ }
+ else
+ {
+ acg.setAttributeName((String) null);
}
- acg.setAttributeName(attribute);
if (!hasThreshold)
{
/**
* A helper method to build the drop-down choice of attributes for a feature.
* Where metadata is available with a description for an attribute, that is
- * added as a tooltip. The list may be restricted to attributes for which we
- * hold a range of numerical values (so suitable candidates for a graduated
- * colour scheme).
+ * added as a tooltip. The list may optionally be restricted to attributes for
+ * which we hold a range of numerical values (so suitable candidates for a
+ * graduated colour scheme).
+ * <p>
+ * 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.
*
* @param featureType
* @param attNames
* @param withNumericRange
*/
protected JComboBox<String> populateAttributesDropdown(
- String featureType, List<String> attNames,
+ String featureType, List<String[]> attNames,
boolean withNumericRange)
{
List<String> validAtts = new ArrayList<>();
List<String> tooltips = new ArrayList<>();
FeatureAttributes fa = FeatureAttributes.getInstance();
- for (String attName : attNames)
+ for (String[] attName : attNames)
{
if (withNumericRange)
{
continue;
}
}
- validAtts.add(attName);
+ validAtts.add(String.join(COLON, attName));
String desc = fa.getDescription(featureType, attName);
if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
{
public class FeatureSettings extends JPanel
implements FeatureSettingsControllerI
{
+ private static final String COLON = ":";
+
private static final int MIN_WIDTH = 400;
private static final int MIN_HEIGHT = 400;
* look up attributes known for feature type
*/
String selectedType = (String) filteredFeatureChoice.getSelectedItem();
- List<String> attNames = FeatureAttributes.getInstance().getAttributes(
- selectedType);
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(selectedType);
/*
* if this feature type has filters set, load them first
/*
* and an empty filter for the user to populate (add)
*/
- KeyedMatcherI noFilter = new KeyedMatcher("", Condition.values()[0], "");
+ KeyedMatcherI noFilter = new KeyedMatcher(Condition.values()[0], "",
+ (String) null);
filters.add(noFilter);
/*
int filterIndex = 0;
for (KeyedMatcherI filter : filters)
{
- String key = filter.getKey();
+ String[] attName = filter.getKey();
Condition condition = filter.getMatcher()
.getCondition();
String pattern = filter.getMatcher().getPattern();
- JPanel row = addFilter(key, attNames, condition, pattern, filterIndex);
+ JPanel row = addFilter(attName, attNames, condition, pattern, filterIndex);
row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
chooseFiltersPanel.add(row);
filterIndex++;
* <li>a text field for input of a match pattern</li>
* <li>optionally, a 'remove' button</li>
* </ul>
- * If attribute, condition or pattern are not null, they are set as defaults
- * for the input fields. The 'remove' button is added unless the pattern is
- * null or empty (incomplete filter condition).
+ * If attribute, condition or pattern are not null, they are set as defaults for
+ * the input fields. The 'remove' button is added unless the pattern is null or
+ * empty (incomplete filter condition).
*
- * @param attribute
+ * @param attName
* @param attNames
* @param cond
* @param pattern
* @param filterIndex
* @return
*/
- protected JPanel addFilter(String attribute, List<String> attNames,
+ protected JPanel addFilter(String[] attName, List<String[]> attNames,
Condition cond, String pattern, int filterIndex)
{
JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
}
};
- if ("".equals(attribute))
+ if (attName == null) // the 'add a condition' row
{
attCombo.setSelectedItem(null);
}
else
{
- attCombo.setSelectedItem(attribute);
+ attCombo.setSelectedItem(String.join(COLON, attName));
}
attCombo.addItemListener(itemListener);
* @param attNames
*/
protected JComboBox<String> populateAttributesDropdown(
- String featureType, List<String> attNames)
+ String featureType, List<String[]> attNames)
{
+ List<String> displayNames = new ArrayList<>();
List<String> tooltips = new ArrayList<>();
FeatureAttributes fa = FeatureAttributes.getInstance();
- for (String attName : attNames)
+ for (String[] attName : attNames)
{
String desc = fa.getDescription(featureType, attName);
if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
{
desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
}
+ displayNames.add(String.join(COLON, attName));
tooltips.add(desc == null ? "" : desc);
}
JComboBox<String> attCombo = JvSwingUtils.buildComboWithTooltips(
- attNames, tooltips);
+ displayNames, tooltips);
if (attNames.isEmpty())
{
attCombo.setToolTipText(MessageManager
String attName = (String) attCombo.getSelectedItem();
Condition cond = (Condition) condCombo.getSelectedItem();
String pattern = valueField.getText();
- KeyedMatcherI km = new KeyedMatcher(attName, cond, pattern);
+ KeyedMatcherI km = new KeyedMatcher(cond, pattern,
+ attName.split(COLON));
filters.set(filterIndex, km);
}
if (gcol.isColourByAttribute())
{
- tx.append(gcol.getAttributeName());
+ tx.append(String.join(":", gcol.getAttributeName()));
}
else if (!gcol.isColourByLabel())
{
FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
if (fc != null && fc.isColourByAttribute())
{
- String attName = fc.getAttributeName();
+ String[] attName = fc.getAttributeName();
String attVal = feature.getValueAsString(attName);
if (attVal != null)
{
- sb.append("; ").append(attName).append("=").append(attVal);
+ sb.append("; ").append(String.join(":", attName)).append("=")
+ .append(attVal);
}
}
}
*/
Collection<List<String>> createLinksFrom(SequenceI seq, String link)
{
- Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
+ Map<String, List<String>> urlSets = new LinkedHashMap<>();
UrlLink urlLink = new UrlLink(link);
if (!urlLink.isValid())
{
private boolean colourByLabel;
/*
- * if not null, the value of this named attribute is used for
- * colourByLabel or graduatedColour
+ * if not null, the value of [attribute, [sub-attribute] ...]
+ * is used for colourByLabel or graduatedColour
*/
- private String byAttributeName;
+ private String[] attributeName;
private float threshold;
base = fc.base;
range = fc.range;
isHighToLow = fc.isHighToLow;
- byAttributeName = fc.byAttributeName;
+ attributeName = fc.attributeName;
setAboveThreshold(fc.isAboveThreshold());
setBelowThreshold(fc.isBelowThreshold());
setThreshold(fc.getThreshold());
{
if (isColourByLabel())
{
- String label = byAttributeName == null ? feature.getDescription()
- : feature.getValueAsString(byAttributeName);
+ String label = attributeName == null ? feature.getDescription()
+ : feature.getValueAsString(attributeName);
return label == null ? noColour : ColorUtils
.createColourFromName(label);
}
* no such attribute is assigned the 'no value' colour
*/
float scr = feature.getScore();
- if (byAttributeName != null)
+ if (attributeName != null)
{
try
{
- String attVal = feature.getValueAsString(byAttributeName);
+ String attVal = feature.getValueAsString(attributeName);
scr = Float.valueOf(attVal);
} catch (Throwable e)
{
@Override
public boolean isColourByAttribute()
{
- return byAttributeName != null;
+ return attributeName != null;
}
@Override
- public String getAttributeName()
+ public String[] getAttributeName()
{
- return byAttributeName;
+ return attributeName;
}
@Override
- public void setAttributeName(String name)
+ public void setAttributeName(String... name)
{
- byAttributeName = name;
+ attributeName = name;
}
}
--- /dev/null
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Comparator;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class FeatureAttributesTest
+{
+
+ /**
+ * Test the method that keeps attribute names in non-case-sensitive order,
+ * including handling of 'compound' names
+ */
+ @Test(groups="Functional")
+ public void testAttributeNameComparator()
+ {
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ Comparator<String[]> comp = (Comparator<String[]>) PA.getValue(fa,
+ "comparator");
+
+ assertEquals(
+ comp.compare(new String[] { "CSQ" }, new String[] { "csq" }), 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ", "a" },
+ new String[] { "csq" }) > 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ" }, new String[] { "csq",
+ "b" }) < 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ", "AF" }, new String[] {
+ "csq", "ac" }) > 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ", "ac" }, new String[] {
+ "csq", "AF" }) < 0);
+ }
+}