JAL-4199 Organise annotation task imports and fields
[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 AlignViewportI viewport;
35
36   public AnnotationTask(AnnotationWebServiceClientI client,
37       AnnotationAction action, List<ArgumentI> args, Credentials credentials,
38       AlignViewportI viewport)
39   {
40     super(client, args, credentials);
41     this.client = client;
42     this.action = action;
43     this.viewport = viewport;
44   }
45
46   /**
47    * Create and return a list of annotation jobs from the current state of the
48    * viewport. Returned job are not started by this method and should be stored
49    * in a field and started separately.
50    * 
51    * @return list of annotation jobs
52    * @throws ServiceInputInvalidException
53    *           input data is not valid
54    */
55   @Override
56   public List<AnnotationJob> prepareJobs() throws ServiceInputInvalidException
57   {
58     AlignmentI alignment = viewport.getAlignment();
59     if (alignment == null || alignment.getWidth() <= 0 ||
60         alignment.getSequences() == null)
61       throw new ServiceInputInvalidException("Alignment does not contain sequences");
62     if (alignment.isNucleotide() && !action.doAllowNucleotide())
63       throw new ServiceInputInvalidException(
64           action.getFullName() + " does not allow nucleotide sequences");
65     if (!alignment.isNucleotide() && !action.doAllowProtein())
66       throw new ServiceInputInvalidException(
67           action.getFullName() + " does not allow protein sequences");
68     boolean bySequence = !action.isAlignmentAnalysis();
69     AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
70     if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
71         inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
72       inputSeqs = alignment;
73     boolean submitGaps = action.isAlignmentAnalysis();
74     boolean requireAligned = action.getRequireAlignedSequences();
75     boolean filterSymbols = action.getFilterSymbols();
76     int minSize = action.getMinSequences();
77     AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
78         submitGaps, requireAligned, filterSymbols, minSize);
79     if (!job.isInputValid())
80     {
81       job.setStatus(JobStatus.INVALID);
82       throw new ServiceInputInvalidException("Annotation job has invalid input");
83     }
84     job.setStatus(JobStatus.READY);
85     return List.of(job);
86   }
87
88   @Override
89   protected AnnotationResult collectResult(List<AnnotationJob> jobs) throws IOException
90   {
91     final Map<String, FeatureColourI> featureColours = new HashMap<>();
92     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
93     var job = jobs.get(0);
94     List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
95         job.getServerJob(), job.getInputSequences(), featureColours,
96         featureFilters);
97     /* TODO
98      * copy over each annotation row returned and also defined on each
99      * sequence, excluding regions not annotated due to gapMap/column
100      * visibility */
101
102     udpateCalcId(returnedAnnot);
103     int graphGroup = viewport.getAlignment().getLastGraphGroup();
104     shiftGraphGroup(returnedAnnot, graphGroup);
105     List<AlignmentAnnotation> annotations = new ArrayList<>();
106     for (AlignmentAnnotation ala : returnedAnnot)
107     {
108       SequenceI aseq = null;
109       if (ala.sequenceRef != null) {
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       AlignmentAnnotation newAnnot = viewport.getAlignment()
118           .updateFromOrCopyAnnotation(ala);
119       if (aseq != null)
120       {
121         aseq.addAlignmentAnnotation(newAnnot);
122         newAnnot.adjustForAlignment();
123         AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
124             newAnnot, newAnnot.label, newAnnot.getCalcId());
125       }
126       annotations.add(newAnnot);
127     }
128
129     boolean hasFeatures = false;
130     for (SequenceI sq : job.getInputSequences())
131     {
132       if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
133         continue;
134       hasFeatures = true;
135       SequenceI seq = job.seqNames.get(sq.getName());
136       SequenceI datasetSeq = seq.getRootDatasetSequence();
137       List<ContiguousI> sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end);
138       int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange);
139       Mapping mp = new Mapping(new MapList(
140           sourceStartEnd, new int[]
141           { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1));
142       datasetSeq.transferAnnotation(sq, mp);
143     }
144
145     return new AnnotationResult(annotations, hasFeatures, featureColours, featureFilters);
146   }
147
148   /**
149    * Updates calcId on provided annotations if not already set.
150    */
151   public void udpateCalcId(Iterable<AlignmentAnnotation> annotations)
152   {
153     for (var annotation : annotations)
154     {
155       if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty())
156       {
157         annotation.setCalcId(action.getFullName());
158       }
159       annotation.autoCalculated = action.isAlignmentAnalysis() &&
160           action.getWebService().isInteractive();
161     }
162   }
163
164   private static void shiftGraphGroup(Iterable<AlignmentAnnotation> annotations, int shift)
165   {
166     for (AlignmentAnnotation ala : annotations)
167     {
168       if (ala.graphGroup > 0)
169       {
170         ala.graphGroup += shift;
171       }
172     }
173   }
174
175   private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap)
176   {
177     var size = Math.max(viewport.getAlignment().getWidth(), gapMap.length);
178     Annotation[] gappedAnnotations = new Annotation[size];
179     for (int p = 0, ap = start; ap < size; ap++)
180     {
181       if (gapMap != null && gapMap.length > ap && !gapMap[ap])
182       {
183         gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN);
184       }
185       else if (p < annotations.length)
186       {
187         gappedAnnotations[ap] = annotations[p++];
188       }
189     }
190     return gappedAnnotations;
191   }
192
193   private List<ContiguousI> findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end)
194   {
195     if (gapMap == null || gapMap.length < end)
196       return List.of(seq.findPositions(start, end));
197     List<ContiguousI> ranges = new ArrayList<>();
198     int lastcol = start, col = start;
199     do
200     {
201       if (col == end || !gapMap[col])
202       {
203         if (lastcol < col)
204           ranges.add(seq.findPositions(lastcol, col));
205         lastcol = col + 1;
206       }
207     } while (++col <= end);
208     return ranges;
209   }
210
211   @Override
212   public String toString()
213   {
214     var status = taskStatus != null ? taskStatus.name() : "UNSET";
215     return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);
216   }
217 }