JAL-4241 Fix annotation and feature alignment with selection
[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.api.AlignViewportI;
10 import jalview.api.FeatureColourI;
11 import jalview.datamodel.AlignmentAnnotation;
12 import jalview.datamodel.AlignmentI;
13 import jalview.datamodel.AnnotatedCollectionI;
14 import jalview.datamodel.Annotation;
15 import jalview.datamodel.ContiguousI;
16 import jalview.datamodel.Mapping;
17 import jalview.datamodel.SequenceI;
18 import jalview.datamodel.features.FeatureMatcherSetI;
19 import jalview.util.MapList;
20 import jalview.ws.params.ArgumentI;
21 import jalview.ws2.actions.BaseTask;
22 import jalview.ws2.actions.ServiceInputInvalidException;
23 import jalview.ws2.api.Credentials;
24 import jalview.ws2.api.JobStatus;
25 import jalview.ws2.client.api.AnnotationWebServiceClientI;
26
27 public class AnnotationTask
28         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,
40           Credentials credentials, 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()
60           throws ServiceInputInvalidException
61   {
62     if (alignment == null || alignment.getWidth() <= 0
63             || alignment.getSequences() == null)
64       throw new ServiceInputInvalidException(
65               "Alignment does not contain sequences");
66     if (alignment.isNucleotide() && !action.doAllowNucleotide())
67       throw new ServiceInputInvalidException(action.getFullName()
68               + " does not allow nucleotide sequences");
69     if (!alignment.isNucleotide() && !action.doAllowProtein())
70       throw new ServiceInputInvalidException(
71               action.getFullName() + " does not allow protein sequences");
72     boolean bySequence = !action.isAlignmentAnalysis();
73     AnnotatedCollectionI inputSeqs = bySequence ? selectionGroup : null;
74     if (inputSeqs == null || inputSeqs.getWidth() <= 0
75             || inputSeqs.getSequences() == null
76             || inputSeqs.getSequences().size() < 1)
77       inputSeqs = alignment;
78     boolean submitGaps = action.isAlignmentAnalysis();
79     boolean requireAligned = action.getRequireAlignedSequences();
80     boolean filterSymbols = action.getFilterSymbols();
81     int minSize = action.getMinSequences();
82     AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
83             submitGaps, requireAligned, filterSymbols, minSize);
84     if (!job.isInputValid())
85     {
86       job.setStatus(JobStatus.INVALID);
87       throw new ServiceInputInvalidException(
88               "Annotation job has invalid input");
89     }
90     job.setStatus(JobStatus.READY);
91     return List.of(job);
92   }
93
94   @Override
95   protected AnnotationResult collectResult(List<AnnotationJob> jobs)
96           throws IOException
97   {
98     final Map<String, FeatureColourI> featureColours = new HashMap<>();
99     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
100     var job = jobs.get(0);
101     List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
102             job.getServerJob(), job.getInputSequences(), featureColours,
103             featureFilters);
104     /* TODO
105      * copy over each annotation row returned and also defined on each
106      * sequence, excluding regions not annotated due to gapMap/column
107      * visibility */
108
109     udpateCalcId(returnedAnnot);
110     for (AlignmentAnnotation ala : returnedAnnot)
111     {
112       SequenceI seq = (ala.sequenceRef == null) ? null
113               : job.seqNames.get(ala.sequenceRef.getName());
114       if (job.gapMap != null && job.gapMap.length > 0)
115         ala.annotations = createGappedAnnotations(ala.annotations,
116                 job.gapMap);
117       if (seq != null)
118       {
119         int startRes = seq.findPosition(job.regionStart);
120         ala.createSequenceMapping(seq, startRes, false);
121       }
122     }
123
124     boolean hasFeatures = false;
125     for (SequenceI sq : job.getInputSequences())
126     {
127       if (!sq.getFeatures().hasFeatures()
128               && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
129         continue;
130       hasFeatures = true;
131       SequenceI seq = job.seqNames.get(sq.getName());
132       SequenceI datasetSeq = seq.getRootDatasetSequence();
133       List<ContiguousI> sourceRange = findContiguousRanges(seq,
134               job.gapMap, job.regionStart, job.regionEnd);
135       int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange);
136       Mapping mp = new Mapping(
137           new MapList(
138               sourceStartEnd,
139               new int[] { datasetSeq.getStart(), datasetSeq.getEnd() },
140               1, 1));
141       datasetSeq.transferAnnotation(sq, mp);
142     }
143
144     return new AnnotationResult(returnedAnnot, hasFeatures, featureColours,
145             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
156               || annotation.getCalcId().isEmpty())
157       {
158         annotation.setCalcId(action.getFullName());
159       }
160       annotation.autoCalculated = action.isAlignmentAnalysis()
161               && action.getWebService().isInteractive();
162     }
163   }
164
165   private Annotation[] createGappedAnnotations(Annotation[] annotations,
166           boolean[] gapMap)
167   {
168     var size = Math.max(annotations.length, gapMap.length);
169     Annotation[] gappedAnnotations = new Annotation[size];
170     for (int p = 0, ap = 0; ap < size; ap++)
171     {
172       if (ap < gapMap.length && !gapMap[ap])
173       {
174         gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN);
175       }
176       else if (p < annotations.length)
177       {
178         gappedAnnotations[ap] = annotations[p++];
179       }
180     }
181     return gappedAnnotations;
182   }
183
184   // TODO: review ant test!!!
185   private List<ContiguousI> findContiguousRanges(SequenceI seq,
186           boolean[] gapMap, int start, int end)
187   {
188     if (gapMap == null || gapMap.length < end)
189       return List.of(seq.findPositions(start + 1, end + 1));
190     List<ContiguousI> ranges = new ArrayList<>();
191     int lastcol = start, col = start;
192     do
193     {
194       if (col == end || !gapMap[col])
195       {
196         if (lastcol < col)
197           ranges.add(seq.findPositions(lastcol, col));
198         lastcol = col + 1;
199       }
200     } while (++col <= end);
201     return ranges;
202   }
203 }