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