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