JAL-2738 copy to spikes/mungo
[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.FeatureLocationI;
24
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.Map.Entry;
28 import java.util.TreeMap;
29 import java.util.Vector;
30
31 /**
32  * DOCUMENT ME!
33  * 
34  * @author $author$
35  * @version $Revision$
36  */
37 public class SequenceFeature implements FeatureLocationI
38 {
39   /*
40    * score value if none is set; preferably Float.Nan, but see
41    * JAL-2060 and JAL-2554 for a couple of blockers to that
42    */
43   private static final float NO_SCORE = 0f;
44
45   private static final String STATUS = "status";
46
47   private static final String STRAND = "STRAND";
48
49   // private key for Phase designed not to conflict with real GFF data
50   private static final String PHASE = "!Phase";
51
52   // private key for ENA location designed not to conflict with real GFF data
53   private static final String LOCATION = "!Location";
54
55   /*
56    * map of otherDetails special keys, and their value fields' delimiter
57    */
58   private static final Map<String, String> INFO_KEYS = new HashMap<>();
59
60   static
61   {
62     INFO_KEYS.put("CSQ", ",");
63   }
64
65   /*
66    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
67    * name1=value1;name2=value2,value3;...etc
68    */
69   private static final String ATTRIBUTES = "ATTRIBUTES";
70
71   /*
72    * type, begin, end, featureGroup, score and contactFeature are final 
73    * to ensure that the integrity of SequenceFeatures data store 
74    * can't be broken by direct update of these fields
75    */
76   public final String type;
77
78   public final int begin;
79
80   public final int end;
81
82   public final String featureGroup;
83
84   public final float score;
85
86   private final boolean contactFeature;
87
88   public String description;
89
90   /*
91    * a map of key-value pairs; may be populated from GFF 'column 9' data,
92    * other data sources (e.g. GenBank file), or programmatically
93    */
94   public Map<String, Object> otherDetails;
95
96   public Vector<String> links;
97
98   /**
99    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
100    * otherDetails map, so the new and original SequenceFeature may reference the
101    * same objects in the map.
102    * 
103    * @param cpy
104    */
105   public SequenceFeature(SequenceFeature cpy)
106   {
107     this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
108             .getScore());
109   }
110
111   /**
112    * Constructor
113    * 
114    * @param theType
115    * @param theDesc
116    * @param theBegin
117    * @param theEnd
118    * @param group
119    */
120   public SequenceFeature(String theType, String theDesc, int theBegin,
121           int theEnd, String group)
122   {
123     this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
124   }
125
126   /**
127    * Constructor including a score value
128    * 
129    * @param theType
130    * @param theDesc
131    * @param theBegin
132    * @param theEnd
133    * @param theScore
134    * @param group
135    */
136   public SequenceFeature(String theType, String theDesc, int theBegin,
137           int theEnd, float theScore, String group)
138   {
139     this.type = theType;
140     this.description = theDesc;
141     this.begin = theBegin;
142     this.end = theEnd;
143     this.featureGroup = group;
144     this.score = theScore;
145
146     /*
147      * for now, only "Disulfide/disulphide bond" is treated as a contact feature
148      */
149     this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
150             || "disulphide bond".equalsIgnoreCase(type);
151   }
152
153   /**
154    * A copy constructor that allows the value of final fields to be 'modified'
155    * 
156    * @param sf
157    * @param newType
158    * @param newBegin
159    * @param newEnd
160    * @param newGroup
161    * @param newScore
162    */
163   public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
164           int newEnd, String newGroup, float newScore)
165   {
166     this(newType, sf.getDescription(), newBegin, newEnd, newScore,
167             newGroup);
168
169     if (sf.otherDetails != null)
170     {
171       otherDetails = new HashMap<String, Object>();
172       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
173       {
174         otherDetails.put(entry.getKey(), entry.getValue());
175       }
176     }
177     if (sf.links != null && sf.links.size() > 0)
178     {
179       links = new Vector<String>();
180       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
181       {
182         links.addElement(sf.links.elementAt(i));
183       }
184     }
185   }
186
187   /**
188    * A copy constructor that allows the value of final fields to be 'modified'
189    * 
190    * @param sf
191    * @param newBegin
192    * @param newEnd
193    * @param newGroup
194    * @param newScore
195    */
196   public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
197           String newGroup, float newScore)
198   {
199     this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
200   }
201
202   /**
203    * Two features are considered equal if they have the same type, group,
204    * description, start, end, phase, strand, and (if present) 'Name', ID' and
205    * 'Parent' attributes.
206    * 
207    * Note we need to check Parent to distinguish the same exon occurring in
208    * different transcripts (in Ensembl GFF). This allows assembly of transcript
209    * sequences from their component exon regions.
210    */
211   @Override
212   public boolean equals(Object o)
213   {
214     return equals(o, false);
215   }
216
217   /**
218    * Overloaded method allows the equality test to optionally ignore the
219    * 'Parent' attribute of a feature. This supports avoiding adding many
220    * superficially duplicate 'exon' or CDS features to genomic or protein
221    * sequence.
222    * 
223    * @param o
224    * @param ignoreParent
225    * @return
226    */
227   public boolean equals(Object o, boolean ignoreParent)
228   {
229     if (o == null || !(o instanceof SequenceFeature))
230     {
231       return false;
232     }
233
234     SequenceFeature sf = (SequenceFeature) o;
235     boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score)
236             : score == sf.score;
237     if (begin != sf.begin || end != sf.end || !sameScore)
238     {
239       return false;
240     }
241
242     if (getStrand() != sf.getStrand())
243     {
244       return false;
245     }
246
247     if (!(type + description + featureGroup + getPhase()).equals(
248             sf.type + sf.description + sf.featureGroup + sf.getPhase()))
249     {
250       return false;
251     }
252     if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
253     {
254       return false;
255     }
256     if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
257     {
258       return false;
259     }
260     if (!ignoreParent)
261     {
262       if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
263       {
264         return false;
265       }
266     }
267     return true;
268   }
269
270   /**
271    * Returns true if both values are null, are both non-null and equal
272    * 
273    * @param att1
274    * @param att2
275    * @return
276    */
277   protected static boolean equalAttribute(Object att1, Object att2)
278   {
279     if (att1 == null && att2 == null)
280     {
281       return true;
282     }
283     if (att1 != null)
284     {
285       return att1.equals(att2);
286     }
287     return att2.equals(att1);
288   }
289
290   /**
291    * DOCUMENT ME!
292    * 
293    * @return DOCUMENT ME!
294    */
295   @Override
296   public int getBegin()
297   {
298     return begin;
299   }
300
301   /**
302    * DOCUMENT ME!
303    * 
304    * @return DOCUMENT ME!
305    */
306   @Override
307   public int getEnd()
308   {
309     return end;
310   }
311
312   /**
313    * DOCUMENT ME!
314    * 
315    * @return DOCUMENT ME!
316    */
317   public String getType()
318   {
319     return type;
320   }
321
322   /**
323    * DOCUMENT ME!
324    * 
325    * @return DOCUMENT ME!
326    */
327   public String getDescription()
328   {
329     return description;
330   }
331
332   public void setDescription(String desc)
333   {
334     description = desc;
335   }
336
337   public String getFeatureGroup()
338   {
339     return featureGroup;
340   }
341
342   public void addLink(String labelLink)
343   {
344     if (links == null)
345     {
346       links = new Vector<String>();
347     }
348
349     if (!links.contains(labelLink))
350     {
351       links.insertElementAt(labelLink, 0);
352     }
353   }
354
355   public float getScore()
356   {
357     return score;
358   }
359
360   /**
361    * Used for getting values which are not in the basic set. eg STRAND, PHASE
362    * for GFF file
363    * 
364    * @param key
365    *          String
366    */
367   public Object getValue(String key)
368   {
369     if (otherDetails == null)
370     {
371       return null;
372     }
373     else
374     {
375       return otherDetails.get(key);
376     }
377   }
378
379   /**
380    * Returns a property value for the given key if known, else the specified
381    * default value
382    * 
383    * @param key
384    * @param defaultValue
385    * @return
386    */
387   public Object getValue(String key, Object defaultValue)
388   {
389     Object value = getValue(key);
390     return value == null ? defaultValue : value;
391   }
392
393   /**
394    * Used for setting values which are not in the basic set. eg STRAND, FRAME
395    * for GFF file
396    * 
397    * @param key
398    *          eg STRAND
399    * @param value
400    *          eg +
401    */
402   public void setValue(String key, Object value)
403   {
404     if (value != null)
405     {
406       if (otherDetails == null)
407       {
408         otherDetails = new HashMap<String, Object>();
409       }
410
411       otherDetails.put(key, value);
412     }
413   }
414
415   /*
416    * The following methods are added to maintain the castor Uniprot mapping file
417    * for the moment.
418    */
419   public void setStatus(String status)
420   {
421     setValue(STATUS, status);
422   }
423
424   public String getStatus()
425   {
426     return (String) getValue(STATUS);
427   }
428
429   public void setAttributes(String attr)
430   {
431     setValue(ATTRIBUTES, attr);
432   }
433
434   public String getAttributes()
435   {
436     return (String) getValue(ATTRIBUTES);
437   }
438
439   /**
440    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
441    * GFF), and 0 for unknown or not (validly) specified
442    * 
443    * @return
444    */
445   public int getStrand()
446   {
447     int strand = 0;
448     if (otherDetails != null)
449     {
450       Object str = otherDetails.get(STRAND);
451       if ("-".equals(str))
452       {
453         strand = -1;
454       }
455       else if ("+".equals(str))
456       {
457         strand = 1;
458       }
459     }
460     return strand;
461   }
462
463   /**
464    * Set the value of strand
465    * 
466    * @param strand
467    *          should be "+" for forward, or "-" for reverse
468    */
469   public void setStrand(String strand)
470   {
471     setValue(STRAND, strand);
472   }
473
474   public void setPhase(String phase)
475   {
476     setValue(PHASE, phase);
477   }
478
479   public String getPhase()
480   {
481     return (String) getValue(PHASE);
482   }
483
484   /**
485    * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
486    * 
487    * @param loc
488    */
489   public void setEnaLocation(String loc)
490   {
491     setValue(LOCATION, loc);
492   }
493
494   /**
495    * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
496    * 
497    * @param loc
498    */
499   public String getEnaLocation()
500   {
501     return (String) getValue(LOCATION);
502   }
503
504   /**
505    * Readable representation, for debug only, not guaranteed not to change
506    * between versions
507    */
508   @Override
509   public String toString()
510   {
511     return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
512             getDescription());
513   }
514
515   /**
516    * Overridden to ensure that whenever two objects are equal, they have the
517    * same hashCode
518    */
519   @Override
520   public int hashCode()
521   {
522     String s = getType() + getDescription() + getFeatureGroup()
523             + getValue("ID") + getValue("Name") + getValue("Parent")
524             + getPhase();
525     return s.hashCode() + getBegin() + getEnd() + (int) getScore()
526             + getStrand();
527   }
528
529   /**
530    * Answers true if the feature's start/end values represent two related
531    * positions, rather than ends of a range. Such features may be visualised or
532    * reported differently to features on a range.
533    */
534   @Override
535   public boolean isContactFeature()
536   {
537     return contactFeature;
538   }
539
540   /**
541    * Answers true if the sequence has zero start and end position
542    * 
543    * @return
544    */
545   public boolean isNonPositional()
546   {
547     return begin == 0 && end == 0;
548   }
549
550   /**
551    * Answers a formatted text report of feature details
552    * 
553    * @return
554    */
555   public String getDetailsReport()
556   {
557     StringBuilder sb = new StringBuilder(128);
558     if (begin == end)
559     {
560       sb.append(String.format("%s %d %s", type, begin, description));
561     }
562     else
563     {
564       sb.append(String.format("%s %d-%d %s", type, begin, end, description));
565     }
566     if (featureGroup != null)
567     {
568       sb.append(" (").append(featureGroup).append(")");
569     }
570     sb.append("\n\n");
571
572     if (otherDetails != null)
573     {
574       TreeMap<String, Object> ordered = new TreeMap<>(
575               String.CASE_INSENSITIVE_ORDER);
576       ordered.putAll(otherDetails);
577
578       for (Entry<String, Object> entry : ordered.entrySet())
579       {
580         String key = entry.getKey();
581         if (ATTRIBUTES.equals(key))
582         {
583           continue; // to avoid double reporting
584         }
585         if (INFO_KEYS.containsKey(key))
586         {
587           /*
588            * split selected INFO data by delimiter over multiple lines
589            */
590           sb.append(key).append("=\n  ");
591           String delimiter = INFO_KEYS.get(key);
592           String value = entry.getValue().toString();
593           sb.append(value.replace(delimiter, "\n  "));
594         }
595         else
596         {
597           sb.append(key + "=" + entry.getValue().toString() + "\n");
598         }
599       }
600     }
601     String text = sb.toString();
602     return text;
603   }
604 }