JAL-4199 Replace AbstractPollingTask with BaseTask
[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.Collections;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9
10 import jalview.analysis.AlignmentAnnotationUtils;
11 import jalview.api.AlignCalcManagerI2;
12 import jalview.api.AlignCalcWorkerI;
13 import jalview.api.AlignViewportI;
14 import jalview.api.FeatureColourI;
15 import jalview.api.PollableAlignCalcWorkerI;
16 import jalview.bin.Cache;
17 import jalview.bin.Console;
18 import jalview.datamodel.AlignmentAnnotation;
19 import jalview.datamodel.AlignmentI;
20 import jalview.datamodel.AnnotatedCollectionI;
21 import jalview.datamodel.Annotation;
22 import jalview.datamodel.ContiguousI;
23 import jalview.datamodel.Mapping;
24 import jalview.datamodel.SequenceI;
25 import jalview.datamodel.features.FeatureMatcherSetI;
26 import jalview.schemes.FeatureSettingsAdapter;
27 import jalview.util.ArrayUtils;
28 import jalview.util.MapList;
29 import jalview.util.MathUtils;
30 import jalview.util.Pair;
31 import jalview.workers.AlignCalcWorker;
32 import jalview.ws.params.ArgumentI;
33 import jalview.ws2.actions.BaseJob;
34 import jalview.ws2.actions.BaseTask;
35 import jalview.ws2.actions.ServiceInputInvalidException;
36 import jalview.ws2.actions.api.JobI;
37 import jalview.ws2.actions.api.TaskEventListener;
38 import jalview.ws2.actions.api.TaskI;
39 import jalview.ws2.api.Credentials;
40 import jalview.ws2.api.JobStatus;
41 import jalview.ws2.api.WebServiceJobHandle;
42 import jalview.ws2.client.api.AnnotationWebServiceClientI;
43 import jalview.ws2.helpers.DelegateJobEventListener;
44 import jalview.ws2.helpers.TaskEventSupport;
45
46 public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
47 {
48   private AnnotationWebServiceClientI client;
49
50   private final AnnotationAction action;
51
52   private final AlignViewportI viewport;
53
54   private JobStatus taskStatus = null;
55
56   private AlignCalcWorkerAdapter worker = null;
57
58   private DelegateJobEventListener<AnnotationResult> jobEventHandler;
59
60   public AnnotationTask(AnnotationWebServiceClientI client,
61       AnnotationAction action, List<ArgumentI> args, Credentials credentials,
62       AlignViewportI viewport)
63   {
64     super(client, args, credentials);
65     this.client = client;
66     this.action = action;
67     this.viewport = viewport;
68   }
69
70   // public void start(AlignCalcManagerI2 calcManager)
71   // {
72   // if (this.worker != null)
73   // throw new IllegalStateException("task already started");
74   // this.worker = new AlignCalcWorkerAdapter(calcManager);
75   // if (taskStatus != JobStatus.CANCELLED)
76   // {
77   // List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
78   // AlignCalcWorkerAdapter.class);
79   // for (var worker : oldWorkers)
80   // {
81   // if (action.getWebService().getName().equalsIgnoreCase(
82   // ((AlignCalcWorkerAdapter) worker).getServiceName()))
83   // {
84   // // remove interactive workers for the same service.
85   // calcManager.removeWorker(worker);
86   // calcManager.cancelWorker(worker);
87   // }
88   // }
89   // if (action.getWebService().isInteractive())
90   // calcManager.registerWorker(worker);
91   // else
92   // calcManager.startWorker(worker);
93   // }
94   // }
95
96   /**
97    * Create and return a list of annotation jobs from the current state of the
98    * viewport. Returned job are not started by this method and should be stored
99    * in a field and started separately.
100    * 
101    * @return list of annotation jobs
102    * @throws ServiceInputInvalidException
103    *           input data is not valid
104    */
105   @Override
106   public List<AnnotationJob> prepareJobs() throws ServiceInputInvalidException
107   {
108     AlignmentI alignment = viewport.getAlignment();
109     if (alignment == null || alignment.getWidth() <= 0 ||
110         alignment.getSequences() == null)
111       throw new ServiceInputInvalidException("Alignment does not contain sequences");
112     if (alignment.isNucleotide() && !action.doAllowNucleotide())
113       throw new ServiceInputInvalidException(
114           action.getFullName() + " does not allow nucleotide sequences");
115     if (!alignment.isNucleotide() && !action.doAllowProtein())
116       throw new ServiceInputInvalidException(
117           action.getFullName() + " does not allow protein sequences");
118     boolean bySequence = !action.isAlignmentAnalysis();
119     AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
120     if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
121         inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
122       inputSeqs = alignment;
123     boolean submitGaps = action.isAlignmentAnalysis();
124     boolean requireAligned = action.getRequireAlignedSequences();
125     boolean filterSymbols = action.getFilterSymbols();
126     int minSize = action.getMinSequences();
127     AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
128         submitGaps, requireAligned, filterSymbols, minSize);
129     if (!job.isInputValid())
130     {
131       job.setStatus(JobStatus.INVALID);
132       throw new ServiceInputInvalidException("Annotation job has invalid input");
133     }
134     job.setStatus(JobStatus.READY);
135     return List.of(job);
136   }
137
138   @Override
139   protected AnnotationResult collectResult(List<AnnotationJob> jobs) throws IOException
140   {
141     final Map<String, FeatureColourI> featureColours = new HashMap<>();
142     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
143     var job = jobs.get(0);
144     List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
145         job.getServerJob(), job.getInputSequences(), featureColours,
146         featureFilters);
147     /* TODO
148      * copy over each annotation row returned and also defined on each
149      * sequence, excluding regions not annotated due to gapMap/column
150      * visibility */
151
152     udpateCalcId(returnedAnnot);
153     int graphGroup = getNextGraphGroup(viewport.getAlignment());
154     shiftGraphGroup(returnedAnnot, graphGroup);
155     List<AlignmentAnnotation> annotations = new ArrayList<>();
156     for (AlignmentAnnotation ala : returnedAnnot)
157     {
158       SequenceI seq = job.seqNames.get(ala.sequenceRef.getName());
159       SequenceI aseq = getRootDatasetSequence(seq);
160       Annotation[] gappedAnnots = createGappedAnnotations(ala.annotations, job.start, job.gapMap);
161       ala.sequenceRef = aseq;
162       ala.annotations = gappedAnnots;
163
164       AlignmentAnnotation newAnnot = viewport.getAlignment()
165           .updateFromOrCopyAnnotation(ala);
166       if (aseq != null) // I suspect it's always true
167       {
168         aseq.addAlignmentAnnotation(newAnnot);
169         newAnnot.adjustForAlignment();
170         AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
171             newAnnot, newAnnot.label, newAnnot.getCalcId());
172       }
173       annotations.add(newAnnot);
174     }
175
176     boolean hasFeatures = false;
177     for (SequenceI sq : job.getInputSequences())
178     {
179       if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
180         continue;
181       hasFeatures = true;
182       SequenceI seq = job.seqNames.get(sq.getName());
183       SequenceI datasetSeq = getRootDatasetSequence(seq);
184       List<ContiguousI> sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end);
185       int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange);
186       Mapping mp = new Mapping(new MapList(
187           sourceStartEnd, new int[]
188           { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1));
189       datasetSeq.transferAnnotation(sq, mp);
190     }
191
192     return new AnnotationResult(annotations, hasFeatures, featureColours, featureFilters);
193   }
194
195   /**
196    * Updates calcId on provided annotations if not already set.
197    */
198   public void udpateCalcId(Iterable<AlignmentAnnotation> annotations)
199   {
200     for (var annotation : annotations)
201     {
202       if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty())
203       {
204         annotation.setCalcId(action.getFullName());
205       }
206       annotation.autoCalculated = action.isAlignmentAnalysis() &&
207           action.getWebService().isInteractive();
208     }
209   }
210
211   private static int getNextGraphGroup(AlignmentI alignment)
212   {
213     if (alignment == null || alignment.getAlignmentAnnotation() == null)
214       return 1;
215     int graphGroup = 1;
216     for (AlignmentAnnotation ala : alignment.getAlignmentAnnotation())
217       graphGroup = Math.max(graphGroup, ala.graphGroup);
218     return graphGroup;
219   }
220
221   private static void shiftGraphGroup(Iterable<AlignmentAnnotation> annotations, int shift)
222   {
223     for (AlignmentAnnotation ala : annotations)
224     {
225       if (ala.graphGroup > 0)
226       {
227         ala.graphGroup += shift;
228       }
229     }
230   }
231
232   private static SequenceI getRootDatasetSequence(SequenceI sequence)
233   {
234     while (sequence.getDatasetSequence() != null)
235     {
236       sequence = sequence.getDatasetSequence();
237     }
238     return sequence;
239   }
240
241   private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap)
242   {
243     var size = Math.max(viewport.getAlignment().getWidth(), gapMap.length);
244     Annotation[] gappedAnnotations = new Annotation[size];
245     for (int p = 0, ap = start; ap < size; ap++)
246     {
247       if (gapMap != null && gapMap.length > ap && !gapMap[ap])
248       {
249         gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN);
250       }
251       else if (p < annotations.length)
252       {
253         gappedAnnotations[ap] = annotations[p++];
254       }
255     }
256     return gappedAnnotations;
257   }
258
259   private List<ContiguousI> findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end)
260   {
261     if (gapMap == null || gapMap.length < end)
262       return List.of(seq.findPositions(start, end));
263     List<ContiguousI> ranges = new ArrayList<>();
264     int lastcol = start, col = start;
265     do
266     {
267       if (col == end || !gapMap[col])
268       {
269         if (lastcol < col)
270           ranges.add(seq.findPositions(lastcol, col));
271         lastcol = col + 1;
272       }
273     } while (++col <= end);
274     return ranges;
275   }
276
277   @Override
278   public String toString()
279   {
280     var status = taskStatus != null ? taskStatus.name() : "UNSET";
281     return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);
282   }
283 }