Merge branch 'documentation/JAL-3407_2.11.1_release' into releases/Release_2_11_1_Branch
[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 ? Float.min(min, f) : f;
134             max = hasValue ? Float.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 if either no, or more than description is recorded
157      * @return
158      */
159     public String getDescription()
160     {
161       if (description != null && description.size() == 1)
162       {
163         return description.get(0);
164       }
165       return null;
166     }
167
168     public Datatype getType()
169     {
170       return type;
171     }
172
173     /**
174      * Adds the given description to the list of known descriptions (without
175      * duplication)
176      * 
177      * @param desc
178      */
179     public void addDescription(String desc)
180     {
181       if (desc != null)
182       {
183         if (description == null)
184         {
185           description = new ArrayList<>();
186         }
187         if (!description.contains(desc))
188         {
189           description.add(desc);
190         }
191       }
192     }
193   }
194
195   /**
196    * Answers the singleton instance of this class
197    * 
198    * @return
199    */
200   public static FeatureAttributes getInstance()
201   {
202     return instance;
203   }
204
205   private FeatureAttributes()
206   {
207     attributes = new HashMap<>();
208   }
209
210   /**
211    * Answers the attribute names known for the given feature type, in
212    * alphabetical order (not case sensitive), or an empty set if no attributes
213    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
214    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
215    * 
216    * @param featureType
217    * @return
218    */
219   public List<String[]> getAttributes(String featureType)
220   {
221     if (!attributes.containsKey(featureType))
222     {
223       return Collections.<String[]> emptyList();
224     }
225
226     return new ArrayList<>(attributes.get(featureType).keySet());
227   }
228
229   /**
230    * Answers true if at least one attribute is known for the given feature type,
231    * else false
232    * 
233    * @param featureType
234    * @return
235    */
236   public boolean hasAttributes(String featureType)
237   {
238     if (attributes.containsKey(featureType))
239     {
240       if (!attributes.get(featureType).isEmpty())
241       {
242         return true;
243       }
244     }
245     return false;
246   }
247
248   /**
249    * Records the given attribute name and description for the given feature
250    * type, and updates the min-max for any numeric value
251    * 
252    * @param featureType
253    * @param description
254    * @param value
255    * @param attName
256    */
257   public void addAttribute(String featureType, String description,
258           Object value, String... attName)
259   {
260     if (featureType == null || attName == null)
261     {
262       return;
263     }
264
265     /*
266      * if attribute value is a map, drill down one more level to
267      * record its sub-fields
268      */
269     if (value instanceof Map<?, ?>)
270     {
271       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
272       {
273         String[] attNames = new String[attName.length + 1];
274         System.arraycopy(attName, 0, attNames, 0, attName.length);
275         attNames[attName.length] = entry.getKey().toString();
276         addAttribute(featureType, description, entry.getValue(), attNames);
277       }
278       return;
279     }
280
281     String valueAsString = value.toString();
282     Map<String[], AttributeData> atts = attributes.get(featureType);
283     if (atts == null)
284     {
285       atts = new TreeMap<>(comparator);
286       attributes.put(featureType, atts);
287     }
288     AttributeData attData = atts.get(attName);
289     if (attData == null)
290     {
291       attData = new AttributeData();
292       atts.put(attName, attData);
293     }
294     attData.addInstance(description, valueAsString);
295   }
296
297   /**
298    * Answers the description of the given attribute for the given feature type,
299    * if known and unique, else null
300    * 
301    * @param featureType
302    * @param attName
303    * @return
304    */
305   public String getDescription(String featureType, String... attName)
306   {
307     String desc = null;
308     Map<String[], AttributeData> atts = attributes.get(featureType);
309     if (atts != null)
310     {
311       AttributeData attData = atts.get(attName);
312       if (attData != null)
313       {
314         desc = attData.getDescription();
315       }
316     }
317     return desc;
318   }
319
320   /**
321    * Answers the [min, max] value range of the given attribute for the given
322    * feature type, if known, else null. Attributes with a mixture of text and
323    * numeric values are considered text (do not return a min-max range).
324    * 
325    * @param featureType
326    * @param attName
327    * @return
328    */
329   public float[] getMinMax(String featureType, String... attName)
330   {
331     Map<String[], AttributeData> atts = attributes.get(featureType);
332     if (atts != null)
333     {
334       AttributeData attData = atts.get(attName);
335       if (attData != null && attData.hasValue)
336       {
337         return new float[] { attData.min, attData.max };
338       }
339     }
340     return null;
341   }
342
343   /**
344    * Records the given attribute description for the given feature type
345    * 
346    * @param featureType
347    * @param attName
348    * @param description
349    */
350   public void addDescription(String featureType, String description,
351           String... attName)
352   {
353     if (featureType == null || attName == null)
354     {
355       return;
356     }
357   
358     Map<String[], AttributeData> atts = attributes.get(featureType);
359     if (atts == null)
360     {
361       atts = new TreeMap<>(comparator);
362       attributes.put(featureType, atts);
363     }
364     AttributeData attData = atts.get(attName);
365     if (attData == null)
366     {
367       attData = new AttributeData();
368       atts.put(attName, attData);
369     }
370     attData.addDescription(description);
371   }
372
373   /**
374    * Answers the datatype of the feature, which is one of Character, Number or
375    * Mixed (or null if not known), as discovered from values recorded.
376    * 
377    * @param featureType
378    * @param attName
379    * @return
380    */
381   public Datatype getDatatype(String featureType, String... attName)
382   {
383     Map<String[], AttributeData> atts = attributes.get(featureType);
384     if (atts != null)
385     {
386       AttributeData attData = atts.get(attName);
387       if (attData != null)
388       {
389         return attData.getType();
390       }
391     }
392     return null;
393   }
394
395   /**
396    * Resets all attribute metadata
397    */
398   public void clear()
399   {
400     attributes.clear();
401   }
402
403   /**
404    * Resets attribute metadata for one feature type
405    * 
406    * @param featureType
407    */
408   public void clear(String featureType)
409   {
410     Map<String[], AttributeData> map = attributes.get(featureType);
411     if (map != null)
412     {
413       map.clear();
414     }
415
416   }
417 }