Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
FeatureAttributes | 35 | 73 | 38 | ||
FeatureAttributes.Datatype | 37 | 0 | 0 | ||
FeatureAttributes.AttributeData | 84 | 23 | 19 |
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 | 1531098 | @Override |
60 | public int compare(String[] o1, String[] o2) | |
61 | { | |
62 | 1531098 | int i = 0; |
63 | 2064566 | while (i < o1.length || i < o2.length) |
64 | { | |
65 | 1531615 | if (o2.length <= i) |
66 | { | |
67 | 1 | return o1.length <= i ? 0 : 1; |
68 | } | |
69 | 1531614 | if (o1.length <= i) |
70 | { | |
71 | 1 | return -1; |
72 | } | |
73 | 1531613 | int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]); |
74 | 1531613 | if (comp != 0) |
75 | { | |
76 | 998145 | return comp; |
77 | } | |
78 | 533468 | i++; |
79 | } | |
80 | 532951 | 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 | 533090 | void addInstance(String desc, String value) |
117 | { | |
118 | 533090 | addDescription(desc); |
119 | ||
120 | 533090 | if (value != null) |
121 | { | |
122 | 533090 | value = value.trim(); |
123 | ||
124 | /* | |
125 | * Parse numeric value unless we have previously | |
126 | * seen text data for this attribute type | |
127 | */ | |
128 | 533090 | if (type == null || type == Datatype.Number) |
129 | { | |
130 | 842 | try |
131 | { | |
132 | 842 | float f = Float.valueOf(value); |
133 | 691 | min = hasValue ? Math.min(min, f) : f; |
134 | 691 | max = hasValue ? Math.max(max, f) : f; |
135 | 691 | hasValue = true; |
136 | 691 | 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 | 151 | type = (type == null || type == Datatype.Character) |
145 | ? Datatype.Character | |
146 | : Datatype.Mixed; | |
147 | 151 | min = 0f; |
148 | 151 | max = 0f; |
149 | 151 | 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 | 2 | public String getDescription() |
162 | { | |
163 | 2 | if (description != null && description.size() == 1) |
164 | { | |
165 | 1 | return description.get(0); |
166 | } | |
167 | 1 | return null; |
168 | } | |
169 | ||
170 | 3 | public Datatype getType() |
171 | { | |
172 | 3 | 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 | 533092 | public void addDescription(String desc) |
182 | { | |
183 | 533092 | if (desc != null) |
184 | { | |
185 | 212 | if (description == null) |
186 | { | |
187 | 39 | description = new ArrayList<>(); |
188 | } | |
189 | 212 | if (!description.contains(desc)) |
190 | { | |
191 | 40 | description.add(desc); |
192 | } | |
193 | } | |
194 | } | |
195 | } | |
196 | ||
197 | /** | |
198 | * Answers the singleton instance of this class | |
199 | * | |
200 | * @return | |
201 | */ | |
202 | 532989 | public static FeatureAttributes getInstance() |
203 | { | |
204 | 532989 | return instance; |
205 | } | |
206 | ||
207 | 6 | private FeatureAttributes() |
208 | { | |
209 | 6 | 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 | 0 | public List<String[]> getAttributes(String featureType) |
222 | { | |
223 | 0 | if (!attributes.containsKey(featureType)) |
224 | { | |
225 | 0 | return Collections.<String[]> emptyList(); |
226 | } | |
227 | ||
228 | 0 | 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 | 0 | public boolean hasAttributes(String featureType) |
239 | { | |
240 | 0 | if (attributes.containsKey(featureType)) |
241 | { | |
242 | 0 | if (!attributes.get(featureType).isEmpty()) |
243 | { | |
244 | 0 | return true; |
245 | } | |
246 | } | |
247 | 0 | 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 | 533151 | public void addAttribute(String featureType, String description, |
260 | Object value, String... attName) | |
261 | { | |
262 | 533151 | if (featureType == null || attName == null) |
263 | { | |
264 | 0 | return; |
265 | } | |
266 | ||
267 | /* | |
268 | * if attribute value is a map, drill down one more level to | |
269 | * record its sub-fields | |
270 | */ | |
271 | 533151 | if (value instanceof Map<?, ?>) |
272 | { | |
273 | 61 | for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) |
274 | { | |
275 | 175 | String[] attNames = new String[attName.length + 1]; |
276 | 175 | System.arraycopy(attName, 0, attNames, 0, attName.length); |
277 | 175 | attNames[attName.length] = entry.getKey().toString(); |
278 | 175 | addAttribute(featureType, description, entry.getValue(), attNames); |
279 | } | |
280 | 61 | return; |
281 | } | |
282 | ||
283 | 533090 | String valueAsString = value.toString(); |
284 | 533090 | Map<String[], AttributeData> atts = attributes.get(featureType); |
285 | 533090 | if (atts == null) |
286 | { | |
287 | 56 | atts = new TreeMap<>(comparator); |
288 | 56 | attributes.put(featureType, atts); |
289 | } | |
290 | 533090 | AttributeData attData = atts.get(attName); |
291 | 533090 | if (attData == null) |
292 | { | |
293 | 208 | attData = new AttributeData(); |
294 | 208 | atts.put(attName, attData); |
295 | } | |
296 | 533090 | 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 | 3 | public String getDescription(String featureType, String... attName) |
308 | { | |
309 | 3 | String desc = null; |
310 | 3 | Map<String[], AttributeData> atts = attributes.get(featureType); |
311 | 3 | if (atts != null) |
312 | { | |
313 | 2 | AttributeData attData = atts.get(attName); |
314 | 2 | if (attData != null) |
315 | { | |
316 | 2 | desc = attData.getDescription(); |
317 | } | |
318 | } | |
319 | 3 | 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 | 7 | public float[] getMinMax(String featureType, String... attName) |
332 | { | |
333 | 7 | Map<String[], AttributeData> atts = attributes.get(featureType); |
334 | 7 | if (atts != null) |
335 | { | |
336 | 6 | AttributeData attData = atts.get(attName); |
337 | 6 | if (attData != null && attData.hasValue) |
338 | { | |
339 | 4 | return new float[] { attData.min, attData.max }; |
340 | } | |
341 | } | |
342 | 3 | 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 | 2 | public void addDescription(String featureType, String description, |
353 | String... attName) | |
354 | { | |
355 | 2 | if (featureType == null || attName == null) |
356 | { | |
357 | 0 | return; |
358 | } | |
359 | ||
360 | 2 | Map<String[], AttributeData> atts = attributes.get(featureType); |
361 | 2 | if (atts == null) |
362 | { | |
363 | 1 | atts = new TreeMap<>(comparator); |
364 | 1 | attributes.put(featureType, atts); |
365 | } | |
366 | 2 | AttributeData attData = atts.get(attName); |
367 | 2 | if (attData == null) |
368 | { | |
369 | 1 | attData = new AttributeData(); |
370 | 1 | atts.put(attName, attData); |
371 | } | |
372 | 2 | 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 | 4 | public Datatype getDatatype(String featureType, String... attName) |
384 | { | |
385 | 4 | Map<String[], AttributeData> atts = attributes.get(featureType); |
386 | 4 | if (atts != null) |
387 | { | |
388 | 3 | AttributeData attData = atts.get(attName); |
389 | 3 | if (attData != null) |
390 | { | |
391 | 3 | return attData.getType(); |
392 | } | |
393 | } | |
394 | 1 | return null; |
395 | } | |
396 | ||
397 | /** | |
398 | * Resets all attribute metadata | |
399 | */ | |
400 | 4 | public void clear() |
401 | { | |
402 | 4 | attributes.clear(); |
403 | } | |
404 | ||
405 | /** | |
406 | * Resets attribute metadata for one feature type | |
407 | * | |
408 | * @param featureType | |
409 | */ | |
410 | 0 | public void clear(String featureType) |
411 | { | |
412 | 0 | Map<String[], AttributeData> map = attributes.get(featureType); |
413 | 0 | if (map != null) |
414 | { | |
415 | 0 | map.clear(); |
416 | } | |
417 | ||
418 | } | |
419 | } |