JAL-4152 Remove redundant casts from getInstance usages.
[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 ApplicationSingletonProvider.getInstance(FeatureAttributes.class);
48   }
49
50   private FeatureAttributes()
51   {
52     attributes = new HashMap<>();
53   }
54
55   /*
56    * map, by feature type, of a map, by attribute name, of
57    * attribute description and min-max range (if known)
58    */
59   private Map<String, Map<String[], AttributeData>> attributes;
60
61   /*
62    * a case-insensitive comparator so that attributes are ordered e.g.
63    * AC
64    * af
65    * CSQ:AFR_MAF
66    * CSQ:Allele
67    */
68   private Comparator<String[]> comparator = new Comparator<String[]>()
69   {
70     @Override
71     public int compare(String[] o1, String[] o2)
72     {
73       int i = 0;
74       while (i < o1.length || i < o2.length)
75       {
76         if (o2.length <= i)
77         {
78           return o1.length <= i ? 0 : 1;
79         }
80         if (o1.length <= i)
81         {
82           return -1;
83         }
84         int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
85         if (comp != 0)
86         {
87           return comp;
88         }
89         i++;
90       }
91       return 0; // same length and all matched
92     }
93   };
94
95   private class AttributeData
96   {
97     /*
98      * description(s) for this attribute, if known
99      * (different feature source might have differing descriptions)
100      */
101     List<String> description;
102
103     /*
104      * minimum value (of any numeric values recorded)
105      */
106     float min = 0f;
107
108     /*
109      * maximum value (of any numeric values recorded)
110      */
111     float max = 0f;
112
113     /*
114      * flag is set true if any numeric value is detected for this attribute
115      */
116     boolean hasValue = false;
117
118     Datatype type;
119
120     /**
121      * Note one instance of this attribute, recording unique, non-null
122      * descriptions, and the min/max of any numerical values
123      * 
124      * @param desc
125      * @param value
126      */
127     void addInstance(String desc, String value)
128     {
129       addDescription(desc);
130
131       if (value != null)
132       {
133         value = value.trim();
134         if (value.isEmpty())
135         {
136           return;
137         }
138
139         /*
140          * Parse numeric value unless we have previously
141          * seen text data for this attribute type
142          */
143         if ((type == null && couldBeNumber(value))
144                 || type == Datatype.Number)
145         {
146           try
147           {
148             float f = Float.valueOf(value);
149             min = hasValue ? Math.min(min, f) : f;
150             max = hasValue ? Math.max(max, f) : f;
151             hasValue = true;
152             type = (type == null || type == Datatype.Number)
153                     ? Datatype.Number
154                     : Datatype.Mixed;
155           } catch (NumberFormatException e)
156           {
157             /*
158              * non-numeric data: treat attribute as Character (or Mixed)
159              */
160             type = (type == null || type == Datatype.Character)
161                     ? Datatype.Character
162                     : Datatype.Mixed;
163             min = 0f;
164             max = 0f;
165             hasValue = false;
166           }
167         }
168         else
169         {
170           /*
171            * if not a number, and not seen before...
172            */
173           type = Datatype.Character;
174           min = 0f;
175           max = 0f;
176           hasValue = false;
177         }
178       }
179     }
180
181     /**
182      * Answers the description of the attribute, if recorded and unique, or null
183      * if either no, or more than description is recorded
184      * 
185      * @return
186      */
187     public String getDescription()
188     {
189       if (description != null && description.size() == 1)
190       {
191         return description.get(0);
192       }
193       return null;
194     }
195
196     public Datatype getType()
197     {
198       return type;
199     }
200
201     /**
202      * Adds the given description to the list of known descriptions (without
203      * duplication)
204      * 
205      * @param desc
206      */
207     public void addDescription(String desc)
208     {
209       if (desc != null)
210       {
211         if (description == null)
212         {
213           description = new ArrayList<>();
214         }
215         if (!description.contains(desc))
216         {
217           description.add(desc);
218         }
219       }
220     }
221   }
222
223   /**
224    * Answers the attribute names known for the given feature type, in
225    * alphabetical order (not case sensitive), or an empty set if no attributes
226    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
227    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
228    * 
229    * @param featureType
230    * @return
231    */
232   public List<String[]> getAttributes(String featureType)
233   {
234     if (!attributes.containsKey(featureType))
235     {
236       return Collections.<String[]> emptyList();
237     }
238
239     return new ArrayList<>(attributes.get(featureType).keySet());
240   }
241
242   /**
243    * A partial check that the string is numeric - only checks the first
244    * character. Returns true if the first character is a digit, or if it is '.',
245    * '+' or '-' and not the only character. Otherwise returns false (including
246    * for an empty string). Note this is not a complete check as it returns true
247    * for (e.g.) "1A".
248    * 
249    * @param f
250    * @return
251    */
252   public static boolean couldBeNumber(String f)
253   {
254     int len = f.length();
255     if (len == 0)
256     {
257       return false;
258     }
259     char ch = f.charAt(0);
260     switch (ch)
261     {
262     case '.':
263     case '+':
264     case '-':
265       return len > 1;
266     }
267     return (ch <= '9' && ch >= '0');
268   }
269
270   /**
271    * Answers true if at least one attribute is known for the given feature type,
272    * else false
273    * 
274    * @param featureType
275    * @return
276    */
277   public boolean hasAttributes(String featureType)
278   {
279     if (attributes.containsKey(featureType))
280     {
281       if (!attributes.get(featureType).isEmpty())
282       {
283         return true;
284       }
285     }
286     return false;
287   }
288
289   /**
290    * Records the given attribute name and description for the given feature
291    * type, and updates the min-max for any numeric value
292    * 
293    * @param featureType
294    * @param description
295    * @param value
296    * @param attName
297    */
298   public void addAttribute(String featureType, String description,
299           Object value, String... attName)
300   {
301     if (featureType == null || attName == null)
302     {
303       return;
304     }
305
306     /*
307      * if attribute value is a map, drill down one more level to
308      * record its sub-fields
309      */
310     if (value instanceof Map<?, ?>)
311     {
312       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
313       {
314         String[] attNames = new String[attName.length + 1];
315         System.arraycopy(attName, 0, attNames, 0, attName.length);
316         attNames[attName.length] = entry.getKey().toString();
317         addAttribute(featureType, description, entry.getValue(), attNames);
318       }
319       return;
320     }
321
322     String valueAsString = value.toString();
323     Map<String[], AttributeData> atts = attributes.get(featureType);
324     if (atts == null)
325     {
326       atts = new TreeMap<>(comparator);
327       attributes.put(featureType, atts);
328     }
329     AttributeData attData = atts.get(attName);
330     if (attData == null)
331     {
332       attData = new AttributeData();
333       atts.put(attName, attData);
334     }
335     attData.addInstance(description, valueAsString);
336   }
337
338   /**
339    * Answers the description of the given attribute for the given feature type,
340    * if known and unique, else null
341    * 
342    * @param featureType
343    * @param attName
344    * @return
345    */
346   public String getDescription(String featureType, String... attName)
347   {
348     String desc = null;
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         desc = attData.getDescription();
356       }
357     }
358     return desc;
359   }
360
361   /**
362    * Answers the [min, max] value range of the given attribute for the given
363    * feature type, if known, else null. Attributes with a mixture of text and
364    * numeric values are considered text (do not return a min-max range).
365    * 
366    * @param featureType
367    * @param attName
368    * @return
369    */
370   public float[] getMinMax(String featureType, String... attName)
371   {
372     Map<String[], AttributeData> atts = attributes.get(featureType);
373     if (atts != null)
374     {
375       AttributeData attData = atts.get(attName);
376       if (attData != null && attData.hasValue)
377       {
378         return new float[] { attData.min, attData.max };
379       }
380     }
381     return null;
382   }
383
384   /**
385    * Records the given attribute description for the given feature type
386    * 
387    * @param featureType
388    * @param attName
389    * @param description
390    */
391   public void addDescription(String featureType, String description,
392           String... attName)
393   {
394     if (featureType == null || attName == null)
395     {
396       return;
397     }
398
399     Map<String[], AttributeData> atts = attributes.get(featureType);
400     if (atts == null)
401     {
402       atts = new TreeMap<>(comparator);
403       attributes.put(featureType, atts);
404     }
405     AttributeData attData = atts.get(attName);
406     if (attData == null)
407     {
408       attData = new AttributeData();
409       atts.put(attName, attData);
410     }
411     attData.addDescription(description);
412   }
413
414   /**
415    * Answers the datatype of the feature, which is one of Character, Number or
416    * Mixed (or null if not known), as discovered from values recorded.
417    * 
418    * @param featureType
419    * @param attName
420    * @return
421    */
422   public Datatype getDatatype(String featureType, String... attName)
423   {
424     Map<String[], AttributeData> atts = attributes.get(featureType);
425     if (atts != null)
426     {
427       AttributeData attData = atts.get(attName);
428       if (attData != null)
429       {
430         return attData.getType();
431       }
432     }
433     return null;
434   }
435
436   /**
437    * Resets all attribute metadata
438    */
439   public void clear()
440   {
441     attributes.clear();
442   }
443
444   /**
445    * Resets attribute metadata for one feature type
446    * 
447    * @param featureType
448    */
449   public void clear(String featureType)
450   {
451     Map<String[], AttributeData> map = attributes.get(featureType);
452     if (map != null)
453     {
454       map.clear();
455     }
456
457   }
458 }