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;
/**
*/
public class FeatureAttributes
{
+ public enum Datatype
+ {
+ Character, Number, Mixed
+ }
+
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
{
*/
float max = 0f;
+ /*
+ * flag is set true if any numeric value is detected for this attribute
+ */
+ boolean hasValue = false;
+
+ Datatype type;
+
/**
- * Note one instance of this attribute, recording unique, non-null names,
- * and the min/max of any numerical values
+ * Note one instance of this attribute, recording unique, non-null
+ * descriptions, and the min/max of any numerical values
*
* @param desc
* @param value
*/
void addInstance(String desc, String value)
{
- if (desc != null)
+ addDescription(desc);
+
+ if (value != null)
{
- if (description == null)
- {
- description = new ArrayList<>();
- }
- if (!description.contains(desc))
- {
- description.add(desc);
- }
- if (value != null)
+ value = value.trim();
+
+ /*
+ * Parse numeric value unless we have previously
+ * seen text data for this attribute type
+ */
+ if (type == null || type == Datatype.Number)
{
try
{
float f = Float.valueOf(value);
- min = Float.min(min, f);
- max = Float.max(max, f);
+ min = hasValue ? Math.min(min, f) : f;
+ max = hasValue ? Math.max(max, f) : f;
+ hasValue = true;
+ type = (type == null || type == Datatype.Number)
+ ? Datatype.Number
+ : Datatype.Mixed;
} catch (NumberFormatException e)
{
- // ok, wasn't a number
+ /*
+ * non-numeric data: treat attribute as Character (or Mixed)
+ */
+ type = (type == null || type == Datatype.Character)
+ ? Datatype.Character
+ : Datatype.Mixed;
+ min = 0f;
+ max = 0f;
+ hasValue = false;
}
}
}
}
return null;
}
+
+ public Datatype getType()
+ {
+ return type;
+ }
+
+ /**
+ * Adds the given description to the list of known descriptions (without
+ * duplication)
+ *
+ * @param desc
+ */
+ public void addDescription(String desc)
+ {
+ if (desc != null)
+ {
+ if (description == null)
+ {
+ description = new ArrayList<>();
+ }
+ if (!description.contains(desc))
+ {
+ description.add(desc);
+ }
+ }
+ }
}
/**
}
/**
- * 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);
}
return desc;
}
+
+ /**
+ * Answers the [min, max] value range of the given attribute for the given
+ * feature type, if known, else null. Attributes with a mixture of text and
+ * numeric values are considered text (do not return a min-max range).
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public float[] getMinMax(String featureType, String... attName)
+ {
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null && attData.hasValue)
+ {
+ return new float[] { attData.min, attData.max };
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Records the given attribute description for the given feature type
+ *
+ * @param featureType
+ * @param attName
+ * @param description
+ */
+ public void addDescription(String featureType, String description,
+ String... attName)
+ {
+ if (featureType == null || attName == null)
+ {
+ return;
+ }
+
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts == null)
+ {
+ atts = new TreeMap<>(comparator);
+ attributes.put(featureType, atts);
+ }
+ AttributeData attData = atts.get(attName);
+ if (attData == null)
+ {
+ attData = new AttributeData();
+ atts.put(attName, attData);
+ }
+ attData.addDescription(description);
+ }
+
+ /**
+ * Answers the datatype of the feature, which is one of Character, Number or
+ * Mixed (or null if not known), as discovered from values recorded.
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public Datatype getDatatype(String featureType, String... attName)
+ {
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null)
+ {
+ return attData.getType();
+ }
+ }
+ return null;
+ }
}