866f862e1ad10957c0e9a52f4d9a21e5cd7f788f
[jalview.git] / src / jalview / ws2 / actions / annotation / AnnotationTask.java
1 package jalview.ws2.actions.annotation;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8
9 import jalview.analysis.AlignmentAnnotationUtils;
10 import jalview.api.AlignViewportI;
11 import jalview.api.FeatureColourI;
12 import jalview.datamodel.AlignmentAnnotation;
13 import jalview.datamodel.AlignmentI;
14 import jalview.datamodel.AnnotatedCollectionI;
15 import jalview.datamodel.Annotation;
16 import jalview.datamodel.ContiguousI;
17 import jalview.datamodel.Mapping;
18 import jalview.datamodel.SequenceI;
19 import jalview.datamodel.features.FeatureMatcherSetI;
20 import jalview.util.MapList;
21 import jalview.ws.params.ArgumentI;
22 import jalview.ws2.actions.BaseTask;
23 import jalview.ws2.actions.ServiceInputInvalidException;
24 import jalview.ws2.api.Credentials;
25 import jalview.ws2.api.JobStatus;
26 import jalview.ws2.client.api.AnnotationWebServiceClientI;
27
28 public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
29 {
30   private AnnotationWebServiceClientI client;
31
32   private final AnnotationAction action;
33
34   private final AlignmentI alignment;
35
36   private final AnnotatedCollectionI selectionGroup;
37
38   public AnnotationTask(AnnotationWebServiceClientI client,
39       AnnotationAction action, List<ArgumentI> args, Credentials credentials,
40       AlignViewportI viewport)
41   {
42     super(client, args, credentials);
43     this.client = client;
44     this.action = action;
45     this.alignment = viewport.getAlignment();
46     this.selectionGroup = viewport.getSelectionGroup();
47   }
48
49   /**
50    * Create and return a list of annotation jobs from the current state of the
51    * viewport. Returned job are not started by this method and should be stored
52    * in a field and started separately.
53    * 
54    * @return list of annotation jobs
55    * @throws ServiceInputInvalidException
56    *           input data is not valid
57    */
58   @Override
59   public List<AnnotationJob> prepareJobs() throws ServiceInputInvalidException
60   {
61     if (alignment == null || alignment.getWidth() <= 0 ||
62         alignment.getSequences() == null)
63       throw new ServiceInputInvalidException("Alignment does not contain sequences");
64     if (alignment.isNucleotide() && !action.doAllowNucleotide())
65       throw new ServiceInputInvalidException(
66           action.getFullName() + " does not allow nucleotide sequences");
67     if (!alignment.isNucleotide() && !action.doAllowProtein())
68       throw new ServiceInputInvalidException(
69           action.getFullName() + " does not allow protein sequences");
70     boolean bySequence = !action.isAlignmentAnalysis();
71     AnnotatedCollectionI inputSeqs = bySequence ? selectionGroup : null;
72     if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
73         inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
74       inputSeqs = alignment;
75     boolean submitGaps = action.isAlignmentAnalysis();
76     boolean requireAligned = action.getRequireAlignedSequences();
77     boolean filterSymbols = action.getFilterSymbols();
78     int minSize = action.getMinSequences();
79     AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
80         submitGaps, requireAligned, filterSymbols, minSize);
81     if (!job.isInputValid())
82     {
83       job.setStatus(JobStatus.INVALID);
84       throw new ServiceInputInvalidException("Annotation job has invalid input");
85     }
86     job.setStatus(JobStatus.READY);
87     return List.of(job);
88   }
89
90   @Override
91   protected AnnotationResult collectResult(List<AnnotationJob> jobs) throws IOException
92   {
93     final Map<String, FeatureColourI> featureColours = new HashMap<>();
94     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
95     var job = jobs.get(0);
96     List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
97         job.getServerJob(), job.getInputSequences(), featureColours,
98         featureFilters);
99     /* TODO
100      * copy over each annotation row returned and also defined on each
101      * sequence, excluding regions not annotated due to gapMap/column
102      * visibility */
103
104     udpateCalcId(returnedAnnot);
105     for (AlignmentAnnotation ala : returnedAnnot)
106     {
107       SequenceI aseq = null;
108       if (ala.sequenceRef != null)
109       {
110         SequenceI seq = job.seqNames.get(ala.sequenceRef.getName());
111         aseq = seq.getRootDatasetSequence();
112       }
113       ala.sequenceRef = aseq;
114       Annotation[] gappedAnnots = createGappedAnnotations(ala.annotations, job.start, job.gapMap);
115       ala.annotations = gappedAnnots;
116     }
117
118     boolean hasFeatures = false;
119     for (SequenceI sq : job.getInputSequences())
120     {
121       if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
122         continue;
123       hasFeatures = true;
124       SequenceI seq = job.seqNames.get(sq.getName());
125       SequenceI datasetSeq = seq.getRootDatasetSequence();
126       List<ContiguousI> sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end);
127       int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange);
128       Mapping mp = new Mapping(new MapList(
129           sourceStartEnd, new int[]
130           { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1));
131       datasetSeq.transferAnnotation(sq, mp);
132     }
133
134     return new AnnotationResult(returnedAnnot, hasFeatures, featureColours, featureFilters);
135   }
136
137   /**
138    * Updates calcId on provided annotations if not already set.
139    */
140   public void udpateCalcId(Iterable<AlignmentAnnotation> annotations)
141   {
142     for (var annotation : annotations)
143     {
144       if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty())
145       {
146         annotation.setCalcId(action.getFullName());
147       }
148       annotation.autoCalculated = action.isAlignmentAnalysis() &&
149           action.getWebService().isInteractive();
150     }
151   }
152
153   private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap)
154   {
155     var size = Math.max(alignment.getWidth(), gapMap.length);
156     Annotation[] gappedAnnotations = new Annotation[size];
157     for (int p = 0, ap = start; ap < size; ap++)
158     {
159       if (gapMap != null && gapMap.length > ap && !gapMap[ap])
160       {
161         gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN);
162       }
163       else if (p < annotations.length)
164       {
165         gappedAnnotations[ap] = annotations[p++];
166       }
167     }
168     return gappedAnnotations;
169   }
170
171   private List<ContiguousI> findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end)
172   {
173     if (gapMap == null || gapMap.length < end)
174       return List.of(seq.findPositions(start, end));
175     List<ContiguousI> ranges = new ArrayList<>();
176     int lastcol = start, col = start;
177     do
178     {
179       if (col == end || !gapMap[col])
180       {
181         if (lastcol < col)
182           ranges.add(seq.findPositions(lastcol, col));
183         lastcol = col + 1;
184       }
185     } while (++col <= end);
186     return ranges;
187   }
188 }