JAL-3446 from JAL-3253 ApplicationSingletonProvider missing for
[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 || 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    * Answers true if at least one attribute is known for the given feature type,
228    * else false
229    * 
230    * @param featureType
231    * @return
232    */
233   public boolean hasAttributes(String featureType)
234   {
235     if (attributes.containsKey(featureType))
236     {
237       if (!attributes.get(featureType).isEmpty())
238       {
239         return true;
240       }
241     }
242     return false;
243   }
244
245   /**
246    * Records the given attribute name and description for the given feature
247    * type, and updates the min-max for any numeric value
248    * 
249    * @param featureType
250    * @param description
251    * @param value
252    * @param attName
253    */
254   public void addAttribute(String featureType, String description,
255           Object value, String... attName)
256   {
257     if (featureType == null || attName == null)
258     {
259       return;
260     }
261
262     /*
263      * if attribute value is a map, drill down one more level to
264      * record its sub-fields
265      */
266     if (value instanceof Map<?, ?>)
267     {
268       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
269       {
270         String[] attNames = new String[attName.length + 1];
271         System.arraycopy(attName, 0, attNames, 0, attName.length);
272         attNames[attName.length] = entry.getKey().toString();
273         addAttribute(featureType, description, entry.getValue(), attNames);
274       }
275       return;
276     }
277
278     String valueAsString = value.toString();
279     Map<String[], AttributeData> atts = attributes.get(featureType);
280     if (atts == null)
281     {
282       atts = new TreeMap<>(comparator);
283       attributes.put(featureType, atts);
284     }
285     AttributeData attData = atts.get(attName);
286     if (attData == null)
287     {
288       attData = new AttributeData();
289       atts.put(attName, attData);
290     }
291     attData.addInstance(description, valueAsString);
292   }
293
294   /**
295    * Answers the description of the given attribute for the given feature type,
296    * if known and unique, else null
297    * 
298    * @param featureType
299    * @param attName
300    * @return
301    */
302   public String getDescription(String featureType, String... attName)
303   {
304     String desc = null;
305     Map<String[], AttributeData> atts = attributes.get(featureType);
306     if (atts != null)
307     {
308       AttributeData attData = atts.get(attName);
309       if (attData != null)
310       {
311         desc = attData.getDescription();
312       }
313     }
314     return desc;
315   }
316
317   /**
318    * Answers the [min, max] value range of the given attribute for the given
319    * feature type, if known, else null. Attributes with a mixture of text and
320    * numeric values are considered text (do not return a min-max range).
321    * 
322    * @param featureType
323    * @param attName
324    * @return
325    */
326   public float[] getMinMax(String featureType, String... attName)
327   {
328     Map<String[], AttributeData> atts = attributes.get(featureType);
329     if (atts != null)
330     {
331       AttributeData attData = atts.get(attName);
332       if (attData != null && attData.hasValue)
333       {
334         return new float[] { attData.min, attData.max };
335       }
336     }
337     return null;
338   }
339
340   /**
341    * Records the given attribute description for the given feature type
342    * 
343    * @param featureType
344    * @param attName
345    * @param description
346    */
347   public void addDescription(String featureType, String description,
348           String... attName)
349   {
350     if (featureType == null || attName == null)
351     {
352       return;
353     }
354   
355     Map<String[], AttributeData> atts = attributes.get(featureType);
356     if (atts == null)
357     {
358       atts = new TreeMap<>(comparator);
359       attributes.put(featureType, atts);
360     }
361     AttributeData attData = atts.get(attName);
362     if (attData == null)
363     {
364       attData = new AttributeData();
365       atts.put(attName, attData);
366     }
367     attData.addDescription(description);
368   }
369
370   /**
371    * Answers the datatype of the feature, which is one of Character, Number or
372    * Mixed (or null if not known), as discovered from values recorded.
373    * 
374    * @param featureType
375    * @param attName
376    * @return
377    */
378   public Datatype getDatatype(String featureType, String... attName)
379   {
380     Map<String[], AttributeData> atts = attributes.get(featureType);
381     if (atts != null)
382     {
383       AttributeData attData = atts.get(attName);
384       if (attData != null)
385       {
386         return attData.getType();
387       }
388     }
389     return null;
390   }
391
392   /**
393    * Resets all attribute metadata
394    */
395   public void clear()
396   {
397     attributes.clear();
398   }
399
400   /**
401    * Resets attribute metadata for one feature type
402    * 
403    * @param featureType
404    */
405   public void clear(String featureType)
406   {
407     Map<String[], AttributeData> map = attributes.get(featureType);
408     if (map != null)
409     {
410       map.clear();
411     }
412
413   }
414 }