/*
* BioJava development code
*
* This code may be freely distributed and modified under the
* terms of the GNU Lesser General Public Licence. This should
* be distributed with the code. If you do not have a copy,
* see:
*
* http://www.gnu.org/copyleft/lesser.html
*
* Copyright for this code is held jointly by the individual
* authors. These should be listed in @author doc comments.
*
* For more information on the BioJava project and its aims,
* or to join the biojava-l mailing list, visit the home page
* at:
*
* http://www.biojava.org/
*
*/
package org.biojavax.bio.seq;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import org.biojava.bio.Annotation;
import org.biojava.bio.seq.DNATools;
import org.biojava.bio.seq.RNATools;
import org.biojava.bio.seq.io.ParseException;
import org.biojava.bio.symbol.Alphabet;
import org.biojava.bio.symbol.AlphabetManager;
import org.biojava.bio.symbol.IllegalAlphabetException;
import org.biojava.bio.symbol.Location;
import org.biojava.bio.symbol.SymbolList;
import org.biojava.bio.symbol.SymbolListViews;
import org.biojava.utils.AbstractChangeable;
import org.biojava.utils.ChangeEvent;
import org.biojava.utils.ChangeSupport;
import org.biojava.utils.ChangeVetoException;
import org.biojavax.CrossRef;
import org.biojavax.CrossReferenceResolver;
import org.biojavax.Namespace;
import org.biojavax.RichAnnotation;
import org.biojavax.RichObjectFactory;
import org.biojavax.SimpleRichAnnotation;
import org.biojavax.bio.seq.io.GenbankLocationParser;
import org.biojavax.ontology.ComparableTerm;
/**
* A simple implementation of RichLocation.
* @author Richard Holland
* @author Mark Schreiber
* @author George Waldon
* @since 1.5
*/
public class SimpleRichLocation extends AbstractChangeable implements RichLocation {
private CrossRef crossRef;
private RichAnnotation notes = new SimpleRichAnnotation();
protected ComparableTerm term;
private Position min;
private Position max;
private PositionResolver pr = RichObjectFactory.getDefaultPositionResolver();
private CrossReferenceResolver crr = RichObjectFactory.getDefaultCrossReferenceResolver();
private Strand strand;
private int rank;
protected int circularLength = 0;
private RichFeature feature;
/**
* Creates a new instance of SimpleRichSequenceLocation that points to a
* single position on the positive strand.
* @param pos the location position (a point).
* @param rank Rank of location.
*/
public SimpleRichLocation(Position pos, int rank) {
this(pos,pos,rank,Strand.POSITIVE_STRAND);
}
/**
* Creates a new instance of SimpleRichSequenceLocation that points to a
* single position.
* @param pos the location position (a point).
* @param rank Rank of location.
* @param strand The strand of the location
*/
public SimpleRichLocation(Position pos, int rank, Strand strand) {
this(pos,pos,rank,strand,null);
}
/**
* Creates a new instance of SimpleRichSequenceLocation that points to a
* single position on another sequence.
* @param pos the location position (a point).
* @param rank Rank of location.
* @param strand the strand of the location
* @param crossRef a cross reference to another object (null for parent sequence)
*/
public SimpleRichLocation(Position pos, int rank, Strand strand, CrossRef crossRef) {
this(pos,pos,rank,strand,crossRef);
}
/**
* Creates a new instance of SimpleRichSequenceLocation that points to a
* range position on the positive strand.
* @param min the minimum bound of the location
* @param max the maximum bound of the location
* @param rank Rank of location.
*
*/
public SimpleRichLocation(Position min, Position max, int rank) {
this(min,max,rank,Strand.POSITIVE_STRAND);
}
/**
* Creates a new instance of SimpleRichSequenceLocation that points to a
* range position.
* @param min the minimum bound of the location
* @param max the maximum bound of the location
* @param rank Rank of location.
* @param strand the strand of the location
*/
public SimpleRichLocation(Position min, Position max, int rank, Strand strand) {
this(min,max,rank,strand,null);
}
/**
* Creates a new instance of SimpleRichSequenceLocation that points to a
* range position on another sequence.
* @param min the minimum bound of the location
* @param max the maximum bound of the location
* @param rank Rank of location.
* @param strand the strand of the location
* @param crossRef a cross reference to another object (null for parent sequence)
*/
public SimpleRichLocation(Position min, Position max, int rank, Strand strand, CrossRef crossRef) {
this.min = min;
this.max = max;
this.rank = rank;
this.strand = strand;
this.crossRef = crossRef;
this.feature = null;
}
// Hibernate requirement - not for public use.
protected SimpleRichLocation() {}
/**
* {@inheritDoc}
*/
public void sort() {}
/**
* {@inheritDoc}
*/
public RichFeature getFeature() { return this.feature; }
/**
* {@inheritDoc}
*/
public void setFeature(RichFeature feature) throws ChangeVetoException {
if(!this.hasListeners(RichLocation.FEATURE)) {
this.feature = feature;
} else {
ChangeEvent ce = new ChangeEvent(
this,
RichLocation.FEATURE,
feature,
this.feature
);
ChangeSupport cs = this.getChangeSupport(RichLocation.FEATURE);
synchronized(cs) {
cs.firePreChangeEvent(ce);
this.feature = feature;
cs.firePostChangeEvent(ce);
}
}
}
/**
* {@inheritDoc}
*/
public CrossRef getCrossRef() { return this.crossRef; }
// Hibernate requirement - not for public use.
protected void setCrossRef(CrossRef crossRef) { this.crossRef = crossRef; }
/**
* {@inheritDoc}
*/
public Annotation getAnnotation() { return getRichAnnotation(); }
/**
* {@inheritDoc}
*/
public RichAnnotation getRichAnnotation() { return this.notes; }
/**
* {@inheritDoc}
* Warning this method gives access to the original
* Collection not a copy. This is required by Hibernate. If you
* modify the object directly the behaviour may be unpredictable.
*/
public Set getNoteSet() { return this.notes.getNoteSet(); }
/**
* {@inheritDoc}
* Warning this method gives access to the original
* Collection not a copy. This is required by Hibernate. If you
* modify the object directly the behaviour may be unpredictable.
*/
public void setNoteSet(Set notes) throws ChangeVetoException { this.notes.setNoteSet(notes); }
/**
* {@inheritDoc}
*/
public ComparableTerm getTerm() { return this.term; }
/**
* {@inheritDoc}
*/
public void setTerm(ComparableTerm term) throws ChangeVetoException {
if(!this.hasListeners(RichLocation.TERM)) {
this.term = term;
} else {
ChangeEvent ce = new ChangeEvent(
this,
RichLocation.TERM,
term,
this.term
);
ChangeSupport cs = this.getChangeSupport(RichLocation.TERM);
synchronized(cs) {
cs.firePreChangeEvent(ce);
this.term = term;
cs.firePostChangeEvent(ce);
}
}
}
/**
* {@inheritDoc}
*/
public int getCircularLength() { return this.circularLength; }
/**
* {@inheritDoc}
*/
public void setCircularLength(int circularLength) throws ChangeVetoException {
if(!this.hasListeners(RichLocation.CIRCULAR)) {
this.circularLength = circularLength;
} else {
ChangeEvent ce = new ChangeEvent(
this,
RichLocation.CIRCULAR,
new Integer(circularLength),
new Integer(this.circularLength)
);
ChangeSupport cs = this.getChangeSupport(RichLocation.CIRCULAR);
synchronized(cs) {
cs.firePreChangeEvent(ce);
this.circularLength = circularLength;
cs.firePostChangeEvent(ce);
}
}
}
/**
* {@inheritDoc}
*/
public Strand getStrand() { return this.strand; }
// Hibernate requirement - not for public use.
protected void setStrand(Strand strand) { this.strand = strand; }
// Hibernate requirement - not for public use.
int getStrandNum() { return this.strand.intValue(); }
// Hibernate requirement - not for public use.
void setStrandNum(int token) { this.strand = Strand.forValue(token); }
/**
* {@inheritDoc}
*/
public int getRank() { return this.rank; }
/**
* {@inheritDoc}
*/
public void setRank(int rank) throws ChangeVetoException {
if(!this.hasListeners(RichLocation.RANK)) {
this.rank = rank;
} else {
ChangeEvent ce = new ChangeEvent(
this,
RichLocation.RANK,
new Integer(rank),
new Integer(this.rank)
);
ChangeSupport cs = this.getChangeSupport(RichLocation.RANK);
synchronized(cs) {
cs.firePreChangeEvent(ce);
this.rank = rank;
cs.firePostChangeEvent(ce);
}
}
}
/**
* {@inheritDoc}
*/
public int getMax() {
if (this.max.equals(this.min)) return this.getMin(); // because resolver might resolve differently
else return this.pr.getMax(this.max);
}
// Hibernate requirement - not for public use.
void setMax(int max) { this.max = new SimplePosition(false,false,max); }
/**
* {@inheritDoc}
*/
public int getMin() { return this.pr.getMin(this.min); }
// Hibernate requirement - not for public use.
void setMin(int min) { this.min = new SimplePosition(false,false,min); }
/**
* {@inheritDoc}
*/
public Position getMinPosition() { return this.min; }
// Hibernate requirement - not for public use.
protected void setMinPosition(Position min) { this.min = min; }
/**
* {@inheritDoc}
*/
public Position getMaxPosition() { return this.max; }
// Hibernate requirement - not for public use.
protected void setMaxPosition(Position max) { this.max = max; }
/**
* {@inheritDoc}
*/
public void setPositionResolver(PositionResolver p) { this.pr = p; }
/**
* {@inheritDoc}
*/
public Iterator blockIterator() { return Collections.singleton(this).iterator(); }
/**
* {@inheritDoc}
*/
public boolean isContiguous() { return true; }
/**
* {@inheritDoc}
*/
public boolean contains(int p) {
int modStart = this.getMin();
int modEnd = this.getMax();
if (this.circularLength>0) {
// Modulate the point to fall inside our sequence
p = RichLocation.Tools.modulateCircularIndex(p, this.circularLength);
// Modulate our location to the earliest possible point in our sequence
int[] ourModParts = RichLocation.Tools.modulateCircularLocation(modStart, modEnd, this.circularLength);
modStart = ourModParts[0];
modEnd = ourModParts[1];
// If we wrap and the point occurs before us, increment point to fall in correct range
if (modEnd>this.circularLength && p=modStart && p<=modEnd);
}
/**
* {@inheritDoc}
*/
public Location getDecorator(Class decoratorClass) { return null; }
/**
* {@inheritDoc}
*/
public Location newInstance(Location loc) { return loc; }
/**
* {@inheritDoc}
*/
public Location translate(int dist) {
return new SimpleRichLocation(this.min.translate(dist),this.max.translate(dist),0,this.strand,this.crossRef);
}
/**
* {@inheritDoc}
* A location contains another location if it overlaps it, and the coordinates
* enclose those of the other location at both ends, and they fall on
* the same strand.
*/
public boolean contains(Location l) {
if (!(l instanceof RichLocation)) l = RichLocation.Tools.enrich(l);
if (l instanceof EmptyRichLocation) return false;
else {
RichLocation rl = (RichLocation)l;
// Simple vs. simple
if (!this.overlaps(rl)) return false; // No overlap = no possible contain
if (!this.getStrand().equals(rl.getStrand())) return false; // Diff strand = not contained
if (this.circularLength>0) {
// Modulate to shortest possible equivalent region
int parts[] = RichLocation.Tools.modulateCircularLocationPair(this,rl,this.circularLength);
int ourModStart = parts[0];
int ourModEnd = parts[1];
int theirModStart = parts[2];
int theirModEnd = parts[3];
return (ourModStart <= theirModStart && ourModEnd >= theirModEnd);
} else {
return (this.getMin() <= rl.getMin() && this.getMax() >= rl.getMax());
}
}
}
/**
* {@inheritDoc}
* A location overlaps another location if it is on the same sequence, and
* the coordinates overlap, and both are of the same circularity.
*/
public boolean overlaps(Location l) {
if (!(l instanceof RichLocation)) l = RichLocation.Tools.enrich(l);
if (l instanceof EmptyRichLocation) return false;
else if (l instanceof CompoundRichLocation) return l.overlaps(this);
else {
// Simple vs. simple
RichLocation rl = (RichLocation)l;
// Mismatch of crossref is no overlap.
if (rl.getCrossRef()!=null || this.crossRef!=null) {
if (rl.getCrossRef()!=null && this.crossRef!=null) {
if (!this.crossRef.equals(rl.getCrossRef())) return false;
} else return false;
}
if (this.circularLength!=rl.getCircularLength()) return false; // Diff circularLength location sizes = not overlapping
// Modulate our start/end to shortest possible equivalent region
if (this.circularLength>0) {
int parts[] = RichLocation.Tools.modulateCircularLocationPair(this,rl,this.circularLength);
int ourModStart = parts[0];
int ourModEnd = parts[1];
int theirModStart = parts[2];
int theirModEnd = parts[3];
return (ourModStart<=theirModEnd && ourModEnd>=theirModStart);
} else {
return (this.getMin()<=rl.getMax() && this.getMax()>=rl.getMin());
}
}
}
/**
* {@inheritDoc}
* A merged SimpleRichLocation is returned if the two locations overlap and are on
* the same strand. Otherwise, a CompoundRichLocation is returned containing
* the two locations as members.
*/
public Location union(Location l) {
if (!(l instanceof RichLocation)) l = RichLocation.Tools.enrich(l);
if (l instanceof EmptyRichLocation) return this;
else if (l instanceof CompoundRichLocation) return l.union(this);
else {
// Simple vs. simple
RichLocation rl = (RichLocation)l;
if (this.overlaps(rl) && this.getStrand().equals(rl.getStrand())) {
// We can do the one-v-one overlapping same-strand union
if (this.circularLength>0) {
// Union of Overlapping circular locations
// Modulate our start/end to shortest possible equivalent region
int parts[] = RichLocation.Tools.modulateCircularLocationPair(this,rl,this.circularLength);
int ourModStart = parts[0];
int ourModEnd = parts[1];
int theirModStart = parts[2];
int theirModEnd = parts[3];
// Now we can select the minimum and maximum positions using the modded locations
Position startPos = (ourModStarttheirModEnd)?this.max:rl.getMaxPosition();
return new SimpleRichLocation(startPos,endPos,0,this.strand,this.crossRef);
} else {
// Union of Overlapping non-circular locations
return new SimpleRichLocation(this.posmin(this.min,rl.getMinPosition()),this.posmax(this.max,rl.getMaxPosition()),0,this.strand,this.crossRef);
}
}
// We can do the one-v-one non-overlapping or different-strand union too
Collection members = new ArrayList();
members.add(this);
members.add(l);
if (RichLocation.Tools.isMultiSource(members)) return new MultiSourceCompoundRichLocation(members);
else return new CompoundRichLocation(members);
}
}
/**
* {@inheritDoc}
* If the locations overlap and are on the same strand, the intersection
* is returned. If they overlap but are on different strands, a CompoundRichLocation
* of the overlapping portions is returned. If they do not overlap, the empty
* sequence is returned.
*/
public Location intersection(Location l) {
if (!(l instanceof RichLocation)) l = RichLocation.Tools.enrich(l);
if (l instanceof EmptyRichLocation) return l;
else if (l instanceof CompoundRichLocation) return l.intersection(this);
else {
RichLocation rl = (RichLocation)l;
if (this.overlaps(l)) {
if (this.getStrand().equals(rl.getStrand())) {
// We can do the one-v-one same-strand overlapping intersection here
if (this.circularLength>0) {
// Modulate our start/end to shortest possible equivalent region
int parts[] = RichLocation.Tools.modulateCircularLocationPair(this,rl,this.circularLength);
int ourModStart = parts[0];
int ourModEnd = parts[1];
int theirModStart = parts[2];
int theirModEnd = parts[3];
// Now we can select the minimum and maximum positions using the modded locations
Position startPos = (ourModStart>theirModStart)?this.min:rl.getMinPosition();
Position endPos = (ourModEndbr) return a;
else return b;
}
/**
* {@inheritDoc}
*/
public void setCrossRefResolver(CrossReferenceResolver r) {
if (r==null) throw new IllegalArgumentException("Resolver cannot be null");
this.crr = r;
}
/**
* {@inheritDoc}
* If the location is circular but the sequence is not, or they are both
* circular but of different circular lengths, an exception is thrown.
* The symbol list passed in is the sequence used to obtain symbols
* if the cross reference for this location has not been set. If the cross
* reference has been set, then the symbol list passed in is only used
* if it has the same accession, namespace and version as the cross
* reference on this location. Otherwise, the cross referenced symbol list
* is looked up and used instead.
*/
public SymbolList symbols(SymbolList seq) {
if (seq==null) throw new IllegalArgumentException("Sequence cannot be null");
if (seq instanceof RichSequence) {
RichSequence rs = (RichSequence)seq;
if (this.getCircularLength()>0) {
if (!rs.getCircular()) throw new IllegalArgumentException("Attempt to apply circular location to non-circular sequence");
if (rs.length()==this.getCircularLength()) throw new IllegalArgumentException("Attempt to apply circular location to circular sequence of different length");
}
}
// Resolve cross-references to remote sequences
if (this.getCrossRef()!=null) {
CrossRef cr = this.getCrossRef();
if (seq instanceof RichSequence) {
RichSequence rs = (RichSequence)seq;
String accession = rs.getAccession();
Namespace ns = rs.getNamespace();
String raccession = cr.getAccession();
String rnamespace = cr.getDbname();
if (!(accession.equals(raccession) && ns.getName().equals(rnamespace))) {
// It really is remote - the xref doesn't point to the sequence we just got passed
seq = this.crr.getRemoteSymbolList(cr, seq.getAlphabet());
}
} else {
// It's assumed to be remote because we can't tell what the sequence we were passed really is
seq = this.crr.getRemoteSymbolList(this.getCrossRef(), seq.getAlphabet());
}
}
// Carry on as before
seq = seq.subList(this.getMin(),this.getMax());
try {
if (this.strand==Strand.NEGATIVE_STRAND) {
Alphabet a = seq.getAlphabet();
if (a==AlphabetManager.alphabetForName("DNA")) {
seq = DNATools.reverseComplement(seq);
} else if (a==AlphabetManager.alphabetForName("RNA")) {
seq = RNATools.reverseComplement(seq);
} else {
seq = SymbolListViews.reverse(seq);// no complement as no such thing
}
}
} catch (IllegalAlphabetException e) {
IllegalArgumentException ex =
new IllegalArgumentException("Could not understand alphabet of passed sequence");
ex.initCause(e);
throw ex;
}
return seq;
}
/**
* {@inheritDoc}
*/
public int hashCode() {
int code = 17;
// Hibernate comparison - we haven't been populated yet
if (this.strand==null) return code;
// Normal comparison
if (this.term!=null) code = 31*code + this.term.hashCode();
code = 31*code + this.getMin();
code = 31*code + this.getMax();
code = 31*code + this.strand.hashCode();
code = 31*code + this.rank;
if (this.crossRef!=null) code = 31*code + this.crossRef.hashCode();
return code;
}
/**
* {@inheritDoc}
* Locations are equal if their term, min, max, strand, and crossref are
* the same, and if their rank is the same too.
*/
public boolean equals(Object o) {
if (! (o instanceof RichLocation)) return false;
// Hibernate comparison - we haven't been populated yet
if (this.strand==null) return false;
// Normal comparison
RichLocation fo = (RichLocation) o;
if (this.term!=null || fo.getTerm()!=null) {
if (this.term!=null && fo.getTerm()!=null) {
if (!this.term.equals(fo.getTerm())) return false;
} else return false;
}
if (this.getMin()!=fo.getMin()) return false;
if (this.getMax()!=fo.getMax()) return false;
if (!this.strand.equals(fo.getStrand())) return false;
if (this.crossRef!=null || fo.getCrossRef()!=null) {
if (this.crossRef!=null && fo.getCrossRef()!=null) {
if(!this.crossRef.equals(fo.getCrossRef())) return false;
} else return false;
}
return this.rank==fo.getRank();
}
/**
* {@inheritDoc}
* Locations are sorted first by rank, then crossref, then
* strand, then term, then min, then max.
*/
public int compareTo(Object o) {
if (o==this) return 0;
// Hibernate comparison - we haven't been populated yet
if (this.strand==null) return -1;
// Check if we can really compare at all
if (!(o instanceof RichLocation)) return -1;
// Normal comparison
RichLocation fo = (RichLocation) o;
if (this.rank!=fo.getRank()) return this.rank-fo.getRank();
if (this.crossRef!=null || fo.getCrossRef()!=null) {
if (this.crossRef!=null && fo.getCrossRef()!=null) {
return this.crossRef.compareTo(fo.getCrossRef());
} else return -1;
}
if (!this.strand.equals(fo.getStrand())) return this.strand.compareTo(fo.getStrand());
if (this.term!=null || fo.getTerm()!=null) {
if (this.term!=null && fo.getTerm()!=null && !this.term.equals(fo.getTerm())) return this.term.compareTo(fo.getTerm());
else return -1;
}
if (this.getMin()!=fo.getMin()) return this.getMin()-fo.getMin();
return this.getMax()-fo.getMax();
}
/**
* {@inheritDoc}
* Form: "start..end" or just "point" for point locations
*/
public String toString() {
if (this.max.equals(this.min)) {
return this.min.toString();
} else {
return this.min+".."+this.max;
}
}
// Internal use only.
void setLocationText(final String theLocation) throws ParseException {
// System.out.println("SimpleRichLocation.setLocationText-theLocation: ["+theLocation+"]");
if (theLocation == null) {
setMinPosition(RichLocation.EMPTY_LOCATION.getMinPosition());
setMaxPosition(RichLocation.EMPTY_LOCATION.getMaxPosition());
} else {
final RichLocation location = GenbankLocationParser.parseLocation(RichObjectFactory.getDefaultNamespace(), null, theLocation);
setMinPosition(location.getMinPosition());
setMaxPosition(location.getMaxPosition());
}
// System.out.println("SimpleRichLocation.setLocationText-this: ["+this+"]");
}
// Internal use only.
String getLocationText() {
// System.out.println("SimpleRichLocation.getLocationText-returns: ["+GenbankLocationParser.writeLocation(new SimpleRichLocation(getMinPosition(), getMaxPosition(), getRank()))+"], this: ["+this+"]");
return GenbankLocationParser.writeLocation(new SimpleRichLocation(getMinPosition(), getMaxPosition(), getRank()));
}
// Hibernate requirement - not for public use.
private Integer id;
/**
* Gets the Hibernate ID. Should be used with caution.
* @return the Hibernate ID, if using Hibernate.
*/
public Integer getId() { return this.id; }
/**
* Sets the Hibernate ID. Should be used with caution.
* @param id the Hibernate ID, if using Hibernate.
*/
public void setId(Integer id) { this.id = id;}
}