/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.ws.jws2; import jalview.analysis.AlignSeq; import jalview.analysis.AlignmentAnnotationUtils; import jalview.analysis.SeqsetUtils; import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; import jalview.api.FeatureColourI; import jalview.bin.Cache; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.AnnotatedCollectionI; import jalview.datamodel.Annotation; import jalview.datamodel.ContiguousI; import jalview.datamodel.Mapping; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.AlignFrame; import jalview.gui.Desktop; import jalview.gui.IProgressIndicator; import jalview.gui.IProgressIndicatorHandler; import jalview.gui.JvOptionPane; import jalview.gui.WebserviceInfo; import jalview.schemes.FeatureSettingsAdapter; import jalview.schemes.ResidueProperties; import jalview.util.MapList; import jalview.util.MessageManager; import jalview.workers.AlignCalcWorker; import jalview.ws.JobStateSummary; import jalview.ws.api.CancellableI; import jalview.ws.api.JalviewServiceEndpointProviderI; import jalview.ws.api.JobId; import jalview.ws.api.SequenceAnnotationServiceI; import jalview.ws.api.ServiceWithParameters; import jalview.ws.api.WSAnnotationCalcManagerI; import jalview.ws.gui.AnnotationWsJob; import jalview.ws.jws2.dm.AAConSettings; import jalview.ws.params.ArgumentI; import jalview.ws.params.WsParamSetI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class SeqAnnotationServiceCalcWorker extends AlignCalcWorker implements WSAnnotationCalcManagerI { protected ServiceWithParameters service; protected WsParamSetI preset; protected List arguments; protected IProgressIndicator guiProgress; protected boolean submitGaps = true; /** * by default, we filter out non-standard residues before submission */ protected boolean filterNonStandardResidues = true; /** * Recover any existing parameters for this service */ protected void initViewportParams() { if (getCalcId() != null) { ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor( getCalcId(), new AAConSettings(true, service, this.preset, arguments), true); } } /** * * @return null or a string used to recover all annotation generated by this * worker */ public String getCalcId() { return service.getAlignAnalysisUI() == null ? null : service.getAlignAnalysisUI().getCalcId(); } public WsParamSetI getPreset() { return preset; } public List getArguments() { return arguments; } /** * reconfigure and restart the AAConClient. This method will spawn a new * thread that will wait until any current jobs are finished, modify the * parameters and restart the conservation calculation with the new values. * * @param newpreset * @param newarguments */ public void updateParameters(final WsParamSetI newpreset, final List newarguments) { preset = newpreset; arguments = newarguments; calcMan.startWorker(this); initViewportParams(); } protected boolean alignedSeqs = true; protected boolean nucleotidesAllowed = false; protected boolean proteinAllowed = false; /** * record sequences for mapping result back to afterwards */ protected boolean bySequence = false; protected Map seqNames; // TODO: convert to bitset protected boolean[] gapMap; int realw; protected int start; int end; private AlignFrame alignFrame; public boolean[] getGapMap() { return gapMap; } public SeqAnnotationServiceCalcWorker(AlignViewportI alignViewport, AlignmentViewPanel alignPanel) { super(alignViewport, alignPanel); } public SeqAnnotationServiceCalcWorker(ServiceWithParameters service, AlignFrame alignFrame, WsParamSetI preset, List paramset) { this(alignFrame.getCurrentView(), alignFrame.alignPanel); // TODO: both these fields needed ? this.alignFrame = alignFrame; this.guiProgress = alignFrame; this.preset = preset; this.arguments = paramset; this.service = service; try { annotService = (jalview.ws.api.SequenceAnnotationServiceI) ((JalviewServiceEndpointProviderI) service) .getEndpoint(); } catch (ClassCastException cce) { JvOptionPane.showMessageDialog(Desktop.desktop, MessageManager.formatMessage( "label.service_called_is_not_an_annotation_service", new String[] { service.getName() }), MessageManager.getString("label.internal_jalview_error"), JvOptionPane.WARNING_MESSAGE); } // configure submission flags proteinAllowed = service.isProteinService(); nucleotidesAllowed = service.isNucleotideService(); alignedSeqs = service.isNeedsAlignedSequences(); bySequence = !service.isAlignmentAnalysis(); filterNonStandardResidues = service.isFilterSymbols(); min_valid_seqs = service.getMinimumInputSequences(); submitGaps = service.isAlignmentAnalysis(); if (service.isInteractiveUpdate()) { initViewportParams(); } } /** * * @return true if the submission thread should attempt to submit data */ public boolean hasService() { return annotService != null; } protected jalview.ws.api.SequenceAnnotationServiceI annotService = null; volatile JobId rslt = null; AnnotationWsJob running = null; private int min_valid_seqs; @Override public void run() { if (!hasService()) { return; } long progressId = -1; int serverErrorsLeft = 3; final boolean cancellable = CancellableI.class .isAssignableFrom(annotService.getClass()); StringBuffer msg = new StringBuffer(); JobStateSummary job = new JobStateSummary(); WebserviceInfo info = new WebserviceInfo("foo", "bar", false); try { if (checkDone()) { return; } List seqs = getInputSequences( alignViewport.getAlignment(), bySequence ? alignViewport.getSelectionGroup() : null); if (seqs == null || !checkValidInputSeqs(seqs)) { jalview.bin.Cache.log.debug( "Sequences for analysis service were null or not valid"); calcMan.workerComplete(this); return; } if (guiProgress != null) { guiProgress.setProgressBar(service.getActionText(), progressId = System.currentTimeMillis()); } jalview.bin.Cache.log.debug("submitted " + seqs.size() + " sequences to " + service.getActionText()); rslt = annotService.submitToService(seqs, getPreset(), getArguments()); if (rslt == null) { return; } // TODO: handle job submission error reporting here. // /// // otherwise, construct WsJob and any UI handlers running = new AnnotationWsJob(); running.setJobHandle(rslt); running.setSeqNames(seqNames); running.setStartPos(start); running.setSeqs(seqs); job.updateJobPanelState(info, "", running); if (guiProgress != null) { guiProgress.registerHandler(progressId, new IProgressIndicatorHandler() { @Override public boolean cancelActivity(long id) { ((CancellableI) annotService).cancel(running); return true; } @Override public boolean canCancel() { return cancellable; } }); } // /// // and poll for updates until job finishes, fails or becomes stale boolean finished = false; do { Cache.log.debug("Updating status for annotation service."); annotService.updateStatus(running); job.updateJobPanelState(info, "", running); if (running.isSubjobComplete()) { Cache.log.debug( "Finished polling analysis service job: status reported is " + running.getState()); finished = true; } else { Cache.log.debug("Status now " + running.getState()); } if (calcMan.isPending(this) && isInteractiveUpdate()) { Cache.log.debug("Analysis service job is stale. aborting."); // job has become stale. if (!finished) { finished = true; // cancel this job and yield to the new job try { if (cancellable && ((CancellableI) annotService).cancel(running)) { System.err.println("Cancelled job: " + rslt); } else { System.err.println("FAILED TO CANCEL job: " + rslt); } } catch (Exception x) { } } rslt = running.getJobHandle(); return; } // pull any stats - some services need to flush log output before // results are available Cache.log.debug("Updating progress log for annotation service."); try { annotService.updateJobProgress(running); } catch (Throwable thr) { Cache.log.debug("Ignoring exception during progress update.", thr); } Cache.log.trace("Result of poll: " + running.getStatus()); if (!finished && !running.isFailed()) { try { Cache.log.debug("Analysis service job thread sleeping."); Thread.sleep(200); Cache.log.debug("Analysis service job thread woke."); } catch (InterruptedException x) { } ; } } while (!finished); Cache.log.debug("Job poll loop exited. Job is " + running.getState()); // TODO: need to poll/retry if (serverErrorsLeft > 0) { try { Thread.sleep(200); } catch (InterruptedException x) { } } if (running.isFinished()) { // expect there to be results to collect // configure job with the associated view's feature renderer, if one // exists. // TODO: here one would also grab the 'master feature renderer' in order // to enable/disable // features automatically according to user preferences running.setFeatureRenderer( ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer()); Cache.log.debug("retrieving job results."); final Map featureColours = new HashMap<>(); final Map featureFilters = new HashMap<>(); List returnedAnnot = annotService .getAnnotationResult(running.getJobHandle(), seqs, featureColours, featureFilters); Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows" : ("" + returnedAnnot.size()))); Cache.log.debug("There were " + featureColours.size() + " feature colours and " + featureFilters.size() + " filters defined."); // TODO // copy over each annotation row reurned and also defined on each // sequence, excluding regions not annotated due to gapMap/column // visibility running.setAnnotation(returnedAnnot); if (running.hasResults()) { jalview.bin.Cache.log.debug("Updating result annotation from Job " + rslt + " at " + service.getUri()); updateResultAnnotation(true); if (running.isTransferSequenceFeatures()) { // TODO // look at each sequence and lift over any features, excluding // regions // not annotated due to gapMap/column visibility jalview.bin.Cache.log.debug( "Updating feature display settings and transferring features from Job " + rslt + " at " + service.getUri()); // TODO: consider merge rather than apply here alignViewport.applyFeaturesStyle(new FeatureSettingsAdapter() { @Override public FeatureColourI getFeatureColour(String type) { return featureColours.get(type); } @Override public FeatureMatcherSetI getFeatureFilters(String type) { return featureFilters.get(type); } @Override public boolean isFeatureDisplayed(String type) { return featureColours.containsKey(type); } }); // TODO: JAL-1150 - create sequence feature settings API for // defining // styles and enabling/disabling feature overlay on alignment panel if (alignFrame.alignPanel == ap) { alignViewport.setShowSequenceFeatures(true); alignFrame.setMenusForViewport(); } } ap.adjustAnnotationHeight(); } } Cache.log.debug("Annotation Service Worker thread finished."); } // TODO: use service specitic exception handlers // catch (JobSubmissionException x) // { // // System.err.println( // "submission error with " + getServiceActionText() + " :"); // x.printStackTrace(); // calcMan.disableWorker(this); // } catch (ResultNotAvailableException x) // { // System.err.println("collection error:\nJob ID: " + rslt); // x.printStackTrace(); // calcMan.disableWorker(this); // // } catch (OutOfMemoryError error) // { // calcMan.disableWorker(this); // // ap.raiseOOMWarning(getServiceActionText(), error); // } catch (Throwable x) { calcMan.disableWorker(this); System.err .println("Blacklisting worker due to unexpected exception:"); x.printStackTrace(); } finally { calcMan.workerComplete(this); if (ap != null) { calcMan.workerComplete(this); if (guiProgress != null && progressId != -1) { guiProgress.setProgressBar("", progressId); } // TODO: may not need to paintAlignment again ! ap.paintAlignment(false, false); } if (msg.length() > 0) { // TODO: stash message somewhere in annotation or alignment view. // code below shows result in a text box popup /* * jalview.gui.CutAndPasteTransfer cap = new * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString()); * jalview.gui.Desktop.addInternalFrame(cap, * "Job Status for "+getServiceActionText(), 600, 400); */ } } } /** * validate input for dynamic/non-dynamic update context TODO: move to * analysis interface ? * @param seqs * * @return true if input is valid */ boolean checkValidInputSeqs(List seqs) { int nvalid = 0; for (SequenceI sq : seqs) { if (sq.getStart() <= sq.getEnd() && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed)) { if (submitGaps || sq.getLength() == (sq.getEnd() - sq.getStart() + 1)) { nvalid++; } } } return nvalid >= min_valid_seqs; } public void cancelCurrentJob() { try { String id = running.getJobId(); if (((CancellableI) annotService).cancel(running)) { System.err.println("Cancelled job " + id); } else { System.err.println("Job " + id + " couldn't be cancelled."); } } catch (Exception q) { q.printStackTrace(); } } /** * Interactive updating. Analysis calculations that work on the currently * displayed alignment data should cancel existing jobs when the input data * has changed. * * @return true if a running job should be cancelled because new input data is * available for analysis */ boolean isInteractiveUpdate() { return service.isInteractiveUpdate(); } /** * decide what sequences will be analysed TODO: refactor to generate * List for submission to service interface * * @param alignment * @param inputSeqs * @return */ public List getInputSequences(AlignmentI alignment, AnnotatedCollectionI inputSeqs) { if (alignment == null || alignment.getWidth() <= 0 || alignment.getSequences() == null || alignment.isNucleotide() ? !nucleotidesAllowed : !proteinAllowed) { return null; } if (inputSeqs == null || inputSeqs.getWidth() <= 0 || inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1) { inputSeqs = alignment; } List seqs = new ArrayList<>(); int minlen = 10; int ln = -1; if (bySequence) { seqNames = new HashMap<>(); } gapMap = new boolean[0]; start = inputSeqs.getStartRes(); end = inputSeqs.getEndRes(); // TODO: URGENT! unify with JPred / MSA code to handle hidden regions // correctly // TODO: push attributes into WsJob instance (so they can be safely // persisted/restored for (SequenceI sq : (inputSeqs.getSequences())) { if (bySequence ? sq.findPosition(end + 1) - sq.findPosition(start + 1) > minlen - 1 : sq.getEnd() - sq.getStart() > minlen - 1) { String newname = SeqsetUtils.unique_name(seqs.size() + 1); // make new input sequence with or without gaps if (seqNames != null) { seqNames.put(newname, sq); } SequenceI seq; if (submitGaps) { seqs.add(seq = new jalview.datamodel.Sequence(newname, sq.getSequenceAsString())); if (gapMap == null || gapMap.length < seq.getLength()) { boolean[] tg = gapMap; gapMap = new boolean[seq.getLength()]; System.arraycopy(tg, 0, gapMap, 0, tg.length); for (int p = tg.length; p < gapMap.length; p++) { gapMap[p] = false; // init as a gap } } for (int apos : sq.gapMap()) { char sqc = sq.getCharAt(apos); if (!filterNonStandardResidues || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20 : ResidueProperties.nucleotideIndex[sqc] < 5)) { gapMap[apos] = true; // aligned and real amino acid residue } ; } } else { // TODO: add ability to exclude hidden regions seqs.add(seq = new jalview.datamodel.Sequence(newname, AlignSeq.extractGaps(jalview.util.Comparison.GapChars, sq.getSequenceAsString(start, end + 1)))); // for annotation need to also record map to sequence start/end // position in range // then transfer back to original sequence on return. } if (seq.getLength() > ln) { ln = seq.getLength(); } } } if (alignedSeqs && submitGaps) { realw = 0; for (int i = 0; i < gapMap.length; i++) { if (gapMap[i]) { realw++; } } // try real hard to return something submittable // TODO: some of AAcon measures need a minimum of two or three amino // acids at each position, and AAcon doesn't gracefully degrade. for (int p = 0; p < seqs.size(); p++) { SequenceI sq = seqs.get(p); // strip gapped columns char[] padded = new char[realw], orig = sq.getSequence(); for (int i = 0, pp = 0; i < realw; pp++) { if (gapMap[pp]) { if (orig.length > pp) { padded[i++] = orig[pp]; } else { padded[i++] = '-'; } } } seqs.set(p, new jalview.datamodel.Sequence(sq.getName(), new String(padded))); } } return seqs; } @Override public void updateAnnotation() { updateResultAnnotation(false); } public void updateResultAnnotation(boolean immediate) { if ((immediate || !calcMan.isWorking(this)) && running != null && running.hasResults()) { List ourAnnot = running.getAnnotation(), newAnnots = new ArrayList<>(); // // update graphGroup for all annotation // /** * find a graphGroup greater than any existing ones this could be a method * provided by alignment Alignment.getNewGraphGroup() - returns next * unused graph group */ int graphGroup = 1; if (alignViewport.getAlignment().getAlignmentAnnotation() != null) { for (AlignmentAnnotation ala : alignViewport.getAlignment() .getAlignmentAnnotation()) { if (ala.graphGroup > graphGroup) { graphGroup = ala.graphGroup; } } } /** * update graphGroup in the annotation rows returned from service */ // TODO: look at sequence annotation rows and update graph groups in the // case of reference annotation. for (AlignmentAnnotation ala : ourAnnot) { if (ala.graphGroup > 0) { ala.graphGroup += graphGroup; } SequenceI aseq = null; /** * transfer sequence refs and adjust gapmap */ if (ala.sequenceRef != null) { SequenceI seq = running.getSeqNames() .get(ala.sequenceRef.getName()); aseq = seq; while (seq.getDatasetSequence() != null) { seq = seq.getDatasetSequence(); } } Annotation[] resAnnot = ala.annotations, gappedAnnot = new Annotation[Math.max( alignViewport.getAlignment().getWidth(), gapMap.length)]; for (int p = 0, ap = start; ap < gappedAnnot.length; ap++) { if (gapMap != null && gapMap.length > ap && !gapMap[ap]) { gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN); } else if (p < resAnnot.length) { gappedAnnot[ap] = resAnnot[p++]; } } ala.sequenceRef = aseq; ala.annotations = gappedAnnot; AlignmentAnnotation newAnnot = getAlignViewport().getAlignment() .updateFromOrCopyAnnotation(ala); if (aseq != null) { aseq.addAlignmentAnnotation(newAnnot); newAnnot.adjustForAlignment(); AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith( newAnnot, newAnnot.label, newAnnot.getCalcId()); } newAnnots.add(newAnnot); } for (SequenceI sq : running.getSeqs()) { if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().length == 0)) { continue; } running.setTransferSequenceFeatures(true); SequenceI seq = running.getSeqNames().get(sq.getName()); SequenceI dseq; ContiguousI seqRange = seq.findPositions(start, end); while ((dseq = seq).getDatasetSequence() != null) { seq = seq.getDatasetSequence(); } List sourceRange = new ArrayList(); if (gapMap != null && gapMap.length >= end) { int lastcol = start, col = start; do { if (col == end || !gapMap[col]) { if (lastcol <= (col - 1)) { seqRange = seq.findPositions(lastcol, col); sourceRange.add(seqRange); } lastcol = col + 1; } } while (++col <= end); } else { sourceRange.add(seq.findPositions(start, end)); } int i = 0; int source_startend[] = new int[sourceRange.size() * 2]; for (ContiguousI range : sourceRange) { source_startend[i++] = range.getBegin(); source_startend[i++] = range.getEnd(); } Mapping mp = new Mapping( new MapList(source_startend, new int[] { seq.getStart(), seq.getEnd() }, 1, 1)); dseq.transferAnnotation(sq, mp); } updateOurAnnots(newAnnots); } } /** * notify manager that we have started, and wait for a free calculation slot * * @return true if slot is obtained and work still valid, false if another * thread has done our work for us. */ protected boolean checkDone() { calcMan.notifyStart(this); ap.paintAlignment(false, false); while (!calcMan.notifyWorking(this)) { if (calcMan.isWorking(this)) { return true; } try { if (ap != null) { ap.paintAlignment(false, false); } Thread.sleep(200); } catch (Exception ex) { ex.printStackTrace(); } } if (alignViewport.isClosed()) { abortAndDestroy(); return true; } return false; } protected void updateOurAnnots(List ourAnnot) { List our = ourAnnots; ourAnnots = ourAnnot; AlignmentI alignment = alignViewport.getAlignment(); if (our != null) { if (our.size() > 0) { for (AlignmentAnnotation an : our) { if (!ourAnnots.contains(an)) { // remove the old annotation alignment.deleteAnnotation(an); } } } our.clear(); } // validate rows and update Alignmment state for (AlignmentAnnotation an : ourAnnots) { alignViewport.getAlignment().validateAnnotation(an); } // TODO: may need a menu refresh after this // af.setMenusForViewport(); ap.adjustAnnotationHeight(); } public SequenceAnnotationServiceI getService() { return annotService; } }