JAL-3375 ignore '.' values for VCF data
[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
91      * descriptions, 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         value = value.trim();
103
104         /*
105          * Parse numeric value unless we have previously
106          * seen text data for this attribute type
107          */
108         if (type == null || type == Datatype.Number)
109         {
110           try
111           {
112             float f = Float.valueOf(value);
113             min = hasValue ? Float.min(min, f) : f;
114             max = hasValue ? Float.max(max, f) : f;
115             hasValue = true;
116             type = (type == null || type == Datatype.Number)
117                     ? Datatype.Number
118                     : Datatype.Mixed;
119           } catch (NumberFormatException e)
120           {
121             /*
122              * non-numeric data: treat attribute as Character (or Mixed)
123              */
124             type = (type == null || type == Datatype.Character)
125                     ? Datatype.Character
126                     : Datatype.Mixed;
127             min = 0f;
128             max = 0f;
129             hasValue = false;
130           }
131         }
132       }
133     }
134
135     /**
136      * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
137      * @return
138      */
139     public String getDescription()
140     {
141       if (description != null && description.size() == 1)
142       {
143         return description.get(0);
144       }
145       return null;
146     }
147
148     public Datatype getType()
149     {
150       return type;
151     }
152
153     /**
154      * Adds the given description to the list of known descriptions (without
155      * duplication)
156      * 
157      * @param desc
158      */
159     public void addDescription(String desc)
160     {
161       if (desc != null)
162       {
163         if (description == null)
164         {
165           description = new ArrayList<>();
166         }
167         if (!description.contains(desc))
168         {
169           description.add(desc);
170         }
171       }
172     }
173   }
174
175   /**
176    * Answers the singleton instance of this class
177    * 
178    * @return
179    */
180   public static FeatureAttributes getInstance()
181   {
182     return instance;
183   }
184
185   private FeatureAttributes()
186   {
187     attributes = new HashMap<>();
188   }
189
190   /**
191    * Answers the attribute names known for the given feature type, in
192    * alphabetical order (not case sensitive), or an empty set if no attributes
193    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
194    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
195    * 
196    * @param featureType
197    * @return
198    */
199   public List<String[]> getAttributes(String featureType)
200   {
201     if (!attributes.containsKey(featureType))
202     {
203       return Collections.<String[]> emptyList();
204     }
205
206     return new ArrayList<>(attributes.get(featureType).keySet());
207   }
208
209   /**
210    * Answers true if at least one attribute is known for the given feature type,
211    * else false
212    * 
213    * @param featureType
214    * @return
215    */
216   public boolean hasAttributes(String featureType)
217   {
218     if (attributes.containsKey(featureType))
219     {
220       if (!attributes.get(featureType).isEmpty())
221       {
222         return true;
223       }
224     }
225     return false;
226   }
227
228   /**
229    * Records the given attribute name and description for the given feature
230    * type, and updates the min-max for any numeric value
231    * 
232    * @param featureType
233    * @param description
234    * @param value
235    * @param attName
236    */
237   public void addAttribute(String featureType, String description,
238           Object value, String... attName)
239   {
240     if (featureType == null || attName == null)
241     {
242       return;
243     }
244
245     /*
246      * if attribute value is a map, drill down one more level to
247      * record its sub-fields
248      */
249     if (value instanceof Map<?, ?>)
250     {
251       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
252       {
253         String[] attNames = new String[attName.length + 1];
254         System.arraycopy(attName, 0, attNames, 0, attName.length);
255         attNames[attName.length] = entry.getKey().toString();
256         addAttribute(featureType, description, entry.getValue(), attNames);
257       }
258       return;
259     }
260
261     String valueAsString = value.toString();
262     Map<String[], AttributeData> atts = attributes.get(featureType);
263     if (atts == null)
264     {
265       atts = new TreeMap<>(comparator);
266       attributes.put(featureType, atts);
267     }
268     AttributeData attData = atts.get(attName);
269     if (attData == null)
270     {
271       attData = new AttributeData();
272       atts.put(attName, attData);
273     }
274     attData.addInstance(description, valueAsString);
275   }
276
277   /**
278    * Answers the description of the given attribute for the given feature type,
279    * if known and unique, else null
280    * 
281    * @param featureType
282    * @param attName
283    * @return
284    */
285   public String getDescription(String featureType, String... attName)
286   {
287     String desc = null;
288     Map<String[], AttributeData> atts = attributes.get(featureType);
289     if (atts != null)
290     {
291       AttributeData attData = atts.get(attName);
292       if (attData != null)
293       {
294         desc = attData.getDescription();
295       }
296     }
297     return desc;
298   }
299
300   /**
301    * Answers the [min, max] value range of the given attribute for the given
302    * feature type, if known, else null. Attributes with a mixture of text and
303    * numeric values are considered text (do not return a min-max range).
304    * 
305    * @param featureType
306    * @param attName
307    * @return
308    */
309   public float[] getMinMax(String featureType, String... attName)
310   {
311     Map<String[], AttributeData> atts = attributes.get(featureType);
312     if (atts != null)
313     {
314       AttributeData attData = atts.get(attName);
315       if (attData != null && attData.hasValue)
316       {
317         return new float[] { attData.min, attData.max };
318       }
319     }
320     return null;
321   }
322
323   /**
324    * Records the given attribute description for the given feature type
325    * 
326    * @param featureType
327    * @param attName
328    * @param description
329    */
330   public void addDescription(String featureType, String description,
331           String... attName)
332   {
333     if (featureType == null || attName == null)
334     {
335       return;
336     }
337   
338     Map<String[], AttributeData> atts = attributes.get(featureType);
339     if (atts == null)
340     {
341       atts = new TreeMap<>(comparator);
342       attributes.put(featureType, atts);
343     }
344     AttributeData attData = atts.get(attName);
345     if (attData == null)
346     {
347       attData = new AttributeData();
348       atts.put(attName, attData);
349     }
350     attData.addDescription(description);
351   }
352
353   /**
354    * Answers the datatype of the feature, which is one of Character, Number or
355    * Mixed (or null if not known), as discovered from values recorded.
356    * 
357    * @param featureType
358    * @param attName
359    * @return
360    */
361   public Datatype getDatatype(String featureType, String... attName)
362   {
363     Map<String[], AttributeData> atts = attributes.get(featureType);
364     if (atts != null)
365     {
366       AttributeData attData = atts.get(attName);
367       if (attData != null)
368       {
369         return attData.getType();
370       }
371     }
372     return null;
373   }
374
375   /**
376    * Resets all attribute metadata
377    */
378   public void clear()
379   {
380     attributes.clear();
381   }
382
383   /**
384    * Resets attribute metadata for one feature type
385    * 
386    * @param featureType
387    */
388   public void clear(String featureType)
389   {
390     Map<String[], AttributeData> map = attributes.get(featureType);
391     if (map != null)
392     {
393       map.clear();
394     }
395
396   }
397 }