package jalview.datamodel.features; 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; /** * A singleton class to hold the set of attributes known for each feature type */ public class FeatureAttributes { public enum Datatype { Character, Number, Mixed } private static FeatureAttributes instance = new FeatureAttributes(); /* * map, by feature type, of a map, by attribute name, of * attribute description and min-max range (if known) */ private Map> attributes; /* * a case-insensitive comparator so that attributes are ordered e.g. * AC * af * CSQ:AFR_MAF * CSQ:Allele */ private Comparator comparator = new Comparator() { @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 { /* * description(s) for this attribute, if known * (different feature source might have differing descriptions) */ List description; /* * minimum value (of any numeric values recorded) */ float min = 0f; /* * maximum value (of any numeric values recorded) */ 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 * descriptions, and the min/max of any numerical values * * @param desc * @param value */ void addInstance(String desc, String value) { addDescription(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 = hasValue ? Float.min(min, f) : f; max = hasValue ? Float.max(max, f) : f; hasValue = true; type = (type == null || type == Datatype.Number) ? Datatype.Number : Datatype.Mixed; } catch (NumberFormatException e) { /* * 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; } } } } /** * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded * @return */ public String getDescription() { if (description != null && description.size() == 1) { return description.get(0); } 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 singleton instance of this class * * @return */ public static FeatureAttributes getInstance() { return instance; } private FeatureAttributes() { attributes = new HashMap<>(); } /** * 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 getAttributes(String featureType) { if (!attributes.containsKey(featureType)) { return Collections. emptyList(); } return new ArrayList<>(attributes.get(featureType).keySet()); } /** * Answers true if at least one attribute is known for the given feature type, * else false * * @param featureType * @return */ public boolean hasAttributes(String featureType) { if (attributes.containsKey(featureType)) { if (!attributes.get(featureType).isEmpty()) { return true; } } return false; } /** * Records the given attribute name and description for the given feature * type, and updates the min-max for any numeric value * * @param featureType * @param description * @param value * @param attName */ public void addAttribute(String featureType, String description, Object value, String... attName) { if (featureType == null || attName == null) { return; } /* * 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 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.addInstance(description, valueAsString); } /** * Answers the description of the given attribute for the given feature type, * if known and unique, else null * * @param featureType * @param attName * @return */ public String getDescription(String featureType, String... attName) { String desc = null; Map atts = attributes.get(featureType); if (atts != null) { AttributeData attData = atts.get(attName); if (attData != null) { desc = attData.getDescription(); } } 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 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 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 atts = attributes.get(featureType); if (atts != null) { AttributeData attData = atts.get(attName); if (attData != null) { return attData.getType(); } } return null; } }