JAL-2835 spike updated with latest
[jalview.git] / src / jalview / datamodel / features / FeatureAttributes.java
1 package jalview.datamodel.features;
2
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.Comparator;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Map.Entry;
10 import java.util.TreeMap;
11
12 /**
13  * A singleton class to hold the set of attributes known for each feature type
14  */
15 public class FeatureAttributes
16 {
17   private static FeatureAttributes instance = new FeatureAttributes();
18
19   /*
20    * map, by feature type, of a map, by attribute name, of
21    * attribute description and min-max range (if known)
22    */
23   private Map<String, Map<String[], AttributeData>> attributes;
24
25   /*
26    * a case-insensitive comparator so that attributes are ordered e.g.
27    * AC
28    * af
29    * CSQ:AFR_MAF
30    * CSQ:Allele
31    */
32   private Comparator<String[]> comparator = new Comparator<String[]>()
33   {
34     @Override
35     public int compare(String[] o1, String[] o2)
36     {
37       int i = 0;
38       while (i < o1.length || i < o2.length)
39       {
40         if (o2.length <= i)
41         {
42           return o1.length <= i ? 0 : 1;
43         }
44         if (o1.length <= i)
45         {
46           return -1;
47         }
48         int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
49         if (comp != 0)
50         {
51           return comp;
52         }
53         i++;
54       }
55       return 0; // same length and all matched
56     }
57   };
58
59   private class AttributeData
60   {
61     /*
62      * description(s) for this attribute, if known
63      * (different feature source might have differing descriptions)
64      */
65     List<String> description;
66
67     /*
68      * minimum value (of any numeric values recorded)
69      */
70     float min = 0f;
71
72     /*
73      * maximum value (of any numeric values recorded)
74      */
75     float max = 0f;
76
77     /*
78      * flag is set true if any numeric value is detected for this attribute
79      */
80     boolean hasValue = false;
81
82     /**
83      * Note one instance of this attribute, recording unique, non-null names,
84      * and the min/max of any numerical values
85      * 
86      * @param desc
87      * @param value
88      */
89     void addInstance(String desc, String value)
90     {
91       addDescription(desc);
92
93       if (value != null)
94       {
95         try
96         {
97           float f = Float.valueOf(value);
98           min = Float.min(min, f);
99           max = Float.max(max, f);
100           hasValue = true;
101         } catch (NumberFormatException e)
102         {
103           // ok, wasn't a number, ignore for min-max purposes
104         }
105       }
106     }
107
108     /**
109      * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
110      * @return
111      */
112     public String getDescription()
113     {
114       if (description != null && description.size() == 1)
115       {
116         return description.get(0);
117       }
118       return null;
119     }
120
121     /**
122      * Adds the given description to the list of known descriptions (without
123      * duplication)
124      * 
125      * @param desc
126      */
127     public void addDescription(String desc)
128     {
129       if (desc != null)
130       {
131         if (description == null)
132         {
133           description = new ArrayList<>();
134         }
135         if (!description.contains(desc))
136         {
137           description.add(desc);
138         }
139       }
140     }
141   }
142
143   /**
144    * Answers the singleton instance of this class
145    * 
146    * @return
147    */
148   public static FeatureAttributes getInstance()
149   {
150     return instance;
151   }
152
153   private FeatureAttributes()
154   {
155     attributes = new HashMap<>();
156   }
157
158   /**
159    * Answers the attribute names known for the given feature type, in
160    * alphabetical order (not case sensitive), or an empty set if no attributes
161    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
162    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
163    * 
164    * @param featureType
165    * @return
166    */
167   public List<String[]> getAttributes(String featureType)
168   {
169     if (!attributes.containsKey(featureType))
170     {
171       return Collections.<String[]> emptyList();
172     }
173
174     return new ArrayList<>(attributes.get(featureType).keySet());
175   }
176
177   /**
178    * Answers true if at least one attribute is known for the given feature type,
179    * else false
180    * 
181    * @param featureType
182    * @return
183    */
184   public boolean hasAttributes(String featureType)
185   {
186     if (attributes.containsKey(featureType))
187     {
188       if (!attributes.get(featureType).isEmpty())
189       {
190         return true;
191       }
192     }
193     return false;
194   }
195
196   /**
197    * Records the given attribute name and description for the given feature
198    * type, and updates the min-max for any numeric value
199    * 
200    * @param featureType
201    * @param description
202    * @param value
203    * @param attName
204    */
205   public void addAttribute(String featureType, String description,
206           Object value, String... attName)
207   {
208     if (featureType == null || attName == null)
209     {
210       return;
211     }
212
213     /*
214      * if attribute value is a map, drill down one more level to
215      * record its sub-fields
216      */
217     if (value instanceof Map<?, ?>)
218     {
219       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
220       {
221         String[] attNames = new String[attName.length + 1];
222         System.arraycopy(attName, 0, attNames, 0, attName.length);
223         attNames[attName.length] = entry.getKey().toString();
224         addAttribute(featureType, description, entry.getValue(), attNames);
225       }
226       return;
227     }
228
229     String valueAsString = value.toString();
230     Map<String[], AttributeData> atts = attributes.get(featureType);
231     if (atts == null)
232     {
233       atts = new TreeMap<>(comparator);
234       attributes.put(featureType, atts);
235     }
236     AttributeData attData = atts.get(attName);
237     if (attData == null)
238     {
239       attData = new AttributeData();
240       atts.put(attName, attData);
241     }
242     attData.addInstance(description, valueAsString);
243   }
244
245   /**
246    * Answers the description of the given attribute for the given feature type,
247    * if known and unique, else null
248    * 
249    * @param featureType
250    * @param attName
251    * @return
252    */
253   public String getDescription(String featureType, String... attName)
254   {
255     String desc = null;
256     Map<String[], AttributeData> atts = attributes.get(featureType);
257     if (atts != null)
258     {
259       AttributeData attData = atts.get(attName);
260       if (attData != null)
261       {
262         desc = attData.getDescription();
263       }
264     }
265     return desc;
266   }
267
268   /**
269    * Answers the [min, max] value range of the given attribute for the given
270    * feature type, if known, else null. Attributes which only have text values
271    * would normally return null, however text values which happen to be numeric
272    * could result in a 'min-max' range.
273    * 
274    * @param featureType
275    * @param attName
276    * @return
277    */
278   public float[] getMinMax(String featureType, String... attName)
279   {
280     Map<String[], AttributeData> atts = attributes.get(featureType);
281     if (atts != null)
282     {
283       AttributeData attData = atts.get(attName);
284       if (attData != null && attData.hasValue)
285       {
286         return new float[] { attData.min, attData.max };
287       }
288     }
289     return null;
290   }
291
292   /**
293    * Records the given attribute description for the given feature type
294    * 
295    * @param featureType
296    * @param attName
297    * @param description
298    */
299   public void addDescription(String featureType, String description,
300           String... attName)
301   {
302     if (featureType == null || attName == null)
303     {
304       return;
305     }
306   
307     Map<String[], AttributeData> atts = attributes.get(featureType);
308     if (atts == null)
309     {
310       atts = new TreeMap<>(comparator);
311       attributes.put(featureType, atts);
312     }
313     AttributeData attData = atts.get(attName);
314     if (attData == null)
315     {
316       attData = new AttributeData();
317       atts.put(attName, attData);
318     }
319     attData.addDescription(description);
320   }
321 }