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