package jalview.ws2.actions.annotation; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.AnnotatedCollectionI; import jalview.datamodel.Annotation; import jalview.datamodel.ContiguousI; import jalview.datamodel.Mapping; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSetI; import jalview.util.MapList; import jalview.ws.params.ArgumentI; import jalview.ws2.actions.BaseTask; import jalview.ws2.actions.ServiceInputInvalidException; import jalview.ws2.api.Credentials; import jalview.ws2.api.JobStatus; import jalview.ws2.client.api.AnnotationWebServiceClientI; public class AnnotationTask extends BaseTask { private AnnotationWebServiceClientI client; private final AnnotationAction action; private final AlignmentI alignment; private final AnnotatedCollectionI selectionGroup; public AnnotationTask(AnnotationWebServiceClientI client, AnnotationAction action, List args, Credentials credentials, AlignViewportI viewport) { super(client, args, credentials); this.client = client; this.action = action; this.alignment = viewport.getAlignment(); this.selectionGroup = viewport.getSelectionGroup(); } /** * 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 { if (alignment == null || alignment.getWidth() <= 0 || alignment.getSequences() == null) throw new ServiceInputInvalidException( "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"); boolean bySequence = !action.isAlignmentAnalysis(); AnnotatedCollectionI inputSeqs = bySequence ? selectionGroup : null; 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); if (!job.isInputValid()) { job.setStatus(JobStatus.INVALID); throw new ServiceInputInvalidException( "Annotation job has invalid input"); } job.setStatus(JobStatus.READY); return List.of(job); } @Override 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); /* TODO * copy over each annotation row returned and also defined on each * sequence, excluding regions not annotated due to gapMap/column * visibility */ udpateCalcId(returnedAnnot); for (AlignmentAnnotation ala : returnedAnnot) { 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) { int startRes = seq.findPosition(job.regionStart); ala.createSequenceMapping(seq, startRes, false); } } boolean hasFeatures = false; for (SequenceI sq : job.getInputSequences()) { 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(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)); datasetSeq.transferAnnotation(sq, mp); } return new AnnotationResult(returnedAnnot, hasFeatures, featureColours, featureFilters); } /** * Updates calcId on provided annotations if not already set. */ public void udpateCalcId(Iterable annotations) { for (var annotation : annotations) { if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty()) { annotation.setCalcId(action.getFullName()); } annotation.autoCalculated = action.isAlignmentAnalysis() && action.getWebService().isInteractive(); } } // TODO: review and test // may produce wrong output if annotations longer than gapMap private Annotation[] createGappedAnnotations(Annotation[] annotations, boolean[] gapMap) { var size = Math.max(annotations.length, gapMap.length); Annotation[] gappedAnnotations = new Annotation[size]; for (int p = 0, ap = 0; ap < size; ap++) { if (ap < gapMap.length && !gapMap[ap]) { gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN); } else if (p < annotations.length) { gappedAnnotations[ap] = annotations[p++]; } } return gappedAnnotations; } // 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 + 1, end + 1)); List ranges = new ArrayList<>(); int lastcol = start, col = start; do { if (col == end || !gapMap[col]) { if (lastcol < col) ranges.add(seq.findPositions(lastcol, col)); lastcol = col + 1; } } while (++col <= end); return ranges; } }