f836c91a2600054bd1d11814ccb3a121276e23a2
[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       Cache.log.info(format("Starting new %s job.", service.getName()));
370       String outputHeader = String.format("%s of %s%nJob details%n",
371               submitGaps ? "Re-alignment" : "Alignment", alnTitle);
372       SequenceI[][] conmsa = msa.getVisibleContigs('-');
373       if (conmsa == null)
374       {
375         return;
376       }
377       WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
378       updater.setOutputHeader(outputHeader);
379       int numValid = 0;
380       for (int i = 0; i < conmsa.length; i++)
381       {
382         JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
383         WSJob job = new WSJob(service.getProviderName(), service.getName(),
384                 service.getHostName());
385         job.setJobNum(wsInfo.addJobPane());
386         if (conmsa.length > 1)
387         {
388           wsInfo.setProgressName(String.format("region %d", i),
389                   job.getJobNum());
390         }
391         wsInfo.setProgressText(job.getJobNum(), outputHeader);
392         job.addPropertyChangeListener(updater);
393         inputs.put(job.getUid(), input);
394         jobs.add(job);
395         if (input.isInputValid())
396         {
397           int count;
398           String jobId = null;
399           do
400           {
401             count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
402             try
403             {
404               jobId = service.submit(input.inputSequences, args);
405               Cache.log.debug((format("Job %s submitted", job)));
406               exceptionCount.remove(job.getUid());
407             } catch (IOException e)
408             {
409               exceptionCount.put(job.getUid(), --count);
410             }
411           } while (jobId == null && count > 0);
412           if (jobId != null)
413           {
414             job.setJobId(jobId);
415             job.setStatus(WSJobStatus.SUBMITTED);
416             numValid++;
417           }
418           else
419           {
420             job.setStatus(WSJobStatus.SERVER_ERROR);
421           }
422         }
423         else
424         {
425           job.setStatus(WSJobStatus.INVALID);
426           job.setErrorLog(
427                   MessageManager.getString("label.empty_alignment_job"));
428         }
429       }
430       if (numValid > 0)
431       {
432         // wsInfo.setThisService() should happen here
433         wsInfo.setVisible(true);
434       }
435       else
436       {
437         wsInfo.setVisible(false);
438         // TODO show notification dialog.
439         // JvOptionPane.showMessageDialog(frame,
440         // MessageManager.getString("info.invalid_msa_input_mininfo"),
441         // MessageManager.getString("info.invalid_msa_notenough"),
442         // JvOptionPane.INFORMATION_MESSAGE);
443       }
444     }
445
446     @Override
447     public boolean pollJobs()
448     {
449       boolean done = true;
450       for (WSJob job : getJobs())
451       {
452         if (!job.getStatus().isDone() && !job.getStatus().isFailed())
453         {
454           Cache.log.debug(format("Polling job %s.", job));
455           try
456           {
457             service.updateProgress(job);
458             exceptionCount.remove(job.getUid());
459           } catch (IOException e)
460           {
461             Cache.log.error(format("Polling job %s failed.", job), e);
462             wsInfo.appendProgressText(job.getJobNum(),
463                     MessageManager.formatMessage("info.server_exception",
464                             service.getName(), e.getMessage()));
465             int count = exceptionCount.getOrDefault(job.getUid(),
466                     MAX_RETRY);
467             if (--count <= 0)
468             {
469               job.setStatus(WSJobStatus.SERVER_ERROR);
470               Cache.log.warn(format(
471                       "Attempts limit exceeded. Droping job %s.", job));
472             }
473             exceptionCount.put(job.getUid(), count);
474           } catch (OutOfMemoryError e)
475           {
476             job.setStatus(WSJobStatus.BROKEN);
477             Cache.log.error(
478                     format("Out of memory when retrieving job %s", job), e);
479           }
480           Cache.log.debug(
481                   format("Job %s status is %s", job, job.getStatus()));
482         }
483         done &= job.getStatus().isDone() || job.getStatus().isFailed();
484       }
485       updateWSInfoGlobalStatus();
486       return done;
487     }
488
489     private void updateWSInfoGlobalStatus()
490     {
491       if (jobs.countRunning() > 0)
492       {
493         wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
494       }
495       else if (jobs.countQueuing() > 0
496               || jobs.countSubmitted() < jobs.size())
497       {
498         wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
499       }
500       else
501       {
502         if (jobs.countSuccessful() > 0)
503         {
504           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
505         }
506         else if (jobs.countCancelled() > 0)
507         {
508           wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
509         }
510         else if (jobs.countFailed() > 0)
511         {
512           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
513         }
514       }
515     }
516
517     @Override
518     public void done()
519     {
520       long progbarId = MathUtils.getUID();
521       wsInfo.setProgressBar(
522               MessageManager.getString("status.collecting_job_results"),
523               progbarId);
524       Map<Long, AlignmentI> results = new LinkedHashMap<>();
525       for (WSJob job : getJobs())
526       {
527         if (job.getStatus().isFailed())
528           continue;
529         try
530         {
531           AlignmentI alignment = supplier.getResult(job);
532           if (alignment != null)
533           {
534             results.put(job.getUid(), alignment);
535           }
536         } catch (Exception e)
537         {
538           if (!service.handleCollectionError(job, e))
539           {
540             Cache.log.error("Couldn't get alignment for job.", e);
541             // TODO: Increment exception count and retry.
542             job.setStatus(WSJobStatus.SERVER_ERROR);
543           }
544         }
545       }
546       updateWSInfoGlobalStatus();
547       if (results.size() > 0)
548       {
549         OutputWrapper out = prepareOutput(results);
550         wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
551                 new Alignment(out.aln), out.alorders, out.hidden));
552         wsInfo.setResultsReady();
553       }
554       else
555       {
556         wsInfo.setFinishedNoResults();
557       }
558       wsInfo.removeProgressBar(progbarId);
559     }
560
561     private class OutputWrapper
562     {
563       AlignmentI aln;
564
565       List<AlignmentOrder> alorders;
566
567       HiddenColumns hidden;
568
569       OutputWrapper(AlignmentI aln, List<AlignmentOrder> alorders,
570               HiddenColumns hidden)
571       {
572         this.aln = aln;
573         this.alorders = alorders;
574         this.hidden = hidden;
575       }
576     }
577
578     private OutputWrapper prepareOutput(Map<Long, AlignmentI> alignments)
579     {
580       List<AlignmentOrder> alorders = new ArrayList<>();
581       SequenceI[][] results = new SequenceI[jobs.size()][];
582       AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
583       for (int i = 0; i < jobs.size(); i++)
584       {
585         WSJob job = jobs.get(i);
586         AlignmentI aln = alignments.get(job.getUid());
587         if (aln != null) // equivalent of job.hasResults()
588         {
589           /* Get the alignment including any empty sequences in the original
590            * order with original ids. */
591           JobInput input = inputs.get(job.getUid());
592           char gapChar = aln.getGapCharacter();
593           List<SequenceI> emptySeqs = input.emptySequences;
594           List<SequenceI> alnSeqs = aln.getSequences();
595           // find the width of the longest sequence
596           int width = 0;
597           for (var seq : alnSeqs)
598             width = Integer.max(width, seq.getLength());
599           for (var emptySeq : emptySeqs)
600             width = Integer.max(width, emptySeq.getLength());
601           // pad shorter sequences with gaps
602           String gapSeq = String.join("",
603                   Collections.nCopies(width, Character.toString(gapChar)));
604           List<SequenceI> seqs = new ArrayList<>(
605                   alnSeqs.size() + emptySeqs.size());
606           seqs.addAll(alnSeqs);
607           seqs.addAll(emptySeqs);
608           for (var seq : seqs)
609           {
610             if (seq.getLength() < width)
611               seq.setSequence(seq.getSequenceAsString()
612                       + gapSeq.substring(seq.getLength()));
613           }
614           SequenceI[] result = seqs.toArray(new SequenceI[0]);
615           AlignmentOrder msaOrder = new AlignmentOrder(result);
616           AlignmentSorter.recoverOrder(result);
617           // temporary workaround for deuniquify
618           @SuppressWarnings({ "rawtypes", "unchecked" })
619           Hashtable names = new Hashtable(input.sequenceNames);
620           // FIXME first call to deuniquify alters original alignment
621           SeqsetUtils.deuniquify(names, result);
622           alorders.add(msaOrder);
623           results[i] = result;
624           orders[i] = msaOrder;
625         }
626         else
627         {
628           results[i] = null;
629         }
630       }
631
632       Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
633       // free references to original data
634       for (int i = 0; i < jobs.size(); i++)
635       {
636         results[i] = null;
637         orders[i] = null;
638       }
639       SequenceI[] alignment = (SequenceI[]) newView[0];
640       HiddenColumns hidden = (HiddenColumns) newView[1];
641       Alignment aln = new Alignment(alignment);
642       aln.setProperty("Alignment Program", service.getName());
643       if (dataset != null)
644         aln.setDataset(dataset);
645
646       propagateDatasetMappings(aln);
647       return new OutputWrapper(aln, alorders, hidden);
648       // displayNewFrame(aln, alorders, hidden);
649     }
650
651     /*
652      * conserves dataset references to sequence objects returned from web
653      * services. propagate codon frame data to alignment.
654      */
655     private void propagateDatasetMappings(Alignment aln)
656     {
657       if (codonFrame != null)
658       {
659         SequenceI[] alignment = aln.getSequencesArray();
660         for (SequenceI seq : alignment)
661         {
662           for (AlignedCodonFrame acf : codonFrame)
663           {
664             if (acf != null && acf.involvesSequence(seq))
665             {
666               aln.addCodonFrame(acf);
667               break;
668             }
669           }
670         }
671       }
672     }
673
674     private void displayNewFrame(AlignmentI aln,
675             List<AlignmentOrder> alorders, HiddenColumns hidden)
676     {
677       AlignFrame frame = new AlignFrame(aln, hidden,
678               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
679       // TODO store feature renderer settings in worker object
680       // frame.getFeatureRenderer().transferSettings(featureSettings);
681       var regions = sortOrders(alorders);
682       if (alorders.size() == 1)
683       {
684         frame.addSortByOrderMenuItem(
685                 format("%s Ordering", service.getName()), alorders.get(0));
686       }
687       else
688       {
689         for (int i = 0; i < alorders.size(); i++)
690         {
691           final int j = i;
692           Iterable<String> iter = () -> regions.get(j).stream()
693                   .map(it -> Integer.toString(it)).iterator();
694           var orderName = format("%s Region %s Ordering", service.getName(),
695                   String.join(",", iter));
696           frame.addSortByOrderMenuItem(orderName, alorders.get(i));
697         }
698       }
699
700       /* TODO
701        * If alignment was requested from one half of a SplitFrame, show in a
702        * SplitFrame with the other pane similarly aligned.
703        */
704
705       Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH,
706               AlignFrame.DEFAULT_HEIGHT);
707     }
708
709     private List<List<Integer>> sortOrders(List<?> alorders)
710     {
711       List<List<Integer>> regions = new ArrayList<>();
712       for (int i = 0; i < alorders.size(); i++)
713       {
714         List<Integer> regs = new ArrayList<>();
715         regs.add(i);
716         int j = i + 1;
717         while (j < alorders.size())
718         {
719           if (alorders.get(i).equals(alorders.get(j)))
720           {
721             alorders.remove(j);
722             regs.add(j);
723           }
724           else
725           {
726             j++;
727           }
728         }
729         regions.add(regs);
730       }
731       return regions;
732     }
733   }
734
735   private static class JobInput
736   {
737     final List<SequenceI> inputSequences;
738
739     final List<SequenceI> emptySequences;
740
741     @SuppressWarnings("rawtypes")
742     final Map<String, ? extends Map> sequenceNames;
743
744     private JobInput(int numSequences, List<SequenceI> inputSequences,
745             List<SequenceI> emptySequences,
746             @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
747     {
748       this.inputSequences = Collections.unmodifiableList(inputSequences);
749       this.emptySequences = Collections.unmodifiableList(emptySequences);
750       this.sequenceNames = names;
751     }
752
753     boolean isInputValid()
754     {
755       return inputSequences.size() >= 2;
756     }
757
758     static JobInput create(SequenceI[] sequences, int minLength,
759             boolean submitGaps)
760     {
761       assert minLength >= 0 : MessageManager.getString(
762               "error.implementation_error_minlen_must_be_greater_zero");
763       int numSeq = 0;
764       for (SequenceI seq : sequences)
765       {
766         if (seq.getEnd() - seq.getStart() >= minLength)
767         {
768           numSeq++;
769         }
770       }
771
772       List<SequenceI> inputSequences = new ArrayList<>();
773       List<SequenceI> emptySequences = new ArrayList<>();
774       @SuppressWarnings("rawtypes")
775       Map<String, Hashtable> names = new LinkedHashMap<>();
776       for (int i = 0; i < sequences.length; i++)
777       {
778         SequenceI seq = sequences[i];
779         String newName = SeqsetUtils.unique_name(i);
780         @SuppressWarnings("rawtypes")
781         Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
782         names.put(newName, hash);
783         if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
784         {
785           String seqString = seq.getSequenceAsString();
786           if (!submitGaps)
787           {
788             seqString = AlignSeq.extractGaps(
789                     jalview.util.Comparison.GapChars, seqString);
790           }
791           inputSequences.add(new Sequence(newName, seqString));
792         }
793         else
794         {
795           String seqString = "";
796           if (seq.getEnd() >= seq.getStart())  // true if gaps only
797           {
798             seqString = seq.getSequenceAsString();
799             if (!submitGaps)
800             {
801               seqString = AlignSeq.extractGaps(
802                       jalview.util.Comparison.GapChars, seqString);
803             }
804           }
805           emptySequences.add(new Sequence(newName, seqString));
806         }
807       }
808
809       return new JobInput(numSeq, inputSequences, emptySequences, names);
810     }
811   }
812
813 }