JAL-2792 sort feature attributes for output, not for storage
[jalview.git] / src / jalview / datamodel / SequenceFeature.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;
22
23 import jalview.datamodel.features.FeatureAttributeType;
24 import jalview.datamodel.features.FeatureAttributes;
25 import jalview.datamodel.features.FeatureLocationI;
26 import jalview.datamodel.features.FeatureSourceI;
27 import jalview.datamodel.features.FeatureSources;
28 import jalview.util.StringUtils;
29
30 import java.util.HashMap;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.SortedMap;
34 import java.util.TreeMap;
35 import java.util.Vector;
36
37 /**
38  * A class that models a single contiguous feature on a sequence. If flag
39  * 'contactFeature' is true, the start and end positions are interpreted instead
40  * as two contact points.
41  */
42 public class SequenceFeature implements FeatureLocationI
43 {
44   /*
45    * score value if none is set; preferably Float.Nan, but see
46    * JAL-2060 and JAL-2554 for a couple of blockers to that
47    */
48   private static final float NO_SCORE = 0f;
49
50   private static final String STATUS = "status";
51
52   private static final String STRAND = "STRAND";
53
54   // private key for Phase designed not to conflict with real GFF data
55   private static final String PHASE = "!Phase";
56
57   // private key for ENA location designed not to conflict with real GFF data
58   private static final String LOCATION = "!Location";
59
60   private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
61
62   /*
63    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
64    * name1=value1;name2=value2,value3;...etc
65    */
66   private static final String ATTRIBUTES = "ATTRIBUTES";
67
68   /*
69    * type, begin, end, featureGroup, score and contactFeature are final 
70    * to ensure that the integrity of SequenceFeatures data store 
71    * can't be broken by direct update of these fields
72    */
73   public final String type;
74
75   public final int begin;
76
77   public final int end;
78
79   public final String featureGroup;
80
81   public final float score;
82
83   private final boolean contactFeature;
84
85   public String description;
86
87   /*
88    * a map of key-value pairs; may be populated from GFF 'column 9' data,
89    * other data sources (e.g. GenBank file), or programmatically
90    */
91   public Map<String, Object> otherDetails;
92
93   public Vector<String> links;
94
95   /*
96    * the identifier (if known) for the FeatureSource held in FeatureSources,
97    * as a provider of metadata about feature attributes 
98    */
99   private String source;
100
101   /**
102    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
103    * otherDetails map, so the new and original SequenceFeature may reference the
104    * same objects in the map.
105    * 
106    * @param cpy
107    */
108   public SequenceFeature(SequenceFeature cpy)
109   {
110     this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
111             .getScore());
112   }
113
114   /**
115    * Constructor
116    * 
117    * @param theType
118    * @param theDesc
119    * @param theBegin
120    * @param theEnd
121    * @param group
122    */
123   public SequenceFeature(String theType, String theDesc, int theBegin,
124           int theEnd, String group)
125   {
126     this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
127   }
128
129   /**
130    * Constructor including a score value
131    * 
132    * @param theType
133    * @param theDesc
134    * @param theBegin
135    * @param theEnd
136    * @param theScore
137    * @param group
138    */
139   public SequenceFeature(String theType, String theDesc, int theBegin,
140           int theEnd, float theScore, String group)
141   {
142     this.type = theType;
143     this.description = theDesc;
144     this.begin = theBegin;
145     this.end = theEnd;
146     this.featureGroup = group;
147     this.score = theScore;
148
149     /*
150      * for now, only "Disulfide/disulphide bond" is treated as a contact feature
151      */
152     this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
153             || "disulphide bond".equalsIgnoreCase(type);
154   }
155
156   /**
157    * A copy constructor that allows the value of final fields to be 'modified'
158    * 
159    * @param sf
160    * @param newType
161    * @param newBegin
162    * @param newEnd
163    * @param newGroup
164    * @param newScore
165    */
166   public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
167           int newEnd, String newGroup, float newScore)
168   {
169     this(newType, sf.getDescription(), newBegin, newEnd, newScore,
170             newGroup);
171
172     this.source = sf.source;
173
174     if (sf.otherDetails != null)
175     {
176       otherDetails = new HashMap<>();
177       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
178       {
179         otherDetails.put(entry.getKey(), entry.getValue());
180       }
181     }
182     if (sf.links != null && sf.links.size() > 0)
183     {
184       links = new Vector<>();
185       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
186       {
187         links.addElement(sf.links.elementAt(i));
188       }
189     }
190   }
191
192   /**
193    * A copy constructor that allows the value of final fields to be 'modified'
194    * 
195    * @param sf
196    * @param newBegin
197    * @param newEnd
198    * @param newGroup
199    * @param newScore
200    */
201   public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
202           String newGroup, float newScore)
203   {
204     this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
205   }
206
207   /**
208    * Two features are considered equal if they have the same type, group,
209    * description, start, end, phase, strand, and (if present) 'Name', ID' and
210    * 'Parent' attributes.
211    * 
212    * Note we need to check Parent to distinguish the same exon occurring in
213    * different transcripts (in Ensembl GFF). This allows assembly of transcript
214    * sequences from their component exon regions.
215    */
216   @Override
217   public boolean equals(Object o)
218   {
219     return equals(o, false);
220   }
221
222   /**
223    * Overloaded method allows the equality test to optionally ignore the
224    * 'Parent' attribute of a feature. This supports avoiding adding many
225    * superficially duplicate 'exon' or CDS features to genomic or protein
226    * sequence.
227    * 
228    * @param o
229    * @param ignoreParent
230    * @return
231    */
232   public boolean equals(Object o, boolean ignoreParent)
233   {
234     if (o == null || !(o instanceof SequenceFeature))
235     {
236       return false;
237     }
238
239     SequenceFeature sf = (SequenceFeature) o;
240     boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score)
241             : score == sf.score;
242     if (begin != sf.begin || end != sf.end || !sameScore)
243     {
244       return false;
245     }
246
247     if (getStrand() != sf.getStrand())
248     {
249       return false;
250     }
251
252     if (!(type + description + featureGroup + getPhase()).equals(
253             sf.type + sf.description + sf.featureGroup + sf.getPhase()))
254     {
255       return false;
256     }
257     if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
258     {
259       return false;
260     }
261     if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
262     {
263       return false;
264     }
265     if (!ignoreParent)
266     {
267       if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
268       {
269         return false;
270       }
271     }
272     return true;
273   }
274
275   /**
276    * Returns true if both values are null, are both non-null and equal
277    * 
278    * @param att1
279    * @param att2
280    * @return
281    */
282   protected static boolean equalAttribute(Object att1, Object att2)
283   {
284     if (att1 == null && att2 == null)
285     {
286       return true;
287     }
288     if (att1 != null)
289     {
290       return att1.equals(att2);
291     }
292     return att2.equals(att1);
293   }
294
295   /**
296    * DOCUMENT ME!
297    * 
298    * @return DOCUMENT ME!
299    */
300   @Override
301   public int getBegin()
302   {
303     return begin;
304   }
305
306   /**
307    * DOCUMENT ME!
308    * 
309    * @return DOCUMENT ME!
310    */
311   @Override
312   public int getEnd()
313   {
314     return end;
315   }
316
317   /**
318    * DOCUMENT ME!
319    * 
320    * @return DOCUMENT ME!
321    */
322   public String getType()
323   {
324     return type;
325   }
326
327   /**
328    * DOCUMENT ME!
329    * 
330    * @return DOCUMENT ME!
331    */
332   public String getDescription()
333   {
334     return description;
335   }
336
337   public void setDescription(String desc)
338   {
339     description = desc;
340   }
341
342   public String getFeatureGroup()
343   {
344     return featureGroup;
345   }
346
347   public void addLink(String labelLink)
348   {
349     if (links == null)
350     {
351       links = new Vector<>();
352     }
353
354     if (!links.contains(labelLink))
355     {
356       links.insertElementAt(labelLink, 0);
357     }
358   }
359
360   public float getScore()
361   {
362     return score;
363   }
364
365   /**
366    * Used for getting values which are not in the basic set. eg STRAND, PHASE
367    * for GFF file
368    * 
369    * @param key
370    *          String
371    */
372   public Object getValue(String key)
373   {
374     if (otherDetails == null)
375     {
376       return null;
377     }
378     else
379     {
380       return otherDetails.get(key);
381     }
382   }
383
384   /**
385    * Answers the value of the specified attribute as string, or null if no such
386    * value. If more than one attribute name is provided, tries to resolve as keys
387    * to nested maps. For example, if attribute "CSQ" holds a map of key-value
388    * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
389    * in that map.
390    * 
391    * @param key
392    * @return
393    */
394   public String getValueAsString(String... key)
395   {
396     if (otherDetails == null)
397     {
398       return null;
399     }
400     Object value = otherDetails.get(key[0]);
401     if (key.length > 1 && value instanceof Map<?, ?>)
402     {
403       value = ((Map) value).get(key[1]);
404     }
405     return value == null ? null : value.toString();
406   }
407
408   /**
409    * Returns a property value for the given key if known, else the specified
410    * default value
411    * 
412    * @param key
413    * @param defaultValue
414    * @return
415    */
416   public Object getValue(String key, Object defaultValue)
417   {
418     Object value = getValue(key);
419     return value == null ? defaultValue : value;
420   }
421
422   /**
423    * Used for setting values which are not in the basic set. eg STRAND, FRAME
424    * for GFF file
425    * 
426    * @param key
427    *          eg STRAND
428    * @param value
429    *          eg +
430    */
431   public void setValue(String key, Object value)
432   {
433     if (value != null)
434     {
435       if (otherDetails == null)
436       {
437         otherDetails = new HashMap<>();
438       }
439
440       otherDetails.put(key, value);
441       recordAttribute(key, value);
442     }
443   }
444
445   /**
446    * Notifies the addition of a feature attribute. This lets us keep track of
447    * which attributes are present on each feature type, and also the range of
448    * numerical-valued attributes.
449    * 
450    * @param key
451    * @param value
452    */
453   protected void recordAttribute(String key, Object value)
454   {
455     String attDesc = null;
456     if (source != null)
457     {
458       attDesc = FeatureSources.getInstance().getSource(source)
459               .getAttributeName(key);
460     }
461
462     FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
463             key);
464   }
465
466   /*
467    * The following methods are added to maintain the castor Uniprot mapping file
468    * for the moment.
469    */
470   public void setStatus(String status)
471   {
472     setValue(STATUS, status);
473   }
474
475   public String getStatus()
476   {
477     return (String) getValue(STATUS);
478   }
479
480   public void setAttributes(String attr)
481   {
482     setValue(ATTRIBUTES, attr);
483   }
484
485   public String getAttributes()
486   {
487     return (String) getValue(ATTRIBUTES);
488   }
489
490   /**
491    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
492    * GFF), and 0 for unknown or not (validly) specified
493    * 
494    * @return
495    */
496   public int getStrand()
497   {
498     int strand = 0;
499     if (otherDetails != null)
500     {
501       Object str = otherDetails.get(STRAND);
502       if ("-".equals(str))
503       {
504         strand = -1;
505       }
506       else if ("+".equals(str))
507       {
508         strand = 1;
509       }
510     }
511     return strand;
512   }
513
514   /**
515    * Set the value of strand
516    * 
517    * @param strand
518    *          should be "+" for forward, or "-" for reverse
519    */
520   public void setStrand(String strand)
521   {
522     setValue(STRAND, strand);
523   }
524
525   public void setPhase(String phase)
526   {
527     setValue(PHASE, phase);
528   }
529
530   public String getPhase()
531   {
532     return (String) getValue(PHASE);
533   }
534
535   /**
536    * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
537    * 
538    * @param loc
539    */
540   public void setEnaLocation(String loc)
541   {
542     setValue(LOCATION, loc);
543   }
544
545   /**
546    * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
547    * 
548    * @param loc
549    */
550   public String getEnaLocation()
551   {
552     return (String) getValue(LOCATION);
553   }
554
555   /**
556    * Readable representation, for debug only, not guaranteed not to change
557    * between versions
558    */
559   @Override
560   public String toString()
561   {
562     return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
563             getDescription());
564   }
565
566   /**
567    * Overridden to ensure that whenever two objects are equal, they have the
568    * same hashCode
569    */
570   @Override
571   public int hashCode()
572   {
573     String s = getType() + getDescription() + getFeatureGroup()
574             + getValue("ID") + getValue("Name") + getValue("Parent")
575             + getPhase();
576     return s.hashCode() + getBegin() + getEnd() + (int) getScore()
577             + getStrand();
578   }
579
580   /**
581    * Answers true if the feature's start/end values represent two related
582    * positions, rather than ends of a range. Such features may be visualised or
583    * reported differently to features on a range.
584    */
585   @Override
586   public boolean isContactFeature()
587   {
588     return contactFeature;
589   }
590
591   /**
592    * Answers true if the sequence has zero start and end position
593    * 
594    * @return
595    */
596   public boolean isNonPositional()
597   {
598     return begin == 0 && end == 0;
599   }
600
601   /**
602    * Answers an html-formatted report of feature details
603    * 
604    * @return
605    */
606   public String getDetailsReport()
607   {
608     FeatureSourceI metadata = FeatureSources.getInstance()
609             .getSource(source);
610
611     StringBuilder sb = new StringBuilder(128);
612     sb.append("<br>");
613     sb.append("<table>");
614     sb.append(String.format(ROW_DATA, "Type", type, ""));
615     sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
616             : begin + (isContactFeature() ? ":" : "-") + end, ""));
617     String desc = StringUtils.stripHtmlTags(description);
618     sb.append(String.format(ROW_DATA, "Description", desc, ""));
619     if (!Float.isNaN(score) && score != 0f)
620     {
621       sb.append(String.format(ROW_DATA, "Score", score, ""));
622     }
623     if (featureGroup != null)
624     {
625       sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
626     }
627
628     if (otherDetails != null)
629     {
630       TreeMap<String, Object> ordered = new TreeMap<>(
631               String.CASE_INSENSITIVE_ORDER);
632       ordered.putAll(otherDetails);
633
634       for (Entry<String, Object> entry : ordered.entrySet())
635       {
636         String key = entry.getKey();
637         if (ATTRIBUTES.equals(key))
638         {
639           continue; // to avoid double reporting
640         }
641
642         Object value = entry.getValue();
643         if (value instanceof Map<?, ?>)
644         {
645           /*
646            * expand values in a Map attribute across separate lines
647            * copy to a TreeMap for alphabetical ordering
648            */
649           Map<String, Object> values = (Map<String, Object>) value;
650           SortedMap<String, Object> sm = new TreeMap<>(
651                   String.CASE_INSENSITIVE_ORDER);
652           sm.putAll(values);
653           for (Entry<?, ?> e : sm.entrySet())
654           {
655             sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
656                     .getValue().toString()));
657           }
658         }
659         else
660         {
661           // tried <td title="key"> but it failed to provide a tooltip :-(
662           String attDesc = null;
663           if (metadata != null)
664           {
665             attDesc = metadata.getAttributeName(key);
666           }
667           String s = entry.getValue().toString();
668           if (isValueInteresting(key, s, metadata))
669           {
670             sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
671                     : attDesc, s));
672           }
673         }
674       }
675     }
676     sb.append("</table>");
677
678     String text = sb.toString();
679     return text;
680   }
681
682   /**
683    * Answers true if we judge the value is worth displaying, by some heuristic
684    * rules, else false
685    * 
686    * @param key
687    * @param value
688    * @param metadata
689    * @return
690    */
691   boolean isValueInteresting(String key, String value,
692           FeatureSourceI metadata)
693   {
694     /*
695      * currently suppressing zero values as well as null or empty
696      */
697     if (value == null || "".equals(value) || ".".equals(value)
698             || "0".equals(value))
699     {
700       return false;
701     }
702
703     if (metadata == null)
704     {
705       return true;
706     }
707
708     FeatureAttributeType attType = metadata.getAttributeType(key);
709     if (attType != null
710             && (attType == FeatureAttributeType.Float || attType
711                     .equals(FeatureAttributeType.Integer)))
712     {
713       try
714       {
715         float fval = Float.valueOf(value);
716         if (fval == 0f)
717         {
718           return false;
719         }
720       } catch (NumberFormatException e)
721       {
722         // ignore
723       }
724     }
725
726     return true; // default to interesting
727   }
728
729   /**
730    * Sets the feature source identifier
731    * 
732    * @param theSource
733    */
734   public void setSource(String theSource)
735   {
736     source = theSource;
737   }
738 }