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