JAL-3446 post-merge unit test fixes, formatting
[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         if (value.isEmpty())
136         {
137           return;
138         }
139
140         /*
141          * Parse numeric value unless we have previously
142          * seen text data for this attribute type
143          */
144         if ((type == null && couldBeNumber(value))
145                 || type == Datatype.Number)
146         {
147           try
148           {
149             float f = Float.valueOf(value);
150             min = hasValue ? Math.min(min, f) : f;
151             max = hasValue ? Math.max(max, f) : f;
152             hasValue = true;
153             type = (type == null || type == Datatype.Number)
154                     ? Datatype.Number
155                     : Datatype.Mixed;
156           } catch (NumberFormatException e)
157           {
158             /*
159              * non-numeric data: treat attribute as Character (or Mixed)
160              */
161             type = (type == null || type == Datatype.Character)
162                     ? Datatype.Character
163                     : Datatype.Mixed;
164             min = 0f;
165             max = 0f;
166             hasValue = false;
167           }
168         }
169         else
170         {
171           /*
172            * if not a number, and not seen before...
173            */
174           type = Datatype.Character;
175           min = 0f;
176           max = 0f;
177           hasValue = false;
178         }
179       }
180     }
181
182     /**
183      * Answers the description of the attribute, if recorded and unique, or null
184      * if either no, or more than description is recorded
185      * 
186      * @return
187      */
188     public String getDescription()
189     {
190       if (description != null && description.size() == 1)
191       {
192         return description.get(0);
193       }
194       return null;
195     }
196
197     public Datatype getType()
198     {
199       return type;
200     }
201
202     /**
203      * Adds the given description to the list of known descriptions (without
204      * duplication)
205      * 
206      * @param desc
207      */
208     public void addDescription(String desc)
209     {
210       if (desc != null)
211       {
212         if (description == null)
213         {
214           description = new ArrayList<>();
215         }
216         if (!description.contains(desc))
217         {
218           description.add(desc);
219         }
220       }
221     }
222   }
223
224   /**
225    * Answers the attribute names known for the given feature type, in
226    * alphabetical order (not case sensitive), or an empty set if no attributes
227    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
228    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
229    * 
230    * @param featureType
231    * @return
232    */
233   public List<String[]> getAttributes(String featureType)
234   {
235     if (!attributes.containsKey(featureType))
236     {
237       return Collections.<String[]> emptyList();
238     }
239
240     return new ArrayList<>(attributes.get(featureType).keySet());
241   }
242
243   /**
244    * A partial check that the string is numeric - only checks the first
245    * character. Returns true if the first character is a digit, or if it is '.',
246    * '+' or '-' and not the only character. Otherwise returns false (including
247    * for an empty string). Note this is not a complete check as it returns true
248    * for (e.g.) "1A".
249    * 
250    * @param f
251    * @return
252    */
253   public static boolean couldBeNumber(String f)
254   {
255     int len = f.length();
256     if (len == 0)
257     {
258       return false;
259     }
260     char ch = f.charAt(0);
261     switch (ch)
262     {
263     case '.':
264     case '+':
265     case '-':
266       return len > 1;
267     }
268     return (ch <= '9' && ch >= '0');
269   }
270
271   /**
272    * Answers true if at least one attribute is known for the given feature type,
273    * else false
274    * 
275    * @param featureType
276    * @return
277    */
278   public boolean hasAttributes(String featureType)
279   {
280     if (attributes.containsKey(featureType))
281     {
282       if (!attributes.get(featureType).isEmpty())
283       {
284         return true;
285       }
286     }
287     return false;
288   }
289
290   /**
291    * Records the given attribute name and description for the given feature
292    * type, and updates the min-max for any numeric value
293    * 
294    * @param featureType
295    * @param description
296    * @param value
297    * @param attName
298    */
299   public void addAttribute(String featureType, String description,
300           Object value, String... attName)
301   {
302     if (featureType == null || attName == null)
303     {
304       return;
305     }
306
307     /*
308      * if attribute value is a map, drill down one more level to
309      * record its sub-fields
310      */
311     if (value instanceof Map<?, ?>)
312     {
313       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
314       {
315         String[] attNames = new String[attName.length + 1];
316         System.arraycopy(attName, 0, attNames, 0, attName.length);
317         attNames[attName.length] = entry.getKey().toString();
318         addAttribute(featureType, description, entry.getValue(), attNames);
319       }
320       return;
321     }
322
323     String valueAsString = value.toString();
324     Map<String[], AttributeData> atts = attributes.get(featureType);
325     if (atts == null)
326     {
327       atts = new TreeMap<>(comparator);
328       attributes.put(featureType, atts);
329     }
330     AttributeData attData = atts.get(attName);
331     if (attData == null)
332     {
333       attData = new AttributeData();
334       atts.put(attName, attData);
335     }
336     attData.addInstance(description, valueAsString);
337   }
338
339   /**
340    * Answers the description of the given attribute for the given feature type,
341    * if known and unique, else null
342    * 
343    * @param featureType
344    * @param attName
345    * @return
346    */
347   public String getDescription(String featureType, String... attName)
348   {
349     String desc = null;
350     Map<String[], AttributeData> atts = attributes.get(featureType);
351     if (atts != null)
352     {
353       AttributeData attData = atts.get(attName);
354       if (attData != null)
355       {
356         desc = attData.getDescription();
357       }
358     }
359     return desc;
360   }
361
362   /**
363    * Answers the [min, max] value range of the given attribute for the given
364    * feature type, if known, else null. Attributes with a mixture of text and
365    * numeric values are considered text (do not return a min-max range).
366    * 
367    * @param featureType
368    * @param attName
369    * @return
370    */
371   public float[] getMinMax(String featureType, String... attName)
372   {
373     Map<String[], AttributeData> atts = attributes.get(featureType);
374     if (atts != null)
375     {
376       AttributeData attData = atts.get(attName);
377       if (attData != null && attData.hasValue)
378       {
379         return new float[] { attData.min, attData.max };
380       }
381     }
382     return null;
383   }
384
385   /**
386    * Records the given attribute description for the given feature type
387    * 
388    * @param featureType
389    * @param attName
390    * @param description
391    */
392   public void addDescription(String featureType, String description,
393           String... attName)
394   {
395     if (featureType == null || attName == null)
396     {
397       return;
398     }
399
400     Map<String[], AttributeData> atts = attributes.get(featureType);
401     if (atts == null)
402     {
403       atts = new TreeMap<>(comparator);
404       attributes.put(featureType, atts);
405     }
406     AttributeData attData = atts.get(attName);
407     if (attData == null)
408     {
409       attData = new AttributeData();
410       atts.put(attName, attData);
411     }
412     attData.addDescription(description);
413   }
414
415   /**
416    * Answers the datatype of the feature, which is one of Character, Number or
417    * Mixed (or null if not known), as discovered from values recorded.
418    * 
419    * @param featureType
420    * @param attName
421    * @return
422    */
423   public Datatype getDatatype(String featureType, String... attName)
424   {
425     Map<String[], AttributeData> atts = attributes.get(featureType);
426     if (atts != null)
427     {
428       AttributeData attData = atts.get(attName);
429       if (attData != null)
430       {
431         return attData.getType();
432       }
433     }
434     return null;
435   }
436
437   /**
438    * Resets all attribute metadata
439    */
440   public void clear()
441   {
442     attributes.clear();
443   }
444
445   /**
446    * Resets attribute metadata for one feature type
447    * 
448    * @param featureType
449    */
450   public void clear(String featureType)
451   {
452     Map<String[], AttributeData> map = attributes.get(featureType);
453     if (map != null)
454     {
455       map.clear();
456     }
457
458   }
459 }