/*
* 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.api.PollableAlignCalcWorkerI;
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, PollableAlignCalcWorkerI
{
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(ServiceWithParameters service,
AlignFrame alignFrame,
WsParamSetI preset, List paramset)
{
super(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)
{
annotService = null;
JvOptionPane.showMessageDialog(Desktop.getInstance(),
MessageManager.formatMessage(
"label.service_called_is_not_an_annotation_service",
new String[]
{ service.getName() }),
MessageManager.getString("label.internal_jalview_error"),
JvOptionPane.WARNING_MESSAGE);
}
cancellable = CancellableI.class.isInstance(annotService);
// 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 SequenceAnnotationServiceI annotService;
protected final boolean cancellable;
volatile JobId rslt = null;
AnnotationWsJob running = null;
private int min_valid_seqs;
private long progressId = -1;
JobStateSummary job = null;
WebserviceInfo info = null;
List seqs = null;
@Override
public String getCalcName()
{
return service.getName();
}
@Override public void startUp() throws Throwable
{
if (alignViewport.isClosed())
{
abortAndDestroy();
return;
}
if (!hasService())
{
return;
}
StringBuffer msg = new StringBuffer();
job = new JobStateSummary();
info = new WebserviceInfo("foo", "bar", false);
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");
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.
Cache.log.debug("Service " + service.getUri() + "\nSubmitted job ID: "
+ rslt);
// ///
// 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)
{
calcMan.cancelWorker(SeqAnnotationServiceCalcWorker.this);
return true;
}
@Override
public boolean canCancel()
{
return cancellable;
}
});
}
}
@Override public boolean poll() throws Throwable
{
boolean finished = false;
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());
}
// 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.debug("Result of poll: " + running.getStatus());
if (finished)
{
Cache.log.debug("Job poll loop exited. Job is " + running.getState());
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
// update calcId if it is not already set on returned annotation
if (returnedAnnot != null)
{
for (AlignmentAnnotation aa : returnedAnnot)
{
// assume that any CalcIds already set
if (getCalcId() != null && aa.getCalcId() == null
|| "".equals(aa.getCalcId()))
{
aa.setCalcId(getCalcId());
}
// autocalculated annotation are created by interactive alignment
// analysis services
aa.autoCalculated = service.isAlignmentAnalysis()
&& service.isInteractiveUpdate();
}
}
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.");
}
return finished;
}
@Override public void cancel()
{
cancelCurrentJob();
}
@Override public void done()
{
if (ap != null)
{
if (guiProgress != null && progressId != -1)
{
guiProgress.removeProgressBar(progressId);
}
// TODO: may not need to paintAlignment again !
ap.paintAlignment(false, false);
}
}
/**
* 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 (cancellable && ((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().size() == 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);
}
}
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;
}
}