+
+ /**
+ * Test a cut action's relocation of sequence features
+ */
+ @Test(groups = { "Functional" })
+ public void testCut_withFeatures()
+ {
+ /*
+ * create sequence features before, after and overlapping
+ * a cut of columns/residues 4-7
+ */
+ SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
+ seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
+ null));
+ seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
+ 0f, null));
+ seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
+ null));
+ seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
+ 0f, null));
+ seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
+ null));
+
+ /*
+ * add some contact features
+ */
+ SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
+ 6, 0f, null);
+ seq0.addSequenceFeature(internalContact); // should get deleted
+ SequenceFeature overlapLeftContact = new SequenceFeature(
+ "disulphide bond", "", 2, 6, 0f, null);
+ seq0.addSequenceFeature(overlapLeftContact); // should get deleted
+ SequenceFeature overlapRightContact = new SequenceFeature(
+ "disulphide bond", "", 5, 8, 0f, null);
+ seq0.addSequenceFeature(overlapRightContact); // should get deleted
+ SequenceFeature spanningContact = new SequenceFeature(
+ "disulphide bond", "", 2, 9, 0f, null);
+ seq0.addSequenceFeature(spanningContact); // should get shortened 3'
+
+ /*
+ * cut columns 3-6 (base 0), residues d-g 4-7
+ */
+ Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
+ EditCommand.cut(ec, new AlignmentI[] { al });
+
+ List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+ SequenceFeatures.sortFeatures(sfs, true);
+
+ assertEquals(5, sfs.size()); // features internal to cut were deleted
+ SequenceFeature sf = sfs.get(0);
+ assertEquals("before", sf.getType());
+ assertEquals(1, sf.getBegin());
+ assertEquals(3, sf.getEnd());
+ sf = sfs.get(1);
+ assertEquals("disulphide bond", sf.getType());
+ assertEquals(2, sf.getBegin());
+ assertEquals(5, sf.getEnd()); // truncated by cut
+ sf = sfs.get(2);
+ assertEquals("overlap left", sf.getType());
+ assertEquals(2, sf.getBegin());
+ assertEquals(3, sf.getEnd()); // truncated by cut
+ sf = sfs.get(3);
+ assertEquals("after", sf.getType());
+ assertEquals(4, sf.getBegin()); // shifted left by cut
+ assertEquals(6, sf.getEnd()); // shifted left by cut
+ sf = sfs.get(4);
+ assertEquals("overlap right", sf.getType());
+ assertEquals(4, sf.getBegin()); // shifted left by cut
+ assertEquals(4, sf.getEnd()); // truncated by cut
+ }
+
+ /**
+ * Test a cut action's relocation of sequence features, with full coverage of
+ * all possible feature and cut locations for a 5-position ungapped sequence
+ */
+ @Test(groups = { "Functional" })
+ public void testCut_withFeatures_exhaustive()
+ {
+ /*
+ * create a sequence features on each subrange of 1-5
+ */
+ SequenceI seq0 = new Sequence("seq", "ABCDE");
+ int start = 8;
+ int end = 12;
+ seq0.setStart(start);
+ seq0.setEnd(end);
+ AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+ alignment.setDataset(null);
+
+ /*
+ * create a new alignment with shared dataset sequence
+ */
+ AlignmentI copy = new Alignment(
+ new SequenceI[]
+ { alignment.getDataset().getSequenceAt(0).deriveSequence() });
+ SequenceI copySeq0 = copy.getSequenceAt(0);
+
+ for (int from = start; from <= end; from++)
+ {
+ for (int to = from; to <= end; to++)
+ {
+ String desc = String.format("%d-%d", from, to);
+ SequenceFeature sf = new SequenceFeature("test", desc, from, to,
+ 0f, null);
+ sf.setValue("from", Integer.valueOf(from));
+ sf.setValue("to", Integer.valueOf(to));
+ seq0.addSequenceFeature(sf);
+ }
+ }
+ // sanity check
+ List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+ assertEquals(func(5), sfs.size());
+ assertEquals(sfs, copySeq0.getSequenceFeatures());
+ String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
+
+ /*
+ * now perform all possible cuts of subranges of columns 1-5
+ * and validate the resulting remaining sequence features!
+ */
+ SequenceI[] sqs = new SequenceI[] { seq0 };
+
+ for (int from = 0; from < seq0.getLength(); from++)
+ {
+ for (int to = from; to < seq0.getLength(); to++)
+ {
+ EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
+ - from + 1), alignment);
+ final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
+ boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
+ .getDatasetSequence();
+
+ verifyCut(seq0, from, to, msg, start);
+
+ /*
+ * verify copy alignment dataset sequence unaffected
+ */
+ assertEquals("Original dataset sequence was modified",
+ copySequenceFeatures,
+ copySeq0.getSequenceFeatures().toString());
+
+ /*
+ * verify any new dataset sequence was added to the
+ * alignment dataset
+ */
+ assertEquals("Wrong Dataset size after " + msg,
+ newDatasetSequence ? 2 : 1,
+ alignment.getDataset().getHeight());
+
+ /*
+ * undo and verify all restored
+ */
+ AlignmentI[] views = new AlignmentI[] { alignment };
+ ec.undoCommand(views);
+ sfs = seq0.getSequenceFeatures();
+ assertEquals("After undo of " + msg, func(5), sfs.size());
+ verifyUndo(from, to, sfs);
+
+ /*
+ * verify copy alignment dataset sequence still unaffected
+ * and alignment dataset has shrunk (if it was added to)
+ */
+ assertEquals("Original dataset sequence was modified",
+ copySequenceFeatures,
+ copySeq0.getSequenceFeatures().toString());
+ assertEquals("Wrong Dataset size after Undo of " + msg, 1,
+ alignment.getDataset().getHeight());
+
+ /*
+ * redo and verify
+ */
+ ec.doCommand(views);
+ verifyCut(seq0, from, to, msg, start);
+
+ /*
+ * verify copy alignment dataset sequence unaffected
+ * and any new dataset sequence readded to alignment dataset
+ */
+ assertEquals("Original dataset sequence was modified",
+ copySequenceFeatures,
+ copySeq0.getSequenceFeatures().toString());
+ assertEquals("Wrong Dataset size after Redo of " + msg,
+ newDatasetSequence ? 2 : 1,
+ alignment.getDataset().getHeight());
+
+ /*
+ * undo ready for next cut
+ */
+ ec.undoCommand(views);
+
+ /*
+ * final verify that copy alignment dataset sequence is still unaffected
+ * and that alignment dataset has shrunk
+ */
+ assertEquals("Original dataset sequence was modified",
+ copySequenceFeatures,
+ copySeq0.getSequenceFeatures().toString());
+ assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
+ alignment.getDataset().getHeight());
+ }
+ }
+ }
+
+ /**
+ * Verify by inspection that the sequence features left on the sequence after
+ * a cut match the expected results. The trick to this is that we can parse
+ * each feature's original start-end positions from its description.
+ *
+ * @param seq0
+ * @param from
+ * @param to
+ * @param msg
+ * @param seqStart
+ */
+ protected void verifyCut(SequenceI seq0, int from, int to,
+ final String msg, int seqStart)
+ {
+ List<SequenceFeature> sfs;
+ sfs = seq0.getSequenceFeatures();
+
+ Collections.sort(sfs, BY_DESCRIPTION);
+
+ /*
+ * confirm the number of features has reduced by the
+ * number of features within the cut region i.e. by
+ * func(length of cut); exception is a cut at start or end of sequence,
+ * which retains the original coordinates, dataset sequence
+ * and all its features
+ */
+ boolean datasetRetained = from == 0 || to == 4;
+ if (datasetRetained)
+ {
+ // dataset and all features retained
+ assertEquals(msg, func(5), sfs.size());
+ }
+ else if (to - from == 4)
+ {
+ // all columns were cut
+ assertTrue(sfs.isEmpty());
+ }
+ else
+ {
+ // failure in checkFeatureRelocation is more informative!
+ assertEquals(msg + "wrong number of features left", func(5)
+ - func(to - from + 1), sfs.size());
+ }
+
+ /*
+ * inspect individual features
+ */
+ for (SequenceFeature sf : sfs)
+ {
+ verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
+ seqStart);
+ }
+ }
+
+ /**
+ * Check that after Undo, every feature has start/end that match its original
+ * "start" and "end" properties
+ *
+ * @param from
+ * @param to
+ * @param sfs
+ */
+ protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
+ {
+ for (SequenceFeature sf : sfs)
+ {
+ final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+ final int oldTo = ((Integer) sf.getValue("to")).intValue();
+ String msg = String.format(
+ "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
+ oldFrom, oldTo);
+ assertEquals(msg + "start", oldFrom, sf.getBegin());
+ assertEquals(msg + "end", oldTo, sf.getEnd());
+ }
+ }
+
+ /**
+ * Helper method to check a feature has been correctly relocated after a cut
+ *
+ * @param sf
+ * @param from
+ * start of cut (first residue cut 1..)
+ * @param to
+ * end of cut (last residue cut 1..)
+ * @param newDataset
+ * @param seqStart
+ */
+ private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
+ boolean newDataset, int seqStart)
+ {
+ // TODO handle the gapped sequence case as well
+ int cutSize = to - from + 1;
+ final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+ final int oldTo = ((Integer) sf.getValue("to")).intValue();
+ final int oldFromPosition = oldFrom - seqStart + 1; // 1..
+ final int oldToPosition = oldTo - seqStart + 1; // 1..
+
+ String msg = String.format(
+ "Feature %s relocated to %d-%d after cut of %d-%d",
+ sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
+ if (!newDataset)
+ {
+ // dataset retained with all features unchanged
+ assertEquals("0: " + msg, oldFrom, sf.getBegin());
+ assertEquals("0: " + msg, oldTo, sf.getEnd());
+ }
+ else if (oldToPosition < from)
+ {
+ // before cut region so unchanged
+ assertEquals("1: " + msg, oldFrom, sf.getBegin());
+ assertEquals("2: " + msg, oldTo, sf.getEnd());
+ }
+ else if (oldFromPosition > to)
+ {
+ // follows cut region - shift by size of cut
+ assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
+ sf.getBegin());
+ assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
+ sf.getEnd());
+ }
+ else if (oldFromPosition < from && oldToPosition > to)
+ {
+ // feature encloses cut region - shrink it right
+ assertEquals("5: " + msg, oldFrom, sf.getBegin());
+ assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
+ }
+ else if (oldFromPosition < from)
+ {
+ // feature overlaps left side of cut region - truncated right
+ assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
+ }
+ else if (oldToPosition > to)
+ {
+ // feature overlaps right side of cut region - truncated left
+ assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
+ sf.getBegin());
+ assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
+ sf.getEnd());
+ }
+ else
+ {
+ // feature internal to cut - should have been deleted!
+ Assert.fail(msg + " - should have been deleted");
+ }
+ }
+
+ /**
+ * Test a cut action's relocation of sequence features
+ */
+ @Test(groups = { "Functional" })
+ public void testCut_withFeatures5prime()
+ {
+ SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
+ seq0.createDatasetSequence();
+ assertEquals(8, seq0.getStart());
+ seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
+ null));
+ SequenceI[] seqsArray = new SequenceI[] { seq0 };
+ AlignmentI alignment = new Alignment(seqsArray);
+
+ /*
+ * cut columns of A-B; same dataset sequence is retained, aligned sequence
+ * start becomes 10
+ */
+ Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
+ EditCommand.cut(ec, new AlignmentI[] { alignment });
+
+ /*
+ * feature on CC(10-11) should still be on CC(10-11)
+ */
+ assertSame(seq0, alignment.getSequenceAt(0));
+ assertEquals(10, seq0.getStart());
+ List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+ assertEquals(1, sfs.size());
+ SequenceFeature sf = sfs.get(0);
+ assertEquals(10, sf.getBegin());
+ assertEquals(11, sf.getEnd());
+ }