655d1eb6b4dfee16c67a3e6a2863cd7a56372a78
[jalview.git] / src / jalview / datamodel / features / FeatureAttributes.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.datamodel.features;
22
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.TreeMap;
31
32 import jalview.bin.ApplicationSingletonProvider;
33 import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
34
35 /**
36  * A singleton class to hold the set of attributes known for each feature type
37  */
38 public class FeatureAttributes implements ApplicationSingletonI
39 {
40   public enum Datatype
41   {
42     Character, Number, Mixed
43   }
44
45   public static FeatureAttributes getInstance()
46   {
47     return (FeatureAttributes) ApplicationSingletonProvider
48             .getInstance(FeatureAttributes.class);
49   }
50
51   private FeatureAttributes()
52   {
53     attributes = new HashMap<>();
54   }
55
56   /*
57    * map, by feature type, of a map, by attribute name, of
58    * attribute description and min-max range (if known)
59    */
60   private Map<String, Map<String[], AttributeData>> attributes;
61
62   /*
63    * a case-insensitive comparator so that attributes are ordered e.g.
64    * AC
65    * af
66    * CSQ:AFR_MAF
67    * CSQ:Allele
68    */
69   private Comparator<String[]> comparator = new Comparator<String[]>()
70   {
71     @Override
72     public int compare(String[] o1, String[] o2)
73     {
74       int i = 0;
75       while (i < o1.length || i < o2.length)
76       {
77         if (o2.length <= i)
78         {
79           return o1.length <= i ? 0 : 1;
80         }
81         if (o1.length <= i)
82         {
83           return -1;
84         }
85         int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
86         if (comp != 0)
87         {
88           return comp;
89         }
90         i++;
91       }
92       return 0; // same length and all matched
93     }
94   };
95
96   private class AttributeData
97   {
98     /*
99      * description(s) for this attribute, if known
100      * (different feature source might have differing descriptions)
101      */
102     List<String> description;
103
104     /*
105      * minimum value (of any numeric values recorded)
106      */
107     float min = 0f;
108
109     /*
110      * maximum value (of any numeric values recorded)
111      */
112     float max = 0f;
113
114     /*
115      * flag is set true if any numeric value is detected for this attribute
116      */
117     boolean hasValue = false;
118
119     Datatype type;
120
121     /**
122      * Note one instance of this attribute, recording unique, non-null
123      * descriptions, and the min/max of any numerical values
124      * 
125      * @param desc
126      * @param value
127      */
128     void addInstance(String desc, String value)
129     {
130       addDescription(desc);
131
132       if (value != null)
133       {
134         value = value.trim();
135
136         /*
137          * Parse numeric value unless we have previously
138          * seen text data for this attribute type
139          */
140         if (type == null && couldBeNumber(value) || type == Datatype.Number)
141         {
142           try
143           {
144             float f = Float.valueOf(value);
145             min = hasValue ? Math.min(min, f) : f;
146             max = hasValue ? Math.max(max, f) : f;
147             hasValue = true;
148             type = (type == null || type == Datatype.Number)
149                     ? Datatype.Number
150                     : Datatype.Mixed;
151           } catch (NumberFormatException e)
152           {
153             /*
154              * non-numeric data: treat attribute as Character (or Mixed)
155              */
156             type = (type == null || type == Datatype.Character)
157                     ? Datatype.Character
158                     : Datatype.Mixed;
159             min = 0f;
160             max = 0f;
161             hasValue = false;
162           }
163         }
164       }
165     }
166
167     /**
168      * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
169      * @return
170      */
171     public String getDescription()
172     {
173       if (description != null && description.size() == 1)
174       {
175         return description.get(0);
176       }
177       return null;
178     }
179
180     public Datatype getType()
181     {
182       return type;
183     }
184
185     /**
186      * Adds the given description to the list of known descriptions (without
187      * duplication)
188      * 
189      * @param desc
190      */
191     public void addDescription(String desc)
192     {
193       if (desc != null)
194       {
195         if (description == null)
196         {
197           description = new ArrayList<>();
198         }
199         if (!description.contains(desc))
200         {
201           description.add(desc);
202         }
203       }
204     }
205   }
206
207   /**
208    * Answers the attribute names known for the given feature type, in
209    * alphabetical order (not case sensitive), or an empty set if no attributes
210    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
211    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
212    * 
213    * @param featureType
214    * @return
215    */
216   public List<String[]> getAttributes(String featureType)
217   {
218     if (!attributes.containsKey(featureType))
219     {
220       return Collections.<String[]> emptyList();
221     }
222
223     return new ArrayList<>(attributes.get(featureType).keySet());
224   }
225
226   /**
227    * This quick check will save significant time avoiding numerous NumberFormatExceptions.
228    * 
229    * @param f
230    * @return
231    */
232   public boolean couldBeNumber(String f)
233   {
234     int len = f.length();
235     if (len == 0)
236       return false;
237     char ch = f.charAt(0);
238     switch (ch) {
239     case '.':
240     case '+':
241     case '-':
242       return len > 1;
243     }
244     return (ch <= '9' &&  ch >= '0');
245   }
246
247   /**
248    * Answers true if at least one attribute is known for the given feature type,
249    * else false
250    * 
251    * @param featureType
252    * @return
253    */
254   public boolean hasAttributes(String featureType)
255   {
256     if (attributes.containsKey(featureType))
257     {
258       if (!attributes.get(featureType).isEmpty())
259       {
260         return true;
261       }
262     }
263     return false;
264   }
265
266   /**
267    * Records the given attribute name and description for the given feature
268    * type, and updates the min-max for any numeric value
269    * 
270    * @param featureType
271    * @param description
272    * @param value
273    * @param attName
274    */
275   public void addAttribute(String featureType, String description,
276           Object value, String... attName)
277   {
278     if (featureType == null || attName == null)
279     {
280       return;
281     }
282
283     /*
284      * if attribute value is a map, drill down one more level to
285      * record its sub-fields
286      */
287     if (value instanceof Map<?, ?>)
288     {
289       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
290       {
291         String[] attNames = new String[attName.length + 1];
292         System.arraycopy(attName, 0, attNames, 0, attName.length);
293         attNames[attName.length] = entry.getKey().toString();
294         addAttribute(featureType, description, entry.getValue(), attNames);
295       }
296       return;
297     }
298
299     String valueAsString = value.toString();
300     Map<String[], AttributeData> atts = attributes.get(featureType);
301     if (atts == null)
302     {
303       atts = new TreeMap<>(comparator);
304       attributes.put(featureType, atts);
305     }
306     AttributeData attData = atts.get(attName);
307     if (attData == null)
308     {
309       attData = new AttributeData();
310       atts.put(attName, attData);
311     }
312     attData.addInstance(description, valueAsString);
313   }
314
315   /**
316    * Answers the description of the given attribute for the given feature type,
317    * if known and unique, else null
318    * 
319    * @param featureType
320    * @param attName
321    * @return
322    */
323   public String getDescription(String featureType, String... attName)
324   {
325     String desc = null;
326     Map<String[], AttributeData> atts = attributes.get(featureType);
327     if (atts != null)
328     {
329       AttributeData attData = atts.get(attName);
330       if (attData != null)
331       {
332         desc = attData.getDescription();
333       }
334     }
335     return desc;
336   }
337
338   /**
339    * Answers the [min, max] value range of the given attribute for the given
340    * feature type, if known, else null. Attributes with a mixture of text and
341    * numeric values are considered text (do not return a min-max range).
342    * 
343    * @param featureType
344    * @param attName
345    * @return
346    */
347   public float[] getMinMax(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 && attData.hasValue)
354       {
355         return new float[] { attData.min, attData.max };
356       }
357     }
358     return null;
359   }
360
361   /**
362    * Records the given attribute description for the given feature type
363    * 
364    * @param featureType
365    * @param attName
366    * @param description
367    */
368   public void addDescription(String featureType, String description,
369           String... attName)
370   {
371     if (featureType == null || attName == null)
372     {
373       return;
374     }
375   
376     Map<String[], AttributeData> atts = attributes.get(featureType);
377     if (atts == null)
378     {
379       atts = new TreeMap<>(comparator);
380       attributes.put(featureType, atts);
381     }
382     AttributeData attData = atts.get(attName);
383     if (attData == null)
384     {
385       attData = new AttributeData();
386       atts.put(attName, attData);
387     }
388     attData.addDescription(description);
389   }
390
391   /**
392    * Answers the datatype of the feature, which is one of Character, Number or
393    * Mixed (or null if not known), as discovered from values recorded.
394    * 
395    * @param featureType
396    * @param attName
397    * @return
398    */
399   public Datatype getDatatype(String featureType, String... attName)
400   {
401     Map<String[], AttributeData> atts = attributes.get(featureType);
402     if (atts != null)
403     {
404       AttributeData attData = atts.get(attName);
405       if (attData != null)
406       {
407         return attData.getType();
408       }
409     }
410     return null;
411   }
412
413   /**
414    * Resets all attribute metadata
415    */
416   public void clear()
417   {
418     attributes.clear();
419   }
420
421   /**
422    * Resets attribute metadata for one feature type
423    * 
424    * @param featureType
425    */
426   public void clear(String featureType)
427   {
428     Map<String[], AttributeData> map = attributes.get(featureType);
429     if (map != null)
430     {
431       map.clear();
432     }
433
434   }
435 }