2 * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
3 * Copyright (C) 2014 The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
21 package jalview.ws.jws2;
23 import jalview.analysis.AlignSeq;
24 import jalview.bin.Cache;
25 import jalview.datamodel.Alignment;
26 import jalview.datamodel.AlignmentOrder;
27 import jalview.datamodel.AlignmentView;
28 import jalview.datamodel.ColumnSelection;
29 import jalview.datamodel.Sequence;
30 import jalview.datamodel.SequenceI;
31 import jalview.gui.AlignFrame;
32 import jalview.gui.Desktop;
33 import jalview.gui.WebserviceInfo;
34 import jalview.util.MessageManager;
35 import jalview.ws.AWsJob;
36 import jalview.ws.JobStateSummary;
37 import jalview.ws.WSClientI;
38 import jalview.ws.jws2.dm.JabaWsParamSet;
39 import jalview.ws.params.WsParamSetI;
41 import java.util.ArrayList;
42 import java.util.Hashtable;
43 import java.util.List;
45 import java.util.Vector;
47 import compbio.data.msa.MsaWS;
48 import compbio.metadata.Argument;
49 import compbio.metadata.ChunkHolder;
50 import compbio.metadata.JobStatus;
51 import compbio.metadata.Preset;
53 class MsaWSThread extends AWS2Thread implements WSClientI
55 boolean submitGaps = false; // pass sequences including gaps to alignment
59 boolean preserveOrder = true; // and always store and recover sequence
63 class MsaWSJob extends JWs2Job
67 WsParamSetI preset = null;
69 List<Argument> arguments = null;
74 ArrayList<compbio.data.sequence.FastaSequence> seqs = new ArrayList<compbio.data.sequence.FastaSequence>();
79 compbio.data.sequence.Alignment alignment;
81 // set if the job didn't get run - then the input is simply returned to the
83 private boolean returnInput = false;
93 public MsaWSJob(int jobNum, SequenceI[] inSeqs)
96 if (!prepareInput(inSeqs, 2))
99 subjobComplete = true;
105 Hashtable<String, Map> SeqNames = new Hashtable();
107 Vector<String[]> emptySeqs = new Vector();
110 * prepare input sequences for MsaWS service
113 * jalview sequences to be prepared
115 * minimum number of residues required for this MsaWS service
116 * @return true if seqs contains sequences to be submitted to service.
118 // TODO: return compbio.seqs list or nothing to indicate validity.
119 private boolean prepareInput(SequenceI[] seqs, int minlen)
124 throw new Error(MessageManager.getString("error.implementation_error_minlen_must_be_greater_zero"));
126 for (int i = 0; i < seqs.length; i++)
128 if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
133 boolean valid = nseqs > 1; // need at least two seqs
134 compbio.data.sequence.FastaSequence seq;
135 for (int i = 0, n = 0; i < seqs.length; i++)
138 String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same
142 SeqNames.put(newname,
143 jalview.analysis.SeqsetUtils.SeqCharacterHash(seqs[i]));
144 if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
146 // make new input sequence with or without gaps
147 seq = new compbio.data.sequence.FastaSequence(newname,
148 (submitGaps) ? seqs[i].getSequenceAsString()
149 : AlignSeq.extractGaps(
150 jalview.util.Comparison.GapChars,
151 seqs[i].getSequenceAsString()));
157 if (seqs[i].getEnd() >= seqs[i].getStart())
159 empty = (submitGaps) ? seqs[i].getSequenceAsString() : AlignSeq
160 .extractGaps(jalview.util.Comparison.GapChars,
161 seqs[i].getSequenceAsString());
163 emptySeqs.add(new String[]
172 * @return true if getAlignment will return a valid alignment result.
174 public boolean hasResults()
178 && (alignment != null || (emptySeqs != null && emptySeqs
188 * get the alignment including any empty sequences in the original order
189 * with original ids. Caller must access the alignment.getMetadata() object
190 * to annotate the final result passsed to the user.
192 * @return { SequenceI[], AlignmentOrder }
194 public Object[] getAlignment()
196 // is this a generic subjob or a Jws2 specific Object[] return signature
199 SequenceI[] alseqs = null;
200 char alseq_gapchar = '-';
202 if (alignment.getSequences().size() > 0)
204 alseqs = new SequenceI[alignment.getSequences().size()];
205 for (compbio.data.sequence.FastaSequence seq : alignment
208 alseqs[alseq_l++] = new Sequence(seq.getId(), seq.getSequence());
210 alseq_gapchar = alignment.getMetadata().getGapchar();
213 // add in the empty seqs.
214 if (emptySeqs.size() > 0)
216 SequenceI[] t_alseqs = new SequenceI[alseq_l + emptySeqs.size()];
221 for (i = 0, w = alseqs[0].getLength(); i < alseq_l; i++)
223 if (w < alseqs[i].getLength())
225 w = alseqs[i].getLength();
227 t_alseqs[i] = alseqs[i];
231 // check that aligned width is at least as wide as emptySeqs width.
233 for (i = 0, w = emptySeqs.size(); i < w; i++)
235 String[] es = emptySeqs.get(i);
236 if (es != null && es[1] != null)
238 int sw = es[1].length();
245 // make a gapped string.
246 StringBuffer insbuff = new StringBuffer(w);
247 for (i = 0; i < nw; i++)
249 insbuff.append(alseq_gapchar);
253 for (i = 0; i < alseq_l; i++)
255 int sw = t_alseqs[i].getLength();
259 alseqs[i].setSequence(t_alseqs[i].getSequenceAsString()
260 + insbuff.substring(0, sw - nw));
264 for (i = 0, w = emptySeqs.size(); i < w; i++)
266 String[] es = emptySeqs.get(i);
269 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(es[0],
270 insbuff.toString(), 1, 0);
274 if (es[1].length() < nw)
276 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
278 es[1] + insbuff.substring(0, nw - es[1].length()),
279 1, 1 + es[1].length());
283 t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
290 AlignmentOrder msaorder = new AlignmentOrder(alseqs);
291 // always recover the order - makes parseResult()'s life easier.
292 jalview.analysis.AlignmentSorter.recoverOrder(alseqs);
293 // account for any missing sequences
294 jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs);
296 { alseqs, msaorder };
302 * mark subjob as cancelled and set result object appropriatly
307 subjobComplete = true;
313 * @return boolean true if job can be submitted.
315 public boolean hasValidInput()
317 // TODO: get attributes for this MsaWS instance to check if it can do two
318 // sequence alignment.
319 if (seqs != null && seqs.size() >= 2) // two or more sequences is valid ?
326 StringBuffer jobProgress = new StringBuffer();
328 public void setStatus(String string)
330 jobProgress.setLength(0);
331 jobProgress.append(string);
335 public String getStatus()
337 return jobProgress.toString();
341 public boolean hasStatus()
343 return jobProgress != null;
347 * @return the lastChunk
349 public long getLastChunk()
356 * the lastChunk to set
358 public void setLastChunk(long lastChunk)
360 this.lastChunk = lastChunk;
363 String alignmentProgram = null;
365 public String getAlignmentProgram()
367 return alignmentProgram;
370 public boolean hasArguments()
372 return (arguments != null && arguments.size() > 0)
373 || (preset != null && preset instanceof JabaWsParamSet);
376 public List<Argument> getJabaArguments()
378 List<Argument> newargs = new ArrayList<Argument>();
379 if (preset != null && preset instanceof JabaWsParamSet)
381 newargs.addAll(((JabaWsParamSet) preset).getjabaArguments());
383 if (arguments != null && arguments.size() > 0)
385 newargs.addAll(arguments);
391 * add a progess header to status string containing presets/args used
393 public void addInitialStatus()
397 jobProgress.append("Using "
398 + (preset instanceof JabaPreset ? "Server" : "User")
399 + "Preset: " + preset.getName());
400 if (preset instanceof JabaWsParamSet)
402 for (Argument opt : ((JabaWsParamSet) preset).getjabaArguments())
404 jobProgress.append(opt.getName() + " " + opt.getDefaultValue()
409 if (arguments != null && arguments.size() > 0)
411 jobProgress.append("With custom parameters : \n");
412 // merge arguments with preset's own arguments.
413 for (Argument opt : arguments)
415 jobProgress.append(opt.getName() + " " + opt.getDefaultValue()
419 jobProgress.append("\nJob Output:\n");
422 public boolean isPresetJob()
424 return preset != null && preset instanceof JabaPreset;
427 public Preset getServerPreset()
429 return (isPresetJob()) ? ((JabaPreset) preset).p : null;
433 String alTitle; // name which will be used to form new alignment window.
435 Alignment dataset; // dataset to which the new alignment will be
439 @SuppressWarnings("unchecked")
443 * set basic options for this (group) of Msa jobs
450 MsaWSThread(MsaWS server, String wsUrl, WebserviceInfo wsinfo,
451 jalview.gui.AlignFrame alFrame, AlignmentView alview,
452 String wsname, boolean subgaps, boolean presorder)
454 super(alFrame, wsinfo, alview, wsname, wsUrl);
455 this.server = server;
456 this.submitGaps = subgaps;
457 this.preserveOrder = presorder;
461 * create one or more Msa jobs to align visible seuqences in _msa
474 MsaWSThread(MsaWS server2, WsParamSetI preset, List<Argument> paramset,
475 String wsUrl, WebserviceInfo wsinfo,
476 jalview.gui.AlignFrame alFrame, String wsname, String title,
477 AlignmentView _msa, boolean subgaps, boolean presorder,
480 this(server2, wsUrl, wsinfo, alFrame, _msa, wsname, subgaps, presorder);
481 OutputHeader = wsInfo.getProgressText();
485 SequenceI[][] conmsa = _msa.getVisibleContigs('-');
488 int nvalid = 0, njobs = conmsa.length;
489 jobs = new MsaWSJob[njobs];
490 for (int j = 0; j < njobs; j++)
494 jobs[j] = new MsaWSJob(wsinfo.addJobPane(), conmsa[j]);
498 jobs[j] = new MsaWSJob(0, conmsa[j]);
500 if (((MsaWSJob) jobs[j]).hasValidInput())
504 ((MsaWSJob) jobs[j]).preset = preset;
505 ((MsaWSJob) jobs[j]).arguments = paramset;
506 ((MsaWSJob) jobs[j]).alignmentProgram = wsname;
509 wsinfo.setProgressName("region " + jobs[j].getJobnum(),
510 jobs[j].getJobnum());
512 wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
514 validInput = nvalid > 0;
518 boolean validInput = false;
522 * @return true if the thread will perform a calculation
524 public boolean hasValidInput()
528 public boolean isCancellable()
533 public void cancelJob()
535 if (!jobComplete && jobs != null)
537 boolean cancelled = true;
538 for (int job = 0; job < jobs.length; job++)
540 if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
542 String cancelledMessage = "";
545 boolean cancelledJob = server.cancelJob(jobs[job].getJobId());
546 if (true) // cancelledJob || true)
549 // if the Jaba server indicates the job can't be cancelled, its
550 // because its running on the server's local execution engine
551 // so we just close the window anyway.
552 cancelledMessage = "Job cancelled.";
553 ((MsaWSJob) jobs[job]).cancel(); // TODO: refactor to avoid this
555 wsInfo.setStatus(jobs[job].getJobnum(),
556 WebserviceInfo.STATE_CANCELLED_OK);
560 // VALID UNSTOPPABLE JOB
561 cancelledMessage += "Server cannot cancel this job. just close the window.\n";
563 // wsInfo.setStatus(jobs[job].jobnum,
564 // WebserviceInfo.STATE_RUNNING);
566 } catch (Exception exc)
568 cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
571 "Exception whilst cancelling " + jobs[job].getJobId(),
574 wsInfo.setProgressText(jobs[job].getJobnum(), OutputHeader
575 + cancelledMessage + "\n");
579 // if we hadn't submitted then just mark the job as cancelled.
580 jobs[job].setSubjobComplete(true);
581 wsInfo.setStatus(jobs[job].getJobnum(),
582 WebserviceInfo.STATE_CANCELLED_OK);
588 wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
591 this.interrupt(); // kick thread to update job states.
597 wsInfo.setProgressText(OutputHeader
598 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
603 public void pollJob(AWsJob job) throws Exception
605 // TODO: investigate if we still need to cast here in J1.6
606 MsaWSJob j = ((MsaWSJob) job);
607 // this is standard code, but since the interface doesn't comprise of a
608 // basic one that implements (getJobStatus, pullExecStatistics) we have to
609 // repeat the code for all jw2s services.
610 j.setjobStatus(server.getJobStatus(job.getJobId()));
611 updateJobProgress(j);
617 * @return true if more job progress data was available
620 protected boolean updateJobProgress(MsaWSJob j) throws Exception
622 StringBuffer response = j.jobProgress;
623 long lastchunk = j.getLastChunk();
624 boolean changed = false;
627 j.setLastChunk(lastchunk);
628 ChunkHolder chunk = server
629 .pullExecStatistics(j.getJobId(), lastchunk);
632 changed |= chunk.getChunk().length() > 0;
633 response.append(chunk.getChunk());
634 lastchunk = chunk.getNextPosition();
638 } catch (InterruptedException x)
644 } while (lastchunk >= 0 && j.getLastChunk() != lastchunk);
648 public void StartJob(AWsJob job)
650 Exception lex = null;
651 // boiler plate template
652 if (!(job instanceof MsaWSJob))
654 throw new Error(MessageManager.formatMessage("error.implementation_error_msawbjob_called", new String[]{job.getClass().toString()}));
656 MsaWSJob j = (MsaWSJob) job;
659 if (Cache.log.isDebugEnabled())
661 Cache.log.debug("Tried to submit an already submitted job "
668 if (j.seqs == null || j.seqs.size() == 0)
670 // special case - selection consisted entirely of empty sequences...
671 j.setjobStatus(JobStatus.FINISHED);
672 j.setStatus(MessageManager.getString("label.empty_alignment_job"));
676 j.addInitialStatus(); // list the presets/parameters used for the job in
680 j.setJobId(server.presetAlign(j.seqs, j.getServerPreset()));
682 else if (j.hasArguments())
684 j.setJobId(server.customAlign(j.seqs, j.getJabaArguments()));
688 j.setJobId(server.align(j.seqs));
691 if (j.getJobId() != null)
693 j.setSubmitted(true);
694 j.setSubjobComplete(false);
695 // System.out.println(WsURL + " Job Id '" + jobId + "'");
700 throw new Exception(MessageManager.formatMessage("exception.web_service_returned_null_try_later", new String[]{WsUrl}));
702 } catch (compbio.metadata.UnsupportedRuntimeException _lex)
705 wsInfo.appendProgressText(MessageManager.formatMessage("info.job_couldnt_be_run_server_doesnt_support_program", new String[]{_lex.getMessage()}));
706 wsInfo.warnUser(_lex.getMessage(), MessageManager.getString("warn.service_not_supported"));
707 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
708 wsInfo.setStatus(j.getJobnum(),
709 WebserviceInfo.STATE_STOPPED_SERVERERROR);
710 } catch (compbio.metadata.LimitExceededException _lex)
713 wsInfo.appendProgressText(MessageManager.formatMessage("info.job_couldnt_be_run_exceeded_hard_limit", new String[]{_lex.getMessage()}));
714 wsInfo.warnUser(_lex.getMessage(), MessageManager.getString("warn.input_is_too_big"));
715 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
716 wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_ERROR);
717 } catch (compbio.metadata.WrongParameterException _lex)
720 wsInfo.warnUser(_lex.getMessage(), MessageManager.getString("warn.invalid_job_param_set"));
721 wsInfo.appendProgressText(MessageManager.formatMessage("info.job_couldnt_be_run_incorrect_param_setting", new String[]{_lex.getMessage()}));
722 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
723 wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_ERROR);
726 // For unexpected errors
728 .println(WebServiceName
729 + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
730 + "When contacting Server:" + WsUrl + "\n");
731 e.printStackTrace(System.err);
732 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
733 wsInfo.setStatus(j.getJobnum(),
734 WebserviceInfo.STATE_STOPPED_SERVERERROR);
735 } catch (Exception e)
737 // For unexpected errors
739 .println(WebServiceName
740 + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
741 + "When contacting Server:" + WsUrl + "\n");
742 e.printStackTrace(System.err);
743 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
744 wsInfo.setStatus(j.getJobnum(),
745 WebserviceInfo.STATE_STOPPED_SERVERERROR);
748 if (!j.isSubmitted())
750 // Boilerplate code here
751 // TODO: JBPNote catch timeout or other fault types explicitly
753 j.setAllowedServerExceptions(0);
754 wsInfo.appendProgressText(j.getJobnum(),
755 MessageManager.getString("info.failed_to_submit_sequences_for_alignment"));
760 public void parseResult()
762 long progbar = System.currentTimeMillis();
763 wsInfo.setProgressBar(MessageManager.getString("status.collecting_job_results"), progbar);
764 int results = 0; // number of result sets received
765 JobStateSummary finalState = new JobStateSummary();
768 for (int j = 0; j < jobs.length; j++)
770 MsaWSJob msjob = ((MsaWSJob) jobs[j]);
771 if (jobs[j].isFinished() && msjob.alignment == null)
773 int nunchanged = 3, nexcept = 3;
774 boolean jpchanged = false, jpex = false;
779 jpchanged = updateJobProgress(msjob);
785 } catch (Exception e)
789 .warn("Exception when retrieving remaining Job progress data for job "
790 + msjob.getJobId() + " on server " + WsUrl);
794 // set flag remember that we've had an exception.
802 Thread.sleep(jpex ? 2400 : 1200); // wait a bit longer if we
803 // experienced an exception.
804 } catch (Exception ex)
810 } while (nunchanged > 0 && nexcept > 0);
812 if (Cache.log.isDebugEnabled())
814 System.out.println("Job Execution file for job: "
815 + msjob.getJobId() + " on server " + WsUrl);
816 System.out.println(msjob.getStatus());
817 System.out.println("*** End of status");
822 msjob.alignment = server.getResult(msjob.getJobId());
823 } catch (compbio.metadata.ResultNotAvailableException e)
825 // job has failed for some reason - probably due to invalid
828 .debug("Results not available for finished job - marking as broken job.",
831 .append("\nResult not available. Probably due to invalid input or parameter settings. Server error message below:\n\n"
832 + e.getLocalizedMessage());
833 msjob.setjobStatus(JobStatus.FAILED);
834 } catch (Exception e)
836 Cache.log.error("Couldn't get Alignment for job.", e);
837 // TODO: Increment count and retry ?
838 msjob.setjobStatus(JobStatus.UNDEFINED);
841 finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
842 if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
843 && jobs[j].hasResults())
846 compbio.data.sequence.Alignment alignment = ((MsaWSJob) jobs[j]).alignment;
847 if (alignment != null)
849 // server.close(jobs[j].getJobnum());
850 // wsInfo.appendProgressText(jobs[j].getJobnum(),
851 // "\nAlignment Object Method Notes\n");
852 // wsInfo.appendProgressText(jobs[j].getJobnum(),
853 // "Calculated with "+alignment.getMetadata().getProgram().toString());
854 // JBPNote The returned files from a webservice could be
855 // hidden behind icons in the monitor window that,
856 // when clicked, pop up their corresponding data
860 } catch (Exception ex)
863 Cache.log.error("Unexpected exception when processing results for "
865 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
869 wsInfo.showResultsNewFrame
870 .addActionListener(new java.awt.event.ActionListener()
872 public void actionPerformed(java.awt.event.ActionEvent evt)
874 displayResults(true);
878 .addActionListener(new java.awt.event.ActionListener()
880 public void actionPerformed(java.awt.event.ActionEvent evt)
882 displayResults(false);
885 wsInfo.setResultsReady();
889 wsInfo.setFinishedNoResults();
891 updateGlobalStatus(finalState);
892 wsInfo.setProgressBar(null, progbar);
895 void displayResults(boolean newFrame)
897 // view input or result data for each block
898 Vector alorders = new Vector();
899 SequenceI[][] results = new SequenceI[jobs.length][];
900 AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
901 String lastProgram = null;
903 for (int j = 0; j < jobs.length; j++)
905 if (jobs[j].hasResults())
907 msjob = (MsaWSJob) jobs[j];
908 Object[] res = msjob.getAlignment();
909 lastProgram = msjob.getAlignmentProgram();
910 alorders.add(res[1]);
911 results[j] = (SequenceI[]) res[0];
912 orders[j] = (AlignmentOrder) res[1];
914 // SequenceI[] alignment = input.getUpdated
921 Object[] newview = input.getUpdatedView(results, orders, getGapChar());
922 // trash references to original result data
923 for (int j = 0; j < jobs.length; j++)
928 SequenceI[] alignment = (SequenceI[]) newview[0];
929 ColumnSelection columnselection = (ColumnSelection) newview[1];
930 Alignment al = new Alignment(alignment);
931 // TODO: add 'provenance' property to alignment from the method notes
932 if (lastProgram != null)
934 al.setProperty("Alignment Program", lastProgram);
936 // accompanying each subjob
939 al.setDataset(dataset);
942 propagateDatasetMappings(al);
943 // JBNote- TODO: warn user if a block is input rather than aligned data ?
947 AlignFrame af = new AlignFrame(al, columnselection,
948 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
950 // initialise with same renderer settings as in parent alignframe.
951 af.getFeatureRenderer().transferSettings(this.featureSettings);
953 if (alorders.size() > 0)
955 if (alorders.size() == 1)
957 af.addSortByOrderMenuItem(WebServiceName + " Ordering",
958 (AlignmentOrder) alorders.get(0));
962 // construct a non-redundant ordering set
963 Vector names = new Vector();
964 for (int i = 0, l = alorders.size(); i < l; i++)
966 String orderName = new String(" Region " + i);
971 if (((AlignmentOrder) alorders.get(i))
972 .equals(((AlignmentOrder) alorders.get(j))))
976 orderName += "," + j;
984 if (i == 0 && j == 1)
986 names.add(new String(""));
990 names.add(orderName);
993 for (int i = 0, l = alorders.size(); i < l; i++)
995 af.addSortByOrderMenuItem(
996 WebServiceName + ((String) names.get(i)) + " Ordering",
997 (AlignmentOrder) alorders.get(i));
1002 Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
1003 AlignFrame.DEFAULT_HEIGHT);
1008 System.out.println("MERGE WITH OLD FRAME");
1009 // TODO: modify alignment in original frame, replacing old for new
1010 // alignment using the commands.EditCommand model to ensure the update can
1015 public boolean canMergeResults()