2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.datamodel;
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;
30 import java.util.HashMap;
32 import java.util.Map.Entry;
33 import java.util.SortedMap;
34 import java.util.TreeMap;
35 import java.util.Vector;
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.
42 public class SequenceFeature implements FeatureLocationI
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
48 private static final float NO_SCORE = 0f;
50 private static final String STATUS = "status";
52 private static final String STRAND = "STRAND";
54 // private key for Phase designed not to conflict with real GFF data
55 private static final String PHASE = "!Phase";
57 // private key for ENA location designed not to conflict with real GFF data
58 private static final String LOCATION = "!Location";
60 private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
63 * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
64 * name1=value1;name2=value2,value3;...etc
66 private static final String ATTRIBUTES = "ATTRIBUTES";
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
73 public final String type;
75 public final int begin;
79 public final String featureGroup;
81 public final float score;
83 private final boolean contactFeature;
85 public String description;
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
91 public Map<String, Object> otherDetails;
93 public Vector<String> links;
96 * the identifier (if known) for the FeatureSource held in FeatureSources,
97 * as a provider of metadata about feature attributes
99 private String source;
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.
108 public SequenceFeature(SequenceFeature cpy)
110 this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
123 public SequenceFeature(String theType, String theDesc, int theBegin,
124 int theEnd, String group)
126 this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
130 * Constructor including a score value
139 public SequenceFeature(String theType, String theDesc, int theBegin,
140 int theEnd, float theScore, String group)
143 this.description = theDesc;
144 this.begin = theBegin;
146 this.featureGroup = group;
147 this.score = theScore;
150 * for now, only "Disulfide/disulphide bond" is treated as a contact feature
152 this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
153 || "disulphide bond".equalsIgnoreCase(type);
157 * A copy constructor that allows the value of final fields to be 'modified'
166 public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
167 int newEnd, String newGroup, float newScore)
169 this(newType, sf.getDescription(), newBegin, newEnd, newScore,
172 this.source = sf.source;
174 if (sf.otherDetails != null)
176 otherDetails = new HashMap<>();
177 for (Entry<String, Object> entry : sf.otherDetails.entrySet())
179 otherDetails.put(entry.getKey(), entry.getValue());
182 if (sf.links != null && sf.links.size() > 0)
184 links = new Vector<>();
185 for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
187 links.addElement(sf.links.elementAt(i));
193 * A copy constructor that allows the value of final fields to be 'modified'
201 public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
202 String newGroup, float newScore)
204 this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
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.
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.
217 public boolean equals(Object o)
219 return equals(o, false);
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
229 * @param ignoreParent
232 public boolean equals(Object o, boolean ignoreParent)
234 if (o == null || !(o instanceof SequenceFeature))
239 SequenceFeature sf = (SequenceFeature) o;
240 boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score)
242 if (begin != sf.begin || end != sf.end || !sameScore)
247 if (getStrand() != sf.getStrand())
252 if (!(type + description + featureGroup + getPhase()).equals(
253 sf.type + sf.description + sf.featureGroup + sf.getPhase()))
257 if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
261 if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
267 if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
276 * Returns true if both values are null, are both non-null and equal
282 protected static boolean equalAttribute(Object att1, Object att2)
284 if (att1 == null && att2 == null)
290 return att1.equals(att2);
292 return att2.equals(att1);
298 * @return DOCUMENT ME!
301 public int getBegin()
309 * @return DOCUMENT ME!
320 * @return DOCUMENT ME!
322 public String getType()
330 * @return DOCUMENT ME!
332 public String getDescription()
337 public void setDescription(String desc)
342 public String getFeatureGroup()
347 public void addLink(String labelLink)
351 links = new Vector<>();
354 if (!links.contains(labelLink))
356 links.insertElementAt(labelLink, 0);
360 public float getScore()
366 * Used for getting values which are not in the basic set. eg STRAND, PHASE
372 public Object getValue(String key)
374 if (otherDetails == null)
380 return otherDetails.get(key);
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"
394 public String getValueAsString(String... key)
396 if (otherDetails == null)
400 Object value = otherDetails.get(key[0]);
401 if (key.length > 1 && value instanceof Map<?, ?>)
403 value = ((Map) value).get(key[1]);
405 return value == null ? null : value.toString();
409 * Returns a property value for the given key if known, else the specified
413 * @param defaultValue
416 public Object getValue(String key, Object defaultValue)
418 Object value = getValue(key);
419 return value == null ? defaultValue : value;
423 * Used for setting values which are not in the basic set. eg STRAND, FRAME
431 public void setValue(String key, Object value)
435 if (otherDetails == null)
437 otherDetails = new HashMap<>();
440 otherDetails.put(key, value);
441 recordAttribute(key, value);
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.
453 protected void recordAttribute(String key, Object value)
455 String attDesc = null;
458 attDesc = FeatureSources.getInstance().getSource(source)
459 .getAttributeName(key);
462 FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
467 * The following methods are added to maintain the castor Uniprot mapping file
470 public void setStatus(String status)
472 setValue(STATUS, status);
475 public String getStatus()
477 return (String) getValue(STATUS);
480 public void setAttributes(String attr)
482 setValue(ATTRIBUTES, attr);
485 public String getAttributes()
487 return (String) getValue(ATTRIBUTES);
491 * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
492 * GFF), and 0 for unknown or not (validly) specified
496 public int getStrand()
499 if (otherDetails != null)
501 Object str = otherDetails.get(STRAND);
506 else if ("+".equals(str))
515 * Set the value of strand
518 * should be "+" for forward, or "-" for reverse
520 public void setStrand(String strand)
522 setValue(STRAND, strand);
525 public void setPhase(String phase)
527 setValue(PHASE, phase);
530 public String getPhase()
532 return (String) getValue(PHASE);
536 * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
540 public void setEnaLocation(String loc)
542 setValue(LOCATION, loc);
546 * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
550 public String getEnaLocation()
552 return (String) getValue(LOCATION);
556 * Readable representation, for debug only, not guaranteed not to change
560 public String toString()
562 return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
567 * Overridden to ensure that whenever two objects are equal, they have the
571 public int hashCode()
573 String s = getType() + getDescription() + getFeatureGroup()
574 + getValue("ID") + getValue("Name") + getValue("Parent")
576 return s.hashCode() + getBegin() + getEnd() + (int) getScore()
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.
586 public boolean isContactFeature()
588 return contactFeature;
592 * Answers true if the sequence has zero start and end position
596 public boolean isNonPositional()
598 return begin == 0 && end == 0;
602 * Answers an html-formatted report of feature details
606 public String getDetailsReport()
608 FeatureSourceI metadata = FeatureSources.getInstance()
611 StringBuilder sb = new StringBuilder(128);
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)
621 sb.append(String.format(ROW_DATA, "Score", score, ""));
623 if (featureGroup != null)
625 sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
628 if (otherDetails != null)
630 TreeMap<String, Object> ordered = new TreeMap<>(
631 String.CASE_INSENSITIVE_ORDER);
632 ordered.putAll(otherDetails);
634 for (Entry<String, Object> entry : ordered.entrySet())
636 String key = entry.getKey();
637 if (ATTRIBUTES.equals(key))
639 continue; // to avoid double reporting
642 Object value = entry.getValue();
643 if (value instanceof Map<?, ?>)
646 * expand values in a Map attribute across separate lines
647 * copy to a TreeMap for alphabetical ordering
649 Map<String, Object> values = (Map<String, Object>) value;
650 SortedMap<String, Object> sm = new TreeMap<>(
651 String.CASE_INSENSITIVE_ORDER);
653 for (Entry<?, ?> e : sm.entrySet())
655 sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
656 .getValue().toString()));
661 // tried <td title="key"> but it failed to provide a tooltip :-(
662 String attDesc = null;
663 if (metadata != null)
665 attDesc = metadata.getAttributeName(key);
667 String s = entry.getValue().toString();
668 if (isValueInteresting(key, s, metadata))
670 sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
676 sb.append("</table>");
678 String text = sb.toString();
683 * Answers true if we judge the value is worth displaying, by some heuristic
691 boolean isValueInteresting(String key, String value,
692 FeatureSourceI metadata)
695 * currently suppressing zero values as well as null or empty
697 if (value == null || "".equals(value) || ".".equals(value)
698 || "0".equals(value))
703 if (metadata == null)
708 FeatureAttributeType attType = metadata.getAttributeType(key);
710 && (attType == FeatureAttributeType.Float || attType
711 .equals(FeatureAttributeType.Integer)))
715 float fval = Float.valueOf(value);
720 } catch (NumberFormatException e)
726 return true; // default to interesting
730 * Sets the feature source identifier
734 public void setSource(String theSource)