JAL-3446 from JAL-3253 ApplicationSingletonProvider Desktop
[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   /*
52    * map, by feature type, of a map, by attribute name, of
53    * attribute description and min-max range (if known)
54    */
55   private Map<String, Map<String[], AttributeData>> attributes;
56
57   /*
58    * a case-insensitive comparator so that attributes are ordered e.g.
59    * AC
60    * af
61    * CSQ:AFR_MAF
62    * CSQ:Allele
63    */
64   private Comparator<String[]> comparator = new Comparator<String[]>()
65   {
66     @Override
67     public int compare(String[] o1, String[] o2)
68     {
69       int i = 0;
70       while (i < o1.length || i < o2.length)
71       {
72         if (o2.length <= i)
73         {
74           return o1.length <= i ? 0 : 1;
75         }
76         if (o1.length <= i)
77         {
78           return -1;
79         }
80         int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
81         if (comp != 0)
82         {
83           return comp;
84         }
85         i++;
86       }
87       return 0; // same length and all matched
88     }
89   };
90
91   private class AttributeData
92   {
93     /*
94      * description(s) for this attribute, if known
95      * (different feature source might have differing descriptions)
96      */
97     List<String> description;
98
99     /*
100      * minimum value (of any numeric values recorded)
101      */
102     float min = 0f;
103
104     /*
105      * maximum value (of any numeric values recorded)
106      */
107     float max = 0f;
108
109     /*
110      * flag is set true if any numeric value is detected for this attribute
111      */
112     boolean hasValue = false;
113
114     Datatype type;
115
116     /**
117      * Note one instance of this attribute, recording unique, non-null
118      * descriptions, and the min/max of any numerical values
119      * 
120      * @param desc
121      * @param value
122      */
123     void addInstance(String desc, String value)
124     {
125       addDescription(desc);
126
127       if (value != null)
128       {
129         value = value.trim();
130
131         /*
132          * Parse numeric value unless we have previously
133          * seen text data for this attribute type
134          */
135         if (type == null || type == Datatype.Number)
136         {
137           try
138           {
139             float f = Float.valueOf(value);
140             min = hasValue ? Math.min(min, f) : f;
141             max = hasValue ? Math.max(max, f) : f;
142             hasValue = true;
143             type = (type == null || type == Datatype.Number)
144                     ? Datatype.Number
145                     : Datatype.Mixed;
146           } catch (NumberFormatException e)
147           {
148             /*
149              * non-numeric data: treat attribute as Character (or Mixed)
150              */
151             type = (type == null || type == Datatype.Character)
152                     ? Datatype.Character
153                     : Datatype.Mixed;
154             min = 0f;
155             max = 0f;
156             hasValue = false;
157           }
158         }
159       }
160     }
161
162     /**
163      * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
164      * @return
165      */
166     public String getDescription()
167     {
168       if (description != null && description.size() == 1)
169       {
170         return description.get(0);
171       }
172       return null;
173     }
174
175     public Datatype getType()
176     {
177       return type;
178     }
179
180     /**
181      * Adds the given description to the list of known descriptions (without
182      * duplication)
183      * 
184      * @param desc
185      */
186     public void addDescription(String desc)
187     {
188       if (desc != null)
189       {
190         if (description == null)
191         {
192           description = new ArrayList<>();
193         }
194         if (!description.contains(desc))
195         {
196           description.add(desc);
197         }
198       }
199     }
200   }
201
202   private FeatureAttributes()
203   {
204     attributes = new HashMap<>();
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 }