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.TreeMap;
34 import java.util.Vector;
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.
41 public class SequenceFeature implements FeatureLocationI
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
47 private static final float NO_SCORE = 0f;
49 private static final String STATUS = "status";
51 private static final String STRAND = "STRAND";
53 // private key for Phase designed not to conflict with real GFF data
54 private static final String PHASE = "!Phase";
56 // private key for ENA location designed not to conflict with real GFF data
57 private static final String LOCATION = "!Location";
59 private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
62 * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
63 * name1=value1;name2=value2,value3;...etc
65 private static final String ATTRIBUTES = "ATTRIBUTES";
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
72 public final String type;
74 public final int begin;
78 public final String featureGroup;
80 public final float score;
82 private final boolean contactFeature;
84 public String description;
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
90 public Map<String, Object> otherDetails;
92 public Vector<String> links;
95 * the identifier (if known) for the FeatureSource held in FeatureSources,
96 * as a provider of metadata about feature attributes
98 private String source;
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.
107 public SequenceFeature(SequenceFeature cpy)
109 this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
122 public SequenceFeature(String theType, String theDesc, int theBegin,
123 int theEnd, String group)
125 this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
129 * Constructor including a score value
138 public SequenceFeature(String theType, String theDesc, int theBegin,
139 int theEnd, float theScore, String group)
142 this.description = theDesc;
143 this.begin = theBegin;
145 this.featureGroup = group;
146 this.score = theScore;
149 * for now, only "Disulfide/disulphide bond" is treated as a contact feature
151 this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
152 || "disulphide bond".equalsIgnoreCase(type);
156 * A copy constructor that allows the value of final fields to be 'modified'
165 public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
166 int newEnd, String newGroup, float newScore)
168 this(newType, sf.getDescription(), newBegin, newEnd, newScore,
171 this.source = sf.source;
173 if (sf.otherDetails != null)
175 otherDetails = new HashMap<>();
176 for (Entry<String, Object> entry : sf.otherDetails.entrySet())
178 otherDetails.put(entry.getKey(), entry.getValue());
181 if (sf.links != null && sf.links.size() > 0)
183 links = new Vector<>();
184 for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
186 links.addElement(sf.links.elementAt(i));
192 * A copy constructor that allows the value of final fields to be 'modified'
200 public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
201 String newGroup, float newScore)
203 this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
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.
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.
216 public boolean equals(Object o)
218 return equals(o, false);
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
228 * @param ignoreParent
231 public boolean equals(Object o, boolean ignoreParent)
233 if (o == null || !(o instanceof SequenceFeature))
238 SequenceFeature sf = (SequenceFeature) o;
239 boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score)
241 if (begin != sf.begin || end != sf.end || !sameScore)
246 if (getStrand() != sf.getStrand())
251 if (!(type + description + featureGroup + getPhase()).equals(
252 sf.type + sf.description + sf.featureGroup + sf.getPhase()))
256 if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
260 if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
266 if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
275 * Returns true if both values are null, are both non-null and equal
281 protected static boolean equalAttribute(Object att1, Object att2)
283 if (att1 == null && att2 == null)
289 return att1.equals(att2);
291 return att2.equals(att1);
297 * @return DOCUMENT ME!
300 public int getBegin()
308 * @return DOCUMENT ME!
319 * @return DOCUMENT ME!
321 public String getType()
329 * @return DOCUMENT ME!
331 public String getDescription()
336 public void setDescription(String desc)
341 public String getFeatureGroup()
346 public void addLink(String labelLink)
350 links = new Vector<>();
353 if (!links.contains(labelLink))
355 links.insertElementAt(labelLink, 0);
359 public float getScore()
365 * Used for getting values which are not in the basic set. eg STRAND, PHASE
371 public Object getValue(String key)
373 if (otherDetails == null)
379 return otherDetails.get(key);
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"
393 public String getValueAsString(String... key)
395 if (otherDetails == null)
399 Object value = otherDetails.get(key[0]);
400 if (key.length > 1 && value instanceof Map<?, ?>)
402 value = ((Map) value).get(key[1]);
404 return value == null ? null : value.toString();
408 * Returns a property value for the given key if known, else the specified
412 * @param defaultValue
415 public Object getValue(String key, Object defaultValue)
417 Object value = getValue(key);
418 return value == null ? defaultValue : value;
422 * Used for setting values which are not in the basic set. eg STRAND, FRAME
430 public void setValue(String key, Object value)
434 if (otherDetails == null)
436 otherDetails = new HashMap<>();
439 otherDetails.put(key, value);
440 recordAttribute(key, value);
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.
452 protected void recordAttribute(String key, Object value)
454 String attDesc = null;
457 attDesc = FeatureSources.getInstance().getSource(source)
458 .getAttributeName(key);
461 FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
466 * The following methods are added to maintain the castor Uniprot mapping file
469 public void setStatus(String status)
471 setValue(STATUS, status);
474 public String getStatus()
476 return (String) getValue(STATUS);
479 public void setAttributes(String attr)
481 setValue(ATTRIBUTES, attr);
484 public String getAttributes()
486 return (String) getValue(ATTRIBUTES);
490 * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
491 * GFF), and 0 for unknown or not (validly) specified
495 public int getStrand()
498 if (otherDetails != null)
500 Object str = otherDetails.get(STRAND);
505 else if ("+".equals(str))
514 * Set the value of strand
517 * should be "+" for forward, or "-" for reverse
519 public void setStrand(String strand)
521 setValue(STRAND, strand);
524 public void setPhase(String phase)
526 setValue(PHASE, phase);
529 public String getPhase()
531 return (String) getValue(PHASE);
535 * Sets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
539 public void setEnaLocation(String loc)
541 setValue(LOCATION, loc);
545 * Gets the 'raw' ENA format location specifier e.g. join(12..45,89..121)
549 public String getEnaLocation()
551 return (String) getValue(LOCATION);
555 * Readable representation, for debug only, not guaranteed not to change
559 public String toString()
561 return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
566 * Overridden to ensure that whenever two objects are equal, they have the
570 public int hashCode()
572 String s = getType() + getDescription() + getFeatureGroup()
573 + getValue("ID") + getValue("Name") + getValue("Parent")
575 return s.hashCode() + getBegin() + getEnd() + (int) getScore()
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.
585 public boolean isContactFeature()
587 return contactFeature;
591 * Answers true if the sequence has zero start and end position
595 public boolean isNonPositional()
597 return begin == 0 && end == 0;
601 * Answers an html-formatted report of feature details
605 public String getDetailsReport()
607 FeatureSourceI metadata = FeatureSources.getInstance()
610 StringBuilder sb = new StringBuilder(128);
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)
620 sb.append(String.format(ROW_DATA, "Score", score, ""));
622 if (featureGroup != null)
624 sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
627 if (otherDetails != null)
629 TreeMap<String, Object> ordered = new TreeMap<>(
630 String.CASE_INSENSITIVE_ORDER);
631 ordered.putAll(otherDetails);
633 for (Entry<String, Object> entry : ordered.entrySet())
635 String key = entry.getKey();
636 if (ATTRIBUTES.equals(key))
638 continue; // to avoid double reporting
641 Object value = entry.getValue();
642 if (value instanceof Map<?, ?>)
645 * expand values in a Map attribute across separate lines
647 Map<?, ?> values = (Map<?, ?>) value;
648 for (Entry<?, ?> e : values.entrySet())
650 sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
651 .getValue().toString()));
656 // tried <td title="key"> but it failed to provide a tooltip :-(
657 String attDesc = null;
658 if (metadata != null)
660 attDesc = metadata.getAttributeName(key);
662 String s = entry.getValue().toString();
663 if (isValueInteresting(key, s, metadata))
665 sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
671 sb.append("</table>");
673 String text = sb.toString();
678 * Answers true if we judge the value is worth displaying, by some heuristic
686 boolean isValueInteresting(String key, String value,
687 FeatureSourceI metadata)
690 * currently suppressing zero values as well as null or empty
692 if (value == null || "".equals(value) || ".".equals(value)
693 || "0".equals(value))
698 if (metadata == null)
703 FeatureAttributeType attType = metadata.getAttributeType(key);
705 && (attType == FeatureAttributeType.Float || attType
706 .equals(FeatureAttributeType.Integer)))
710 float fval = Float.valueOf(value);
715 } catch (NumberFormatException e)
721 return true; // default to interesting
725 * Sets the feature source identifier
729 public void setSource(String theSource)