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