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 = (String[]) 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 = (String[]) 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 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 ((MsaWSJob) jobs[j]).preset = preset;
501 ((MsaWSJob) jobs[j]).arguments = paramset;
502 ((MsaWSJob) jobs[j]).alignmentProgram = wsname;
505 wsinfo.setProgressName("region " + jobs[j].getJobnum(),
506 jobs[j].getJobnum());
508 wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
513 public boolean isCancellable()
518 public void cancelJob()
520 if (!jobComplete && jobs != null)
522 boolean cancelled = true;
523 for (int job = 0; job < jobs.length; job++)
525 if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
527 String cancelledMessage = "";
530 boolean cancelledJob = server.cancelJob(jobs[job].getJobId());
531 if (true) // cancelledJob || true)
534 // if the Jaba server indicates the job can't be cancelled, its
535 // because its running on the server's local execution engine
536 // so we just close the window anyway.
537 cancelledMessage = "Job cancelled.";
538 ((MsaWSJob) jobs[job]).cancel(); // TODO: refactor to avoid this
540 wsInfo.setStatus(jobs[job].getJobnum(),
541 WebserviceInfo.STATE_CANCELLED_OK);
545 // VALID UNSTOPPABLE JOB
546 cancelledMessage += "Server cannot cancel this job. just close the window.\n";
548 // wsInfo.setStatus(jobs[job].jobnum,
549 // WebserviceInfo.STATE_RUNNING);
551 } catch (Exception exc)
553 cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
556 "Exception whilst cancelling " + jobs[job].getJobId(),
559 wsInfo.setProgressText(jobs[job].getJobnum(), OutputHeader
560 + cancelledMessage + "\n");
564 // if we hadn't submitted then just mark the job as cancelled.
565 jobs[job].setSubjobComplete(true);
566 wsInfo.setStatus(jobs[job].getJobnum(),
567 WebserviceInfo.STATE_CANCELLED_OK);
573 wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
576 this.interrupt(); // kick thread to update job states.
582 wsInfo.setProgressText(OutputHeader
583 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
588 public void pollJob(AWsJob job) throws Exception
590 // TODO: investigate if we still need to cast here in J1.6
591 MsaWSJob j = ((MsaWSJob) job);
592 // this is standard code, but since the interface doesn't comprise of a
593 // basic one that implements (getJobStatus, pullExecStatistics) we have to
594 // repeat the code for all jw2s services.
595 j.setjobStatus(server.getJobStatus(job.getJobId()));
596 updateJobProgress(j);
602 * @return true if more job progress data was available
605 protected boolean updateJobProgress(MsaWSJob j) throws Exception
607 StringBuffer response = j.jobProgress;
608 long lastchunk = j.getLastChunk();
609 boolean changed = false;
612 j.setLastChunk(lastchunk);
613 ChunkHolder chunk = server
614 .pullExecStatistics(j.getJobId(), lastchunk);
617 changed |= chunk.getChunk().length() > 0;
618 response.append(chunk.getChunk());
619 lastchunk = chunk.getNextPosition();
623 } catch (InterruptedException x)
629 } while (lastchunk >= 0 && j.getLastChunk() != lastchunk);
633 public void StartJob(AWsJob job)
635 Exception lex = null;
636 // boiler plate template
637 if (!(job instanceof MsaWSJob))
639 throw new Error(MessageManager.formatMessage("error.implementation_error_msawbjob_called", new String[]{job.getClass().toString()}));
641 MsaWSJob j = (MsaWSJob) job;
644 if (Cache.log.isDebugEnabled())
646 Cache.log.debug("Tried to submit an already submitted job "
653 if (j.seqs == null || j.seqs.size() == 0)
655 // special case - selection consisted entirely of empty sequences...
656 j.setjobStatus(JobStatus.FINISHED);
657 j.setStatus(MessageManager.getString("label.empty_alignment_job"));
661 j.addInitialStatus(); // list the presets/parameters used for the job in
665 j.setJobId(server.presetAlign(j.seqs, j.getServerPreset()));
667 else if (j.hasArguments())
669 j.setJobId(server.customAlign(j.seqs, j.getJabaArguments()));
673 j.setJobId(server.align(j.seqs));
676 if (j.getJobId() != null)
678 j.setSubmitted(true);
679 j.setSubjobComplete(false);
680 // System.out.println(WsURL + " Job Id '" + jobId + "'");
685 throw new Exception(MessageManager.formatMessage("exception.web_service_returned_null_try_later", new String[]{WsUrl}));
687 } catch (compbio.metadata.UnsupportedRuntimeException _lex)
690 wsInfo.appendProgressText(MessageManager.formatMessage("info.job_couldnt_be_run_server_doesnt_support_program", new String[]{_lex.getMessage()}));
691 wsInfo.warnUser(_lex.getMessage(), MessageManager.getString("warn.service_not_supported"));
692 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
693 wsInfo.setStatus(j.getJobnum(),
694 WebserviceInfo.STATE_STOPPED_SERVERERROR);
695 } catch (compbio.metadata.LimitExceededException _lex)
698 wsInfo.appendProgressText(MessageManager.formatMessage("info.job_couldnt_be_run_exceeded_hard_limit", new String[]{_lex.getMessage()}));
699 wsInfo.warnUser(_lex.getMessage(), MessageManager.getString("warn.input_is_too_big"));
700 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
701 wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_ERROR);
702 } catch (compbio.metadata.WrongParameterException _lex)
705 wsInfo.warnUser(_lex.getMessage(), MessageManager.getString("warn.invalid_job_param_set"));
706 wsInfo.appendProgressText(MessageManager.formatMessage("info.job_couldnt_be_run_incorrect_param_setting", new String[]{_lex.getMessage()}));
707 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
708 wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_ERROR);
711 // For unexpected errors
713 .println(WebServiceName
714 + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
715 + "When contacting Server:" + WsUrl + "\n");
716 e.printStackTrace(System.err);
717 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
718 wsInfo.setStatus(j.getJobnum(),
719 WebserviceInfo.STATE_STOPPED_SERVERERROR);
720 } catch (Exception e)
722 // For unexpected errors
724 .println(WebServiceName
725 + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
726 + "When contacting Server:" + WsUrl + "\n");
727 e.printStackTrace(System.err);
728 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
729 wsInfo.setStatus(j.getJobnum(),
730 WebserviceInfo.STATE_STOPPED_SERVERERROR);
733 if (!j.isSubmitted())
735 // Boilerplate code here
736 // TODO: JBPNote catch timeout or other fault types explicitly
738 j.setAllowedServerExceptions(0);
739 wsInfo.appendProgressText(j.getJobnum(),
740 MessageManager.getString("info.failed_to_submit_sequences_for_alignment"));
745 public void parseResult()
747 long progbar = System.currentTimeMillis();
748 wsInfo.setProgressBar(MessageManager.getString("status.collecting_job_results"), progbar);
749 int results = 0; // number of result sets received
750 JobStateSummary finalState = new JobStateSummary();
753 for (int j = 0; j < jobs.length; j++)
755 MsaWSJob msjob = ((MsaWSJob) jobs[j]);
756 if (jobs[j].isFinished() && msjob.alignment == null)
758 int nunchanged = 3, nexcept = 3;
759 boolean jpchanged = false, jpex = false;
764 jpchanged = updateJobProgress(msjob);
770 } catch (Exception e)
774 .warn("Exception when retrieving remaining Job progress data for job "
775 + msjob.getJobId() + " on server " + WsUrl);
779 // set flag remember that we've had an exception.
787 Thread.sleep(jpex ? 2400 : 1200); // wait a bit longer if we
788 // experienced an exception.
789 } catch (Exception ex)
795 } while (nunchanged > 0 && nexcept > 0);
797 if (Cache.log.isDebugEnabled())
799 System.out.println("Job Execution file for job: "
800 + msjob.getJobId() + " on server " + WsUrl);
801 System.out.println(msjob.getStatus());
802 System.out.println("*** End of status");
807 msjob.alignment = server.getResult(msjob.getJobId());
808 } catch (compbio.metadata.ResultNotAvailableException e)
810 // job has failed for some reason - probably due to invalid
813 .debug("Results not available for finished job - marking as broken job.",
816 .append("\nResult not available. Probably due to invalid input or parameter settings. Server error message below:\n\n"
817 + e.getLocalizedMessage());
818 msjob.setjobStatus(JobStatus.FAILED);
819 } catch (Exception e)
821 Cache.log.error("Couldn't get Alignment for job.", e);
822 // TODO: Increment count and retry ?
823 msjob.setjobStatus(JobStatus.UNDEFINED);
826 finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
827 if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
828 && jobs[j].hasResults())
831 compbio.data.sequence.Alignment alignment = ((MsaWSJob) jobs[j]).alignment;
832 if (alignment != null)
834 // server.close(jobs[j].getJobnum());
835 // wsInfo.appendProgressText(jobs[j].getJobnum(),
836 // "\nAlignment Object Method Notes\n");
837 // wsInfo.appendProgressText(jobs[j].getJobnum(),
838 // "Calculated with "+alignment.getMetadata().getProgram().toString());
839 // JBPNote The returned files from a webservice could be
840 // hidden behind icons in the monitor window that,
841 // when clicked, pop up their corresponding data
845 } catch (Exception ex)
848 Cache.log.error("Unexpected exception when processing results for "
850 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
854 wsInfo.showResultsNewFrame
855 .addActionListener(new java.awt.event.ActionListener()
857 public void actionPerformed(java.awt.event.ActionEvent evt)
859 displayResults(true);
863 .addActionListener(new java.awt.event.ActionListener()
865 public void actionPerformed(java.awt.event.ActionEvent evt)
867 displayResults(false);
870 wsInfo.setResultsReady();
874 wsInfo.setFinishedNoResults();
876 updateGlobalStatus(finalState);
877 wsInfo.setProgressBar(null, progbar);
880 void displayResults(boolean newFrame)
882 // view input or result data for each block
883 Vector alorders = new Vector();
884 SequenceI[][] results = new SequenceI[jobs.length][];
885 AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
886 String lastProgram = null;
888 for (int j = 0; j < jobs.length; j++)
890 if (jobs[j].hasResults())
892 msjob = (MsaWSJob) jobs[j];
893 Object[] res = msjob.getAlignment();
894 lastProgram = msjob.getAlignmentProgram();
895 alorders.add(res[1]);
896 results[j] = (SequenceI[]) res[0];
897 orders[j] = (AlignmentOrder) res[1];
899 // SequenceI[] alignment = input.getUpdated
906 Object[] newview = input.getUpdatedView(results, orders, getGapChar());
907 // trash references to original result data
908 for (int j = 0; j < jobs.length; j++)
913 SequenceI[] alignment = (SequenceI[]) newview[0];
914 ColumnSelection columnselection = (ColumnSelection) newview[1];
915 Alignment al = new Alignment(alignment);
916 // TODO: add 'provenance' property to alignment from the method notes
917 if (lastProgram != null)
919 al.setProperty("Alignment Program", lastProgram);
921 // accompanying each subjob
924 al.setDataset(dataset);
927 propagateDatasetMappings(al);
928 // JBNote- TODO: warn user if a block is input rather than aligned data ?
932 AlignFrame af = new AlignFrame(al, columnselection,
933 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
935 // initialise with same renderer settings as in parent alignframe.
936 af.getFeatureRenderer().transferSettings(this.featureSettings);
938 if (alorders.size() > 0)
940 if (alorders.size() == 1)
942 af.addSortByOrderMenuItem(WebServiceName + " Ordering",
943 (AlignmentOrder) alorders.get(0));
947 // construct a non-redundant ordering set
948 Vector names = new Vector();
949 for (int i = 0, l = alorders.size(); i < l; i++)
951 String orderName = new String(" Region " + i);
956 if (((AlignmentOrder) alorders.get(i))
957 .equals(((AlignmentOrder) alorders.get(j))))
961 orderName += "," + j;
969 if (i == 0 && j == 1)
971 names.add(new String(""));
975 names.add(orderName);
978 for (int i = 0, l = alorders.size(); i < l; i++)
980 af.addSortByOrderMenuItem(
981 WebServiceName + ((String) names.get(i)) + " Ordering",
982 (AlignmentOrder) alorders.get(i));
987 Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
988 AlignFrame.DEFAULT_HEIGHT);
993 System.out.println("MERGE WITH OLD FRAME");
994 // TODO: modify alignment in original frame, replacing old for new
995 // alignment using the commands.EditCommand model to ensure the update can
1000 public boolean canMergeResults()