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