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