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