+ private Mapping mapping;
+
+ SequenceToSequenceMapping(SequenceI from, Mapping map)
+ {
+ this.fromSeq = from;
+ this.mapping = map;
+ }
+
+ /**
+ * Readable representation for debugging only, not guaranteed not to change
+ */
+ @Override
+ public String toString()
+ {
+ return String.format("From %s %s", fromSeq.getName(),
+ mapping.toString());
+ }
+
+ /**
+ * Returns a hashCode derived from the hashcodes of the mappings and fromSeq
+ *
+ * @see SequenceToSequenceMapping#hashCode()
+ */
+ @Override
+ public int hashCode()
+ {
+ return (fromSeq == null ? 0 : fromSeq.hashCode() * 31)
+ + mapping.hashCode();
+ }
+
+ /**
+ * Answers true if the objects hold the same mapping between the same two
+ * sequences
+ *
+ * @see Mapping#equals
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (!(obj instanceof SequenceToSequenceMapping))
+ {
+ return false;
+ }
+ SequenceToSequenceMapping that = (SequenceToSequenceMapping) obj;
+ if (this.mapping == null)
+ {
+ return that.mapping == null;
+ }
+ // TODO: can simplify by asserting fromSeq is a dataset sequence
+ return (this.fromSeq == that.fromSeq
+ || (this.fromSeq != null && that.fromSeq != null
+ && this.fromSeq.getDatasetSequence() != null
+ && this.fromSeq.getDatasetSequence() == that.fromSeq
+ .getDatasetSequence()))
+ && this.mapping.equals(that.mapping);
+ }
+
+ public SequenceI getFromSeq()
+ {
+ return fromSeq;
+ }
+
+ public Mapping getMapping()
+ {
+ return mapping;
+ }
+
+ /**
+ * Returns true if the mapping covers the full length of the given sequence.
+ * This allows us to distinguish the CDS that codes for a protein from
+ * another overlapping CDS in the parent dna sequence.
+ *
+ * @param seq
+ * @return
+ */
+ public boolean covers(SequenceI seq)
+ {
+ List<int[]> mappedRanges = null;
+ MapList mapList = mapping.getMap();
+ if (fromSeq == seq || fromSeq == seq.getDatasetSequence())
+ {
+ mappedRanges = mapList.getFromRanges();
+ }
+ else if (mapping.to == seq || mapping.to == seq.getDatasetSequence())
+ {
+ mappedRanges = mapList.getToRanges();
+ }
+ else
+ {
+ return false;
+ }
+
+ /*
+ * check that each mapped range lies within the sequence range
+ * (necessary for circular CDS - example EMBL:J03321:AAA91567)
+ * and mapped length covers (at least) sequence length
+ */
+ int length = 0;
+ for (int[] range : mappedRanges)
+ {
+ int from = Math.min(range[0], range[1]);
+ int to = Math.max(range[0], range[1]);
+ if (from < seq.getStart() || to > seq.getEnd())
+ {
+ return false;
+ }
+ length += (to - from + 1);
+ }
+ // add 1 to mapped length to allow for a mapped stop codon
+ if (length + 1 < (seq.getEnd() - seq.getStart() + 1))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Adds any regions mapped to or from position {@code pos} in sequence
+ * {@code seq} to the given search results
+ *
+ * @param seq
+ * @param pos
+ * @param sr
+ */
+ public void markMappedRegion(SequenceI seq, int pos, SearchResultsI sr)
+ {
+ int[] codon = null;
+ SequenceI mappedSeq = null;
+ SequenceI ds = seq.getDatasetSequence();
+ if (ds == null)
+ {
+ ds = seq;
+ }
+
+ if (this.fromSeq == seq || this.fromSeq == ds)
+ {
+ codon = this.mapping.map.locateInTo(pos, pos);
+ mappedSeq = this.mapping.to;
+ }
+ else if (this.mapping.to == seq || this.mapping.to == ds)
+ {
+ codon = this.mapping.map.locateInFrom(pos, pos);
+ mappedSeq = this.fromSeq;
+ }
+
+ if (codon != null)
+ {
+ for (int i = 0; i < codon.length; i += 2)
+ {
+ sr.addResult(mappedSeq, codon[i], codon[i + 1]);
+ }
+ }
+ }
+ }
+
+ private List<SequenceToSequenceMapping> mappings;