From a6aaf54e567106178f3f6733b59674f13522b919 Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Fri, 28 Jul 2023 16:13:28 +0200 Subject: [PATCH] JAL-4241 Fix annotation and feature alignment with selection --- .../actions/annotation/AlignCalcWorkerAdapter.java | 10 +- .../ws2/actions/annotation/AnnotationJob.java | 166 ++++++++++---------- .../ws2/actions/annotation/AnnotationResult.java | 10 +- .../ws2/actions/annotation/AnnotationTask.java | 97 +++++++----- .../ws2/gui/AnnotationServiceGuiHandler.java | 2 +- 5 files changed, 152 insertions(+), 133 deletions(-) diff --git a/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java b/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java index 0905ea2..60d7f15 100644 --- a/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java +++ b/src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java @@ -3,8 +3,6 @@ package jalview.ws2.actions.annotation; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; import jalview.analysis.AlignmentAnnotationUtils; import jalview.api.AlignViewportI; @@ -45,9 +43,9 @@ public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableA ala.graphGroup += graphGroup; var newAnnot = alignViewport.getAlignment() .updateFromOrCopyAnnotation(ala); - if (ala.sequenceRef != null) + if (newAnnot.sequenceRef != null) { - ala.sequenceRef.addAlignmentAnnotation(newAnnot); + newAnnot.sequenceRef.addAlignmentAnnotation(newAnnot); newAnnot.adjustForAlignment(); AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith( newAnnot, newAnnot.label, newAnnot.getCalcId()); @@ -59,7 +57,7 @@ public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableA AlignCalcWorkerAdapter.this, new AnnotationResult( annotations, - result.transferFeatures, + result.hasFeatures, result.featureColours, result.featureFilters)); } @@ -179,7 +177,7 @@ public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableA } private WorkerListener listener = WorkerListener.NULL_LISTENER; - + public void setWorkerListener(WorkerListener listener) { if (listener == null) listener = WorkerListener.NULL_LISTENER; diff --git a/src/jalview/ws2/actions/annotation/AnnotationJob.java b/src/jalview/ws2/actions/annotation/AnnotationJob.java index 23e462b..467dafd 100644 --- a/src/jalview/ws2/actions/annotation/AnnotationJob.java +++ b/src/jalview/ws2/actions/annotation/AnnotationJob.java @@ -2,20 +2,15 @@ package jalview.ws2.actions.annotation; import java.util.ArrayList; import java.util.BitSet; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import jalview.analysis.AlignSeq; import jalview.analysis.SeqsetUtils; -import jalview.api.FeatureColourI; -import jalview.datamodel.AlignmentAnnotation; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.AnnotatedCollectionI; import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceCollectionI; import jalview.datamodel.SequenceI; -import jalview.datamodel.features.FeatureMatcherSetI; import jalview.schemes.ResidueProperties; import jalview.util.Comparison; import jalview.ws2.actions.BaseJob; @@ -26,25 +21,18 @@ public class AnnotationJob extends BaseJob final Map seqNames; - final int start, end; - - final int minSize; + final int regionStart, regionEnd; - List returnedAnnotations = Collections.emptyList(); - - Map featureColours = Collections.emptyMap(); - - Map featureFilters = Collections.emptyMap(); - + final int minSize; public AnnotationJob(List inputSeqs, boolean[] gapMap, - Map seqNames, int start, int end, int minSize) + Map seqNames, int start, int end, int minSize) { super(inputSeqs); this.gapMap = gapMap; this.seqNames = seqNames; - this.start = start; - this.end = end; + this.regionStart = start; + this.regionEnd = end; this.minSize = minSize; } @@ -58,16 +46,15 @@ public class AnnotationJob extends BaseJob return nvalid >= minSize; } - public static AnnotationJob create(AnnotatedCollectionI inputSeqs, - boolean bySequence, boolean submitGaps, boolean requireAligned, - boolean filterNonStandardResidues, int minSize) + public static AnnotationJob create(SequenceCollectionI inputSeqs, + boolean bySequence, boolean submitGaps, boolean requireAligned, + boolean filterNonStandardResidues, int minSize) { - List seqs = new ArrayList<>(); + List seqences = new ArrayList<>(); int minlen = 10; - int ln = -1; - Map seqNames = bySequence ? new HashMap<>() : null; - BitSet gapMap = new BitSet(); - int gapMapSize = 0; + int width = 0; + Map namesMap = bySequence ? new HashMap<>() : null; + BitSet residueMap = new BitSet(); int start = inputSeqs.getStartRes(); int end = inputSeqs.getEndRes(); // TODO: URGENT! unify with JPred / MSA code to handle hidden regions @@ -76,69 +63,88 @@ public class AnnotationJob extends BaseJob // persisted/restored for (SequenceI sq : inputSeqs.getSequences()) { - int sqlen; - if (bySequence) - sqlen = sq.findPosition(end + 1) - sq.findPosition(start + 1); + int sqLen = (bySequence) + ? sq.findPosition(end + 1) - sq.findPosition(start + 1) + : sq.getEnd() - sq.getStart(); + if (sqLen < minlen) + continue; + String newName = SeqsetUtils.unique_name(seqences.size() + 1); + if (namesMap != null) + namesMap.put(newName, sq); + Sequence seq; + if (submitGaps) + { + seq = new Sequence(newName, sq.getSequenceAsString()); + updateResidueMap(residueMap, seq, filterNonStandardResidues); + } else - sqlen = sq.getEnd() - sq.getStart(); - if (sqlen >= minlen) { - String newName = SeqsetUtils.unique_name(seqs.size() + 1); - if (seqNames != null) - seqNames.put(newName, sq); - Sequence seq; - if (submitGaps) - { - seq = new Sequence(newName, sq.getSequenceAsString()); - gapMapSize = Math.max(gapMapSize, seq.getLength()); - for (int pos : sq.gapMap()) - { - char sqchr = sq.getCharAt(pos); - boolean include = !filterNonStandardResidues; - include |= sq.isProtein() ? ResidueProperties.aaIndex[sqchr] < 20 - : ResidueProperties.nucleotideIndex[sqchr] < 5; - if (include) - gapMap.set(pos); - } - } - else - { - // TODO: add ability to exclude hidden regions - seq = new Sequence(newName, AlignSeq.extractGaps(Comparison.GapChars, - sq.getSequenceAsString(start, end + 1))); - // for annotation need to also record map to sequence start/end - // position in range - // then transfer back to original sequence on return. - } - seqs.add(seq); - ln = Math.max(ln, seq.getLength()); + // TODO: add ability to exclude hidden regions + seq = new Sequence(newName, + AlignSeq.extractGaps(Comparison.GapChars, + sq.getSequenceAsString(start, end + 1))); + // for annotation need to also record map to sequence start/end + // position in range + // then transfer back to original sequence on return. } + seqences.add(seq); + width = Math.max(width, seq.getLength()); } if (requireAligned && submitGaps) { - int realWidth = gapMap.cardinality(); - for (int i = 0; i < seqs.size(); i++) + for (int i = 0; i < seqences.size(); i++) + { + SequenceI sq = seqences.get(i); + char[] padded = fitSequenceToResidueMap(sq.getSequence(), + residueMap); + seqences.set(i, new Sequence(sq.getName(), padded)); + } + } + boolean[] gapMapArray = null; + if (submitGaps) + { + gapMapArray = new boolean[width]; + for (int i = 0; i < width; i++) + gapMapArray[i] = residueMap.get(i); + } + return new AnnotationJob(seqences, gapMapArray, namesMap, start, end, + minSize); + } + + private static void updateResidueMap(BitSet residueMap, SequenceI seq, + boolean filterNonStandardResidues) + { + for (int pos : seq.gapMap()) + { + char sqchr = seq.getCharAt(pos); + boolean include = !filterNonStandardResidues; + include |= seq.isProtein() ? ResidueProperties.aaIndex[sqchr] < 20 + : ResidueProperties.nucleotideIndex[sqchr] < 5; + if (include) + residueMap.set(pos); + } + } + + /** + * Fits the sequence to the residue map removing empty columns where residue + * map is unset and padding the sequence with gaps at the end if needed. + */ + private static char[] fitSequenceToResidueMap(char[] sequence, + BitSet residueMap) + { + int width = residueMap.cardinality(); + char[] padded = new char[width]; + for (int op = 0, pp = 0; pp < width; op++) + { + if (residueMap.get(op)) { - SequenceI sq = seqs.get(i); - char[] padded = new char[realWidth]; - char[] original = sq.getSequence(); - for (int op = 0, pp = 0; pp < realWidth; op++) - { - if (gapMap.get(op)) - { - if (original.length > op) - padded[pp++] = original[op]; - else - padded[pp++] = '-'; - } - } - seqs.set(i, new Sequence(sq.getName(), padded)); + if (sequence.length > op) + padded[pp++] = sequence[op]; + else + padded[pp++] = '-'; } } - boolean[] gapMapArray = new boolean[gapMapSize]; - for (int i = 0; i < gapMapSize; i++) - gapMapArray[i] = gapMap.get(i); - return new AnnotationJob(seqs, gapMapArray, seqNames, start, end, minSize); + return padded; } } diff --git a/src/jalview/ws2/actions/annotation/AnnotationResult.java b/src/jalview/ws2/actions/annotation/AnnotationResult.java index 373ecbb..86614a1 100644 --- a/src/jalview/ws2/actions/annotation/AnnotationResult.java +++ b/src/jalview/ws2/actions/annotation/AnnotationResult.java @@ -19,17 +19,17 @@ public class AnnotationResult { final List annotations; - final boolean transferFeatures; + final boolean hasFeatures; final Map featureColours; final Map featureFilters; - public AnnotationResult(List annotations, boolean transferFeatures, + public AnnotationResult(List annotations, boolean hasFeatures, Map featureColours, Map featureFilters) { this.annotations = annotations; - this.transferFeatures = transferFeatures; + this.hasFeatures = hasFeatures; this.featureColours = featureColours; this.featureFilters = featureFilters; } @@ -39,9 +39,9 @@ public class AnnotationResult return annotations; } - public boolean getTransferFeatures() + public boolean getHasFeatures() { - return transferFeatures; + return hasFeatures; } public Map getFeatureColours() diff --git a/src/jalview/ws2/actions/annotation/AnnotationTask.java b/src/jalview/ws2/actions/annotation/AnnotationTask.java index 866f862..6cb5aa6 100644 --- a/src/jalview/ws2/actions/annotation/AnnotationTask.java +++ b/src/jalview/ws2/actions/annotation/AnnotationTask.java @@ -6,7 +6,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import jalview.analysis.AlignmentAnnotationUtils; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.datamodel.AlignmentAnnotation; @@ -25,7 +24,8 @@ import jalview.ws2.api.Credentials; import jalview.ws2.api.JobStatus; import jalview.ws2.client.api.AnnotationWebServiceClientI; -public class AnnotationTask extends BaseTask +public class AnnotationTask + extends BaseTask { private AnnotationWebServiceClientI client; @@ -36,8 +36,8 @@ public class AnnotationTask extends BaseTask private final AnnotatedCollectionI selectionGroup; public AnnotationTask(AnnotationWebServiceClientI client, - AnnotationAction action, List args, Credentials credentials, - AlignViewportI viewport) + AnnotationAction action, List args, + Credentials credentials, AlignViewportI viewport) { super(client, args, credentials); this.client = client; @@ -50,52 +50,57 @@ public class AnnotationTask extends BaseTask * Create and return a list of annotation jobs from the current state of the * viewport. Returned job are not started by this method and should be stored * in a field and started separately. - * + * * @return list of annotation jobs * @throws ServiceInputInvalidException * input data is not valid */ @Override - public List prepareJobs() throws ServiceInputInvalidException + public List prepareJobs() + throws ServiceInputInvalidException { - if (alignment == null || alignment.getWidth() <= 0 || - alignment.getSequences() == null) - throw new ServiceInputInvalidException("Alignment does not contain sequences"); - if (alignment.isNucleotide() && !action.doAllowNucleotide()) + if (alignment == null || alignment.getWidth() <= 0 + || alignment.getSequences() == null) throw new ServiceInputInvalidException( - action.getFullName() + " does not allow nucleotide sequences"); + "Alignment does not contain sequences"); + if (alignment.isNucleotide() && !action.doAllowNucleotide()) + throw new ServiceInputInvalidException(action.getFullName() + + " does not allow nucleotide sequences"); if (!alignment.isNucleotide() && !action.doAllowProtein()) throw new ServiceInputInvalidException( - action.getFullName() + " does not allow protein sequences"); + action.getFullName() + " does not allow protein sequences"); boolean bySequence = !action.isAlignmentAnalysis(); AnnotatedCollectionI inputSeqs = bySequence ? selectionGroup : null; - if (inputSeqs == null || inputSeqs.getWidth() <= 0 || - inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1) + if (inputSeqs == null || inputSeqs.getWidth() <= 0 + || inputSeqs.getSequences() == null + || inputSeqs.getSequences().size() < 1) inputSeqs = alignment; boolean submitGaps = action.isAlignmentAnalysis(); boolean requireAligned = action.getRequireAlignedSequences(); boolean filterSymbols = action.getFilterSymbols(); int minSize = action.getMinSequences(); AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence, - submitGaps, requireAligned, filterSymbols, minSize); + submitGaps, requireAligned, filterSymbols, minSize); if (!job.isInputValid()) { job.setStatus(JobStatus.INVALID); - throw new ServiceInputInvalidException("Annotation job has invalid input"); + throw new ServiceInputInvalidException( + "Annotation job has invalid input"); } job.setStatus(JobStatus.READY); return List.of(job); } @Override - protected AnnotationResult collectResult(List jobs) throws IOException + protected AnnotationResult collectResult(List jobs) + throws IOException { final Map featureColours = new HashMap<>(); final Map featureFilters = new HashMap<>(); var job = jobs.get(0); List returnedAnnot = client.attachAnnotations( - job.getServerJob(), job.getInputSequences(), featureColours, - featureFilters); + job.getServerJob(), job.getInputSequences(), featureColours, + featureFilters); /* TODO * copy over each annotation row returned and also defined on each * sequence, excluding regions not annotated due to gapMap/column @@ -104,34 +109,40 @@ public class AnnotationTask extends BaseTask udpateCalcId(returnedAnnot); for (AlignmentAnnotation ala : returnedAnnot) { - SequenceI aseq = null; - if (ala.sequenceRef != null) + SequenceI seq = (ala.sequenceRef == null) ? null + : job.seqNames.get(ala.sequenceRef.getName()); + if (job.gapMap != null && job.gapMap.length > 0) + ala.annotations = createGappedAnnotations(ala.annotations, + job.gapMap); + if (seq != null) { - SequenceI seq = job.seqNames.get(ala.sequenceRef.getName()); - aseq = seq.getRootDatasetSequence(); + int startRes = seq.findPosition(job.regionStart); + ala.createSequenceMapping(seq, startRes, false); } - ala.sequenceRef = aseq; - Annotation[] gappedAnnots = createGappedAnnotations(ala.annotations, job.start, job.gapMap); - ala.annotations = gappedAnnots; } boolean hasFeatures = false; for (SequenceI sq : job.getInputSequences()) { - if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty())) + if (!sq.getFeatures().hasFeatures() + && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty())) continue; hasFeatures = true; SequenceI seq = job.seqNames.get(sq.getName()); SequenceI datasetSeq = seq.getRootDatasetSequence(); - List sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end); + List sourceRange = findContiguousRanges(seq, + job.gapMap, job.regionStart, job.regionEnd); int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange); - Mapping mp = new Mapping(new MapList( - sourceStartEnd, new int[] - { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1)); + Mapping mp = new Mapping( + new MapList( + sourceStartEnd, + new int[] { datasetSeq.getStart(), datasetSeq.getEnd() }, + 1, 1)); datasetSeq.transferAnnotation(sq, mp); } - return new AnnotationResult(returnedAnnot, hasFeatures, featureColours, featureFilters); + return new AnnotationResult(returnedAnnot, hasFeatures, featureColours, + featureFilters); } /** @@ -141,22 +152,24 @@ public class AnnotationTask extends BaseTask { for (var annotation : annotations) { - if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty()) + if (annotation.getCalcId() == null + || annotation.getCalcId().isEmpty()) { annotation.setCalcId(action.getFullName()); } - annotation.autoCalculated = action.isAlignmentAnalysis() && - action.getWebService().isInteractive(); + annotation.autoCalculated = action.isAlignmentAnalysis() + && action.getWebService().isInteractive(); } } - private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap) + private Annotation[] createGappedAnnotations(Annotation[] annotations, + boolean[] gapMap) { - var size = Math.max(alignment.getWidth(), gapMap.length); + var size = Math.max(annotations.length, gapMap.length); Annotation[] gappedAnnotations = new Annotation[size]; - for (int p = 0, ap = start; ap < size; ap++) + for (int p = 0, ap = 0; ap < size; ap++) { - if (gapMap != null && gapMap.length > ap && !gapMap[ap]) + if (ap < gapMap.length && !gapMap[ap]) { gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN); } @@ -168,10 +181,12 @@ public class AnnotationTask extends BaseTask return gappedAnnotations; } - private List findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end) + // TODO: review ant test!!! + private List findContiguousRanges(SequenceI seq, + boolean[] gapMap, int start, int end) { if (gapMap == null || gapMap.length < end) - return List.of(seq.findPositions(start, end)); + return List.of(seq.findPositions(start + 1, end + 1)); List ranges = new ArrayList<>(); int lastcol = start, col = start; do diff --git a/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java b/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java index a7202e9..8fc07a5 100644 --- a/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java +++ b/src/jalview/ws2/gui/AnnotationServiceGuiHandler.java @@ -66,7 +66,7 @@ public class AnnotationServiceGuiHandler { if (result == null) return; - if (result.getTransferFeatures()) + if (result.getHasFeatures()) { alignFrame.getViewport().applyFeaturesStyle(new FeatureSettingsAdapter() { -- 1.7.10.2