d71e83972b19f9f3b580b61a48792bedf35b96d7
[jalview.git] / src / jalview / ws2 / operations / AlignmentOperation.java
1 package jalview.ws2.operations;
2
3 import static java.lang.String.format;
4
5 import java.awt.event.MouseAdapter;
6 import java.awt.event.MouseEvent;
7 import java.io.IOException;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.Hashtable;
12 import java.util.LinkedHashMap;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.concurrent.CompletionStage;
17
18 import javax.swing.JMenu;
19 import javax.swing.JMenuItem;
20 import javax.swing.ToolTipManager;
21
22 import jalview.analysis.AlignSeq;
23 import jalview.analysis.AlignmentSorter;
24 import jalview.analysis.SeqsetUtils;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignedCodonFrame;
27 import jalview.datamodel.Alignment;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.AlignmentOrder;
30 import jalview.datamodel.AlignmentView;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.SequenceI;
33 import jalview.datamodel.Sequence;
34 import jalview.gui.AlignFrame;
35 import jalview.gui.AlignViewport;
36 import jalview.gui.Desktop;
37 import jalview.gui.JvSwingUtils;
38 import jalview.gui.WebserviceInfo;
39 import jalview.gui.WsJobParameters;
40 import jalview.util.MathUtils;
41 import jalview.util.MessageManager;
42 import jalview.ws.params.ArgumentI;
43 import jalview.ws.params.WsParamSetI;
44 import jalview.ws2.MenuEntryProviderI;
45 import jalview.ws2.ResultSupplier;
46 import jalview.ws2.WSJob;
47 import jalview.ws2.WSJobStatus;
48 import jalview.ws2.WebServiceExecutor;
49 import jalview.ws2.WebServiceI;
50 import jalview.ws2.WebServiceInfoUpdater;
51 import jalview.ws2.WebServiceWorkerI;
52 import jalview.ws2.utils.WSJobList;
53
54 /**
55  *
56  * @author mmwarowny
57  *
58  */
59 public class AlignmentOperation implements Operation
60 {
61   final WebServiceI service;
62
63   final ResultSupplier<AlignmentI> supplier;
64
65   public AlignmentOperation(WebServiceI service,
66           ResultSupplier<AlignmentI> supplier)
67   {
68     this.service = service;
69     this.supplier = supplier;
70   }
71
72   @Override
73   public String getTypeName()
74   {
75     return "Multiple Sequence Alignment";
76   }
77
78   @Override
79   public int getMinSequences()
80   {
81     return 2;
82   }
83
84   @Override
85   public int getMaxSequences()
86   {
87     return Integer.MAX_VALUE;
88   }
89
90   @Override
91   public boolean isProteinOperation()
92   {
93     return true;
94   }
95
96   @Override
97   public boolean isNucleotideOperation()
98   {
99     return true;
100   }
101
102   @Override
103   public boolean canSubmitGaps()
104   {
105     // hack copied from original jabaws code, don't blame me
106     return service.getName().contains("lustal");
107   }
108
109   @Override
110   public MenuEntryProviderI getMenuBuilder()
111   {
112     return this::buildMenu;
113   }
114
115   protected void buildMenu(JMenu parent, AlignFrame frame)
116   {
117     if (canSubmitGaps())
118     {
119       var alignSubmenu = new JMenu(service.getName());
120       buildMenu(alignSubmenu, frame, false);
121       parent.add(alignSubmenu);
122       var realignSubmenu = new JMenu(MessageManager.formatMessage(
123               "label.realign_with_params", service.getName()));
124       realignSubmenu.setToolTipText(MessageManager
125               .getString("label.align_sequences_to_existing_alignment"));
126       buildMenu(realignSubmenu, frame, true);
127       parent.add(realignSubmenu);
128     }
129     else
130     {
131       buildMenu(parent, frame, false);
132     }
133   }
134
135   protected void buildMenu(JMenu parent, AlignFrame frame,
136           boolean submitGaps)
137   {
138     final String action = submitGaps ? "Align" : "Realign";
139     final var calcName = service.getName();
140
141     final AlignmentView msa = frame.gatherSequencesForAlignment();
142     final AlignViewport viewport = frame.getViewport();
143     final AlignmentI alignment = frame.getViewport().getAlignment();
144     String title = frame.getTitle();
145     WebServiceExecutor executor = frame.getViewport().getWSExecutor();
146     {
147       var item = new JMenuItem(MessageManager.formatMessage(
148               "label.calcname_with_default_settings", calcName));
149       item.setToolTipText(MessageManager
150               .formatMessage("label.action_with_default_settings", action));
151       item.addActionListener((event) -> {
152         if (msa != null)
153         {
154           WebServiceWorkerI worker = new AlignmentWorker(msa,
155                   Collections.emptyList(), title, submitGaps, true,
156                   alignment, viewport);
157           executor.submit(worker);
158         }
159       });
160       parent.add(item);
161     }
162
163     if (service.hasParameters())
164     {
165       var item = new JMenuItem(
166               MessageManager.getString("label.edit_settings_and_run"));
167       item.setToolTipText(MessageManager.getString(
168               "label.view_and_change_parameters_before_alignment"));
169       item.addActionListener((event) -> {
170         if (msa != null)
171         {
172           openEditParamsDialog(service, null, null)
173                   .thenAcceptAsync((arguments) -> {
174                     if (arguments != null)
175                     {
176                       WebServiceWorkerI worker = new AlignmentWorker(msa,
177                               arguments, title, submitGaps, true, alignment,
178                               viewport);
179                       executor.submit(worker);
180                     }
181                   });
182         }
183       });
184       parent.add(item);
185     }
186
187     var presets = service.getParamStore().getPresets();
188     if (presets != null && presets.size() > 0)
189     {
190       final var presetList = new JMenu(MessageManager
191               .formatMessage("label.run_with_preset_params", calcName));
192       final var showToolTipFor = ToolTipManager.sharedInstance()
193               .getDismissDelay();
194       for (final var preset : presets)
195       {
196         var item = new JMenuItem(preset.getName());
197         final int QUICK_TOOLTIP = 1500;
198         item.addMouseListener(new MouseAdapter()
199         {
200           @Override
201           public void mouseEntered(MouseEvent e)
202           {
203             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
204           }
205
206           @Override
207           public void mouseExited(MouseEvent e)
208           {
209             ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
210           }
211         });
212         String tooltip = JvSwingUtils.wrapTooltip(true,
213                 format("<strong>%s</strong><br/>%s",
214                         MessageManager.getString(
215                                 preset.isModifiable() ? "label.user_preset"
216                                         : "label.service_preset"),
217                         preset.getDescription()));
218         item.setToolTipText(tooltip);
219         item.addActionListener((event) -> {
220           if (msa != null)
221           {
222             WebServiceWorkerI worker = new AlignmentWorker(msa,
223                     preset.getArguments(), title, submitGaps, true,
224                     alignment, viewport);
225             executor.submit(worker);
226           }
227         });
228         presetList.add(item);
229       }
230       parent.add(presetList);
231     }
232   }
233
234   private CompletionStage<List<ArgumentI>> openEditParamsDialog(
235           WebServiceI service, WsParamSetI preset,
236           List<ArgumentI> arguments)
237   {
238     WsJobParameters jobParams;
239     if (preset == null && arguments != null && arguments.size() > 0)
240       jobParams = new WsJobParameters(service.getParamStore(), preset,
241               arguments);
242     else
243       jobParams = new WsJobParameters(service.getParamStore(), preset,
244               null);
245     var stage = jobParams.showRunDialog();
246     return stage.thenApply((startJob) -> {
247       if (startJob)
248       {
249         if (jobParams.getPreset() == null)
250         {
251           return jobParams.getJobParams();
252         }
253         else
254         {
255           return jobParams.getPreset().getArguments();
256         }
257       }
258       else
259       {
260         return null;
261       }
262     });
263   }
264
265   /**
266    * Implementation of the web service worker performing multiple sequence
267    * alignment.
268    *
269    * @author mmwarowny
270    *
271    */
272   private class AlignmentWorker implements WebServiceWorkerI
273   {
274
275     private long uid = MathUtils.getUID();
276
277     private final AlignmentView msa;
278
279     private final AlignmentI dataset;
280
281     private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
282
283     private List<ArgumentI> args = Collections.emptyList();
284
285     private String alnTitle = "";
286
287     private boolean submitGaps = false;
288
289     private boolean preserveOrder = false;
290
291     private char gapCharacter;
292
293     private WSJobList jobs = new WSJobList();
294
295     private Map<Long, JobInput> inputs = new LinkedHashMap<>();
296
297     private WebserviceInfo wsInfo;
298
299     private Map<Long, Integer> exceptionCount = new HashMap<>();
300
301     private final int MAX_RETRY = 5;
302
303     AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
304             String alnTitle, boolean submitGaps, boolean preserveOrder,
305             AlignmentI alignment, AlignViewport viewport)
306     {
307       this.msa = msa;
308       this.dataset = alignment.getDataset();
309       List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
310               alignment.getCodonFrames(), Collections.emptyList());
311       this.codonFrame.addAll(cf);
312       this.args = args;
313       this.alnTitle = alnTitle;
314       this.submitGaps = submitGaps;
315       this.preserveOrder = preserveOrder;
316       this.gapCharacter = viewport.getGapCharacter();
317
318       String panelInfo = String.format("%s using service hosted at %s%n%s",
319               service.getName(), service.getHostName(),
320               Objects.requireNonNullElse(service.getDescription(), ""));
321       wsInfo = new WebserviceInfo(service.getName(), panelInfo, false);
322     }
323
324     @Override
325     public long getUID()
326     {
327       return uid;
328     }
329
330     @Override
331     public WebServiceI getWebService()
332     {
333       return service;
334     }
335
336     @Override
337     public List<WSJob> getJobs()
338     {
339       return Collections.unmodifiableList(jobs);
340     }
341
342     @Override
343     public void startJobs() throws IOException
344     {
345       String outputHeader = String.format("%s of %s%nJob details%n",
346               submitGaps ? "Re-alignment" : "Alignment", alnTitle);
347       SequenceI[][] conmsa = msa.getVisibleContigs('-');
348       if (conmsa == null)
349       {
350         return;
351       }
352       WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
353       updater.setOutputHeader(outputHeader);
354       int numValid = 0;
355       for (int i = 0; i < conmsa.length; i++)
356       {
357         JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
358         WSJob job = new WSJob(service.getProviderName(), service.getName(),
359                 service.getHostName());
360         job.setJobNum(wsInfo.addJobPane());
361         if (conmsa.length > 0)
362         {
363           wsInfo.setProgressName(String.format("region %d", i),
364                   job.getJobNum());
365         }
366         wsInfo.setProgressText(job.getJobNum(), outputHeader);
367         job.addPropertyChangeListener(updater);
368         inputs.put(job.getUid(), input);
369         jobs.add(job);
370         if (input.isInputValid())
371         {
372           int count;
373           String jobId = null;
374           do
375           {
376             count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
377             try
378             {
379               jobId = service.submit(input.inputSequences, args);
380               Cache.log.debug((format("Job %s submitted", job)));
381               exceptionCount.remove(job.getUid());
382             } catch (IOException e)
383             {
384               exceptionCount.put(job.getUid(), --count);
385             }
386           } while (jobId == null && count > 0);
387           if (jobId != null)
388           {
389             job.setJobId(jobId);
390             job.setStatus(WSJobStatus.SUBMITTED);
391             numValid++;
392           }
393           else
394           {
395             job.setStatus(WSJobStatus.SERVER_ERROR);
396           }
397         }
398         else
399         {
400           job.setStatus(WSJobStatus.INVALID);
401           job.setErrorLog(
402                   MessageManager.getString("label.empty_alignment_job"));
403         }
404       }
405       if (numValid > 0)
406       {
407         // wsInfo.setThisService() should happen here
408         wsInfo.setVisible(true);
409       }
410       else
411       {
412         wsInfo.setVisible(false);
413         // TODO show notification dialog.
414         // JvOptionPane.showMessageDialog(frame,
415         // MessageManager.getString("info.invalid_msa_input_mininfo"),
416         // MessageManager.getString("info.invalid_msa_notenough"),
417         // JvOptionPane.INFORMATION_MESSAGE);
418       }
419     }
420
421     @Override
422     public boolean pollJobs()
423     {
424       boolean done = true;
425       for (WSJob job : getJobs())
426       {
427         if (!job.getStatus().isDone())
428         {
429           Cache.log.debug(format("Polling job %s.", job));
430           try
431           {
432             service.updateProgress(job);
433             exceptionCount.remove(job.getUid());
434           } catch (IOException e)
435           {
436             Cache.log.error(format("Polling job %s failed.", job), e);
437             wsInfo.appendProgressText(job.getJobNum(),
438                     MessageManager.formatMessage("info.server_exception",
439                             service.getName(), e.getMessage()));
440             int count = exceptionCount.getOrDefault(job.getUid(),
441                     MAX_RETRY);
442             if (--count <= 0)
443             {
444               job.setStatus(WSJobStatus.SERVER_ERROR);
445               Cache.log.warn(format(
446                       "Attempts limit exceeded. Droping job %s.", job));
447             }
448             exceptionCount.put(job.getUid(), count);
449           } catch (OutOfMemoryError e)
450           {
451             job.setStatus(WSJobStatus.BROKEN);
452             Cache.log.error(
453                     format("Out of memory when retrieving job %s", job), e);
454           }
455           Cache.log.debug(
456                   format("Job %s status is %s", job, job.getStatus()));
457         }
458         done &= job.getStatus().isDone();
459       }
460       updateWSInfoGlobalStatus();
461       return done;
462     }
463
464     private void updateWSInfoGlobalStatus()
465     {
466       if (jobs.countRunning() > 0)
467       {
468         wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
469       }
470       else if (jobs.countQueuing() > 0
471               || jobs.countSubmitted() < jobs.size())
472       {
473         wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
474       }
475       else
476       {
477         if (jobs.countSuccessful() > 0)
478         {
479           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
480         }
481         else if (jobs.countCancelled() > 0)
482         {
483           wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
484         }
485         else if (jobs.countFailed() > 0)
486         {
487           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
488         }
489       }
490     }
491
492     @Override
493     public void done()
494     {
495       long progbarId = MathUtils.getUID();
496       wsInfo.setProgressBar(
497               MessageManager.getString("status.collecting_job_results"),
498               progbarId);
499       Map<Long, AlignmentI> results = new LinkedHashMap<>();
500       for (WSJob job : getJobs())
501       {
502         try
503         {
504           AlignmentI alignment = supplier.getResult(job);
505           if (alignment != null)
506           {
507             results.put(job.getUid(), alignment);
508           }
509         } catch (Exception e)
510         {
511           if (!service.handleCollectionError(job, e))
512           {
513             Cache.log.error("Couldn't get alignment for job.", e);
514             // TODO: Increment exception count and retry.
515             job.setStatus(WSJobStatus.SERVER_ERROR);
516           }
517         }
518       }
519       updateWSInfoGlobalStatus();
520       if (results.size() > 0)
521       {
522         OutputWrapper out = prepareOutput(results);
523         wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
524                 new Alignment(out.aln), out.alorders, out.hidden));
525         wsInfo.setResultsReady();
526       }
527       else
528       {
529         wsInfo.setFinishedNoResults();
530       }
531       wsInfo.removeProgressBar(progbarId);
532     }
533
534     private class OutputWrapper
535     {
536       AlignmentI aln;
537
538       List<AlignmentOrder> alorders;
539
540       HiddenColumns hidden;
541
542       OutputWrapper(AlignmentI aln, List<AlignmentOrder> alorders,
543               HiddenColumns hidden)
544       {
545         this.aln = aln;
546         this.alorders = alorders;
547         this.hidden = hidden;
548       }
549     }
550
551     private OutputWrapper prepareOutput(Map<Long, AlignmentI> alignments)
552     {
553       List<AlignmentOrder> alorders = new ArrayList<>();
554       SequenceI[][] results = new SequenceI[jobs.size()][];
555       AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
556       for (int i = 0; i < jobs.size(); i++)
557       {
558         WSJob job = jobs.get(i);
559         AlignmentI aln = alignments.get(job.getUid());
560         if (aln != null) // equivalent of job.hasResults()
561         {
562           /* Get the alignment including any empty sequences in the original
563            * order with original ids. */
564           JobInput input = inputs.get(job.getUid());
565           char gapChar = aln.getGapCharacter();
566           List<SequenceI> emptySeqs = input.emptySequences;
567           List<SequenceI> alnSeqs = aln.getSequences();
568           // find the width of the longest sequence
569           int width = 0;
570           for (var seq : alnSeqs)
571             width = Integer.max(width, seq.getLength());
572           for (var emptySeq : emptySeqs)
573             width = Integer.max(width, emptySeq.getLength());
574           // pad shorter sequences with gaps
575           String gapSeq = String.join("",
576                   Collections.nCopies(width, Character.toString(gapChar)));
577           List<SequenceI> seqs = new ArrayList<>(
578                   alnSeqs.size() + emptySeqs.size());
579           seqs.addAll(alnSeqs);
580           seqs.addAll(emptySeqs);
581           for (var seq : seqs)
582           {
583             if (seq.getLength() < width)
584               seq.setSequence(seq.getSequenceAsString()
585                       + gapSeq.substring(seq.getLength()));
586           }
587           SequenceI[] result = seqs.toArray(new SequenceI[0]);
588           AlignmentOrder msaOrder = new AlignmentOrder(result);
589           AlignmentSorter.recoverOrder(result);
590           // temporary workaround for deuniquify
591           @SuppressWarnings({ "rawtypes", "unchecked" })
592           Hashtable names = new Hashtable(input.sequenceNames);
593           // FIXME first call to deuniquify alters original alignment
594           SeqsetUtils.deuniquify(names, result);
595           alorders.add(msaOrder);
596           results[i] = result;
597           orders[i] = msaOrder;
598         }
599         else
600         {
601           results[i] = null;
602         }
603       }
604
605       Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
606       // free references to original data
607       for (int i = 0; i < jobs.size(); i++)
608       {
609         results[i] = null;
610         orders[i] = null;
611       }
612       SequenceI[] alignment = (SequenceI[]) newView[0];
613       HiddenColumns hidden = (HiddenColumns) newView[1];
614       Alignment aln = new Alignment(alignment);
615       aln.setProperty("Alignment Program", service.getName());
616       if (dataset != null)
617         aln.setDataset(dataset);
618
619       propagateDatasetMappings(aln);
620       return new OutputWrapper(aln, alorders, hidden);
621       // displayNewFrame(aln, alorders, hidden);
622     }
623
624     /*
625      * conserves dataset references to sequence objects returned from web
626      * services. propagate codon frame data to alignment.
627      */
628     private void propagateDatasetMappings(Alignment aln)
629     {
630       if (codonFrame != null)
631       {
632         SequenceI[] alignment = aln.getSequencesArray();
633         for (SequenceI seq : alignment)
634         {
635           for (AlignedCodonFrame acf : codonFrame)
636           {
637             if (acf != null && acf.involvesSequence(seq))
638             {
639               aln.addCodonFrame(acf);
640               break;
641             }
642           }
643         }
644       }
645     }
646
647     private void displayNewFrame(AlignmentI aln,
648             List<AlignmentOrder> alorders, HiddenColumns hidden)
649     {
650       AlignFrame frame = new AlignFrame(aln, hidden,
651               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
652       // TODO store feature renderer settings in worker object
653       // frame.getFeatureRenderer().transferSettings(featureSettings);
654       var regions = sortOrders(alorders);
655       if (alorders.size() == 1)
656       {
657         frame.addSortByOrderMenuItem(
658                 format("%s Ordering", service.getName()), alorders.get(0));
659       }
660       else
661       {
662         for (int i = 0; i < alorders.size(); i++)
663         {
664           final int j = i;
665           Iterable<String> iter = () -> regions.get(j).stream()
666                   .map(it -> Integer.toString(it)).iterator();
667           var orderName = format("%s Region %s Ordering", service.getName(),
668                   String.join(",", iter));
669           frame.addSortByOrderMenuItem(orderName, alorders.get(i));
670         }
671       }
672
673       /* TODO
674        * If alignment was requested from one half of a SplitFrame, show in a
675        * SplitFrame with the other pane similarly aligned.
676        */
677
678       Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH,
679               AlignFrame.DEFAULT_HEIGHT);
680     }
681
682     private List<List<Integer>> sortOrders(List<?> alorders)
683     {
684       List<List<Integer>> regions = new ArrayList<>();
685       for (int i = 0; i < alorders.size(); i++)
686       {
687         List<Integer> regs = new ArrayList<>();
688         regs.add(i);
689         int j = i + 1;
690         while (j < alorders.size())
691         {
692           if (alorders.get(i).equals(alorders.get(j)))
693           {
694             alorders.remove(j);
695             regs.add(j);
696           }
697           else
698           {
699             j++;
700           }
701         }
702         regions.add(regs);
703       }
704       return regions;
705     }
706   }
707
708   private static class JobInput
709   {
710     final List<SequenceI> inputSequences;
711
712     final List<SequenceI> emptySequences;
713
714     @SuppressWarnings("rawtypes")
715     final Map<String, ? extends Map> sequenceNames;
716
717     private JobInput(int numSequences, List<SequenceI> inputSequences,
718             List<SequenceI> emptySequences,
719             @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
720     {
721       this.inputSequences = Collections.unmodifiableList(inputSequences);
722       this.emptySequences = Collections.unmodifiableList(emptySequences);
723       this.sequenceNames = names;
724     }
725
726     boolean isInputValid()
727     {
728       return inputSequences.size() >= 2;
729     }
730
731     static JobInput create(SequenceI[] sequences, int minLength,
732             boolean submitGaps)
733     {
734       assert minLength >= 0 : MessageManager.getString(
735               "error.implementation_error_minlen_must_be_greater_zero");
736       int numSeq = 0;
737       for (SequenceI seq : sequences)
738       {
739         if (seq.getEnd() - seq.getStart() >= minLength)
740         {
741           numSeq++;
742         }
743       }
744
745       List<SequenceI> inputSequences = new ArrayList<>();
746       List<SequenceI> emptySequences = new ArrayList<>();
747       @SuppressWarnings("rawtypes")
748       Map<String, Hashtable> names = new LinkedHashMap<>();
749       for (int i = 0; i < sequences.length; i++)
750       {
751         SequenceI seq = sequences[i];
752         String newName = SeqsetUtils.unique_name(i);
753         @SuppressWarnings("rawtypes")
754         Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
755         names.put(newName, hash);
756         if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
757         {
758           String seqString = seq.getSequenceAsString();
759           if (!submitGaps)
760           {
761             seqString = AlignSeq.extractGaps(
762                     jalview.util.Comparison.GapChars, seqString);
763           }
764           inputSequences.add(new Sequence(newName, seqString));
765         }
766         else
767         {
768           String seqString = null;
769           if (seq.getEnd() >= seq.getStart()) // is it ever false?
770           {
771             seqString = seq.getSequenceAsString();
772             if (!submitGaps)
773             {
774               seqString = AlignSeq.extractGaps(
775                       jalview.util.Comparison.GapChars, seqString);
776             }
777           }
778           emptySequences.add(new Sequence(newName, seqString));
779         }
780       }
781
782       return new JobInput(numSeq, inputSequences, emptySequences, names);
783     }
784   }
785
786 }